Skip to content

Commit

Permalink
Add basic magnetometer service
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-robert committed Dec 23, 2024
1 parent 47594f1 commit c85d45e
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 1 deletion.
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
15 changes: 15 additions & 0 deletions lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,21 @@ export class MicrobitWebBluetoothConnection
ledService?.setLedMatrix(matrix);
}

async getMagnetometerData(): Promise<AccelerometerData | 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 writeUART(data: Uint8Array): Promise<void> {
const uartService = await this.connection?.getUARTService();
uartService?.writeData(data);
Expand Down
125 changes: 125 additions & 0 deletions lib/magnetometer-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 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,
);
return new MagnetometerService(
magnetometerDataCharacteristic,
magnetometerPeriodCharacteristic,
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: 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
const dataView = new DataView(new ArrayBuffer(2));
dataView.setUint16(0, value, true);
return this.queueGattOperation(() =>
this.magnetometerPeriodCharacteristic.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
98 changes: 98 additions & 0 deletions src/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -66,6 +67,7 @@ const recreateUi = async (type: ConnectionType) => {
createButtonSection("A", "buttonachanged"),
createButtonSection("B", "buttonbchanged"),
createAccelerometerSection(),
createMagnetometerSection(),
createLedSection(),
].forEach(({ dom, cleanup }) => {
if (dom) {
Expand Down Expand Up @@ -443,6 +445,102 @@ const createAccelerometerSection = (): Section => {
};
};

const createMagnetometerSection = (): Section => {
if (
!(
connection instanceof MicrobitRadioBridgeConnection ||
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 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",
),
]
: [],
);
return {
dom,
cleanup: () => {
magnetometerConnection.removeEventListener(
"magnetometerdatachanged",
listener,
);
},
};
};

const createButtonSection = (
label: string,
type: "buttonachanged" | "buttonbchanged",
Expand Down

0 comments on commit c85d45e

Please sign in to comment.