@onekeyfe/hd-transport-react-native 1.2.0-alpha.6 → 1.2.0-alpha.61
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 +21 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +428 -368
- 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 +5 -0
- package/package.json +7 -6
- package/src/BleTransport.ts +6 -41
- package/src/__tests__/BleTransport.test.ts +93 -0
- package/src/__tests__/bleStrategy.test.ts +1 -6
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +760 -0
- package/src/bleStrategy.ts +0 -25
- package/src/constants.ts +7 -13
- package/src/index.ts +500 -387
- 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,31 @@ 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
|
+
isPro2FindMyAdvertisementName,
|
|
32
|
+
} from '@onekeyfe/hd-shared';
|
|
23
33
|
|
|
24
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
25
|
-
import {
|
|
26
|
-
hasWritableCapability,
|
|
27
|
-
resolveBleWriteMode,
|
|
28
|
-
resolveProtocolV2PacketCapacity,
|
|
29
|
-
} from './bleStrategy';
|
|
35
|
+
import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
|
|
30
36
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
31
37
|
import {
|
|
32
38
|
ANDROID_PACKET_LENGTH,
|
|
33
39
|
IOS_PACKET_LENGTH,
|
|
34
|
-
getBleUuidKey,
|
|
35
40
|
getBluetoothServiceUuids,
|
|
36
41
|
getInfosForServiceUuid,
|
|
37
42
|
isSameBleUuid,
|
|
@@ -40,6 +45,7 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
40
45
|
import BleTransport from './BleTransport';
|
|
41
46
|
import timer from './utils/timer';
|
|
42
47
|
import { bleLogger, setBleLogger } from './logger';
|
|
48
|
+
import { createTransportCallLog } from './transportLog';
|
|
43
49
|
|
|
44
50
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
45
51
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -55,24 +61,53 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
55
61
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
56
62
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
57
63
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
58
|
-
const
|
|
64
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
59
65
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
60
66
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
61
67
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
62
68
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
63
69
|
|
|
64
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
70
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
65
71
|
type ResolvedBleCharacteristics = {
|
|
66
72
|
writeCharacteristic: Characteristic;
|
|
67
73
|
notifyCharacteristic: Characteristic;
|
|
68
74
|
};
|
|
69
75
|
|
|
76
|
+
const isAsciiWhitespace = (code: number) =>
|
|
77
|
+
code === 0x09 ||
|
|
78
|
+
code === 0x0a ||
|
|
79
|
+
code === 0x0b ||
|
|
80
|
+
code === 0x0c ||
|
|
81
|
+
code === 0x0d ||
|
|
82
|
+
code === 0x20;
|
|
83
|
+
|
|
84
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
85
|
+
let searchFrom = 0;
|
|
86
|
+
while (searchFrom < text.length) {
|
|
87
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
88
|
+
if (statusIndex < 0) return false;
|
|
89
|
+
|
|
90
|
+
let cursor = statusIndex + 'status'.length;
|
|
91
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
92
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
93
|
+
cursor += 1;
|
|
94
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
95
|
+
}
|
|
96
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
97
|
+
|
|
98
|
+
searchFrom = statusIndex + 'status'.length;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
const delay = (ms: number) =>
|
|
71
104
|
new Promise<void>(resolve => {
|
|
72
105
|
setTimeout(resolve, ms);
|
|
73
106
|
});
|
|
74
107
|
|
|
75
|
-
const getFirmwareUploadWriteRetryType = (
|
|
108
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
109
|
+
error: unknown
|
|
110
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
76
111
|
if (!error || typeof error !== 'object') return null;
|
|
77
112
|
const bleWriteError = error as {
|
|
78
113
|
androidErrorCode?: unknown;
|
|
@@ -83,13 +118,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
83
118
|
name?: unknown;
|
|
84
119
|
};
|
|
85
120
|
|
|
86
|
-
if (
|
|
87
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
88
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
89
|
-
) {
|
|
90
|
-
return 'reconnectable';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
121
|
if (
|
|
94
122
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
95
123
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -100,28 +128,19 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
100
128
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
101
129
|
.filter(value => typeof value === 'string')
|
|
102
130
|
.join(' ');
|
|
103
|
-
return
|
|
131
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
104
132
|
};
|
|
105
133
|
|
|
106
134
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
107
135
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
108
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
109
136
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
110
137
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
111
|
-
const DEVICE_SCAN_TIMEOUT_MS =
|
|
138
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
112
139
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
113
140
|
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
141
|
export type ProtocolV2BleTuning = {
|
|
119
142
|
iosPacketLength?: number;
|
|
120
143
|
androidPacketLength?: number;
|
|
121
|
-
highVolumeWriteBurstSize?: number;
|
|
122
|
-
highVolumeWritePauseMs?: number;
|
|
123
|
-
highVolumeWriteFlushDelayMs?: number;
|
|
124
|
-
highVolumeWriteWithResponse?: boolean;
|
|
125
144
|
};
|
|
126
145
|
|
|
127
146
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
@@ -129,10 +148,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
|
129
148
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
130
149
|
iosPacketLength: IOS_PACKET_LENGTH,
|
|
131
150
|
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
151
|
};
|
|
137
152
|
|
|
138
153
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -153,27 +168,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
|
153
168
|
tuning.androidPacketLength,
|
|
154
169
|
protocolV2BleTuning.androidPacketLength
|
|
155
170
|
),
|
|
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
171
|
};
|
|
171
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
172
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
export function resetProtocolV2BleTuning() {
|
|
175
176
|
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
176
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
177
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
export function getProtocolV2BleTuning() {
|
|
@@ -188,16 +189,6 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
188
189
|
return device?.name || device?.localName || null;
|
|
189
190
|
}
|
|
190
191
|
|
|
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
192
|
const ANDROID_REQUEST_MTU = 256;
|
|
202
193
|
|
|
203
194
|
const connectOptions: Record<string, unknown> = {
|
|
@@ -222,9 +213,10 @@ const requestAndroidMtu = async (device: Device) => {
|
|
|
222
213
|
|
|
223
214
|
try {
|
|
224
215
|
const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
|
|
225
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
216
|
+
Log?.debug('[ReactNativeBleTransport] MTU configured', {
|
|
217
|
+
deviceId: device.id,
|
|
226
218
|
requested: ANDROID_REQUEST_MTU,
|
|
227
|
-
|
|
219
|
+
actual: mtuDevice.mtu,
|
|
228
220
|
});
|
|
229
221
|
return mtuDevice;
|
|
230
222
|
} catch (error) {
|
|
@@ -272,6 +264,8 @@ export default class ReactNativeBleTransport {
|
|
|
272
264
|
|
|
273
265
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
274
266
|
|
|
267
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
268
|
+
|
|
275
269
|
name = 'ReactNativeBleTransport';
|
|
276
270
|
|
|
277
271
|
configured = false;
|
|
@@ -282,6 +276,8 @@ export default class ReactNativeBleTransport {
|
|
|
282
276
|
|
|
283
277
|
runPromise: Deferred<any> | null = null;
|
|
284
278
|
|
|
279
|
+
private runPromiseDeviceId: string | null = null;
|
|
280
|
+
|
|
285
281
|
emitter?: EventEmitter;
|
|
286
282
|
|
|
287
283
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -297,12 +293,31 @@ export default class ReactNativeBleTransport {
|
|
|
297
293
|
|
|
298
294
|
private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
299
295
|
|
|
300
|
-
private
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
297
|
+
getSchemas: () => {
|
|
298
|
+
if (!this._messages || !this._messagesV2) {
|
|
299
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
protocolV1: this._messages,
|
|
303
|
+
protocolV2: this._messagesV2,
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
classifyError: () => 'link-fatal',
|
|
307
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
308
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
309
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
310
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
311
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
312
|
+
await this.releaseNative(uuid, true);
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
});
|
|
303
316
|
|
|
304
317
|
private monitorTokens: Map<string, number> = new Map();
|
|
305
318
|
|
|
319
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
320
|
+
|
|
306
321
|
private nextMonitorToken = 1;
|
|
307
322
|
|
|
308
323
|
constructor(options: TransportOptions) {
|
|
@@ -321,8 +336,19 @@ export default class ReactNativeBleTransport {
|
|
|
321
336
|
}
|
|
322
337
|
|
|
323
338
|
configureProtocolV2(signedData: any) {
|
|
339
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
340
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
324
345
|
this._messagesV2 = parseConfigure(signedData);
|
|
325
|
-
|
|
346
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
347
|
+
if (isReconfiguration) {
|
|
348
|
+
this.protocolV2Links
|
|
349
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
350
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
351
|
+
}
|
|
326
352
|
}
|
|
327
353
|
|
|
328
354
|
listen() {
|
|
@@ -352,29 +378,15 @@ export default class ReactNativeBleTransport {
|
|
|
352
378
|
}
|
|
353
379
|
}
|
|
354
380
|
|
|
355
|
-
let fallbackServiceUuid: string | undefined;
|
|
356
|
-
|
|
357
381
|
if (!infos) {
|
|
358
382
|
const services = await device.services();
|
|
359
383
|
Log?.debug(
|
|
360
384
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
361
385
|
services?.map(service => service.uuid)
|
|
362
386
|
);
|
|
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
387
|
}
|
|
376
388
|
|
|
377
|
-
if (!infos
|
|
389
|
+
if (!infos) {
|
|
378
390
|
try {
|
|
379
391
|
Log?.debug('cancel connection when service not found');
|
|
380
392
|
await device.cancelConnection();
|
|
@@ -384,9 +396,7 @@ export default class ReactNativeBleTransport {
|
|
|
384
396
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
385
397
|
}
|
|
386
398
|
|
|
387
|
-
const serviceUuid = infos
|
|
388
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
389
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
399
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
390
400
|
|
|
391
401
|
if (!serviceUuid) {
|
|
392
402
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -436,6 +446,7 @@ export default class ReactNativeBleTransport {
|
|
|
436
446
|
|
|
437
447
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
438
448
|
transport.disconnectSubscription?.remove();
|
|
449
|
+
const { monitorToken } = transport;
|
|
439
450
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
440
451
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
441
452
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -445,18 +456,17 @@ export default class ReactNativeBleTransport {
|
|
|
445
456
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
446
457
|
return;
|
|
447
458
|
}
|
|
459
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
460
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
448
463
|
|
|
449
464
|
try {
|
|
450
465
|
Log?.debug('device disconnect: ', device?.id);
|
|
451
|
-
this.
|
|
452
|
-
|
|
453
|
-
id: device?.id,
|
|
454
|
-
connectId: device?.id,
|
|
455
|
-
});
|
|
456
|
-
if (this.runPromise) {
|
|
466
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
467
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
457
468
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
458
469
|
this.runPromise.reject(error);
|
|
459
|
-
this.rejectAllProtocolV2Frames(error);
|
|
460
470
|
}
|
|
461
471
|
} catch (e) {
|
|
462
472
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -466,6 +476,22 @@ export default class ReactNativeBleTransport {
|
|
|
466
476
|
});
|
|
467
477
|
}
|
|
468
478
|
|
|
479
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
480
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
484
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
488
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
489
|
+
name,
|
|
490
|
+
id: uuid,
|
|
491
|
+
connectId: uuid,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
469
495
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
470
496
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
471
497
|
try {
|
|
@@ -553,14 +579,13 @@ export default class ReactNativeBleTransport {
|
|
|
553
579
|
}
|
|
554
580
|
|
|
555
581
|
blePlxManager.startDeviceScan(
|
|
556
|
-
|
|
582
|
+
getBluetoothServiceUuids(),
|
|
557
583
|
{
|
|
558
584
|
allowDuplicates: true,
|
|
559
585
|
scanMode: ScanMode.LowLatency,
|
|
560
586
|
},
|
|
561
587
|
(error, device) => {
|
|
562
588
|
if (error) {
|
|
563
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
564
589
|
Log?.debug('ble scan error: ', error);
|
|
565
590
|
if (
|
|
566
591
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -584,33 +609,23 @@ export default class ReactNativeBleTransport {
|
|
|
584
609
|
}
|
|
585
610
|
|
|
586
611
|
const displayName = getDeviceDisplayName(device);
|
|
612
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
613
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
614
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
615
|
+
const isFindMyPeripheral =
|
|
616
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
617
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
587
618
|
const isOneKey =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
!!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
|
|
593
|
-
|
|
594
|
-
if (shouldTraceCandidate) {
|
|
595
|
-
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
619
|
+
!isUnnamedIOSPeripheral &&
|
|
620
|
+
!isFindMyPeripheral &&
|
|
621
|
+
isOnekeyBluetoothDevice({
|
|
622
|
+
id: device?.id,
|
|
596
623
|
name: device?.name,
|
|
597
624
|
localName: device?.localName,
|
|
598
|
-
|
|
599
|
-
serviceUUIDs: device?.serviceUUIDs,
|
|
600
|
-
accepted: isOneKey,
|
|
625
|
+
serviceUuids: device?.serviceUUIDs,
|
|
601
626
|
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
627
|
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
628
|
addDevice(device as unknown as Device);
|
|
613
|
-
Log?.debug('search device end ======================\n');
|
|
614
629
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
615
630
|
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
616
631
|
name: device?.name,
|
|
@@ -622,12 +637,32 @@ export default class ReactNativeBleTransport {
|
|
|
622
637
|
}
|
|
623
638
|
);
|
|
624
639
|
|
|
625
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
640
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
641
|
+
devices => {
|
|
642
|
+
for (const device of devices) {
|
|
643
|
+
const localName =
|
|
644
|
+
'localName' in device && typeof device.localName === 'string'
|
|
645
|
+
? device.localName
|
|
646
|
+
: null;
|
|
647
|
+
const isFindMyPeripheral =
|
|
648
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
649
|
+
isPro2FindMyAdvertisementName(localName);
|
|
650
|
+
|
|
651
|
+
if (
|
|
652
|
+
!isFindMyPeripheral &&
|
|
653
|
+
isOnekeyBluetoothDevice({
|
|
654
|
+
id: device.id,
|
|
655
|
+
name: device.name,
|
|
656
|
+
localName,
|
|
657
|
+
serviceUuids: device.serviceUUIDs,
|
|
658
|
+
})
|
|
659
|
+
) {
|
|
660
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
661
|
+
addDevice(device as unknown as Device);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
629
664
|
}
|
|
630
|
-
|
|
665
|
+
);
|
|
631
666
|
|
|
632
667
|
const addDevice = (device: Device) => {
|
|
633
668
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -641,6 +676,12 @@ export default class ReactNativeBleTransport {
|
|
|
641
676
|
name: displayName,
|
|
642
677
|
commType: 'ble',
|
|
643
678
|
} as IOneKeyDevice);
|
|
679
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
680
|
+
deviceId: device.id,
|
|
681
|
+
name: displayName,
|
|
682
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
683
|
+
protocolHint,
|
|
684
|
+
});
|
|
644
685
|
}
|
|
645
686
|
};
|
|
646
687
|
|
|
@@ -651,6 +692,46 @@ export default class ReactNativeBleTransport {
|
|
|
651
692
|
});
|
|
652
693
|
}
|
|
653
694
|
|
|
695
|
+
private async installTransportForAcquire(
|
|
696
|
+
uuid: string,
|
|
697
|
+
device: Device,
|
|
698
|
+
characteristics?: ResolvedBleCharacteristics
|
|
699
|
+
) {
|
|
700
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
701
|
+
characteristics ?? (await this.resolveCharacteristics(device));
|
|
702
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
703
|
+
if (Platform.OS === 'android') {
|
|
704
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
705
|
+
}
|
|
706
|
+
const monitorToken = this.nextMonitorToken;
|
|
707
|
+
this.nextMonitorToken += 1;
|
|
708
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
709
|
+
transport.monitorToken = monitorToken;
|
|
710
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
711
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
712
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
713
|
+
transport.notifyCharacteristic,
|
|
714
|
+
uuid,
|
|
715
|
+
monitorToken,
|
|
716
|
+
notifyTransactionId
|
|
717
|
+
);
|
|
718
|
+
transportCache[uuid] = transport;
|
|
719
|
+
this.protocolV2Assemblers.set(
|
|
720
|
+
uuid,
|
|
721
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
if (Platform.OS === 'ios') {
|
|
725
|
+
await new Promise<void>(resolve => {
|
|
726
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
727
|
+
});
|
|
728
|
+
} else if (Platform.OS === 'android') {
|
|
729
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return transport;
|
|
733
|
+
}
|
|
734
|
+
|
|
654
735
|
async acquire(input: BleAcquireInput) {
|
|
655
736
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
656
737
|
|
|
@@ -684,9 +765,8 @@ export default class ReactNativeBleTransport {
|
|
|
684
765
|
if (forceCleanRunPromise && this.runPromise) {
|
|
685
766
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
686
767
|
this.runPromise.reject(error);
|
|
687
|
-
this.rejectAllProtocolV2Frames(error);
|
|
688
768
|
this.runPromise = null;
|
|
689
|
-
this.
|
|
769
|
+
this.runPromiseDeviceId = null;
|
|
690
770
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
691
771
|
}
|
|
692
772
|
|
|
@@ -775,12 +855,16 @@ export default class ReactNativeBleTransport {
|
|
|
775
855
|
}
|
|
776
856
|
|
|
777
857
|
device = await requestAndroidMtu(device);
|
|
778
|
-
const
|
|
858
|
+
const acquiredDevice = device;
|
|
859
|
+
const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(
|
|
860
|
+
acquiredDevice
|
|
861
|
+
);
|
|
779
862
|
|
|
780
863
|
const protocolHint = expectedProtocol
|
|
781
864
|
? undefined
|
|
782
|
-
:
|
|
783
|
-
|
|
865
|
+
: input.protocolHint ??
|
|
866
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
867
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
784
868
|
|
|
785
869
|
// release transport before new transport instance
|
|
786
870
|
await this.release(uuid, true);
|
|
@@ -788,45 +872,30 @@ export default class ReactNativeBleTransport {
|
|
|
788
872
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
789
873
|
}
|
|
790
874
|
|
|
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,
|
|
875
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
876
|
+
writeCharacteristic,
|
|
877
|
+
notifyCharacteristic,
|
|
825
878
|
});
|
|
826
879
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
880
|
+
try {
|
|
881
|
+
const protocolType = await this.detectProtocol(
|
|
882
|
+
uuid,
|
|
883
|
+
expectedProtocol,
|
|
884
|
+
protocolHint,
|
|
885
|
+
async () => {
|
|
886
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
const currentTransport = transportCache[uuid];
|
|
890
|
+
if (!currentTransport) {
|
|
891
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
892
|
+
}
|
|
893
|
+
this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
|
|
894
|
+
return { uuid, protocolType };
|
|
895
|
+
} catch (error) {
|
|
896
|
+
await this.release(uuid, true);
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
830
899
|
}
|
|
831
900
|
|
|
832
901
|
_monitorCharacteristic(
|
|
@@ -853,7 +922,30 @@ export default class ReactNativeBleTransport {
|
|
|
853
922
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
854
923
|
return;
|
|
855
924
|
}
|
|
856
|
-
if (this.
|
|
925
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
926
|
+
let errorCode:
|
|
927
|
+
| typeof HardwareErrorCode.BleDeviceBondError
|
|
928
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
929
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
930
|
+
| typeof HardwareErrorCode.BleTimeoutError =
|
|
931
|
+
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
932
|
+
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
933
|
+
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
934
|
+
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
935
|
+
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
936
|
+
} else if (
|
|
937
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
938
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
939
|
+
error.reason?.includes('The handle is invalid') ||
|
|
940
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
941
|
+
error.reason?.includes('notify change failed for device')
|
|
942
|
+
) {
|
|
943
|
+
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
944
|
+
}
|
|
945
|
+
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
857
949
|
let ERROR:
|
|
858
950
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
859
951
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -876,7 +968,6 @@ export default class ReactNativeBleTransport {
|
|
|
876
968
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
877
969
|
);
|
|
878
970
|
this.runPromise.reject(notifyError);
|
|
879
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
880
971
|
Log?.debug(
|
|
881
972
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
882
973
|
);
|
|
@@ -884,7 +975,6 @@ export default class ReactNativeBleTransport {
|
|
|
884
975
|
}
|
|
885
976
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
886
977
|
this.runPromise.reject(notifyError);
|
|
887
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
888
978
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
889
979
|
}
|
|
890
980
|
|
|
@@ -908,7 +998,7 @@ export default class ReactNativeBleTransport {
|
|
|
908
998
|
return;
|
|
909
999
|
}
|
|
910
1000
|
if (protocol === 'V2') {
|
|
911
|
-
this.handleProtocolV2Notification(uuid, new Uint8Array(data));
|
|
1001
|
+
this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
|
|
912
1002
|
return;
|
|
913
1003
|
}
|
|
914
1004
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
@@ -929,13 +1019,18 @@ export default class ReactNativeBleTransport {
|
|
|
929
1019
|
// );
|
|
930
1020
|
bufferLength = 0;
|
|
931
1021
|
buffer = [];
|
|
932
|
-
this.
|
|
1022
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1023
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1024
|
+
}
|
|
933
1025
|
}
|
|
934
1026
|
} catch (error) {
|
|
935
1027
|
Log?.debug('monitor data error: ', error);
|
|
936
1028
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
937
|
-
this.
|
|
938
|
-
|
|
1029
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
1030
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1031
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
1032
|
+
this.runPromise?.reject(notifyError);
|
|
1033
|
+
}
|
|
939
1034
|
}
|
|
940
1035
|
}, notifyTransactionId);
|
|
941
1036
|
|
|
@@ -943,13 +1038,18 @@ export default class ReactNativeBleTransport {
|
|
|
943
1038
|
}
|
|
944
1039
|
|
|
945
1040
|
async release(uuid: string, onclose = false) {
|
|
1041
|
+
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
1042
|
+
return this.releaseNative(uuid, onclose);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
946
1046
|
const transport = transportCache[uuid];
|
|
947
|
-
if (this.runPromise) {
|
|
1047
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
948
1048
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
949
1049
|
this.runPromise.reject(error);
|
|
950
1050
|
this.runPromise = null;
|
|
951
|
-
this.
|
|
952
|
-
this.
|
|
1051
|
+
this.runPromiseDeviceId = null;
|
|
1052
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
953
1053
|
} else {
|
|
954
1054
|
this.resetProtocolV2Frames(uuid);
|
|
955
1055
|
}
|
|
@@ -957,9 +1057,6 @@ export default class ReactNativeBleTransport {
|
|
|
957
1057
|
if (Platform.OS === 'android' && !onclose && transport) {
|
|
958
1058
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
959
1059
|
this.resetProtocolV2Frames(uuid);
|
|
960
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
961
|
-
this.activeProtocolV2Call = null;
|
|
962
|
-
}
|
|
963
1060
|
return Promise.resolve(true);
|
|
964
1061
|
}
|
|
965
1062
|
|
|
@@ -993,7 +1090,7 @@ export default class ReactNativeBleTransport {
|
|
|
993
1090
|
}
|
|
994
1091
|
|
|
995
1092
|
this.deviceProtocol.delete(uuid);
|
|
996
|
-
|
|
1093
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
997
1094
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
998
1095
|
this.protocolV2Assemblers.delete(uuid);
|
|
999
1096
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1025,13 +1122,6 @@ export default class ReactNativeBleTransport {
|
|
|
1025
1122
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1026
1123
|
}
|
|
1027
1124
|
|
|
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
1125
|
const protocol = this.getProtocolType(uuid);
|
|
1036
1126
|
if (!protocol) {
|
|
1037
1127
|
throw ERRORS.TypedError(
|
|
@@ -1039,31 +1129,17 @@ export default class ReactNativeBleTransport {
|
|
|
1039
1129
|
`Device protocol has not been detected for ${uuid}`
|
|
1040
1130
|
);
|
|
1041
1131
|
}
|
|
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
|
-
}
|
|
1132
|
+
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1062
1133
|
|
|
1063
1134
|
if (protocol === 'V2') {
|
|
1064
1135
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1065
1136
|
}
|
|
1066
1137
|
|
|
1138
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1139
|
+
if (this.runPromise && !forceRun) {
|
|
1140
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1067
1143
|
return this.callProtocolV1(uuid, name, data, options);
|
|
1068
1144
|
}
|
|
1069
1145
|
|
|
@@ -1081,6 +1157,7 @@ export default class ReactNativeBleTransport {
|
|
|
1081
1157
|
const runPromise = createDeferred<string>();
|
|
1082
1158
|
runPromise.promise.catch(() => undefined);
|
|
1083
1159
|
this.runPromise = runPromise;
|
|
1160
|
+
this.runPromiseDeviceId = uuid;
|
|
1084
1161
|
const messages = this._messages;
|
|
1085
1162
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1086
1163
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1157,14 +1234,13 @@ export default class ReactNativeBleTransport {
|
|
|
1157
1234
|
}
|
|
1158
1235
|
);
|
|
1159
1236
|
} else if (name === 'FirmwareUpload') {
|
|
1160
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1237
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
1161
1238
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
1162
1239
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1163
1240
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1164
1241
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1165
1242
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
1166
1243
|
});
|
|
1167
|
-
|
|
1168
1244
|
await writeFirmwareUploadChunkedData(
|
|
1169
1245
|
buffers,
|
|
1170
1246
|
async data => {
|
|
@@ -1174,38 +1250,21 @@ export default class ReactNativeBleTransport {
|
|
|
1174
1250
|
// eslint-disable-next-line no-constant-condition
|
|
1175
1251
|
while (true) {
|
|
1176
1252
|
try {
|
|
1177
|
-
await transport.
|
|
1253
|
+
await transport.writeWithRetry(data);
|
|
1178
1254
|
return;
|
|
1179
1255
|
} catch (error) {
|
|
1180
1256
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1181
1257
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1182
1258
|
throw error;
|
|
1183
1259
|
}
|
|
1184
|
-
const
|
|
1185
|
-
const delayMs = shouldReconnect
|
|
1186
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1187
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1260
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1188
1261
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1189
1262
|
attempt: attempt + 1,
|
|
1190
1263
|
delayMs,
|
|
1191
|
-
reconnect: shouldReconnect,
|
|
1192
1264
|
error,
|
|
1193
1265
|
});
|
|
1194
|
-
if (shouldReconnect) {
|
|
1195
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1196
|
-
}
|
|
1197
1266
|
await delay(delayMs);
|
|
1198
1267
|
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
1268
|
}
|
|
1210
1269
|
}
|
|
1211
1270
|
},
|
|
@@ -1218,9 +1277,14 @@ export default class ReactNativeBleTransport {
|
|
|
1218
1277
|
for (const o of buffers) {
|
|
1219
1278
|
const outData = o.toString('base64');
|
|
1220
1279
|
// Upload resources on low-end phones may OOM
|
|
1221
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
1222
1280
|
try {
|
|
1223
|
-
|
|
1281
|
+
const shouldUseWriteWithResponse =
|
|
1282
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1283
|
+
if (shouldUseWriteWithResponse) {
|
|
1284
|
+
await transport.writeCharacteristic.writeWithResponse(outData);
|
|
1285
|
+
} else {
|
|
1286
|
+
await transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
1287
|
+
}
|
|
1224
1288
|
} catch (e) {
|
|
1225
1289
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1226
1290
|
this.runPromise = null;
|
|
@@ -1256,20 +1320,28 @@ export default class ReactNativeBleTransport {
|
|
|
1256
1320
|
throw new Error('Returning data is not string.');
|
|
1257
1321
|
}
|
|
1258
1322
|
|
|
1259
|
-
Log?.debug('receive data: ', response);
|
|
1260
1323
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1261
1324
|
return check.call(jsonData);
|
|
1262
1325
|
} catch (e) {
|
|
1263
|
-
if (name === '
|
|
1264
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1326
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1327
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1265
1328
|
} else {
|
|
1266
1329
|
Log?.error('call error: ', e);
|
|
1267
1330
|
}
|
|
1331
|
+
const isProbeTimeout =
|
|
1332
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1333
|
+
if (
|
|
1334
|
+
!isProbeTimeout &&
|
|
1335
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1336
|
+
) {
|
|
1337
|
+
await this.disconnect(uuid);
|
|
1338
|
+
}
|
|
1268
1339
|
throw e;
|
|
1269
1340
|
} finally {
|
|
1270
1341
|
if (timeout) clearTimeout(timeout);
|
|
1271
1342
|
if (this.runPromise === runPromise) {
|
|
1272
1343
|
this.runPromise = null;
|
|
1344
|
+
this.runPromiseDeviceId = null;
|
|
1273
1345
|
}
|
|
1274
1346
|
}
|
|
1275
1347
|
}
|
|
@@ -1279,8 +1351,9 @@ export default class ReactNativeBleTransport {
|
|
|
1279
1351
|
}
|
|
1280
1352
|
|
|
1281
1353
|
async disconnect(session: string) {
|
|
1282
|
-
|
|
1354
|
+
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1283
1355
|
const transport = transportCache[session];
|
|
1356
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1284
1357
|
|
|
1285
1358
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1286
1359
|
// from being triggered when we cancel the connection below
|
|
@@ -1341,20 +1414,16 @@ export default class ReactNativeBleTransport {
|
|
|
1341
1414
|
this.deviceProtocolHints.delete(session);
|
|
1342
1415
|
this.protocolV2Assemblers.delete(session);
|
|
1343
1416
|
this.resetProtocolV2Frames(session);
|
|
1344
|
-
if (this.activeProtocolV2Call?.uuid === session) {
|
|
1345
|
-
this.activeProtocolV2Call = null;
|
|
1346
|
-
}
|
|
1347
1417
|
|
|
1348
1418
|
// emit the disconnect event
|
|
1349
1419
|
try {
|
|
1350
|
-
this.
|
|
1351
|
-
name: transport?.device?.name,
|
|
1352
|
-
id: session,
|
|
1353
|
-
connectId: session,
|
|
1354
|
-
});
|
|
1420
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1355
1421
|
} catch (e) {
|
|
1356
1422
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1357
1423
|
}
|
|
1424
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1425
|
+
this.monitorTokens.delete(session);
|
|
1426
|
+
}
|
|
1358
1427
|
// eslint-disable-next-line no-promise-executor-return
|
|
1359
1428
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1360
1429
|
}
|
|
@@ -1365,6 +1434,7 @@ export default class ReactNativeBleTransport {
|
|
|
1365
1434
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1366
1435
|
}
|
|
1367
1436
|
this.runPromise = null;
|
|
1437
|
+
this.runPromiseDeviceId = null;
|
|
1368
1438
|
}
|
|
1369
1439
|
|
|
1370
1440
|
private getCachedTransport(uuid: string) {
|
|
@@ -1385,7 +1455,7 @@ export default class ReactNativeBleTransport {
|
|
|
1385
1455
|
private createProtocolDetectionError() {
|
|
1386
1456
|
return ERRORS.TypedError(
|
|
1387
1457
|
HardwareErrorCode.BleTimeoutError,
|
|
1388
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1458
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1389
1459
|
);
|
|
1390
1460
|
}
|
|
1391
1461
|
|
|
@@ -1398,42 +1468,71 @@ export default class ReactNativeBleTransport {
|
|
|
1398
1468
|
private async detectProtocol(
|
|
1399
1469
|
uuid: string,
|
|
1400
1470
|
expectedProtocol?: ProtocolType,
|
|
1401
|
-
protocolHint?: ProtocolType
|
|
1471
|
+
protocolHint?: ProtocolType,
|
|
1472
|
+
rebuildTransport?: () => Promise<void>
|
|
1402
1473
|
): Promise<ProtocolType> {
|
|
1474
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1475
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1476
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1477
|
+
deviceId: uuid,
|
|
1478
|
+
protocol: expectedProtocol,
|
|
1479
|
+
source: 'expected',
|
|
1480
|
+
});
|
|
1481
|
+
return expectedProtocol;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1403
1484
|
if (expectedProtocol === 'V1') {
|
|
1404
1485
|
if (await this.probeProtocolV1(uuid)) {
|
|
1405
1486
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1406
|
-
Log?.debug(
|
|
1487
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1488
|
+
deviceId: uuid,
|
|
1489
|
+
protocol: 'V1',
|
|
1490
|
+
source: 'expected',
|
|
1491
|
+
});
|
|
1407
1492
|
return 'V1';
|
|
1408
1493
|
}
|
|
1409
1494
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1410
1495
|
}
|
|
1411
1496
|
|
|
1412
1497
|
if (expectedProtocol === 'V2') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1498
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1499
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1500
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1501
|
+
deviceId: uuid,
|
|
1502
|
+
protocol: 'V2',
|
|
1503
|
+
source: 'expected',
|
|
1504
|
+
});
|
|
1505
|
+
return 'V2';
|
|
1506
|
+
}
|
|
1507
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1418
1508
|
}
|
|
1419
1509
|
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
// 不能作为最终结论。
|
|
1510
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1511
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1423
1512
|
const probeOrder: ProtocolType[] =
|
|
1424
1513
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1425
1514
|
|
|
1426
1515
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1427
1516
|
const protocol = probeOrder[i];
|
|
1428
1517
|
if (i > 0) {
|
|
1429
|
-
//
|
|
1518
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1430
1519
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1520
|
+
if (!transportCache[uuid]) {
|
|
1521
|
+
if (!rebuildTransport) {
|
|
1522
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1523
|
+
}
|
|
1524
|
+
await rebuildTransport();
|
|
1525
|
+
}
|
|
1431
1526
|
}
|
|
1432
1527
|
const detected =
|
|
1433
1528
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1434
1529
|
if (detected) {
|
|
1435
1530
|
this.deviceProtocol.set(uuid, protocol);
|
|
1436
|
-
Log?.debug(
|
|
1531
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1532
|
+
deviceId: uuid,
|
|
1533
|
+
protocol,
|
|
1534
|
+
source: 'probe',
|
|
1535
|
+
});
|
|
1437
1536
|
return protocol;
|
|
1438
1537
|
}
|
|
1439
1538
|
}
|
|
@@ -1444,11 +1543,12 @@ export default class ReactNativeBleTransport {
|
|
|
1444
1543
|
|
|
1445
1544
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
1446
1545
|
const transport = transportCache[uuid];
|
|
1546
|
+
await this.protocolV2Links.invalidateLink(
|
|
1547
|
+
uuid,
|
|
1548
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
1549
|
+
);
|
|
1447
1550
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1448
1551
|
this.resetProtocolV2Frames(uuid);
|
|
1449
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
1450
|
-
this.activeProtocolV2Call = null;
|
|
1451
|
-
}
|
|
1452
1552
|
if (this.runPromise) {
|
|
1453
1553
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1454
1554
|
this.runPromise.reject(error);
|
|
@@ -1500,11 +1600,13 @@ export default class ReactNativeBleTransport {
|
|
|
1500
1600
|
|
|
1501
1601
|
try {
|
|
1502
1602
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1503
|
-
|
|
1603
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1604
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
1605
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1504
1606
|
return true;
|
|
1505
1607
|
} catch (error) {
|
|
1506
1608
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1507
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1609
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1508
1610
|
return false;
|
|
1509
1611
|
}
|
|
1510
1612
|
}
|
|
@@ -1533,13 +1635,9 @@ export default class ReactNativeBleTransport {
|
|
|
1533
1635
|
return detected;
|
|
1534
1636
|
}
|
|
1535
1637
|
|
|
1536
|
-
private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
|
|
1638
|
+
private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
|
|
1537
1639
|
try {
|
|
1538
|
-
if (
|
|
1539
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1540
|
-
this.resetProtocolV2Frames(uuid);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1640
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) return;
|
|
1543
1641
|
|
|
1544
1642
|
if (data.length === 0) return;
|
|
1545
1643
|
|
|
@@ -1552,8 +1650,15 @@ export default class ReactNativeBleTransport {
|
|
|
1552
1650
|
} catch (error) {
|
|
1553
1651
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1554
1652
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
1653
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1654
|
+
this.protocolV2Links
|
|
1655
|
+
.invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
|
|
1656
|
+
.catch(invalidateError =>
|
|
1657
|
+
Log?.debug(
|
|
1658
|
+
'[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
|
|
1659
|
+
invalidateError
|
|
1660
|
+
)
|
|
1661
|
+
);
|
|
1557
1662
|
}
|
|
1558
1663
|
}
|
|
1559
1664
|
|
|
@@ -1576,21 +1681,17 @@ export default class ReactNativeBleTransport {
|
|
|
1576
1681
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1577
1682
|
}
|
|
1578
1683
|
|
|
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
1684
|
private resetProtocolV2Frames(uuid: string) {
|
|
1588
|
-
this.
|
|
1589
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
1685
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1590
1686
|
}
|
|
1591
1687
|
|
|
1592
|
-
private
|
|
1593
|
-
|
|
1688
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
1689
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
1690
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
1691
|
+
if (framePromise) {
|
|
1692
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1693
|
+
framePromise.reject(error);
|
|
1694
|
+
}
|
|
1594
1695
|
}
|
|
1595
1696
|
|
|
1596
1697
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -1610,10 +1711,53 @@ export default class ReactNativeBleTransport {
|
|
|
1610
1711
|
}
|
|
1611
1712
|
}
|
|
1612
1713
|
|
|
1714
|
+
private async writeProtocolV2Packet(
|
|
1715
|
+
transport: BleTransport,
|
|
1716
|
+
base64: string,
|
|
1717
|
+
context: ProtocolV2CallContext,
|
|
1718
|
+
assertCurrentGeneration: () => void
|
|
1719
|
+
) {
|
|
1720
|
+
const shouldUseWriteWithResponse =
|
|
1721
|
+
transport.writeCharacteristic.isWritableWithResponse &&
|
|
1722
|
+
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
1723
|
+
let attempt = 0;
|
|
1724
|
+
for (;;) {
|
|
1725
|
+
assertCurrentGeneration();
|
|
1726
|
+
if (context.signal.aborted) {
|
|
1727
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1728
|
+
}
|
|
1729
|
+
try {
|
|
1730
|
+
if (shouldUseWriteWithResponse) {
|
|
1731
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1732
|
+
} else {
|
|
1733
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1734
|
+
}
|
|
1735
|
+
assertCurrentGeneration();
|
|
1736
|
+
return;
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (
|
|
1739
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
1740
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
1741
|
+
) {
|
|
1742
|
+
throw error;
|
|
1743
|
+
}
|
|
1744
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1745
|
+
attempt += 1;
|
|
1746
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
1747
|
+
name: context.messageName,
|
|
1748
|
+
attempt,
|
|
1749
|
+
delayMs,
|
|
1750
|
+
});
|
|
1751
|
+
await delay(delayMs);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1613
1756
|
private async writeProtocolV2Frame(
|
|
1614
1757
|
transport: BleTransport,
|
|
1615
1758
|
frame: Uint8Array,
|
|
1616
|
-
|
|
1759
|
+
context: ProtocolV2CallContext,
|
|
1760
|
+
assertCurrentGeneration: () => void
|
|
1617
1761
|
) {
|
|
1618
1762
|
const tuning = getProtocolV2BleTuning();
|
|
1619
1763
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
@@ -1622,37 +1766,31 @@ export default class ReactNativeBleTransport {
|
|
|
1622
1766
|
androidPacketLength: tuning.androidPacketLength,
|
|
1623
1767
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1624
1768
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (shouldThrottle) {
|
|
1654
|
-
await delay(tuning.highVolumeWriteFlushDelayMs);
|
|
1655
|
-
}
|
|
1769
|
+
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
1770
|
+
// before the next single-packet control command is written.
|
|
1771
|
+
const initialDelayMs =
|
|
1772
|
+
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1773
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1774
|
+
: 0;
|
|
1775
|
+
await writeProtocolV2BleFrame({
|
|
1776
|
+
frame,
|
|
1777
|
+
packetCapacity,
|
|
1778
|
+
assertActive: assertCurrentGeneration,
|
|
1779
|
+
signal: context.signal,
|
|
1780
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1781
|
+
initialDelayMs,
|
|
1782
|
+
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1783
|
+
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1784
|
+
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1785
|
+
wait: delay,
|
|
1786
|
+
writePacket: packet =>
|
|
1787
|
+
this.writeProtocolV2Packet(
|
|
1788
|
+
transport,
|
|
1789
|
+
Buffer.from(packet).toString('base64'),
|
|
1790
|
+
context,
|
|
1791
|
+
assertCurrentGeneration
|
|
1792
|
+
),
|
|
1793
|
+
});
|
|
1656
1794
|
}
|
|
1657
1795
|
|
|
1658
1796
|
private async callProtocolV2(
|
|
@@ -1665,101 +1803,76 @@ export default class ReactNativeBleTransport {
|
|
|
1665
1803
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1666
1804
|
}
|
|
1667
1805
|
|
|
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
|
-
};
|
|
1806
|
+
const callOptions = options;
|
|
1693
1807
|
const highVolumeWrite = LogBlockCommand.has(name);
|
|
1694
1808
|
|
|
1695
1809
|
if (highVolumeWrite) {
|
|
1696
1810
|
const tuning = getProtocolV2BleTuning();
|
|
1697
|
-
Log?.debug(
|
|
1698
|
-
'[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
|
|
1811
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1699
1812
|
name,
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
burstSize: tuning.highVolumeWriteBurstSize,
|
|
1704
|
-
pauseMs: tuning.highVolumeWritePauseMs,
|
|
1705
|
-
flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
|
|
1706
|
-
writeWithResponse: tuning.highVolumeWriteWithResponse,
|
|
1707
|
-
}
|
|
1708
|
-
);
|
|
1813
|
+
writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
|
|
1814
|
+
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1815
|
+
});
|
|
1709
1816
|
}
|
|
1710
1817
|
|
|
1711
1818
|
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;
|
|
1819
|
+
return await this.protocolV2Links.call(
|
|
1820
|
+
uuid,
|
|
1821
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
1822
|
+
name,
|
|
1823
|
+
data,
|
|
1824
|
+
callOptions
|
|
1825
|
+
);
|
|
1742
1826
|
} catch (e) {
|
|
1743
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
1744
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1745
|
-
this.resetProtocolV2Frames(uuid);
|
|
1746
|
-
}
|
|
1747
1827
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1748
1828
|
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
1829
|
}
|
|
1761
1830
|
}
|
|
1762
1831
|
|
|
1832
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
1833
|
+
const generation = this.monitorTokens.get(uuid) ?? 0;
|
|
1834
|
+
const assertCurrentGeneration = () => {
|
|
1835
|
+
if (this.monitorTokens.get(uuid) !== generation) {
|
|
1836
|
+
throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
|
|
1840
|
+
return {
|
|
1841
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
1842
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
1843
|
+
generation,
|
|
1844
|
+
prepareCall: () => {
|
|
1845
|
+
assertCurrentGeneration();
|
|
1846
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1847
|
+
this.resetProtocolV2Frames(uuid);
|
|
1848
|
+
},
|
|
1849
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1850
|
+
assertCurrentGeneration();
|
|
1851
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
1852
|
+
await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
|
|
1853
|
+
},
|
|
1854
|
+
readFrame: async () => {
|
|
1855
|
+
assertCurrentGeneration();
|
|
1856
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
1857
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
1858
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1859
|
+
}
|
|
1860
|
+
return rxFrame;
|
|
1861
|
+
},
|
|
1862
|
+
reset: (reason: string) => {
|
|
1863
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1864
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1865
|
+
},
|
|
1866
|
+
logger: Log,
|
|
1867
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1868
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
1869
|
+
ERRORS.TypedError(
|
|
1870
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1871
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
1872
|
+
),
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1763
1876
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1764
1877
|
return this.deviceProtocol.get(path);
|
|
1765
1878
|
}
|