From bb2eb6bb11ab97f95c8c551cec84ea8bbc765abc Mon Sep 17 00:00:00 2001 From: Robert Knight <95928279+microbit-robert@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:35:23 +0000 Subject: [PATCH] Add magnetometer service (#48) --- lib/accelerometer-service.ts | 2 +- lib/bluetooth-device-wrapper.ts | 16 +++- lib/bluetooth.ts | 26 ++++++ lib/index.ts | 3 + lib/magnetometer-service.ts | 152 ++++++++++++++++++++++++++++++++ lib/magnetometer.ts | 11 +++ lib/service-events.ts | 2 + src/demo.ts | 114 ++++++++++++++++++++++++ 8 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 lib/magnetometer-service.ts create mode 100644 lib/magnetometer.ts diff --git a/lib/accelerometer-service.ts b/lib/accelerometer-service.ts index db12491..25dcf82 100644 --- a/lib/accelerometer-service.ts +++ b/lib/accelerometer-service.ts @@ -96,7 +96,7 @@ export class AccelerometerService implements Service { // Allowed values: 2, 5, 10, 20, 40, 100, 1000 // Values passed are rounded up to the allowed values on device. // Documentation for allowed values looks wrong. - // https://lancaster-university.github.io/microbit-docs/resources/bluetooth/bluetooth_profile.html + // https://lancaster-university.github.io/microbit-docs/ble/profile/#about-the-accelerometer-service const dataView = new DataView(new ArrayBuffer(2)); dataView.setUint16(0, value, true); return this.queueGattOperation(() => diff --git a/lib/bluetooth-device-wrapper.ts b/lib/bluetooth-device-wrapper.ts index 6bbc12c..b0c0622 100644 --- a/lib/bluetooth-device-wrapper.ts +++ b/lib/bluetooth-device-wrapper.ts @@ -10,6 +10,7 @@ import { ButtonService } from "./button-service.js"; import { BoardVersion, DeviceError } from "./device.js"; import { LedService } from "./led-service.js"; import { Logging, NullLogging } from "./logging.js"; +import { MagnetometerService } from "./magnetometer-service.js"; import { PromiseQueue } from "./promise-queue.js"; import { ServiceConnectionEventMap, @@ -117,9 +118,18 @@ export class BluetoothDeviceWrapper { "buttonbchanged", ]); private led = new ServiceInfo(LedService.createService, []); + private magnetometer = new ServiceInfo(MagnetometerService.createService, [ + "magnetometerdatachanged", + ]); private uart = new ServiceInfo(UARTService.createService, ["uartdata"]); - private serviceInfo = [this.accelerometer, this.buttons, this.led, this.uart]; + private serviceInfo = [ + this.accelerometer, + this.buttons, + this.led, + this.magnetometer, + this.uart, + ]; boardVersion: BoardVersion | undefined; @@ -387,6 +397,10 @@ export class BluetoothDeviceWrapper { return this.createIfNeeded(this.led, false); } + async getMagnetometerService(): Promise { + return this.createIfNeeded(this.magnetometer, false); + } + async getUARTService(): Promise { return this.createIfNeeded(this.uart, false); } diff --git a/lib/bluetooth.ts b/lib/bluetooth.ts index bf9e9d7..1a98759 100644 --- a/lib/bluetooth.ts +++ b/lib/bluetooth.ts @@ -21,6 +21,7 @@ import { import { TypedEventTarget } from "./events.js"; import { LedMatrix } from "./led.js"; import { Logging, NullLogging } from "./logging.js"; +import { MagnetometerData } from "./magnetometer.js"; import { ServiceConnectionEventMap, TypedServiceEvent, @@ -274,6 +275,31 @@ export class MicrobitWebBluetoothConnection ledService?.setLedMatrix(matrix); } + async getMagnetometerData(): Promise { + const magnetometerService = await this.connection?.getMagnetometerService(); + return magnetometerService?.getData(); + } + + async getMagnetometerPeriod(): Promise { + const magnetometerService = await this.connection?.getMagnetometerService(); + return magnetometerService?.getPeriod(); + } + + async setMagnetometerPeriod(value: number): Promise { + const magnetometerService = await this.connection?.getMagnetometerService(); + return magnetometerService?.setPeriod(value); + } + + async getMagnetometerBearing(): Promise { + const magnetometerService = await this.connection?.getMagnetometerService(); + return magnetometerService?.getBearing(); + } + + async triggerMagnetometerCalibration(): Promise { + const magnetometerService = await this.connection?.getMagnetometerService(); + return magnetometerService?.triggerCalibration(); + } + async writeUART(data: Uint8Array): Promise { const uartService = await this.connection?.getUARTService(); uartService?.writeData(data); diff --git a/lib/index.ts b/lib/index.ts index 4604c93..c07bb62 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -21,6 +21,7 @@ import { } from "./device.js"; import { TypedEventTarget } from "./events.js"; import { createUniversalHexFlashDataSource } from "./hex-flash-data-source.js"; +import { MagnetometerData, MagnetometerDataEvent } from "./magnetometer.js"; import { ServiceConnectionEventMap } from "./service-events.js"; import { MicrobitRadioBridgeConnection } from "./usb-radio-bridge.js"; import { MicrobitWebUSBConnection } from "./usb.js"; @@ -56,4 +57,6 @@ export type { DeviceConnection, DeviceErrorCode, FlashDataSource, + MagnetometerData, + MagnetometerDataEvent, }; diff --git a/lib/magnetometer-service.ts b/lib/magnetometer-service.ts new file mode 100644 index 0000000..d0182f9 --- /dev/null +++ b/lib/magnetometer-service.ts @@ -0,0 +1,152 @@ +import { MagnetometerData, MagnetometerDataEvent } from "./magnetometer.js"; +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"; + +export class MagnetometerService implements Service { + constructor( + private magnetometerDataCharacteristic: BluetoothRemoteGATTCharacteristic, + private magnetometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic, + private magnetometerBearingCharacteristic: BluetoothRemoteGATTCharacteristic, + private magnetometerCalibrationCharacteristic: BluetoothRemoteGATTCharacteristic, + private dispatchTypedEvent: TypedServiceEventDispatcher, + private queueGattOperation: (action: () => Promise) => Promise, + ) { + this.magnetometerDataCharacteristic.addEventListener( + "characteristicvaluechanged", + (event: Event) => { + const target = event.target as CharacteristicDataTarget; + const data = this.dataViewToData(target.value); + this.dispatchTypedEvent( + "magnetometerdatachanged", + new MagnetometerDataEvent(data), + ); + }, + ); + } + + static async createService( + gattServer: BluetoothRemoteGATTServer, + dispatcher: TypedServiceEventDispatcher, + queueGattOperation: (action: () => Promise) => Promise, + listenerInit: boolean, + ): Promise { + let magnetometerService: BluetoothRemoteGATTService; + try { + magnetometerService = await gattServer.getPrimaryService( + profile.magnetometer.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 magnetometerDataCharacteristic = + await magnetometerService.getCharacteristic( + profile.magnetometer.characteristics.data.id, + ); + const magnetometerPeriodCharacteristic = + await magnetometerService.getCharacteristic( + profile.magnetometer.characteristics.period.id, + ); + const magnetometerBearingCharacteristic = + await magnetometerService.getCharacteristic( + profile.magnetometer.characteristics.bearing.id, + ); + const magnetometerCalibrationCharacteristic = + await magnetometerService.getCharacteristic( + profile.magnetometer.characteristics.calibration.id, + ); + return new MagnetometerService( + magnetometerDataCharacteristic, + magnetometerPeriodCharacteristic, + magnetometerBearingCharacteristic, + magnetometerCalibrationCharacteristic, + dispatcher, + queueGattOperation, + ); + } + + private dataViewToData(dataView: DataView): MagnetometerData { + return { + x: dataView.getInt16(0, true), + y: dataView.getInt16(2, true), + z: dataView.getInt16(4, true), + }; + } + + async getData(): Promise { + const dataView = await this.queueGattOperation(() => + this.magnetometerDataCharacteristic.readValue(), + ); + return this.dataViewToData(dataView); + } + + async getPeriod(): Promise { + const dataView = await this.queueGattOperation(() => + this.magnetometerPeriodCharacteristic.readValue(), + ); + return dataView.getUint16(0, true); + } + + async setPeriod(value: number): Promise { + if (value === 0) { + // Writing 0 causes the device to crash. + return; + } + // Allowed values: 10, 20, 50, 100 + // Values passed are rounded up to the allowed values on device. + // Documentation for allowed values looks wrong. + // https://lancaster-university.github.io/microbit-docs/ble/profile/#about-the-magnetometer-service + const dataView = new DataView(new ArrayBuffer(2)); + dataView.setUint16(0, value, true); + return this.queueGattOperation(() => + this.magnetometerPeriodCharacteristic.writeValue(dataView), + ); + } + + async getBearing(): Promise { + const dataView = await this.queueGattOperation(() => + this.magnetometerBearingCharacteristic.readValue(), + ); + return dataView.getUint16(0, true); + } + + async triggerCalibration(): Promise { + const dataView = new DataView(new ArrayBuffer(1)); + dataView.setUint8(0, 1); + return this.queueGattOperation(() => + this.magnetometerCalibrationCharacteristic.writeValue(dataView), + ); + } + + async startNotifications(type: TypedServiceEvent): Promise { + await this.characteristicForEvent(type)?.startNotifications(); + } + + async stopNotifications(type: TypedServiceEvent): Promise { + await this.characteristicForEvent(type)?.stopNotifications(); + } + + private characteristicForEvent(type: TypedServiceEvent) { + switch (type) { + case "magnetometerdatachanged": { + return this.magnetometerDataCharacteristic; + } + default: { + return undefined; + } + } + } +} diff --git a/lib/magnetometer.ts b/lib/magnetometer.ts new file mode 100644 index 0000000..9e52ba9 --- /dev/null +++ b/lib/magnetometer.ts @@ -0,0 +1,11 @@ +export interface MagnetometerData { + x: number; + y: number; + z: number; +} + +export class MagnetometerDataEvent extends Event { + constructor(public readonly data: MagnetometerData) { + super("magnetometerdatachanged"); + } +} diff --git a/lib/service-events.ts b/lib/service-events.ts index a82241d..df70e21 100644 --- a/lib/service-events.ts +++ b/lib/service-events.ts @@ -1,12 +1,14 @@ import { AccelerometerDataEvent } from "./accelerometer.js"; import { ButtonEvent } from "./buttons.js"; import { DeviceConnectionEventMap } from "./device.js"; +import { MagnetometerDataEvent } from "./magnetometer.js"; import { UARTDataEvent } from "./uart.js"; export class ServiceConnectionEventMap { "accelerometerdatachanged": AccelerometerDataEvent; "buttonachanged": ButtonEvent; "buttonbchanged": ButtonEvent; + "magnetometerdatachanged": MagnetometerDataEvent; "uartdata": UARTDataEvent; } diff --git a/src/demo.ts b/src/demo.ts index 0bd7ca2..76a5656 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -19,6 +19,7 @@ import { UARTDataEvent } from "../lib/uart"; import { MicrobitWebUSBConnection } from "../lib/usb"; import { MicrobitRadioBridgeConnection } from "../lib/usb-radio-bridge"; import "./demo.css"; +import { MagnetometerDataEvent } from "../lib/magnetometer"; type ConnectionType = "usb" | "bluetooth" | "radio"; @@ -66,6 +67,7 @@ const recreateUi = async (type: ConnectionType) => { createButtonSection("A", "buttonachanged"), createButtonSection("B", "buttonbchanged"), createAccelerometerSection(), + createMagnetometerSection(), createLedSection(), ].forEach(({ dom, cleanup }) => { if (dom) { @@ -443,6 +445,118 @@ const createAccelerometerSection = (): Section => { }; }; +const createMagnetometerSection = (): Section => { + if (!(connection instanceof MicrobitWebBluetoothConnection)) { + return {}; + } + const magnetometerConnection = connection; + const bluetoothConnection = + connection instanceof MicrobitWebBluetoothConnection + ? connection + : undefined; + const statusParagraph = crelt("p"); + const listener = (e: MagnetometerDataEvent) => { + statusParagraph.innerText = JSON.stringify(e.data); + }; + let period = ""; + const periodInput = crelt("input", { + type: "number", + onchange: (e: Event) => { + period = (e.currentTarget as HTMLInputElement).value; + }, + }) as HTMLInputElement; + const bearingParagraph = crelt("p"); + const dom = crelt( + "section", + crelt("h2", "Magnetometer"), + crelt("h3", "Events"), + crelt( + "button", + { + onclick: () => { + magnetometerConnection.addEventListener( + "magnetometerdatachanged", + listener, + ); + }, + }, + "Listen", + ), + crelt( + "button", + { + onclick: () => { + magnetometerConnection.removeEventListener( + "magnetometerdatachanged", + listener, + ); + }, + }, + "Stop listening", + ), + statusParagraph, + bluetoothConnection + ? [ + crelt("h3", "Period"), + crelt("label", "Value", periodInput), + crelt( + "button", + { + onclick: async () => { + period = + ( + await bluetoothConnection.getMagnetometerPeriod() + )?.toString() ?? ""; + periodInput.value = period; + }, + }, + "Get period", + ), + crelt( + "button", + { + onclick: async () => { + await bluetoothConnection.setMagnetometerPeriod( + parseInt(period, 10), + ); + }, + }, + "Set period", + ), + ] + : [], + bearingParagraph, + crelt( + "button", + { + onclick: async () => { + void bluetoothConnection?.triggerMagnetometerCalibration(); + }, + }, + "Trigger calibration", + ), + crelt( + "button", + { + onclick: async () => { + const bearing = await bluetoothConnection?.getMagnetometerBearing(); + bearingParagraph.textContent = `Bearing: ${bearing ?? 0} degrees`; + }, + }, + "Get bearing", + ), + ); + return { + dom, + cleanup: () => { + magnetometerConnection.removeEventListener( + "magnetometerdatachanged", + listener, + ); + }, + }; +}; + const createButtonSection = ( label: string, type: "buttonachanged" | "buttonbchanged",