@onekeyfe/hd-transport-react-native 1.1.34-alpha.2 → 1.1.34-alpha.3
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/BleManager.d.ts.map +1 -1
- package/dist/BleTransport.d.ts +2 -0
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +13 -0
- package/dist/bleStrategy.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +62 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +799 -150
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/dist/transportLog.d.ts +13 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/BleManager.ts +11 -14
- package/src/BleTransport.ts +9 -5
- package/src/__tests__/bleStrategy.test.ts +42 -0
- package/src/__tests__/constants.test.ts +18 -0
- package/src/__tests__/protocolV2Link.test.ts +243 -0
- package/src/bleStrategy.ts +35 -0
- package/src/constants.ts +33 -1
- package/src/index.ts +836 -91
- package/src/logger.ts +19 -0
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +18 -0
- package/src/types.ts +3 -0
- package/src/utils/validateNotify.ts +4 -4
package/src/index.ts
CHANGED
|
@@ -9,33 +9,44 @@ import {
|
|
|
9
9
|
} from 'react-native-ble-plx';
|
|
10
10
|
import ByteBuffer from 'bytebuffer';
|
|
11
11
|
import transport, {
|
|
12
|
-
COMMON_HEADER_SIZE,
|
|
13
12
|
LogBlockCommand,
|
|
14
13
|
type OneKeyDeviceInfoBase,
|
|
14
|
+
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
|
+
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
17
|
+
type ProtocolType,
|
|
18
|
+
ProtocolV2FrameAssembler,
|
|
19
|
+
ProtocolV2LinkManager,
|
|
20
|
+
type TransportCallOptions,
|
|
21
|
+
probeProtocolV2 as probeProtocolV2Helper,
|
|
15
22
|
} from '@onekeyfe/hd-transport';
|
|
16
23
|
import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
|
|
17
|
-
import { LoggerNames, getLogger } from '@onekeyfe/hd-core';
|
|
18
24
|
|
|
19
25
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
26
|
+
import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
|
|
20
27
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
21
28
|
import {
|
|
22
29
|
ANDROID_PACKET_LENGTH,
|
|
23
30
|
IOS_PACKET_LENGTH,
|
|
31
|
+
getBleUuidKey,
|
|
24
32
|
getBluetoothServiceUuids,
|
|
25
33
|
getInfosForServiceUuid,
|
|
34
|
+
isSameBleUuid,
|
|
26
35
|
} from './constants';
|
|
27
36
|
import { isHeaderChunk } from './utils/validateNotify';
|
|
28
37
|
import BleTransport from './BleTransport';
|
|
29
38
|
import timer from './utils/timer';
|
|
39
|
+
import { bleLogger, setBleLogger } from './logger';
|
|
40
|
+
import { createTransportCallLog } from './transportLog';
|
|
30
41
|
|
|
31
42
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
32
43
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
33
44
|
import type EventEmitter from 'events';
|
|
34
45
|
import type { BleAcquireInput, TransportOptions } from './types';
|
|
35
46
|
|
|
36
|
-
const { check,
|
|
47
|
+
const { check, ProtocolV1, parseConfigure } = transport;
|
|
37
48
|
|
|
38
|
-
const Log =
|
|
49
|
+
const Log = bleLogger;
|
|
39
50
|
|
|
40
51
|
const transportCache: Record<string, BleTransport> = {};
|
|
41
52
|
const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
@@ -54,11 +65,6 @@ type ResolvedBleCharacteristics = {
|
|
|
54
65
|
notifyCharacteristic: Characteristic;
|
|
55
66
|
};
|
|
56
67
|
|
|
57
|
-
const getBleIdentityName = (device?: { name?: string | null } | null): string | null => {
|
|
58
|
-
const localName = (device as { localName?: string | null } | undefined)?.localName;
|
|
59
|
-
return device?.name ?? localName ?? null;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
68
|
const delay = (ms: number) =>
|
|
63
69
|
new Promise<void>(resolve => {
|
|
64
70
|
setTimeout(resolve, ms);
|
|
@@ -97,9 +103,76 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
97
103
|
|
|
98
104
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
99
105
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
106
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
107
|
+
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
108
|
+
const DEVICE_SCAN_TIMEOUT_MS = 8000;
|
|
109
|
+
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
110
|
+
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
111
|
+
export type ProtocolV2BleTuning = {
|
|
112
|
+
iosPacketLength?: number;
|
|
113
|
+
androidPacketLength?: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
117
|
+
|
|
118
|
+
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
119
|
+
iosPacketLength: IOS_PACKET_LENGTH,
|
|
120
|
+
androidPacketLength: ANDROID_PACKET_LENGTH,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
124
|
+
|
|
125
|
+
const normalizePositiveInteger = (value: unknown, fallback: number) => {
|
|
126
|
+
const normalized = Number(value);
|
|
127
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
|
|
128
|
+
return Math.floor(normalized);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
132
|
+
protocolV2BleTuning = {
|
|
133
|
+
iosPacketLength: normalizePositiveInteger(
|
|
134
|
+
tuning.iosPacketLength,
|
|
135
|
+
protocolV2BleTuning.iosPacketLength
|
|
136
|
+
),
|
|
137
|
+
androidPacketLength: normalizePositiveInteger(
|
|
138
|
+
tuning.androidPacketLength,
|
|
139
|
+
protocolV2BleTuning.androidPacketLength
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resetProtocolV2BleTuning() {
|
|
146
|
+
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
147
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getProtocolV2BleTuning() {
|
|
151
|
+
return { ...protocolV2BleTuning };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
155
|
+
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getDeviceDisplayName(device?: Device | null) {
|
|
159
|
+
return device?.name || device?.localName || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isGenericBleService(uuid?: string | null) {
|
|
163
|
+
return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
|
|
164
|
+
}
|
|
100
165
|
|
|
101
|
-
|
|
102
|
-
|
|
166
|
+
function hasKnownOneKeyService(device?: Device | null) {
|
|
167
|
+
return (device?.serviceUUIDs ?? []).some(serviceUuid =>
|
|
168
|
+
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ANDROID_REQUEST_MTU = 256;
|
|
173
|
+
|
|
174
|
+
const connectOptions: Record<string, unknown> = {
|
|
175
|
+
requestMTU: ANDROID_REQUEST_MTU,
|
|
103
176
|
timeout: 3000,
|
|
104
177
|
refreshGatt: 'OnConnected',
|
|
105
178
|
};
|
|
@@ -108,12 +181,30 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
|
108
181
|
|
|
109
182
|
const tryToGetConfiguration = (device: Device) => {
|
|
110
183
|
if (!device || !device.serviceUUIDs) return null;
|
|
111
|
-
const
|
|
184
|
+
const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
|
|
185
|
+
if (!serviceUUID) return null;
|
|
112
186
|
const infos = getInfosForServiceUuid(serviceUUID, 'classic');
|
|
113
187
|
if (!infos) return null;
|
|
114
188
|
return infos;
|
|
115
189
|
};
|
|
116
190
|
|
|
191
|
+
const requestAndroidMtu = async (device: Device) => {
|
|
192
|
+
if (Platform.OS !== 'android') return device;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
|
|
196
|
+
Log?.debug('[ReactNativeBleTransport] MTU configured', {
|
|
197
|
+
deviceId: device.id,
|
|
198
|
+
requested: ANDROID_REQUEST_MTU,
|
|
199
|
+
actual: mtuDevice.mtu,
|
|
200
|
+
});
|
|
201
|
+
return mtuDevice;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
Log?.debug('[ReactNativeBleTransport] Android MTU request failed:', error);
|
|
204
|
+
return device;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
117
208
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
118
209
|
|
|
119
210
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -151,13 +242,15 @@ export default class ReactNativeBleTransport {
|
|
|
151
242
|
|
|
152
243
|
_messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
153
244
|
|
|
245
|
+
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
246
|
+
|
|
154
247
|
name = 'ReactNativeBleTransport';
|
|
155
248
|
|
|
156
249
|
configured = false;
|
|
157
250
|
|
|
158
251
|
stopped = false;
|
|
159
252
|
|
|
160
|
-
scanTimeout =
|
|
253
|
+
scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
|
|
161
254
|
|
|
162
255
|
runPromise: Deferred<any> | null = null;
|
|
163
256
|
|
|
@@ -165,11 +258,48 @@ export default class ReactNativeBleTransport {
|
|
|
165
258
|
|
|
166
259
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
167
260
|
|
|
261
|
+
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
262
|
+
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
263
|
+
|
|
264
|
+
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
265
|
+
|
|
266
|
+
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
267
|
+
|
|
268
|
+
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
269
|
+
|
|
270
|
+
private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
271
|
+
|
|
272
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
273
|
+
getSchemas: () => {
|
|
274
|
+
if (!this._messages || !this._messagesV2) {
|
|
275
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
protocolV1: this._messages,
|
|
279
|
+
protocolV2: this._messagesV2,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
classifyError: () => 'link-fatal',
|
|
283
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
284
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
285
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
286
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
287
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
288
|
+
await this.release(uuid, true);
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
private monitorTokens: Map<string, number> = new Map();
|
|
294
|
+
|
|
295
|
+
private nextMonitorToken = 1;
|
|
296
|
+
|
|
168
297
|
constructor(options: TransportOptions) {
|
|
169
|
-
this.scanTimeout = options.scanTimeout ??
|
|
298
|
+
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
170
299
|
}
|
|
171
300
|
|
|
172
|
-
init(
|
|
301
|
+
init(logger: any, emitter: EventEmitter) {
|
|
302
|
+
setBleLogger(logger);
|
|
173
303
|
this.emitter = emitter;
|
|
174
304
|
}
|
|
175
305
|
|
|
@@ -179,6 +309,13 @@ export default class ReactNativeBleTransport {
|
|
|
179
309
|
this._messages = messages;
|
|
180
310
|
}
|
|
181
311
|
|
|
312
|
+
configureProtocolV2(signedData: any) {
|
|
313
|
+
this._messagesV2 = parseConfigure(signedData);
|
|
314
|
+
this.protocolV2Links
|
|
315
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
316
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
317
|
+
}
|
|
318
|
+
|
|
182
319
|
listen() {
|
|
183
320
|
// empty
|
|
184
321
|
}
|
|
@@ -206,7 +343,29 @@ export default class ReactNativeBleTransport {
|
|
|
206
343
|
}
|
|
207
344
|
}
|
|
208
345
|
|
|
346
|
+
let fallbackServiceUuid: string | undefined;
|
|
347
|
+
|
|
209
348
|
if (!infos) {
|
|
349
|
+
const services = await device.services();
|
|
350
|
+
Log?.debug(
|
|
351
|
+
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
352
|
+
services?.map(service => service.uuid)
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const knownService = services.find(service =>
|
|
356
|
+
getInfosForServiceUuid(service.uuid, 'classic')
|
|
357
|
+
);
|
|
358
|
+
const fallbackService =
|
|
359
|
+
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
360
|
+
|
|
361
|
+
if (fallbackService) {
|
|
362
|
+
fallbackServiceUuid = fallbackService.uuid;
|
|
363
|
+
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
364
|
+
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!infos && !fallbackServiceUuid) {
|
|
210
369
|
try {
|
|
211
370
|
Log?.debug('cancel connection when service not found');
|
|
212
371
|
await device.cancelConnection();
|
|
@@ -216,7 +375,13 @@ export default class ReactNativeBleTransport {
|
|
|
216
375
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
217
376
|
}
|
|
218
377
|
|
|
219
|
-
const
|
|
378
|
+
const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
|
|
379
|
+
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
380
|
+
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
381
|
+
|
|
382
|
+
if (!serviceUuid) {
|
|
383
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
384
|
+
}
|
|
220
385
|
|
|
221
386
|
if (!characteristics) {
|
|
222
387
|
characteristics = await device.characteristicsForService(serviceUuid);
|
|
@@ -229,9 +394,9 @@ export default class ReactNativeBleTransport {
|
|
|
229
394
|
let writeCharacteristic;
|
|
230
395
|
let notifyCharacteristic;
|
|
231
396
|
for (const c of characteristics) {
|
|
232
|
-
if (c.uuid
|
|
397
|
+
if (isSameBleUuid(c.uuid, writeUuid)) {
|
|
233
398
|
writeCharacteristic = c;
|
|
234
|
-
} else if (c.uuid
|
|
399
|
+
} else if (isSameBleUuid(c.uuid, notifyUuid)) {
|
|
235
400
|
notifyCharacteristic = c;
|
|
236
401
|
}
|
|
237
402
|
}
|
|
@@ -244,7 +409,7 @@ export default class ReactNativeBleTransport {
|
|
|
244
409
|
throw ERRORS.TypedError('BLECharacteristicNotFound: notify characteristic not found');
|
|
245
410
|
}
|
|
246
411
|
|
|
247
|
-
if (!writeCharacteristic
|
|
412
|
+
if (!hasWritableCapability(writeCharacteristic)) {
|
|
248
413
|
throw ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable');
|
|
249
414
|
}
|
|
250
415
|
|
|
@@ -267,6 +432,10 @@ export default class ReactNativeBleTransport {
|
|
|
267
432
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
268
433
|
return;
|
|
269
434
|
}
|
|
435
|
+
if (transportCache[uuid] !== transport) {
|
|
436
|
+
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
270
439
|
|
|
271
440
|
try {
|
|
272
441
|
Log?.debug('device disconnect: ', device?.id);
|
|
@@ -276,12 +445,14 @@ export default class ReactNativeBleTransport {
|
|
|
276
445
|
connectId: device?.id,
|
|
277
446
|
});
|
|
278
447
|
if (this.runPromise) {
|
|
279
|
-
|
|
448
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
449
|
+
this.runPromise.reject(error);
|
|
450
|
+
this.rejectAllProtocolV2Frames(error);
|
|
280
451
|
}
|
|
281
452
|
} catch (e) {
|
|
282
453
|
Log?.debug('device disconnect error: ', e);
|
|
283
454
|
} finally {
|
|
284
|
-
this.release(uuid);
|
|
455
|
+
this.release(uuid, true);
|
|
285
456
|
}
|
|
286
457
|
});
|
|
287
458
|
}
|
|
@@ -304,7 +475,6 @@ export default class ReactNativeBleTransport {
|
|
|
304
475
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
305
476
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
306
477
|
) {
|
|
307
|
-
connectOptions = {};
|
|
308
478
|
device = await device.connect();
|
|
309
479
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
310
480
|
throw e;
|
|
@@ -319,7 +489,18 @@ export default class ReactNativeBleTransport {
|
|
|
319
489
|
transport.device = device;
|
|
320
490
|
transport.writeCharacteristic = writeCharacteristic;
|
|
321
491
|
transport.notifyCharacteristic = notifyCharacteristic;
|
|
322
|
-
|
|
492
|
+
const monitorToken = this.nextMonitorToken;
|
|
493
|
+
this.nextMonitorToken += 1;
|
|
494
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
495
|
+
transport.monitorToken = monitorToken;
|
|
496
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
497
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
498
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
499
|
+
notifyCharacteristic,
|
|
500
|
+
uuid,
|
|
501
|
+
monitorToken,
|
|
502
|
+
notifyTransactionId
|
|
503
|
+
);
|
|
323
504
|
this.attachDisconnectSubscription(transport, device, uuid);
|
|
324
505
|
} finally {
|
|
325
506
|
this.firmwareUploadWriteRecoveryIds.delete(uuid);
|
|
@@ -365,11 +546,11 @@ export default class ReactNativeBleTransport {
|
|
|
365
546
|
blePlxManager.startDeviceScan(
|
|
366
547
|
getBluetoothServiceUuids(),
|
|
367
548
|
{
|
|
549
|
+
allowDuplicates: true,
|
|
368
550
|
scanMode: ScanMode.LowLatency,
|
|
369
551
|
},
|
|
370
552
|
(error, device) => {
|
|
371
553
|
if (error) {
|
|
372
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
373
554
|
Log?.debug('ble scan error: ', error);
|
|
374
555
|
if (
|
|
375
556
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -392,14 +573,20 @@ export default class ReactNativeBleTransport {
|
|
|
392
573
|
return;
|
|
393
574
|
}
|
|
394
575
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
576
|
+
const displayName = getDeviceDisplayName(device);
|
|
577
|
+
const isOneKey =
|
|
578
|
+
isOnekeyDevice(device?.name ?? null, device?.id) ||
|
|
579
|
+
isOnekeyDevice(device?.localName ?? null, device?.id) ||
|
|
580
|
+
hasKnownOneKeyService(device);
|
|
581
|
+
if (isOneKey) {
|
|
401
582
|
addDevice(device as unknown as Device);
|
|
402
|
-
|
|
583
|
+
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
584
|
+
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
585
|
+
name: device?.name,
|
|
586
|
+
localName: device?.localName,
|
|
587
|
+
id: device?.id,
|
|
588
|
+
serviceUUIDs: device?.serviceUUIDs,
|
|
589
|
+
});
|
|
403
590
|
}
|
|
404
591
|
}
|
|
405
592
|
);
|
|
@@ -411,7 +598,6 @@ export default class ReactNativeBleTransport {
|
|
|
411
598
|
const hasCachedServiceUuid = Boolean(serviceUUIDs?.length);
|
|
412
599
|
const keepDevice = Platform.OS === 'ios' || hasCachedServiceUuid;
|
|
413
600
|
if (keepDevice) {
|
|
414
|
-
Log?.debug('search connected peripheral: ', device.id);
|
|
415
601
|
addDevice(device as unknown as Device);
|
|
416
602
|
}
|
|
417
603
|
}
|
|
@@ -420,7 +606,22 @@ export default class ReactNativeBleTransport {
|
|
|
420
606
|
|
|
421
607
|
const addDevice = (device: Device) => {
|
|
422
608
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
423
|
-
|
|
609
|
+
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
610
|
+
const protocolHint = inferProtocolHintFromDeviceName(displayName);
|
|
611
|
+
if (protocolHint) {
|
|
612
|
+
this.deviceProtocolHints.set(device.id, protocolHint);
|
|
613
|
+
}
|
|
614
|
+
deviceList.push({
|
|
615
|
+
...device,
|
|
616
|
+
name: displayName,
|
|
617
|
+
commType: 'ble',
|
|
618
|
+
} as IOneKeyDevice);
|
|
619
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
620
|
+
deviceId: device.id,
|
|
621
|
+
name: displayName,
|
|
622
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
623
|
+
protocolHint,
|
|
624
|
+
});
|
|
424
625
|
}
|
|
425
626
|
};
|
|
426
627
|
|
|
@@ -432,25 +633,40 @@ export default class ReactNativeBleTransport {
|
|
|
432
633
|
}
|
|
433
634
|
|
|
434
635
|
async acquire(input: BleAcquireInput) {
|
|
435
|
-
const { uuid, forceCleanRunPromise } = input;
|
|
636
|
+
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
436
637
|
|
|
437
638
|
if (!uuid) {
|
|
438
639
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
439
640
|
}
|
|
440
641
|
|
|
441
|
-
|
|
642
|
+
const cachedTransport = transportCache[uuid];
|
|
643
|
+
if (cachedTransport) {
|
|
644
|
+
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
645
|
+
const isCachedDeviceConnected = await cachedTransport.device.isConnected().catch(() => false);
|
|
646
|
+
if (
|
|
647
|
+
isCachedDeviceConnected &&
|
|
648
|
+
cachedProtocol &&
|
|
649
|
+
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
650
|
+
) {
|
|
651
|
+
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
652
|
+
return { uuid, protocolType: cachedProtocol };
|
|
653
|
+
}
|
|
442
654
|
|
|
443
|
-
if (transportCache[uuid]) {
|
|
444
655
|
/**
|
|
445
|
-
* If the transport is not
|
|
446
|
-
* it
|
|
656
|
+
* If the transport is not reusable due to a protocol mismatch or stale
|
|
657
|
+
* connection, clean it up before creating a new transport instance.
|
|
447
658
|
*/
|
|
448
|
-
Log?.debug('transport not
|
|
449
|
-
await this.release(uuid);
|
|
659
|
+
Log?.debug('transport not reusable, will release: ', uuid);
|
|
660
|
+
await this.release(uuid, true);
|
|
450
661
|
}
|
|
451
662
|
|
|
663
|
+
let device: Device | null = null;
|
|
664
|
+
|
|
452
665
|
if (forceCleanRunPromise && this.runPromise) {
|
|
453
|
-
|
|
666
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
667
|
+
this.runPromise.reject(error);
|
|
668
|
+
this.rejectAllProtocolV2Frames(error);
|
|
669
|
+
this.runPromise = null;
|
|
454
670
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
455
671
|
}
|
|
456
672
|
|
|
@@ -462,11 +678,12 @@ export default class ReactNativeBleTransport {
|
|
|
462
678
|
throw error;
|
|
463
679
|
}
|
|
464
680
|
|
|
465
|
-
// check device is bonded
|
|
466
681
|
if (Platform.OS === 'android') {
|
|
467
682
|
const bondState = await pairDevice(uuid);
|
|
468
683
|
if (bondState.bonding) {
|
|
469
684
|
await onDeviceBondState(uuid);
|
|
685
|
+
} else if (!bondState.bonded) {
|
|
686
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded, 'device is not bonded');
|
|
470
687
|
}
|
|
471
688
|
}
|
|
472
689
|
|
|
@@ -492,7 +709,6 @@ export default class ReactNativeBleTransport {
|
|
|
492
709
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
493
710
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
494
711
|
) {
|
|
495
|
-
connectOptions = {};
|
|
496
712
|
Log?.debug('first try to reconnect without params');
|
|
497
713
|
device = await blePlxManager.connectToDevice(uuid);
|
|
498
714
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
@@ -512,17 +728,16 @@ export default class ReactNativeBleTransport {
|
|
|
512
728
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
513
729
|
|
|
514
730
|
try {
|
|
515
|
-
await device.connect(connectOptions);
|
|
731
|
+
device = await device.connect(connectOptions);
|
|
516
732
|
} catch (e) {
|
|
517
733
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
518
734
|
if (
|
|
519
735
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
520
736
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
521
737
|
) {
|
|
522
|
-
connectOptions = {};
|
|
523
738
|
Log?.debug('second try to reconnect without params');
|
|
524
739
|
try {
|
|
525
|
-
await device.connect();
|
|
740
|
+
device = await device.connect();
|
|
526
741
|
} catch (e) {
|
|
527
742
|
Log?.debug('last try to reconnect error: ', e);
|
|
528
743
|
// last try to reconnect device if this issue exists
|
|
@@ -530,7 +745,7 @@ export default class ReactNativeBleTransport {
|
|
|
530
745
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
531
746
|
Log?.debug('last try to reconnect');
|
|
532
747
|
await device.cancelConnection();
|
|
533
|
-
await device.connect();
|
|
748
|
+
device = await device.connect();
|
|
534
749
|
}
|
|
535
750
|
}
|
|
536
751
|
} else {
|
|
@@ -539,18 +754,50 @@ export default class ReactNativeBleTransport {
|
|
|
539
754
|
}
|
|
540
755
|
}
|
|
541
756
|
|
|
757
|
+
device = await requestAndroidMtu(device);
|
|
542
758
|
const { writeCharacteristic, notifyCharacteristic } = await this.resolveCharacteristics(device);
|
|
543
759
|
|
|
760
|
+
const protocolHint = expectedProtocol
|
|
761
|
+
? undefined
|
|
762
|
+
: this.deviceProtocolHints.get(uuid) ??
|
|
763
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
|
|
764
|
+
|
|
544
765
|
// release transport before new transport instance
|
|
545
|
-
await this.release(uuid);
|
|
766
|
+
await this.release(uuid, true);
|
|
767
|
+
if (protocolHint) {
|
|
768
|
+
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
769
|
+
}
|
|
546
770
|
|
|
547
771
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
772
|
+
if (Platform.OS === 'android') {
|
|
773
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
774
|
+
}
|
|
775
|
+
const monitorToken = this.nextMonitorToken;
|
|
776
|
+
this.nextMonitorToken += 1;
|
|
777
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
778
|
+
transport.monitorToken = monitorToken;
|
|
779
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
780
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
548
781
|
transport.notifySubscription = this._monitorCharacteristic(
|
|
549
782
|
transport.notifyCharacteristic,
|
|
550
|
-
uuid
|
|
783
|
+
uuid,
|
|
784
|
+
monitorToken,
|
|
785
|
+
notifyTransactionId
|
|
551
786
|
);
|
|
552
787
|
transportCache[uuid] = transport;
|
|
553
788
|
|
|
789
|
+
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
790
|
+
|
|
791
|
+
if (Platform.OS === 'ios') {
|
|
792
|
+
await new Promise<void>(resolve => {
|
|
793
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
794
|
+
});
|
|
795
|
+
} else if (Platform.OS === 'android') {
|
|
796
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
800
|
+
|
|
554
801
|
this.emitter?.emit('device-connect', {
|
|
555
802
|
name: device.name,
|
|
556
803
|
id: device.id,
|
|
@@ -559,13 +806,19 @@ export default class ReactNativeBleTransport {
|
|
|
559
806
|
|
|
560
807
|
this.attachDisconnectSubscription(transport, device, uuid);
|
|
561
808
|
|
|
562
|
-
return { uuid };
|
|
809
|
+
return { uuid, protocolType };
|
|
563
810
|
}
|
|
564
811
|
|
|
565
|
-
_monitorCharacteristic(
|
|
812
|
+
_monitorCharacteristic(
|
|
813
|
+
characteristic: Characteristic,
|
|
814
|
+
uuid: string,
|
|
815
|
+
monitorToken: number,
|
|
816
|
+
notifyTransactionId: string
|
|
817
|
+
): Subscription {
|
|
566
818
|
let bufferLength = 0;
|
|
567
819
|
let buffer: any[] = [];
|
|
568
820
|
const subscription = characteristic.monitor((error, c) => {
|
|
821
|
+
const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
|
|
569
822
|
if (error) {
|
|
570
823
|
Log?.debug(
|
|
571
824
|
`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
|
|
@@ -576,6 +829,33 @@ export default class ReactNativeBleTransport {
|
|
|
576
829
|
Log?.debug('notify error ignored during FirmwareUpload write recovery: ', uuid);
|
|
577
830
|
return;
|
|
578
831
|
}
|
|
832
|
+
if (!isCurrentMonitor) {
|
|
833
|
+
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
837
|
+
let errorCode:
|
|
838
|
+
| typeof HardwareErrorCode.BleDeviceBondError
|
|
839
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
840
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
841
|
+
| typeof HardwareErrorCode.BleTimeoutError =
|
|
842
|
+
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
843
|
+
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
844
|
+
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
845
|
+
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
846
|
+
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
847
|
+
} else if (
|
|
848
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
849
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
850
|
+
error.reason?.includes('The handle is invalid') ||
|
|
851
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
852
|
+
error.reason?.includes('notify change failed for device')
|
|
853
|
+
) {
|
|
854
|
+
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
855
|
+
}
|
|
856
|
+
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
579
859
|
if (this.runPromise) {
|
|
580
860
|
let ERROR:
|
|
581
861
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
@@ -595,27 +875,45 @@ export default class ReactNativeBleTransport {
|
|
|
595
875
|
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
596
876
|
error.reason?.includes('notify change failed for device')
|
|
597
877
|
) {
|
|
598
|
-
|
|
599
|
-
|
|
878
|
+
const notifyError = ERRORS.TypedError(
|
|
879
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
600
880
|
);
|
|
881
|
+
this.runPromise.reject(notifyError);
|
|
882
|
+
this.rejectAllProtocolV2Frames(notifyError);
|
|
601
883
|
Log?.debug(
|
|
602
884
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
603
885
|
);
|
|
604
886
|
return;
|
|
605
887
|
}
|
|
606
|
-
|
|
888
|
+
const notifyError = ERRORS.TypedError(ERROR);
|
|
889
|
+
this.runPromise.reject(notifyError);
|
|
890
|
+
this.rejectAllProtocolV2Frames(notifyError);
|
|
607
891
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
608
892
|
}
|
|
609
893
|
|
|
610
894
|
return;
|
|
611
895
|
}
|
|
612
896
|
|
|
897
|
+
if (!isCurrentMonitor) {
|
|
898
|
+
Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
613
902
|
if (!c) {
|
|
614
903
|
throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
|
|
615
904
|
}
|
|
616
905
|
|
|
617
906
|
try {
|
|
618
907
|
const data = Buffer.from(c.value as string, 'base64');
|
|
908
|
+
const protocol = this.deviceProtocol.get(uuid);
|
|
909
|
+
if (!protocol) {
|
|
910
|
+
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (protocol === 'V2') {
|
|
914
|
+
this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
619
917
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
620
918
|
if (isHeaderChunk(data)) {
|
|
621
919
|
bufferLength = data.readInt32BE(5);
|
|
@@ -624,7 +922,7 @@ export default class ReactNativeBleTransport {
|
|
|
624
922
|
buffer = buffer.concat([...data]);
|
|
625
923
|
}
|
|
626
924
|
|
|
627
|
-
if (buffer.length -
|
|
925
|
+
if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
|
|
628
926
|
const value = Buffer.from(buffer);
|
|
629
927
|
// console.log(
|
|
630
928
|
// '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
|
|
@@ -638,17 +936,41 @@ export default class ReactNativeBleTransport {
|
|
|
638
936
|
}
|
|
639
937
|
} catch (error) {
|
|
640
938
|
Log?.debug('monitor data error: ', error);
|
|
641
|
-
|
|
939
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
940
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
941
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
942
|
+
} else {
|
|
943
|
+
this.runPromise?.reject(notifyError);
|
|
944
|
+
}
|
|
642
945
|
}
|
|
643
|
-
},
|
|
946
|
+
}, notifyTransactionId);
|
|
644
947
|
|
|
645
948
|
return subscription;
|
|
646
949
|
}
|
|
647
950
|
|
|
648
|
-
async release(uuid: string) {
|
|
951
|
+
async release(uuid: string, onclose = false) {
|
|
649
952
|
const transport = transportCache[uuid];
|
|
953
|
+
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
954
|
+
if (this.runPromise) {
|
|
955
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
956
|
+
this.runPromise.reject(error);
|
|
957
|
+
this.runPromise = null;
|
|
958
|
+
this.rejectAllProtocolV2Frames(error);
|
|
959
|
+
} else {
|
|
960
|
+
this.resetProtocolV2Frames(uuid);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (Platform.OS === 'android' && !onclose && transport) {
|
|
964
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
965
|
+
this.resetProtocolV2Frames(uuid);
|
|
966
|
+
return Promise.resolve(true);
|
|
967
|
+
}
|
|
650
968
|
|
|
651
969
|
if (transport) {
|
|
970
|
+
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
971
|
+
this.monitorTokens.delete(uuid);
|
|
972
|
+
}
|
|
973
|
+
|
|
652
974
|
// Clean up disconnect subscription first to prevent callbacks on released transport
|
|
653
975
|
Log?.debug('release: removing disconnect subscription for device: ', uuid);
|
|
654
976
|
transport.disconnectSubscription?.remove();
|
|
@@ -662,12 +984,27 @@ export default class ReactNativeBleTransport {
|
|
|
662
984
|
transport.notifySubscription?.remove();
|
|
663
985
|
transport.notifySubscription = undefined;
|
|
664
986
|
|
|
987
|
+
if (transport.notifyTransactionId) {
|
|
988
|
+
try {
|
|
989
|
+
await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
|
|
990
|
+
} catch (e) {
|
|
991
|
+
Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
665
995
|
delete transportCache[uuid];
|
|
996
|
+
}
|
|
666
997
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
998
|
+
this.deviceProtocol.delete(uuid);
|
|
999
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1000
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1001
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1002
|
+
this.resetProtocolV2Frames(uuid);
|
|
1003
|
+
|
|
1004
|
+
try {
|
|
1005
|
+
await this.blePlxManager?.cancelTransaction(uuid);
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
|
|
671
1008
|
}
|
|
672
1009
|
|
|
673
1010
|
return Promise.resolve(true);
|
|
@@ -677,7 +1014,12 @@ export default class ReactNativeBleTransport {
|
|
|
677
1014
|
await this.call(session, name, data);
|
|
678
1015
|
}
|
|
679
1016
|
|
|
680
|
-
async call(
|
|
1017
|
+
async call(
|
|
1018
|
+
uuid: string,
|
|
1019
|
+
name: string,
|
|
1020
|
+
data: Record<string, unknown>,
|
|
1021
|
+
options?: TransportCallOptions
|
|
1022
|
+
) {
|
|
681
1023
|
if (this.stopped) {
|
|
682
1024
|
// eslint-disable-next-line prefer-promise-reject-errors
|
|
683
1025
|
return Promise.reject(ERRORS.TypedError('Transport stopped.'));
|
|
@@ -686,33 +1028,44 @@ export default class ReactNativeBleTransport {
|
|
|
686
1028
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
687
1029
|
}
|
|
688
1030
|
|
|
689
|
-
const
|
|
1031
|
+
const protocol = this.getProtocolType(uuid);
|
|
1032
|
+
if (!protocol) {
|
|
1033
|
+
throw ERRORS.TypedError(
|
|
1034
|
+
HardwareErrorCode.RuntimeError,
|
|
1035
|
+
`Device protocol has not been detected for ${uuid}`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1039
|
+
|
|
1040
|
+
if (protocol === 'V2') {
|
|
1041
|
+
return this.callProtocolV2(uuid, name, data, options);
|
|
1042
|
+
}
|
|
690
1043
|
|
|
691
|
-
|
|
1044
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
692
1045
|
if (this.runPromise && !forceRun) {
|
|
693
1046
|
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
694
1047
|
}
|
|
695
1048
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
699
|
-
}
|
|
1049
|
+
return this.callProtocolV1(uuid, name, data, options);
|
|
1050
|
+
}
|
|
700
1051
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
} else if (LogBlockCommand.has(name)) {
|
|
710
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name);
|
|
711
|
-
} else {
|
|
712
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', data);
|
|
1052
|
+
private async callProtocolV1(
|
|
1053
|
+
uuid: string,
|
|
1054
|
+
name: string,
|
|
1055
|
+
data: Record<string, unknown>,
|
|
1056
|
+
options?: TransportCallOptions
|
|
1057
|
+
) {
|
|
1058
|
+
if (!this._messages) {
|
|
1059
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
713
1060
|
}
|
|
714
1061
|
|
|
715
|
-
const
|
|
1062
|
+
const transport = this.getCachedTransport(uuid);
|
|
1063
|
+
const runPromise = createDeferred<string>();
|
|
1064
|
+
runPromise.promise.catch(() => undefined);
|
|
1065
|
+
this.runPromise = runPromise;
|
|
1066
|
+
const messages = this._messages;
|
|
1067
|
+
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1068
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
716
1069
|
|
|
717
1070
|
async function writeChunkedData(
|
|
718
1071
|
buffers: ByteBuffer[],
|
|
@@ -786,14 +1139,13 @@ export default class ReactNativeBleTransport {
|
|
|
786
1139
|
}
|
|
787
1140
|
);
|
|
788
1141
|
} else if (name === 'FirmwareUpload') {
|
|
789
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1142
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
790
1143
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
791
1144
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
792
1145
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
793
1146
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
794
1147
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
795
1148
|
});
|
|
796
|
-
|
|
797
1149
|
await writeFirmwareUploadChunkedData(
|
|
798
1150
|
buffers,
|
|
799
1151
|
async data => {
|
|
@@ -847,7 +1199,6 @@ export default class ReactNativeBleTransport {
|
|
|
847
1199
|
for (const o of buffers) {
|
|
848
1200
|
const outData = o.toString('base64');
|
|
849
1201
|
// Upload resources on low-end phones may OOM
|
|
850
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
851
1202
|
try {
|
|
852
1203
|
await transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
853
1204
|
} catch (e) {
|
|
@@ -865,20 +1216,40 @@ export default class ReactNativeBleTransport {
|
|
|
865
1216
|
}
|
|
866
1217
|
|
|
867
1218
|
try {
|
|
868
|
-
const response = await
|
|
1219
|
+
const response = await Promise.race([
|
|
1220
|
+
runPromise.promise,
|
|
1221
|
+
new Promise<never>((_, reject) => {
|
|
1222
|
+
if (options?.timeoutMs) {
|
|
1223
|
+
timeout = setTimeout(() => {
|
|
1224
|
+
const error = ERRORS.TypedError(
|
|
1225
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1226
|
+
`BLE response timeout after ${options.timeoutMs}ms for ${name}`
|
|
1227
|
+
);
|
|
1228
|
+
runPromise.reject(error);
|
|
1229
|
+
reject(error);
|
|
1230
|
+
}, options.timeoutMs);
|
|
1231
|
+
}
|
|
1232
|
+
}),
|
|
1233
|
+
]);
|
|
869
1234
|
|
|
870
1235
|
if (typeof response !== 'string') {
|
|
871
1236
|
throw new Error('Returning data is not string.');
|
|
872
1237
|
}
|
|
873
1238
|
|
|
874
|
-
|
|
875
|
-
const jsonData = receiveOne(messages, response);
|
|
1239
|
+
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
876
1240
|
return check.call(jsonData);
|
|
877
1241
|
} catch (e) {
|
|
878
|
-
|
|
1242
|
+
if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1243
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
|
|
1244
|
+
} else {
|
|
1245
|
+
Log?.error('call error: ', e);
|
|
1246
|
+
}
|
|
879
1247
|
throw e;
|
|
880
1248
|
} finally {
|
|
881
|
-
|
|
1249
|
+
if (timeout) clearTimeout(timeout);
|
|
1250
|
+
if (this.runPromise === runPromise) {
|
|
1251
|
+
this.runPromise = null;
|
|
1252
|
+
}
|
|
882
1253
|
}
|
|
883
1254
|
}
|
|
884
1255
|
|
|
@@ -887,7 +1258,7 @@ export default class ReactNativeBleTransport {
|
|
|
887
1258
|
}
|
|
888
1259
|
|
|
889
1260
|
async disconnect(session: string) {
|
|
890
|
-
|
|
1261
|
+
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
891
1262
|
const transport = transportCache[session];
|
|
892
1263
|
|
|
893
1264
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
@@ -945,6 +1316,10 @@ export default class ReactNativeBleTransport {
|
|
|
945
1316
|
if (transportCache[session]) {
|
|
946
1317
|
delete transportCache[session];
|
|
947
1318
|
}
|
|
1319
|
+
this.deviceProtocol.delete(session);
|
|
1320
|
+
this.deviceProtocolHints.delete(session);
|
|
1321
|
+
this.protocolV2Assemblers.delete(session);
|
|
1322
|
+
this.resetProtocolV2Frames(session);
|
|
948
1323
|
|
|
949
1324
|
// emit the disconnect event
|
|
950
1325
|
try {
|
|
@@ -967,4 +1342,374 @@ export default class ReactNativeBleTransport {
|
|
|
967
1342
|
}
|
|
968
1343
|
this.runPromise = null;
|
|
969
1344
|
}
|
|
1345
|
+
|
|
1346
|
+
private getCachedTransport(uuid: string) {
|
|
1347
|
+
const transport = transportCache[uuid];
|
|
1348
|
+
if (!transport) {
|
|
1349
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1350
|
+
}
|
|
1351
|
+
return transport;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1355
|
+
return ERRORS.TypedError(
|
|
1356
|
+
HardwareErrorCode.RuntimeError,
|
|
1357
|
+
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
private createProtocolDetectionError() {
|
|
1362
|
+
return ERRORS.TypedError(
|
|
1363
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1364
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 Initialize or Protocol V2 Ping'
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1369
|
+
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1370
|
+
this.deviceProtocol.delete(uuid);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private async detectProtocol(
|
|
1375
|
+
uuid: string,
|
|
1376
|
+
expectedProtocol?: ProtocolType,
|
|
1377
|
+
protocolHint?: ProtocolType
|
|
1378
|
+
): Promise<ProtocolType> {
|
|
1379
|
+
if (expectedProtocol === 'V1') {
|
|
1380
|
+
if (await this.probeProtocolV1(uuid)) {
|
|
1381
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1382
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1383
|
+
deviceId: uuid,
|
|
1384
|
+
protocol: 'V1',
|
|
1385
|
+
source: 'expected',
|
|
1386
|
+
});
|
|
1387
|
+
return 'V1';
|
|
1388
|
+
}
|
|
1389
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (expectedProtocol === 'V2') {
|
|
1393
|
+
// Skip probing when the caller explicitly confirms V2, such as reconnect after a
|
|
1394
|
+
// firmware reboot where expectedProtocol carries the previously probed result.
|
|
1395
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1396
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1397
|
+
deviceId: uuid,
|
|
1398
|
+
protocol: 'V2',
|
|
1399
|
+
source: 'expected',
|
|
1400
|
+
});
|
|
1401
|
+
return 'V2';
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1405
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1406
|
+
const probeOrder: ProtocolType[] =
|
|
1407
|
+
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1408
|
+
|
|
1409
|
+
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1410
|
+
const protocol = probeOrder[i];
|
|
1411
|
+
if (i > 0) {
|
|
1412
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1413
|
+
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1414
|
+
}
|
|
1415
|
+
const detected =
|
|
1416
|
+
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1417
|
+
if (detected) {
|
|
1418
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
1419
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1420
|
+
deviceId: uuid,
|
|
1421
|
+
protocol,
|
|
1422
|
+
source: 'probe',
|
|
1423
|
+
});
|
|
1424
|
+
return protocol;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
this.deviceProtocol.delete(uuid);
|
|
1429
|
+
throw this.createProtocolDetectionError();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
1433
|
+
const transport = transportCache[uuid];
|
|
1434
|
+
await this.protocolV2Links.invalidateLink(
|
|
1435
|
+
uuid,
|
|
1436
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
1437
|
+
);
|
|
1438
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1439
|
+
this.resetProtocolV2Frames(uuid);
|
|
1440
|
+
if (this.runPromise) {
|
|
1441
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1442
|
+
this.runPromise.reject(error);
|
|
1443
|
+
this.runPromise = null;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (!transport) return;
|
|
1447
|
+
|
|
1448
|
+
const previousNotifyTransactionId = transport.notifyTransactionId;
|
|
1449
|
+
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
1450
|
+
this.monitorTokens.delete(uuid);
|
|
1451
|
+
}
|
|
1452
|
+
transport.notifySubscription?.remove();
|
|
1453
|
+
transport.notifySubscription = undefined;
|
|
1454
|
+
if (previousNotifyTransactionId) {
|
|
1455
|
+
try {
|
|
1456
|
+
await this.blePlxManager?.cancelTransaction(previousNotifyTransactionId);
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
Log?.debug(
|
|
1459
|
+
`[ReactNativeBleTransport] cancel notify after Protocol ${protocol} probe failed:`,
|
|
1460
|
+
error?.message || error
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const monitorToken = this.nextMonitorToken;
|
|
1466
|
+
this.nextMonitorToken += 1;
|
|
1467
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
1468
|
+
transport.monitorToken = monitorToken;
|
|
1469
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
1470
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
1471
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
1472
|
+
transport.notifyCharacteristic,
|
|
1473
|
+
uuid,
|
|
1474
|
+
monitorToken,
|
|
1475
|
+
notifyTransactionId
|
|
1476
|
+
);
|
|
1477
|
+
if (Platform.OS === 'ios') {
|
|
1478
|
+
await new Promise<void>(resolve => {
|
|
1479
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private async probeProtocolV1(uuid: string) {
|
|
1485
|
+
if (!this._messages) {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
try {
|
|
1490
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1491
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
this.clearProbeProtocol(uuid, 'V1');
|
|
1495
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private async probeProtocolV2(uuid: string) {
|
|
1501
|
+
if (!this._messages || !this._messagesV2) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1506
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1507
|
+
const detected = await probeProtocolV2Helper({
|
|
1508
|
+
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
1509
|
+
this.callProtocolV2(uuid, name, data, options),
|
|
1510
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
|
|
1511
|
+
logger: Log,
|
|
1512
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1513
|
+
onProbeFailed: () => {
|
|
1514
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1515
|
+
this.resetProtocolV2Frames(uuid);
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
if (!detected) {
|
|
1519
|
+
this.clearProbeProtocol(uuid, 'V2');
|
|
1520
|
+
}
|
|
1521
|
+
return detected;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
|
|
1525
|
+
try {
|
|
1526
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) return;
|
|
1527
|
+
|
|
1528
|
+
if (data.length === 0) return;
|
|
1529
|
+
|
|
1530
|
+
const assembler = this.protocolV2Assemblers.get(uuid);
|
|
1531
|
+
if (!assembler) return;
|
|
1532
|
+
|
|
1533
|
+
for (const frameData of assembler.drain(data)) {
|
|
1534
|
+
this.resolveProtocolV2Frame(uuid, frameData);
|
|
1535
|
+
}
|
|
1536
|
+
} catch (error) {
|
|
1537
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1538
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1539
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1540
|
+
this.protocolV2Links
|
|
1541
|
+
.invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
|
|
1542
|
+
.catch(invalidateError =>
|
|
1543
|
+
Log?.debug(
|
|
1544
|
+
'[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
|
|
1545
|
+
invalidateError
|
|
1546
|
+
)
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
private getProtocolV2FrameQueue(uuid: string) {
|
|
1552
|
+
let queue = this.protocolV2FrameQueues.get(uuid);
|
|
1553
|
+
if (!queue) {
|
|
1554
|
+
queue = [];
|
|
1555
|
+
this.protocolV2FrameQueues.set(uuid, queue);
|
|
1556
|
+
}
|
|
1557
|
+
return queue;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
private resolveProtocolV2Frame(uuid: string, frame: Uint8Array) {
|
|
1561
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
1562
|
+
if (framePromise) {
|
|
1563
|
+
framePromise.resolve(frame);
|
|
1564
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
private rejectAllProtocolV2Frames(error: Error) {
|
|
1571
|
+
this.protocolV2FrameQueues.clear();
|
|
1572
|
+
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1573
|
+
framePromise.reject(error);
|
|
1574
|
+
}
|
|
1575
|
+
this.protocolV2FramePromises.clear();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
private resetProtocolV2Frames(uuid: string) {
|
|
1579
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
1580
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
1584
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
1585
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
1586
|
+
if (framePromise) {
|
|
1587
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1588
|
+
framePromise.reject(error);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
private async readProtocolV2Frame(uuid: string) {
|
|
1593
|
+
const queuedFrame = this.getProtocolV2FrameQueue(uuid).shift();
|
|
1594
|
+
if (queuedFrame) {
|
|
1595
|
+
return queuedFrame;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const framePromise = createDeferred<Uint8Array>();
|
|
1599
|
+
this.protocolV2FramePromises.set(uuid, framePromise);
|
|
1600
|
+
try {
|
|
1601
|
+
return await framePromise.promise;
|
|
1602
|
+
} finally {
|
|
1603
|
+
if (this.protocolV2FramePromises.get(uuid) === framePromise) {
|
|
1604
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
private async writeProtocolV2Frame(transport: BleTransport, frame: Uint8Array) {
|
|
1610
|
+
const tuning = getProtocolV2BleTuning();
|
|
1611
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1612
|
+
platform: Platform.OS,
|
|
1613
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
1614
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
1615
|
+
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1616
|
+
});
|
|
1617
|
+
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1618
|
+
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1619
|
+
const base64 = Buffer.from(chunk).toString('base64');
|
|
1620
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
private async callProtocolV2(
|
|
1625
|
+
uuid: string,
|
|
1626
|
+
name: string,
|
|
1627
|
+
data: Record<string, unknown>,
|
|
1628
|
+
options?: TransportCallOptions
|
|
1629
|
+
) {
|
|
1630
|
+
if (!this._messages || !this._messagesV2) {
|
|
1631
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const callOptions = {
|
|
1635
|
+
...options,
|
|
1636
|
+
// Align with V1 BLE and the USB V2 base transport: only arm a watchdog when the caller
|
|
1637
|
+
// explicitly passes timeoutMs. Interactive acks (ButtonAck/PinMatrixAck/PassphraseAck)
|
|
1638
|
+
// have their timeoutMs removed by DeviceCommands.stripInteractiveAckTimeout, so we must
|
|
1639
|
+
// not fill in a 30s default here — that would hard-cap the time a user spends confirming
|
|
1640
|
+
// a transaction or entering a PIN/passphrase on the device and tear down the BLE link.
|
|
1641
|
+
timeoutMs: options?.timeoutMs,
|
|
1642
|
+
};
|
|
1643
|
+
const highVolumeWrite = LogBlockCommand.has(name);
|
|
1644
|
+
|
|
1645
|
+
if (highVolumeWrite) {
|
|
1646
|
+
const tuning = getProtocolV2BleTuning();
|
|
1647
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1648
|
+
name,
|
|
1649
|
+
writeMode: 'withoutResponse',
|
|
1650
|
+
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
try {
|
|
1655
|
+
return await this.protocolV2Links.call(
|
|
1656
|
+
uuid,
|
|
1657
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
1658
|
+
name,
|
|
1659
|
+
data,
|
|
1660
|
+
callOptions
|
|
1661
|
+
);
|
|
1662
|
+
} catch (e) {
|
|
1663
|
+
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1664
|
+
throw e;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
1669
|
+
const generation = this.monitorTokens.get(uuid) ?? 0;
|
|
1670
|
+
const assertCurrentGeneration = () => {
|
|
1671
|
+
if (this.monitorTokens.get(uuid) !== generation) {
|
|
1672
|
+
throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
1678
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
1679
|
+
generation,
|
|
1680
|
+
prepareCall: () => {
|
|
1681
|
+
assertCurrentGeneration();
|
|
1682
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1683
|
+
this.resetProtocolV2Frames(uuid);
|
|
1684
|
+
},
|
|
1685
|
+
writeFrame: async (frame: Uint8Array) => {
|
|
1686
|
+
assertCurrentGeneration();
|
|
1687
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
1688
|
+
await this.writeProtocolV2Frame(currentTransport, frame);
|
|
1689
|
+
},
|
|
1690
|
+
readFrame: async () => {
|
|
1691
|
+
assertCurrentGeneration();
|
|
1692
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
1693
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
1694
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1695
|
+
}
|
|
1696
|
+
return rxFrame;
|
|
1697
|
+
},
|
|
1698
|
+
reset: (reason: string) => {
|
|
1699
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1700
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1701
|
+
},
|
|
1702
|
+
logger: Log,
|
|
1703
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1704
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
1705
|
+
ERRORS.TypedError(
|
|
1706
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1707
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
1708
|
+
),
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
getProtocolType(path: string): ProtocolType | undefined {
|
|
1713
|
+
return this.deviceProtocol.get(path);
|
|
1714
|
+
}
|
|
970
1715
|
}
|