Skip to content

Commit 2645519

Browse files
Bluetooth UART support (#46)
1 parent 86cbb6a commit 2645519

File tree

7 files changed

+178
-5
lines changed

7 files changed

+178
-5
lines changed

lib/bluetooth-device-wrapper.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
TypedServiceEvent,
1717
TypedServiceEventDispatcher,
1818
} from "./service-events.js";
19+
import { UARTService } from "./uart-service.js";
1920

2021
const deviceIdToWrapper: Map<string, BluetoothDeviceWrapper> = new Map();
2122

@@ -116,8 +117,9 @@ export class BluetoothDeviceWrapper {
116117
"buttonbchanged",
117118
]);
118119
private led = new ServiceInfo(LedService.createService, []);
120+
private uart = new ServiceInfo(UARTService.createService, ["uartdata"]);
119121

120-
private serviceInfo = [this.accelerometer, this.buttons, this.led];
122+
private serviceInfo = [this.accelerometer, this.buttons, this.led, this.uart];
121123

122124
boardVersion: BoardVersion | undefined;
123125

@@ -385,6 +387,10 @@ export class BluetoothDeviceWrapper {
385387
return this.createIfNeeded(this.led, false);
386388
}
387389

390+
async getUARTService(): Promise<UARTService | undefined> {
391+
return this.createIfNeeded(this.uart, false);
392+
}
393+
388394
async startNotifications(type: TypedServiceEvent) {
389395
const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
390396
if (serviceInfo) {

lib/bluetooth.ts

+5
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,9 @@ export class MicrobitWebBluetoothConnection
273273
const ledService = await this.connection?.getLedService();
274274
ledService?.setLedMatrix(matrix);
275275
}
276+
277+
async writeUART(data: Uint8Array): Promise<void> {
278+
const uartService = await this.connection?.getUARTService();
279+
uartService?.writeData(data);
280+
}
276281
}

lib/device.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66
import { TypedEventTarget } from "./events.js";
7+
import { UARTDataEvent } from "./uart.js";
78

