@onekeyfe/hd-transport-react-native 1.2.0-alpha.12 → 1.2.0-alpha.121
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/dist/BleTransport.d.ts +2 -4
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +7 -0
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +114 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +736 -249
- package/dist/transportLog.d.ts +1 -12
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +7 -6
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStrategy.test.ts +93 -5
- package/src/__tests__/connectTimeout.test.ts +292 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +736 -30
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +26 -11
- package/src/constants.ts +9 -14
- package/src/index.ts +980 -226
- package/src/transportLog.ts +1 -18
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -5,30 +5,46 @@ import {
|
|
|
5
5
|
BleError,
|
|
6
6
|
BleErrorCode,
|
|
7
7
|
BleManager as BlePlxManager,
|
|
8
|
+
ConnectionPriority,
|
|
8
9
|
ScanMode,
|
|
9
10
|
} from 'react-native-ble-plx';
|
|
10
11
|
import ByteBuffer from 'bytebuffer';
|
|
11
12
|
import transport, {
|
|
12
|
-
LogBlockCommand,
|
|
13
13
|
type OneKeyDeviceInfoBase,
|
|
14
14
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
15
|
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
16
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
17
17
|
type ProtocolType,
|
|
18
|
+
type ProtocolV2CallContext,
|
|
18
19
|
ProtocolV2FrameAssembler,
|
|
19
20
|
ProtocolV2LinkManager,
|
|
21
|
+
TRANSPORT_EVENT,
|
|
20
22
|
type TransportCallOptions,
|
|
23
|
+
isProtocolV2HighThroughputCall,
|
|
21
24
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
25
|
+
writeProtocolV2BleFrame,
|
|
22
26
|
} from '@onekeyfe/hd-transport';
|
|
23
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ERRORS,
|
|
29
|
+
HardwareErrorCode,
|
|
30
|
+
createDeferred,
|
|
31
|
+
isOnekeyBluetoothDevice,
|
|
32
|
+
isPro2FindMyAdvertisementName,
|
|
33
|
+
} from '@onekeyfe/hd-shared';
|
|
24
34
|
|
|
25
35
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
26
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
hasWritableCapability,
|
|
38
|
+
resolveProtocolV2PacketCapacity,
|
|
39
|
+
shouldRefreshNegotiatedMtu,
|
|
40
|
+
shouldWriteProtocolV2WithResponse,
|
|
41
|
+
} from './bleStrategy';
|
|
27
42
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
28
43
|
import {
|
|
29
44
|
ANDROID_PACKET_LENGTH,
|
|
45
|
+
ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
30
46
|
IOS_PACKET_LENGTH,
|
|
31
|
-
|
|
47
|
+
IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
32
48
|
getBluetoothServiceUuids,
|
|
33
49
|
getInfosForServiceUuid,
|
|
34
50
|
isSameBleUuid,
|
|
@@ -37,7 +53,6 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
37
53
|
import BleTransport from './BleTransport';
|
|
38
54
|
import timer from './utils/timer';
|
|
39
55
|
import { bleLogger, setBleLogger } from './logger';
|
|
40
|
-
import { createTransportCallLog } from './transportLog';
|
|
41
56
|
|
|
42
57
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
43
58
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -53,24 +68,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
53
68
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
54
69
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
55
70
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
56
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
57
71
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
58
72
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
59
73
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
60
74
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
61
75
|
|
|
62
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
76
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
63
77
|
type ResolvedBleCharacteristics = {
|
|
64
78
|
writeCharacteristic: Characteristic;
|
|
65
79
|
notifyCharacteristic: Characteristic;
|
|
66
80
|
};
|
|
67
81
|
|
|
82
|
+
const isAsciiWhitespace = (code: number) =>
|
|
83
|
+
code === 0x09 ||
|
|
84
|
+
code === 0x0a ||
|
|
85
|
+
code === 0x0b ||
|
|
86
|
+
code === 0x0c ||
|
|
87
|
+
code === 0x0d ||
|
|
88
|
+
code === 0x20;
|
|
89
|
+
|
|
90
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
91
|
+
let searchFrom = 0;
|
|
92
|
+
while (searchFrom < text.length) {
|
|
93
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
94
|
+
if (statusIndex < 0) return false;
|
|
95
|
+
|
|
96
|
+
let cursor = statusIndex + 'status'.length;
|
|
97
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
98
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
99
|
+
cursor += 1;
|
|
100
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
101
|
+
}
|
|
102
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
103
|
+
|
|
104
|
+
searchFrom = statusIndex + 'status'.length;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
68
109
|
const delay = (ms: number) =>
|
|
69
110
|
new Promise<void>(resolve => {
|
|
70
111
|
setTimeout(resolve, ms);
|
|
71
112
|
});
|
|
72
113
|
|
|
73
|
-
const getFirmwareUploadWriteRetryType = (
|
|
114
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
115
|
+
error: unknown
|
|
116
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
74
117
|
if (!error || typeof error !== 'object') return null;
|
|
75
118
|
const bleWriteError = error as {
|
|
76
119
|
androidErrorCode?: unknown;
|
|
@@ -81,13 +124,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
81
124
|
name?: unknown;
|
|
82
125
|
};
|
|
83
126
|
|
|
84
|
-
if (
|
|
85
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
86
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
87
|
-
) {
|
|
88
|
-
return 'reconnectable';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
127
|
if (
|
|
92
128
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
93
129
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -98,15 +134,30 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
98
134
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
99
135
|
.filter(value => typeof value === 'string')
|
|
100
136
|
.join(' ');
|
|
101
|
-
return
|
|
137
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
102
138
|
};
|
|
103
139
|
|
|
104
140
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
105
141
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
106
|
-
const
|
|
107
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
142
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
108
143
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
146
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
147
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
148
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
149
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
150
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
151
|
+
*/
|
|
152
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
153
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
154
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
155
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
156
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
157
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
158
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
159
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
160
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
110
161
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
111
162
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
112
163
|
export type ProtocolV2BleTuning = {
|
|
@@ -117,8 +168,8 @@ export type ProtocolV2BleTuning = {
|
|
|
117
168
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
118
169
|
|
|
119
170
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
120
|
-
iosPacketLength:
|
|
121
|
-
androidPacketLength:
|
|
171
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
172
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
122
173
|
};
|
|
123
174
|
|
|
124
175
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -160,24 +211,60 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
160
211
|
return device?.name || device?.localName || null;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
214
|
+
const IOS_REQUEST_MTU = 247;
|
|
215
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
216
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
217
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
170
|
-
);
|
|
171
|
-
}
|
|
219
|
+
const getRequestedBleMtu = () =>
|
|
220
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
172
221
|
|
|
173
|
-
const
|
|
222
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
174
223
|
|
|
175
224
|
const connectOptions: Record<string, unknown> = {
|
|
176
|
-
requestMTU:
|
|
177
|
-
timeout:
|
|
225
|
+
requestMTU: getRequestedBleMtu(),
|
|
226
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
178
227
|
refreshGatt: 'OnConnected',
|
|
179
228
|
};
|
|
180
229
|
|
|
230
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
231
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
232
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
237
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
238
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
239
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
240
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
241
|
+
*/
|
|
242
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
243
|
+
/**
|
|
244
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
245
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
246
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
247
|
+
* budget.
|
|
248
|
+
*/
|
|
249
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
250
|
+
/**
|
|
251
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
252
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
253
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
254
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
255
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
256
|
+
*/
|
|
257
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
258
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
259
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
260
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
261
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
262
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
263
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
264
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
265
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
266
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
267
|
+
|
|
181
268
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
182
269
|
|
|
183
270
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -189,23 +276,32 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
189
276
|
return infos;
|
|
190
277
|
};
|
|
191
278
|
|
|
192
|
-
const
|
|
193
|
-
|
|
279
|
+
const requestNegotiatedMtu = async (
|
|
280
|
+
device: Device,
|
|
281
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
282
|
+
attempt: number
|
|
283
|
+
) => {
|
|
284
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
194
285
|
|
|
195
286
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
requested: ANDROID_REQUEST_MTU,
|
|
200
|
-
actual: mtuDevice.mtu,
|
|
201
|
-
});
|
|
287
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
288
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
289
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
202
290
|
return mtuDevice;
|
|
203
291
|
} catch (error) {
|
|
204
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
292
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
293
|
+
platform: Platform.OS,
|
|
294
|
+
stage,
|
|
295
|
+
attempt,
|
|
296
|
+
actual: device.mtu,
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
});
|
|
205
299
|
return device;
|
|
206
300
|
}
|
|
207
301
|
};
|
|
208
302
|
|
|
303
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
304
|
+
|
|
209
305
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
210
306
|
|
|
211
307
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -245,6 +341,8 @@ export default class ReactNativeBleTransport {
|
|
|
245
341
|
|
|
246
342
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
247
343
|
|
|
344
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
345
|
+
|
|
248
346
|
name = 'ReactNativeBleTransport';
|
|
249
347
|
|
|
250
348
|
configured = false;
|
|
@@ -255,6 +353,8 @@ export default class ReactNativeBleTransport {
|
|
|
255
353
|
|
|
256
354
|
runPromise: Deferred<any> | null = null;
|
|
257
355
|
|
|
356
|
+
private runPromiseDeviceId: string | null = null;
|
|
357
|
+
|
|
258
358
|
emitter?: EventEmitter;
|
|
259
359
|
|
|
260
360
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -262,8 +362,28 @@ export default class ReactNativeBleTransport {
|
|
|
262
362
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
263
363
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
264
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
367
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
368
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
369
|
+
* transport that was never validated.
|
|
370
|
+
*/
|
|
371
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
372
|
+
|
|
373
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
374
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
375
|
+
|
|
376
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
377
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
378
|
+
|
|
265
379
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
266
380
|
|
|
381
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
382
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
383
|
+
|
|
384
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
385
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
386
|
+
|
|
267
387
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
268
388
|
|
|
269
389
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -286,13 +406,21 @@ export default class ReactNativeBleTransport {
|
|
|
286
406
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
287
407
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
288
408
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
289
|
-
await this.
|
|
409
|
+
await this.releaseNative(uuid, true);
|
|
290
410
|
}
|
|
291
411
|
},
|
|
292
412
|
});
|
|
293
413
|
|
|
294
414
|
private monitorTokens: Map<string, number> = new Map();
|
|
295
415
|
|
|
416
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
417
|
+
|
|
418
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
419
|
+
|
|
420
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
421
|
+
|
|
422
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
423
|
+
|
|
296
424
|
private nextMonitorToken = 1;
|
|
297
425
|
|
|
298
426
|
constructor(options: TransportOptions) {
|
|
@@ -311,10 +439,19 @@ export default class ReactNativeBleTransport {
|
|
|
311
439
|
}
|
|
312
440
|
|
|
313
441
|
configureProtocolV2(signedData: any) {
|
|
442
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
443
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
314
448
|
this._messagesV2 = parseConfigure(signedData);
|
|
315
|
-
this.
|
|
316
|
-
|
|
317
|
-
.
|
|
449
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
450
|
+
if (isReconfiguration) {
|
|
451
|
+
this.protocolV2Links
|
|
452
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
453
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
454
|
+
}
|
|
318
455
|
}
|
|
319
456
|
|
|
320
457
|
listen() {
|
|
@@ -344,29 +481,15 @@ export default class ReactNativeBleTransport {
|
|
|
344
481
|
}
|
|
345
482
|
}
|
|
346
483
|
|
|
347
|
-
let fallbackServiceUuid: string | undefined;
|
|
348
|
-
|
|
349
484
|
if (!infos) {
|
|
350
485
|
const services = await device.services();
|
|
351
486
|
Log?.debug(
|
|
352
487
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
353
488
|
services?.map(service => service.uuid)
|
|
354
489
|
);
|
|
355
|
-
|
|
356
|
-
const knownService = services.find(service =>
|
|
357
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
358
|
-
);
|
|
359
|
-
const fallbackService =
|
|
360
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
361
|
-
|
|
362
|
-
if (fallbackService) {
|
|
363
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
364
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
365
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
366
|
-
}
|
|
367
490
|
}
|
|
368
491
|
|
|
369
|
-
if (!infos
|
|
492
|
+
if (!infos) {
|
|
370
493
|
try {
|
|
371
494
|
Log?.debug('cancel connection when service not found');
|
|
372
495
|
await device.cancelConnection();
|
|
@@ -376,9 +499,7 @@ export default class ReactNativeBleTransport {
|
|
|
376
499
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
377
500
|
}
|
|
378
501
|
|
|
379
|
-
const serviceUuid = infos
|
|
380
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
381
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
502
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
382
503
|
|
|
383
504
|
if (!serviceUuid) {
|
|
384
505
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -428,6 +549,7 @@ export default class ReactNativeBleTransport {
|
|
|
428
549
|
|
|
429
550
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
430
551
|
transport.disconnectSubscription?.remove();
|
|
552
|
+
const { monitorToken } = transport;
|
|
431
553
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
432
554
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
433
555
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -437,18 +559,17 @@ export default class ReactNativeBleTransport {
|
|
|
437
559
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
438
560
|
return;
|
|
439
561
|
}
|
|
562
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
563
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
440
566
|
|
|
441
567
|
try {
|
|
442
568
|
Log?.debug('device disconnect: ', device?.id);
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
id: device?.id,
|
|
446
|
-
connectId: device?.id,
|
|
447
|
-
});
|
|
448
|
-
if (this.runPromise) {
|
|
569
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
570
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
449
571
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
450
572
|
this.runPromise.reject(error);
|
|
451
|
-
this.rejectAllProtocolV2Frames(error);
|
|
452
573
|
}
|
|
453
574
|
} catch (e) {
|
|
454
575
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -458,6 +579,22 @@ export default class ReactNativeBleTransport {
|
|
|
458
579
|
});
|
|
459
580
|
}
|
|
460
581
|
|
|
582
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
583
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
587
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
591
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
592
|
+
name,
|
|
593
|
+
id: uuid,
|
|
594
|
+
connectId: uuid,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
461
598
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
462
599
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
463
600
|
try {
|
|
@@ -470,22 +607,21 @@ export default class ReactNativeBleTransport {
|
|
|
470
607
|
const isConnected = await device.isConnected().catch(() => false);
|
|
471
608
|
if (!isConnected) {
|
|
472
609
|
try {
|
|
473
|
-
device = await device.connect(connectOptions);
|
|
610
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
474
611
|
} catch (e) {
|
|
475
612
|
if (
|
|
476
613
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
477
614
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
478
615
|
) {
|
|
479
|
-
device = await device.connect();
|
|
616
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
480
617
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
481
618
|
throw e;
|
|
482
619
|
}
|
|
483
620
|
}
|
|
484
621
|
}
|
|
485
622
|
|
|
486
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
487
|
-
device
|
|
488
|
-
);
|
|
623
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
624
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
489
625
|
|
|
490
626
|
transport.device = device;
|
|
491
627
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -545,7 +681,7 @@ export default class ReactNativeBleTransport {
|
|
|
545
681
|
}
|
|
546
682
|
|
|
547
683
|
blePlxManager.startDeviceScan(
|
|
548
|
-
|
|
684
|
+
getBluetoothServiceUuids(),
|
|
549
685
|
{
|
|
550
686
|
allowDuplicates: true,
|
|
551
687
|
scanMode: ScanMode.LowLatency,
|
|
@@ -575,10 +711,21 @@ export default class ReactNativeBleTransport {
|
|
|
575
711
|
}
|
|
576
712
|
|
|
577
713
|
const displayName = getDeviceDisplayName(device);
|
|
714
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
715
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
716
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
717
|
+
const isFindMyPeripheral =
|
|
718
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
719
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
578
720
|
const isOneKey =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
721
|
+
!isUnnamedIOSPeripheral &&
|
|
722
|
+
!isFindMyPeripheral &&
|
|
723
|
+
isOnekeyBluetoothDevice({
|
|
724
|
+
id: device?.id,
|
|
725
|
+
name: device?.name,
|
|
726
|
+
localName: device?.localName,
|
|
727
|
+
serviceUuids: device?.serviceUUIDs,
|
|
728
|
+
});
|
|
582
729
|
if (isOneKey) {
|
|
583
730
|
addDevice(device as unknown as Device);
|
|
584
731
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -592,11 +739,32 @@ export default class ReactNativeBleTransport {
|
|
|
592
739
|
}
|
|
593
740
|
);
|
|
594
741
|
|
|
595
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
596
|
-
|
|
597
|
-
|
|
742
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
743
|
+
devices => {
|
|
744
|
+
for (const device of devices) {
|
|
745
|
+
const localName =
|
|
746
|
+
'localName' in device && typeof device.localName === 'string'
|
|
747
|
+
? device.localName
|
|
748
|
+
: null;
|
|
749
|
+
const isFindMyPeripheral =
|
|
750
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
751
|
+
isPro2FindMyAdvertisementName(localName);
|
|
752
|
+
|
|
753
|
+
if (
|
|
754
|
+
!isFindMyPeripheral &&
|
|
755
|
+
isOnekeyBluetoothDevice({
|
|
756
|
+
id: device.id,
|
|
757
|
+
name: device.name,
|
|
758
|
+
localName,
|
|
759
|
+
serviceUuids: device.serviceUUIDs,
|
|
760
|
+
})
|
|
761
|
+
) {
|
|
762
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
763
|
+
addDevice(device as unknown as Device);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
598
766
|
}
|
|
599
|
-
|
|
767
|
+
);
|
|
600
768
|
|
|
601
769
|
const addDevice = (device: Device) => {
|
|
602
770
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -626,6 +794,79 @@ export default class ReactNativeBleTransport {
|
|
|
626
794
|
});
|
|
627
795
|
}
|
|
628
796
|
|
|
797
|
+
private async installTransportForAcquire(
|
|
798
|
+
uuid: string,
|
|
799
|
+
device: Device,
|
|
800
|
+
characteristics?: ResolvedBleCharacteristics
|
|
801
|
+
) {
|
|
802
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
803
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
804
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
805
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
806
|
+
const monitorToken = this.nextMonitorToken;
|
|
807
|
+
this.nextMonitorToken += 1;
|
|
808
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
809
|
+
transport.monitorToken = monitorToken;
|
|
810
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
811
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
812
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
813
|
+
transport.notifyCharacteristic,
|
|
814
|
+
uuid,
|
|
815
|
+
monitorToken,
|
|
816
|
+
notifyTransactionId
|
|
817
|
+
);
|
|
818
|
+
transportCache[uuid] = transport;
|
|
819
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
820
|
+
this.protocolV2Assemblers.set(
|
|
821
|
+
uuid,
|
|
822
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (Platform.OS === 'ios') {
|
|
826
|
+
await new Promise<void>(resolve => {
|
|
827
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
828
|
+
});
|
|
829
|
+
} else if (Platform.OS === 'android') {
|
|
830
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const initialMtu = transport.mtuSize;
|
|
834
|
+
let refreshAttempts = 0;
|
|
835
|
+
if (
|
|
836
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
837
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
838
|
+
) {
|
|
839
|
+
refreshAttempts += 1;
|
|
840
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
841
|
+
transport.device,
|
|
842
|
+
'servicesAndNotifyReady',
|
|
843
|
+
1
|
|
844
|
+
);
|
|
845
|
+
transport.device = refreshedDevice;
|
|
846
|
+
transport.mtuSize =
|
|
847
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
848
|
+
|
|
849
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
850
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
851
|
+
refreshAttempts += 1;
|
|
852
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
853
|
+
transport.device = refreshedDevice;
|
|
854
|
+
transport.mtuSize =
|
|
855
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
860
|
+
platform: Platform.OS,
|
|
861
|
+
requested: getRequestedBleMtu(),
|
|
862
|
+
initial: initialMtu,
|
|
863
|
+
actual: transport.mtuSize,
|
|
864
|
+
refreshAttempts,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return transport;
|
|
868
|
+
}
|
|
869
|
+
|
|
629
870
|
async acquire(input: BleAcquireInput) {
|
|
630
871
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
631
872
|
|
|
@@ -659,8 +900,8 @@ export default class ReactNativeBleTransport {
|
|
|
659
900
|
if (forceCleanRunPromise && this.runPromise) {
|
|
660
901
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
661
902
|
this.runPromise.reject(error);
|
|
662
|
-
this.rejectAllProtocolV2Frames(error);
|
|
663
903
|
this.runPromise = null;
|
|
904
|
+
this.runPromiseDeviceId = null;
|
|
664
905
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
665
906
|
}
|
|
666
907
|
|
|
@@ -696,15 +937,22 @@ export default class ReactNativeBleTransport {
|
|
|
696
937
|
if (!device) {
|
|
697
938
|
Log?.debug('try to connect to device: ', uuid);
|
|
698
939
|
try {
|
|
699
|
-
device = await
|
|
940
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
941
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
942
|
+
);
|
|
700
943
|
} catch (e) {
|
|
701
944
|
Log?.debug('try to connect to device has error: ', e);
|
|
945
|
+
if (isConnectTimeoutError(e)) {
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
702
948
|
if (
|
|
703
949
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
704
950
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
705
951
|
) {
|
|
706
952
|
Log?.debug('first try to reconnect without params');
|
|
707
|
-
device = await
|
|
953
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
954
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
955
|
+
);
|
|
708
956
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
709
957
|
Log?.debug('device already connected');
|
|
710
958
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -720,26 +968,36 @@ export default class ReactNativeBleTransport {
|
|
|
720
968
|
|
|
721
969
|
if (!(await device.isConnected())) {
|
|
722
970
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
971
|
+
const disconnectedDevice = device;
|
|
723
972
|
|
|
724
973
|
try {
|
|
725
|
-
device = await
|
|
974
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
975
|
+
disconnectedDevice.connect(connectOptions)
|
|
976
|
+
);
|
|
726
977
|
} catch (e) {
|
|
727
978
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
979
|
+
if (isConnectTimeoutError(e)) {
|
|
980
|
+
throw e;
|
|
981
|
+
}
|
|
728
982
|
if (
|
|
729
983
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
730
984
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
731
985
|
) {
|
|
732
986
|
Log?.debug('second try to reconnect without params');
|
|
733
987
|
try {
|
|
734
|
-
device = await
|
|
988
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
989
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
990
|
+
);
|
|
735
991
|
} catch (e) {
|
|
736
992
|
Log?.debug('last try to reconnect error: ', e);
|
|
737
993
|
// last try to reconnect device if this issue exists
|
|
738
994
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
739
995
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
740
996
|
Log?.debug('last try to reconnect');
|
|
741
|
-
await
|
|
742
|
-
device = await
|
|
997
|
+
await disconnectedDevice.cancelConnection();
|
|
998
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
999
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1000
|
+
);
|
|
743
1001
|
}
|
|
744
1002
|
}
|
|
745
1003
|
} else {
|
|
@@ -748,13 +1006,16 @@ export default class ReactNativeBleTransport {
|
|
|
748
1006
|
}
|
|
749
1007
|
}
|
|
750
1008
|
|
|
751
|
-
device = await
|
|
752
|
-
const
|
|
1009
|
+
device = await resolveNegotiatedMtu(device);
|
|
1010
|
+
const acquiredDevice = device;
|
|
1011
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1012
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
753
1013
|
|
|
754
1014
|
const protocolHint = expectedProtocol
|
|
755
1015
|
? undefined
|
|
756
|
-
:
|
|
757
|
-
|
|
1016
|
+
: input.protocolHint ??
|
|
1017
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
1018
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
758
1019
|
|
|
759
1020
|
// release transport before new transport instance
|
|
760
1021
|
await this.release(uuid, true);
|
|
@@ -762,45 +1023,30 @@ export default class ReactNativeBleTransport {
|
|
|
762
1023
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
763
1024
|
}
|
|
764
1025
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}
|
|
769
|
-
const monitorToken = this.nextMonitorToken;
|
|
770
|
-
this.nextMonitorToken += 1;
|
|
771
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
772
|
-
transport.monitorToken = monitorToken;
|
|
773
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
774
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
775
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
776
|
-
transport.notifyCharacteristic,
|
|
777
|
-
uuid,
|
|
778
|
-
monitorToken,
|
|
779
|
-
notifyTransactionId
|
|
780
|
-
);
|
|
781
|
-
transportCache[uuid] = transport;
|
|
782
|
-
|
|
783
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
784
|
-
|
|
785
|
-
if (Platform.OS === 'ios') {
|
|
786
|
-
await new Promise<void>(resolve => {
|
|
787
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
788
|
-
});
|
|
789
|
-
} else if (Platform.OS === 'android') {
|
|
790
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
794
|
-
|
|
795
|
-
this.emitter?.emit('device-connect', {
|
|
796
|
-
name: device.name,
|
|
797
|
-
id: device.id,
|
|
798
|
-
connectId: device.id,
|
|
1026
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1027
|
+
writeCharacteristic,
|
|
1028
|
+
notifyCharacteristic,
|
|
799
1029
|
});
|
|
800
1030
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1031
|
+
try {
|
|
1032
|
+
const protocolType = await this.detectProtocol(
|
|
1033
|
+
uuid,
|
|
1034
|
+
expectedProtocol,
|
|
1035
|
+
protocolHint,
|
|
1036
|
+
async () => {
|
|
1037
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
const currentTransport = transportCache[uuid];
|
|
1041
|
+
if (!currentTransport) {
|
|
1042
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1043
|
+
}
|
|
1044
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1045
|
+
return { uuid, protocolType };
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
await this.release(uuid, true);
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
804
1050
|
}
|
|
805
1051
|
|
|
806
1052
|
_monitorCharacteristic(
|
|
@@ -827,7 +1073,7 @@ export default class ReactNativeBleTransport {
|
|
|
827
1073
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
828
1074
|
return;
|
|
829
1075
|
}
|
|
830
|
-
if (this.
|
|
1076
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
831
1077
|
let errorCode:
|
|
832
1078
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
833
1079
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -850,7 +1096,7 @@ export default class ReactNativeBleTransport {
|
|
|
850
1096
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
851
1097
|
return;
|
|
852
1098
|
}
|
|
853
|
-
if (this.runPromise) {
|
|
1099
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
854
1100
|
let ERROR:
|
|
855
1101
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
856
1102
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -873,7 +1119,6 @@ export default class ReactNativeBleTransport {
|
|
|
873
1119
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
874
1120
|
);
|
|
875
1121
|
this.runPromise.reject(notifyError);
|
|
876
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
877
1122
|
Log?.debug(
|
|
878
1123
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
879
1124
|
);
|
|
@@ -881,7 +1126,6 @@ export default class ReactNativeBleTransport {
|
|
|
881
1126
|
}
|
|
882
1127
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
883
1128
|
this.runPromise.reject(notifyError);
|
|
884
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
885
1129
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
886
1130
|
}
|
|
887
1131
|
|
|
@@ -899,7 +1143,7 @@ export default class ReactNativeBleTransport {
|
|
|
899
1143
|
|
|
900
1144
|
try {
|
|
901
1145
|
const data = Buffer.from(c.value as string, 'base64');
|
|
902
|
-
const protocol = this.
|
|
1146
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
903
1147
|
if (!protocol) {
|
|
904
1148
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
905
1149
|
return;
|
|
@@ -926,14 +1170,16 @@ export default class ReactNativeBleTransport {
|
|
|
926
1170
|
// );
|
|
927
1171
|
bufferLength = 0;
|
|
928
1172
|
buffer = [];
|
|
929
|
-
this.
|
|
1173
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1174
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1175
|
+
}
|
|
930
1176
|
}
|
|
931
1177
|
} catch (error) {
|
|
932
1178
|
Log?.debug('monitor data error: ', error);
|
|
933
1179
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
934
|
-
if (this.
|
|
1180
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
935
1181
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
936
|
-
} else {
|
|
1182
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
937
1183
|
this.runPromise?.reject(notifyError);
|
|
938
1184
|
}
|
|
939
1185
|
}
|
|
@@ -943,13 +1189,18 @@ export default class ReactNativeBleTransport {
|
|
|
943
1189
|
}
|
|
944
1190
|
|
|
945
1191
|
async release(uuid: string, onclose = false) {
|
|
946
|
-
const transport = transportCache[uuid];
|
|
947
1192
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
948
|
-
|
|
1193
|
+
return this.releaseNative(uuid, onclose);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
1197
|
+
const transport = transportCache[uuid];
|
|
1198
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
949
1199
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
950
1200
|
this.runPromise.reject(error);
|
|
951
1201
|
this.runPromise = null;
|
|
952
|
-
this.
|
|
1202
|
+
this.runPromiseDeviceId = null;
|
|
1203
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
953
1204
|
} else {
|
|
954
1205
|
this.resetProtocolV2Frames(uuid);
|
|
955
1206
|
}
|
|
@@ -960,6 +1211,8 @@ export default class ReactNativeBleTransport {
|
|
|
960
1211
|
return Promise.resolve(true);
|
|
961
1212
|
}
|
|
962
1213
|
|
|
1214
|
+
await this.restoreAndroidConnectionPriority(uuid, transport);
|
|
1215
|
+
|
|
963
1216
|
if (transport) {
|
|
964
1217
|
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
965
1218
|
this.monitorTokens.delete(uuid);
|
|
@@ -989,8 +1242,11 @@ export default class ReactNativeBleTransport {
|
|
|
989
1242
|
delete transportCache[uuid];
|
|
990
1243
|
}
|
|
991
1244
|
|
|
1245
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1246
|
+
|
|
992
1247
|
this.deviceProtocol.delete(uuid);
|
|
993
|
-
|
|
1248
|
+
this.probingProtocols.delete(uuid);
|
|
1249
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
994
1250
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
995
1251
|
this.protocolV2Assemblers.delete(uuid);
|
|
996
1252
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1029,8 +1285,6 @@ export default class ReactNativeBleTransport {
|
|
|
1029
1285
|
`Device protocol has not been detected for ${uuid}`
|
|
1030
1286
|
);
|
|
1031
1287
|
}
|
|
1032
|
-
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1033
|
-
|
|
1034
1288
|
if (protocol === 'V2') {
|
|
1035
1289
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1036
1290
|
}
|
|
@@ -1056,7 +1310,25 @@ export default class ReactNativeBleTransport {
|
|
|
1056
1310
|
const transport = this.getCachedTransport(uuid);
|
|
1057
1311
|
const runPromise = createDeferred<string>();
|
|
1058
1312
|
runPromise.promise.catch(() => undefined);
|
|
1313
|
+
const supersededRunPromise = this.runPromise;
|
|
1314
|
+
if (supersededRunPromise) {
|
|
1315
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1316
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1317
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1318
|
+
// tear down the shared connection while another call is using it.
|
|
1319
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1320
|
+
}
|
|
1059
1321
|
this.runPromise = runPromise;
|
|
1322
|
+
this.runPromiseDeviceId = uuid;
|
|
1323
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1324
|
+
// only the call that still owns the slot may release it.
|
|
1325
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1326
|
+
if (this.runPromise === runPromise) {
|
|
1327
|
+
this.runPromise = null;
|
|
1328
|
+
this.runPromiseDeviceId = null;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1060
1332
|
const messages = this._messages;
|
|
1061
1333
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1062
1334
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1082,6 +1354,9 @@ export default class ReactNativeBleTransport {
|
|
|
1082
1354
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1083
1355
|
} catch (e) {
|
|
1084
1356
|
onError(e);
|
|
1357
|
+
if (isWedgedWriteError(e)) {
|
|
1358
|
+
throw e;
|
|
1359
|
+
}
|
|
1085
1360
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1086
1361
|
}
|
|
1087
1362
|
}
|
|
@@ -1113,6 +1388,9 @@ export default class ReactNativeBleTransport {
|
|
|
1113
1388
|
}
|
|
1114
1389
|
} catch (e) {
|
|
1115
1390
|
onError(e);
|
|
1391
|
+
if (isWedgedWriteError(e)) {
|
|
1392
|
+
throw e;
|
|
1393
|
+
}
|
|
1116
1394
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1117
1395
|
}
|
|
1118
1396
|
}
|
|
@@ -1126,9 +1404,15 @@ export default class ReactNativeBleTransport {
|
|
|
1126
1404
|
if (name === 'EmmcFileWrite') {
|
|
1127
1405
|
await writeChunkedData(
|
|
1128
1406
|
buffers,
|
|
1129
|
-
data =>
|
|
1407
|
+
data =>
|
|
1408
|
+
this.writeBlePacket(
|
|
1409
|
+
uuid,
|
|
1410
|
+
data,
|
|
1411
|
+
payload => transport.writeWithRetry(payload),
|
|
1412
|
+
isCurrentOwner
|
|
1413
|
+
),
|
|
1130
1414
|
e => {
|
|
1131
|
-
|
|
1415
|
+
releaseOwnershipIfCurrent();
|
|
1132
1416
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1133
1417
|
}
|
|
1134
1418
|
);
|
|
@@ -1149,43 +1433,31 @@ export default class ReactNativeBleTransport {
|
|
|
1149
1433
|
// eslint-disable-next-line no-constant-condition
|
|
1150
1434
|
while (true) {
|
|
1151
1435
|
try {
|
|
1152
|
-
await
|
|
1436
|
+
await this.writeBlePacket(
|
|
1437
|
+
uuid,
|
|
1438
|
+
data,
|
|
1439
|
+
payload => transport.writeWithRetry(payload),
|
|
1440
|
+
isCurrentOwner
|
|
1441
|
+
);
|
|
1153
1442
|
return;
|
|
1154
1443
|
} catch (error) {
|
|
1155
1444
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1156
1445
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1157
1446
|
throw error;
|
|
1158
1447
|
}
|
|
1159
|
-
const
|
|
1160
|
-
const delayMs = shouldReconnect
|
|
1161
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1162
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1448
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1163
1449
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1164
1450
|
attempt: attempt + 1,
|
|
1165
1451
|
delayMs,
|
|
1166
|
-
reconnect: shouldReconnect,
|
|
1167
1452
|
error,
|
|
1168
1453
|
});
|
|
1169
|
-
if (shouldReconnect) {
|
|
1170
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1171
|
-
}
|
|
1172
1454
|
await delay(delayMs);
|
|
1173
1455
|
attempt += 1;
|
|
1174
|
-
if (shouldReconnect) {
|
|
1175
|
-
try {
|
|
1176
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1177
|
-
} catch (e) {
|
|
1178
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1179
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1180
|
-
throw e;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
1456
|
}
|
|
1185
1457
|
}
|
|
1186
1458
|
},
|
|
1187
1459
|
e => {
|
|
1188
|
-
|
|
1460
|
+
releaseOwnershipIfCurrent();
|
|
1189
1461
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1190
1462
|
}
|
|
1191
1463
|
);
|
|
@@ -1194,10 +1466,23 @@ export default class ReactNativeBleTransport {
|
|
|
1194
1466
|
const outData = o.toString('base64');
|
|
1195
1467
|
// Upload resources on low-end phones may OOM
|
|
1196
1468
|
try {
|
|
1197
|
-
|
|
1469
|
+
const shouldUseWriteWithResponse =
|
|
1470
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1471
|
+
await this.writeBlePacket(
|
|
1472
|
+
uuid,
|
|
1473
|
+
outData,
|
|
1474
|
+
payload =>
|
|
1475
|
+
shouldUseWriteWithResponse
|
|
1476
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1477
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1478
|
+
isCurrentOwner
|
|
1479
|
+
);
|
|
1198
1480
|
} catch (e) {
|
|
1199
1481
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1200
|
-
|
|
1482
|
+
releaseOwnershipIfCurrent();
|
|
1483
|
+
if (isWedgedWriteError(e)) {
|
|
1484
|
+
throw e;
|
|
1485
|
+
}
|
|
1201
1486
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1202
1487
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1203
1488
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1233,16 +1518,30 @@ export default class ReactNativeBleTransport {
|
|
|
1233
1518
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1234
1519
|
return check.call(jsonData);
|
|
1235
1520
|
} catch (e) {
|
|
1236
|
-
if (name === '
|
|
1237
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1521
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1522
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1238
1523
|
} else {
|
|
1239
1524
|
Log?.error('call error: ', e);
|
|
1240
1525
|
}
|
|
1526
|
+
const isProbeTimeout =
|
|
1527
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1528
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1529
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1530
|
+
// call is actively using.
|
|
1531
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1532
|
+
if (
|
|
1533
|
+
!isProbeTimeout &&
|
|
1534
|
+
!isStaleCall &&
|
|
1535
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1536
|
+
) {
|
|
1537
|
+
await this.disconnect(uuid);
|
|
1538
|
+
}
|
|
1241
1539
|
throw e;
|
|
1242
1540
|
} finally {
|
|
1243
1541
|
if (timeout) clearTimeout(timeout);
|
|
1244
1542
|
if (this.runPromise === runPromise) {
|
|
1245
1543
|
this.runPromise = null;
|
|
1544
|
+
this.runPromiseDeviceId = null;
|
|
1246
1545
|
}
|
|
1247
1546
|
}
|
|
1248
1547
|
}
|
|
@@ -1254,6 +1553,7 @@ export default class ReactNativeBleTransport {
|
|
|
1254
1553
|
async disconnect(session: string) {
|
|
1255
1554
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1256
1555
|
const transport = transportCache[session];
|
|
1556
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1257
1557
|
|
|
1258
1558
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1259
1559
|
// from being triggered when we cancel the connection below
|
|
@@ -1311,20 +1611,22 @@ export default class ReactNativeBleTransport {
|
|
|
1311
1611
|
delete transportCache[session];
|
|
1312
1612
|
}
|
|
1313
1613
|
this.deviceProtocol.delete(session);
|
|
1614
|
+
this.probingProtocols.delete(session);
|
|
1314
1615
|
this.deviceProtocolHints.delete(session);
|
|
1616
|
+
this.sessionProtocols.delete(session);
|
|
1617
|
+
this.protocolReprobeFailures.delete(session);
|
|
1315
1618
|
this.protocolV2Assemblers.delete(session);
|
|
1316
1619
|
this.resetProtocolV2Frames(session);
|
|
1317
1620
|
|
|
1318
1621
|
// emit the disconnect event
|
|
1319
1622
|
try {
|
|
1320
|
-
this.
|
|
1321
|
-
name: transport?.device?.name,
|
|
1322
|
-
id: session,
|
|
1323
|
-
connectId: session,
|
|
1324
|
-
});
|
|
1623
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1325
1624
|
} catch (e) {
|
|
1326
1625
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1327
1626
|
}
|
|
1627
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1628
|
+
this.monitorTokens.delete(session);
|
|
1629
|
+
}
|
|
1328
1630
|
// eslint-disable-next-line no-promise-executor-return
|
|
1329
1631
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1330
1632
|
}
|
|
@@ -1335,6 +1637,114 @@ export default class ReactNativeBleTransport {
|
|
|
1335
1637
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1336
1638
|
}
|
|
1337
1639
|
this.runPromise = null;
|
|
1640
|
+
this.runPromiseDeviceId = null;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1644
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1645
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1646
|
+
let timedOut = false;
|
|
1647
|
+
const pending = connect();
|
|
1648
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1649
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1650
|
+
pending.catch(() => undefined);
|
|
1651
|
+
try {
|
|
1652
|
+
const result = await Promise.race([
|
|
1653
|
+
pending,
|
|
1654
|
+
new Promise<never>((_, reject) => {
|
|
1655
|
+
timer = setTimeout(() => {
|
|
1656
|
+
timedOut = true;
|
|
1657
|
+
reject(
|
|
1658
|
+
ERRORS.TypedError(
|
|
1659
|
+
HardwareErrorCode.BleConnectedError,
|
|
1660
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1661
|
+
)
|
|
1662
|
+
);
|
|
1663
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1664
|
+
}),
|
|
1665
|
+
]);
|
|
1666
|
+
return result;
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1669
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1670
|
+
}
|
|
1671
|
+
throw error;
|
|
1672
|
+
} finally {
|
|
1673
|
+
if (timer) clearTimeout(timer);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1678
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1679
|
+
uuid: string,
|
|
1680
|
+
device: Device
|
|
1681
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1682
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1683
|
+
let timedOut = false;
|
|
1684
|
+
const pending = this.resolveCharacteristics(device);
|
|
1685
|
+
pending.catch(() => undefined);
|
|
1686
|
+
try {
|
|
1687
|
+
const result = await Promise.race([
|
|
1688
|
+
pending,
|
|
1689
|
+
new Promise<never>((_, reject) => {
|
|
1690
|
+
timer = setTimeout(() => {
|
|
1691
|
+
timedOut = true;
|
|
1692
|
+
reject(
|
|
1693
|
+
ERRORS.TypedError(
|
|
1694
|
+
HardwareErrorCode.BleConnectedError,
|
|
1695
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1699
|
+
}),
|
|
1700
|
+
]);
|
|
1701
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1702
|
+
return result;
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1705
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1706
|
+
}
|
|
1707
|
+
throw error;
|
|
1708
|
+
} finally {
|
|
1709
|
+
if (timer) clearTimeout(timer);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1715
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1716
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1717
|
+
*/
|
|
1718
|
+
private abandonStalledConnection(
|
|
1719
|
+
uuid: string,
|
|
1720
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1721
|
+
) {
|
|
1722
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1723
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1724
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1725
|
+
stage,
|
|
1726
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1730
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1731
|
+
});
|
|
1732
|
+
const stalled = transportCache[uuid];
|
|
1733
|
+
if (stalled) {
|
|
1734
|
+
delete transportCache[uuid];
|
|
1735
|
+
}
|
|
1736
|
+
this.deviceProtocol.delete(uuid);
|
|
1737
|
+
this.probingProtocols.delete(uuid);
|
|
1738
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1739
|
+
this.resetProtocolV2Frames(uuid);
|
|
1740
|
+
|
|
1741
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1742
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1743
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1744
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1745
|
+
this.resetPlxManager();
|
|
1746
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1747
|
+
}
|
|
1338
1748
|
}
|
|
1339
1749
|
|
|
1340
1750
|
private getCachedTransport(uuid: string) {
|
|
@@ -1345,6 +1755,109 @@ export default class ReactNativeBleTransport {
|
|
|
1345
1755
|
return transport;
|
|
1346
1756
|
}
|
|
1347
1757
|
|
|
1758
|
+
/**
|
|
1759
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1760
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1761
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1762
|
+
* cached and every later call would hang on it again.
|
|
1763
|
+
*/
|
|
1764
|
+
private async writeBlePacket(
|
|
1765
|
+
uuid: string,
|
|
1766
|
+
data: string,
|
|
1767
|
+
write: (payload: string) => Promise<unknown>,
|
|
1768
|
+
isCurrentOwner?: () => boolean
|
|
1769
|
+
) {
|
|
1770
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1771
|
+
let timedOut = false;
|
|
1772
|
+
try {
|
|
1773
|
+
await Promise.race([
|
|
1774
|
+
write(data),
|
|
1775
|
+
new Promise<never>((_, reject) => {
|
|
1776
|
+
timer = setTimeout(() => {
|
|
1777
|
+
timedOut = true;
|
|
1778
|
+
reject(
|
|
1779
|
+
ERRORS.TypedError(
|
|
1780
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1781
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1785
|
+
}),
|
|
1786
|
+
]);
|
|
1787
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
if (timedOut) {
|
|
1790
|
+
// A superseded call's late write must not tear down the link the current
|
|
1791
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1792
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1793
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1794
|
+
} else {
|
|
1795
|
+
this.tearDownWedgedLink(uuid);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
throw error;
|
|
1799
|
+
} finally {
|
|
1800
|
+
if (timer) clearTimeout(timer);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1806
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1807
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1808
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1809
|
+
*/
|
|
1810
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1811
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1812
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1813
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1814
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
const wedged = transportCache[uuid];
|
|
1818
|
+
this.disconnect(uuid).catch(error => {
|
|
1819
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1820
|
+
});
|
|
1821
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1822
|
+
delete transportCache[uuid];
|
|
1823
|
+
}
|
|
1824
|
+
this.deviceProtocol.delete(uuid);
|
|
1825
|
+
this.probingProtocols.delete(uuid);
|
|
1826
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1827
|
+
this.resetProtocolV2Frames(uuid);
|
|
1828
|
+
|
|
1829
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1830
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1831
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1832
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1833
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1834
|
+
this.resetPlxManager();
|
|
1835
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private resetPlxManager() {
|
|
1840
|
+
const manager = this.blePlxManager;
|
|
1841
|
+
this.blePlxManager = undefined;
|
|
1842
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1843
|
+
Object.keys(transportCache).forEach(key => {
|
|
1844
|
+
delete transportCache[key];
|
|
1845
|
+
});
|
|
1846
|
+
this.deviceProtocol.clear();
|
|
1847
|
+
this.probingProtocols.clear();
|
|
1848
|
+
this.sessionProtocols.clear();
|
|
1849
|
+
this.protocolReprobeFailures.clear();
|
|
1850
|
+
this.writeTimeoutCounts.clear();
|
|
1851
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
1852
|
+
this.monitorTokens.clear();
|
|
1853
|
+
this.protocolV2Assemblers.clear();
|
|
1854
|
+
try {
|
|
1855
|
+
manager?.destroy();
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1348
1861
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1349
1862
|
return ERRORS.TypedError(
|
|
1350
1863
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1355,24 +1868,44 @@ export default class ReactNativeBleTransport {
|
|
|
1355
1868
|
private createProtocolDetectionError() {
|
|
1356
1869
|
return ERRORS.TypedError(
|
|
1357
1870
|
HardwareErrorCode.BleTimeoutError,
|
|
1358
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1871
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1359
1872
|
);
|
|
1360
1873
|
}
|
|
1361
1874
|
|
|
1362
1875
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1876
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1877
|
+
this.probingProtocols.delete(uuid);
|
|
1878
|
+
}
|
|
1363
1879
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1364
1880
|
this.deviceProtocol.delete(uuid);
|
|
1365
1881
|
}
|
|
1366
1882
|
}
|
|
1367
1883
|
|
|
1884
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1885
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1886
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1368
1889
|
private async detectProtocol(
|
|
1369
1890
|
uuid: string,
|
|
1370
1891
|
expectedProtocol?: ProtocolType,
|
|
1371
|
-
protocolHint?: ProtocolType
|
|
1892
|
+
protocolHint?: ProtocolType,
|
|
1893
|
+
rebuildTransport?: () => Promise<void>
|
|
1372
1894
|
): Promise<ProtocolType> {
|
|
1895
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1896
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1897
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1898
|
+
deviceId: uuid,
|
|
1899
|
+
protocol: expectedProtocol,
|
|
1900
|
+
source: 'expected',
|
|
1901
|
+
});
|
|
1902
|
+
return expectedProtocol;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1373
1905
|
if (expectedProtocol === 'V1') {
|
|
1374
1906
|
if (await this.probeProtocolV1(uuid)) {
|
|
1375
1907
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1908
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1376
1909
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1377
1910
|
deviceId: uuid,
|
|
1378
1911
|
protocol: 'V1',
|
|
@@ -1384,33 +1917,52 @@ export default class ReactNativeBleTransport {
|
|
|
1384
1917
|
}
|
|
1385
1918
|
|
|
1386
1919
|
if (expectedProtocol === 'V2') {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1920
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1921
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1922
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1923
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1924
|
+
deviceId: uuid,
|
|
1925
|
+
protocol: 'V2',
|
|
1926
|
+
source: 'expected',
|
|
1927
|
+
});
|
|
1928
|
+
return 'V2';
|
|
1929
|
+
}
|
|
1930
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1396
1931
|
}
|
|
1397
1932
|
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1933
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1934
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1935
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1936
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1937
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1402
1938
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1939
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1940
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1941
|
+
// only adds its timeout to every poll.
|
|
1942
|
+
const trustSessionProtocol =
|
|
1943
|
+
sessionProtocol !== undefined &&
|
|
1944
|
+
!protocolHint &&
|
|
1945
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1946
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1403
1947
|
|
|
1404
1948
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1405
1949
|
const protocol = probeOrder[i];
|
|
1406
1950
|
if (i > 0) {
|
|
1407
|
-
//
|
|
1951
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1408
1952
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1953
|
+
if (!transportCache[uuid]) {
|
|
1954
|
+
if (!rebuildTransport) {
|
|
1955
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1956
|
+
}
|
|
1957
|
+
await rebuildTransport();
|
|
1958
|
+
}
|
|
1409
1959
|
}
|
|
1410
1960
|
const detected =
|
|
1411
1961
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1412
1962
|
if (detected) {
|
|
1413
1963
|
this.deviceProtocol.set(uuid, protocol);
|
|
1964
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1965
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1414
1966
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1415
1967
|
deviceId: uuid,
|
|
1416
1968
|
protocol,
|
|
@@ -1420,7 +1972,16 @@ export default class ReactNativeBleTransport {
|
|
|
1420
1972
|
}
|
|
1421
1973
|
}
|
|
1422
1974
|
|
|
1975
|
+
if (trustSessionProtocol) {
|
|
1976
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1977
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1978
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1979
|
+
} else {
|
|
1980
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1423
1983
|
this.deviceProtocol.delete(uuid);
|
|
1984
|
+
this.probingProtocols.delete(uuid);
|
|
1424
1985
|
throw this.createProtocolDetectionError();
|
|
1425
1986
|
}
|
|
1426
1987
|
|
|
@@ -1482,12 +2043,20 @@ export default class ReactNativeBleTransport {
|
|
|
1482
2043
|
}
|
|
1483
2044
|
|
|
1484
2045
|
try {
|
|
1485
|
-
this.
|
|
1486
|
-
|
|
2046
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2047
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
2048
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
2049
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2050
|
+
this.probingProtocols.delete(uuid);
|
|
1487
2051
|
return true;
|
|
1488
2052
|
} catch (error) {
|
|
1489
2053
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1490
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
2054
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2055
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2056
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2057
|
+
if (isWedgedWriteError(error)) {
|
|
2058
|
+
throw error;
|
|
2059
|
+
}
|
|
1491
2060
|
return false;
|
|
1492
2061
|
}
|
|
1493
2062
|
}
|
|
@@ -1497,7 +2066,7 @@ export default class ReactNativeBleTransport {
|
|
|
1497
2066
|
return false;
|
|
1498
2067
|
}
|
|
1499
2068
|
|
|
1500
|
-
this.
|
|
2069
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1501
2070
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1502
2071
|
const detected = await probeProtocolV2Helper({
|
|
1503
2072
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1512,6 +2081,8 @@ export default class ReactNativeBleTransport {
|
|
|
1512
2081
|
});
|
|
1513
2082
|
if (!detected) {
|
|
1514
2083
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2084
|
+
} else {
|
|
2085
|
+
this.probingProtocols.delete(uuid);
|
|
1515
2086
|
}
|
|
1516
2087
|
return detected;
|
|
1517
2088
|
}
|
|
@@ -1562,17 +2133,8 @@ export default class ReactNativeBleTransport {
|
|
|
1562
2133
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1563
2134
|
}
|
|
1564
2135
|
|
|
1565
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1566
|
-
this.protocolV2FrameQueues.clear();
|
|
1567
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1568
|
-
framePromise.reject(error);
|
|
1569
|
-
}
|
|
1570
|
-
this.protocolV2FramePromises.clear();
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
2136
|
private resetProtocolV2Frames(uuid: string) {
|
|
1574
|
-
this.
|
|
1575
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2137
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1576
2138
|
}
|
|
1577
2139
|
|
|
1578
2140
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -1601,19 +2163,95 @@ export default class ReactNativeBleTransport {
|
|
|
1601
2163
|
}
|
|
1602
2164
|
}
|
|
1603
2165
|
|
|
1604
|
-
private async
|
|
2166
|
+
private async writeProtocolV2Packet(
|
|
2167
|
+
uuid: string,
|
|
2168
|
+
transport: BleTransport,
|
|
2169
|
+
base64: string,
|
|
2170
|
+
context: ProtocolV2CallContext,
|
|
2171
|
+
assertCurrentGeneration: () => void
|
|
2172
|
+
) {
|
|
2173
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2174
|
+
platform: Platform.OS,
|
|
2175
|
+
highThroughput: context.highThroughput,
|
|
2176
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2177
|
+
characteristic: transport.writeCharacteristic,
|
|
2178
|
+
});
|
|
2179
|
+
let attempt = 0;
|
|
2180
|
+
for (;;) {
|
|
2181
|
+
assertCurrentGeneration();
|
|
2182
|
+
if (context.signal.aborted) {
|
|
2183
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2184
|
+
}
|
|
2185
|
+
try {
|
|
2186
|
+
await this.writeBlePacket(
|
|
2187
|
+
uuid,
|
|
2188
|
+
base64,
|
|
2189
|
+
payload =>
|
|
2190
|
+
shouldUseWriteWithResponse
|
|
2191
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2192
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2193
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2194
|
+
// tear down the link that the current generation is using.
|
|
2195
|
+
() => {
|
|
2196
|
+
try {
|
|
2197
|
+
assertCurrentGeneration();
|
|
2198
|
+
return !context.signal.aborted;
|
|
2199
|
+
} catch {
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
);
|
|
2204
|
+
assertCurrentGeneration();
|
|
2205
|
+
return;
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
if (
|
|
2208
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2209
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2210
|
+
) {
|
|
2211
|
+
throw error;
|
|
2212
|
+
}
|
|
2213
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2214
|
+
attempt += 1;
|
|
2215
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2216
|
+
name: context.messageName,
|
|
2217
|
+
attempt,
|
|
2218
|
+
delayMs,
|
|
2219
|
+
});
|
|
2220
|
+
await delay(delayMs);
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
private async writeProtocolV2Frame(
|
|
2226
|
+
uuid: string,
|
|
2227
|
+
transport: BleTransport,
|
|
2228
|
+
frame: Uint8Array,
|
|
2229
|
+
context: ProtocolV2CallContext,
|
|
2230
|
+
assertCurrentGeneration: () => void
|
|
2231
|
+
) {
|
|
1605
2232
|
const tuning = getProtocolV2BleTuning();
|
|
1606
2233
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1607
2234
|
platform: Platform.OS,
|
|
1608
2235
|
iosPacketLength: tuning.iosPacketLength,
|
|
1609
2236
|
androidPacketLength: tuning.androidPacketLength,
|
|
1610
|
-
mtu:
|
|
2237
|
+
mtu: transport.mtuSize,
|
|
2238
|
+
});
|
|
2239
|
+
await writeProtocolV2BleFrame({
|
|
2240
|
+
frame,
|
|
2241
|
+
packetCapacity,
|
|
2242
|
+
assertActive: assertCurrentGeneration,
|
|
2243
|
+
signal: context.signal,
|
|
2244
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2245
|
+
wait: delay,
|
|
2246
|
+
writePacket: packet =>
|
|
2247
|
+
this.writeProtocolV2Packet(
|
|
2248
|
+
uuid,
|
|
2249
|
+
transport,
|
|
2250
|
+
Buffer.from(packet).toString('base64'),
|
|
2251
|
+
context,
|
|
2252
|
+
assertCurrentGeneration
|
|
2253
|
+
),
|
|
1611
2254
|
});
|
|
1612
|
-
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1613
|
-
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1614
|
-
const base64 = Buffer.from(chunk).toString('base64');
|
|
1615
|
-
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1616
|
-
}
|
|
1617
2255
|
}
|
|
1618
2256
|
|
|
1619
2257
|
private async callProtocolV2(
|
|
@@ -1626,19 +2264,45 @@ export default class ReactNativeBleTransport {
|
|
|
1626
2264
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1627
2265
|
}
|
|
1628
2266
|
|
|
1629
|
-
const callOptions =
|
|
1630
|
-
|
|
1631
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1632
|
-
};
|
|
1633
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2267
|
+
const callOptions = options;
|
|
2268
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
1634
2269
|
|
|
1635
|
-
if (
|
|
2270
|
+
if (highThroughputWrite) {
|
|
2271
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
1636
2272
|
const tuning = getProtocolV2BleTuning();
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
2273
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2274
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2275
|
+
platform: Platform.OS,
|
|
2276
|
+
highThroughput: true,
|
|
2277
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2278
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
2279
|
+
});
|
|
2280
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2281
|
+
platform: Platform.OS,
|
|
2282
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2283
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2284
|
+
mtu: currentTransport.mtuSize,
|
|
1641
2285
|
});
|
|
2286
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2287
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2288
|
+
currentTransport.mtuSize
|
|
2289
|
+
)}:${packetCapacity}`;
|
|
2290
|
+
const loggedSignatures =
|
|
2291
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2292
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2293
|
+
loggedSignatures.add(logSignature);
|
|
2294
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2295
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2296
|
+
name,
|
|
2297
|
+
writeMode,
|
|
2298
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2299
|
+
packetCapacity,
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
if (highThroughputWrite) {
|
|
2305
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
1642
2306
|
}
|
|
1643
2307
|
|
|
1644
2308
|
try {
|
|
@@ -1652,6 +2316,90 @@ export default class ReactNativeBleTransport {
|
|
|
1652
2316
|
} catch (e) {
|
|
1653
2317
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1654
2318
|
throw e;
|
|
2319
|
+
} finally {
|
|
2320
|
+
if (highThroughputWrite) {
|
|
2321
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2327
|
+
const transport = this.getCachedTransport(uuid);
|
|
2328
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2329
|
+
|
|
2330
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2331
|
+
transport.device = refreshedDevice;
|
|
2332
|
+
transport.mtuSize =
|
|
2333
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2334
|
+
|
|
2335
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2336
|
+
throw ERRORS.TypedError(
|
|
2337
|
+
HardwareErrorCode.BleConnectedError,
|
|
2338
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2344
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2345
|
+
if (timerId !== undefined) {
|
|
2346
|
+
clearTimeout(timerId);
|
|
2347
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2352
|
+
if (Platform.OS !== 'android') return;
|
|
2353
|
+
|
|
2354
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2355
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2356
|
+
|
|
2357
|
+
const transport = transportCache[uuid];
|
|
2358
|
+
if (!transport) return;
|
|
2359
|
+
|
|
2360
|
+
try {
|
|
2361
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2362
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2363
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2364
|
+
priority: 'high',
|
|
2365
|
+
});
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2368
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2374
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2375
|
+
|
|
2376
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2377
|
+
const timerId = setTimeout(() => {
|
|
2378
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2379
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2380
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2381
|
+
);
|
|
2382
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2383
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2387
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2388
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
try {
|
|
2393
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2394
|
+
ConnectionPriority.Balanced
|
|
2395
|
+
);
|
|
2396
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2397
|
+
priority: 'balanced',
|
|
2398
|
+
});
|
|
2399
|
+
} catch (error) {
|
|
2400
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2401
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2402
|
+
});
|
|
1655
2403
|
}
|
|
1656
2404
|
}
|
|
1657
2405
|
|
|
@@ -1672,10 +2420,16 @@ export default class ReactNativeBleTransport {
|
|
|
1672
2420
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1673
2421
|
this.resetProtocolV2Frames(uuid);
|
|
1674
2422
|
},
|
|
1675
|
-
writeFrame: async (frame: Uint8Array) => {
|
|
2423
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1676
2424
|
assertCurrentGeneration();
|
|
1677
2425
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1678
|
-
await this.writeProtocolV2Frame(
|
|
2426
|
+
await this.writeProtocolV2Frame(
|
|
2427
|
+
uuid,
|
|
2428
|
+
currentTransport,
|
|
2429
|
+
frame,
|
|
2430
|
+
context,
|
|
2431
|
+
assertCurrentGeneration
|
|
2432
|
+
);
|
|
1679
2433
|
},
|
|
1680
2434
|
readFrame: async () => {
|
|
1681
2435
|
assertCurrentGeneration();
|
|
@@ -1700,6 +2454,6 @@ export default class ReactNativeBleTransport {
|
|
|
1700
2454
|
}
|
|
1701
2455
|
|
|
1702
2456
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1703
|
-
return this.
|
|
2457
|
+
return this.getActiveProtocol(path);
|
|
1704
2458
|
}
|
|
1705
2459
|
}
|