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