@onekeyfe/hd-transport-react-native 1.2.0-alpha.7 → 1.2.0-alpha.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BleTransport.d.ts +1 -3
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +0 -2
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +99 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +705 -387
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +7 -6
- package/src/BleTransport.ts +6 -41
- package/src/__tests__/BleTransport.test.ts +93 -0
- package/src/__tests__/bleStrategy.test.ts +1 -6
- package/src/__tests__/connectTimeout.test.ts +281 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +769 -0
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +0 -25
- package/src/constants.ts +7 -13
- package/src/index.ts +931 -408
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +1 -0
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -12,26 +12,31 @@ import transport, {
|
|
|
12
12
|
LogBlockCommand,
|
|
13
13
|
type OneKeyDeviceInfoBase,
|
|
14
14
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
|
+
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
15
16
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
16
17
|
type ProtocolType,
|
|
18
|
+
type ProtocolV2CallContext,
|
|
17
19
|
ProtocolV2FrameAssembler,
|
|
18
|
-
|
|
20
|
+
ProtocolV2LinkManager,
|
|
21
|
+
TRANSPORT_EVENT,
|
|
19
22
|
type TransportCallOptions,
|
|
20
23
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
24
|
+
writeProtocolV2BleFrame,
|
|
21
25
|
} from '@onekeyfe/hd-transport';
|
|
22
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
ERRORS,
|
|
28
|
+
HardwareErrorCode,
|
|
29
|
+
createDeferred,
|
|
30
|
+
isOnekeyBluetoothDevice,
|
|
31
|
+
isPro2FindMyAdvertisementName,
|
|
32
|
+
} from '@onekeyfe/hd-shared';
|
|
23
33
|
|
|
24
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
25
|
-
import {
|
|
26
|
-
hasWritableCapability,
|
|
27
|
-
resolveBleWriteMode,
|
|
28
|
-
resolveProtocolV2PacketCapacity,
|
|
29
|
-
} from './bleStrategy';
|
|
35
|
+
import { hasWritableCapability, resolveProtocolV2PacketCapacity } from './bleStrategy';
|
|
30
36
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
31
37
|
import {
|
|
32
38
|
ANDROID_PACKET_LENGTH,
|
|
33
39
|
IOS_PACKET_LENGTH,
|
|
34
|
-
getBleUuidKey,
|
|
35
40
|
getBluetoothServiceUuids,
|
|
36
41
|
getInfosForServiceUuid,
|
|
37
42
|
isSameBleUuid,
|
|
@@ -40,6 +45,7 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
40
45
|
import BleTransport from './BleTransport';
|
|
41
46
|
import timer from './utils/timer';
|
|
42
47
|
import { bleLogger, setBleLogger } from './logger';
|
|
48
|
+
import { createTransportCallLog } from './transportLog';
|
|
43
49
|
|
|
44
50
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
45
51
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -55,24 +61,53 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
55
61
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
56
62
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
57
63
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
58
|
-
const
|
|
64
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
59
65
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
60
66
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
61
67
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
62
68
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
63
69
|
|
|
64
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
70
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
65
71
|
type ResolvedBleCharacteristics = {
|
|
66
72
|
writeCharacteristic: Characteristic;
|
|
67
73
|
notifyCharacteristic: Characteristic;
|
|
68
74
|
};
|
|
69
75
|
|
|
76
|
+
const isAsciiWhitespace = (code: number) =>
|
|
77
|
+
code === 0x09 ||
|
|
78
|
+
code === 0x0a ||
|
|
79
|
+
code === 0x0b ||
|
|
80
|
+
code === 0x0c ||
|
|
81
|
+
code === 0x0d ||
|
|
82
|
+
code === 0x20;
|
|
83
|
+
|
|
84
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
85
|
+
let searchFrom = 0;
|
|
86
|
+
while (searchFrom < text.length) {
|
|
87
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
88
|
+
if (statusIndex < 0) return false;
|
|
89
|
+
|
|
90
|
+
let cursor = statusIndex + 'status'.length;
|
|
91
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
92
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
93
|
+
cursor += 1;
|
|
94
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
95
|
+
}
|
|
96
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
97
|
+
|
|
98
|
+
searchFrom = statusIndex + 'status'.length;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
70
103
|
const delay = (ms: number) =>
|
|
71
104
|
new Promise<void>(resolve => {
|
|
72
105
|
setTimeout(resolve, ms);
|
|
73
106
|
});
|
|
74
107
|
|
|
75
|
-
const getFirmwareUploadWriteRetryType = (
|
|
108
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
109
|
+
error: unknown
|
|
110
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
76
111
|
if (!error || typeof error !== 'object') return null;
|
|
77
112
|
const bleWriteError = error as {
|
|
78
113
|
androidErrorCode?: unknown;
|
|
@@ -83,13 +118,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
83
118
|
name?: unknown;
|
|
84
119
|
};
|
|
85
120
|
|
|
86
|
-
if (
|
|
87
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
88
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
89
|
-
) {
|
|
90
|
-
return 'reconnectable';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
121
|
if (
|
|
94
122
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
95
123
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -100,28 +128,35 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
100
128
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
101
129
|
.filter(value => typeof value === 'string')
|
|
102
130
|
.join(' ');
|
|
103
|
-
return
|
|
131
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
104
132
|
};
|
|
105
133
|
|
|
106
134
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
107
135
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
108
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
109
136
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
110
137
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
111
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
140
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
141
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
142
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
143
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
144
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
145
|
+
*/
|
|
146
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
147
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
148
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
149
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
150
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
151
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
152
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
153
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
154
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
112
155
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
113
156
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
114
|
-
const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
|
|
115
|
-
const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
|
|
116
|
-
const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
|
|
117
|
-
|
|
118
157
|
export type ProtocolV2BleTuning = {
|
|
119
158
|
iosPacketLength?: number;
|
|
120
159
|
androidPacketLength?: number;
|
|
121
|
-
highVolumeWriteBurstSize?: number;
|
|
122
|
-
highVolumeWritePauseMs?: number;
|
|
123
|
-
highVolumeWriteFlushDelayMs?: number;
|
|
124
|
-
highVolumeWriteWithResponse?: boolean;
|
|
125
160
|
};
|
|
126
161
|
|
|
127
162
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
@@ -129,10 +164,6 @@ type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
|
129
164
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
130
165
|
iosPacketLength: IOS_PACKET_LENGTH,
|
|
131
166
|
androidPacketLength: ANDROID_PACKET_LENGTH,
|
|
132
|
-
highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
|
|
133
|
-
highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
|
|
134
|
-
highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
|
|
135
|
-
highVolumeWriteWithResponse: false,
|
|
136
167
|
};
|
|
137
168
|
|
|
138
169
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -153,27 +184,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
|
153
184
|
tuning.androidPacketLength,
|
|
154
185
|
protocolV2BleTuning.androidPacketLength
|
|
155
186
|
),
|
|
156
|
-
highVolumeWriteBurstSize: normalizePositiveInteger(
|
|
157
|
-
tuning.highVolumeWriteBurstSize,
|
|
158
|
-
protocolV2BleTuning.highVolumeWriteBurstSize
|
|
159
|
-
),
|
|
160
|
-
highVolumeWritePauseMs: normalizePositiveInteger(
|
|
161
|
-
tuning.highVolumeWritePauseMs,
|
|
162
|
-
protocolV2BleTuning.highVolumeWritePauseMs
|
|
163
|
-
),
|
|
164
|
-
highVolumeWriteFlushDelayMs: normalizePositiveInteger(
|
|
165
|
-
tuning.highVolumeWriteFlushDelayMs,
|
|
166
|
-
protocolV2BleTuning.highVolumeWriteFlushDelayMs
|
|
167
|
-
),
|
|
168
|
-
highVolumeWriteWithResponse:
|
|
169
|
-
tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
|
|
170
187
|
};
|
|
171
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
188
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
export function resetProtocolV2BleTuning() {
|
|
175
192
|
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
176
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
193
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
177
194
|
}
|
|
178
195
|
|
|
179
196
|
export function getProtocolV2BleTuning() {
|
|
@@ -188,24 +205,54 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
188
205
|
return device?.name || device?.localName || null;
|
|
189
206
|
}
|
|
190
207
|
|
|
191
|
-
function isGenericBleService(uuid?: string | null) {
|
|
192
|
-
return ['1800', '1801', '180a'].includes(getBleUuidKey(uuid));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function hasKnownOneKeyService(device?: Device | null) {
|
|
196
|
-
return (device?.serviceUUIDs ?? []).some(serviceUuid =>
|
|
197
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
208
|
const ANDROID_REQUEST_MTU = 256;
|
|
202
209
|
|
|
210
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
211
|
+
|
|
203
212
|
const connectOptions: Record<string, unknown> = {
|
|
204
213
|
requestMTU: ANDROID_REQUEST_MTU,
|
|
205
|
-
timeout:
|
|
214
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
206
215
|
refreshGatt: 'OnConnected',
|
|
207
216
|
};
|
|
208
217
|
|
|
218
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
219
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
220
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
225
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
226
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
227
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
228
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
229
|
+
*/
|
|
230
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
231
|
+
/**
|
|
232
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
233
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
234
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
235
|
+
* budget.
|
|
236
|
+
*/
|
|
237
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
238
|
+
/**
|
|
239
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
240
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
241
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
242
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
243
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
244
|
+
*/
|
|
245
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
246
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
247
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
248
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
249
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
250
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
251
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
252
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
253
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
254
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
255
|
+
|
|
209
256
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
210
257
|
|
|
211
258
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -222,9 +269,10 @@ const requestAndroidMtu = async (device: Device) => {
|
|
|
222
269
|
|
|
223
270
|
try {
|
|
224
271
|
const mtuDevice = await device.requestMTU(ANDROID_REQUEST_MTU);
|
|
225
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
272
|
+
Log?.debug('[ReactNativeBleTransport] MTU configured', {
|
|
273
|
+
deviceId: device.id,
|
|
226
274
|
requested: ANDROID_REQUEST_MTU,
|
|
227
|
-
|
|
275
|
+
actual: mtuDevice.mtu,
|
|
228
276
|
});
|
|
229
277
|
return mtuDevice;
|
|
230
278
|
} catch (error) {
|
|
@@ -272,6 +320,8 @@ export default class ReactNativeBleTransport {
|
|
|
272
320
|
|
|
273
321
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
274
322
|
|
|
323
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
324
|
+
|
|
275
325
|
name = 'ReactNativeBleTransport';
|
|
276
326
|
|
|
277
327
|
configured = false;
|
|
@@ -282,6 +332,8 @@ export default class ReactNativeBleTransport {
|
|
|
282
332
|
|
|
283
333
|
runPromise: Deferred<any> | null = null;
|
|
284
334
|
|
|
335
|
+
private runPromiseDeviceId: string | null = null;
|
|
336
|
+
|
|
285
337
|
emitter?: EventEmitter;
|
|
286
338
|
|
|
287
339
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -289,20 +341,59 @@ export default class ReactNativeBleTransport {
|
|
|
289
341
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
290
342
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
291
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
346
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
347
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
348
|
+
* transport that was never validated.
|
|
349
|
+
*/
|
|
350
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
351
|
+
|
|
352
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
353
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
354
|
+
|
|
355
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
356
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
357
|
+
|
|
292
358
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
293
359
|
|
|
360
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
361
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
362
|
+
|
|
363
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
364
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
365
|
+
|
|
294
366
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
295
367
|
|
|
296
368
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
297
369
|
|
|
298
370
|
private protocolV2FramePromises: Map<string, Deferred<Uint8Array>> = new Map();
|
|
299
371
|
|
|
300
|
-
private
|
|
301
|
-
|
|
302
|
-
|
|
372
|
+
private protocolV2Links = new ProtocolV2LinkManager<string>({
|
|
373
|
+
getSchemas: () => {
|
|
374
|
+
if (!this._messages || !this._messagesV2) {
|
|
375
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
protocolV1: this._messages,
|
|
379
|
+
protocolV2: this._messagesV2,
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
classifyError: () => 'link-fatal',
|
|
383
|
+
onLinkInvalidated: async (uuid, reason) => {
|
|
384
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
385
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
386
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
387
|
+
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
388
|
+
await this.releaseNative(uuid, true);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
});
|
|
303
392
|
|
|
304
393
|
private monitorTokens: Map<string, number> = new Map();
|
|
305
394
|
|
|
395
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
396
|
+
|
|
306
397
|
private nextMonitorToken = 1;
|
|
307
398
|
|
|
308
399
|
constructor(options: TransportOptions) {
|
|
@@ -321,8 +412,19 @@ export default class ReactNativeBleTransport {
|
|
|
321
412
|
}
|
|
322
413
|
|
|
323
414
|
configureProtocolV2(signedData: any) {
|
|
415
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
416
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
324
421
|
this._messagesV2 = parseConfigure(signedData);
|
|
325
|
-
|
|
422
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
423
|
+
if (isReconfiguration) {
|
|
424
|
+
this.protocolV2Links
|
|
425
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
426
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
427
|
+
}
|
|
326
428
|
}
|
|
327
429
|
|
|
328
430
|
listen() {
|
|
@@ -352,29 +454,15 @@ export default class ReactNativeBleTransport {
|
|
|
352
454
|
}
|
|
353
455
|
}
|
|
354
456
|
|
|
355
|
-
let fallbackServiceUuid: string | undefined;
|
|
356
|
-
|
|
357
457
|
if (!infos) {
|
|
358
458
|
const services = await device.services();
|
|
359
459
|
Log?.debug(
|
|
360
460
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
361
461
|
services?.map(service => service.uuid)
|
|
362
462
|
);
|
|
363
|
-
|
|
364
|
-
const knownService = services.find(service =>
|
|
365
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
366
|
-
);
|
|
367
|
-
const fallbackService =
|
|
368
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
369
|
-
|
|
370
|
-
if (fallbackService) {
|
|
371
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
372
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
373
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
374
|
-
}
|
|
375
463
|
}
|
|
376
464
|
|
|
377
|
-
if (!infos
|
|
465
|
+
if (!infos) {
|
|
378
466
|
try {
|
|
379
467
|
Log?.debug('cancel connection when service not found');
|
|
380
468
|
await device.cancelConnection();
|
|
@@ -384,9 +472,7 @@ export default class ReactNativeBleTransport {
|
|
|
384
472
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
385
473
|
}
|
|
386
474
|
|
|
387
|
-
const serviceUuid = infos
|
|
388
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
389
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
475
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
390
476
|
|
|
391
477
|
if (!serviceUuid) {
|
|
392
478
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -436,6 +522,7 @@ export default class ReactNativeBleTransport {
|
|
|
436
522
|
|
|
437
523
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
438
524
|
transport.disconnectSubscription?.remove();
|
|
525
|
+
const { monitorToken } = transport;
|
|
439
526
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
440
527
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
441
528
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -445,18 +532,17 @@ export default class ReactNativeBleTransport {
|
|
|
445
532
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
446
533
|
return;
|
|
447
534
|
}
|
|
535
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
536
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
448
539
|
|
|
449
540
|
try {
|
|
450
541
|
Log?.debug('device disconnect: ', device?.id);
|
|
451
|
-
this.
|
|
452
|
-
|
|
453
|
-
id: device?.id,
|
|
454
|
-
connectId: device?.id,
|
|
455
|
-
});
|
|
456
|
-
if (this.runPromise) {
|
|
542
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
543
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
457
544
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
458
545
|
this.runPromise.reject(error);
|
|
459
|
-
this.rejectAllProtocolV2Frames(error);
|
|
460
546
|
}
|
|
461
547
|
} catch (e) {
|
|
462
548
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -466,6 +552,22 @@ export default class ReactNativeBleTransport {
|
|
|
466
552
|
});
|
|
467
553
|
}
|
|
468
554
|
|
|
555
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
556
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
560
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
564
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
565
|
+
name,
|
|
566
|
+
id: uuid,
|
|
567
|
+
connectId: uuid,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
469
571
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
470
572
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
471
573
|
try {
|
|
@@ -478,22 +580,21 @@ export default class ReactNativeBleTransport {
|
|
|
478
580
|
const isConnected = await device.isConnected().catch(() => false);
|
|
479
581
|
if (!isConnected) {
|
|
480
582
|
try {
|
|
481
|
-
device = await device.connect(connectOptions);
|
|
583
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
482
584
|
} catch (e) {
|
|
483
585
|
if (
|
|
484
586
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
485
587
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
486
588
|
) {
|
|
487
|
-
device = await device.connect();
|
|
589
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
488
590
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
489
591
|
throw e;
|
|
490
592
|
}
|
|
491
593
|
}
|
|
492
594
|
}
|
|
493
595
|
|
|
494
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
495
|
-
device
|
|
496
|
-
);
|
|
596
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
597
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
497
598
|
|
|
498
599
|
transport.device = device;
|
|
499
600
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -553,14 +654,13 @@ export default class ReactNativeBleTransport {
|
|
|
553
654
|
}
|
|
554
655
|
|
|
555
656
|
blePlxManager.startDeviceScan(
|
|
556
|
-
|
|
657
|
+
getBluetoothServiceUuids(),
|
|
557
658
|
{
|
|
558
659
|
allowDuplicates: true,
|
|
559
660
|
scanMode: ScanMode.LowLatency,
|
|
560
661
|
},
|
|
561
662
|
(error, device) => {
|
|
562
663
|
if (error) {
|
|
563
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
564
664
|
Log?.debug('ble scan error: ', error);
|
|
565
665
|
if (
|
|
566
666
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -584,33 +684,23 @@ export default class ReactNativeBleTransport {
|
|
|
584
684
|
}
|
|
585
685
|
|
|
586
686
|
const displayName = getDeviceDisplayName(device);
|
|
687
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
688
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
689
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
690
|
+
const isFindMyPeripheral =
|
|
691
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
692
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
587
693
|
const isOneKey =
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
!!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
|
|
593
|
-
|
|
594
|
-
if (shouldTraceCandidate) {
|
|
595
|
-
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
694
|
+
!isUnnamedIOSPeripheral &&
|
|
695
|
+
!isFindMyPeripheral &&
|
|
696
|
+
isOnekeyBluetoothDevice({
|
|
697
|
+
id: device?.id,
|
|
596
698
|
name: device?.name,
|
|
597
699
|
localName: device?.localName,
|
|
598
|
-
|
|
599
|
-
serviceUUIDs: device?.serviceUUIDs,
|
|
600
|
-
accepted: isOneKey,
|
|
700
|
+
serviceUuids: device?.serviceUUIDs,
|
|
601
701
|
});
|
|
602
|
-
}
|
|
603
|
-
|
|
604
702
|
if (isOneKey) {
|
|
605
|
-
Log?.debug('search device start ======================');
|
|
606
|
-
const { name, localName, id, serviceUUIDs } = device ?? {};
|
|
607
|
-
Log?.debug(
|
|
608
|
-
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
609
|
-
id ?? ''
|
|
610
|
-
}\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
|
|
611
|
-
);
|
|
612
703
|
addDevice(device as unknown as Device);
|
|
613
|
-
Log?.debug('search device end ======================\n');
|
|
614
704
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
615
705
|
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
616
706
|
name: device?.name,
|
|
@@ -622,12 +712,32 @@ export default class ReactNativeBleTransport {
|
|
|
622
712
|
}
|
|
623
713
|
);
|
|
624
714
|
|
|
625
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
715
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
716
|
+
devices => {
|
|
717
|
+
for (const device of devices) {
|
|
718
|
+
const localName =
|
|
719
|
+
'localName' in device && typeof device.localName === 'string'
|
|
720
|
+
? device.localName
|
|
721
|
+
: null;
|
|
722
|
+
const isFindMyPeripheral =
|
|
723
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
724
|
+
isPro2FindMyAdvertisementName(localName);
|
|
725
|
+
|
|
726
|
+
if (
|
|
727
|
+
!isFindMyPeripheral &&
|
|
728
|
+
isOnekeyBluetoothDevice({
|
|
729
|
+
id: device.id,
|
|
730
|
+
name: device.name,
|
|
731
|
+
localName,
|
|
732
|
+
serviceUuids: device.serviceUUIDs,
|
|
733
|
+
})
|
|
734
|
+
) {
|
|
735
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
736
|
+
addDevice(device as unknown as Device);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
629
739
|
}
|
|
630
|
-
|
|
740
|
+
);
|
|
631
741
|
|
|
632
742
|
const addDevice = (device: Device) => {
|
|
633
743
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -641,6 +751,12 @@ export default class ReactNativeBleTransport {
|
|
|
641
751
|
name: displayName,
|
|
642
752
|
commType: 'ble',
|
|
643
753
|
} as IOneKeyDevice);
|
|
754
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
755
|
+
deviceId: device.id,
|
|
756
|
+
name: displayName,
|
|
757
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
758
|
+
protocolHint,
|
|
759
|
+
});
|
|
644
760
|
}
|
|
645
761
|
};
|
|
646
762
|
|
|
@@ -651,6 +767,46 @@ export default class ReactNativeBleTransport {
|
|
|
651
767
|
});
|
|
652
768
|
}
|
|
653
769
|
|
|
770
|
+
private async installTransportForAcquire(
|
|
771
|
+
uuid: string,
|
|
772
|
+
device: Device,
|
|
773
|
+
characteristics?: ResolvedBleCharacteristics
|
|
774
|
+
) {
|
|
775
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
776
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
777
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
778
|
+
if (Platform.OS === 'android') {
|
|
779
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : transport.mtuSize;
|
|
780
|
+
}
|
|
781
|
+
const monitorToken = this.nextMonitorToken;
|
|
782
|
+
this.nextMonitorToken += 1;
|
|
783
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
784
|
+
transport.monitorToken = monitorToken;
|
|
785
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
786
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
787
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
788
|
+
transport.notifyCharacteristic,
|
|
789
|
+
uuid,
|
|
790
|
+
monitorToken,
|
|
791
|
+
notifyTransactionId
|
|
792
|
+
);
|
|
793
|
+
transportCache[uuid] = transport;
|
|
794
|
+
this.protocolV2Assemblers.set(
|
|
795
|
+
uuid,
|
|
796
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
if (Platform.OS === 'ios') {
|
|
800
|
+
await new Promise<void>(resolve => {
|
|
801
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
802
|
+
});
|
|
803
|
+
} else if (Platform.OS === 'android') {
|
|
804
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return transport;
|
|
808
|
+
}
|
|
809
|
+
|
|
654
810
|
async acquire(input: BleAcquireInput) {
|
|
655
811
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
656
812
|
|
|
@@ -684,9 +840,8 @@ export default class ReactNativeBleTransport {
|
|
|
684
840
|
if (forceCleanRunPromise && this.runPromise) {
|
|
685
841
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
686
842
|
this.runPromise.reject(error);
|
|
687
|
-
this.rejectAllProtocolV2Frames(error);
|
|
688
843
|
this.runPromise = null;
|
|
689
|
-
this.
|
|
844
|
+
this.runPromiseDeviceId = null;
|
|
690
845
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
691
846
|
}
|
|
692
847
|
|
|
@@ -722,15 +877,22 @@ export default class ReactNativeBleTransport {
|
|
|
722
877
|
if (!device) {
|
|
723
878
|
Log?.debug('try to connect to device: ', uuid);
|
|
724
879
|
try {
|
|
725
|
-
device = await
|
|
880
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
881
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
882
|
+
);
|
|
726
883
|
} catch (e) {
|
|
727
884
|
Log?.debug('try to connect to device has error: ', e);
|
|
885
|
+
if (isConnectTimeoutError(e)) {
|
|
886
|
+
throw e;
|
|
887
|
+
}
|
|
728
888
|
if (
|
|
729
889
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
730
890
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
731
891
|
) {
|
|
732
892
|
Log?.debug('first try to reconnect without params');
|
|
733
|
-
device = await
|
|
893
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
894
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
895
|
+
);
|
|
734
896
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
735
897
|
Log?.debug('device already connected');
|
|
736
898
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -746,26 +908,36 @@ export default class ReactNativeBleTransport {
|
|
|
746
908
|
|
|
747
909
|
if (!(await device.isConnected())) {
|
|
748
910
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
911
|
+
const disconnectedDevice = device;
|
|
749
912
|
|
|
750
913
|
try {
|
|
751
|
-
device = await
|
|
914
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
915
|
+
disconnectedDevice.connect(connectOptions)
|
|
916
|
+
);
|
|
752
917
|
} catch (e) {
|
|
753
918
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
919
|
+
if (isConnectTimeoutError(e)) {
|
|
920
|
+
throw e;
|
|
921
|
+
}
|
|
754
922
|
if (
|
|
755
923
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
756
924
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
757
925
|
) {
|
|
758
926
|
Log?.debug('second try to reconnect without params');
|
|
759
927
|
try {
|
|
760
|
-
device = await
|
|
928
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
929
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
930
|
+
);
|
|
761
931
|
} catch (e) {
|
|
762
932
|
Log?.debug('last try to reconnect error: ', e);
|
|
763
933
|
// last try to reconnect device if this issue exists
|
|
764
934
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
765
935
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
766
936
|
Log?.debug('last try to reconnect');
|
|
767
|
-
await
|
|
768
|
-
device = await
|
|
937
|
+
await disconnectedDevice.cancelConnection();
|
|
938
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
939
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
940
|
+
);
|
|
769
941
|
}
|
|
770
942
|
}
|
|
771
943
|
} else {
|
|
@@ -775,12 +947,15 @@ export default class ReactNativeBleTransport {
|
|
|
775
947
|
}
|
|
776
948
|
|
|
777
949
|
device = await requestAndroidMtu(device);
|
|
778
|
-
const
|
|
950
|
+
const acquiredDevice = device;
|
|
951
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
952
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
779
953
|
|
|
780
954
|
const protocolHint = expectedProtocol
|
|
781
955
|
? undefined
|
|
782
|
-
:
|
|
783
|
-
|
|
956
|
+
: input.protocolHint ??
|
|
957
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
958
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
784
959
|
|
|
785
960
|
// release transport before new transport instance
|
|
786
961
|
await this.release(uuid, true);
|
|
@@ -788,45 +963,30 @@ export default class ReactNativeBleTransport {
|
|
|
788
963
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
789
964
|
}
|
|
790
965
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
const monitorToken = this.nextMonitorToken;
|
|
796
|
-
this.nextMonitorToken += 1;
|
|
797
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
798
|
-
transport.monitorToken = monitorToken;
|
|
799
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
800
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
801
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
802
|
-
transport.notifyCharacteristic,
|
|
803
|
-
uuid,
|
|
804
|
-
monitorToken,
|
|
805
|
-
notifyTransactionId
|
|
806
|
-
);
|
|
807
|
-
transportCache[uuid] = transport;
|
|
808
|
-
|
|
809
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
810
|
-
|
|
811
|
-
if (Platform.OS === 'ios') {
|
|
812
|
-
await new Promise<void>(resolve => {
|
|
813
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
814
|
-
});
|
|
815
|
-
} else if (Platform.OS === 'android') {
|
|
816
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
820
|
-
|
|
821
|
-
this.emitter?.emit('device-connect', {
|
|
822
|
-
name: device.name,
|
|
823
|
-
id: device.id,
|
|
824
|
-
connectId: device.id,
|
|
966
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
967
|
+
writeCharacteristic,
|
|
968
|
+
notifyCharacteristic,
|
|
825
969
|
});
|
|
826
970
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
971
|
+
try {
|
|
972
|
+
const protocolType = await this.detectProtocol(
|
|
973
|
+
uuid,
|
|
974
|
+
expectedProtocol,
|
|
975
|
+
protocolHint,
|
|
976
|
+
async () => {
|
|
977
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
const currentTransport = transportCache[uuid];
|
|
981
|
+
if (!currentTransport) {
|
|
982
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
983
|
+
}
|
|
984
|
+
this.attachDisconnectSubscription(currentTransport, acquiredDevice, uuid);
|
|
985
|
+
return { uuid, protocolType };
|
|
986
|
+
} catch (error) {
|
|
987
|
+
await this.release(uuid, true);
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
830
990
|
}
|
|
831
991
|
|
|
832
992
|
_monitorCharacteristic(
|
|
@@ -853,7 +1013,30 @@ export default class ReactNativeBleTransport {
|
|
|
853
1013
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
854
1014
|
return;
|
|
855
1015
|
}
|
|
856
|
-
if (this.
|
|
1016
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1017
|
+
let errorCode:
|
|
1018
|
+
| typeof HardwareErrorCode.BleDeviceBondError
|
|
1019
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
1020
|
+
| typeof HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
1021
|
+
| typeof HardwareErrorCode.BleTimeoutError =
|
|
1022
|
+
HardwareErrorCode.BleCharacteristicNotifyError;
|
|
1023
|
+
if (error.reason?.includes('The connection has timed out unexpectedly')) {
|
|
1024
|
+
errorCode = HardwareErrorCode.BleTimeoutError;
|
|
1025
|
+
} else if (error.reason?.includes('Encryption is insufficient')) {
|
|
1026
|
+
errorCode = HardwareErrorCode.BleDeviceBondError;
|
|
1027
|
+
} else if (
|
|
1028
|
+
error.reason?.includes('Cannot write client characteristic config descriptor') ||
|
|
1029
|
+
error.reason?.includes('Cannot find client characteristic config descriptor') ||
|
|
1030
|
+
error.reason?.includes('The handle is invalid') ||
|
|
1031
|
+
error.reason?.includes('Writing is not permitted') ||
|
|
1032
|
+
error.reason?.includes('notify change failed for device')
|
|
1033
|
+
) {
|
|
1034
|
+
errorCode = HardwareErrorCode.BleCharacteristicNotifyChangeFailure;
|
|
1035
|
+
}
|
|
1036
|
+
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
857
1040
|
let ERROR:
|
|
858
1041
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
859
1042
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -876,7 +1059,6 @@ export default class ReactNativeBleTransport {
|
|
|
876
1059
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
877
1060
|
);
|
|
878
1061
|
this.runPromise.reject(notifyError);
|
|
879
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
880
1062
|
Log?.debug(
|
|
881
1063
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
882
1064
|
);
|
|
@@ -884,7 +1066,6 @@ export default class ReactNativeBleTransport {
|
|
|
884
1066
|
}
|
|
885
1067
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
886
1068
|
this.runPromise.reject(notifyError);
|
|
887
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
888
1069
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
889
1070
|
}
|
|
890
1071
|
|
|
@@ -902,13 +1083,13 @@ export default class ReactNativeBleTransport {
|
|
|
902
1083
|
|
|
903
1084
|
try {
|
|
904
1085
|
const data = Buffer.from(c.value as string, 'base64');
|
|
905
|
-
const protocol = this.
|
|
1086
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
906
1087
|
if (!protocol) {
|
|
907
1088
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
908
1089
|
return;
|
|
909
1090
|
}
|
|
910
1091
|
if (protocol === 'V2') {
|
|
911
|
-
this.handleProtocolV2Notification(uuid, new Uint8Array(data));
|
|
1092
|
+
this.handleProtocolV2Notification(uuid, monitorToken, new Uint8Array(data));
|
|
912
1093
|
return;
|
|
913
1094
|
}
|
|
914
1095
|
// console.log('[hd-transport-react-native] Received a packet, ', 'buffer: ', data);
|
|
@@ -929,13 +1110,18 @@ export default class ReactNativeBleTransport {
|
|
|
929
1110
|
// );
|
|
930
1111
|
bufferLength = 0;
|
|
931
1112
|
buffer = [];
|
|
932
|
-
this.
|
|
1113
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1114
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1115
|
+
}
|
|
933
1116
|
}
|
|
934
1117
|
} catch (error) {
|
|
935
1118
|
Log?.debug('monitor data error: ', error);
|
|
936
1119
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
937
|
-
this.
|
|
938
|
-
|
|
1120
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1121
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1122
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
1123
|
+
this.runPromise?.reject(notifyError);
|
|
1124
|
+
}
|
|
939
1125
|
}
|
|
940
1126
|
}, notifyTransactionId);
|
|
941
1127
|
|
|
@@ -943,13 +1129,18 @@ export default class ReactNativeBleTransport {
|
|
|
943
1129
|
}
|
|
944
1130
|
|
|
945
1131
|
async release(uuid: string, onclose = false) {
|
|
1132
|
+
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
1133
|
+
return this.releaseNative(uuid, onclose);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
946
1137
|
const transport = transportCache[uuid];
|
|
947
|
-
if (this.runPromise) {
|
|
1138
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
948
1139
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
949
1140
|
this.runPromise.reject(error);
|
|
950
1141
|
this.runPromise = null;
|
|
951
|
-
this.
|
|
952
|
-
this.
|
|
1142
|
+
this.runPromiseDeviceId = null;
|
|
1143
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
953
1144
|
} else {
|
|
954
1145
|
this.resetProtocolV2Frames(uuid);
|
|
955
1146
|
}
|
|
@@ -957,9 +1148,6 @@ export default class ReactNativeBleTransport {
|
|
|
957
1148
|
if (Platform.OS === 'android' && !onclose && transport) {
|
|
958
1149
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
959
1150
|
this.resetProtocolV2Frames(uuid);
|
|
960
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
961
|
-
this.activeProtocolV2Call = null;
|
|
962
|
-
}
|
|
963
1151
|
return Promise.resolve(true);
|
|
964
1152
|
}
|
|
965
1153
|
|
|
@@ -993,7 +1181,8 @@ export default class ReactNativeBleTransport {
|
|
|
993
1181
|
}
|
|
994
1182
|
|
|
995
1183
|
this.deviceProtocol.delete(uuid);
|
|
996
|
-
this.
|
|
1184
|
+
this.probingProtocols.delete(uuid);
|
|
1185
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
997
1186
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
998
1187
|
this.protocolV2Assemblers.delete(uuid);
|
|
999
1188
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1025,13 +1214,6 @@ export default class ReactNativeBleTransport {
|
|
|
1025
1214
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1026
1215
|
}
|
|
1027
1216
|
|
|
1028
|
-
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1029
|
-
|
|
1030
|
-
Log?.debug('transport-react-native call this.runPromise', this.runPromise);
|
|
1031
|
-
if (this.runPromise && !forceRun) {
|
|
1032
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1217
|
const protocol = this.getProtocolType(uuid);
|
|
1036
1218
|
if (!protocol) {
|
|
1037
1219
|
throw ERRORS.TypedError(
|
|
@@ -1039,31 +1221,17 @@ export default class ReactNativeBleTransport {
|
|
|
1039
1221
|
`Device protocol has not been detected for ${uuid}`
|
|
1040
1222
|
);
|
|
1041
1223
|
}
|
|
1042
|
-
|
|
1043
|
-
if (name === 'ResourceUpdate' || name === 'ResourceAck') {
|
|
1044
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
|
|
1045
|
-
file_name: data?.file_name,
|
|
1046
|
-
hash: data?.hash,
|
|
1047
|
-
});
|
|
1048
|
-
} else if (LogBlockCommand.has(name)) {
|
|
1049
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
1050
|
-
} else {
|
|
1051
|
-
Log?.debug(
|
|
1052
|
-
'transport-react-native',
|
|
1053
|
-
'call-',
|
|
1054
|
-
' name: ',
|
|
1055
|
-
name,
|
|
1056
|
-
' data: ',
|
|
1057
|
-
data,
|
|
1058
|
-
' protocol: ',
|
|
1059
|
-
protocol
|
|
1060
|
-
);
|
|
1061
|
-
}
|
|
1224
|
+
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1062
1225
|
|
|
1063
1226
|
if (protocol === 'V2') {
|
|
1064
1227
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1065
1228
|
}
|
|
1066
1229
|
|
|
1230
|
+
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1231
|
+
if (this.runPromise && !forceRun) {
|
|
1232
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1067
1235
|
return this.callProtocolV1(uuid, name, data, options);
|
|
1068
1236
|
}
|
|
1069
1237
|
|
|
@@ -1080,7 +1248,25 @@ export default class ReactNativeBleTransport {
|
|
|
1080
1248
|
const transport = this.getCachedTransport(uuid);
|
|
1081
1249
|
const runPromise = createDeferred<string>();
|
|
1082
1250
|
runPromise.promise.catch(() => undefined);
|
|
1251
|
+
const supersededRunPromise = this.runPromise;
|
|
1252
|
+
if (supersededRunPromise) {
|
|
1253
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1254
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1255
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1256
|
+
// tear down the shared connection while another call is using it.
|
|
1257
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1258
|
+
}
|
|
1083
1259
|
this.runPromise = runPromise;
|
|
1260
|
+
this.runPromiseDeviceId = uuid;
|
|
1261
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1262
|
+
// only the call that still owns the slot may release it.
|
|
1263
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1264
|
+
if (this.runPromise === runPromise) {
|
|
1265
|
+
this.runPromise = null;
|
|
1266
|
+
this.runPromiseDeviceId = null;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1084
1270
|
const messages = this._messages;
|
|
1085
1271
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1086
1272
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1106,6 +1292,9 @@ export default class ReactNativeBleTransport {
|
|
|
1106
1292
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1107
1293
|
} catch (e) {
|
|
1108
1294
|
onError(e);
|
|
1295
|
+
if (isWedgedWriteError(e)) {
|
|
1296
|
+
throw e;
|
|
1297
|
+
}
|
|
1109
1298
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1110
1299
|
}
|
|
1111
1300
|
}
|
|
@@ -1137,6 +1326,9 @@ export default class ReactNativeBleTransport {
|
|
|
1137
1326
|
}
|
|
1138
1327
|
} catch (e) {
|
|
1139
1328
|
onError(e);
|
|
1329
|
+
if (isWedgedWriteError(e)) {
|
|
1330
|
+
throw e;
|
|
1331
|
+
}
|
|
1140
1332
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1141
1333
|
}
|
|
1142
1334
|
}
|
|
@@ -1150,21 +1342,26 @@ export default class ReactNativeBleTransport {
|
|
|
1150
1342
|
if (name === 'EmmcFileWrite') {
|
|
1151
1343
|
await writeChunkedData(
|
|
1152
1344
|
buffers,
|
|
1153
|
-
data =>
|
|
1345
|
+
data =>
|
|
1346
|
+
this.writeBlePacket(
|
|
1347
|
+
uuid,
|
|
1348
|
+
data,
|
|
1349
|
+
payload => transport.writeWithRetry(payload),
|
|
1350
|
+
isCurrentOwner
|
|
1351
|
+
),
|
|
1154
1352
|
e => {
|
|
1155
|
-
|
|
1353
|
+
releaseOwnershipIfCurrent();
|
|
1156
1354
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1157
1355
|
}
|
|
1158
1356
|
);
|
|
1159
1357
|
} else if (name === 'FirmwareUpload') {
|
|
1160
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1358
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
1161
1359
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
1162
1360
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1163
1361
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1164
1362
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1165
1363
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
1166
1364
|
});
|
|
1167
|
-
|
|
1168
1365
|
await writeFirmwareUploadChunkedData(
|
|
1169
1366
|
buffers,
|
|
1170
1367
|
async data => {
|
|
@@ -1174,43 +1371,31 @@ export default class ReactNativeBleTransport {
|
|
|
1174
1371
|
// eslint-disable-next-line no-constant-condition
|
|
1175
1372
|
while (true) {
|
|
1176
1373
|
try {
|
|
1177
|
-
await
|
|
1374
|
+
await this.writeBlePacket(
|
|
1375
|
+
uuid,
|
|
1376
|
+
data,
|
|
1377
|
+
payload => transport.writeWithRetry(payload),
|
|
1378
|
+
isCurrentOwner
|
|
1379
|
+
);
|
|
1178
1380
|
return;
|
|
1179
1381
|
} catch (error) {
|
|
1180
1382
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1181
1383
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1182
1384
|
throw error;
|
|
1183
1385
|
}
|
|
1184
|
-
const
|
|
1185
|
-
const delayMs = shouldReconnect
|
|
1186
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1187
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1386
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1188
1387
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1189
1388
|
attempt: attempt + 1,
|
|
1190
1389
|
delayMs,
|
|
1191
|
-
reconnect: shouldReconnect,
|
|
1192
1390
|
error,
|
|
1193
1391
|
});
|
|
1194
|
-
if (shouldReconnect) {
|
|
1195
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1196
|
-
}
|
|
1197
1392
|
await delay(delayMs);
|
|
1198
1393
|
attempt += 1;
|
|
1199
|
-
if (shouldReconnect) {
|
|
1200
|
-
try {
|
|
1201
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1202
|
-
} catch (e) {
|
|
1203
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1204
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1205
|
-
throw e;
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
1394
|
}
|
|
1210
1395
|
}
|
|
1211
1396
|
},
|
|
1212
1397
|
e => {
|
|
1213
|
-
|
|
1398
|
+
releaseOwnershipIfCurrent();
|
|
1214
1399
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1215
1400
|
}
|
|
1216
1401
|
);
|
|
@@ -1218,12 +1403,24 @@ export default class ReactNativeBleTransport {
|
|
|
1218
1403
|
for (const o of buffers) {
|
|
1219
1404
|
const outData = o.toString('base64');
|
|
1220
1405
|
// Upload resources on low-end phones may OOM
|
|
1221
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
1222
1406
|
try {
|
|
1223
|
-
|
|
1407
|
+
const shouldUseWriteWithResponse =
|
|
1408
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1409
|
+
await this.writeBlePacket(
|
|
1410
|
+
uuid,
|
|
1411
|
+
outData,
|
|
1412
|
+
payload =>
|
|
1413
|
+
shouldUseWriteWithResponse
|
|
1414
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1415
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1416
|
+
isCurrentOwner
|
|
1417
|
+
);
|
|
1224
1418
|
} catch (e) {
|
|
1225
1419
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1226
|
-
|
|
1420
|
+
releaseOwnershipIfCurrent();
|
|
1421
|
+
if (isWedgedWriteError(e)) {
|
|
1422
|
+
throw e;
|
|
1423
|
+
}
|
|
1227
1424
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1228
1425
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1229
1426
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1256,20 +1453,33 @@ export default class ReactNativeBleTransport {
|
|
|
1256
1453
|
throw new Error('Returning data is not string.');
|
|
1257
1454
|
}
|
|
1258
1455
|
|
|
1259
|
-
Log?.debug('receive data: ', response);
|
|
1260
1456
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1261
1457
|
return check.call(jsonData);
|
|
1262
1458
|
} catch (e) {
|
|
1263
|
-
if (name === '
|
|
1264
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1459
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1460
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1265
1461
|
} else {
|
|
1266
1462
|
Log?.error('call error: ', e);
|
|
1267
1463
|
}
|
|
1464
|
+
const isProbeTimeout =
|
|
1465
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1466
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1467
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1468
|
+
// call is actively using.
|
|
1469
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1470
|
+
if (
|
|
1471
|
+
!isProbeTimeout &&
|
|
1472
|
+
!isStaleCall &&
|
|
1473
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1474
|
+
) {
|
|
1475
|
+
await this.disconnect(uuid);
|
|
1476
|
+
}
|
|
1268
1477
|
throw e;
|
|
1269
1478
|
} finally {
|
|
1270
1479
|
if (timeout) clearTimeout(timeout);
|
|
1271
1480
|
if (this.runPromise === runPromise) {
|
|
1272
1481
|
this.runPromise = null;
|
|
1482
|
+
this.runPromiseDeviceId = null;
|
|
1273
1483
|
}
|
|
1274
1484
|
}
|
|
1275
1485
|
}
|
|
@@ -1279,8 +1489,9 @@ export default class ReactNativeBleTransport {
|
|
|
1279
1489
|
}
|
|
1280
1490
|
|
|
1281
1491
|
async disconnect(session: string) {
|
|
1282
|
-
|
|
1492
|
+
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1283
1493
|
const transport = transportCache[session];
|
|
1494
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1284
1495
|
|
|
1285
1496
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1286
1497
|
// from being triggered when we cancel the connection below
|
|
@@ -1338,23 +1549,22 @@ export default class ReactNativeBleTransport {
|
|
|
1338
1549
|
delete transportCache[session];
|
|
1339
1550
|
}
|
|
1340
1551
|
this.deviceProtocol.delete(session);
|
|
1552
|
+
this.probingProtocols.delete(session);
|
|
1341
1553
|
this.deviceProtocolHints.delete(session);
|
|
1554
|
+
this.sessionProtocols.delete(session);
|
|
1555
|
+
this.protocolReprobeFailures.delete(session);
|
|
1342
1556
|
this.protocolV2Assemblers.delete(session);
|
|
1343
1557
|
this.resetProtocolV2Frames(session);
|
|
1344
|
-
if (this.activeProtocolV2Call?.uuid === session) {
|
|
1345
|
-
this.activeProtocolV2Call = null;
|
|
1346
|
-
}
|
|
1347
1558
|
|
|
1348
1559
|
// emit the disconnect event
|
|
1349
1560
|
try {
|
|
1350
|
-
this.
|
|
1351
|
-
name: transport?.device?.name,
|
|
1352
|
-
id: session,
|
|
1353
|
-
connectId: session,
|
|
1354
|
-
});
|
|
1561
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1355
1562
|
} catch (e) {
|
|
1356
1563
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1357
1564
|
}
|
|
1565
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1566
|
+
this.monitorTokens.delete(session);
|
|
1567
|
+
}
|
|
1358
1568
|
// eslint-disable-next-line no-promise-executor-return
|
|
1359
1569
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1360
1570
|
}
|
|
@@ -1365,6 +1575,114 @@ export default class ReactNativeBleTransport {
|
|
|
1365
1575
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1366
1576
|
}
|
|
1367
1577
|
this.runPromise = null;
|
|
1578
|
+
this.runPromiseDeviceId = null;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1582
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1583
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1584
|
+
let timedOut = false;
|
|
1585
|
+
const pending = connect();
|
|
1586
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1587
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1588
|
+
pending.catch(() => undefined);
|
|
1589
|
+
try {
|
|
1590
|
+
const result = await Promise.race([
|
|
1591
|
+
pending,
|
|
1592
|
+
new Promise<never>((_, reject) => {
|
|
1593
|
+
timer = setTimeout(() => {
|
|
1594
|
+
timedOut = true;
|
|
1595
|
+
reject(
|
|
1596
|
+
ERRORS.TypedError(
|
|
1597
|
+
HardwareErrorCode.BleConnectedError,
|
|
1598
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1599
|
+
)
|
|
1600
|
+
);
|
|
1601
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1602
|
+
}),
|
|
1603
|
+
]);
|
|
1604
|
+
return result;
|
|
1605
|
+
} catch (error) {
|
|
1606
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1607
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1608
|
+
}
|
|
1609
|
+
throw error;
|
|
1610
|
+
} finally {
|
|
1611
|
+
if (timer) clearTimeout(timer);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1616
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1617
|
+
uuid: string,
|
|
1618
|
+
device: Device
|
|
1619
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1620
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1621
|
+
let timedOut = false;
|
|
1622
|
+
const pending = this.resolveCharacteristics(device);
|
|
1623
|
+
pending.catch(() => undefined);
|
|
1624
|
+
try {
|
|
1625
|
+
const result = await Promise.race([
|
|
1626
|
+
pending,
|
|
1627
|
+
new Promise<never>((_, reject) => {
|
|
1628
|
+
timer = setTimeout(() => {
|
|
1629
|
+
timedOut = true;
|
|
1630
|
+
reject(
|
|
1631
|
+
ERRORS.TypedError(
|
|
1632
|
+
HardwareErrorCode.BleConnectedError,
|
|
1633
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1634
|
+
)
|
|
1635
|
+
);
|
|
1636
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1637
|
+
}),
|
|
1638
|
+
]);
|
|
1639
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1640
|
+
return result;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1643
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1644
|
+
}
|
|
1645
|
+
throw error;
|
|
1646
|
+
} finally {
|
|
1647
|
+
if (timer) clearTimeout(timer);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1653
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1654
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1655
|
+
*/
|
|
1656
|
+
private abandonStalledConnection(
|
|
1657
|
+
uuid: string,
|
|
1658
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1659
|
+
) {
|
|
1660
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1661
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1662
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1663
|
+
stage,
|
|
1664
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1668
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1669
|
+
});
|
|
1670
|
+
const stalled = transportCache[uuid];
|
|
1671
|
+
if (stalled) {
|
|
1672
|
+
delete transportCache[uuid];
|
|
1673
|
+
}
|
|
1674
|
+
this.deviceProtocol.delete(uuid);
|
|
1675
|
+
this.probingProtocols.delete(uuid);
|
|
1676
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1677
|
+
this.resetProtocolV2Frames(uuid);
|
|
1678
|
+
|
|
1679
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1680
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1681
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1682
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1683
|
+
this.resetPlxManager();
|
|
1684
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1685
|
+
}
|
|
1368
1686
|
}
|
|
1369
1687
|
|
|
1370
1688
|
private getCachedTransport(uuid: string) {
|
|
@@ -1375,6 +1693,107 @@ export default class ReactNativeBleTransport {
|
|
|
1375
1693
|
return transport;
|
|
1376
1694
|
}
|
|
1377
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1698
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1699
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1700
|
+
* cached and every later call would hang on it again.
|
|
1701
|
+
*/
|
|
1702
|
+
private async writeBlePacket(
|
|
1703
|
+
uuid: string,
|
|
1704
|
+
data: string,
|
|
1705
|
+
write: (payload: string) => Promise<unknown>,
|
|
1706
|
+
isCurrentOwner?: () => boolean
|
|
1707
|
+
) {
|
|
1708
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1709
|
+
let timedOut = false;
|
|
1710
|
+
try {
|
|
1711
|
+
await Promise.race([
|
|
1712
|
+
write(data),
|
|
1713
|
+
new Promise<never>((_, reject) => {
|
|
1714
|
+
timer = setTimeout(() => {
|
|
1715
|
+
timedOut = true;
|
|
1716
|
+
reject(
|
|
1717
|
+
ERRORS.TypedError(
|
|
1718
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1719
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1720
|
+
)
|
|
1721
|
+
);
|
|
1722
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1723
|
+
}),
|
|
1724
|
+
]);
|
|
1725
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1726
|
+
} catch (error) {
|
|
1727
|
+
if (timedOut) {
|
|
1728
|
+
// A superseded call's late write must not tear down the link the current
|
|
1729
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1730
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1731
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1732
|
+
} else {
|
|
1733
|
+
this.tearDownWedgedLink(uuid);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
throw error;
|
|
1737
|
+
} finally {
|
|
1738
|
+
if (timer) clearTimeout(timer);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1744
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1745
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1746
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1747
|
+
*/
|
|
1748
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1749
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1750
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1751
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1752
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
const wedged = transportCache[uuid];
|
|
1756
|
+
this.disconnect(uuid).catch(error => {
|
|
1757
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1758
|
+
});
|
|
1759
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1760
|
+
delete transportCache[uuid];
|
|
1761
|
+
}
|
|
1762
|
+
this.deviceProtocol.delete(uuid);
|
|
1763
|
+
this.probingProtocols.delete(uuid);
|
|
1764
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1765
|
+
this.resetProtocolV2Frames(uuid);
|
|
1766
|
+
|
|
1767
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1768
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1769
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1770
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1771
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1772
|
+
this.resetPlxManager();
|
|
1773
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
private resetPlxManager() {
|
|
1778
|
+
const manager = this.blePlxManager;
|
|
1779
|
+
this.blePlxManager = undefined;
|
|
1780
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1781
|
+
Object.keys(transportCache).forEach(key => {
|
|
1782
|
+
delete transportCache[key];
|
|
1783
|
+
});
|
|
1784
|
+
this.deviceProtocol.clear();
|
|
1785
|
+
this.probingProtocols.clear();
|
|
1786
|
+
this.sessionProtocols.clear();
|
|
1787
|
+
this.protocolReprobeFailures.clear();
|
|
1788
|
+
this.monitorTokens.clear();
|
|
1789
|
+
this.protocolV2Assemblers.clear();
|
|
1790
|
+
try {
|
|
1791
|
+
manager?.destroy();
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1378
1797
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1379
1798
|
return ERRORS.TypedError(
|
|
1380
1799
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1385,70 +1804,131 @@ export default class ReactNativeBleTransport {
|
|
|
1385
1804
|
private createProtocolDetectionError() {
|
|
1386
1805
|
return ERRORS.TypedError(
|
|
1387
1806
|
HardwareErrorCode.BleTimeoutError,
|
|
1388
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1807
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1389
1808
|
);
|
|
1390
1809
|
}
|
|
1391
1810
|
|
|
1392
1811
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1812
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1813
|
+
this.probingProtocols.delete(uuid);
|
|
1814
|
+
}
|
|
1393
1815
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1394
1816
|
this.deviceProtocol.delete(uuid);
|
|
1395
1817
|
}
|
|
1396
1818
|
}
|
|
1397
1819
|
|
|
1820
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1821
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1822
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1398
1825
|
private async detectProtocol(
|
|
1399
1826
|
uuid: string,
|
|
1400
1827
|
expectedProtocol?: ProtocolType,
|
|
1401
|
-
protocolHint?: ProtocolType
|
|
1828
|
+
protocolHint?: ProtocolType,
|
|
1829
|
+
rebuildTransport?: () => Promise<void>
|
|
1402
1830
|
): Promise<ProtocolType> {
|
|
1831
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1832
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1833
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1834
|
+
deviceId: uuid,
|
|
1835
|
+
protocol: expectedProtocol,
|
|
1836
|
+
source: 'expected',
|
|
1837
|
+
});
|
|
1838
|
+
return expectedProtocol;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1403
1841
|
if (expectedProtocol === 'V1') {
|
|
1404
1842
|
if (await this.probeProtocolV1(uuid)) {
|
|
1405
1843
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1406
|
-
|
|
1844
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1845
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1846
|
+
deviceId: uuid,
|
|
1847
|
+
protocol: 'V1',
|
|
1848
|
+
source: 'expected',
|
|
1849
|
+
});
|
|
1407
1850
|
return 'V1';
|
|
1408
1851
|
}
|
|
1409
1852
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1410
1853
|
}
|
|
1411
1854
|
|
|
1412
1855
|
if (expectedProtocol === 'V2') {
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1856
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1857
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1858
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1859
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1860
|
+
deviceId: uuid,
|
|
1861
|
+
protocol: 'V2',
|
|
1862
|
+
source: 'expected',
|
|
1863
|
+
});
|
|
1864
|
+
return 'V2';
|
|
1865
|
+
}
|
|
1866
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1418
1867
|
}
|
|
1419
1868
|
|
|
1420
|
-
//
|
|
1421
|
-
//
|
|
1422
|
-
|
|
1423
|
-
const
|
|
1869
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1870
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1871
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1872
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1873
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1424
1874
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1875
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1876
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1877
|
+
// only adds its timeout to every poll.
|
|
1878
|
+
const trustSessionProtocol =
|
|
1879
|
+
sessionProtocol !== undefined &&
|
|
1880
|
+
!protocolHint &&
|
|
1881
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1882
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1425
1883
|
|
|
1426
1884
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1427
1885
|
const protocol = probeOrder[i];
|
|
1428
1886
|
if (i > 0) {
|
|
1429
|
-
//
|
|
1887
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1430
1888
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1889
|
+
if (!transportCache[uuid]) {
|
|
1890
|
+
if (!rebuildTransport) {
|
|
1891
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1892
|
+
}
|
|
1893
|
+
await rebuildTransport();
|
|
1894
|
+
}
|
|
1431
1895
|
}
|
|
1432
1896
|
const detected =
|
|
1433
1897
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1434
1898
|
if (detected) {
|
|
1435
1899
|
this.deviceProtocol.set(uuid, protocol);
|
|
1436
|
-
|
|
1900
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1901
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1902
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1903
|
+
deviceId: uuid,
|
|
1904
|
+
protocol,
|
|
1905
|
+
source: 'probe',
|
|
1906
|
+
});
|
|
1437
1907
|
return protocol;
|
|
1438
1908
|
}
|
|
1439
1909
|
}
|
|
1440
1910
|
|
|
1911
|
+
if (trustSessionProtocol) {
|
|
1912
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1913
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1914
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1915
|
+
} else {
|
|
1916
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1441
1919
|
this.deviceProtocol.delete(uuid);
|
|
1920
|
+
this.probingProtocols.delete(uuid);
|
|
1442
1921
|
throw this.createProtocolDetectionError();
|
|
1443
1922
|
}
|
|
1444
1923
|
|
|
1445
1924
|
private async resetProbeStateAfterProtocolProbe(uuid: string, protocol: ProtocolType) {
|
|
1446
1925
|
const transport = transportCache[uuid];
|
|
1926
|
+
await this.protocolV2Links.invalidateLink(
|
|
1927
|
+
uuid,
|
|
1928
|
+
`Reset notify state after Protocol ${protocol} probe`
|
|
1929
|
+
);
|
|
1447
1930
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1448
1931
|
this.resetProtocolV2Frames(uuid);
|
|
1449
|
-
if (this.activeProtocolV2Call?.uuid === uuid) {
|
|
1450
|
-
this.activeProtocolV2Call = null;
|
|
1451
|
-
}
|
|
1452
1932
|
if (this.runPromise) {
|
|
1453
1933
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1454
1934
|
this.runPromise.reject(error);
|
|
@@ -1499,12 +1979,20 @@ export default class ReactNativeBleTransport {
|
|
|
1499
1979
|
}
|
|
1500
1980
|
|
|
1501
1981
|
try {
|
|
1502
|
-
this.
|
|
1503
|
-
|
|
1982
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1983
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1984
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
1985
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1986
|
+
this.probingProtocols.delete(uuid);
|
|
1504
1987
|
return true;
|
|
1505
1988
|
} catch (error) {
|
|
1506
1989
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1507
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1990
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1991
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
1992
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
1993
|
+
if (isWedgedWriteError(error)) {
|
|
1994
|
+
throw error;
|
|
1995
|
+
}
|
|
1508
1996
|
return false;
|
|
1509
1997
|
}
|
|
1510
1998
|
}
|
|
@@ -1514,7 +2002,7 @@ export default class ReactNativeBleTransport {
|
|
|
1514
2002
|
return false;
|
|
1515
2003
|
}
|
|
1516
2004
|
|
|
1517
|
-
this.
|
|
2005
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1518
2006
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1519
2007
|
const detected = await probeProtocolV2Helper({
|
|
1520
2008
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1529,17 +2017,15 @@ export default class ReactNativeBleTransport {
|
|
|
1529
2017
|
});
|
|
1530
2018
|
if (!detected) {
|
|
1531
2019
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2020
|
+
} else {
|
|
2021
|
+
this.probingProtocols.delete(uuid);
|
|
1532
2022
|
}
|
|
1533
2023
|
return detected;
|
|
1534
2024
|
}
|
|
1535
2025
|
|
|
1536
|
-
private handleProtocolV2Notification(uuid: string, data: Uint8Array) {
|
|
2026
|
+
private handleProtocolV2Notification(uuid: string, monitorToken: number, data: Uint8Array) {
|
|
1537
2027
|
try {
|
|
1538
|
-
if (
|
|
1539
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1540
|
-
this.resetProtocolV2Frames(uuid);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
2028
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) return;
|
|
1543
2029
|
|
|
1544
2030
|
if (data.length === 0) return;
|
|
1545
2031
|
|
|
@@ -1552,8 +2038,15 @@ export default class ReactNativeBleTransport {
|
|
|
1552
2038
|
} catch (error) {
|
|
1553
2039
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 notification error:', error);
|
|
1554
2040
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1555
|
-
this.
|
|
1556
|
-
this.
|
|
2041
|
+
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
2042
|
+
this.protocolV2Links
|
|
2043
|
+
.invalidateLink(uuid, `Protocol V2 notification error: ${error}`)
|
|
2044
|
+
.catch(invalidateError =>
|
|
2045
|
+
Log?.debug(
|
|
2046
|
+
'[ReactNativeBleTransport] Protocol V2 notify cleanup failed:',
|
|
2047
|
+
invalidateError
|
|
2048
|
+
)
|
|
2049
|
+
);
|
|
1557
2050
|
}
|
|
1558
2051
|
}
|
|
1559
2052
|
|
|
@@ -1576,21 +2069,17 @@ export default class ReactNativeBleTransport {
|
|
|
1576
2069
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1577
2070
|
}
|
|
1578
2071
|
|
|
1579
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1580
|
-
this.protocolV2FrameQueues.clear();
|
|
1581
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1582
|
-
framePromise.reject(error);
|
|
1583
|
-
}
|
|
1584
|
-
this.protocolV2FramePromises.clear();
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
2072
|
private resetProtocolV2Frames(uuid: string) {
|
|
1588
|
-
this.
|
|
1589
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2073
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1590
2074
|
}
|
|
1591
2075
|
|
|
1592
|
-
private
|
|
1593
|
-
|
|
2076
|
+
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
2077
|
+
this.protocolV2FrameQueues.delete(uuid);
|
|
2078
|
+
const framePromise = this.protocolV2FramePromises.get(uuid);
|
|
2079
|
+
if (framePromise) {
|
|
2080
|
+
this.protocolV2FramePromises.delete(uuid);
|
|
2081
|
+
framePromise.reject(error);
|
|
2082
|
+
}
|
|
1594
2083
|
}
|
|
1595
2084
|
|
|
1596
2085
|
private async readProtocolV2Frame(uuid: string) {
|
|
@@ -1610,10 +2099,68 @@ export default class ReactNativeBleTransport {
|
|
|
1610
2099
|
}
|
|
1611
2100
|
}
|
|
1612
2101
|
|
|
2102
|
+
private async writeProtocolV2Packet(
|
|
2103
|
+
uuid: string,
|
|
2104
|
+
transport: BleTransport,
|
|
2105
|
+
base64: string,
|
|
2106
|
+
context: ProtocolV2CallContext,
|
|
2107
|
+
assertCurrentGeneration: () => void
|
|
2108
|
+
) {
|
|
2109
|
+
const shouldUseWriteWithResponse =
|
|
2110
|
+
transport.writeCharacteristic.isWritableWithResponse &&
|
|
2111
|
+
(context.writeWithResponse === true || (Platform.OS === 'ios' && !context.highVolume));
|
|
2112
|
+
let attempt = 0;
|
|
2113
|
+
for (;;) {
|
|
2114
|
+
assertCurrentGeneration();
|
|
2115
|
+
if (context.signal.aborted) {
|
|
2116
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2117
|
+
}
|
|
2118
|
+
try {
|
|
2119
|
+
await this.writeBlePacket(
|
|
2120
|
+
uuid,
|
|
2121
|
+
base64,
|
|
2122
|
+
payload =>
|
|
2123
|
+
shouldUseWriteWithResponse
|
|
2124
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2125
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2126
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2127
|
+
// tear down the link that the current generation is using.
|
|
2128
|
+
() => {
|
|
2129
|
+
try {
|
|
2130
|
+
assertCurrentGeneration();
|
|
2131
|
+
return !context.signal.aborted;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
);
|
|
2137
|
+
assertCurrentGeneration();
|
|
2138
|
+
return;
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
if (
|
|
2141
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2142
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2143
|
+
) {
|
|
2144
|
+
throw error;
|
|
2145
|
+
}
|
|
2146
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2147
|
+
attempt += 1;
|
|
2148
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2149
|
+
name: context.messageName,
|
|
2150
|
+
attempt,
|
|
2151
|
+
delayMs,
|
|
2152
|
+
});
|
|
2153
|
+
await delay(delayMs);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
|
|
1613
2158
|
private async writeProtocolV2Frame(
|
|
2159
|
+
uuid: string,
|
|
1614
2160
|
transport: BleTransport,
|
|
1615
2161
|
frame: Uint8Array,
|
|
1616
|
-
|
|
2162
|
+
context: ProtocolV2CallContext,
|
|
2163
|
+
assertCurrentGeneration: () => void
|
|
1617
2164
|
) {
|
|
1618
2165
|
const tuning = getProtocolV2BleTuning();
|
|
1619
2166
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
@@ -1622,37 +2169,32 @@ export default class ReactNativeBleTransport {
|
|
|
1622
2169
|
androidPacketLength: tuning.androidPacketLength,
|
|
1623
2170
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1624
2171
|
});
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (shouldThrottle) {
|
|
1654
|
-
await delay(tuning.highVolumeWriteFlushDelayMs);
|
|
1655
|
-
}
|
|
2172
|
+
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
2173
|
+
// before the next single-packet control command is written.
|
|
2174
|
+
const initialDelayMs =
|
|
2175
|
+
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
2176
|
+
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
2177
|
+
: 0;
|
|
2178
|
+
await writeProtocolV2BleFrame({
|
|
2179
|
+
frame,
|
|
2180
|
+
packetCapacity,
|
|
2181
|
+
assertActive: assertCurrentGeneration,
|
|
2182
|
+
signal: context.signal,
|
|
2183
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2184
|
+
initialDelayMs,
|
|
2185
|
+
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
2186
|
+
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
2187
|
+
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
2188
|
+
wait: delay,
|
|
2189
|
+
writePacket: packet =>
|
|
2190
|
+
this.writeProtocolV2Packet(
|
|
2191
|
+
uuid,
|
|
2192
|
+
transport,
|
|
2193
|
+
Buffer.from(packet).toString('base64'),
|
|
2194
|
+
context,
|
|
2195
|
+
assertCurrentGeneration
|
|
2196
|
+
),
|
|
2197
|
+
});
|
|
1656
2198
|
}
|
|
1657
2199
|
|
|
1658
2200
|
private async callProtocolV2(
|
|
@@ -1665,102 +2207,83 @@ export default class ReactNativeBleTransport {
|
|
|
1665
2207
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1666
2208
|
}
|
|
1667
2209
|
|
|
1668
|
-
const
|
|
1669
|
-
if (this.runPromise) {
|
|
1670
|
-
if (!forceRun) {
|
|
1671
|
-
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1672
|
-
}
|
|
1673
|
-
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
1674
|
-
this.runPromise.reject(error);
|
|
1675
|
-
this.rejectAllProtocolV2Frames(error);
|
|
1676
|
-
this.runPromise = null;
|
|
1677
|
-
this.activeProtocolV2Call = null;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const transport = this.getCachedTransport(uuid);
|
|
1681
|
-
const runPromise = createDeferred<Uint8Array>();
|
|
1682
|
-
runPromise.promise.catch(() => undefined);
|
|
1683
|
-
this.runPromise = runPromise;
|
|
1684
|
-
const callToken = this.nextProtocolV2CallToken++;
|
|
1685
|
-
this.activeProtocolV2Call = { uuid, token: callToken };
|
|
1686
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1687
|
-
this.resetProtocolV2Frames(uuid);
|
|
1688
|
-
let completed = false;
|
|
1689
|
-
const callOptions = {
|
|
1690
|
-
...options,
|
|
1691
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1692
|
-
};
|
|
2210
|
+
const callOptions = options;
|
|
1693
2211
|
const highVolumeWrite = LogBlockCommand.has(name);
|
|
1694
2212
|
|
|
1695
2213
|
if (highVolumeWrite) {
|
|
1696
2214
|
const tuning = getProtocolV2BleTuning();
|
|
1697
|
-
Log?.debug(
|
|
1698
|
-
'[ReactNativeBleTransport] Protocol V2 high-volume write uses throttled writeWithoutResponse:',
|
|
2215
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
1699
2216
|
name,
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
burstSize: tuning.highVolumeWriteBurstSize,
|
|
1704
|
-
pauseMs: tuning.highVolumeWritePauseMs,
|
|
1705
|
-
flushDelayMs: tuning.highVolumeWriteFlushDelayMs,
|
|
1706
|
-
writeWithResponse: tuning.highVolumeWriteWithResponse,
|
|
1707
|
-
}
|
|
1708
|
-
);
|
|
2217
|
+
writeMode: options?.writeWithResponse ? 'withResponse' : 'withoutResponse',
|
|
2218
|
+
packetCapacity: Platform.OS === 'ios' ? tuning.iosPacketLength : tuning.androidPacketLength,
|
|
2219
|
+
});
|
|
1709
2220
|
}
|
|
1710
2221
|
|
|
1711
2222
|
try {
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
await this.writeProtocolV2Frame(transport, frame, {
|
|
1720
|
-
highVolume: highVolumeWrite,
|
|
1721
|
-
});
|
|
1722
|
-
},
|
|
1723
|
-
readFrame: async () => {
|
|
1724
|
-
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
1725
|
-
if (!(rxFrame instanceof Uint8Array)) {
|
|
1726
|
-
throw new Error('Protocol V2 response is not Uint8Array');
|
|
1727
|
-
}
|
|
1728
|
-
return rxFrame;
|
|
1729
|
-
},
|
|
1730
|
-
logger: Log,
|
|
1731
|
-
logPrefix: 'ProtocolV2 RN-BLE',
|
|
1732
|
-
createTimeoutError: (_messageName: string, timeout: number) =>
|
|
1733
|
-
ERRORS.TypedError(
|
|
1734
|
-
HardwareErrorCode.BleTimeoutError,
|
|
1735
|
-
`BLE response timeout after ${timeout}ms for ${name}`
|
|
1736
|
-
),
|
|
1737
|
-
});
|
|
1738
|
-
|
|
1739
|
-
const result = await session.call(name, data, callOptions);
|
|
1740
|
-
completed = true;
|
|
1741
|
-
return result;
|
|
2223
|
+
return await this.protocolV2Links.call(
|
|
2224
|
+
uuid,
|
|
2225
|
+
() => this.createProtocolV2Adapter(uuid),
|
|
2226
|
+
name,
|
|
2227
|
+
data,
|
|
2228
|
+
callOptions
|
|
2229
|
+
);
|
|
1742
2230
|
} catch (e) {
|
|
1743
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
1744
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1745
|
-
this.resetProtocolV2Frames(uuid);
|
|
1746
|
-
}
|
|
1747
2231
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1748
2232
|
throw e;
|
|
1749
|
-
} finally {
|
|
1750
|
-
if (this.isActiveProtocolV2Call(uuid, callToken)) {
|
|
1751
|
-
if (!completed) {
|
|
1752
|
-
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1753
|
-
}
|
|
1754
|
-
this.resetProtocolV2Frames(uuid);
|
|
1755
|
-
this.activeProtocolV2Call = null;
|
|
1756
|
-
}
|
|
1757
|
-
if (this.runPromise === runPromise) {
|
|
1758
|
-
this.runPromise = null;
|
|
1759
|
-
}
|
|
1760
2233
|
}
|
|
1761
2234
|
}
|
|
1762
2235
|
|
|
2236
|
+
private createProtocolV2Adapter(uuid: string) {
|
|
2237
|
+
const generation = this.monitorTokens.get(uuid) ?? 0;
|
|
2238
|
+
const assertCurrentGeneration = () => {
|
|
2239
|
+
if (this.monitorTokens.get(uuid) !== generation) {
|
|
2240
|
+
throw new Error(`Protocol V2 monitor generation changed for ${uuid}`);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
2244
|
+
return {
|
|
2245
|
+
router: PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
2246
|
+
maxFrameBytes: PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
2247
|
+
generation,
|
|
2248
|
+
prepareCall: () => {
|
|
2249
|
+
assertCurrentGeneration();
|
|
2250
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2251
|
+
this.resetProtocolV2Frames(uuid);
|
|
2252
|
+
},
|
|
2253
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
2254
|
+
assertCurrentGeneration();
|
|
2255
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2256
|
+
await this.writeProtocolV2Frame(
|
|
2257
|
+
uuid,
|
|
2258
|
+
currentTransport,
|
|
2259
|
+
frame,
|
|
2260
|
+
context,
|
|
2261
|
+
assertCurrentGeneration
|
|
2262
|
+
);
|
|
2263
|
+
},
|
|
2264
|
+
readFrame: async () => {
|
|
2265
|
+
assertCurrentGeneration();
|
|
2266
|
+
const rxFrame = await this.readProtocolV2Frame(uuid);
|
|
2267
|
+
if (!(rxFrame instanceof Uint8Array)) {
|
|
2268
|
+
throw new Error('Protocol V2 response is not Uint8Array');
|
|
2269
|
+
}
|
|
2270
|
+
return rxFrame;
|
|
2271
|
+
},
|
|
2272
|
+
reset: (reason: string) => {
|
|
2273
|
+
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
2274
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2275
|
+
},
|
|
2276
|
+
logger: Log,
|
|
2277
|
+
logPrefix: 'ProtocolV2 RN-BLE',
|
|
2278
|
+
createTimeoutError: (messageName: string, timeout: number) =>
|
|
2279
|
+
ERRORS.TypedError(
|
|
2280
|
+
HardwareErrorCode.BleTimeoutError,
|
|
2281
|
+
`BLE response timeout after ${timeout}ms for ${messageName}`
|
|
2282
|
+
),
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1763
2286
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1764
|
-
return this.
|
|
2287
|
+
return this.getActiveProtocol(path);
|
|
1765
2288
|
}
|
|
1766
2289
|
}
|