Skip to content

Commit fa96588

Browse files
Apps branch for experimentation
This contains breaking changes (e.g. flash progress interface, connect signature, device error codes) and will form part of a v1 at some point. Migrating USB-only code should be trivial though. Changes: - Switch to capacitor-ble for bluetooth - Quite a simplication as the service/characteristic lookups are deferred to point of use and the interactions are internally queued. - Support DFU and partial flashing on iOS/Android platforms - Drop a bunch of workarounds that need reevaluating after the switch - Temporarily drop uBit name support due to capacitor-ble limitation - Don't try to start notifications on absent services. Prevents issues when not in application mode for a flash. - Improve connect interface (which had a misleading return value) and connect/flash progress. This branch is going to be long lived for a month or two during apps work, then we'll loop back around and see what it means for Web Bluetooth - does it replace it or do we have both implementations. Design issues: - Should connecting and connecting for flashing both be the same flow? Or are the ideas different enough that we split them? It's nice in that it matches USB but it's also quite different because of pairing.
1 parent c01107d commit fa96588

33 files changed

+2563
-832
lines changed

.github/workflows/build-docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
path: ./docs/build
2525

2626
deploy:
27-
if: ${{ startsWith(github.ref, 'refs/tags/') }}
27+
if: false
2828
permissions:
2929
pages: write
3030
id-token: write

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ jobs:
1616
- uses: actions/setup-node@v4
1717
with:
1818
node-version: "20.x"
19-
registry-url: 'https://registry.npmjs.org'
19+
registry-url: "https://registry.npmjs.org"
2020
cache: npm
2121
- uses: microbit-foundation/npm-package-versioner-action@v1
2222
- run: npm ci
2323
env:
2424
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2525
- run: npm run ci
26-
- run: npm publish
26+
- run: npm publish --tag apps
2727
if: github.event_name == 'release' && github.event.action == 'created'
2828
env:
2929
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import { createUniversalHexFlashDataSource } from "@microbit/microbit-connection
3838

