@onekeyfe/hd-transport-react-native 1.1.26-alpha.12 → 1.1.26-alpha.30
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/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +47 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +571 -66
- package/dist/logger.d.ts +14 -0
- package/dist/logger.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 +3 -2
- package/src/BleTransport.ts +8 -3
- package/src/constants.ts +24 -1
- package/src/index.ts +674 -55
- package/src/logger.ts +19 -0
- package/src/types.ts +3 -0
- package/src/utils/validateNotify.ts +4 -4
package/src/index.ts
CHANGED
|
@@ -9,35 +9,136 @@ 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_CHANNEL_BLE_UART,
|
|
16
|
+
type ProtocolType,
|
|
17
|
+
ProtocolV2FrameAssembler,
|
|
18
|
+
ProtocolV2Session,
|
|
19
|
+
type TransportCallOptions,
|
|
20
|
+
probeProtocolV2 as probeProtocolV2Helper,
|
|
15
21
|
} from '@onekeyfe/hd-transport';
|
|
16
22
|
import { ERRORS, HardwareErrorCode, createDeferred, isOnekeyDevice } from '@onekeyfe/hd-shared';
|
|
17
|
-
import { LoggerNames, getLogger } from '@onekeyfe/hd-core';
|
|
18
23
|
|
|
19
24
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
20
25
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
21
26
|
import {
|
|
22
27
|
ANDROID_PACKET_LENGTH,
|
|
23
28
|
IOS_PACKET_LENGTH,
|
|
29
|
+
getBleUuidKey,
|
|
24
30
|
getBluetoothServiceUuids,
|
|
25
31
|
getInfosForServiceUuid,
|
|
32
|
+
isSameBleUuid,
|
|
26
33
|
} from './constants';
|
|
27
34
|
import { isHeaderChunk } from './utils/validateNotify';
|
|
28
35
|
import BleTransport from './BleTransport';
|
|
29
36
|
import timer from './utils/timer';
|
|
37
|
+
import { bleLogger, setBleLogger } from './logger';
|
|
30
38
|
|
|
31
39
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
32
40
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
33
41
|
import type EventEmitter from 'events';
|
|
34
42
|
import type { BleAcquireInput, TransportOptions } from './types';
|
|
35
43
|
|
|
36
|
-
const { check,
|
|
44
|
+
const { check, ProtocolV1, parseConfigure } = transport;
|
|
37
45
|
|
|
38
|
-
const Log =
|
|
46
|
+
const Log = bleLogger;
|
|
39
47
|
|
|
40
48
|
const transportCache: Record<string, BleTransport> = {};
|
|
49
|
+
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
50
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
51
|
+
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 5000;
|
|
52
|
+
const DEVICE_SCAN_TIMEOUT_MS = 8000;
|
|
53
|
+
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
54
|
+
const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
|
|
55
|
+
const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
|
|
56
|
+
const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
|
|
57
|
+
|
|
58
|
+
const delay = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
59
|
+
|
|
60
|
+
export type ProtocolV2BleTuning = {
|
|
61
|
+
iosPacketLength?: number;
|
|
62
|
+
androidPacketLength?: number;
|
|
63
|
+
highVolumeWriteBurstSize?: number;
|
|
64
|
+
highVolumeWritePauseMs?: number;
|
|
65
|
+
highVolumeWriteFlushDelayMs?: number;
|
|
66
|
+
highVolumeWriteWithResponse?: boolean;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
70
|
+
|
|
71
|
+
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
72
|
+
iosPacketLength: IOS_PACKET_LENGTH,
|
|
73
|
+
androidPacketLength: ANDROID_PACKET_LENGTH,
|
|
74
|
+
highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
|
|
75
|
+
highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
|
|
76
|
+
highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
|
|
77
|
+
highVolumeWriteWithResponse: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
81
|
+
|
|
82
|
+
const normalizePositiveInteger = (value: unknown, fallback: number) => {
|
|
83
|
+
const normalized = Number(value);
|
|
84
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return fallback;
|
|
85
|
+
return Math.floor(normalized);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
89
|
+
protocolV2BleTuning = {
|
|
90
|
+
iosPacketLength: normalizePositiveInteger(
|
|
91
|
+
tuning.iosPacketLength,
|
|
92
|
+
protocolV2BleTuning.iosPacketLength
|
|
93
|
+
),
|
|
94
|
+
androidPacketLength: normalizePositiveInteger(
|
|
95
|
+
tuning.androidPacketLength,
|
|
96
|
+
protocolV2BleTuning.androidPacketLength
|
|
97
|
+
),
|
|
98
|
+
highVolumeWriteBurstSize: normalizePositiveInteger(
|
|
99
|
+
tuning.highVolumeWriteBurstSize,
|
|
100
|
+
protocolV2BleTuning.highVolumeWriteBurstSize
|
|
101
|
+
),
|
|
102
|
+
highVolumeWritePauseMs: normalizePositiveInteger(
|
|
103
|
+
tuning.highVolumeWritePauseMs,
|
|
104
|
+
protocolV2BleTuning.highVolumeWritePauseMs
|
|
105
|
+
),
|
|
106
|
+
highVolumeWriteFlushDelayMs: normalizePositiveInteger(
|
|
107
|
+
tuning.highVolumeWriteFlushDelayMs,
|
|
108
|
+
protocolV2BleTuning.highVolumeWriteFlushDelayMs
|
|
109
|
+
),
|
|
110
|
+
highVolumeWriteWithResponse:
|
|
111
|
+
tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
|
|
112
|
+
};
|
|
113
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning configured:', protocolV2BleTuning);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resetProtocolV2BleTuning() {
|
|
117
|
+
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
118
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 BLE tuning reset:', protocolV2BleTuning);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getProtocolV2BleTuning() {
|
|
122
|
+
return { ...protocolV2BleTuning };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inferProtocolTypeFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
126
|
+
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getDeviceDisplayName(device?: Device | null) {
|
|
130
|
+
return device?.name || device?.localName || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isGenericBleService(uuid?: string | null) {
|
|
134
|
+
return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hasKnownOneKeyService(device?: Device | null) {
|
|
138
|
+
return (device?.serviceUUIDs ?? []).some(serviceUuid =>
|
|
139
|
+
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
140
|
+
);
|
|
141
|
+
}
|
|
41
142
|
|
|
42
143
|
let connectOptions: Record<string, unknown> = {
|
|
43
144
|
requestMTU: 256,
|
|
@@ -49,7 +150,8 @@ export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
|
49
150
|
|
|
50
151
|
const tryToGetConfiguration = (device: Device) => {
|
|
51
152
|
if (!device || !device.serviceUUIDs) return null;
|
|
52
|
-
const
|
|
153
|
+
const serviceUUID = device.serviceUUIDs.find(uuid => getInfosForServiceUuid(uuid, 'classic'));
|
|
154
|
+
if (!serviceUUID) return null;
|
|
53
155
|
const infos = getInfosForServiceUuid(serviceUUID, 'classic');
|
|
54
156
|
if (!infos) return null;
|
|
55
157
|
return infos;
|
|
@@ -92,23 +194,39 @@ export default class ReactNativeBleTransport {
|
|
|
92
194
|
|
|
93
195
|
_messages: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
94
196
|
|
|
197
|
+
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
198
|
+
|
|
95
199
|
name = 'ReactNativeBleTransport';
|
|
96
200
|
|
|
97
201
|
configured = false;
|
|
98
202
|
|
|
99
203
|
stopped = false;
|
|
100
204
|
|
|
101
|
-
scanTimeout =
|
|
205
|
+
scanTimeout = DEVICE_SCAN_TIMEOUT_MS;
|
|
102
206
|
|
|
103
207
|
runPromise: Deferred<any> | null = null;
|
|
104
208
|
|
|
105
209
|
emitter?: EventEmitter;
|
|
106
210
|
|
|
211
|
+
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
212
|
+
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
213
|
+
|
|
214
|
+
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
215
|
+
|
|
216
|
+
private protocolV2FrameQueue: Uint8Array[] = [];
|
|
217
|
+
|
|
218
|
+
private protocolV2FramePromise: Deferred<Uint8Array> | null = null;
|
|
219
|
+
|
|
220
|
+
private monitorTokens: Map<string, number> = new Map();
|
|
221
|
+
|
|
222
|
+
private nextMonitorToken = 1;
|
|
223
|
+
|
|
107
224
|
constructor(options: TransportOptions) {
|
|
108
|
-
this.scanTimeout = options.scanTimeout ??
|
|
225
|
+
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
109
226
|
}
|
|
110
227
|
|
|
111
|
-
init(
|
|
228
|
+
init(logger: any, emitter: EventEmitter) {
|
|
229
|
+
setBleLogger(logger);
|
|
112
230
|
this.emitter = emitter;
|
|
113
231
|
}
|
|
114
232
|
|
|
@@ -118,6 +236,11 @@ export default class ReactNativeBleTransport {
|
|
|
118
236
|
this._messages = messages;
|
|
119
237
|
}
|
|
120
238
|
|
|
239
|
+
configureProtocolV2(signedData: any) {
|
|
240
|
+
this._messagesV2 = parseConfigure(signedData);
|
|
241
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 schema configured');
|
|
242
|
+
}
|
|
243
|
+
|
|
121
244
|
listen() {
|
|
122
245
|
// empty
|
|
123
246
|
}
|
|
@@ -167,6 +290,7 @@ export default class ReactNativeBleTransport {
|
|
|
167
290
|
blePlxManager.startDeviceScan(
|
|
168
291
|
null,
|
|
169
292
|
{
|
|
293
|
+
allowDuplicates: true,
|
|
170
294
|
scanMode: ScanMode.LowLatency,
|
|
171
295
|
},
|
|
172
296
|
(error, device) => {
|
|
@@ -194,14 +318,41 @@ export default class ReactNativeBleTransport {
|
|
|
194
318
|
return;
|
|
195
319
|
}
|
|
196
320
|
|
|
197
|
-
|
|
321
|
+
const displayName = getDeviceDisplayName(device);
|
|
322
|
+
const isOneKey =
|
|
323
|
+
isOnekeyDevice(device?.name ?? null, device?.id) ||
|
|
324
|
+
isOnekeyDevice(device?.localName ?? null, device?.id) ||
|
|
325
|
+
hasKnownOneKeyService(device);
|
|
326
|
+
const shouldTraceCandidate =
|
|
327
|
+
!!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
|
|
328
|
+
|
|
329
|
+
if (shouldTraceCandidate) {
|
|
330
|
+
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
331
|
+
name: device?.name,
|
|
332
|
+
localName: device?.localName,
|
|
333
|
+
id: device?.id,
|
|
334
|
+
serviceUUIDs: device?.serviceUUIDs,
|
|
335
|
+
accepted: isOneKey,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isOneKey) {
|
|
198
340
|
Log?.debug('search device start ======================');
|
|
199
|
-
const { name, localName, id } = device ?? {};
|
|
341
|
+
const { name, localName, id, serviceUUIDs } = device ?? {};
|
|
200
342
|
Log?.debug(
|
|
201
|
-
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
343
|
+
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
344
|
+
id ?? ''
|
|
345
|
+
}\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
|
|
202
346
|
);
|
|
203
347
|
addDevice(device as unknown as Device);
|
|
204
348
|
Log?.debug('search device end ======================\n');
|
|
349
|
+
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
350
|
+
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
351
|
+
name: device?.name,
|
|
352
|
+
localName: device?.localName,
|
|
353
|
+
id: device?.id,
|
|
354
|
+
serviceUUIDs: device?.serviceUUIDs,
|
|
355
|
+
});
|
|
205
356
|
}
|
|
206
357
|
}
|
|
207
358
|
);
|
|
@@ -215,7 +366,14 @@ export default class ReactNativeBleTransport {
|
|
|
215
366
|
|
|
216
367
|
const addDevice = (device: Device) => {
|
|
217
368
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
218
|
-
|
|
369
|
+
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
370
|
+
const protocolType = inferProtocolTypeFromDeviceName(displayName);
|
|
371
|
+
deviceList.push({
|
|
372
|
+
...device,
|
|
373
|
+
name: displayName,
|
|
374
|
+
commType: 'ble',
|
|
375
|
+
...(protocolType ? { protocolType } : {}),
|
|
376
|
+
} as IOneKeyDevice);
|
|
219
377
|
}
|
|
220
378
|
};
|
|
221
379
|
|
|
@@ -227,7 +385,7 @@ export default class ReactNativeBleTransport {
|
|
|
227
385
|
}
|
|
228
386
|
|
|
229
387
|
async acquire(input: BleAcquireInput) {
|
|
230
|
-
const { uuid, forceCleanRunPromise } = input;
|
|
388
|
+
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
231
389
|
|
|
232
390
|
if (!uuid) {
|
|
233
391
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
@@ -245,7 +403,10 @@ export default class ReactNativeBleTransport {
|
|
|
245
403
|
}
|
|
246
404
|
|
|
247
405
|
if (forceCleanRunPromise && this.runPromise) {
|
|
248
|
-
|
|
406
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
407
|
+
this.runPromise.reject(error);
|
|
408
|
+
this.rejectProtocolV2Frame(error);
|
|
409
|
+
this.runPromise = null;
|
|
249
410
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
250
411
|
}
|
|
251
412
|
|
|
@@ -307,7 +468,7 @@ export default class ReactNativeBleTransport {
|
|
|
307
468
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
308
469
|
|
|
309
470
|
try {
|
|
310
|
-
await device.connect(connectOptions);
|
|
471
|
+
device = await device.connect(connectOptions);
|
|
311
472
|
} catch (e) {
|
|
312
473
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
313
474
|
if (
|
|
@@ -317,7 +478,7 @@ export default class ReactNativeBleTransport {
|
|
|
317
478
|
connectOptions = {};
|
|
318
479
|
Log?.debug('second try to reconnect without params');
|
|
319
480
|
try {
|
|
320
|
-
await device.connect();
|
|
481
|
+
device = await device.connect();
|
|
321
482
|
} catch (e) {
|
|
322
483
|
Log?.debug('last try to reconnect error: ', e);
|
|
323
484
|
// last try to reconnect device if this issue exists
|
|
@@ -325,7 +486,7 @@ export default class ReactNativeBleTransport {
|
|
|
325
486
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
326
487
|
Log?.debug('last try to reconnect');
|
|
327
488
|
await device.cancelConnection();
|
|
328
|
-
await device.connect();
|
|
489
|
+
device = await device.connect();
|
|
329
490
|
}
|
|
330
491
|
}
|
|
331
492
|
} else {
|
|
@@ -337,6 +498,7 @@ export default class ReactNativeBleTransport {
|
|
|
337
498
|
await device.discoverAllServicesAndCharacteristics();
|
|
338
499
|
let infos = tryToGetConfiguration(device);
|
|
339
500
|
let characteristics;
|
|
501
|
+
let fallbackServiceUuid: string | undefined;
|
|
340
502
|
|
|
341
503
|
if (!infos) {
|
|
342
504
|
for (const serviceUuid of getBluetoothServiceUuids()) {
|
|
@@ -351,16 +513,44 @@ export default class ReactNativeBleTransport {
|
|
|
351
513
|
}
|
|
352
514
|
|
|
353
515
|
if (!infos) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
516
|
+
const services = await device.services();
|
|
517
|
+
Log?.debug(
|
|
518
|
+
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
519
|
+
services?.map(service => service.uuid)
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const knownService = services.find(service =>
|
|
523
|
+
getInfosForServiceUuid(service.uuid, 'classic')
|
|
524
|
+
);
|
|
525
|
+
const fallbackService =
|
|
526
|
+
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
527
|
+
|
|
528
|
+
if (fallbackService) {
|
|
529
|
+
fallbackServiceUuid = fallbackService.uuid;
|
|
530
|
+
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
531
|
+
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
359
532
|
}
|
|
360
|
-
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
361
533
|
}
|
|
362
534
|
|
|
363
|
-
|
|
535
|
+
if (!infos) {
|
|
536
|
+
if (!fallbackServiceUuid) {
|
|
537
|
+
try {
|
|
538
|
+
Log?.debug('cancel connection when service not found');
|
|
539
|
+
await device.cancelConnection();
|
|
540
|
+
} catch (e) {
|
|
541
|
+
Log?.debug('cancel connection error when service not found: ', e.message || e.reason);
|
|
542
|
+
}
|
|
543
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const serviceUuid = infos?.serviceUuid ?? fallbackServiceUuid;
|
|
548
|
+
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
549
|
+
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
550
|
+
|
|
551
|
+
if (!serviceUuid) {
|
|
552
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
553
|
+
}
|
|
364
554
|
|
|
365
555
|
if (!characteristics) {
|
|
366
556
|
characteristics = await device.characteristicsForService(serviceUuid);
|
|
@@ -373,9 +563,9 @@ export default class ReactNativeBleTransport {
|
|
|
373
563
|
let writeCharacteristic;
|
|
374
564
|
let notifyCharacteristic;
|
|
375
565
|
for (const c of characteristics) {
|
|
376
|
-
if (c.uuid
|
|
566
|
+
if (isSameBleUuid(c.uuid, writeUuid)) {
|
|
377
567
|
writeCharacteristic = c;
|
|
378
|
-
} else if (c.uuid
|
|
568
|
+
} else if (isSameBleUuid(c.uuid, notifyUuid)) {
|
|
379
569
|
notifyCharacteristic = c;
|
|
380
570
|
}
|
|
381
571
|
}
|
|
@@ -402,12 +592,28 @@ export default class ReactNativeBleTransport {
|
|
|
402
592
|
await this.release(uuid);
|
|
403
593
|
|
|
404
594
|
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
595
|
+
const monitorToken = this.nextMonitorToken;
|
|
596
|
+
this.nextMonitorToken += 1;
|
|
597
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
598
|
+
transport.monitorToken = monitorToken;
|
|
599
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
600
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
405
601
|
transport.notifySubscription = this._monitorCharacteristic(
|
|
406
602
|
transport.notifyCharacteristic,
|
|
407
|
-
uuid
|
|
603
|
+
uuid,
|
|
604
|
+
monitorToken,
|
|
605
|
+
notifyTransactionId
|
|
408
606
|
);
|
|
409
607
|
transportCache[uuid] = transport;
|
|
410
608
|
|
|
609
|
+
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
610
|
+
|
|
611
|
+
if (Platform.OS === 'ios') {
|
|
612
|
+
await new Promise<void>(resolve => setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const protocolType = await this.detectProtocol(uuid, expectedProtocol);
|
|
616
|
+
|
|
411
617
|
this.emitter?.emit('device-connect', {
|
|
412
618
|
name: device.name,
|
|
413
619
|
id: device.id,
|
|
@@ -415,6 +621,11 @@ export default class ReactNativeBleTransport {
|
|
|
415
621
|
});
|
|
416
622
|
|
|
417
623
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
624
|
+
if (transportCache[uuid] !== transport) {
|
|
625
|
+
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
418
629
|
try {
|
|
419
630
|
Log?.debug('device disconnect: ', device?.id);
|
|
420
631
|
this.emitter?.emit('device-disconnect', {
|
|
@@ -423,7 +634,9 @@ export default class ReactNativeBleTransport {
|
|
|
423
634
|
connectId: device?.id,
|
|
424
635
|
});
|
|
425
636
|
if (this.runPromise) {
|
|
426
|
-
|
|
637
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
638
|
+
this.runPromise.reject(error);
|
|
639
|
+
this.rejectProtocolV2Frame(error);
|
|
427
640
|
}
|
|
428
641
|
} catch (e) {
|
|
429
642
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -432,19 +645,29 @@ export default class ReactNativeBleTransport {
|
|
|
432
645
|
}
|
|
433
646
|
});
|
|
434
647
|
|
|
435
|
-
return { uuid };
|
|
648
|
+
return { uuid, protocolType };
|
|
436
649
|
}
|
|
437
650
|
|
|
438
|
-
_monitorCharacteristic(
|
|
651
|
+
_monitorCharacteristic(
|
|
652
|
+
characteristic: Characteristic,
|
|
653
|
+
uuid: string,
|
|
654
|
+
monitorToken: number,
|
|
655
|
+
notifyTransactionId: string
|
|
656
|
+
): Subscription {
|
|
439
657
|
let bufferLength = 0;
|
|
440
658
|
let buffer: any[] = [];
|
|
441
659
|
const subscription = characteristic.monitor((error, c) => {
|
|
660
|
+
const isCurrentMonitor = this.monitorTokens.get(uuid) === monitorToken;
|
|
442
661
|
if (error) {
|
|
443
662
|
Log?.debug(
|
|
444
663
|
`error monitor ${characteristic.uuid}, deviceId: ${characteristic.deviceID}: ${
|
|
445
664
|
error as unknown as string
|
|
446
665
|
}`
|
|
447
666
|
);
|
|
667
|
+
if (!isCurrentMonitor) {
|
|
668
|
+
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
448
671
|
if (this.runPromise) {
|
|
449
672
|
let ERROR:
|
|
450
673
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
@@ -464,27 +687,40 @@ export default class ReactNativeBleTransport {
|
|
|
464
687
|
error.reason?.includes('Writing is not permitted') || // pro firmware 2.3.4 upgrade
|
|
465
688
|
error.reason?.includes('notify change failed for device')
|
|
466
689
|
) {
|
|
467
|
-
|
|
468
|
-
|
|
690
|
+
const notifyError = ERRORS.TypedError(
|
|
691
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
469
692
|
);
|
|
693
|
+
this.runPromise.reject(notifyError);
|
|
694
|
+
this.rejectProtocolV2Frame(notifyError);
|
|
470
695
|
Log?.debug(
|
|
471
696
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
472
697
|
);
|
|
473
698
|
return;
|
|
474
699
|
}
|
|
475
|
-
|
|
700
|
+
const notifyError = ERRORS.TypedError(ERROR);
|
|
701
|
+
this.runPromise.reject(notifyError);
|
|
702
|
+
this.rejectProtocolV2Frame(notifyError);
|
|
476
703
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
477
704
|
}
|
|
478
705
|
|
|
479
706
|
return;
|
|
480
707
|
}
|
|
481
708
|
|
|
709
|
+
if (!isCurrentMonitor) {
|
|
710
|
+
Log?.debug('monitor data ignored for stale transport: ', uuid, notifyTransactionId);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
482
714
|
if (!c) {
|
|
483
715
|
throw ERRORS.TypedError(HardwareErrorCode.BleMonitorError);
|
|
484
716
|
}
|
|
485
717
|
|
|
486
718
|
try {
|
|
487
719
|
const data = Buffer.from(c.value as string, 'base64');
|
|
720
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
721
|
+
this.handleProtocolV2Notification(uuid, new Uint8Array(data));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
488
724
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
489
725
|
if (isHeaderChunk(data)) {
|
|
490
726
|
bufferLength = data.readInt32BE(5);
|
|
@@ -493,7 +729,7 @@ export default class ReactNativeBleTransport {
|
|
|
493
729
|
buffer = buffer.concat([...data]);
|
|
494
730
|
}
|
|
495
731
|
|
|
496
|
-
if (buffer.length -
|
|
732
|
+
if (buffer.length - PROTOCOL_V1_MESSAGE_HEADER_SIZE >= bufferLength) {
|
|
497
733
|
const value = Buffer.from(buffer);
|
|
498
734
|
// console.log(
|
|
499
735
|
// '[hd-transport-react-native] Received a complete packet of data, resolve Promise, this.runPromise: ',
|
|
@@ -507,17 +743,31 @@ export default class ReactNativeBleTransport {
|
|
|
507
743
|
}
|
|
508
744
|
} catch (error) {
|
|
509
745
|
Log?.debug('monitor data error: ', error);
|
|
510
|
-
|
|
746
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
747
|
+
this.runPromise?.reject(notifyError);
|
|
748
|
+
this.rejectProtocolV2Frame(notifyError);
|
|
511
749
|
}
|
|
512
|
-
},
|
|
750
|
+
}, notifyTransactionId);
|
|
513
751
|
|
|
514
752
|
return subscription;
|
|
515
753
|
}
|
|
516
754
|
|
|
517
755
|
async release(uuid: string) {
|
|
518
756
|
const transport = transportCache[uuid];
|
|
757
|
+
if (this.runPromise) {
|
|
758
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
759
|
+
this.runPromise.reject(error);
|
|
760
|
+
this.runPromise = null;
|
|
761
|
+
this.rejectProtocolV2Frame(error);
|
|
762
|
+
} else {
|
|
763
|
+
this.resetProtocolV2Frames();
|
|
764
|
+
}
|
|
519
765
|
|
|
520
766
|
if (transport) {
|
|
767
|
+
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
768
|
+
this.monitorTokens.delete(uuid);
|
|
769
|
+
}
|
|
770
|
+
|
|
521
771
|
// Clean up disconnect subscription first to prevent callbacks on released transport
|
|
522
772
|
Log?.debug('release: removing disconnect subscription for device: ', uuid);
|
|
523
773
|
transport.disconnectSubscription?.remove();
|
|
@@ -531,12 +781,25 @@ export default class ReactNativeBleTransport {
|
|
|
531
781
|
transport.notifySubscription?.remove();
|
|
532
782
|
transport.notifySubscription = undefined;
|
|
533
783
|
|
|
784
|
+
if (transport.notifyTransactionId) {
|
|
785
|
+
try {
|
|
786
|
+
await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
|
|
787
|
+
} catch (e) {
|
|
788
|
+
Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
534
792
|
delete transportCache[uuid];
|
|
793
|
+
this.deviceProtocol.delete(uuid);
|
|
794
|
+
}
|
|
535
795
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
796
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
797
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
await this.blePlxManager?.cancelTransaction(uuid);
|
|
801
|
+
} catch (e) {
|
|
802
|
+
Log?.debug('release: cancel transaction error (ignored): ', e?.message || e);
|
|
540
803
|
}
|
|
541
804
|
|
|
542
805
|
return Promise.resolve(true);
|
|
@@ -546,7 +809,12 @@ export default class ReactNativeBleTransport {
|
|
|
546
809
|
await this.call(session, name, data);
|
|
547
810
|
}
|
|
548
811
|
|
|
549
|
-
async call(
|
|
812
|
+
async call(
|
|
813
|
+
uuid: string,
|
|
814
|
+
name: string,
|
|
815
|
+
data: Record<string, unknown>,
|
|
816
|
+
options?: TransportCallOptions
|
|
817
|
+
) {
|
|
550
818
|
if (this.stopped) {
|
|
551
819
|
// eslint-disable-next-line prefer-promise-reject-errors
|
|
552
820
|
return Promise.reject(ERRORS.TypedError('Transport stopped.'));
|
|
@@ -562,13 +830,7 @@ export default class ReactNativeBleTransport {
|
|
|
562
830
|
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
563
831
|
}
|
|
564
832
|
|
|
565
|
-
const
|
|
566
|
-
if (!transport) {
|
|
567
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
this.runPromise = createDeferred();
|
|
571
|
-
const messages = this._messages;
|
|
833
|
+
const protocol = this.getProtocolType(uuid);
|
|
572
834
|
// Upload resources on low-end phones may OOM
|
|
573
835
|
if (name === 'ResourceUpdate' || name === 'ResourceAck') {
|
|
574
836
|
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
|
|
@@ -576,12 +838,44 @@ export default class ReactNativeBleTransport {
|
|
|
576
838
|
hash: data?.hash,
|
|
577
839
|
});
|
|
578
840
|
} else if (LogBlockCommand.has(name)) {
|
|
579
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name);
|
|
841
|
+
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
580
842
|
} else {
|
|
581
|
-
Log?.debug(
|
|
843
|
+
Log?.debug(
|
|
844
|
+
'transport-react-native',
|
|
845
|
+
'call-',
|
|
846
|
+
' name: ',
|
|
847
|
+
name,
|
|
848
|
+
' data: ',
|
|
849
|
+
data,
|
|
850
|
+
' protocol: ',
|
|
851
|
+
protocol
|
|
852
|
+
);
|
|
582
853
|
}
|
|
583
854
|
|
|
584
|
-
|
|
855
|
+
if (protocol === 'V2') {
|
|
856
|
+
return this.callProtocolV2(uuid, name, data, options);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return this.callProtocolV1(uuid, name, data, options);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private async callProtocolV1(
|
|
863
|
+
uuid: string,
|
|
864
|
+
name: string,
|
|
865
|
+
data: Record<string, unknown>,
|
|
866
|
+
options?: TransportCallOptions
|
|
867
|
+
) {
|
|
868
|
+
if (!this._messages) {
|
|
869
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const transport = this.getCachedTransport(uuid);
|
|
873
|
+
const runPromise = createDeferred<string>();
|
|
874
|
+
runPromise.promise.catch(() => undefined);
|
|
875
|
+
this.runPromise = runPromise;
|
|
876
|
+
const messages = this._messages;
|
|
877
|
+
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
878
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
585
879
|
|
|
586
880
|
async function writeChunkedData(
|
|
587
881
|
buffers: ByteBuffer[],
|
|
@@ -652,20 +946,41 @@ export default class ReactNativeBleTransport {
|
|
|
652
946
|
}
|
|
653
947
|
|
|
654
948
|
try {
|
|
655
|
-
const response = await
|
|
949
|
+
const response = await Promise.race([
|
|
950
|
+
runPromise.promise,
|
|
951
|
+
new Promise<never>((_, reject) => {
|
|
952
|
+
if (options?.timeoutMs) {
|
|
953
|
+
timeout = setTimeout(() => {
|
|
954
|
+
const error = ERRORS.TypedError(
|
|
955
|
+
HardwareErrorCode.BleTimeoutError,
|
|
956
|
+
`BLE response timeout after ${options.timeoutMs}ms for ${name}`
|
|
957
|
+
);
|
|
958
|
+
runPromise.reject(error);
|
|
959
|
+
reject(error);
|
|
960
|
+
}, options.timeoutMs);
|
|
961
|
+
}
|
|
962
|
+
}),
|
|
963
|
+
]);
|
|
656
964
|
|
|
657
965
|
if (typeof response !== 'string') {
|
|
658
966
|
throw new Error('Returning data is not string.');
|
|
659
967
|
}
|
|
660
968
|
|
|
661
969
|
Log?.debug('receive data: ', response);
|
|
662
|
-
const jsonData =
|
|
970
|
+
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
663
971
|
return check.call(jsonData);
|
|
664
972
|
} catch (e) {
|
|
665
|
-
|
|
973
|
+
if (name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
974
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe call failed:', e);
|
|
975
|
+
} else {
|
|
976
|
+
Log?.error('call error: ', e);
|
|
977
|
+
}
|
|
666
978
|
throw e;
|
|
667
979
|
} finally {
|
|
668
|
-
|
|
980
|
+
if (timeout) clearTimeout(timeout);
|
|
981
|
+
if (this.runPromise === runPromise) {
|
|
982
|
+
this.runPromise = null;
|
|
983
|
+
}
|
|
669
984
|
}
|
|
670
985
|
}
|
|
671
986
|
|
|
@@ -732,6 +1047,8 @@ export default class ReactNativeBleTransport {
|
|
|
732
1047
|
if (transportCache[session]) {
|
|
733
1048
|
delete transportCache[session];
|
|
734
1049
|
}
|
|
1050
|
+
this.deviceProtocol.delete(session);
|
|
1051
|
+
this.protocolV2Assemblers.delete(session);
|
|
735
1052
|
|
|
736
1053
|
// emit the disconnect event
|
|
737
1054
|
try {
|
|
@@ -754,4 +1071,306 @@ export default class ReactNativeBleTransport {
|
|
|
754
1071
|
}
|
|
755
1072
|
this.runPromise = null;
|
|
756
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
private getCachedTransport(uuid: string) {
|
|
1076
|
+
const transport = transportCache[uuid];
|
|
1077
|
+
if (!transport) {
|
|
1078
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1079
|
+
}
|
|
1080
|
+
return transport;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1084
|
+
return ERRORS.TypedError(
|
|
1085
|
+
HardwareErrorCode.RuntimeError,
|
|
1086
|
+
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private async detectProtocol(
|
|
1091
|
+
uuid: string,
|
|
1092
|
+
expectedProtocol?: ProtocolType
|
|
1093
|
+
): Promise<ProtocolType> {
|
|
1094
|
+
if (expectedProtocol === 'V1') {
|
|
1095
|
+
if (await this.probeProtocolV1(uuid)) {
|
|
1096
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1097
|
+
Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V1 (expected)`);
|
|
1098
|
+
return 'V1';
|
|
1099
|
+
}
|
|
1100
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (expectedProtocol === 'V2') {
|
|
1104
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1105
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1106
|
+
Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (expected)`);
|
|
1107
|
+
return 'V2';
|
|
1108
|
+
}
|
|
1109
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (this.deviceProtocol.get(uuid) === 'V2' && (await this.probeProtocolV2(uuid))) {
|
|
1113
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1114
|
+
Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> V2 (cached)`);
|
|
1115
|
+
return 'V2';
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
let protocol: ProtocolType = 'V1';
|
|
1119
|
+
if (!(await this.probeProtocolV1(uuid)) && (await this.probeProtocolV2(uuid))) {
|
|
1120
|
+
protocol = 'V2';
|
|
1121
|
+
}
|
|
1122
|
+
this.deviceProtocol.set(uuid, protocol);
|
|
1123
|
+
Log?.debug(`[ReactNativeBleTransport] detectProtocol: uuid=${uuid} -> ${protocol}`);
|
|
1124
|
+
return protocol;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private async probeProtocolV1(uuid: string) {
|
|
1128
|
+
if (!this._messages) {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
try {
|
|
1133
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1134
|
+
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1135
|
+
return true;
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
private async probeProtocolV2(uuid: string) {
|
|
1143
|
+
if (!this._messages || !this._messagesV2) {
|
|
1144
|
+
return false;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1148
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1149
|
+
return probeProtocolV2Helper({
|
|
1150
|
+
call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
|
|
1151
|
+
timeoutMs: PROTOCOL_V2_PROBE_TIMEOUT_MS,
|
|
1152
|
+
logger: Log,
|
|
1153
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1154
|
+
onProbeFailed: () => {
|
|
1155
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1156
|
+
this.resetProtocolV2Frames();
|
|
1157
|
+
},
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
|
|
1162
|
+
try {
|
|
1163
|
+
if (!this.runPromise) {
|
|
1164
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1165
|
+
this.resetProtocolV2Frames();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (data.length === 0) return;
|
|
1170
|
+
|
|
1171
|
+
const assembler = this.protocolV2Assemblers.get(uuid);
|
|
1172
|
+
if (!assembler) return;
|
|
1173
|
+
|
|
1174
|
+
let frameData = assembler.push(data);
|
|
1175
|
+
while (frameData) {
|
|
1176
|
+
this.resolveProtocolV2Frame(frameData);
|
|
1177
|
+
frameData = assembler.push(new Uint8Array(0));
|
|
1178
|
+
}
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1181
|
+
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1182
|
+
this.runPromise?.reject(notifyError);
|
|
1183
|
+
this.rejectProtocolV2Frame(notifyError);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private resolveProtocolV2Frame(frame: Uint8Array) {
|
|
1188
|
+
if (this.protocolV2FramePromise) {
|
|
1189
|
+
this.protocolV2FramePromise.resolve(frame);
|
|
1190
|
+
this.protocolV2FramePromise = null;
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
this.protocolV2FrameQueue.push(frame);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private rejectProtocolV2Frame(error: Error) {
|
|
1197
|
+
this.protocolV2FrameQueue = [];
|
|
1198
|
+
if (this.protocolV2FramePromise) {
|
|
1199
|
+
this.protocolV2FramePromise.reject(error);
|
|
1200
|
+
this.protocolV2FramePromise = null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private resetProtocolV2Frames() {
|
|
1205
|
+
this.protocolV2FrameQueue = [];
|
|
1206
|
+
this.protocolV2FramePromise = null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private async readProtocolV2Frame() {
|
|
1210
|
+
const queuedFrame = this.protocolV2FrameQueue.shift();
|
|
1211
|
+
if (queuedFrame) {
|
|
1212
|
+
return queuedFrame;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const framePromise = createDeferred<Uint8Array>();
|
|
1216
|
+
this.protocolV2FramePromise = framePromise;
|
|
1217
|
+
try {
|
|
1218
|
+
return await framePromise.promise;
|
|
1219
|
+
} finally {
|
|
1220
|
+
if (this.protocolV2FramePromise === framePromise) {
|
|
1221
|
+
this.protocolV2FramePromise = null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private async writeProtocolV2Frame(
|
|
1227
|
+
transport: BleTransport,
|
|
1228
|
+
frame: Uint8Array,
|
|
1229
|
+
options?: { highVolume?: boolean; writeWithResponse?: boolean }
|
|
1230
|
+
) {
|
|
1231
|
+
const tuning = getProtocolV2BleTuning();
|
|
1232
|
+
const packetCapacity =
|
|
1233
|
+
Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength;
|
|
1234
|
+
const writeWithResponse =
|
|
1235
|
+
!!options?.writeWithResponse ||
|
|
1236
|
+
(!!options?.highVolume && tuning.highVolumeWriteWithResponse);
|
|
1237
|
+
const shouldThrottle = !!options?.highVolume && !writeWithResponse;
|
|
1238
|
+
let packetsWritten = 0;
|
|
1239
|
+
|
|
1240
|
+
try {
|
|
1241
|
+
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1242
|
+
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1243
|
+
const base64 = Buffer.from(chunk).toString('base64');
|
|
1244
|
+
if (writeWithResponse) {
|
|
1245
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1246
|
+
} else {
|
|
1247
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1248
|
+
}
|
|
1249
|
+
packetsWritten += 1;
|
|
1250
|
+
|
|
1251
|
+
if (
|
|
1252
|
+
shouldThrottle &&
|
|
1253
|
+
packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
|
|
1254
|
+
offset + packetCapacity < frame.length
|
|
1255
|
+
) {
|
|
1256
|
+
await delay(tuning.highVolumeWritePauseMs);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (shouldThrottle) {
|
|
1261
|
+
await delay(tuning.highVolumeWriteFlushDelayMs);
|
|
1262
|
+
}
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
if (options?.highVolume && !writeWithResponse && packetsWritten === 0) {
|
|
1265
|
+
Log?.debug(
|
|
1266
|
+
'[ReactNativeBleTransport] Protocol V2 high-volume writeWithoutResponse failed before data was sent, fallback to writeWithResponse:',
|
|
1267
|
+
error
|
|
1268
|
+
);
|
|
1269
|
+
await this.writeProtocolV2Frame(transport, frame, {
|
|
1270
|
+
highVolume: true,
|
|
1271
|
+
writeWithResponse: true,
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
private async callProtocolV2(
|
|
1280
|
+
uuid: string,
|
|
1281
|
+
name: string,
|
|
1282
|
+
data: Record<string, unknown>,
|
|
1283
|
+
options?: TransportCallOptions
|
|
1284
|
+
) {
|
|
1285
|
+
if (!this._messages || !this._messagesV2) {
|
|
1286
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
const forceRun = name === 'Initialize' || name === 'Cancel' || name === 'GetProtoVersion';
|
|
1290
|
+
if (this.runPromise) {
|
|
1291
|
+
if (!forceRun) {
|
|
1292
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1293
|
+
}
|
|
1294
|
+
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1295
|
+
this.runPromise.reject(error);
|
|
1296
|
+
this.rejectProtocolV2Frame(error);
|
|
1297
|
+
this.runPromise = null;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
const transport = this.getCachedTransport(uuid);
|
|
1301
|
+
const runPromise = createDeferred<Uint8Array>();
|
|
1302
|
+
runPromise.promise.catch(() => undefined);
|
|
1303
|
+
this.runPromise = runPromise;
|
|
1304
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1305
|
+
this.resetProtocolV2Frames();
|
|
1306
|
+
let completed = false;
|
|
1307
|
+
const callOptions = {
|
|
1308
|
+
...options,
|
|
1309
|
+
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1310
|
+
};
|
|
1311
|
+
const highVolumeWrite = LogBlockCommand.has(name);
|
|
1312
|
+
|
|
1313
|
+
if (highVolumeWrite) {
|
|
1314
|
+
const tuning = getProtocolV2BleTuning();
|
|
1315
|
+
Log?.debug(
|
|
1316
|
+
'[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
|
|
1317
|
+
name,
|
|
1318
|
+
{
|
|
1319
|
+
packetCapacity:
|
|
1320
|
+
Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
1321
|
+
burstSize: tuning.highVolumeWriteBurstSize,
|
|
1322
|
+
pauseMs: tuning.highVolumeWritePauseMs,
|
|
1323
|
+
flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
|
|
1324
|
+
writeWithResponse: tuning.highVolumeWriteWithResponse,
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
const session = new ProtocolV2Session({
|
|
1331
|
+
schemas: {
|
|
1332
|
+
protocolV1: this._messages,
|
|
1333
|
+
protocolV2: this._messagesV2,
|
|
1334
|
+
},
|
|
1335
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
1336
|
+
writeFrame: (frame: Uint8Array) =>
|
|
1337
|
+
this.writeProtocolV2Frame(transport, frame, { highVolume: highVolumeWrite }),
|
|
1338
|
+
readFrame: async () => {
|
|
1339
|
+
const rxFrame = await this.readProtocolV2Frame();
|
|
1340
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
1341
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1342
|
+
}
|
|
1343
|
+
return rxFrame;
|
|
1344
|
+
},
|
|
1345
|
+
logger: Log,
|
|
1346
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1347
|
+
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
1348
|
+
ERRORS.TypedError(
|
|
1349
|
+
HardwareErrorCode.BleTimeoutError,
|
|
1350
|
+
`BLE response timeout after ${timeout}ms for ${name}`
|
|
1351
|
+
),
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
const result = await session.call(name, data, callOptions);
|
|
1355
|
+
completed = true;
|
|
1356
|
+
return result;
|
|
1357
|
+
} catch (e) {
|
|
1358
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1359
|
+
this.resetProtocolV2Frames();
|
|
1360
|
+
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1361
|
+
throw e;
|
|
1362
|
+
} finally {
|
|
1363
|
+
if (!completed) {
|
|
1364
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1365
|
+
}
|
|
1366
|
+
this.resetProtocolV2Frames();
|
|
1367
|
+
if (this.runPromise === runPromise) {
|
|
1368
|
+
this.runPromise = null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
getProtocolType(path: string): ProtocolType {
|
|
1374
|
+
return this.deviceProtocol.get(path) ?? 'V1';
|
|
1375
|
+
}
|
|
757
1376
|
}
|