Skip to content

Commit 6e5d108

Browse files
LED service support (#27)
1 parent 7762012 commit 6e5d108

File tree

5 files changed

+250
-1
lines changed

5 files changed

+250
-1
lines changed

lib/bluetooth-device-wrapper.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { AccelerometerService } from "./accelerometer-service.js";
88
import { profile } from "./bluetooth-profile.js";
99
import { ButtonService } from "./button-service.js";
1010
import { BoardVersion, DeviceError } from "./device.js";
11+
import { LedService } from "./led-service.js";
1112
import { Logging, NullLogging } from "./logging.js";
1213
import { PromiseQueue } from "./promise-queue.js";
1314
import {
@@ -114,7 +115,9 @@ export class BluetoothDeviceWrapper {
114115
"buttonachanged",
115116
"buttonbchanged",
116117
]);
117-
private serviceInfo = [this.accelerometer, this.buttons];
118+
private led = new ServiceInfo(LedService.createService, []);
119+
120+
private serviceInfo = [this.accelerometer, this.buttons, this.led];
118121

119122
boardVersion: BoardVersion | undefined;
120123

@@ -376,6 +379,10 @@ export class BluetoothDeviceWrapper {
376379
return this.createIfNeeded(this.accelerometer, false);
377380
}
378381

382+
async getLedService(): Promise<LedService | undefined> {
383+
return this.createIfNeeded(this.led, false);
384+
}
385+
379386
async startNotifications(type: TypedServiceEvent) {
380387
const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
381388
if (serviceInfo) {

lib/bluetooth.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
DeviceConnectionEventMap,
2020
} from "./device.js";
2121
import { TypedEventTarget } from "./events.js";
22+
import { LedMatrix } from "./led.js";
2223
import { Logging, NullLogging } from "./logging.js";
2324
import {
2425
ServiceConnectionEventMap,
@@ -244,4 +245,29 @@ export class MicrobitWebBluetoothConnection
244245
await this.connection?.getAccelerometerService();
245246
return accelerometerService?.setPeriod(value);
246247
}
248+
249+
async setLedText(text: string): Promise<void> {
250+
const ledService = await this.connection?.getLedService();
251+
return ledService?.setText(text);
252+
}
253+
254+
async getLedScrollingDelay(): Promise<number | undefined> {
255+
const ledService = await this.connection?.getLedService();
256+
return ledService?.getScrollingDelay();
257+
}
258+
259+
async setLedScrollingDelay(delayInMillis: number): Promise<void> {
260+
const ledService = await this.connection?.getLedService();
261+
await ledService?.setScrollingDelay(delayInMillis);
262+
}
263+
264+
async getLedMatrix(): Promise<LedMatrix | undefined> {
265+
const ledService = await this.connection?.getLedService();
266+
return ledService?.getLedMatrix();
267+
}
268+
269+
async setLedMatrix(matrix: LedMatrix): Promise<void> {
270+
const ledService = await this.connection?.getLedService();
271+
ledService?.setLedMatrix(matrix);
272+
}
247273
}

lib/led-service.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { Service } from "./bluetooth-device-wrapper.js";
2+
import { profile } from "./bluetooth-profile.js";
3+
import { BackgroundErrorEvent, DeviceError } from "./device.js";
4+
import { LedMatrix } from "./led.js";
5+
import {
6+
TypedServiceEvent,
7+
TypedServiceEventDispatcher,
8+
} from "./service-events.js";
9+
10+
const createLedMatrix = (): LedMatrix => {
11+
return [
12+
[false, false, false, false, false],
13+
[false, false, false, false, false],
14+
[false, false, false, false, false],
15+
[false, false, false, false, false],
16+
[false, false, false, false, false],
17+
];
18+
};
19+
20+
export class LedService implements Service {
21+
constructor(
22+
private matrixStateCharacteristic: BluetoothRemoteGATTCharacteristic,
23+
private scrollingDelayCharacteristic: BluetoothRemoteGATTCharacteristic,
24+
private textCharactertistic: BluetoothRemoteGATTCharacteristic,
25+
private queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
26+
) {}
27+
28+
static async createService(
29+
gattServer: BluetoothRemoteGATTServer,
30+
dispatcher: TypedServiceEventDispatcher,
31+
queueGattOperation: <R>(action: () => Promise<R>) => Promise<R>,
32+
listenerInit: boolean,
33+
): Promise<LedService | undefined> {
34+
let ledService: BluetoothRemoteGATTService;
35+
try {
36+
ledService = await gattServer.getPrimaryService(profile.led.id);
37+
} catch (err) {
38+
if (listenerInit) {
39+
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
40+
return;
41+
} else {
42+
throw new DeviceError({
43+
code: "service-missing",
44+
message: err as string,
45+
});
46+
}
47+
}
48+
const matrixStateCharacteristic = await ledService.getCharacteristic(
49+
profile.led.characteristics.matrixState.id,
50+
);
51+
const scrollingDelayCharacteristic = await ledService.getCharacteristic(
52+
profile.led.characteristics.scrollingDelay.id,
53+
);
54+
const textCharacteristic = await ledService.getCharacteristic(
55+
profile.led.characteristics.text.id,
56+
);
57+
return new LedService(
58+
matrixStateCharacteristic,
59+
scrollingDelayCharacteristic,
60+
textCharacteristic,
61+
queueGattOperation,
62+
);
63+
}
64+
65+
async getLedMatrix(): Promise<LedMatrix> {
66+
const dataView = await this.queueGattOperation(() =>
67+
this.matrixStateCharacteristic.readValue(),
68+
);
69+
return this.dataViewToLedMatrix(dataView);
70+
}
71+
72+
async setLedMatrix(value: LedMatrix): Promise<void> {
73+
const dataView = this.ledMatrixToDataView(value);
74+
return this.queueGattOperation(() =>
75+
this.matrixStateCharacteristic.writeValue(dataView),
76+
);
77+
}
78+
79+
private dataViewToLedMatrix(dataView: DataView): LedMatrix {
80+
if (dataView.byteLength !== 5) {
81+
throw new Error("Unexpected LED matrix byte length");
82+
}
83+
const matrix = createLedMatrix();
84+
for (let row = 0; row < 5; ++row) {
85+
const rowByte = dataView.getUint8(row);
86+
for (let column = 0; column < 5; ++column) {
87+
const columnMask = 0x1 << (4 - column);
88+
matrix[row][column] = (rowByte & columnMask) != 0;
89+
}
90+
}
91+
return matrix;
92+
}
93+
94+
private ledMatrixToDataView(matrix: LedMatrix): DataView {
95+
const dataView = new DataView(new ArrayBuffer(5));
96+
for (let row = 0; row < 5; ++row) {
97+
let rowByte = 0;
98+
for (let column = 0; column < 5; ++column) {
99+
const columnMask = 0x1 << (4 - column);
100+
if (matrix[row][column]) {
101+
rowByte |= columnMask;
102+
}
103+
}
104+
dataView.setUint8(row, rowByte);
105+
}
106+
return dataView;
107+
}
108+
109+
async setText(text: string) {
110+
const bytes = new TextEncoder().encode(text);
111+
if (bytes.length > 20) {
112+
throw new Error("Text must be <= 20 bytes when encoded as UTF-8");
113+
}
114+
return this.queueGattOperation(() =>
115+
this.textCharactertistic.writeValue(bytes),
116+
);
117+
}
118+
119+
async setScrollingDelay(value: number) {
120+
const dataView = new DataView(new ArrayBuffer(2));
121+
dataView.setUint16(0, value, true);
122+
return this.queueGattOperation(() =>
123+
this.scrollingDelayCharacteristic.writeValue(dataView),
124+
);
125+
}
126+
127+
async getScrollingDelay(): Promise<number> {
128+
const dataView = await this.queueGattOperation(() =>
129+
this.scrollingDelayCharacteristic.readValue(),
130+
);
131+
return dataView.getUint16(0, true);
132+
}
133+
134+
async startNotifications(type: TypedServiceEvent): Promise<void> {}
135+
136+
async stopNotifications(type: TypedServiceEvent): Promise<void> {}
137+
}

lib/led.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type FixedArray<T, L extends number> = T[] & { length: L };
2+
type LedRow = FixedArray<boolean, 5>;
3+
export type LedMatrix = FixedArray<LedRow, 5>;

src/demo.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const recreateUi = async (type: ConnectionType) => {
6767
createButtonSection("A", "buttonachanged"),
6868
createButtonSection("B", "buttonbchanged"),
6969
createAccelerometerSection(),
70+
createLedSection(),
7071
].forEach(({ dom, cleanup }) => {
7172
if (dom) {
7273
document.body.appendChild(dom);
@@ -385,3 +386,78 @@ const createButtonSection = (
385386
},
386387
};
387388
};
389+
390+
const createLedSection = (): Section => {
391+
if (!(connection instanceof MicrobitWebBluetoothConnection)) {
392+
return {};
393+
}
394+
const ledConnection = connection;
395+
396+
const delayInput = crelt("input") as HTMLInputElement;
397+
const textInput = crelt("input") as HTMLInputElement;
398+
const matrixInput = crelt("textarea") as HTMLTextAreaElement;
399+
const dom = crelt("section", crelt("h2", "LED"), [
400+
crelt("h3", "Matrix"),
401+
crelt("h3", "Text"),
402+
crelt("label", "Text", textInput),
403+
crelt(
404+
"button",
405+
{
406+
onclick: async () => {
407+
await ledConnection.setLedText(textInput.value);
408+
},
409+
},
410+
"Set text",
411+
),
412+
crelt("h3", "Scrolling delay"),
413+
crelt("label", "Scrolling delay", delayInput),
414+
crelt(
415+
"button",
416+
{
417+
onclick: async () => {
418+
const value = await ledConnection.getLedScrollingDelay();
419+
if (value) {
420+
delayInput.value = value.toString();
421+
}
422+
},
423+
},
424+
"Get scrolling delay",
425+
),
426+
crelt(
427+
"button",
428+
{
429+
onclick: async () => {
430+
await ledConnection.setLedScrollingDelay(parseInt(delayInput.value));
431+
},
432+
},
433+
"Set scrolling delay",
434+
),
435+
crelt("h3", "Matrix"),
436+
crelt("label", "Matrix as JSON", matrixInput),
437+
crelt(
438+
"button",
439+
{
440+
onclick: async () => {
441+
const matrix = await ledConnection.getLedMatrix();
442+
matrixInput.value = JSON.stringify(matrix, null, 2);
443+
},
444+
},
445+
"Get matrix",
446+
),
447+
crelt(
448+
"button",
449+
{
450+
onclick: async () => {
451+
const matrix = JSON.parse(matrixInput.value);
452+
await ledConnection.setLedMatrix(matrix);
453+
},
454+
},
455+
"Set matrix",
456+
),
457+
]);
458+
459+
return {
460+
dom,
461+
cleanup: () => {},
462+
};
463+
};

0 commit comments

Comments
 (0)