Skip to content

Commit

Permalink
Bluetooth UART support (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-grace authored Dec 13, 2024
1 parent 86cbb6a commit 2645519
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 5 deletions.
8 changes: 7 additions & 1 deletion lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
TypedServiceEvent,
TypedServiceEventDispatcher,
} from "./service-events.js";
import { UARTService } from "./uart-service.js";

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

Expand Down Expand Up @@ -116,8 +117,9 @@ export class BluetoothDeviceWrapper {
"buttonbchanged",
]);
private led = new ServiceInfo(LedService.createService, []);
private uart = new ServiceInfo(UARTService.createService, ["uartdata"]);

private serviceInfo = [this.accelerometer, this.buttons, this.led];
private serviceInfo = [this.accelerometer, this.buttons, this.led, this.uart];

boardVersion: BoardVersion | undefined;

Expand Down Expand Up @@ -385,6 +387,10 @@ export class BluetoothDeviceWrapper {
return this.createIfNeeded(this.led, false);
}

async getUARTService(): Promise<UARTService | undefined> {
return this.createIfNeeded(this.uart, false);
}

async startNotifications(type: TypedServiceEvent) {
const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
if (serviceInfo) {
Expand Down
5 changes: 5 additions & 0 deletions lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,9 @@ export class MicrobitWebBluetoothConnection
const ledService = await this.connection?.getLedService();
ledService?.setLedMatrix(matrix);
}

async writeUART(data: Uint8Array): Promise<void> {
const uartService = await this.connection?.getUARTService();
uartService?.writeData(data);
}
}
2 changes: 2 additions & 0 deletions lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
import { TypedEventTarget } from "./events.js";
import { UARTDataEvent } from "./uart.js";

