Skip to content

Commit

Permalink
Add magnetometer service (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-robert authored Jan 2, 2025
1 parent 47594f1 commit bb2eb6b
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 2 deletions.
2 changes: 1 addition & 1 deletion lib/accelerometer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() =>
Expand Down
16 changes: 15 additions & 1 deletion lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

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

async getMagnetometerService(): Promise<MagnetometerService | undefined> {
return this.createIfNeeded(this.magnetometer, false);
}

async getUARTService(): Promise<UARTService | undefined> {
return this.createIfNeeded(this.uart, false);
}
Expand Down
26 changes: 26 additions & 0 deletions lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -274,6 +275,31 @@ export class MicrobitWebBluetoothConnection
ledService?.setLedMatrix(matrix);
}

async getMagnetometerData(): Promise<MagnetometerData | undefined> {
const magnetometerService = await this.connection?.getMagnetometerService();
return magnetometerService?.getData();
}

async getMagnetometerPeriod(): Promise<number | undefined> {
const magnetometerService = await this.connection?.getMagnetometerService();
return magnetometerService?.getPeriod();
}

async setMagnetometerPeriod(value: number): Promise<void> {
const magnetometerService = await this.connection?.getMagnetometerService();
return magnetometerService?.setPeriod(value);
}

async getMagnetometerBearing(): Promise<number | undefined> {
const magnetometerService = await this.connection?.getMagnetometerService();
return magnetometerService?.getBearing();
}

async triggerMagnetometerCalibration(): Promise<void> {
const magnetometerService = await this.connection?.getMagnetometerService();
return magnetometerService?.triggerCalibration();
}

async writeUART(data: Uint8Array): Promise<void> {
const uartService = await this.connection?.getUARTService();
uartService?.writeData(data);
Expand Down
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -56,4 +57,6 @@ export type {
DeviceConnection,
DeviceErrorCode,
FlashDataSource,
MagnetometerData,
MagnetometerDataEvent,
};
152 changes: 152 additions & 0 deletions lib/magnetometer-service.ts
Original file line number Diff line number Diff line change
@@ -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: <R>(action: () => Promise<R>) => Promise<R>,
) {
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: <R>(action: () => Promise<R>) => Promise<R>,
listenerInit: boolean,
): Promise<MagnetometerService | undefined> {
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<MagnetometerData> {
const dataView = await this.queueGattOperation(() =>
this.magnetometerDataCharacteristic.readValue(),
);
return this.dataViewToData(dataView);
}

async getPeriod(): Promise<number> {
const dataView = await this.queueGattOperation(() =>
this.magnetometerPeriodCharacteristic.readValue(),
);
return dataView.getUint16(0, true);
}

async setPeriod(value: number): Promise<void> {
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<number> {
const dataView = await this.queueGattOperation(() =>
this.magnetometerBearingCharacteristic.readValue(),
);
return dataView.getUint16(0, true);
}

async triggerCalibration(): Promise<void> {
const dataView = new DataView(new ArrayBuffer(1));
dataView.setUint8(0, 1);
return this.queueGattOperation(() =>
this.magnetometerCalibrationCharacteristic.writeValue(dataView),
);
}

async startNotifications(type: TypedServiceEvent): Promise<void> {
await this.characteristicForEvent(type)?.startNotifications();
}

async stopNotifications(type: TypedServiceEvent): Promise<void> {
await this.characteristicForEvent(type)?.stopNotifications();
}

private characteristicForEvent(type: TypedServiceEvent) {
switch (type) {
case "magnetometerdatachanged": {
return this.magnetometerDataCharacteristic;
}
default: {
return undefined;
}
}
}
}
11 changes: 11 additions & 0 deletions lib/magnetometer.ts
Original file line number Diff line number Diff line change
@@ -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");
}
}
2 changes: 2 additions & 0 deletions lib/service-events.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Loading

0 comments on commit bb2eb6b

Please sign in to comment.