Skip to content

Commit b7beb12

Browse files
Add radio bridge support and button service (#10)
Co-authored-by: Matt Hillsdon <[email protected]>
1 parent ce957a4 commit b7beb12

15 files changed

+894
-572
lines changed

lib/accelerometer-service.ts

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
11
import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js";
2-
import { GattOperation } from "./bluetooth-device-wrapper.js";
2+
import { GattOperation, Service } from "./bluetooth-device-wrapper.js";
33
import { profile } from "./bluetooth-profile.js";
44
import { createGattOperationPromise } from "./bluetooth-utils.js";
55
import { BackgroundErrorEvent, DeviceError } from "./device.js";
66
import {
77
CharacteristicDataTarget,
8+
TypedServiceEvent,
89
TypedServiceEventDispatcher,
910
} from "./service-events.js";
1011

11-
export class AccelerometerService {
12+
export class AccelerometerService implements Service {
1213
constructor(
1314
private accelerometerDataCharacteristic: BluetoothRemoteGATTCharacteristic,
1415
private accelerometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic,
1516
private dispatchTypedEvent: TypedServiceEventDispatcher,
16-
private isNotifying: boolean,
1717
private queueGattOperation: (gattOperation: GattOperation) => void,
1818
) {
19-
this.addDataEventListener();
20-
if (this.isNotifying) {
21-
this.startNotifications();
22-
}
19+
this.accelerometerDataCharacteristic.addEventListener(
20+
"characteristicvaluechanged",
21+
(event: Event) => {
22+
const target = event.target as CharacteristicDataTarget;
23+
const data = this.dataViewToData(target.value);
24+
this.dispatchTypedEvent(
25+
"accelerometerdatachanged",
26+
new AccelerometerDataEvent(data),
27+
);
28+
},
29+
);
2330
}
2431

2532
static async createService(
2633
gattServer: BluetoothRemoteGATTServer,
2734
dispatcher: TypedServiceEventDispatcher,
28-
isNotifying: boolean,
2935
queueGattOperation: (gattOperation: GattOperation) => void,
3036
listenerInit: boolean,
3137
): Promise<AccelerometerService | undefined> {
@@ -57,7 +63,6 @@ export class AccelerometerService {
5763
accelerometerDataCharacteristic,
5864
accelerometerPeriodCharacteristic,
5965
dispatcher,
60-
isNotifying,
6166
queueGattOperation,
6267
);
6368
}
@@ -99,7 +104,7 @@ export class AccelerometerService {
99104
// Values passed are rounded up to the allowed values on device.
100105
// Documentation for allowed values looks wrong.
101106
// https://lancaster-university.github.io/microbit-docs/resources/bluetooth/bluetooth_profile.html
102-
const { callback } = createGattOperationPromise();
107+
const { callback, gattOperationPromise } = createGattOperationPromise();
103108
const dataView = new DataView(new ArrayBuffer(2));
104109
dataView.setUint16(0, value, true);
105110
this.queueGattOperation({
@@ -109,29 +114,25 @@ export class AccelerometerService {
109114
dataView,
110115
),
111116
});
117+
await gattOperationPromise;
112118
}
113119

114-
private addDataEventListener(): void {
115-
this.accelerometerDataCharacteristic.addEventListener(
116-
"characteristicvaluechanged",
117-
(event: Event) => {
118-
const target = event.target as CharacteristicDataTarget;
119-
const data = this.dataViewToData(target.value);
120-
this.dispatchTypedEvent(
121-
"accelerometerdatachanged",
122-
new AccelerometerDataEvent(data),
123-
);
124-
},
125-
);
120+
async startNotifications(type: TypedServiceEvent): Promise<void> {
121+
await this.characteristicForEvent(type)?.startNotifications();
126122
}
127123

128-
startNotifications(): void {
129-
this.accelerometerDataCharacteristic.startNotifications();
130-
this.isNotifying = true;
124+
async stopNotifications(type: TypedServiceEvent): Promise<void> {
125+
await this.characteristicForEvent(type)?.stopNotifications();
131126
}
132127

133-
stopNotifications(): void {
134-
this.isNotifying = false;
135-
this.accelerometerDataCharacteristic.stopNotifications();
128+
private characteristicForEvent(type: TypedServiceEvent) {
129+
switch (type) {
130+
case "accelerometerdatachanged": {
131+
return this.accelerometerDataCharacteristic;
132+
}
133+
default: {
134+
return undefined;
135+
}
136+
}
136137
}
137138
}

lib/bluetooth-device-wrapper.ts

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
import { AccelerometerService } from "./accelerometer-service.js";
88
import { profile } from "./bluetooth-profile.js";
9+
import { ButtonService } from "./button-service.js";
910
import { BoardVersion, DeviceError } from "./device.js";
1011
import { Logging, NullLogging } from "./logging.js";
1112
import {
1213
ServiceConnectionEventMap,
14+
TypedServiceEvent,
1315
TypedServiceEventDispatcher,
1416
} from "./service-events.js";
1517

@@ -48,6 +50,50 @@ function findPlatform(): string | undefined {
4850
const platform = findPlatform();
4951
const isWindowsOS = platform && /^Win/.test(platform);
5052

53+
export interface Service {
54+
startNotifications(type: TypedServiceEvent): Promise<void>;
55+
stopNotifications(type: TypedServiceEvent): Promise<void>;
56+
}
57+
58+
class ServiceInfo<T extends Service> {
59+
private service: T | undefined;
60+
61+
constructor(
62+
private serviceFactory: (
63+
gattServer: BluetoothRemoteGATTServer,
64+
dispatcher: TypedServiceEventDispatcher,
65+
queueGattOperation: (gattOperation: GattOperation) => void,
66+
listenerInit: boolean,
67+
) => Promise<T | undefined>,
68+
public events: TypedServiceEvent[],
69+
) {}
70+
71+
get(): T | undefined {
72+
return this.service;
73+
}
74+
75+
async createIfNeeded(
76+
gattServer: BluetoothRemoteGATTServer,
77+
dispatcher: TypedServiceEventDispatcher,
78+
queueGattOperation: (gattOperation: GattOperation) => void,
79+
listenerInit: boolean,
80+
): Promise<T | undefined> {
81+
this.service =
82+
this.service ??
83+
(await this.serviceFactory(
84+
gattServer,
85+
dispatcher,
86+
queueGattOperation,
87+
listenerInit,
88+
));
89+
return this.service;
90+
}
91+
92+
dispose() {
93+
this.service = undefined;
94+
}
95+
}
96+
5197
export class BluetoothDeviceWrapper {
5298
// Used to avoid automatic reconnection during user triggered connect/disconnect
5399
// or reconnection itself.
@@ -67,15 +113,17 @@ export class BluetoothDeviceWrapper {
67113
private connecting = false;
68114
private isReconnect = false;
69115
private reconnectReadyPromise: Promise<void> | undefined;
70-
private accelerometerService: AccelerometerService | undefined;
116+
117+
private accelerometer = new ServiceInfo(AccelerometerService.createService, [
118+
"accelerometerdatachanged",
119+
]);
120+
private buttons = new ServiceInfo(ButtonService.createService, [
121+
"buttonachanged",
122+
"buttonbchanged",
123+
]);
124+
private serviceInfo = [this.accelerometer, this.buttons];
71125

72126
boardVersion: BoardVersion | undefined;
73-
serviceListenerState = {
74-
accelerometerdatachanged: {
75-
notifying: false,
76-
service: this.getAccelerometerService,
77-
},
78-
};
79127

80128
private gattOperations: GattOperations = {
81129
busy: false,
@@ -86,20 +134,13 @@ export class BluetoothDeviceWrapper {
86134
public readonly device: BluetoothDevice,
87135
private logging: Logging = new NullLogging(),
88136
private dispatchTypedEvent: TypedServiceEventDispatcher,
89-
private addedServiceListeners: Record<
90-
keyof ServiceConnectionEventMap,
91-
boolean
92-
>,
137+
// We recreate this for the same connection and need to re-setup notifications for the old events
138+
private events: Record<keyof ServiceConnectionEventMap, boolean>,
93139
) {
94140
device.addEventListener(
95141
"gattserverdisconnected",
96142
this.handleDisconnectEvent,
97143
);
98-
for (const [key, value] of Object.entries(this.addedServiceListeners)) {
99-
this.serviceListenerState[
100-
key as keyof ServiceConnectionEventMap
101-
].notifying = value;
102-
}
103144
}
104145

105146
async connect(): Promise<void> {
@@ -184,13 +225,9 @@ export class BluetoothDeviceWrapper {
184225
this.connecting = false;
185226
}
186227

187-
// Restart notifications for services and characteristics
188-
// the user has listened to.
189-
for (const serviceListener of Object.values(this.serviceListenerState)) {
190-
if (serviceListener.notifying) {
191-
serviceListener.service.call(this, { listenerInit: true });
192-
}
193-
}
228+
Object.keys(this.events).forEach((e) =>
229+
this.startNotifications(e as TypedServiceEvent),
230+
);
194231

195232
this.logging.event({
196233
type: this.isReconnect ? "Reconnect" : "Connect",
@@ -354,26 +391,39 @@ export class BluetoothDeviceWrapper {
354391
this.gattOperations = { busy: false, queue: [] };
355392
}
356393

357-
async getAccelerometerService(
358-
options: {
359-
listenerInit: boolean;
360-
} = { listenerInit: false },
361-
): Promise<AccelerometerService | undefined> {
362-
if (!this.accelerometerService) {
363-
const gattServer = this.assertGattServer();
364-
this.accelerometerService = await AccelerometerService.createService(
365-
gattServer,
366-
this.dispatchTypedEvent,
367-
this.serviceListenerState.accelerometerdatachanged.notifying,
368-
this.queueGattOperation.bind(this),
369-
options?.listenerInit,
370-
);
394+
private createIfNeeded<T extends Service>(
395+
info: ServiceInfo<T>,
396+
listenerInit: boolean,
397+
): Promise<T | undefined> {
398+
const gattServer = this.assertGattServer();
399+
return info.createIfNeeded(
400+
gattServer,
401+
this.dispatchTypedEvent,
402+
this.queueGattOperation.bind(this),
403+
listenerInit,
404+
);
405+
}
406+
407+
async getAccelerometerService(): Promise<AccelerometerService | undefined> {
408+
return this.createIfNeeded(this.accelerometer, false);
409+
}
410+
411+
async startNotifications(type: TypedServiceEvent) {
412+
const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
413+
if (serviceInfo) {
414+
// TODO: type cheat! why?
415+
const service = await this.createIfNeeded(serviceInfo as any, true);
416+
service?.startNotifications(type);
371417
}
372-
return this.accelerometerService;
418+
}
419+
420+
async stopNotifications(type: TypedServiceEvent) {
421+
const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
422+
serviceInfo?.get()?.stopNotifications(type);
373423
}
374424

375425
private disposeServices() {
376-
this.accelerometerService = undefined;
426+
this.serviceInfo.forEach((s) => s.dispose());
377427
this.clearGattQueueOnDisconnect();
378428
}
379429
}

0 commit comments

Comments
 (0)