@microbit/microbit-connection 0.1.0 → 0.9.0-apps.alpha.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.
Files changed (148) hide show
  1. package/README.md +4 -4
  2. package/build/cjs/accelerometer-service.d.ts +5 -5
  3. package/build/cjs/accelerometer-service.js +41 -42
  4. package/build/cjs/accelerometer-service.js.map +1 -1
  5. package/build/cjs/async-util.d.ts +9 -0
  6. package/build/cjs/async-util.js +27 -7
  7. package/build/cjs/async-util.js.map +1 -1
  8. package/build/cjs/bluetooth-device-wrapper.d.ts +61 -27
  9. package/build/cjs/bluetooth-device-wrapper.js +285 -266
  10. package/build/cjs/bluetooth-device-wrapper.js.map +1 -1
  11. package/build/cjs/bluetooth-profile.d.ts +43 -35
  12. package/build/cjs/bluetooth-profile.js +35 -29
  13. package/build/cjs/bluetooth-profile.js.map +1 -1
  14. package/build/cjs/bluetooth.d.ts +10 -6
  15. package/build/cjs/bluetooth.js +286 -100
  16. package/build/cjs/bluetooth.js.map +1 -1
  17. package/build/cjs/button-service.d.ts +5 -5
  18. package/build/cjs/button-service.js +31 -45
  19. package/build/cjs/button-service.js.map +1 -1
  20. package/build/cjs/device-information-service.d.ts +6 -0
  21. package/build/cjs/device-information-service.js +34 -0
  22. package/build/cjs/device-information-service.js.map +1 -0
  23. package/build/cjs/device.d.ts +65 -8
  24. package/build/cjs/device.js +16 -2
  25. package/build/cjs/device.js.map +1 -1
  26. package/build/cjs/dfu-service.d.ts +9 -0
  27. package/build/cjs/dfu-service.js +26 -0
  28. package/build/cjs/dfu-service.js.map +1 -0
  29. package/build/cjs/flashing/flashing-full.d.ts +14 -0
  30. package/build/cjs/flashing/flashing-full.js +87 -0
  31. package/build/cjs/flashing/flashing-full.js.map +1 -0
  32. package/build/cjs/flashing/flashing-makecode.d.ts +6 -0
  33. package/build/cjs/flashing/flashing-makecode.js +48 -0
  34. package/build/cjs/flashing/flashing-makecode.js.map +1 -0
  35. package/build/cjs/flashing/flashing-partial.d.ts +9 -0
  36. package/build/cjs/flashing/flashing-partial.js +98 -0
  37. package/build/cjs/flashing/flashing-partial.js.map +1 -0
  38. package/build/cjs/flashing/flashing-v1.d.ts +11 -0
  39. package/build/cjs/flashing/flashing-v1.js +24 -0
  40. package/build/cjs/flashing/flashing-v1.js.map +1 -0
  41. package/build/cjs/flashing/nordic-dfu.d.ts +3 -0
  42. package/build/cjs/flashing/nordic-dfu.js +214 -0
  43. package/build/cjs/flashing/nordic-dfu.js.map +1 -0
  44. package/build/cjs/flashing/zip.d.ts +12 -0
  45. package/build/cjs/flashing/zip.js +177 -0
  46. package/build/cjs/flashing/zip.js.map +1 -0
  47. package/build/cjs/index.d.ts +3 -3
  48. package/build/cjs/index.js +2 -1
  49. package/build/cjs/index.js.map +1 -1
  50. package/build/cjs/led-service.d.ts +5 -7
  51. package/build/cjs/led-service.js +13 -44
  52. package/build/cjs/led-service.js.map +1 -1
  53. package/build/cjs/logging.d.ts +1 -1
  54. package/build/cjs/logging.js +3 -3
  55. package/build/cjs/logging.js.map +1 -1
  56. package/build/cjs/magnetometer-service.d.ts +5 -9
  57. package/build/cjs/magnetometer-service.js +30 -65
  58. package/build/cjs/magnetometer-service.js.map +1 -1
  59. package/build/cjs/partial-flashing-service.d.ts +45 -0
  60. package/build/cjs/partial-flashing-service.js +110 -0
  61. package/build/cjs/partial-flashing-service.js.map +1 -0
  62. package/build/cjs/uart-service.d.ts +4 -5
  63. package/build/cjs/uart-service.js +19 -40
  64. package/build/cjs/uart-service.js.map +1 -1
  65. package/build/cjs/usb-device-wrapper.js +33 -8
  66. package/build/cjs/usb-device-wrapper.js.map +1 -1
  67. package/build/cjs/usb-partial-flashing.d.ts +1 -3
  68. package/build/cjs/usb-partial-flashing.js +4 -3
  69. package/build/cjs/usb-partial-flashing.js.map +1 -1
  70. package/build/cjs/usb-radio-bridge.js +1 -2
  71. package/build/cjs/usb-radio-bridge.js.map +1 -1
  72. package/build/cjs/usb.d.ts +4 -2
  73. package/build/cjs/usb.js +31 -16
  74. package/build/cjs/usb.js.map +1 -1
  75. package/build/esm/accelerometer-service.d.ts +5 -5
  76. package/build/esm/accelerometer-service.js +42 -43
  77. package/build/esm/accelerometer-service.js.map +1 -1
  78. package/build/esm/async-util.d.ts +9 -0
  79. package/build/esm/async-util.js +22 -6
  80. package/build/esm/async-util.js.map +1 -1
  81. package/build/esm/bluetooth-device-wrapper.d.ts +61 -27
  82. package/build/esm/bluetooth-device-wrapper.js +284 -265
  83. package/build/esm/bluetooth-device-wrapper.js.map +1 -1
  84. package/build/esm/bluetooth-profile.d.ts +43 -35
  85. package/build/esm/bluetooth-profile.js +35 -29
  86. package/build/esm/bluetooth-profile.js.map +1 -1
  87. package/build/esm/bluetooth.d.ts +10 -6
  88. package/build/esm/bluetooth.js +263 -103
  89. package/build/esm/bluetooth.js.map +1 -1
  90. package/build/esm/button-service.d.ts +5 -5
  91. package/build/esm/button-service.js +32 -46
  92. package/build/esm/button-service.js.map +1 -1
  93. package/build/esm/device-information-service.d.ts +6 -0
  94. package/build/esm/device-information-service.js +30 -0
  95. package/build/esm/device-information-service.js.map +1 -0
  96. package/build/esm/device.d.ts +65 -8
  97. package/build/esm/device.js +15 -1
  98. package/build/esm/device.js.map +1 -1
  99. package/build/esm/dfu-service.d.ts +9 -0
  100. package/build/esm/dfu-service.js +22 -0
  101. package/build/esm/dfu-service.js.map +1 -0
  102. package/build/esm/flashing/flashing-full.d.ts +14 -0
  103. package/build/esm/flashing/flashing-full.js +84 -0
  104. package/build/esm/flashing/flashing-full.js.map +1 -0
  105. package/build/esm/flashing/flashing-makecode.d.ts +6 -0
  106. package/build/esm/flashing/flashing-makecode.js +44 -0
  107. package/build/esm/flashing/flashing-makecode.js.map +1 -0
  108. package/build/esm/flashing/flashing-partial.d.ts +9 -0
  109. package/build/esm/flashing/flashing-partial.js +95 -0
  110. package/build/esm/flashing/flashing-partial.js.map +1 -0
  111. package/build/esm/flashing/flashing-v1.d.ts +11 -0
  112. package/build/esm/flashing/flashing-v1.js +20 -0
  113. package/build/esm/flashing/flashing-v1.js.map +1 -0
  114. package/build/esm/flashing/nordic-dfu.d.ts +3 -0
  115. package/build/esm/flashing/nordic-dfu.js +211 -0
  116. package/build/esm/flashing/nordic-dfu.js.map +1 -0
  117. package/build/esm/flashing/zip.d.ts +12 -0
  118. package/build/esm/flashing/zip.js +174 -0
  119. package/build/esm/flashing/zip.js.map +1 -0
  120. package/build/esm/index.d.ts +3 -3
  121. package/build/esm/index.js +2 -2
  122. package/build/esm/index.js.map +1 -1
  123. package/build/esm/led-service.d.ts +5 -7
  124. package/build/esm/led-service.js +13 -44
  125. package/build/esm/led-service.js.map +1 -1
  126. package/build/esm/logging.d.ts +1 -1
  127. package/build/esm/logging.js +1 -1
  128. package/build/esm/logging.js.map +1 -1
  129. package/build/esm/magnetometer-service.d.ts +5 -9
  130. package/build/esm/magnetometer-service.js +31 -66
  131. package/build/esm/magnetometer-service.js.map +1 -1
  132. package/build/esm/partial-flashing-service.d.ts +45 -0
  133. package/build/esm/partial-flashing-service.js +106 -0
  134. package/build/esm/partial-flashing-service.js.map +1 -0
  135. package/build/esm/uart-service.d.ts +4 -5
  136. package/build/esm/uart-service.js +19 -40
  137. package/build/esm/uart-service.js.map +1 -1
  138. package/build/esm/usb-device-wrapper.js +33 -8
  139. package/build/esm/usb-device-wrapper.js.map +1 -1
  140. package/build/esm/usb-partial-flashing.d.ts +1 -3
  141. package/build/esm/usb-partial-flashing.js +4 -3
  142. package/build/esm/usb-partial-flashing.js.map +1 -1
  143. package/build/esm/usb-radio-bridge.js +2 -3
  144. package/build/esm/usb-radio-bridge.js.map +1 -1
  145. package/build/esm/usb.d.ts +4 -2
  146. package/build/esm/usb.js +33 -18
  147. package/build/esm/usb.js.map +1 -1
  148. package/package.json +7 -3
