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