@onekeyfe/hd-transport-react-native 1.2.0-alpha.6 → 1.2.0-alpha.63
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 +1 -3
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +0 -2
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +79 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +588 -388
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- 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 +2 -43
- package/src/__tests__/BleTransport.test.ts +41 -0
- package/src/__tests__/bleStrategy.test.ts +1 -6
- package/src/__tests__/connectTimeout.test.ts +192 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/protocolV2Link.test.ts +527 -0
- package/src/__tests__/staleCallTimeout.test.ts +211 -0
- package/src/__tests__/writePacketTimeout.test.ts +306 -0
- package/src/bleStrategy.ts +0 -25
- package/src/constants.ts +7 -13
- package/src/index.ts +774 -407
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +1 -0
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -12,26 +12,30 @@ import transport, {
|
|
|
12
12
|
LogBlockCommand,
|
|
13
13
|
type OneKeyDeviceInfoBase,
|
|
14
14
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
15
16
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
16
17
|
type ProtocolType,
|
|
18
|
+
type ProtocolV2CallContext,
|
|
17
19
|
ProtocolV2FrameAssembler,
|
|
18
|
-
|
|
20
|
+
ProtocolV2LinkManager,
|
|
21
|
+
TRANSPORT_EVENT,
|
|
19
22
|
type TransportCallOptions,
|
|
20
23
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
24
|
+
writeProtocolV2BleFrame,
|
|
21
25
|
} from '@onekeyfe/hd-transport';
|
|
22
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
ERRORS,
|
|
28
|
+
HardwareErrorCode,
|
|
29
|
+
createDeferred,
|
|
30
|
+
isOnekeyBluetoothDevice,
|
|
31
|
+
} from '@onekeyfe/hd-shared';
|
|
23
32
|
|
|
24
33
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
25
|
-
import {
|
|
26
|
-
hasWritableCapability,
|
|
27
|
-
resolveBleWriteMode,
|
|
28
|
-
resolveProtocolV2PacketCapacity,
|
|
29
|
-
} from './bleStrategy';
|
|
34
|
+
import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
|
|
30
35
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
31
36
|
import {
|
|
32
37
|
ANDROID_PACKET_LENGTH,
|
|
33
38
|
IOS_PACKET_LENGTH,
|
|
34
|
-
getBleUuidKey,
|
|
35
39
|
getBluetoothServiceUuids,
|
|
36
40
|
getInfosForServiceUuid,
|
|
37
41
|
isSameBleUuid,
|
|
@@ -40,6 +44,7 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
40
44
|
import BleTransport from './BleTransport';
|
|
41
45
|
import timer from './utils/timer';
|
|
42
46
|
import { bleLogger, setBleLogger } from './logger';
|
|
47
|
+
import { createTransportCallLog } from './transportLog';
|
|
43
48
|
|
|
44
49
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
45
50
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -55,13 +60,12 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
55
60
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
56
61
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
57
62
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
58
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
59
63
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
60
64
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
61
65
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
62
66
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
63
67
|
|
|
64
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
68
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
65
69
|
type ResolvedBleCharacteristics = {
|
|
66
70
|
writeCharacteristic: Characteristic;
|
|
67
71
|
notifyCharacteristic: Characteristic;
|
|
@@ -72,7 +76,9 @@ const delay = (ms: number) =>
|
|
|
72
76
|
setTimeout(resolve, ms);
|
|
73
77
|
});
|
|
74
78
|
|
|
75
|
-
const getFirmwareUploadWriteRetryType = (
|
|
79
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
80
|
+
error: unknown
|
|
81
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
76
82
|
if (!error || typeof error !== 'object') return null;
|
|
77
83
|
const bleWriteError = error as {
|
|
78
84
|
androidErrorCode?: unknown;
|
|
@@ -83,13 +89,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
83
89
|
name?: unknown;
|
|
84
90
|
};
|
|
85
91
|
|
|
86
|
-
if (
|
|
87
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
88
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
89
|
-
) {
|
|
90
|
-
return 'reconnectable';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
92
|
if (
|
|
94
93
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
95
94
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -105,23 +104,30 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
105
104
|
|
|
106
105
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
107
106
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
108
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
109
107
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
110
108
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
111
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
111
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
112
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
113
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
114
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
115
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
116
|
+
*/
|
|
117
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
118
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
119
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
120
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
121
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
122
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
123
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
124
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
125
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
112
126
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
113
127
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
114
|
-
const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
|
|
115
|
-
const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
|
|
116
|
-
const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
|
|
117
|
-
|
|
118
128
|
export type ProtocolV2BleTuning = {
|
|
119
129
|
iosPacketLength?: number;
|
|
120
130
|
androidPacketLength?: number;
|
|
121
|
-
highVolumeWriteBurstSize?: number;
|
|
122
|
-
highVolumeWritePauseMs?: number;
|
|
123
|
-
highVolumeWriteFlushDelayMs?: number;
|
|
124
|
-
highVolumeWriteWithResponse?: boolean;
|
|
125
131
|
};
|
|
126
132
|
|
|
127
133
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
@@ -129,10 +135,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
|
129
135
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
130
136
|
iosPacketLength: IOS_PACKET_LENGTH,
|
|
131
137
|
androidPacketLength: ANDROID_PACKET_LENGTH,
|
|
132
|
-
highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
|
|
133
|
-
highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
|
|
134
|
-
highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
|
|
135
|
-
highVolumeWriteWithResponse: false,
|
|
136
138
|
};
|
|
137
139
|
|
|
138
140
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -153,27 +155,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
|
153
155
|
tuning.androidPacketLength,
|
|
154
156
|
protocolV2BleTuning.androidPacketLength
|
|
155
157
|
),
|
|
156
|
-
highVolumeWriteBurstSize: normalizePositiveInteger(
|
|
157
|
-
tuning.highVolumeWriteBurstSize,
|
|
158
|
-
protocolV2BleTuning.highVolumeWriteBurstSize
|
|
159
|
-
),
|
|
160
|
-
highVolumeWritePauseMs: normalizePositiveInteger(
|
|
161
|
-
tuning.highVolumeWritePauseMs,
|
|
162
|
-
protocolV2BleTuning.highVolumeWritePauseMs
|
|
163
|
-
),
|
|
164
|
-
highVolumeWriteFlushDelayMs: normalizePositiveInteger(
|
|
165
|
-
tuning.highVolumeWriteFlushDelayMs,
|
|
166
|
-
protocolV2BleTuning.highVolumeWriteFlushDelayMs
|
|
167
|
-
),
|
|
168
|
-
highVolumeWriteWithResponse:
|
|
169
|
-
tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
|
|
170
158
|
};
|
|
171
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
159
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
172
160
|
}
|
|
173
161
|
|
|
174
162
|
export function resetProtocolV2BleTuning() {
|
|
175
163
|
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
176
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
164
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
177
165
|
}
|
|
178
166
|
|
|
179
167
|
export function getProtocolV2BleTuning() {
|
|
@@ -188,24 +176,37 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
188
176
|
return device?.name || device?.localName || null;
|
|
189
177
|
}
|
|
190
178
|
|
|
191
|
-
function isGenericBleService(uuid?: string | null) {
|
|
192
|
-
return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function hasKnownOneKeyService(device?: Device | null) {
|
|
196
|
-
return (device?.serviceUUIDs ?? []).some(serviceUuid =>
|
|
197
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
179
|
const ANDROID_REQUEST_MTU = 256;
|
|
202
180
|
|
|
181
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
182
|
+
|
|
203
183
|
const connectOptions: Record<string, unknown> = {
|
|
204
184
|
requestMTU: ANDROID_REQUEST_MTU,
|
|
205
|
-
timeout:
|
|
185
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
206
186
|
refreshGatt: 'OnConnected',
|
|
207
187
|
};
|
|
208
188
|
|
|
189
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
190
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
191
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
196
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
197
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
198
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
199
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
200
|
+
*/
|
|
201
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
202
|
+
/** Consecutive connect timeouts on one device before the BLE manager itself is recreated. */
|
|
203
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
204
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
205
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
206
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
207
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
208
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
209
|
+
|
|
209
210
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
210
211
|
|
|
211
212
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -222,9 +223,10 @@ const requestAndroidMtu = async (device: Device) => {
|
|
|
222
223
|
|
|
223
224
|
try {
|
|
224
225
|
const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
|
|
225
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
226
|
+
Log?.debug('[ReactNativeBleTransport] MTU configured', {
|
|
227
|
+
deviceId: device.id,
|
|
226
228
|
requested: ANDROID_REQUEST_MTU,
|
|
227
|
-
|
|
229
|
+
actual: mtuDevice.mtu,
|
|
228
230
|
});
|
|
229
231
|
return mtuDevice;
|
|
230
232
|
} catch (error) {
|
|
@@ -272,6 +274,8 @@ export default class ReactNativeBleTransport {
|
|
|
272
274
|
|
|
273
275
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
274
276
|
|
|
277
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
278
|
+
|
|
275
279
|
name = 'ReactNativeBleTransport';
|
|
276
280
|
|
|
277
281
|
configured = false;
|
|
@@ -282,6 +286,8 @@ export default class ReactNativeBleTransport {
|
|
|
282
286
|
|
|
283
287
|
runPromise: Deferred<any> | null = null;
|
|
284
288
|
|
|
289
|
+
private runPromiseDeviceId: string | null = null;
|
|
290
|
+
|
|
285
291
|
emitter?: EventEmitter;
|
|
286
292
|
|
|
287
293
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -289,6 +295,20 @@ export default class ReactNativeBleTransport {
|
|
|
289
295
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
290
296
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
291
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
300
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
301
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
302
|
+
* transport that was never validated.
|
|
303
|
+
*/
|
|
304
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
305
|
+
|
|
306
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
307
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
308
|
+
|
|
309
|
+
/** Consecutive connect timeouts per device; reset by any connect that settles. */
|
|
310
|
+
private connectTimeoutCounts: Map<string, number> = new Map();
|
|
311
|
+
|
|
292
312
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
293
313
|
|
|
294
314
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -297,12 +317,31 @@ export default class ReactNativeBleTransport {
|
|
|
297
317
|
|
|
298
318
|
private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
299
319
|
|
|
300
|
-
private
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
321
|
+
getSchemas: () => {
|
|
322
|
+
if (!this._messages || !this._messagesV2) {
|
|
323
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
protocolV1: this._messages,
|
|
327
|
+
protocolV2: this._messagesV2,
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
classifyError: () => 'link-fatal',
|
|
331
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
332
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
333
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
334
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
335
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
336
|
+
await this.releaseNative(uuid, true);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
});
|
|
303
340
|
|
|
304
341
|
private monitorTokens: Map<string, number> = new Map();
|
|
305
342
|
|
|
343
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
344
|
+
|
|
306
345
|
private nextMonitorToken = 1;
|
|
307
346
|
|
|
308
347
|
constructor(options: TransportOptions) {
|
|
@@ -321,8 +360,19 @@ export default class ReactNativeBleTransport {
|
|
|
321
360
|
}
|
|
322
361
|
|
|
323
362
|
configureProtocolV2(signedData: any) {
|
|
363
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
364
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
324
369
|
this._messagesV2 = parseConfigure(signedData);
|
|
325
|
-
|
|
370
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
371
|
+
if (isReconfiguration) {
|
|
372
|
+
this.protocolV2Links
|
|
373
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
374
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
375
|
+
}
|
|
326
376
|
}
|
|
327
377
|
|
|
328
378
|
listen() {
|
|
@@ -352,29 +402,15 @@ export default class ReactNativeBleTransport {
|
|
|
352
402
|
}
|
|
353
403
|
}
|
|
354
404
|
|
|
355
|
-
let fallbackServiceUuid: string | undefined;
|
|
356
|
-
|
|
357
405
|
if (!infos) {
|
|
358
406
|
const services = await device.services();
|
|
359
407
|
Log?.debug(
|
|
360
408
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
361
409
|
services?.map(service => service.uuid)
|
|
362
410
|
);
|
|
363
|
-
|
|
364
|
-
const knownService = services.find(service =>
|
|
365
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
366
|
-
);
|
|
367
|
-
const fallbackService =
|
|
368
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
369
|
-
|
|
370
|
-
if (fallbackService) {
|
|
371
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
372
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
373
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
374
|
-
}
|
|
375
411
|
}
|
|
376
412
|
|
|
377
|
-
if (!infos
|
|
413
|
+
if (!infos) {
|
|
378
414
|
try {
|
|
379
415
|
Log?.debug('cancel connection when service not found');
|
|
380
416
|
await device.cancelConnection();
|
|
@@ -384,9 +420,7 @@ export default class ReactNativeBleTransport {
|
|
|
384
420
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
385
421
|
}
|
|
386
422
|
|
|
387
|
-
const serviceUuid = infos
|
|
388
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
389
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
423
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
390
424
|
|
|
391
425
|
if (!serviceUuid) {
|
|
392
426
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -436,6 +470,7 @@ export default class ReactNativeBleTransport {
|
|
|
436
470
|
|
|
437
471
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
438
472
|
transport.disconnectSubscription?.remove();
|
|
473
|
+
const { monitorToken } = transport;
|
|
439
474
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
440
475
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
441
476
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -445,18 +480,17 @@ export default class ReactNativeBleTransport {
|
|
|
445
480
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
446
481
|
return;
|
|
447
482
|
}
|
|
483
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
484
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
448
487
|
|
|
449
488
|
try {
|
|
450
489
|
Log?.debug('device disconnect: ', device?.id);
|
|
451
|
-
this.
|
|
452
|
-
|
|
453
|
-
id: device?.id,
|
|
454
|
-
connectId: device?.id,
|
|
455
|
-
});
|
|
456
|
-
if (this.runPromise) {
|
|
490
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
491
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
457
492
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
458
493
|
this.runPromise.reject(error);
|
|
459
|
-
this.rejectAllProtocolV2Frames(error);
|
|
460
494
|
}
|
|
461
495
|
} catch (e) {
|
|
462
496
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -466,6 +500,22 @@ export default class ReactNativeBleTransport {
|
|
|
466
500
|
});
|
|
467
501
|
}
|
|
468
502
|
|
|
503
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
504
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
508
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
512
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
513
|
+
name,
|
|
514
|
+
id: uuid,
|
|
515
|
+
connectId: uuid,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
469
519
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
470
520
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
471
521
|
try {
|
|
@@ -478,13 +528,13 @@ export default class ReactNativeBleTransport {
|
|
|
478
528
|
const isConnected = await device.isConnected().catch(() => false);
|
|
479
529
|
if (!isConnected) {
|
|
480
530
|
try {
|
|
481
|
-
device = await device.connect(connectOptions);
|
|
531
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
482
532
|
} catch (e) {
|
|
483
533
|
if (
|
|
484
534
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
485
535
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
486
536
|
) {
|
|
487
|
-
device = await device.connect();
|
|
537
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
488
538
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
489
539
|
throw e;
|
|
490
540
|
}
|
|
@@ -553,14 +603,13 @@ export default class ReactNativeBleTransport {
|
|
|
553
603
|
}
|
|
554
604
|
|
|
555
605
|
blePlxManager.startDeviceScan(
|
|
556
|
-
|
|
606
|
+
getBluetoothServiceUuids(),
|
|
557
607
|
{
|
|
558
608
|
allowDuplicates: true,
|
|
559
609
|
scanMode: ScanMode.LowLatency,
|
|
560
610
|
},
|
|
561
611
|
(error, device) => {
|
|
562
612
|
if (error) {
|
|
563
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
564
613
|
Log?.debug('ble scan error: ', error);
|
|
565
614
|
if (
|
|
566
615
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -584,33 +633,14 @@ export default class ReactNativeBleTransport {
|
|
|
584
633
|
}
|
|
585
634
|
|
|
586
635
|
const displayName = getDeviceDisplayName(device);
|
|
587
|
-
const isOneKey =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (shouldTraceCandidate) {
|
|
595
|
-
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
596
|
-
name: device?.name,
|
|
597
|
-
localName: device?.localName,
|
|
598
|
-
id: device?.id,
|
|
599
|
-
serviceUUIDs: device?.serviceUUIDs,
|
|
600
|
-
accepted: isOneKey,
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
|
|
636
|
+
const isOneKey = isOnekeyBluetoothDevice({
|
|
637
|
+
id: device?.id,
|
|
638
|
+
name: device?.name,
|
|
639
|
+
localName: device?.localName,
|
|
640
|
+
serviceUuids: device?.serviceUUIDs,
|
|
641
|
+
});
|
|
604
642
|
if (isOneKey) {
|
|
605
|
-
Log?.debug('search device start ======================');
|
|
606
|
-
const { name, localName, id, serviceUUIDs } = device ?? {};
|
|
607
|
-
Log?.debug(
|
|
608
|
-
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
609
|
-
id ?? ''
|
|
610
|
-
}\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
|
|
611
|
-
);
|
|
612
643
|
addDevice(device as unknown as Device);
|
|
613
|
-
Log?.debug('search device end ======================\n');
|
|
614
644
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
615
645
|
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
616
646
|
name: device?.name,
|
|
@@ -622,12 +652,27 @@ export default class ReactNativeBleTransport {
|
|
|
622
652
|
}
|
|
623
653
|
);
|
|
624
654
|
|
|
625
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
655
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
656
|
+
devices => {
|
|
657
|
+
for (const device of devices) {
|
|
658
|
+
const localName =
|
|
659
|
+
'localName' in device && typeof device.localName === 'string'
|
|
660
|
+
? device.localName
|
|
661
|
+
: null;
|
|
662
|
+
if (
|
|
663
|
+
isOnekeyBluetoothDevice({
|
|
664
|
+
id: device.id,
|
|
665
|
+
name: device.name,
|
|
666
|
+
localName,
|
|
667
|
+
serviceUuids: device.serviceUUIDs,
|
|
668
|
+
})
|
|
669
|
+
) {
|
|
670
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
671
|
+
addDevice(device as unknown as Device);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
629
674
|
}
|
|
630
|
-
|
|
675
|
+
);
|
|
631
676
|
|
|
632
677
|
const addDevice = (device: Device) => {
|
|
633
678
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -641,6 +686,12 @@ export default class ReactNativeBleTransport {
|
|
|
641
686
|
name: displayName,
|
|
642
687
|
commType: 'ble',
|
|
643
688
|
} as IOneKeyDevice);
|
|
689
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
690
|
+
deviceId: device.id,
|
|
691
|
+
name: displayName,
|
|
692
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
693
|
+
protocolHint,
|
|
694
|
+
});
|
|
644
695
|
}
|
|
645
696
|
};
|
|
646
697
|
|
|
@@ -651,6 +702,46 @@ export default class ReactNativeBleTransport {
|
|
|
651
702
|
});
|
|
652
703
|
}
|
|
653
704
|
|
|
705
|
+
private async installTransportForAcquire(
|
|
706
|
+
uuid: string,
|
|
707
|
+
device: Device,
|
|
708
|
+
characteristics?: ResolvedBleCharacteristics
|
|
709
|
+
) {
|
|
710
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
711
|
+
characteristics ?? (await this.resolveCharacteristics(device));
|
|
712
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
713
|
+
if (Platform.OS === 'android') {
|
|
714
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
715
|
+
}
|
|
716
|
+
const monitorToken = this.nextMonitorToken;
|
|
717
|
+
this.nextMonitorToken += 1;
|
|
718
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
719
|
+
transport.monitorToken = monitorToken;
|
|
720
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
721
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
722
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
723
|
+
transport.notifyCharacteristic,
|
|
724
|
+
uuid,
|
|
725
|
+
monitorToken,
|
|
726
|
+
notifyTransactionId
|
|
727
|
+
);
|
|
728
|
+
transportCache[uuid] = transport;
|
|
729
|
+
this.protocolV2Assemblers.set(
|
|
730
|
+
uuid,
|
|
731
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
if (Platform.OS === 'ios') {
|
|
735
|
+
await new Promise<void>(resolve => {
|
|
736
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
737
|
+
});
|
|
738
|
+
} else if (Platform.OS === 'android') {
|
|
739
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return transport;
|
|
743
|
+
}
|
|
744
|
+
|
|
654
745
|
async acquire(input: BleAcquireInput) {
|
|
655
746
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
656
747
|
|
|
@@ -684,9 +775,8 @@ export default class ReactNativeBleTransport {
|
|
|
684
775
|
if (forceCleanRunPromise && this.runPromise) {
|
|
685
776
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
686
777
|
this.runPromise.reject(error);
|
|
687
|
-
this.rejectAllProtocolV2Frames(error);
|
|
688
778
|
this.runPromise = null;
|
|
689
|
-
this.
|
|
779
|
+
this.runPromiseDeviceId = null;
|
|
690
780
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
691
781
|
}
|
|
692
782
|
|
|
@@ -722,15 +812,22 @@ export default class ReactNativeBleTransport {
|
|
|
722
812
|
if (!device) {
|
|
723
813
|
Log?.debug('try to connect to device: ', uuid);
|
|
724
814
|
try {
|
|
725
|
-
device = await
|
|
815
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
816
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
817
|
+
);
|
|
726
818
|
} catch (e) {
|
|
727
819
|
Log?.debug('try to connect to device has error: ', e);
|
|
820
|
+
if (isConnectTimeoutError(e)) {
|
|
821
|
+
throw e;
|
|
822
|
+
}
|
|
728
823
|
if (
|
|
729
824
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
730
825
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
731
826
|
) {
|
|
732
827
|
Log?.debug('first try to reconnect without params');
|
|
733
|
-
device = await
|
|
828
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
829
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
830
|
+
);
|
|
734
831
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
735
832
|
Log?.debug('device already connected');
|
|
736
833
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -746,26 +843,36 @@ export default class ReactNativeBleTransport {
|
|
|
746
843
|
|
|
747
844
|
if (!(await device.isConnected())) {
|
|
748
845
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
846
|
+
const disconnectedDevice = device;
|
|
749
847
|
|
|
750
848
|
try {
|
|
751
|
-
device = await
|
|
849
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
850
|
+
disconnectedDevice.connect(connectOptions)
|
|
851
|
+
);
|
|
752
852
|
} catch (e) {
|
|
753
853
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
854
|
+
if (isConnectTimeoutError(e)) {
|
|
855
|
+
throw e;
|
|
856
|
+
}
|
|
754
857
|
if (
|
|
755
858
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
756
859
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
757
860
|
) {
|
|
758
861
|
Log?.debug('second try to reconnect without params');
|
|
759
862
|
try {
|
|
760
|
-
device = await
|
|
863
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
864
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
865
|
+
);
|
|
761
866
|
} catch (e) {
|
|
762
867
|
Log?.debug('last try to reconnect error: ', e);
|
|
763
868
|
// last try to reconnect device if this issue exists
|
|
764
869
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
765
870
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
766
871
|
Log?.debug('last try to reconnect');
|
|
767
|
-
await
|
|
768
|
-
device = await
|
|
872
|
+
await disconnectedDevice.cancelConnection();
|
|
873
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
874
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
875
|
+
);
|
|
769
876
|
}
|
|
770
877
|
}
|
|
771
878
|
} else {
|
|
@@ -775,12 +882,16 @@ export default class ReactNativeBleTransport {
|
|
|
775
882
|
}
|
|
776
883
|
|
|
777
884
|
device = await requestAndroidMtu(device);
|
|
778
|
-
const
|
|
885
|
+
const acquiredDevice = device;
|
|
886
|
+
const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
|
|
887
|
+
acquiredDevice
|
|
888
|
+
);
|
|
779
889
|
|
|
780
890
|
const protocolHint = expectedProtocol
|
|
781
891
|
? undefined
|
|
782
|
-
:
|
|
783
|
-
|
|
892
|
+
: input.protocolHint ??
|
|
893
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
894
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
784
895
|
|
|
785
896
|
// release transport before new transport instance
|
|
786
897
|
await this.release(uuid, true);
|
|
@@ -788,45 +899,30 @@ export default class ReactNativeBleTransport {
|
|
|
788
899
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
789
900
|
}
|
|
790
901
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
const monitorToken = this.nextMonitorToken;
|
|
796
|
-
this.nextMonitorToken += 1;
|
|
797
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
798
|
-
transport.monitorToken = monitorToken;
|
|
799
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
800
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
801
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
802
|
-
transport.notifyCharacteristic,
|
|
803
|
-
uuid,
|
|
804
|
-
monitorToken,
|
|
805
|
-
notifyTransactionId
|
|
806
|
-
);
|
|
807
|
-
transportCache[uuid] = transport;
|
|
808
|
-
|
|
809
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
810
|
-
|
|
811
|
-
if (Platform.OS === 'ios') {
|
|
812
|
-
await new Promise<void>(resolve => {
|
|
813
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
814
|
-
});
|
|
815
|
-
} else if (Platform.OS === 'android') {
|
|
816
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
820
|
-
|
|
821
|
-
this.emitter?.emit('device-connect', {
|
|
822
|
-
name: device.name,
|
|
823
|
-
id: device.id,
|
|
824
|
-
connectId: device.id,
|
|
902
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
903
|
+
writeCharacteristic,
|
|
904
|
+
notifyCharacteristic,
|
|
825
905
|
});
|
|
826
906
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
907
|
+
try {
|
|
908
|
+
const protocolType = await this.detectProtocol(
|
|
909
|
+
uuid,
|
|
910
|
+
expectedProtocol,
|
|
911
|
+
protocolHint,
|
|
912
|
+
async () => {
|
|
913
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
914
|
+
}
|
|
915
|
+
);
|
|
916
|
+
const currentTransport = transportCache[uuid];
|
|
917
|
+
if (!currentTransport) {
|
|
918
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
919
|
+
}
|
|
920
|
+
this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
|
|
921
|
+
return { uuid, protocolType };
|
|
922
|
+
} catch (error) {
|
|
923
|
+
await this.release(uuid, true);
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
830
926
|
}
|
|
831
927
|
|
|
832
928
|
_monitorCharacteristic(
|
|
@@ -853,7 +949,30 @@ export default class ReactNativeBleTransport {
|
|
|
853
949
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
854
950
|
return;
|
|
855
951
|
}
|
|
856
|
-
if (this.
|
|
952
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
953
|
+
let errorCode:
|
|
954
|
+
| typeof HardwareErrorCode.BleDeviceBondError
|
|
955
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
956
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
957
|
+
| typeof HardwareErrorCode.BleTimeoutError =
|
|
958
|
+
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
959
|
+
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
960
|
+
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
961
|
+
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
962
|
+
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
963
|
+
} else if (
|
|
964
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
965
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
966
|
+
error.reason?.includes('The handle is invalid') ||
|
|
967
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
968
|
+
error.reason?.includes('notify change failed for device')
|
|
969
|
+
) {
|
|
970
|
+
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
971
|
+
}
|
|
972
|
+
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
857
976
|
let ERROR:
|
|
858
977
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
859
978
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -876,7 +995,6 @@ export default class ReactNativeBleTransport {
|
|
|
876
995
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
877
996
|
);
|
|
878
997
|
this.runPromise.reject(notifyError);
|
|
879
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
880
998
|
Log?.debug(
|
|
881
999
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
882
1000
|
);
|
|
@@ -884,7 +1002,6 @@ export default class ReactNativeBleTransport {
|
|
|
884
1002
|
}
|
|
885
1003
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
886
1004
|
this.runPromise.reject(notifyError);
|
|
887
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
888
1005
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
889
1006
|
}
|
|
890
1007
|
|
|
@@ -902,13 +1019,13 @@ export default class ReactNativeBleTransport {
|
|
|
902
1019
|
|
|
903
1020
|
try {
|
|
904
1021
|
const data = Buffer.from(c.value as string, 'base64');
|
|
905
|
-
const protocol = this.
|
|
1022
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
906
1023
|
if (!protocol) {
|
|
907
1024
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
908
1025
|
return;
|
|
909
1026
|
}
|
|
910
1027
|
if (protocol === 'V2') {
|
|
911
|
-
this.handleProtocolV2Notification(uuid, new Uint8Array(data));
|
|
1028
|
+
this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
|
|
912
1029
|
return;
|
|
913
1030
|
}
|
|
914
1031
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
@@ -929,13 +1046,18 @@ export default class ReactNativeBleTransport {
|
|
|
929
1046
|
// );
|
|
930
1047
|
bufferLength = 0;
|
|
931
1048
|
buffer = [];
|
|
932
|
-
this.
|
|
1049
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1050
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1051
|
+
}
|
|
933
1052
|
}
|
|
934
1053
|
} catch (error) {
|
|
935
1054
|
Log?.debug('monitor data error: ', error);
|
|
936
1055
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
937
|
-
this.
|
|
938
|
-
|
|
1056
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1057
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1058
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
1059
|
+
this.runPromise?.reject(notifyError);
|
|
1060
|
+
}
|
|
939
1061
|
}
|
|
940
1062
|
}, notifyTransactionId);
|
|
941
1063
|
|
|
@@ -943,13 +1065,18 @@ export default class ReactNativeBleTransport {
|
|
|
943
1065
|
}
|
|
944
1066
|
|
|
945
1067
|
async release(uuid: string, onclose = false) {
|
|
1068
|
+
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
1069
|
+
return this.releaseNative(uuid, onclose);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
946
1073
|
const transport = transportCache[uuid];
|
|
947
|
-
if (this.runPromise) {
|
|
1074
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
948
1075
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
949
1076
|
this.runPromise.reject(error);
|
|
950
1077
|
this.runPromise = null;
|
|
951
|
-
this.
|
|
952
|
-
this.
|
|
1078
|
+
this.runPromiseDeviceId = null;
|
|
1079
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
953
1080
|
} else {
|
|
954
1081
|
this.resetProtocolV2Frames(uuid);
|
|
955
1082
|
}
|
|
@@ -957,9 +1084,6 @@ export default class ReactNativeBleTransport {
|
|
|
957
1084
|
if (Platform.OS === 'android' && !onclose && transport) {
|
|
958
1085
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
959
1086
|
this.resetProtocolV2Frames(uuid);
|
|
960
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
961
|
-
this.activeProtocolV2Call = null;
|
|
962
|
-
}
|
|
963
1087
|
return Promise.resolve(true);
|
|
964
1088
|
}
|
|
965
1089
|
|
|
@@ -993,7 +1117,8 @@ export default class ReactNativeBleTransport {
|
|
|
993
1117
|
}
|
|
994
1118
|
|
|
995
1119
|
this.deviceProtocol.delete(uuid);
|
|
996
|
-
this.
|
|
1120
|
+
this.probingProtocols.delete(uuid);
|
|
1121
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
997
1122
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
998
1123
|
this.protocolV2Assemblers.delete(uuid);
|
|
999
1124
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1025,13 +1150,6 @@ export default class ReactNativeBleTransport {
|
|
|
1025
1150
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1026
1151
|
}
|
|
1027
1152
|
|
|
1028
|
-
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1029
|
-
|
|
1030
|
-
Log?.debug('transport-react-native call this.runPromise', this.runPromise);
|
|
1031
|
-
if (this.runPromise && !forceRun) {
|
|
1032
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1153
|
const protocol = this.getProtocolType(uuid);
|
|
1036
1154
|
if (!protocol) {
|
|
1037
1155
|
throw ERRORS.TypedError(
|
|
@@ -1039,31 +1157,17 @@ export default class ReactNativeBleTransport {
|
|
|
1039
1157
|
`Device protocol has not been detected for ${uuid}`
|
|
1040
1158
|
);
|
|
1041
1159
|
}
|
|
1042
|
-
|
|
1043
|
-
if (name === 'ResourceUpdate' || name === 'ResourceAck') {
|
|
1044
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
|
|
1045
|
-
file_name: data?.file_name,
|
|
1046
|
-
hash: data?.hash,
|
|
1047
|
-
});
|
|
1048
|
-
} else if (LogBlockCommand.has(name)) {
|
|
1049
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
1050
|
-
} else {
|
|
1051
|
-
Log?.debug(
|
|
1052
|
-
'transport-react-native',
|
|
1053
|
-
'call-',
|
|
1054
|
-
' name: ',
|
|
1055
|
-
name,
|
|
1056
|
-
' data: ',
|
|
1057
|
-
data,
|
|
1058
|
-
' protocol: ',
|
|
1059
|
-
protocol
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1160
|
+
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1062
1161
|
|
|
1063
1162
|
if (protocol === 'V2') {
|
|
1064
1163
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1065
1164
|
}
|
|
1066
1165
|
|
|
1166
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1167
|
+
if (this.runPromise && !forceRun) {
|
|
1168
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1067
1171
|
return this.callProtocolV1(uuid, name, data, options);
|
|
1068
1172
|
}
|
|
1069
1173
|
|
|
@@ -1080,7 +1184,25 @@ export default class ReactNativeBleTransport {
|
|
|
1080
1184
|
const transport = this.getCachedTransport(uuid);
|
|
1081
1185
|
const runPromise = createDeferred<string>();
|
|
1082
1186
|
runPromise.promise.catch(() => undefined);
|
|
1187
|
+
const supersededRunPromise = this.runPromise;
|
|
1188
|
+
if (supersededRunPromise) {
|
|
1189
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1190
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1191
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1192
|
+
// tear down the shared connection while another call is using it.
|
|
1193
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1194
|
+
}
|
|
1083
1195
|
this.runPromise = runPromise;
|
|
1196
|
+
this.runPromiseDeviceId = uuid;
|
|
1197
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1198
|
+
// only the call that still owns the slot may release it.
|
|
1199
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1200
|
+
if (this.runPromise === runPromise) {
|
|
1201
|
+
this.runPromise = null;
|
|
1202
|
+
this.runPromiseDeviceId = null;
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1084
1206
|
const messages = this._messages;
|
|
1085
1207
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1086
1208
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1106,6 +1228,9 @@ export default class ReactNativeBleTransport {
|
|
|
1106
1228
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1107
1229
|
} catch (e) {
|
|
1108
1230
|
onError(e);
|
|
1231
|
+
if (isWedgedWriteError(e)) {
|
|
1232
|
+
throw e;
|
|
1233
|
+
}
|
|
1109
1234
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1110
1235
|
}
|
|
1111
1236
|
}
|
|
@@ -1137,6 +1262,9 @@ export default class ReactNativeBleTransport {
|
|
|
1137
1262
|
}
|
|
1138
1263
|
} catch (e) {
|
|
1139
1264
|
onError(e);
|
|
1265
|
+
if (isWedgedWriteError(e)) {
|
|
1266
|
+
throw e;
|
|
1267
|
+
}
|
|
1140
1268
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1141
1269
|
}
|
|
1142
1270
|
}
|
|
@@ -1150,21 +1278,26 @@ export default class ReactNativeBleTransport {
|
|
|
1150
1278
|
if (name === 'EmmcFileWrite') {
|
|
1151
1279
|
await writeChunkedData(
|
|
1152
1280
|
buffers,
|
|
1153
|
-
data =>
|
|
1281
|
+
data =>
|
|
1282
|
+
this.writeBlePacket(
|
|
1283
|
+
uuid,
|
|
1284
|
+
data,
|
|
1285
|
+
payload => transport.writeWithRetry(payload),
|
|
1286
|
+
isCurrentOwner
|
|
1287
|
+
),
|
|
1154
1288
|
e => {
|
|
1155
|
-
|
|
1289
|
+
releaseOwnershipIfCurrent();
|
|
1156
1290
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1157
1291
|
}
|
|
1158
1292
|
);
|
|
1159
1293
|
} else if (name === 'FirmwareUpload') {
|
|
1160
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1294
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
1161
1295
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
1162
1296
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1163
1297
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1164
1298
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1165
1299
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
1166
1300
|
});
|
|
1167
|
-
|
|
1168
1301
|
await writeFirmwareUploadChunkedData(
|
|
1169
1302
|
buffers,
|
|
1170
1303
|
async data => {
|
|
@@ -1174,43 +1307,31 @@ export default class ReactNativeBleTransport {
|
|
|
1174
1307
|
// eslint-disable-next-line no-constant-condition
|
|
1175
1308
|
while (true) {
|
|
1176
1309
|
try {
|
|
1177
|
-
await
|
|
1310
|
+
await this.writeBlePacket(
|
|
1311
|
+
uuid,
|
|
1312
|
+
data,
|
|
1313
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1314
|
+
isCurrentOwner
|
|
1315
|
+
);
|
|
1178
1316
|
return;
|
|
1179
1317
|
} catch (error) {
|
|
1180
1318
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1181
1319
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1182
1320
|
throw error;
|
|
1183
1321
|
}
|
|
1184
|
-
const
|
|
1185
|
-
const delayMs = shouldReconnect
|
|
1186
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1187
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1322
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1188
1323
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1189
1324
|
attempt: attempt + 1,
|
|
1190
1325
|
delayMs,
|
|
1191
|
-
reconnect: shouldReconnect,
|
|
1192
1326
|
error,
|
|
1193
1327
|
});
|
|
1194
|
-
if (shouldReconnect) {
|
|
1195
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1196
|
-
}
|
|
1197
1328
|
await delay(delayMs);
|
|
1198
1329
|
attempt += 1;
|
|
1199
|
-
if (shouldReconnect) {
|
|
1200
|
-
try {
|
|
1201
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1202
|
-
} catch (e) {
|
|
1203
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1204
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1205
|
-
throw e;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
1330
|
}
|
|
1210
1331
|
}
|
|
1211
1332
|
},
|
|
1212
1333
|
e => {
|
|
1213
|
-
|
|
1334
|
+
releaseOwnershipIfCurrent();
|
|
1214
1335
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1215
1336
|
}
|
|
1216
1337
|
);
|
|
@@ -1218,12 +1339,19 @@ export default class ReactNativeBleTransport {
|
|
|
1218
1339
|
for (const o of buffers) {
|
|
1219
1340
|
const outData = o.toString('base64');
|
|
1220
1341
|
// Upload resources on low-end phones may OOM
|
|
1221
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
1222
1342
|
try {
|
|
1223
|
-
await
|
|
1343
|
+
await this.writeBlePacket(
|
|
1344
|
+
uuid,
|
|
1345
|
+
outData,
|
|
1346
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1347
|
+
isCurrentOwner
|
|
1348
|
+
);
|
|
1224
1349
|
} catch (e) {
|
|
1225
1350
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1226
|
-
|
|
1351
|
+
releaseOwnershipIfCurrent();
|
|
1352
|
+
if (isWedgedWriteError(e)) {
|
|
1353
|
+
throw e;
|
|
1354
|
+
}
|
|
1227
1355
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1228
1356
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1229
1357
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1256,20 +1384,33 @@ export default class ReactNativeBleTransport {
|
|
|
1256
1384
|
throw new Error('Returning data is not string.');
|
|
1257
1385
|
}
|
|
1258
1386
|
|
|
1259
|
-
Log?.debug('receive data: ', response);
|
|
1260
1387
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1261
1388
|
return check.call(jsonData);
|
|
1262
1389
|
} catch (e) {
|
|
1263
|
-
if (name === '
|
|
1264
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1390
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1391
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1265
1392
|
} else {
|
|
1266
1393
|
Log?.error('call error: ', e);
|
|
1267
1394
|
}
|
|
1395
|
+
const isProbeTimeout =
|
|
1396
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1397
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1398
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1399
|
+
// call is actively using.
|
|
1400
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1401
|
+
if (
|
|
1402
|
+
!isProbeTimeout &&
|
|
1403
|
+
!isStaleCall &&
|
|
1404
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1405
|
+
) {
|
|
1406
|
+
await this.disconnect(uuid);
|
|
1407
|
+
}
|
|
1268
1408
|
throw e;
|
|
1269
1409
|
} finally {
|
|
1270
1410
|
if (timeout) clearTimeout(timeout);
|
|
1271
1411
|
if (this.runPromise === runPromise) {
|
|
1272
1412
|
this.runPromise = null;
|
|
1413
|
+
this.runPromiseDeviceId = null;
|
|
1273
1414
|
}
|
|
1274
1415
|
}
|
|
1275
1416
|
}
|
|
@@ -1279,8 +1420,9 @@ export default class ReactNativeBleTransport {
|
|
|
1279
1420
|
}
|
|
1280
1421
|
|
|
1281
1422
|
async disconnect(session: string) {
|
|
1282
|
-
|
|
1423
|
+
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1283
1424
|
const transport = transportCache[session];
|
|
1425
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1284
1426
|
|
|
1285
1427
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1286
1428
|
// from being triggered when we cancel the connection below
|
|
@@ -1338,23 +1480,20 @@ export default class ReactNativeBleTransport {
|
|
|
1338
1480
|
delete transportCache[session];
|
|
1339
1481
|
}
|
|
1340
1482
|
this.deviceProtocol.delete(session);
|
|
1483
|
+
this.probingProtocols.delete(session);
|
|
1341
1484
|
this.deviceProtocolHints.delete(session);
|
|
1342
1485
|
this.protocolV2Assemblers.delete(session);
|
|
1343
1486
|
this.resetProtocolV2Frames(session);
|
|
1344
|
-
if (this.activeProtocolV2Call?.uuid === session) {
|
|
1345
|
-
this.activeProtocolV2Call = null;
|
|
1346
|
-
}
|
|
1347
1487
|
|
|
1348
1488
|
// emit the disconnect event
|
|
1349
1489
|
try {
|
|
1350
|
-
this.
|
|
1351
|
-
name: transport?.device?.name,
|
|
1352
|
-
id: session,
|
|
1353
|
-
connectId: session,
|
|
1354
|
-
});
|
|
1490
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1355
1491
|
} catch (e) {
|
|
1356
1492
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1357
1493
|
}
|
|
1494
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1495
|
+
this.monitorTokens.delete(session);
|
|
1496
|
+
}
|
|
1358
1497
|
// eslint-disable-next-line no-promise-executor-return
|
|
1359
1498
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1360
1499
|
}
|
|
@@ -1365,6 +1504,76 @@ export default class ReactNativeBleTransport {
|
|
|
1365
1504
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1366
1505
|
}
|
|
1367
1506
|
this.runPromise = null;
|
|
1507
|
+
this.runPromiseDeviceId = null;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1511
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1512
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1513
|
+
let timedOut = false;
|
|
1514
|
+
const pending = connect();
|
|
1515
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1516
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1517
|
+
pending.catch(() => undefined);
|
|
1518
|
+
try {
|
|
1519
|
+
const result = await Promise.race([
|
|
1520
|
+
pending,
|
|
1521
|
+
new Promise<never>((_, reject) => {
|
|
1522
|
+
timer = setTimeout(() => {
|
|
1523
|
+
timedOut = true;
|
|
1524
|
+
reject(
|
|
1525
|
+
ERRORS.TypedError(
|
|
1526
|
+
HardwareErrorCode.BleConnectedError,
|
|
1527
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1531
|
+
}),
|
|
1532
|
+
]);
|
|
1533
|
+
this.connectTimeoutCounts.delete(uuid);
|
|
1534
|
+
return result;
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
if (timedOut) {
|
|
1537
|
+
this.abandonStalledConnect(uuid);
|
|
1538
|
+
}
|
|
1539
|
+
throw error;
|
|
1540
|
+
} finally {
|
|
1541
|
+
if (timer) clearTimeout(timer);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Give up on a connect the native layer never settled. The abandoned attempt still
|
|
1547
|
+
* holds a native "connecting" entry that would cancel the NEXT attempt out from under
|
|
1548
|
+
* itself, so it is cleared here — fire and forget, because that call talks to the very
|
|
1549
|
+
* queue that just stopped responding.
|
|
1550
|
+
*/
|
|
1551
|
+
private abandonStalledConnect(uuid: string) {
|
|
1552
|
+
const timeouts = (this.connectTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1553
|
+
this.connectTimeoutCounts.set(uuid, timeouts);
|
|
1554
|
+
Log?.error('[ReactNativeBleTransport] BLE connect timed out:', uuid, {
|
|
1555
|
+
consecutiveConnectTimeouts: timeouts,
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1559
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1560
|
+
});
|
|
1561
|
+
const stalled = transportCache[uuid];
|
|
1562
|
+
if (stalled) {
|
|
1563
|
+
delete transportCache[uuid];
|
|
1564
|
+
}
|
|
1565
|
+
this.deviceProtocol.delete(uuid);
|
|
1566
|
+
this.probingProtocols.delete(uuid);
|
|
1567
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1568
|
+
this.resetProtocolV2Frames(uuid);
|
|
1569
|
+
|
|
1570
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1571
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1572
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1573
|
+
Log?.error('[ReactNativeBleTransport] BLE connects wedged repeatedly, resetting BLE manager');
|
|
1574
|
+
this.resetPlxManager();
|
|
1575
|
+
this.connectTimeoutCounts.delete(uuid);
|
|
1576
|
+
}
|
|
1368
1577
|
}
|
|
1369
1578
|
|
|
1370
1579
|
private getCachedTransport(uuid: string) {
|
|
@@ -1375,6 +1584,105 @@ export default class ReactNativeBleTransport {
|
|
|
1375
1584
|
return transport;
|
|
1376
1585
|
}
|
|
1377
1586
|
|
|
1587
|
+
/**
|
|
1588
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1589
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1590
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1591
|
+
* cached and every later call would hang on it again.
|
|
1592
|
+
*/
|
|
1593
|
+
private async writeBlePacket(
|
|
1594
|
+
uuid: string,
|
|
1595
|
+
data: string,
|
|
1596
|
+
write: (payload: string) => Promise<unknown>,
|
|
1597
|
+
isCurrentOwner?: () => boolean
|
|
1598
|
+
) {
|
|
1599
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1600
|
+
let timedOut = false;
|
|
1601
|
+
try {
|
|
1602
|
+
await Promise.race([
|
|
1603
|
+
write(data),
|
|
1604
|
+
new Promise<never>((_, reject) => {
|
|
1605
|
+
timer = setTimeout(() => {
|
|
1606
|
+
timedOut = true;
|
|
1607
|
+
reject(
|
|
1608
|
+
ERRORS.TypedError(
|
|
1609
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1610
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1611
|
+
)
|
|
1612
|
+
);
|
|
1613
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1614
|
+
}),
|
|
1615
|
+
]);
|
|
1616
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
if (timedOut) {
|
|
1619
|
+
// A superseded call's late write must not tear down the link the current
|
|
1620
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1621
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1622
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1623
|
+
} else {
|
|
1624
|
+
this.tearDownWedgedLink(uuid);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
throw error;
|
|
1628
|
+
} finally {
|
|
1629
|
+
if (timer) clearTimeout(timer);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/**
|
|
1634
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1635
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1636
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1637
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1638
|
+
*/
|
|
1639
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1640
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1641
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1642
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1643
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
const wedged = transportCache[uuid];
|
|
1647
|
+
this.disconnect(uuid).catch(error => {
|
|
1648
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1649
|
+
});
|
|
1650
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1651
|
+
delete transportCache[uuid];
|
|
1652
|
+
}
|
|
1653
|
+
this.deviceProtocol.delete(uuid);
|
|
1654
|
+
this.probingProtocols.delete(uuid);
|
|
1655
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1656
|
+
this.resetProtocolV2Frames(uuid);
|
|
1657
|
+
|
|
1658
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1659
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1660
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1661
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1662
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1663
|
+
this.resetPlxManager();
|
|
1664
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private resetPlxManager() {
|
|
1669
|
+
const manager = this.blePlxManager;
|
|
1670
|
+
this.blePlxManager = undefined;
|
|
1671
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1672
|
+
Object.keys(transportCache).forEach(key => {
|
|
1673
|
+
delete transportCache[key];
|
|
1674
|
+
});
|
|
1675
|
+
this.deviceProtocol.clear();
|
|
1676
|
+
this.probingProtocols.clear();
|
|
1677
|
+
this.monitorTokens.clear();
|
|
1678
|
+
this.protocolV2Assemblers.clear();
|
|
1679
|
+
try {
|
|
1680
|
+
manager?.destroy();
|
|
1681
|
+
} catch (error) {
|
|
1682
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1378
1686
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1379
1687
|
return ERRORS.TypedError(
|
|
1380
1688
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1385,70 +1693,99 @@ export default class ReactNativeBleTransport {
|
|
|
1385
1693
|
private createProtocolDetectionError() {
|
|
1386
1694
|
return ERRORS.TypedError(
|
|
1387
1695
|
HardwareErrorCode.BleTimeoutError,
|
|
1388
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1696
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1389
1697
|
);
|
|
1390
1698
|
}
|
|
1391
1699
|
|
|
1392
1700
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1701
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1702
|
+
this.probingProtocols.delete(uuid);
|
|
1703
|
+
}
|
|
1393
1704
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1394
1705
|
this.deviceProtocol.delete(uuid);
|
|
1395
1706
|
}
|
|
1396
1707
|
}
|
|
1397
1708
|
|
|
1709
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1710
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1711
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1398
1714
|
private async detectProtocol(
|
|
1399
1715
|
uuid: string,
|
|
1400
1716
|
expectedProtocol?: ProtocolType,
|
|
1401
|
-
protocolHint?: ProtocolType
|
|
1717
|
+
protocolHint?: ProtocolType,
|
|
1718
|
+
rebuildTransport?: () => Promise<void>
|
|
1402
1719
|
): Promise<ProtocolType> {
|
|
1403
1720
|
if (expectedProtocol === 'V1') {
|
|
1404
1721
|
if (await this.probeProtocolV1(uuid)) {
|
|
1405
1722
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1406
|
-
Log?.debug(
|
|
1723
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1724
|
+
deviceId: uuid,
|
|
1725
|
+
protocol: 'V1',
|
|
1726
|
+
source: 'expected',
|
|
1727
|
+
});
|
|
1407
1728
|
return 'V1';
|
|
1408
1729
|
}
|
|
1409
1730
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1410
1731
|
}
|
|
1411
1732
|
|
|
1412
1733
|
if (expectedProtocol === 'V2') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1734
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1735
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1736
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1737
|
+
deviceId: uuid,
|
|
1738
|
+
protocol: 'V2',
|
|
1739
|
+
source: 'expected',
|
|
1740
|
+
});
|
|
1741
|
+
return 'V2';
|
|
1742
|
+
}
|
|
1743
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1418
1744
|
}
|
|
1419
1745
|
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
// 不能作为最终结论。
|
|
1746
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1747
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1423
1748
|
const probeOrder: ProtocolType[] =
|
|
1424
1749
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1425
1750
|
|
|
1426
1751
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1427
1752
|
const protocol = probeOrder[i];
|
|
1428
1753
|
if (i > 0) {
|
|
1429
|
-
//
|
|
1754
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1430
1755
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1756
|
+
if (!transportCache[uuid]) {
|
|
1757
|
+
if (!rebuildTransport) {
|
|
1758
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1759
|
+
}
|
|
1760
|
+
await rebuildTransport();
|
|
1761
|
+
}
|
|
1431
1762
|
}
|
|
1432
1763
|
const detected =
|
|
1433
1764
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1434
1765
|
if (detected) {
|
|
1435
1766
|
this.deviceProtocol.set(uuid, protocol);
|
|
1436
|
-
Log?.debug(
|
|
1767
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1768
|
+
deviceId: uuid,
|
|
1769
|
+
protocol,
|
|
1770
|
+
source: 'probe',
|
|
1771
|
+
});
|
|
1437
1772
|
return protocol;
|
|
1438
1773
|
}
|
|
1439
1774
|
}
|
|
1440
1775
|
|
|
1441
1776
|
this.deviceProtocol.delete(uuid);
|
|
1777
|
+
this.probingProtocols.delete(uuid);
|
|
1442
1778
|
throw this.createProtocolDetectionError();
|
|
1443
1779
|
}
|
|
1444
1780
|
|
|
1445
1781
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
1446
1782
|
const transport = transportCache[uuid];
|
|
1783
|
+
await this.protocolV2Links.invalidateLink(
|
|
1784
|
+
uuid,
|
|
1785
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
1786
|
+
);
|
|
1447
1787
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1448
1788
|
this.resetProtocolV2Frames(uuid);
|
|
1449
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
1450
|
-
this.activeProtocolV2Call = null;
|
|
1451
|
-
}
|
|
1452
1789
|
if (this.runPromise) {
|
|
1453
1790
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1454
1791
|
this.runPromise.reject(error);
|
|
@@ -1499,12 +1836,20 @@ export default class ReactNativeBleTransport {
|
|
|
1499
1836
|
}
|
|
1500
1837
|
|
|
1501
1838
|
try {
|
|
1502
|
-
this.
|
|
1503
|
-
|
|
1839
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1840
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1841
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
1842
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1843
|
+
this.probingProtocols.delete(uuid);
|
|
1504
1844
|
return true;
|
|
1505
1845
|
} catch (error) {
|
|
1506
1846
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1507
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1847
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1848
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
1849
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
1850
|
+
if (isWedgedWriteError(error)) {
|
|
1851
|
+
throw error;
|
|
1852
|
+
}
|
|
1508
1853
|
return false;
|
|
1509
1854
|
}
|
|
1510
1855
|
}
|
|
@@ -1514,7 +1859,7 @@ export default class ReactNativeBleTransport {
|
|
|
1514
1859
|
return false;
|
|
1515
1860
|
}
|
|
1516
1861
|
|
|
1517
|
-
this.
|
|
1862
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1518
1863
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1519
1864
|
const detected = await probeProtocolV2Helper({
|
|
1520
1865
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1529,17 +1874,15 @@ export default class ReactNativeBleTransport {
|
|
|
1529
1874
|
});
|
|
1530
1875
|
if (!detected) {
|
|
1531
1876
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1877
|
+
} else {
|
|
1878
|
+
this.probingProtocols.delete(uuid);
|
|
1532
1879
|
}
|
|
1533
1880
|
return detected;
|
|
1534
1881
|
}
|
|
1535
1882
|
|
|
1536
|
-
private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
|
|
1883
|
+
private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
|
|
1537
1884
|
try {
|
|
1538
|
-
if (
|
|
1539
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1540
|
-
this.resetProtocolV2Frames(uuid);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1885
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) return;
|
|
1543
1886
|
|
|
1544
1887
|
if (data.length === 0) return;
|
|
1545
1888
|
|
|
@@ -1552,8 +1895,15 @@ export default class ReactNativeBleTransport {
|
|
|
1552
1895
|
} catch (error) {
|
|
1553
1896
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1554
1897
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
1898
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1899
|
+
this.protocolV2Links
|
|
1900
|
+
.invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
|
|
1901
|
+
.catch(invalidateError =>
|
|
1902
|
+
Log?.debug(
|
|
1903
|
+
'[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
|
|
1904
|
+
invalidateError
|
|
1905
|
+
)
|
|
1906
|
+
);
|
|
1557
1907
|
}
|
|
1558
1908
|
}
|
|
1559
1909
|
|
|
@@ -1576,21 +1926,17 @@ export default class ReactNativeBleTransport {
|
|
|
1576
1926
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1577
1927
|
}
|
|
1578
1928
|
|
|
1579
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1580
|
-
this.protocolV2FrameQueues.clear();
|
|
1581
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1582
|
-
framePromise.reject(error);
|
|
1583
|
-
}
|
|
1584
|
-
this.protocolV2FramePromises.clear();
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
1929
|
private resetProtocolV2Frames(uuid: string) {
|
|
1588
|
-
this.
|
|
1589
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
1930
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1590
1931
|
}
|
|
1591
1932
|
|
|
1592
|
-
private
|
|
1593
|
-
|
|
1933
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
1934
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
1935
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
1936
|
+
if (framePromise) {
|
|
1937
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1938
|
+
framePromise.reject(error);
|
|
1939
|
+
}
|
|
1594
1940
|
}
|
|
1595
1941
|
|
|
1596
1942
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -1610,10 +1956,62 @@ export default class ReactNativeBleTransport {
|
|
|
1610
1956
|
}
|
|
1611
1957
|
}
|
|
1612
1958
|
|
|
1959
|
+
private async writeProtocolV2Packet(
|
|
1960
|
+
uuid: string,
|
|
1961
|
+
transport: BleTransport,
|
|
1962
|
+
base64: string,
|
|
1963
|
+
context: ProtocolV2CallContext,
|
|
1964
|
+
assertCurrentGeneration: () => void
|
|
1965
|
+
) {
|
|
1966
|
+
let attempt = 0;
|
|
1967
|
+
for (;;) {
|
|
1968
|
+
assertCurrentGeneration();
|
|
1969
|
+
if (context.signal.aborted) {
|
|
1970
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
await this.writeBlePacket(
|
|
1974
|
+
uuid,
|
|
1975
|
+
base64,
|
|
1976
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1977
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
1978
|
+
// tear down the link that the current generation is using.
|
|
1979
|
+
() => {
|
|
1980
|
+
try {
|
|
1981
|
+
assertCurrentGeneration();
|
|
1982
|
+
return !context.signal.aborted;
|
|
1983
|
+
} catch {
|
|
1984
|
+
return false;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
);
|
|
1988
|
+
assertCurrentGeneration();
|
|
1989
|
+
return;
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
if (
|
|
1992
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
1993
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
1994
|
+
) {
|
|
1995
|
+
throw error;
|
|
1996
|
+
}
|
|
1997
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1998
|
+
attempt += 1;
|
|
1999
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2000
|
+
name: context.messageName,
|
|
2001
|
+
attempt,
|
|
2002
|
+
delayMs,
|
|
2003
|
+
});
|
|
2004
|
+
await delay(delayMs);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
|
|
1613
2009
|
private async writeProtocolV2Frame(
|
|
2010
|
+
uuid: string,
|
|
1614
2011
|
transport: BleTransport,
|
|
1615
2012
|
frame: Uint8Array,
|
|
1616
|
-
|
|
2013
|
+
context: ProtocolV2CallContext,
|
|
2014
|
+
assertCurrentGeneration: () => void
|
|
1617
2015
|
) {
|
|
1618
2016
|
const tuning = getProtocolV2BleTuning();
|
|
1619
2017
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
@@ -1622,37 +2020,25 @@ export default class ReactNativeBleTransport {
|
|
|
1622
2020
|
androidPacketLength: tuning.androidPacketLength,
|
|
1623
2021
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1624
2022
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
if (
|
|
1645
|
-
shouldThrottle &&
|
|
1646
|
-
packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
|
|
1647
|
-
offset + packetCapacity < frame.length
|
|
1648
|
-
) {
|
|
1649
|
-
await delay(tuning.highVolumeWritePauseMs);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (shouldThrottle) {
|
|
1654
|
-
await delay(tuning.highVolumeWriteFlushDelayMs);
|
|
1655
|
-
}
|
|
2023
|
+
await writeProtocolV2BleFrame({
|
|
2024
|
+
frame,
|
|
2025
|
+
packetCapacity,
|
|
2026
|
+
assertActive: assertCurrentGeneration,
|
|
2027
|
+
signal: context.signal,
|
|
2028
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2029
|
+
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
2030
|
+
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
2031
|
+
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
2032
|
+
wait: delay,
|
|
2033
|
+
writePacket: packet =>
|
|
2034
|
+
this.writeProtocolV2Packet(
|
|
2035
|
+
uuid,
|
|
2036
|
+
transport,
|
|
2037
|
+
Buffer.from(packet).toString('base64'),
|
|
2038
|
+
context,
|
|
2039
|
+
assertCurrentGeneration
|
|
2040
|
+
),
|
|
2041
|
+
});
|
|
1656
2042
|
}
|
|
1657
2043
|
|
|
1658
2044
|
private async callProtocolV2(
|
|
@@ -1665,102 +2051,83 @@ export default class ReactNativeBleTransport {
|
|
|
1665
2051
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1666
2052
|
}
|
|
1667
2053
|
|
|
1668
|
-
const
|
|
1669
|
-
if (this.runPromise) {
|
|
1670
|
-
if (!forceRun) {
|
|
1671
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1672
|
-
}
|
|
1673
|
-
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1674
|
-
this.runPromise.reject(error);
|
|
1675
|
-
this.rejectAllProtocolV2Frames(error);
|
|
1676
|
-
this.runPromise = null;
|
|
1677
|
-
this.activeProtocolV2Call = null;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const transport = this.getCachedTransport(uuid);
|
|
1681
|
-
const runPromise = createDeferred<Uint8Array>();
|
|
1682
|
-
runPromise.promise.catch(() => undefined);
|
|
1683
|
-
this.runPromise = runPromise;
|
|
1684
|
-
const callToken = this.nextProtocolV2CallToken++;
|
|
1685
|
-
this.activeProtocolV2Call = { uuid, token: callToken };
|
|
1686
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1687
|
-
this.resetProtocolV2Frames(uuid);
|
|
1688
|
-
let completed = false;
|
|
1689
|
-
const callOptions = {
|
|
1690
|
-
...options,
|
|
1691
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1692
|
-
};
|
|
2054
|
+
const callOptions = options;
|
|
1693
2055
|
const highVolumeWrite = LogBlockCommand.has(name);
|
|
1694
2056
|
|
|
1695
2057
|
if (highVolumeWrite) {
|
|
1696
2058
|
const tuning = getProtocolV2BleTuning();
|
|
1697
|
-
Log?.debug(
|
|
1698
|
-
'[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
|
|
2059
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1699
2060
|
name,
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
burstSize: tuning.highVolumeWriteBurstSize,
|
|
1704
|
-
pauseMs: tuning.highVolumeWritePauseMs,
|
|
1705
|
-
flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
|
|
1706
|
-
writeWithResponse: tuning.highVolumeWriteWithResponse,
|
|
1707
|
-
}
|
|
1708
|
-
);
|
|
2061
|
+
writeMode: 'withoutResponse',
|
|
2062
|
+
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
2063
|
+
});
|
|
1709
2064
|
}
|
|
1710
2065
|
|
|
1711
2066
|
try {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
await this.writeProtocolV2Frame(transport, frame, {
|
|
1720
|
-
highVolume: highVolumeWrite,
|
|
1721
|
-
});
|
|
1722
|
-
},
|
|
1723
|
-
readFrame: async () => {
|
|
1724
|
-
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
1725
|
-
if (!(rxFrame instanceof Uint8Array)) {
|
|
1726
|
-
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1727
|
-
}
|
|
1728
|
-
return rxFrame;
|
|
1729
|
-
},
|
|
1730
|
-
logger: Log,
|
|
1731
|
-
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1732
|
-
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
1733
|
-
ERRORS.TypedError(
|
|
1734
|
-
HardwareErrorCode.BleTimeoutError,
|
|
1735
|
-
`BLE response timeout after ${timeout}ms for ${name}`
|
|
1736
|
-
),
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
const result = await session.call(name, data, callOptions);
|
|
1740
|
-
completed = true;
|
|
1741
|
-
return result;
|
|
2067
|
+
return await this.protocolV2Links.call(
|
|
2068
|
+
uuid,
|
|
2069
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
2070
|
+
name,
|
|
2071
|
+
data,
|
|
2072
|
+
callOptions
|
|
2073
|
+
);
|
|
1742
2074
|
} catch (e) {
|
|
1743
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
1744
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1745
|
-
this.resetProtocolV2Frames(uuid);
|
|
1746
|
-
}
|
|
1747
2075
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1748
2076
|
throw e;
|
|
1749
|
-
} finally {
|
|
1750
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
1751
|
-
if (!completed) {
|
|
1752
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1753
|
-
}
|
|
1754
|
-
this.resetProtocolV2Frames(uuid);
|
|
1755
|
-
this.activeProtocolV2Call = null;
|
|
1756
|
-
}
|
|
1757
|
-
if (this.runPromise === runPromise) {
|
|
1758
|
-
this.runPromise = null;
|
|
1759
|
-
}
|
|
1760
2077
|
}
|
|
1761
2078
|
}
|
|
1762
2079
|
|
|
2080
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
2081
|
+
const generation = this.monitorTokens.get(uuid) ?? 0;
|
|
2082
|
+
const assertCurrentGeneration = () => {
|
|
2083
|
+
if (this.monitorTokens.get(uuid) !== generation) {
|
|
2084
|
+
throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
|
|
2088
|
+
return {
|
|
2089
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
2090
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
2091
|
+
generation,
|
|
2092
|
+
prepareCall: () => {
|
|
2093
|
+
assertCurrentGeneration();
|
|
2094
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2095
|
+
this.resetProtocolV2Frames(uuid);
|
|
2096
|
+
},
|
|
2097
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2098
|
+
assertCurrentGeneration();
|
|
2099
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2100
|
+
await this.writeProtocolV2Frame(
|
|
2101
|
+
uuid,
|
|
2102
|
+
currentTransport,
|
|
2103
|
+
frame,
|
|
2104
|
+
context,
|
|
2105
|
+
assertCurrentGeneration
|
|
2106
|
+
);
|
|
2107
|
+
},
|
|
2108
|
+
readFrame: async () => {
|
|
2109
|
+
assertCurrentGeneration();
|
|
2110
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
2111
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
2112
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
2113
|
+
}
|
|
2114
|
+
return rxFrame;
|
|
2115
|
+
},
|
|
2116
|
+
reset: (reason: string) => {
|
|
2117
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2118
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2119
|
+
},
|
|
2120
|
+
logger: Log,
|
|
2121
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
2122
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
2123
|
+
ERRORS.TypedError(
|
|
2124
|
+
HardwareErrorCode.BleTimeoutError,
|
|
2125
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
2126
|
+
),
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1763
2130
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1764
|
-
return this.
|
|
2131
|
+
return this.getActiveProtocol(path);
|
|
1765
2132
|
}
|
|
1766
2133
|
}
|