@intomind/sdk 0.0.0-stage → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLA.md +71 -0
- package/CONTRIBUTING.md +53 -0
- package/LICENSE +662 -0
- package/LICENSING.md +45 -0
- package/README.md +147 -2
- package/dist/digest.d.ts +22 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +109 -0
- package/dist/digest.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +1433 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2227 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session.d.ts +284 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +523 -0
- package/dist/session.js.map +1 -0
- package/dist/timebase.d.ts +65 -0
- package/dist/timebase.d.ts.map +1 -0
- package/dist/timebase.js +114 -0
- package/dist/timebase.js.map +1 -0
- package/dist/transfer.d.ts +139 -0
- package/dist/transfer.d.ts.map +1 -0
- package/dist/transfer.js +269 -0
- package/dist/transfer.js.map +1 -0
- package/dist/web-bluetooth.d.ts +172 -0
- package/dist/web-bluetooth.d.ts.map +1 -0
- package/dist/web-bluetooth.js +310 -0
- package/dist/web-bluetooth.js.map +1 -0
- package/package.json +74 -4
- package/src/digest.ts +115 -0
- package/src/index.ts +103 -0
- package/src/protocol.ts +2987 -0
- package/src/session.ts +675 -0
- package/src/timebase.ts +138 -0
- package/src/transfer.ts +313 -0
- package/src/web-bluetooth.ts +419 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser transport.
|
|
3
|
+
*
|
|
4
|
+
* This is the only file that touches `navigator.bluetooth`. Everything
|
|
5
|
+
* else in this package is bytes in and events out, so the protocol, the
|
|
6
|
+
* session, and the timebase are all testable without a browser and without
|
|
7
|
+
* a device.
|
|
8
|
+
*
|
|
9
|
+
* It stays thin on purpose: find a device, connect, resolve the
|
|
10
|
+
* characteristics, subscribe, and hand every notification to the session.
|
|
11
|
+
* What the notifications mean is the session's business.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
DEVICE_INFO,
|
|
16
|
+
FILL,
|
|
17
|
+
OPCODES,
|
|
18
|
+
SERVICE,
|
|
19
|
+
uuid,
|
|
20
|
+
type DeviceInfo,
|
|
21
|
+
type Response,
|
|
22
|
+
} from "./protocol.ts";
|
|
23
|
+
import { Session, type Command, type Event as SessionEvent } from "./session.ts";
|
|
24
|
+
import type { Fit } from "./timebase.ts";
|
|
25
|
+
import type { Transfer } from "./transfer.ts";
|
|
26
|
+
|
|
27
|
+
// The Web Bluetooth surface this file uses, and no more. Declaring it here
|
|
28
|
+
// rather than depending on a types package keeps the dependency count at
|
|
29
|
+
// zero and states exactly what the transport touches.
|
|
30
|
+
|
|
31
|
+
/** The part of a GATT characteristic this transport uses. */
|
|
32
|
+
export interface BluetoothRemoteGATTCharacteristicLike extends EventTarget {
|
|
33
|
+
/** The characteristic's full identifier. */
|
|
34
|
+
readonly uuid: string;
|
|
35
|
+
/** The last value read or notified. */
|
|
36
|
+
readonly value?: DataView;
|
|
37
|
+
/** Read the current value. */
|
|
38
|
+
readValue(): Promise<DataView>;
|
|
39
|
+
/** Write a value and wait for the device's acknowledgement. */
|
|
40
|
+
writeValueWithResponse(value: BufferSource): Promise<void>;
|
|
41
|
+
/** Write a value without waiting for an acknowledgement. */
|
|
42
|
+
writeValueWithoutResponse(value: BufferSource): Promise<void>;
|
|
43
|
+
/** Subscribe to value-changed notifications. */
|
|
44
|
+
startNotifications(): Promise<BluetoothRemoteGATTCharacteristicLike>;
|
|
45
|
+
/** Unsubscribe from value-changed notifications. */
|
|
46
|
+
stopNotifications(): Promise<BluetoothRemoteGATTCharacteristicLike>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The part of a GATT service this transport uses. */
|
|
50
|
+
export interface BluetoothRemoteGATTServiceLike {
|
|
51
|
+
/** Resolve one of the service's characteristics by its full identifier. */
|
|
52
|
+
getCharacteristic(uuid: string): Promise<BluetoothRemoteGATTCharacteristicLike>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The part of a GATT server this transport uses. */
|
|
56
|
+
export interface BluetoothRemoteGATTServerLike {
|
|
57
|
+
/** Whether the link is currently connected. */
|
|
58
|
+
readonly connected: boolean;
|
|
59
|
+
/** Open the link. */
|
|
60
|
+
connect(): Promise<BluetoothRemoteGATTServerLike>;
|
|
61
|
+
/** Drop the link. */
|
|
62
|
+
disconnect(): void;
|
|
63
|
+
/** Resolve the device's primary service by its full identifier. */
|
|
64
|
+
getPrimaryService(uuid: string): Promise<BluetoothRemoteGATTServiceLike>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The part of a chosen Bluetooth device this transport uses. */
|
|
68
|
+
export interface BluetoothDeviceLike extends EventTarget {
|
|
69
|
+
/** The browser's identifier for this device. */
|
|
70
|
+
readonly id: string;
|
|
71
|
+
/** The device's advertised name, when known. */
|
|
72
|
+
readonly name?: string;
|
|
73
|
+
/** The device's GATT server, when it has one. */
|
|
74
|
+
readonly gatt?: BluetoothRemoteGATTServerLike;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The part of `navigator.bluetooth` this transport uses. */
|
|
78
|
+
export interface BluetoothLike {
|
|
79
|
+
/** Put the browser's device chooser up. */
|
|
80
|
+
requestDevice(options: {
|
|
81
|
+
filters?: Array<{ services?: string[]; namePrefix?: string }>;
|
|
82
|
+
optionalServices?: string[];
|
|
83
|
+
acceptAllDevices?: boolean;
|
|
84
|
+
}): Promise<BluetoothDeviceLike>;
|
|
85
|
+
/** Whether this browser has Bluetooth available at all, when the browser reports it. */
|
|
86
|
+
getAvailability?(): Promise<boolean>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Thrown when the page cannot reach Web Bluetooth at all. */
|
|
90
|
+
export class NoBluetooth extends Error {
|
|
91
|
+
constructor() {
|
|
92
|
+
super("this page has no Web Bluetooth: it needs a supporting browser, a secure context, and a user gesture");
|
|
93
|
+
this.name = "NoBluetooth";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function bluetooth(): BluetoothLike {
|
|
98
|
+
const nav = (globalThis as { navigator?: { bluetooth?: BluetoothLike } }).navigator;
|
|
99
|
+
if (nav?.bluetooth === undefined) throw new NoBluetooth();
|
|
100
|
+
return nav.bluetooth;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The clock the exchanges are read on, in seconds.
|
|
105
|
+
*
|
|
106
|
+
* Monotonic where the runtime has one, because the fit between the two
|
|
107
|
+
* clocks is a line and a wall clock step would bend it. A caller that puts
|
|
108
|
+
* its own events on the same ruler reads this same function.
|
|
109
|
+
*/
|
|
110
|
+
export function hostClock(): number {
|
|
111
|
+
const p = (globalThis as { performance?: { now(): number; timeOrigin?: number } }).performance;
|
|
112
|
+
if (p !== undefined) return (p.timeOrigin ?? 0) / 1000 + p.now() / 1000;
|
|
113
|
+
return Date.now() / 1000;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Options for `requestDevice`. */
|
|
117
|
+
export interface RequestOptions {
|
|
118
|
+
/** Narrow the chooser further, for instance to one unit on a bench. */
|
|
119
|
+
namePrefix?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Put the browser's device chooser up, filtered to devices that advertise
|
|
124
|
+
* the IntoMind service. This has to be called from a user gesture.
|
|
125
|
+
*/
|
|
126
|
+
export function requestDevice(options: RequestOptions = {}): Promise<BluetoothDeviceLike> {
|
|
127
|
+
const filter: { services: string[]; namePrefix?: string } = { services: [SERVICE] };
|
|
128
|
+
if (options.namePrefix !== undefined) filter.namePrefix = options.namePrefix;
|
|
129
|
+
return bluetooth().requestDevice({ filters: [filter], optionalServices: [SERVICE] });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Options for `connect`. */
|
|
133
|
+
export interface ConnectOptions {
|
|
134
|
+
/** Drive an existing session rather than a new one. */
|
|
135
|
+
session?: Session;
|
|
136
|
+
/** Called with every event, in the order the session produced them. */
|
|
137
|
+
onEvent?: (event: SessionEvent) => void;
|
|
138
|
+
/** Called when the link drops, for whatever reason. */
|
|
139
|
+
onDisconnect?: () => void;
|
|
140
|
+
/** How long a control request waits for its answer. */
|
|
141
|
+
timeoutMs?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** A live link to one device. */
|
|
145
|
+
export class Connection {
|
|
146
|
+
/** The device this link is to. */
|
|
147
|
+
readonly device: BluetoothDeviceLike;
|
|
148
|
+
/** The session reading this device's notifications. */
|
|
149
|
+
readonly session: Session;
|
|
150
|
+
/** What the device said it is, read before anything else was asked of it. */
|
|
151
|
+
readonly info: DeviceInfo;
|
|
152
|
+
|
|
153
|
+
readonly #server: BluetoothRemoteGATTServerLike;
|
|
154
|
+
readonly #characteristics: Map<number, BluetoothRemoteGATTCharacteristicLike>;
|
|
155
|
+
readonly #listeners = new Set<(event: SessionEvent) => void>();
|
|
156
|
+
readonly #waiting: Array<{ opcode: number; settle: (answer: Response, at: number) => void }> = [];
|
|
157
|
+
/** Whoever is waiting for the next update answer, in order. */
|
|
158
|
+
readonly #updateWaiting: Array<(bytes: Uint8Array) => void> = [];
|
|
159
|
+
readonly #timeoutMs: number;
|
|
160
|
+
/**
|
|
161
|
+
* Verifying a finished image takes the device longer than answering a
|
|
162
|
+
* command, because it hashes and checks a signature over the whole of
|
|
163
|
+
* it before it says anything.
|
|
164
|
+
*/
|
|
165
|
+
readonly #updateTimeoutMs = 30_000;
|
|
166
|
+
|
|
167
|
+
/** Build one from an open server and the characteristics `connect` resolved. */
|
|
168
|
+
constructor(f: {
|
|
169
|
+
device: BluetoothDeviceLike;
|
|
170
|
+
server: BluetoothRemoteGATTServerLike;
|
|
171
|
+
session: Session;
|
|
172
|
+
info: DeviceInfo;
|
|
173
|
+
characteristics: Map<number, BluetoothRemoteGATTCharacteristicLike>;
|
|
174
|
+
timeoutMs: number;
|
|
175
|
+
}) {
|
|
176
|
+
this.device = f.device;
|
|
177
|
+
this.session = f.session;
|
|
178
|
+
this.info = f.info;
|
|
179
|
+
this.#server = f.server;
|
|
180
|
+
this.#characteristics = f.characteristics;
|
|
181
|
+
this.#timeoutMs = f.timeoutMs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether the link is currently connected. */
|
|
185
|
+
get connected(): boolean {
|
|
186
|
+
return this.#server.connected;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Take every event. The returned function stops taking them. */
|
|
190
|
+
on(listener: (event: SessionEvent) => void): () => void {
|
|
191
|
+
this.#listeners.add(listener);
|
|
192
|
+
return () => this.#listeners.delete(listener);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Write a command. The session says which characteristic it goes to. */
|
|
196
|
+
async send(command: Command): Promise<void> {
|
|
197
|
+
const c = this.#characteristics.get(command.characteristic);
|
|
198
|
+
if (c === undefined) {
|
|
199
|
+
throw new Error(`this device has no characteristic ${uuid(command.characteristic)}`);
|
|
200
|
+
}
|
|
201
|
+
const bytes = command.bytes.slice();
|
|
202
|
+
if (command.withResponse) await c.writeValueWithResponse(bytes);
|
|
203
|
+
else await c.writeValueWithoutResponse(bytes);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Write a command and wait for the device's answer to it. */
|
|
207
|
+
async request(command: Command): Promise<Response> {
|
|
208
|
+
const answer = this.#await(command.bytes[0]!);
|
|
209
|
+
await this.send(command);
|
|
210
|
+
return (await answer).answer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* One time exchange, with the host clock read either side of it. The
|
|
215
|
+
* device's answer is known to lie between those two readings, which is
|
|
216
|
+
* the whole uncertainty in this one exchange.
|
|
217
|
+
*/
|
|
218
|
+
async timeSync(): Promise<Fit | null> {
|
|
219
|
+
const waiting = this.#await(OPCODES.time_sync);
|
|
220
|
+
const before = hostClock();
|
|
221
|
+
await this.send(this.session.timeSync());
|
|
222
|
+
const { answer, at } = await waiting;
|
|
223
|
+
return this.session.onTimeSync(answer.payload, before, at);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Carry an image or a head to the device and have it verified there.
|
|
228
|
+
*
|
|
229
|
+
* Nothing is activated by this. Use `activate` afterwards, and expect
|
|
230
|
+
* the link to drop when you do.
|
|
231
|
+
*
|
|
232
|
+
* ```javascript
|
|
233
|
+
* const t = Transfer.head(link.session, 1, blob);
|
|
234
|
+
* const result = await link.transfer(t, blob, ({ taken, total }) => {
|
|
235
|
+
* console.log(`${taken} of ${total}`);
|
|
236
|
+
* });
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
async transfer(
|
|
240
|
+
transfer: Transfer,
|
|
241
|
+
image: Uint8Array,
|
|
242
|
+
onProgress?: (progress: { taken: number; total: number }) => void,
|
|
243
|
+
): Promise<string | null> {
|
|
244
|
+
try {
|
|
245
|
+
for (;;) {
|
|
246
|
+
const step = transfer.step(image);
|
|
247
|
+
if (step.type === "verified") return step.result;
|
|
248
|
+
if (step.type === "send") {
|
|
249
|
+
const answer = this.#awaitUpdate();
|
|
250
|
+
await this.send(step.command);
|
|
251
|
+
transfer.onAnswer(await answer, image);
|
|
252
|
+
onProgress?.(transfer.progress);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
await this.send({
|
|
256
|
+
characteristic: step.characteristic,
|
|
257
|
+
bytes: image.subarray(step.from, step.to),
|
|
258
|
+
withResponse: false,
|
|
259
|
+
});
|
|
260
|
+
transfer.sent(step.to - step.from);
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// A transfer that stopped leaves the device holding a part of
|
|
264
|
+
// something. Tell it to drop that, then report what went wrong.
|
|
265
|
+
try {
|
|
266
|
+
await this.send(transfer.abort());
|
|
267
|
+
this.#updateWaiting.shift();
|
|
268
|
+
} catch {
|
|
269
|
+
// The link is what failed. Nothing further to say to it.
|
|
270
|
+
}
|
|
271
|
+
throw e;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* End every subscription this link started, then drop the link. The
|
|
277
|
+
* order matters: on some platforms the browser keeps a subscribed link
|
|
278
|
+
* open after a disconnect, and the next program on the same computer
|
|
279
|
+
* then hears every packet twice.
|
|
280
|
+
*/
|
|
281
|
+
async disconnect(): Promise<void> {
|
|
282
|
+
for (const fill of NOTIFYING) {
|
|
283
|
+
const c = this.#characteristics.get(fill);
|
|
284
|
+
if (c === undefined) continue;
|
|
285
|
+
try {
|
|
286
|
+
await c.stopNotifications();
|
|
287
|
+
} catch {
|
|
288
|
+
// A link that is already gone has nothing left to stop.
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (this.#server.connected) this.#server.disconnect();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#awaitUpdate(): Promise<Uint8Array> {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
const timer = setTimeout(() => {
|
|
297
|
+
const i = this.#updateWaiting.indexOf(settle);
|
|
298
|
+
if (i >= 0) this.#updateWaiting.splice(i, 1);
|
|
299
|
+
reject(new Error("the device did not answer the update service in time"));
|
|
300
|
+
}, this.#updateTimeoutMs);
|
|
301
|
+
const settle = (bytes: Uint8Array) => {
|
|
302
|
+
clearTimeout(timer);
|
|
303
|
+
resolve(bytes);
|
|
304
|
+
};
|
|
305
|
+
this.#updateWaiting.push(settle);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#await(opcode: number): Promise<{ answer: Response; at: number }> {
|
|
310
|
+
return new Promise((resolve, reject) => {
|
|
311
|
+
const waiter = {
|
|
312
|
+
opcode,
|
|
313
|
+
settle: (answer: Response, at: number) => {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
resolve({ answer, at });
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const timer = setTimeout(() => {
|
|
319
|
+
const i = this.#waiting.indexOf(waiter);
|
|
320
|
+
if (i >= 0) this.#waiting.splice(i, 1);
|
|
321
|
+
reject(new Error(`the device did not answer opcode 0x${opcode.toString(16)} in time`));
|
|
322
|
+
}, this.#timeoutMs);
|
|
323
|
+
this.#waiting.push(waiter);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** @internal Called by `connect` for every notification that arrives. */
|
|
328
|
+
deliver(characteristic: number, bytes: Uint8Array): void {
|
|
329
|
+
const at = hostClock();
|
|
330
|
+
if (characteristic === FILL.UPDATE_CONTROL) {
|
|
331
|
+
// An update answer is not a session event. It goes to whichever
|
|
332
|
+
// transfer asked for it.
|
|
333
|
+
const settle = this.#updateWaiting.shift();
|
|
334
|
+
if (settle !== undefined) settle(bytes);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
for (const event of this.session.onNotification(characteristic, bytes)) {
|
|
338
|
+
if (event.type === "answer") {
|
|
339
|
+
const i = this.#waiting.findIndex((w) => w.opcode === event.answer.opcode);
|
|
340
|
+
if (i >= 0) this.#waiting.splice(i, 1)[0]!.settle(event.answer, at);
|
|
341
|
+
}
|
|
342
|
+
for (const listener of this.#listeners) listener(event);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The characteristics this transport subscribes to, in the order it tries them. */
|
|
348
|
+
const NOTIFYING = [
|
|
349
|
+
FILL.EEG_DATA,
|
|
350
|
+
FILL.STATUS,
|
|
351
|
+
FILL.CONTROL_RESPONSE,
|
|
352
|
+
FILL.PREDICTIONS,
|
|
353
|
+
// The update service answers on its own characteristic, by indication.
|
|
354
|
+
// Without this subscription a transfer writes into silence.
|
|
355
|
+
FILL.UPDATE_CONTROL,
|
|
356
|
+
];
|
|
357
|
+
const WRITABLE = [FILL.CONTROL, FILL.UPDATE_CONTROL, FILL.UPDATE_DATA];
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Connect to a device the user chose, read what it is, and subscribe to
|
|
361
|
+
* everything it notifies.
|
|
362
|
+
*/
|
|
363
|
+
export async function connect(device: BluetoothDeviceLike, options: ConnectOptions = {}): Promise<Connection> {
|
|
364
|
+
if (device.gatt === undefined) throw new NoBluetooth();
|
|
365
|
+
const server = await device.gatt.connect();
|
|
366
|
+
const service = await server.getPrimaryService(SERVICE);
|
|
367
|
+
|
|
368
|
+
const session = options.session ?? new Session();
|
|
369
|
+
const infoCharacteristic = await service.getCharacteristic(DEVICE_INFO);
|
|
370
|
+
const info = session.onDeviceInfo(asBytes(await infoCharacteristic.readValue()));
|
|
371
|
+
|
|
372
|
+
const characteristics = new Map<number, BluetoothRemoteGATTCharacteristicLike>();
|
|
373
|
+
characteristics.set(FILL.DEVICE_INFO, infoCharacteristic);
|
|
374
|
+
// A device carries the characteristics its firmware offers, so one that
|
|
375
|
+
// is absent is not an error. What is absent shows up as a capability the
|
|
376
|
+
// device never claimed.
|
|
377
|
+
for (const fill of [...NOTIFYING, ...WRITABLE]) {
|
|
378
|
+
const c = await optional(service, fill);
|
|
379
|
+
if (c !== null) characteristics.set(fill, c);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const connection = new Connection({ device, server, session, info, characteristics, timeoutMs: options.timeoutMs ?? 5000 });
|
|
383
|
+
if (options.onEvent !== undefined) connection.on(options.onEvent);
|
|
384
|
+
|
|
385
|
+
for (const fill of NOTIFYING) {
|
|
386
|
+
const c = characteristics.get(fill);
|
|
387
|
+
if (c === undefined) continue;
|
|
388
|
+
c.addEventListener("characteristicvaluechanged", (event: Event) => {
|
|
389
|
+
const value = (event.target as BluetoothRemoteGATTCharacteristicLike).value;
|
|
390
|
+
if (value !== undefined) connection.deliver(fill, asBytes(value));
|
|
391
|
+
});
|
|
392
|
+
await c.startNotifications();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (options.onDisconnect !== undefined) {
|
|
396
|
+
device.addEventListener("gattserverdisconnected", options.onDisconnect, { once: true });
|
|
397
|
+
}
|
|
398
|
+
return connection;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Ask for a device and connect to it, which is what most callers want. */
|
|
402
|
+
export async function requestAndConnect(options: RequestOptions & ConnectOptions = {}): Promise<Connection> {
|
|
403
|
+
return connect(await requestDevice(options), options);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function optional(
|
|
407
|
+
service: BluetoothRemoteGATTServiceLike,
|
|
408
|
+
fill: number,
|
|
409
|
+
): Promise<BluetoothRemoteGATTCharacteristicLike | null> {
|
|
410
|
+
try {
|
|
411
|
+
return await service.getCharacteristic(uuid(fill));
|
|
412
|
+
} catch {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function asBytes(value: DataView): Uint8Array {
|
|
418
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
419
|
+
}
|