Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add magnetometer service #48

Merged
merged 4 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +110 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, interesting that setting the values to the documented values don't seem to work.

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
Loading