@@ -3,67 +3,45 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
+ import { BleClient, } from "@capacitor-community/bluetooth-le";
7
+ import { Capacitor } from "@capacitor/core";
6
8
  import { AccelerometerService } from "./accelerometer-service.js";
7
- import { profile } from "./bluetooth-profile.js";
9
+ import { delay, DisconnectError, disconnectErrorCallback, TimeoutError, timeoutErrorAfter, } from "./async-util.js";
8
10
  import { ButtonService } from "./button-service.js";
11
+ import { DeviceInformationService } from "./device-information-service.js";
9
12
  import { DeviceError } from "./device.js";
10
13
  import { LedService } from "./led-service.js";
11
- import { NullLogging } from "./logging.js";
14
+ import { ConsoleLogging } from "./logging.js";
12
15
  import { MagnetometerService } from "./magnetometer-service.js";
13
- import { PromiseQueue } from "./promise-queue.js";
16
+ import { MicroBitMode, PartialFlashingService, } from "./partial-flashing-service.js";
14
17
  import { UARTService } from "./uart-service.js";
15
- const deviceIdToWrapper = new Map();
16
- const connectTimeoutDuration = 10000;
17
- function findPlatform() {
18
- const navigator = typeof window !== "undefined" ? window.navigator : undefined;
19
- if (!navigator) {
20
- return "unknown";
21
- }
22
- const platform = navigator.userAgentData?.platform;
23
- if (platform) {
24
- return platform;
25
- }
26
- const isAndroid = /android/.test(navigator.userAgent.toLowerCase());
27
- return isAndroid ? "android" : navigator.platform ?? "unknown";
28
- }
29
- const platform = findPlatform();
30
- const isWindowsOS = platform && /^Win/.test(platform);
31
- class ServiceInfo {
32
- constructor(serviceFactory, events) {
33
- Object.defineProperty(this, "serviceFactory", {
34
- enumerable: true,
35
- configurable: true,
36
- writable: true,
37
- value: serviceFactory
38
- });
39
- Object.defineProperty(this, "events", {
40
- enumerable: true,
41
- configurable: true,
42
- writable: true,
43
- value: events
44
- });
45
- Object.defineProperty(this, "service", {
46
- enumerable: true,
47
- configurable: true,
48
- writable: true,
49
- value: void 0
50
- });
51
- }
52
- get() {
53
- return this.service;
54
- }
55
- async createIfNeeded(gattServer, dispatcher, queueGattOperation, listenerInit) {
56
- this.service =
57
- this.service ??
58
- (await this.serviceFactory(gattServer, dispatcher, queueGattOperation, listenerInit));
59
- return this.service;
60
- }
61
- dispose() {
62
- this.service = undefined;
63
- }
64
- }
18
+ export const bondingTimeoutInMs = 40_000;
19
+ export const connectTimeoutInMs = 10_000;
20
+ export const scanningTimeoutInMs = 10_000;
21
+ export const isAndroid = () => Capacitor.getPlatform() === "android";
22
+ // TODO: We've removed the support for these behaviours as we need to
23
+ // re-evaluate how best to support then via capacitor-ble (or reinstate
24
+ // the direct Web Bluetooth connection code.
25
+ //
26
+ // On ChromeOS and Mac there's no timeout and no clear way to abort
27
+ // device.gatt.connect(), so we accept that sometimes we'll still
28
+ // be trying to connect when we'd rather not be. If it succeeds when
29
+ // we no longer intend to be connected then we disconnect at that
30
+ // point. If we try to connect when a previous connection attempt is
31
+ // still around then we wait for it for our timeout period.
32
+ //
33
+ // On Windows it times out after 7s.
34
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=684073
35
+ //
36
+ // Additionally we've remove the delay before trying to connect again
37
+ // on Windows.
38
+ //
39
+ // We also used to have a timeout around requestDevice that reloaded the page.
40
+ //
41
+ // > In some situations the Chrome device prompt simply doesn't appear so we time
42
+ // > this out after 30 seconds and reload the page
65
43
  export class BluetoothDeviceWrapper {
66
- constructor(device, logging = new NullLogging(), dispatchTypedEvent, currentEvents, callbacks) {
44
+ constructor(device, logging = new ConsoleLogging(), dispatchTypedEvent, currentEvents, callbacks) {
67
45
  Object.defineProperty(this, "device", {
68
46
  enumerable: true,
69
47
  configurable: true,
@@ -76,12 +54,6 @@ export class BluetoothDeviceWrapper {
76
54
  writable: true,
77
55
  value: logging
78
56
  });
79
- Object.defineProperty(this, "dispatchTypedEvent", {
80
- enumerable: true,
81
- configurable: true,
82
- writable: true,
83
- value: dispatchTypedEvent
84
- });
85
57
  Object.defineProperty(this, "currentEvents", {
86
58
  enumerable: true,
87
59
  configurable: true,
@@ -102,138 +74,103 @@ export class BluetoothDeviceWrapper {
102
74
  writable: true,
103
75
  value: 0
104
76
  });
105
- // On ChromeOS and Mac there's no timeout and no clear way to abort
106
- // device.gatt.connect(), so we accept that sometimes we'll still
107
- // be trying to connect when we'd rather not be. If it succeeds when
108
- // we no longer intend to be connected then we disconnect at that
109
- // point. If we try to connect when a previous connection attempt is
110
- // still around then we wait for it for our timeout period.
111
- //
112
- // On Windows it times out after 7s.
113
- // https://bugs.chromium.org/p/chromium/issues/detail?id=684073
114
- Object.defineProperty(this, "gattConnectPromise", {
77
+ Object.defineProperty(this, "connected", {
115
78
  enumerable: true,
116
79
  configurable: true,
117
80
  writable: true,
118
- value: void 0
119
- });
120
- Object.defineProperty(this, "disconnectPromise", {
121
- enumerable: true,
122
- configurable: true,
123
- writable: true,
124
- value: void 0
81
+ value: false
125
82
  });
126
- Object.defineProperty(this, "connecting", {
83
+ Object.defineProperty(this, "isReconnect", {
127
84
  enumerable: true,
128
85
  configurable: true,
129
86
  writable: true,
130
87
  value: false
131
88
  });
132
- Object.defineProperty(this, "isReconnect", {
89
+ // Only updated after the full connection flow completes not during bond handling.
90
+ Object.defineProperty(this, "serviceIds", {
133
91
  enumerable: true,
134
92
  configurable: true,
135
93
  writable: true,
136
- value: false
94
+ value: new Set()
137
95
  });
138
- Object.defineProperty(this, "connectReadyPromise", {
96
+ Object.defineProperty(this, "accelerometer", {
139
97
  enumerable: true,
140
98
  configurable: true,
141
99
  writable: true,
142
100
  value: void 0
143
101
  });
144
- Object.defineProperty(this, "accelerometer", {
102
+ Object.defineProperty(this, "buttons", {
145
103
  enumerable: true,
146
104
  configurable: true,
147
105
  writable: true,
148
- value: new ServiceInfo(AccelerometerService.createService, [
149
- "accelerometerdatachanged",
150
- ])
106
+ value: void 0
151
107
  });
152
- Object.defineProperty(this, "buttons", {
108
+ Object.defineProperty(this, "deviceInformation", {
153
109
  enumerable: true,
154
110
  configurable: true,
155
111
  writable: true,
156
- value: new ServiceInfo(ButtonService.createService, [
157
- "buttonachanged",
158
- "buttonbchanged",
159
- ])
112
+ value: void 0
160
113
  });
161
114
  Object.defineProperty(this, "led", {
162
115
  enumerable: true,
163
116
  configurable: true,
164
117
  writable: true,
165
- value: new ServiceInfo(LedService.createService, [])
118
+ value: void 0
166
119
  });
167
120
  Object.defineProperty(this, "magnetometer", {
168
121
  enumerable: true,
169
122
  configurable: true,
170
123
  writable: true,
171
- value: new ServiceInfo(MagnetometerService.createService, [
172
- "magnetometerdatachanged",
173
- ])
124
+ value: void 0
174
125
  });
175
126
  Object.defineProperty(this, "uart", {
176
127
  enumerable: true,
177
128
  configurable: true,
178
129
  writable: true,
179
- value: new ServiceInfo(UARTService.createService, ["uartdata"])
130
+ value: void 0
180
131
  });
181
- Object.defineProperty(this, "serviceInfo", {
132
+ /**
133
+ * Only defined after connection.
134
+ */
135
+ Object.defineProperty(this, "boardVersion", {
182
136
  enumerable: true,
183
137
  configurable: true,
184
138
  writable: true,
185
- value: [
186
- this.accelerometer,
187
- this.buttons,
188
- this.led,
189
- this.magnetometer,
190
- this.uart,
191
- ]
139
+ value: void 0
192
140
  });
193
- Object.defineProperty(this, "boardVersion", {
141
+ Object.defineProperty(this, "services", {
194
142
  enumerable: true,
195
143
  configurable: true,
196
144
  writable: true,
197
145
  value: void 0
198
146
  });
199
- Object.defineProperty(this, "disconnectedRejectionErrorFactory", {
147
+ Object.defineProperty(this, "waitingForDisconnectEventCallbacks", {
200
148
  enumerable: true,
201
149
  configurable: true,
202
150
  writable: true,
203
- value: () => {
204
- return new DeviceError({
205
- code: "device-disconnected",
206
- message: "Error processing gatt operations queue - device disconnected",
207
- });
208
- }
151
+ value: []
209
152
  });
210
- Object.defineProperty(this, "gattOperations", {
153
+ Object.defineProperty(this, "internalNotificationListeners", {
211
154
  enumerable: true,
212
155
  configurable: true,
213
156
  writable: true,
214
- value: new PromiseQueue({
215
- abortCheck: () => {
216
- if (!this.device.gatt?.connected) {
217
- return this.disconnectedRejectionErrorFactory;
218
- }
219
- return undefined;
220
- },
221
- })
157
+ value: new Map()
222
158
  });
223
159
  Object.defineProperty(this, "handleDisconnectEvent", {
224
160
  enumerable: true,
225
161
  configurable: true,
226
162
  writable: true,
227
163
  value: async () => {
164
+ this.waitingForDisconnectEventCallbacks.forEach((cb) => cb());
165
+ this.waitingForDisconnectEventCallbacks.length = 0;
166
+ this.connected = false;
228
167
  try {
229
168
  if (!this.duringExplicitConnectDisconnect) {
230
- this.logging.log("Bluetooth GATT disconnected... automatically trying reconnect");
231
- // stateOnReconnectionAttempt();
232
- this.disposeServices();
169
+ this.logging.log("Bluetooth disconnected... automatically trying reconnect");
233
170
  await this.reconnect();
234
171
  }
235
172
  else {
236
- this.logging.log("Bluetooth GATT disconnect ignored during explicit disconnect");
173
+ this.logging.log("Bluetooth disconnect ignored during explicit disconnect");
237
174
  }
238
175
  }
239
176
  catch (e) {
@@ -241,21 +178,25 @@ export class BluetoothDeviceWrapper {
241
178
  }
242
179
  }
243
180
  });
244
- device.addEventListener("gattserverdisconnected", this.handleDisconnectEvent);
181
+ this.accelerometer = new AccelerometerService(device.deviceId, dispatchTypedEvent);
182
+ this.buttons = new ButtonService(device.deviceId, dispatchTypedEvent);
183
+ this.deviceInformation = new DeviceInformationService(device.deviceId);
184
+ this.led = new LedService(device.deviceId);
185
+ this.magnetometer = new MagnetometerService(device.deviceId, dispatchTypedEvent);
186
+ this.uart = new UARTService(device.deviceId, dispatchTypedEvent);
187
+ this.services = [
188
+ this.accelerometer,
189
+ this.buttons,
190
+ this.led,
191
+ this.magnetometer,
192
+ this.uart,
193
+ ];
245
194
  }
246
195
  async connect() {
247
196
  this.logging.event({
248
197
  type: this.isReconnect ? "Reconnect" : "Connect",
249
198
  message: "Bluetooth connect start",
250
199
  });
251
- if (this.duringExplicitConnectDisconnect) {
252
- this.logging.log("Skipping connect attempt when one is already in progress");
253
- // Wait for the gattConnectPromise while showing a "connecting" dialog.
254
- // If the user clicks disconnect while the automatic reconnect is in progress,
255
- // then clicks reconnect, we need to wait rather than return immediately.
256
- await this.gattConnectPromise;
257
- return;
258
- }
259
200
  if (this.isReconnect) {
260
201
  this.callbacks.onReconnecting();
261
202
  }
@@ -263,70 +204,19 @@ export class BluetoothDeviceWrapper {
263
204
  this.callbacks.onConnecting();
264
205
  }
265
206
  this.duringExplicitConnectDisconnect++;
266
- if (this.device.gatt === undefined) {
267
- throw new Error("BluetoothRemoteGATTServer for micro:bit device is undefined");
268
- }
269
- if (isWindowsOS) {
270
- // On Windows, the micro:bit can take around 3 seconds to respond to gatt.disconnect().
271
- // Attempting to connect/reconnect before the micro:bit has responded results in another
272
- // gattserverdisconnected event being fired. We then fail to get primaryService on a
273
- // disconnected GATT server.
274
- await this.connectReadyPromise;
275
- }
276
207
  try {
277
- // A previous connect might have completed in the background as a device was replugged etc.
278
- await this.disconnectPromise;
279
- this.gattConnectPromise =
280
- this.gattConnectPromise ??
281
- this.device.gatt
282
- .connect()
283
- .then(async () => {
284
- // We always do this even if we might immediately disconnect as disconnecting
285
- // without using services causes getPrimaryService calls to hang on subsequent
286
- // reconnect - probably a device-side issue.
287
- this.boardVersion = await this.getBoardVersion();
288
- // This connection could be arbitrarily later when our manual timeout may have passed.
289
- // Do we still want to be connected?
290
- if (!this.connecting) {
291
- this.logging.log("Bluetooth GATT server connect after timeout, triggering disconnect");
292
- this.disconnectPromise = (async () => {
293
- await this.disconnectInternal(false);
294
- this.disconnectPromise = undefined;
295
- })();
296
- }
297
- else {
298
- this.logging.log("Bluetooth GATT server connected when connecting");
299
- }
300
- })
301
- .catch((e) => {
302
- if (this.connecting) {
303
- // Error will be logged by main connect error handling.
304
- throw e;
305
- }
306
- else {
307
- this.logging.error("Bluetooth GATT server connect error after our timeout", e);
308
- return undefined;
309
- }
310
- })
311
- .finally(() => {
312
- this.logging.log("Bluetooth GATT server promise field cleared");
313
- this.gattConnectPromise = undefined;
314
- });
315
- this.connecting = true;
316
- try {
317
- const gattConnectResult = await Promise.race([
318
- this.gattConnectPromise,
319
- new Promise((resolve) => setTimeout(() => resolve("timeout"), connectTimeoutDuration)),
320
- ]);
321
- if (gattConnectResult === "timeout") {
322
- this.logging.log("Bluetooth GATT server connect timeout");
323
- throw new Error("Bluetooth GATT server connect timeout");
324
- }
208
+ if (Capacitor.isNativePlatform()) {
209
+ await this.connectHandlingBond();
325
210
  }
326
- finally {
327
- this.connecting = false;
211
+ else {
212
+ await this.connectInternal();
328
213
  }
329
- this.currentEvents().forEach((e) => this.startNotifications(e));
214
+ await this.getBoardVersion();
215
+ const events = this.currentEvents();
216
+ const services = await BleClient.getServices(this.device.deviceId);
217
+ this.serviceIds = new Set(services.map((s) => s.uuid));
218
+ this.logging.log(`Starting notifications for current events ${events}`);
219
+ events.forEach((e) => this.startNotifications(e));
330
220
  this.logging.event({
331
221
  type: this.isReconnect ? "Reconnect" : "Connect",
332
222
  message: "Bluetooth connect success",
@@ -341,7 +231,19 @@ export class BluetoothDeviceWrapper {
341
231
  });
342
232
  await this.disconnectInternal(false);
343
233
  this.callbacks.onFail();
344
- throw new Error("Failed to establish a connection!");
234
+ if (e instanceof DeviceError) {
235
+ throw e;
236
+ }
237
+ if (e instanceof TimeoutError) {
238
+ throw new DeviceError({
239
+ code: "timeout-error",
240
+ message: e instanceof Error ? e.message : String(e),
241
+ });
242
+ }
243
+ throw new DeviceError({
244
+ code: "bluetooth-connection-failed",
245
+ message: e instanceof Error ? e.message : String(e),
246
+ });
345
247
  }
346
248
  finally {
347
249
  this.duringExplicitConnectDisconnect--;
@@ -349,6 +251,13 @@ export class BluetoothDeviceWrapper {
349
251
  this.isReconnect = false;
350
252
  }
351
253
  }
254
+ async connectInternal() {
255
+ this.waitingForDisconnectEventCallbacks.length = 0;
256
+ await BleClient.connect(this.device.deviceId, this.handleDisconnectEvent, {
257
+ timeout: connectTimeoutInMs,
258
+ });
259
+ this.connected = true;
260
+ }
352
261
  async disconnect() {
353
262
  return this.disconnectInternal(true);
354
263
  }
@@ -356,8 +265,8 @@ export class BluetoothDeviceWrapper {
356
265
  this.logging.log(`Bluetooth disconnect ${userTriggered ? "(user triggered)" : "(programmatic)"}`);
357
266
  this.duringExplicitConnectDisconnect++;
358
267
  try {
359
- if (this.device.gatt?.connected) {
360
- this.device.gatt?.disconnect();
268
+ if (this.connected) {
269
+ await BleClient.disconnect(this.device.deviceId);
361
270
  }
362
271
  }
363
272
  catch (e) {
@@ -365,98 +274,208 @@ export class BluetoothDeviceWrapper {
365
274
  // We might have already lost the connection.
366
275
  }
367
276
  finally {
368
- this.disposeServices();
369
277
  this.duringExplicitConnectDisconnect--;
370
278
  }
371
- this.connectReadyPromise = new Promise((resolve) => setTimeout(resolve, 3_500));
372
279
  }
373
280
  async reconnect() {
374
281
  this.logging.log("Bluetooth reconnect");
375
282
  this.isReconnect = true;
376
283
  await this.connect();
377
284
  }
378
- assertGattServer() {
379
- if (!this.device.gatt?.connected) {
380
- throw new Error("Could not listen to services, no microbit connected!");
285
+ async getBoardVersion() {
286
+ // We read this when we connect and it won't change.
287
+ if (this.boardVersion) {
288
+ return this.boardVersion;
381
289
  }
382
- return this.device.gatt;
290
+ this.boardVersion = await this.deviceInformation.getBoardVersion();
291
+ return this.boardVersion;
383
292
  }
384
- async getBoardVersion() {
385
- this.assertGattServer();
386
- const serviceMeta = profile.deviceInformation;
293
+ async startNotifications(type) {
294
+ await this.getServicesForEvent(type)?.startNotifications(type);
295
+ }
296
+ async stopNotifications(type) {
297
+ await this.getServicesForEvent(type)?.stopNotifications(type);
298
+ }
299
+ getServicesForEvent(type) {
300
+ return this.services.find((s) => this.serviceIds.has(s.uuid) && s.getRelevantEvents().includes(type));
301
+ }
302
+ async startInternalNotifications(serviceId, characteristicId, options) {
303
+ const key = this.getNotificationKey(serviceId, characteristicId);
304
+ await this.raceDisconnectAndTimeout(BleClient.startNotifications(this.device.deviceId, serviceId, characteristicId, (value) => {
305
+ const bytes = new Uint8Array(value.buffer);
306
+ // Notify all registered callbacks.
307
+ this.internalNotificationListeners
308
+ .get(key)
309
+ ?.forEach((cb) => cb(bytes));
310
+ }, options), { actionName: "start notifications" });
311
+ }
312
+ subscribe(serviceId, characteristicId, callback) {
313
+ const key = this.getNotificationKey(serviceId, characteristicId);
314
+ if (!this.internalNotificationListeners.has(key)) {
315
+ this.internalNotificationListeners.set(key, new Set());
316
+ }
317
+ this.internalNotificationListeners.get(key).add(callback);
318
+ }
319
+ unsubscribe(serviceId, characteristicId, callback) {
320
+ const key = this.getNotificationKey(serviceId, characteristicId);
321
+ this.internalNotificationListeners.get(key)?.delete(callback);
322
+ }
323
+ async stopInternalNotifications(serviceId, characteristicId) {
324
+ await BleClient.stopNotifications(this.device.deviceId, serviceId, characteristicId);
325
+ const key = this.getNotificationKey(serviceId, characteristicId);
326
+ this.internalNotificationListeners.delete(key);
327
+ }
328
+ /**
329
+ * Write to characteristic and wait for a notification response.
330
+ *
331
+ * It is the responsibility of the caller to have started notifications
332
+ * for the characteristic.
333
+ */
334
+ async writeForNotification(serviceId, characteristicId, value, notificationId, isFinalNotification = () => true) {
335
+ let notificationListener;
336
+ const notificationPromise = new Promise((resolve) => {
337
+ notificationListener = (bytes) => {
338
+ if (bytes[0] === notificationId && isFinalNotification(bytes)) {
339
+ resolve(bytes);
340
+ }
341
+ };
342
+ this.subscribe(serviceId, characteristicId, notificationListener);
343
+ });
387
344
  try {
388
- const deviceInfo = await this.assertGattServer().getPrimaryService(serviceMeta.id);
389
- const characteristic = await deviceInfo.getCharacteristic(serviceMeta.characteristics.modelNumber.id);
390
- const modelNumberBytes = await characteristic.readValue();
391
- const modelNumber = new TextDecoder().decode(modelNumberBytes);
392
- if (modelNumber.toLowerCase() === "BBC micro:bit".toLowerCase()) {
393
- return "V1";
394
- }
395
- if (modelNumber.toLowerCase().includes("BBC micro:bit v2".toLowerCase())) {
396
- return "V2";
345
+ await BleClient.writeWithoutResponse(this.device.deviceId, serviceId, characteristicId, value);
346
+ return await this.raceDisconnectAndTimeout(notificationPromise, {
347
+ timeout: 3_000,
348
+ actionName: "flash notification wait",
349
+ });
350
+ }
351
+ finally {
352
+ if (notificationListener) {
353
+ this.unsubscribe(serviceId, characteristicId, notificationListener);
397
354
  }
398
- throw new Error(`Unexpected model number ${modelNumber}`);
355
+ }
356
+ }
357
+ async waitForDisconnect(timeout) {
358
+ if (!this.connected) {
359
+ this.log("Waiting for disconnect but not connected");
360
+ return;
361
+ }
362
+ this.log(`Waiting for disconnect (timeout ${timeout})`);
363
+ try {
364
+ await Promise.race([
365
+ this.disconnectErrorPromise("wait"),
366
+ timeoutErrorAfter(timeout),
367
+ ]);
399
368
  }
400
369
  catch (e) {
401
- this.logging.error("Could not read model number", e);
402
- throw new Error("Could not read model number");
370
+ if (e instanceof TimeoutError) {
371
+ this.error("Timeout waiting for disconnect");
372
+ }
373
+ if (!(e instanceof DisconnectError)) {
374
+ throw e;
375
+ }
403
376
  }
404
377
  }
405
- queueGattOperation(action) {
406
- // Previously we wrapped rejections with:
407
- // new DeviceError({ code: "background-comms-error", message: err }),
408
- return this.gattOperations.add(action);
378
+ /**
379
+ * Suitable for running a series of BLE interactions with an overall timeout
380
+ * and general disconnection
381
+ */
382
+ async raceDisconnectAndTimeout(promise, options = {}) {
383
+ if (!this.connected) {
384
+ throw new DisconnectError();
385
+ }
386
+ const actionName = options.actionName ?? "action";
387
+ const errorOnDisconnectPromise = this.disconnectErrorPromise(actionName);
388
+ return await Promise.race([
389
+ promise,
390
+ errorOnDisconnectPromise,
391
+ ...(options.timeout ? [timeoutErrorAfter(options.timeout)] : []),
392
+ ]);
409
393
  }
410
- createIfNeeded(info, listenerInit) {
411
- const gattServer = this.assertGattServer();
412
- return info.createIfNeeded(gattServer, this.dispatchTypedEvent, this.queueGattOperation.bind(this), listenerInit);
394
+ disconnectErrorPromise(actionName) {
395
+ const { promise, callback } = disconnectErrorCallback(`Disconnected during ${actionName}`);
396
+ this.waitingForDisconnectEventCallbacks.push(callback);
397
+ return promise;
413
398
  }
414
- async getAccelerometerService() {
415
- return this.createIfNeeded(this.accelerometer, false);
399
+ event(event) {
400
+ this.logging.event(event);
416
401
  }
417
- async getLedService() {
418
- return this.createIfNeeded(this.led, false);
402
+ log(message) {
403
+ this.logging.log(message);
419
404
  }
420
- async getMagnetometerService() {
421
- return this.createIfNeeded(this.magnetometer, false);
405
+ error(message, e) {
406
+ this.logging.error(message, e);
422
407
  }
423
- async getUARTService() {
424
- return this.createIfNeeded(this.uart, false);
408
+ getNotificationKey(serviceId, characteristicId) {
409
+ return `${serviceId}:${characteristicId}`;
425
410
  }
426
- async startNotifications(type) {
427
- const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
428
- if (serviceInfo) {
429
- this.queueGattOperation(async () => {
430
- // TODO: type cheat! why?
431
- const service = await this.createIfNeeded(serviceInfo, true);
432
- await service?.startNotifications(type);
433
- });
411
+ /**
412
+ * Bonds with device and handles the post-bond device state only returning
413
+ * when we can reattempt a connection with the device.
414
+ */
415
+ async connectHandlingBond() {
416
+ const startTime = Date.now();
417
+ const maybeJustBonded = await this.bondConnectDeviceInternal();
418
+ if (maybeJustBonded) {
419
+ // If we did just bond then the device disconnects after 2_000 and then
420
+ // resets after a further 13_000 In future we'd like a firmware change
421
+ // that means it doesn't reset when partial flashing is in progress.
422
+ this.log(isAndroid() ? "New bond" : "Potential new bond");
423
+ // On Android with micro:bit V1 we don't see this disconnect (just the 15s
424
+ // reboot) so we hit the timeout and then continue to reset into pairing
425
+ // mode.
426
+ // TODO: document what happens with iOS micro:bit V1 in the new bond case.
427
+ try {
428
+ await this.waitForDisconnect(3000);
429
+ }
430
+ catch (e) {
431
+ if (e instanceof TimeoutError) {
432
+ this.log("No disconnect after bond, assuming connection is stable");
433
+ if (!isAndroid()) {
434
+ // We never knew for sure whether this was a new bond. Assume we
435
+ // were already bonded on the basis we didn't get disconnected.
436
+ return;
437
+ }
438
+ }
439
+ else {
440
+ throw e;
441
+ }
442
+ }
443
+ await this.connectInternal();
444
+ // TODO: check this is needed, potentially inline into connect if always needed
445
+ await delay(500);
446
+ this.log("Resetting to pairing mode");
447
+ const pf = new PartialFlashingService(this);
448
+ await pf.resetToMode(MicroBitMode.Pairing);
449
+ await this.waitForDisconnect(10_000);
450
+ await this.connectInternal();
451
+ this.log(`Connection ready; took ${Date.now() - startTime}`);
434
452
  }
435
453
  }
436
- async stopNotifications(type) {
437
- this.queueGattOperation(async () => {
438
- const serviceInfo = this.serviceInfo.find((s) => s.events.includes(type));
439
- await serviceInfo?.get()?.stopNotifications(type);
440
- });
441
- }
442
- disposeServices() {
443
- this.serviceInfo.forEach((s) => s.dispose());
444
- this.gattOperations.clear(this.disconnectedRejectionErrorFactory);
454
+ async bondConnectDeviceInternal() {
455
+ const { deviceId } = this.device;
456
+ if (isAndroid()) {
457
+ let justBonded = false;
458
+ // This gets us a nicer pairing dialog than just going straight for the characteristic.
459
+ if (!(await BleClient.isBonded(deviceId))) {
460
+ await BleClient.createBond(deviceId, { timeout: bondingTimeoutInMs });
461
+ justBonded = true;
462
+ }
463
+ await this.connectInternal();
464
+ return justBonded;
465
+ }
466
+ else {
467
+ // Long timeout as this is the point that the pairing dialog will show.
468
+ // If this responds very quickly maybe we could assume there was a bond?
469
+ // At the moment we always do the disconnect dance so subsequent code will
470
+ // need to call startNotifications again. We need to be connected to
471
+ // startNotifications.
472
+ await this.connectInternal();
473
+ const pf = new PartialFlashingService(this);
474
+ await pf.startNotifications({ timeout: bondingTimeoutInMs });
475
+ // We just did it now to trigger pairing at a well defined point.
476
+ await pf.stopNotifications();
477
+ return true;
478
+ }
445
479
  }
446
480
  }
447
- export const createBluetoothDeviceWrapper = async (device, logging, dispatchTypedEvent, currentEvents, callbacks) => {
448
- try {
449
- // Reuse our connection objects for the same device as they
450
- // track the GATT connect promise that never resolves.
451
- const bluetooth = deviceIdToWrapper.get(device.id) ??
452
- new BluetoothDeviceWrapper(device, logging, dispatchTypedEvent, currentEvents, callbacks);
453
- deviceIdToWrapper.set(device.id, bluetooth);
454
- await bluetooth.connect();
455
- return bluetooth;
456
- }
457
- catch (e) {
458
- logging.error("Bluetooth connect error", e);
459
- return undefined;
460
- }
461
- };
462
481
  //# sourceMappingURL=bluetooth-device-wrapper.js.map