3939
await usb.flash(createUniversalHexFlashDataSource(universalHexString), {
4040
partial: true,
41-
progress: (percentage: number | undefined) => {
42-
console.log(percentage);
41+
progress: (stage, percentage) => {
42+
console.log(stage, percentage);
4343
},
4444
});
4545
```
@@ -67,8 +67,8 @@ await usb.flash(
6767
},
6868
{
6969
partial: true,
70-
progress: (percentage: number | undefined) => {
71-
console.log(percentage);
70+
progress: (stage, percentage) => {
71+
console.log(stage, percentage);
7272
},
7373
},
7474
);

lib/accelerometer-service.ts

Lines changed: 72 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,30 @@
1+
import { BleClient } from "@capacitor-community/bluetooth-le";
12
import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js";
23
import { Service } from "./bluetooth-device-wrapper.js";
3-
import { profile } from "./bluetooth-profile.js";
4-
import { BackgroundErrorEvent, DeviceError } from "./device.js";
54
import {
6-
CharacteristicDataTarget,
75
TypedServiceEvent,
86
TypedServiceEventDispatcher,
97
} from "./service-events.js";
8+
import { profile } from "./bluetooth-profile.js";
9+
import { BackgroundErrorEvent } from "./device.js";
1010

1111
export class AccelerometerService implements Service {
12+
uuid = profile.accelerometer.id;
13+
14+
static createService(
15+
deviceId: string,
16+
dispatchTypedEvent: TypedServiceEventDispatcher,
17+
): AccelerometerService {
18+
return new AccelerometerService(deviceId, dispatchTypedEvent);
19+
}
20+
1221
constructor(
13-
private accelerometerDataCharacteristic: BluetoothRemoteGATTCharacteristic,
14-
private accelerometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic,
22+
private deviceId: string,
1523
private dispatchTypedEvent: TypedServiceEventDispatcher,
16-
private queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
17-
) {
18-
this.accelerometerDataCharacteristic.addEventListener(
19-
"characteristicvaluechanged",
20-
(event: Event) => {
21-
const target = event.target as CharacteristicDataTarget;
22-
const data = this.dataViewToData(target.value);
23-
this.dispatchTypedEvent(
24-
"accelerometerdatachanged",
25-
new AccelerometerDataEvent(data),
26-
);
27-
},
28-
);
29-
}
24+
) {}
3025

31-
static async createService(
32-
gattServer: BluetoothRemoteGATTServer,
33-
dispatcher: TypedServiceEventDispatcher,
34-
queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
35-
listenerInit: boolean,
36-
): Promise<AccelerometerService | undefined> {
37-
let accelerometerService: BluetoothRemoteGATTService;
38-
try {
39-
accelerometerService = await gattServer.getPrimaryService(
40-
profile.accelerometer.id,
41-
);
42-
} catch (err) {
43-
if (listenerInit) {
44-
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
45-
return;
46-
} else {
47-
throw new DeviceError({
48-
code: "service-missing",
49-
message: err as string,
50-
});
51-
}
52-
}
53-
const accelerometerDataCharacteristic =
54-
await accelerometerService.getCharacteristic(
55-
profile.accelerometer.characteristics.data.id,
56-
);
57-
const accelerometerPeriodCharacteristic =
58-
await accelerometerService.getCharacteristic(
59-
profile.accelerometer.characteristics.period.id,
60-
);
61-
return new AccelerometerService(
62-
accelerometerDataCharacteristic,
63-
accelerometerPeriodCharacteristic,
64-
dispatcher,
65-
queueGattOperation,
66-
);
26+
getRelevantEvents(): TypedServiceEvent[] {
27+
return ["accelerometerdatachanged"];
6728
}
6829

6930
private dataViewToData(dataView: DataView): AccelerometerData {
@@ -75,15 +36,19 @@ export class AccelerometerService implements Service {
7536
}
7637

7738
async getData(): Promise<AccelerometerData> {
78-
const dataView = await this.queueGattOperation(() =>
79-
this.accelerometerDataCharacteristic.readValue(),
39+
const dataView = await BleClient.read(
40+
this.deviceId,
41+
profile.accelerometer.id,
42+
profile.accelerometer.characteristics.data.id,
8043
);
8144
return this.dataViewToData(dataView);
8245
}
8346

8447
async getPeriod(): Promise<number> {
85-
const dataView = await this.queueGattOperation(() =>
86-
this.accelerometerPeriodCharacteristic.readValue(),
48+
const dataView = await BleClient.read(
49+
this.deviceId,
50+
profile.accelerometer.id,
51+
profile.accelerometer.characteristics.period.id,
8752
);
8853
return dataView.getUint16(0, true);
8954
}
@@ -99,23 +64,66 @@ export class AccelerometerService implements Service {
9964
// https://lancaster-university.github.io/microbit-docs/ble/profile/#about-the-accelerometer-service
10065
const dataView = new DataView(new ArrayBuffer(2));
10166
dataView.setUint16(0, value, true);
102-
return this.queueGattOperation(() =>
103-
this.accelerometerPeriodCharacteristic.writeValue(dataView),
67+
await BleClient.write(
68+
this.deviceId,
69+
profile.accelerometer.id,
70+
profile.accelerometer.characteristics.period.id,
71+
dataView,
10472
);
10573
}
10674

10775
async startNotifications(type: TypedServiceEvent): Promise<void> {
108-
await this.characteristicForEvent(type)?.startNotifications();
76+
const result = this.characteristicForEvent(type);
77+
if (result) {
78+
const { service, characteristic } = result;
79+
try {
80+
await BleClient.startNotifications(
81+
this.deviceId,
82+
service,
83+
characteristic,
84+
(value) => {
85+
const data = this.dataViewToData(value);
86+
this.dispatchTypedEvent(
87+
"accelerometerdatachanged",
88+
new AccelerometerDataEvent(data),
89+
);
90+
},
91+
);
92+
} catch (e) {
93+
this.dispatchTypedEvent(
94+
"backgrounderror",
95+
new BackgroundErrorEvent("Failed to start notifications", e),
96+
);
97+
}
98+
}
10999
}
110100

111101
async stopNotifications(type: TypedServiceEvent): Promise<void> {
112-
await this.characteristicForEvent(type)?.stopNotifications();
102+
const result = this.characteristicForEvent(type);
103+
if (result) {
104+
const { service, characteristic } = result;
105+
try {
106+
await BleClient.stopNotifications(
107+
this.deviceId,
108+
service,
109+
characteristic,
110+
);
111+
} catch (e) {
112+
this.dispatchTypedEvent(
113+
"backgrounderror",
114+
new BackgroundErrorEvent("Failed to stop notifications", e),
115+
);
116+
}
117+
}
113118
}
114119

115120
private characteristicForEvent(type: TypedServiceEvent) {
116121
switch (type) {
117122
case "accelerometerdatachanged": {
118-
return this.accelerometerDataCharacteristic;
123+
return {
124+
service: profile.accelerometer.id,
125+
characteristic: profile.accelerometer.characteristics.data.id,
126+
};
119127
}
120128
default: {
121129
return undefined;

lib/async-util.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
*/
66
export class TimeoutError extends Error {}
77

8+
export class DisconnectError extends Error {
9+
constructor(message: string = "Disconnect") {
10+
super(message);
11+
}
12+
}
13+
814
/**
915
* Utility to time out an action after a delay.
1016
*
@@ -14,11 +20,28 @@ export async function withTimeout<T>(
1420
actionPromise: Promise<T>,
1521
timeout: number,
1622
): Promise<T> {
17-
const timeoutPromise = new Promise((_, reject) => {
18-
setTimeout(() => {
19-
reject(new TimeoutError());
20-
}, timeout);
23+
return Promise.race([
24+
actionPromise,
25+
timeoutErrorAfter(timeout),
26+
]) as Promise<T>;
27+
}
28+
29+
export async function delay(millis: number) {
30+
return new Promise((resolve) => setTimeout(resolve, millis));
31+
}
32+
33+
export async function timeoutErrorAfter<T>(
34+
millis: number,
35+
message: string = "Timeout",
36+
): Promise<T> {
37+
await delay(millis);
38+
throw new TimeoutError(message);
39+
}
40+
41+
export function disconnectErrorCallback<T>(message: string = "Disconnect") {
42+
let callback: () => void | undefined;
43+
const promise = new Promise<T>((_, reject) => {
44+
callback = () => reject(new DisconnectError(message));
2145
});
22-
// timeoutPromise never resolves so result must be from action
23-
return Promise.race([actionPromise, timeoutPromise]) as Promise<T>;
46+
return { promise, callback: callback! };
2447
}

0 commit comments

Comments
 (0)