89
/**
910
* Specific identified error types.
@@ -180,6 +181,7 @@ export class DeviceConnectionEventMap {
180181
"serialdata": SerialDataEvent;
181182
"serialreset": Event;
182183
"serialerror": SerialErrorEvent;
184+
"uartdata": UARTDataEvent;
183185
"flash": Event;
184186
"beforerequestdevice": Event;
185187
"afterrequestdevice": Event;

lib/service-events.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { AccelerometerDataEvent } from "./accelerometer.js";
22
import { ButtonEvent } from "./buttons.js";
33
import { DeviceConnectionEventMap } from "./device.js";
4+
import { UARTDataEvent } from "./uart.js";
45

56
export class ServiceConnectionEventMap {
67
"accelerometerdatachanged": AccelerometerDataEvent;
78
"buttonachanged": ButtonEvent;
89
"buttonbchanged": ButtonEvent;
10+
"uartdata": UARTDataEvent;
911
}
1012

1113
export type CharacteristicDataTarget = EventTarget & {

lib/uart-service.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Service } from "./bluetooth-device-wrapper.js";
2+
import { profile } from "./bluetooth-profile.js";
3+
import { BackgroundErrorEvent, DeviceError } from "./device.js";
4+
import {
5+
CharacteristicDataTarget,
6+
TypedServiceEvent,
7+
TypedServiceEventDispatcher,
8+
} from "./service-events.js";
9+
import { UARTDataEvent } from "./uart.js";
10+
11+
export class UARTService implements Service {
12+
constructor(
13+
private txCharacteristic: BluetoothRemoteGATTCharacteristic,
14+
private rxCharacteristic: BluetoothRemoteGATTCharacteristic,
15+
private dispatchTypedEvent: TypedServiceEventDispatcher,
16+
private queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
17+
) {
18+
this.txCharacteristic.addEventListener(
19+
"characteristicvaluechanged",
20+
(event: Event) => {
21+
const target = event.target as CharacteristicDataTarget;
22+
const value = new Uint8Array(target.value.buffer);
23+
this.dispatchTypedEvent("uartdata", new UARTDataEvent(value));
24+
},
25+
);
26+
}
27+
28+
static async createService(
29+
gattServer: BluetoothRemoteGATTServer,
30+
dispatcher: TypedServiceEventDispatcher,
31+
queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
32+
listenerInit: boolean,
33+
): Promise<UARTService | undefined> {
34+
let uartService: BluetoothRemoteGATTService;
35+
try {
36+
uartService = await gattServer.getPrimaryService(profile.uart.id);
37+
} catch (err) {
38+
if (listenerInit) {
39+
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
40+
return;
41+
} else {
42+
throw new DeviceError({
43+
code: "service-missing",
44+
message: err as string,
45+
});
46+
}
47+
}
48+
const rxCharacteristic = await uartService.getCharacteristic(
49+
profile.uart.characteristics.rx.id,
50+
);
51+
const txCharacteristic = await uartService.getCharacteristic(
52+
profile.uart.characteristics.tx.id,
53+
);
54+
return new UARTService(
55+
txCharacteristic,
56+
rxCharacteristic,
57+
dispatcher,
58+
queueGattOperation,
59+
);
60+
}
61+
62+
async startNotifications(type: TypedServiceEvent): Promise<void> {
63+
if (type === "uartdata") {
64+
await this.txCharacteristic.startNotifications();
65+
}
66+
}
67+
68+
async stopNotifications(type: TypedServiceEvent): Promise<void> {
69+
if (type === "uartdata") {
70+
await this.txCharacteristic.stopNotifications();
71+
}
72+
}
73+
74+
async writeData(value: Uint8Array): Promise<void> {
75+
const dataView = new DataView(value.buffer);
76+
return this.queueGattOperation(() =>
77+
this.rxCharacteristic.writeValueWithoutResponse(dataView),
78+
);
79+
}
80+
}

lib/uart.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class UARTDataEvent extends Event {
2+
constructor(public readonly value: Uint8Array) {
3+
super("uartdata");
4+
}
5+
}

src/demo.ts

+77-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: MIT
55
*/
66
import crelt from "crelt";
7+
import { AccelerometerDataEvent } from "../lib/accelerometer";
78
import { MicrobitWebBluetoothConnection } from "../lib/bluetooth";
89
import { ButtonEvent } from "../lib/buttons";
910
import {
@@ -14,13 +15,10 @@ import {
1415
SerialDataEvent,
1516
} from "../lib/device";
1617
import { createUniversalHexFlashDataSource } from "../lib/hex-flash-data-source";
18+
import { UARTDataEvent } from "../lib/uart";
1719
import { MicrobitWebUSBConnection } from "../lib/usb";
1820
import { MicrobitRadioBridgeConnection } from "../lib/usb-radio-bridge";
1921
import "./demo.css";
20-
import {
21-
AccelerometerData,
22-
AccelerometerDataEvent,
23-
} from "../lib/accelerometer";
2422

2523
type ConnectionType = "usb" | "bluetooth" | "radio";
2624

@@ -64,6 +62,7 @@ const recreateUi = async (type: ConnectionType) => {
6462
createConnectSection(type),
6563
createFlashSection(),
6664
createSerialSection(),
65+
createUARTSection(),
6766
createButtonSection("A", "buttonachanged"),
6867
createButtonSection("B", "buttonbchanged"),
6968
createAccelerometerSection(),
@@ -274,6 +273,80 @@ const createSerialSection = (): Section => {
274273
};
275274
};
276275

276+
const createUARTSection = (): Section => {
277+
if (!(connection instanceof MicrobitWebBluetoothConnection)) {
278+
return {};
279+
}
280+
281+
const uartDataListener = (event: UARTDataEvent) => {
282+
const value = new TextDecoder().decode(event.value);
283+
console.log(value);
284+
};
285+
286+
const bluetoothConnection =
287+
connection instanceof MicrobitWebBluetoothConnection
288+
? connection
289+
: undefined;
290+
291+
let dataToWrite = "";
292+
const dataToWriteFieldId = "dataToWrite";
293+
const dom = crelt(
294+
"section",
295+
crelt("h2", "UART"),
296+
crelt("h3", "Receive"),
297+
crelt(
298+
"button",
299+
{
300+
onclick: () => {
301+
bluetoothConnection?.addEventListener("uartdata", uartDataListener);
302+
},
303+
},
304+
"Listen to UART",
305+
),
306+
crelt(
307+
"button",
308+
{
309+
onclick: () => {
310+
bluetoothConnection?.removeEventListener(
311+
"uartdata",
312+
uartDataListener,
313+
);
314+
},
315+
},
316+
"Stop listening to UART",
317+
),
318+
crelt("h3", "Write"),
319+
crelt("label", { name: "Data", for: dataToWriteFieldId }),
320+
crelt("textarea", {
321+
id: dataToWriteFieldId,
322+
type: "text",
323+
onchange: (e: Event) => {
324+
dataToWrite = (e.currentTarget as HTMLInputElement).value;
325+
},
326+
}),
327+
crelt(
328+
"div",
329+
crelt(
330+
"button",
331+
{
332+
onclick: async () => {
333+
const encoded = new TextEncoder().encode(dataToWrite);
334+
await bluetoothConnection?.writeUART(encoded);
335+
},
336+
},
337+
"Write to micro:bit",
338+
),
339+
),
340+
);
341+
342+
return {
343+
dom,
344+
cleanup: () => {
345+
connection.removeEventListener("uartdata", uartDataListener);
346+
},
347+
};
348+
};
349+
277350
const createAccelerometerSection = (): Section => {
278351
if (
279352
!(

0 commit comments

Comments
 (0)