/**
* Specific identified error types.
Expand Down Expand Up @@ -180,6 +181,7 @@ export class DeviceConnectionEventMap {
"serialdata": SerialDataEvent;
"serialreset": Event;
"serialerror": SerialErrorEvent;
"uartdata": UARTDataEvent;
"flash": Event;
"beforerequestdevice": Event;
"afterrequestdevice": Event;
Expand Down
2 changes: 2 additions & 0 deletions lib/service-events.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { AccelerometerDataEvent } from "./accelerometer.js";
import { ButtonEvent } from "./buttons.js";
import { DeviceConnectionEventMap } from "./device.js";
import { UARTDataEvent } from "./uart.js";

export class ServiceConnectionEventMap {
"accelerometerdatachanged": AccelerometerDataEvent;
"buttonachanged": ButtonEvent;
"buttonbchanged": ButtonEvent;
"uartdata": UARTDataEvent;
}

export type CharacteristicDataTarget = EventTarget & {
Expand Down
80 changes: 80 additions & 0 deletions lib/uart-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Service } from "./bluetooth-device-wrapper.js";
import { profile } from "./bluetooth-profile.js";
import { BackgroundErrorEvent, DeviceError } from "./device.js";
import {
CharacteristicDataTarget,
TypedServiceEvent,
TypedServiceEventDispatcher,
} from "./service-events.js";
import { UARTDataEvent } from "./uart.js";

export class UARTService implements Service {
constructor(
private txCharacteristic: BluetoothRemoteGATTCharacteristic,
private rxCharacteristic: BluetoothRemoteGATTCharacteristic,
private dispatchTypedEvent: TypedServiceEventDispatcher,
private queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
) {
this.txCharacteristic.addEventListener(
"characteristicvaluechanged",
(event: Event) => {
const target = event.target as CharacteristicDataTarget;
const value = new Uint8Array(target.value.buffer);
this.dispatchTypedEvent("uartdata", new UARTDataEvent(value));
},
);
}

static async createService(
gattServer: BluetoothRemoteGATTServer,
dispatcher: TypedServiceEventDispatcher,
queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
listenerInit: boolean,
): Promise<UARTService | undefined> {
let uartService: BluetoothRemoteGATTService;
try {
uartService = await gattServer.getPrimaryService(profile.uart.id);
} catch (err) {
if (listenerInit) {
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
return;
} else {
throw new DeviceError({
code: "service-missing",
message: err as string,
});
}
}
const rxCharacteristic = await uartService.getCharacteristic(
profile.uart.characteristics.rx.id,
);
const txCharacteristic = await uartService.getCharacteristic(
profile.uart.characteristics.tx.id,
);
return new UARTService(
txCharacteristic,
rxCharacteristic,
dispatcher,
queueGattOperation,
);
}

async startNotifications(type: TypedServiceEvent): Promise<void> {
if (type === "uartdata") {
await this.txCharacteristic.startNotifications();
}
}

async stopNotifications(type: TypedServiceEvent): Promise<void> {
if (type === "uartdata") {
await this.txCharacteristic.stopNotifications();
}
}

async writeData(value: Uint8Array): Promise<void> {
const dataView = new DataView(value.buffer);
return this.queueGattOperation(() =>
this.rxCharacteristic.writeValueWithoutResponse(dataView),
);
}
}
5 changes: 5 additions & 0 deletions lib/uart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UARTDataEvent extends Event {
constructor(public readonly value: Uint8Array) {
super("uartdata");
}
}
81 changes: 77 additions & 4 deletions src/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: MIT
*/
import crelt from "crelt";
import { AccelerometerDataEvent } from "../lib/accelerometer";
import { MicrobitWebBluetoothConnection } from "../lib/bluetooth";
import { ButtonEvent } from "../lib/buttons";
import {
Expand All @@ -14,13 +15,10 @@ import {
SerialDataEvent,
} from "../lib/device";
import { createUniversalHexFlashDataSource } from "../lib/hex-flash-data-source";
import { UARTDataEvent } from "../lib/uart";
import { MicrobitWebUSBConnection } from "../lib/usb";
import { MicrobitRadioBridgeConnection } from "../lib/usb-radio-bridge";
import "./demo.css";
import {
AccelerometerData,
AccelerometerDataEvent,
} from "../lib/accelerometer";

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

Expand Down Expand Up @@ -64,6 +62,7 @@ const recreateUi = async (type: ConnectionType) => {
createConnectSection(type),
createFlashSection(),
createSerialSection(),
createUARTSection(),
createButtonSection("A", "buttonachanged"),
createButtonSection("B", "buttonbchanged"),
createAccelerometerSection(),
Expand Down Expand Up @@ -274,6 +273,80 @@ const createSerialSection = (): Section => {
};
};

const createUARTSection = (): Section => {
if (!(connection instanceof MicrobitWebBluetoothConnection)) {
return {};
}

const uartDataListener = (event: UARTDataEvent) => {
const value = new TextDecoder().decode(event.value);
console.log(value);
};

const bluetoothConnection =
connection instanceof MicrobitWebBluetoothConnection
? connection
: undefined;

let dataToWrite = "";
const dataToWriteFieldId = "dataToWrite";
const dom = crelt(
"section",
crelt("h2", "UART"),
crelt("h3", "Receive"),
crelt(
"button",
{
onclick: () => {
bluetoothConnection?.addEventListener("uartdata", uartDataListener);
},
},
"Listen to UART",
),
crelt(
"button",
{
onclick: () => {
bluetoothConnection?.removeEventListener(
"uartdata",
uartDataListener,
);
},
},
"Stop listening to UART",
),
crelt("h3", "Write"),
crelt("label", { name: "Data", for: dataToWriteFieldId }),
crelt("textarea", {
id: dataToWriteFieldId,
type: "text",
onchange: (e: Event) => {
dataToWrite = (e.currentTarget as HTMLInputElement).value;
},
}),
crelt(
"div",
crelt(
"button",
{
onclick: async () => {
const encoded = new TextEncoder().encode(dataToWrite);
await bluetoothConnection?.writeUART(encoded);
},
},
"Write to micro:bit",
),
),
);

return {
dom,
cleanup: () => {
connection.removeEventListener("uartdata", uartDataListener);
},
};
};

const createAccelerometerSection = (): Section => {
if (
!(
Expand Down

0 comments on commit 2645519

Please sign in to comment.