@onekeyfe/hd-transport-react-native 1.2.0-alpha.14 → 1.2.0-alpha.140
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 +2 -4
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +7 -0
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +123 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +869 -292
- package/dist/transportLog.d.ts +1 -12
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +6 -5
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStrategy.test.ts +93 -5
- package/src/__tests__/connectTimeout.test.ts +292 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +150 -0
- package/src/__tests__/protocolReprobe.test.ts +156 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +829 -32
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +26 -11
- package/src/constants.ts +9 -14
- package/src/index.ts +1137 -268
- package/src/transportLog.ts +1 -18
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -5,30 +5,45 @@ import {
|
|
|
5
5
|
BleError,
|
|
6
6
|
BleErrorCode,
|
|
7
7
|
BleManager as BlePlxManager,
|
|
8
|
+
ConnectionPriority,
|
|
8
9
|
ScanMode,
|
|
9
10
|
} from 'react-native-ble-plx';
|
|
10
11
|
import ByteBuffer from 'bytebuffer';
|
|
11
12
|
import transport, {
|
|
12
|
-
LogBlockCommand,
|
|
13
13
|
type OneKeyDeviceInfoBase,
|
|
14
14
|
PROTOCOL_V1_MESSAGE_HEADER_SIZE,
|
|
15
15
|
PROTOCOL_V2_BLE_FRAME_MAX_BYTES,
|
|
16
16
|
PROTOCOL_V2_CHANNEL_BLE_UART,
|
|
17
17
|
type ProtocolType,
|
|
18
|
+
type ProtocolV2CallContext,
|
|
18
19
|
ProtocolV2FrameAssembler,
|
|
19
20
|
ProtocolV2LinkManager,
|
|
21
|
+
TRANSPORT_EVENT,
|
|
20
22
|
type TransportCallOptions,
|
|
23
|
+
isProtocolV2HighThroughputCall,
|
|
21
24
|
probeProtocolV2 as probeProtocolV2Helper,
|
|
25
|
+
writeProtocolV2BleFrame,
|
|
22
26
|
} from '@onekeyfe/hd-transport';
|
|
23
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ERRORS,
|
|
29
|
+
HardwareErrorCode,
|
|
30
|
+
createDeferred,
|
|
31
|
+
isOnekeyBluetoothDevice,
|
|
32
|
+
} from '@onekeyfe/hd-shared';
|
|
24
33
|
|
|
25
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
26
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
hasWritableCapability,
|
|
37
|
+
resolveProtocolV2PacketCapacity,
|
|
38
|
+
shouldRefreshNegotiatedMtu,
|
|
39
|
+
shouldWriteProtocolV2WithResponse,
|
|
40
|
+
} from './bleStrategy';
|
|
27
41
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
28
42
|
import {
|
|
29
43
|
ANDROID_PACKET_LENGTH,
|
|
44
|
+
ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
30
45
|
IOS_PACKET_LENGTH,
|
|
31
|
-
|
|
46
|
+
IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
32
47
|
getBluetoothServiceUuids,
|
|
33
48
|
getInfosForServiceUuid,
|
|
34
49
|
isSameBleUuid,
|
|
@@ -37,7 +52,6 @@ import { isHeaderChunk } from './utils/validateNotify';
|
|
|
37
52
|
import BleTransport from './BleTransport';
|
|
38
53
|
import timer from './utils/timer';
|
|
39
54
|
import { bleLogger, setBleLogger } from './logger';
|
|
40
|
-
import { createTransportCallLog } from './transportLog';
|
|
41
55
|
|
|
42
56
|
import type { Deferred } from '@onekeyfe/hd-shared';
|
|
43
57
|
import type { Characteristic, Device, Subscription } from 'react-native-ble-plx';
|
|
@@ -53,24 +67,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
53
67
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
54
68
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
55
69
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
56
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
57
70
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
58
71
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
59
72
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
60
73
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
61
74
|
|
|
62
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
75
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
63
76
|
type ResolvedBleCharacteristics = {
|
|
64
77
|
writeCharacteristic: Characteristic;
|
|
65
78
|
notifyCharacteristic: Characteristic;
|
|
66
79
|
};
|
|
67
80
|
|
|
81
|
+
const isAsciiWhitespace = (code: number) =>
|
|
82
|
+
code === 0x09 ||
|
|
83
|
+
code === 0x0a ||
|
|
84
|
+
code === 0x0b ||
|
|
85
|
+
code === 0x0c ||
|
|
86
|
+
code === 0x0d ||
|
|
87
|
+
code === 0x20;
|
|
88
|
+
|
|
89
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
90
|
+
let searchFrom = 0;
|
|
91
|
+
while (searchFrom < text.length) {
|
|
92
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
93
|
+
if (statusIndex < 0) return false;
|
|
94
|
+
|
|
95
|
+
let cursor = statusIndex + 'status'.length;
|
|
96
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
97
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
98
|
+
cursor += 1;
|
|
99
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
100
|
+
}
|
|
101
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
102
|
+
|
|
103
|
+
searchFrom = statusIndex + 'status'.length;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
|
|
68
108
|
const delay = (ms: number) =>
|
|
69
109
|
new Promise<void>(resolve => {
|
|
70
110
|
setTimeout(resolve, ms);
|
|
71
111
|
});
|
|
72
112
|
|
|
73
|
-
const getFirmwareUploadWriteRetryType = (
|
|
113
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
114
|
+
error: unknown
|
|
115
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
74
116
|
if (!error || typeof error !== 'object') return null;
|
|
75
117
|
const bleWriteError = error as {
|
|
76
118
|
androidErrorCode?: unknown;
|
|
@@ -81,13 +123,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
81
123
|
name?: unknown;
|
|
82
124
|
};
|
|
83
125
|
|
|
84
|
-
if (
|
|
85
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
86
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
87
|
-
) {
|
|
88
|
-
return 'reconnectable';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
126
|
if (
|
|
92
127
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
93
128
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -98,15 +133,31 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
98
133
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
99
134
|
.filter(value => typeof value === 'string')
|
|
100
135
|
.join(' ');
|
|
101
|
-
return
|
|
136
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
102
137
|
};
|
|
103
138
|
|
|
104
139
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
105
140
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
106
|
-
const
|
|
107
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
141
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
108
142
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
145
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
146
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
147
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
148
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
149
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
150
|
+
*/
|
|
151
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
152
|
+
export const BLE_NATIVE_TEARDOWN_TIMEOUT_MS = 3_000;
|
|
153
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
154
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
155
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
156
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
157
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
158
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
159
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
160
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
110
161
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
111
162
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
112
163
|
export type ProtocolV2BleTuning = {
|
|
@@ -117,8 +168,8 @@ export type ProtocolV2BleTuning = {
|
|
|
117
168
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
118
169
|
|
|
119
170
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
120
|
-
iosPacketLength:
|
|
121
|
-
androidPacketLength:
|
|
171
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
172
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
122
173
|
};
|
|
123
174
|
|
|
124
175
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -160,24 +211,60 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
160
211
|
return device?.name || device?.localName || null;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
214
|
+
const IOS_REQUEST_MTU = 247;
|
|
215
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
216
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
217
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
166
218
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
170
|
-
);
|
|
171
|
-
}
|
|
219
|
+
const getRequestedBleMtu = () =>
|
|
220
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
172
221
|
|
|
173
|
-
const
|
|
222
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
174
223
|
|
|
175
224
|
const connectOptions: Record<string, unknown> = {
|
|
176
|
-
requestMTU:
|
|
177
|
-
timeout:
|
|
225
|
+
requestMTU: getRequestedBleMtu(),
|
|
226
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
178
227
|
refreshGatt: 'OnConnected',
|
|
179
228
|
};
|
|
180
229
|
|
|
230
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
231
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
232
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
237
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
238
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
239
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
240
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
241
|
+
*/
|
|
242
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
243
|
+
/**
|
|
244
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
245
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
246
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
247
|
+
* budget.
|
|
248
|
+
*/
|
|
249
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
250
|
+
/**
|
|
251
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
252
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
253
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
254
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
255
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
256
|
+
*/
|
|
257
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
258
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
259
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
260
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
261
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
262
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
263
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
264
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
265
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
266
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
267
|
+
|
|
181
268
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
182
269
|
|
|
183
270
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -189,23 +276,32 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
189
276
|
return infos;
|
|
190
277
|
};
|
|
191
278
|
|
|
192
|
-
const
|
|
193
|
-
|
|
279
|
+
const requestNegotiatedMtu = async (
|
|
280
|
+
device: Device,
|
|
281
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
282
|
+
attempt: number
|
|
283
|
+
) => {
|
|
284
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
194
285
|
|
|
195
286
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
requested: ANDROID_REQUEST_MTU,
|
|
200
|
-
actual: mtuDevice.mtu,
|
|
201
|
-
});
|
|
287
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
288
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
289
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
202
290
|
return mtuDevice;
|
|
203
291
|
} catch (error) {
|
|
204
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
292
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
293
|
+
platform: Platform.OS,
|
|
294
|
+
stage,
|
|
295
|
+
attempt,
|
|
296
|
+
actual: device.mtu,
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
});
|
|
205
299
|
return device;
|
|
206
300
|
}
|
|
207
301
|
};
|
|
208
302
|
|
|
303
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
304
|
+
|
|
209
305
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
210
306
|
|
|
211
307
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -245,6 +341,8 @@ export default class ReactNativeBleTransport {
|
|
|
245
341
|
|
|
246
342
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
247
343
|
|
|
344
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
345
|
+
|
|
248
346
|
name = 'ReactNativeBleTransport';
|
|
249
347
|
|
|
250
348
|
configured = false;
|
|
@@ -255,6 +353,8 @@ export default class ReactNativeBleTransport {
|
|
|
255
353
|
|
|
256
354
|
runPromise: Deferred<any> | null = null;
|
|
257
355
|
|
|
356
|
+
private runPromiseDeviceId: string | null = null;
|
|
357
|
+
|
|
258
358
|
emitter?: EventEmitter;
|
|
259
359
|
|
|
260
360
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -262,8 +362,28 @@ export default class ReactNativeBleTransport {
|
|
|
262
362
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
263
363
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
264
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
367
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
368
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
369
|
+
* transport that was never validated.
|
|
370
|
+
*/
|
|
371
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
372
|
+
|
|
373
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
374
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
375
|
+
|
|
376
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
377
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
378
|
+
|
|
265
379
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
266
380
|
|
|
381
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
382
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
383
|
+
|
|
384
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
385
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
386
|
+
|
|
267
387
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
268
388
|
|
|
269
389
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -286,15 +406,26 @@ export default class ReactNativeBleTransport {
|
|
|
286
406
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
287
407
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
288
408
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
289
|
-
await this.
|
|
409
|
+
await this.releaseNative(uuid, true);
|
|
290
410
|
}
|
|
291
411
|
},
|
|
292
412
|
});
|
|
293
413
|
|
|
294
414
|
private monitorTokens: Map<string, number> = new Map();
|
|
295
415
|
|
|
416
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
417
|
+
|
|
418
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
419
|
+
|
|
420
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
421
|
+
|
|
422
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
423
|
+
|
|
296
424
|
private nextMonitorToken = 1;
|
|
297
425
|
|
|
426
|
+
/** Serializes transport lifecycle changes for the same physical device. */
|
|
427
|
+
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
428
|
+
|
|
298
429
|
constructor(options: TransportOptions) {
|
|
299
430
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
300
431
|
}
|
|
@@ -311,10 +442,19 @@ export default class ReactNativeBleTransport {
|
|
|
311
442
|
}
|
|
312
443
|
|
|
313
444
|
configureProtocolV2(signedData: any) {
|
|
445
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
446
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
314
451
|
this._messagesV2 = parseConfigure(signedData);
|
|
315
|
-
this.
|
|
316
|
-
|
|
317
|
-
.
|
|
452
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
453
|
+
if (isReconfiguration) {
|
|
454
|
+
this.protocolV2Links
|
|
455
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
456
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
457
|
+
}
|
|
318
458
|
}
|
|
319
459
|
|
|
320
460
|
listen() {
|
|
@@ -344,29 +484,15 @@ export default class ReactNativeBleTransport {
|
|
|
344
484
|
}
|
|
345
485
|
}
|
|
346
486
|
|
|
347
|
-
let fallbackServiceUuid: string | undefined;
|
|
348
|
-
|
|
349
487
|
if (!infos) {
|
|
350
488
|
const services = await device.services();
|
|
351
489
|
Log?.debug(
|
|
352
490
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
353
491
|
services?.map(service => service.uuid)
|
|
354
492
|
);
|
|
355
|
-
|
|
356
|
-
const knownService = services.find(service =>
|
|
357
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
358
|
-
);
|
|
359
|
-
const fallbackService =
|
|
360
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
361
|
-
|
|
362
|
-
if (fallbackService) {
|
|
363
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
364
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
365
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
366
|
-
}
|
|
367
493
|
}
|
|
368
494
|
|
|
369
|
-
if (!infos
|
|
495
|
+
if (!infos) {
|
|
370
496
|
try {
|
|
371
497
|
Log?.debug('cancel connection when service not found');
|
|
372
498
|
await device.cancelConnection();
|
|
@@ -376,9 +502,7 @@ export default class ReactNativeBleTransport {
|
|
|
376
502
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
377
503
|
}
|
|
378
504
|
|
|
379
|
-
const serviceUuid = infos
|
|
380
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
381
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
505
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
382
506
|
|
|
383
507
|
if (!serviceUuid) {
|
|
384
508
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -428,6 +552,7 @@ export default class ReactNativeBleTransport {
|
|
|
428
552
|
|
|
429
553
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
430
554
|
transport.disconnectSubscription?.remove();
|
|
555
|
+
const { monitorToken } = transport;
|
|
431
556
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
432
557
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
433
558
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -437,18 +562,17 @@ export default class ReactNativeBleTransport {
|
|
|
437
562
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
438
563
|
return;
|
|
439
564
|
}
|
|
565
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
566
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
440
569
|
|
|
441
570
|
try {
|
|
442
571
|
Log?.debug('device disconnect: ', device?.id);
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
id: device?.id,
|
|
446
|
-
connectId: device?.id,
|
|
447
|
-
});
|
|
448
|
-
if (this.runPromise) {
|
|
572
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
573
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
449
574
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
450
575
|
this.runPromise.reject(error);
|
|
451
|
-
this.rejectAllProtocolV2Frames(error);
|
|
452
576
|
}
|
|
453
577
|
} catch (e) {
|
|
454
578
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -458,6 +582,22 @@ export default class ReactNativeBleTransport {
|
|
|
458
582
|
});
|
|
459
583
|
}
|
|
460
584
|
|
|
585
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
586
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
590
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
594
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
595
|
+
name,
|
|
596
|
+
id: uuid,
|
|
597
|
+
connectId: uuid,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
461
601
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
462
602
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
463
603
|
try {
|
|
@@ -470,22 +610,21 @@ export default class ReactNativeBleTransport {
|
|
|
470
610
|
const isConnected = await device.isConnected().catch(() => false);
|
|
471
611
|
if (!isConnected) {
|
|
472
612
|
try {
|
|
473
|
-
device = await device.connect(connectOptions);
|
|
613
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
474
614
|
} catch (e) {
|
|
475
615
|
if (
|
|
476
616
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
477
617
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
478
618
|
) {
|
|
479
|
-
device = await device.connect();
|
|
619
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
480
620
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
481
621
|
throw e;
|
|
482
622
|
}
|
|
483
623
|
}
|
|
484
624
|
}
|
|
485
625
|
|
|
486
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
487
|
-
device
|
|
488
|
-
);
|
|
626
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
627
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
489
628
|
|
|
490
629
|
transport.device = device;
|
|
491
630
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -575,10 +714,19 @@ export default class ReactNativeBleTransport {
|
|
|
575
714
|
}
|
|
576
715
|
|
|
577
716
|
const displayName = getDeviceDisplayName(device);
|
|
717
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
718
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
719
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
578
720
|
const isOneKey =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
721
|
+
!isUnnamedIOSPeripheral &&
|
|
722
|
+
isOnekeyBluetoothDevice({
|
|
723
|
+
id: device?.id,
|
|
724
|
+
name: device?.name,
|
|
725
|
+
localName: device?.localName,
|
|
726
|
+
// The native scan is already restricted to the OneKey communication service,
|
|
727
|
+
// but ble-plx permits the returned advertisement field to be null.
|
|
728
|
+
serviceUuids: device?.serviceUUIDs ?? getBluetoothServiceUuids(),
|
|
729
|
+
});
|
|
582
730
|
if (isOneKey) {
|
|
583
731
|
addDevice(device as unknown as Device);
|
|
584
732
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -595,10 +743,18 @@ export default class ReactNativeBleTransport {
|
|
|
595
743
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
596
744
|
devices => {
|
|
597
745
|
for (const device of devices) {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
746
|
+
const localName =
|
|
747
|
+
'localName' in device && typeof device.localName === 'string'
|
|
748
|
+
? device.localName
|
|
749
|
+
: null;
|
|
750
|
+
if (
|
|
751
|
+
isOnekeyBluetoothDevice({
|
|
752
|
+
id: device.id,
|
|
753
|
+
name: device.name,
|
|
754
|
+
localName,
|
|
755
|
+
serviceUuids: device.serviceUUIDs,
|
|
756
|
+
})
|
|
757
|
+
) {
|
|
602
758
|
Log?.debug('search connected peripheral: ', device.id);
|
|
603
759
|
addDevice(device as unknown as Device);
|
|
604
760
|
}
|
|
@@ -634,13 +790,92 @@ export default class ReactNativeBleTransport {
|
|
|
634
790
|
});
|
|
635
791
|
}
|
|
636
792
|
|
|
793
|
+
private async installTransportForAcquire(
|
|
794
|
+
uuid: string,
|
|
795
|
+
device: Device,
|
|
796
|
+
characteristics?: ResolvedBleCharacteristics
|
|
797
|
+
) {
|
|
798
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
799
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
800
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
801
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
802
|
+
const monitorToken = this.nextMonitorToken;
|
|
803
|
+
this.nextMonitorToken += 1;
|
|
804
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
805
|
+
transport.monitorToken = monitorToken;
|
|
806
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
807
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
808
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
809
|
+
transport.notifyCharacteristic,
|
|
810
|
+
uuid,
|
|
811
|
+
monitorToken,
|
|
812
|
+
notifyTransactionId
|
|
813
|
+
);
|
|
814
|
+
transportCache[uuid] = transport;
|
|
815
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
816
|
+
this.protocolV2Assemblers.set(
|
|
817
|
+
uuid,
|
|
818
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
if (Platform.OS === 'ios') {
|
|
822
|
+
await new Promise<void>(resolve => {
|
|
823
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
824
|
+
});
|
|
825
|
+
} else if (Platform.OS === 'android') {
|
|
826
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const initialMtu = transport.mtuSize;
|
|
830
|
+
let refreshAttempts = 0;
|
|
831
|
+
if (
|
|
832
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
833
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
834
|
+
) {
|
|
835
|
+
refreshAttempts += 1;
|
|
836
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
837
|
+
transport.device,
|
|
838
|
+
'servicesAndNotifyReady',
|
|
839
|
+
1
|
|
840
|
+
);
|
|
841
|
+
transport.device = refreshedDevice;
|
|
842
|
+
transport.mtuSize =
|
|
843
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
844
|
+
|
|
845
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
846
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
847
|
+
refreshAttempts += 1;
|
|
848
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
849
|
+
transport.device = refreshedDevice;
|
|
850
|
+
transport.mtuSize =
|
|
851
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
856
|
+
platform: Platform.OS,
|
|
857
|
+
requested: getRequestedBleMtu(),
|
|
858
|
+
initial: initialMtu,
|
|
859
|
+
actual: transport.mtuSize,
|
|
860
|
+
refreshAttempts,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
return transport;
|
|
864
|
+
}
|
|
865
|
+
|
|
637
866
|
async acquire(input: BleAcquireInput) {
|
|
638
|
-
const { uuid
|
|
867
|
+
const { uuid } = input;
|
|
639
868
|
|
|
640
869
|
if (!uuid) {
|
|
641
870
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
642
871
|
}
|
|
643
872
|
|
|
873
|
+
return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private async acquireUnlocked(input: BleAcquireInput) {
|
|
877
|
+
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
878
|
+
|
|
644
879
|
const cachedTransport = transportCache[uuid];
|
|
645
880
|
if (cachedTransport) {
|
|
646
881
|
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
@@ -659,7 +894,7 @@ export default class ReactNativeBleTransport {
|
|
|
659
894
|
* connection, clean it up before creating a new transport instance.
|
|
660
895
|
*/
|
|
661
896
|
Log?.debug('transport not reusable, will release: ', uuid);
|
|
662
|
-
await this.
|
|
897
|
+
await this.releaseUnlocked(uuid, true);
|
|
663
898
|
}
|
|
664
899
|
|
|
665
900
|
let device: Device | null = null;
|
|
@@ -667,8 +902,8 @@ export default class ReactNativeBleTransport {
|
|
|
667
902
|
if (forceCleanRunPromise && this.runPromise) {
|
|
668
903
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
669
904
|
this.runPromise.reject(error);
|
|
670
|
-
this.rejectAllProtocolV2Frames(error);
|
|
671
905
|
this.runPromise = null;
|
|
906
|
+
this.runPromiseDeviceId = null;
|
|
672
907
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
673
908
|
}
|
|
674
909
|
|
|
@@ -704,15 +939,22 @@ export default class ReactNativeBleTransport {
|
|
|
704
939
|
if (!device) {
|
|
705
940
|
Log?.debug('try to connect to device: ', uuid);
|
|
706
941
|
try {
|
|
707
|
-
device = await
|
|
942
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
943
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
944
|
+
);
|
|
708
945
|
} catch (e) {
|
|
709
946
|
Log?.debug('try to connect to device has error: ', e);
|
|
947
|
+
if (isConnectTimeoutError(e)) {
|
|
948
|
+
throw e;
|
|
949
|
+
}
|
|
710
950
|
if (
|
|
711
951
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
712
952
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
713
953
|
) {
|
|
714
954
|
Log?.debug('first try to reconnect without params');
|
|
715
|
-
device = await
|
|
955
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
956
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
957
|
+
);
|
|
716
958
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
717
959
|
Log?.debug('device already connected');
|
|
718
960
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -728,26 +970,36 @@ export default class ReactNativeBleTransport {
|
|
|
728
970
|
|
|
729
971
|
if (!(await device.isConnected())) {
|
|
730
972
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
973
|
+
const disconnectedDevice = device;
|
|
731
974
|
|
|
732
975
|
try {
|
|
733
|
-
device = await
|
|
976
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
977
|
+
disconnectedDevice.connect(connectOptions)
|
|
978
|
+
);
|
|
734
979
|
} catch (e) {
|
|
735
980
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
981
|
+
if (isConnectTimeoutError(e)) {
|
|
982
|
+
throw e;
|
|
983
|
+
}
|
|
736
984
|
if (
|
|
737
985
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
738
986
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
739
987
|
) {
|
|
740
988
|
Log?.debug('second try to reconnect without params');
|
|
741
989
|
try {
|
|
742
|
-
device = await
|
|
990
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
991
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
992
|
+
);
|
|
743
993
|
} catch (e) {
|
|
744
994
|
Log?.debug('last try to reconnect error: ', e);
|
|
745
995
|
// last try to reconnect device if this issue exists
|
|
746
996
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
747
997
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
748
998
|
Log?.debug('last try to reconnect');
|
|
749
|
-
await
|
|
750
|
-
device = await
|
|
999
|
+
await disconnectedDevice.cancelConnection();
|
|
1000
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1001
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1002
|
+
);
|
|
751
1003
|
}
|
|
752
1004
|
}
|
|
753
1005
|
} else {
|
|
@@ -756,59 +1008,47 @@ export default class ReactNativeBleTransport {
|
|
|
756
1008
|
}
|
|
757
1009
|
}
|
|
758
1010
|
|
|
759
|
-
device = await
|
|
760
|
-
const
|
|
1011
|
+
device = await resolveNegotiatedMtu(device);
|
|
1012
|
+
const acquiredDevice = device;
|
|
1013
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1014
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
761
1015
|
|
|
762
1016
|
const protocolHint = expectedProtocol
|
|
763
1017
|
? undefined
|
|
764
|
-
:
|
|
765
|
-
|
|
1018
|
+
: input.protocolHint ??
|
|
1019
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
1020
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
766
1021
|
|
|
767
1022
|
// release transport before new transport instance
|
|
768
|
-
await this.
|
|
1023
|
+
await this.releaseUnlocked(uuid, true);
|
|
769
1024
|
if (protocolHint) {
|
|
770
1025
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
771
1026
|
}
|
|
772
1027
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
777
|
-
const monitorToken = this.nextMonitorToken;
|
|
778
|
-
this.nextMonitorToken += 1;
|
|
779
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
780
|
-
transport.monitorToken = monitorToken;
|
|
781
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
782
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
783
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
784
|
-
transport.notifyCharacteristic,
|
|
785
|
-
uuid,
|
|
786
|
-
monitorToken,
|
|
787
|
-
notifyTransactionId
|
|
788
|
-
);
|
|
789
|
-
transportCache[uuid] = transport;
|
|
790
|
-
|
|
791
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
792
|
-
|
|
793
|
-
if (Platform.OS === 'ios') {
|
|
794
|
-
await new Promise<void>(resolve => {
|
|
795
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
796
|
-
});
|
|
797
|
-
} else if (Platform.OS === 'android') {
|
|
798
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
802
|
-
|
|
803
|
-
this.emitter?.emit('device-connect', {
|
|
804
|
-
name: device.name,
|
|
805
|
-
id: device.id,
|
|
806
|
-
connectId: device.id,
|
|
1028
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1029
|
+
writeCharacteristic,
|
|
1030
|
+
notifyCharacteristic,
|
|
807
1031
|
});
|
|
808
1032
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1033
|
+
try {
|
|
1034
|
+
const protocolType = await this.detectProtocol(
|
|
1035
|
+
uuid,
|
|
1036
|
+
expectedProtocol,
|
|
1037
|
+
protocolHint,
|
|
1038
|
+
async () => {
|
|
1039
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
const currentTransport = transportCache[uuid];
|
|
1043
|
+
if (!currentTransport) {
|
|
1044
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1045
|
+
}
|
|
1046
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1047
|
+
return { uuid, protocolType };
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
await this.releaseUnlocked(uuid, true);
|
|
1050
|
+
throw error;
|
|
1051
|
+
}
|
|
812
1052
|
}
|
|
813
1053
|
|
|
814
1054
|
_monitorCharacteristic(
|
|
@@ -835,7 +1075,7 @@ export default class ReactNativeBleTransport {
|
|
|
835
1075
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
836
1076
|
return;
|
|
837
1077
|
}
|
|
838
|
-
if (this.
|
|
1078
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
839
1079
|
let errorCode:
|
|
840
1080
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
841
1081
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -858,7 +1098,7 @@ export default class ReactNativeBleTransport {
|
|
|
858
1098
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
859
1099
|
return;
|
|
860
1100
|
}
|
|
861
|
-
if (this.runPromise) {
|
|
1101
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
862
1102
|
let ERROR:
|
|
863
1103
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
864
1104
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -881,7 +1121,6 @@ export default class ReactNativeBleTransport {
|
|
|
881
1121
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
882
1122
|
);
|
|
883
1123
|
this.runPromise.reject(notifyError);
|
|
884
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
885
1124
|
Log?.debug(
|
|
886
1125
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
887
1126
|
);
|
|
@@ -889,7 +1128,6 @@ export default class ReactNativeBleTransport {
|
|
|
889
1128
|
}
|
|
890
1129
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
891
1130
|
this.runPromise.reject(notifyError);
|
|
892
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
893
1131
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
894
1132
|
}
|
|
895
1133
|
|
|
@@ -907,7 +1145,7 @@ export default class ReactNativeBleTransport {
|
|
|
907
1145
|
|
|
908
1146
|
try {
|
|
909
1147
|
const data = Buffer.from(c.value as string, 'base64');
|
|
910
|
-
const protocol = this.
|
|
1148
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
911
1149
|
if (!protocol) {
|
|
912
1150
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
913
1151
|
return;
|
|
@@ -934,14 +1172,16 @@ export default class ReactNativeBleTransport {
|
|
|
934
1172
|
// );
|
|
935
1173
|
bufferLength = 0;
|
|
936
1174
|
buffer = [];
|
|
937
|
-
this.
|
|
1175
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1176
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1177
|
+
}
|
|
938
1178
|
}
|
|
939
1179
|
} catch (error) {
|
|
940
1180
|
Log?.debug('monitor data error: ', error);
|
|
941
1181
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
942
|
-
if (this.
|
|
1182
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
943
1183
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
944
|
-
} else {
|
|
1184
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
945
1185
|
this.runPromise?.reject(notifyError);
|
|
946
1186
|
}
|
|
947
1187
|
}
|
|
@@ -951,13 +1191,23 @@ export default class ReactNativeBleTransport {
|
|
|
951
1191
|
}
|
|
952
1192
|
|
|
953
1193
|
async release(uuid: string, onclose = false) {
|
|
954
|
-
|
|
1194
|
+
return this.runLifecycleOperation(uuid, () => this.releaseUnlocked(uuid, onclose));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private async releaseUnlocked(uuid: string, onclose = false) {
|
|
955
1198
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
956
|
-
|
|
1199
|
+
return this.releaseNative(uuid, onclose);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
1203
|
+
const transport = transportCache[uuid];
|
|
1204
|
+
const manager = this.blePlxManager;
|
|
1205
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
957
1206
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
958
1207
|
this.runPromise.reject(error);
|
|
959
1208
|
this.runPromise = null;
|
|
960
|
-
this.
|
|
1209
|
+
this.runPromiseDeviceId = null;
|
|
1210
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
961
1211
|
} else {
|
|
962
1212
|
this.resetProtocolV2Frames(uuid);
|
|
963
1213
|
}
|
|
@@ -985,34 +1235,56 @@ export default class ReactNativeBleTransport {
|
|
|
985
1235
|
);
|
|
986
1236
|
transport.notifySubscription?.remove();
|
|
987
1237
|
transport.notifySubscription = undefined;
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
try {
|
|
991
|
-
await this.blePlxManager?.cancelTransaction(transport.notifyTransactionId);
|
|
992
|
-
} catch (e) {
|
|
993
|
-
Log?.debug('release: cancel notify transaction error (ignored): ', e?.message || e);
|
|
994
|
-
}
|
|
1238
|
+
if (transportCache[uuid] === transport) {
|
|
1239
|
+
delete transportCache[uuid];
|
|
995
1240
|
}
|
|
996
|
-
|
|
997
|
-
delete transportCache[uuid];
|
|
998
1241
|
}
|
|
999
1242
|
|
|
1243
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1244
|
+
|
|
1000
1245
|
this.deviceProtocol.delete(uuid);
|
|
1001
|
-
|
|
1246
|
+
this.probingProtocols.delete(uuid);
|
|
1247
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1002
1248
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1003
1249
|
this.protocolV2Assemblers.delete(uuid);
|
|
1004
1250
|
this.resetProtocolV2Frames(uuid);
|
|
1005
1251
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1252
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
1253
|
+
const operations: Promise<unknown>[] = [
|
|
1254
|
+
this.runBestEffortNativeOperation('release: restore connection priority', () =>
|
|
1255
|
+
this.restoreAndroidConnectionPriority(uuid, transport)
|
|
1256
|
+
),
|
|
1257
|
+
];
|
|
1258
|
+
if (transport?.notifyTransactionId && manager) {
|
|
1259
|
+
operations.push(
|
|
1260
|
+
this.runBestEffortNativeOperation('release: cancel notify transaction', () =>
|
|
1261
|
+
manager.cancelTransaction(transport.notifyTransactionId as string)
|
|
1262
|
+
)
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
if (manager) {
|
|
1266
|
+
operations.push(
|
|
1267
|
+
this.runBestEffortNativeOperation('release: cancel transaction', () =>
|
|
1268
|
+
manager.cancelTransaction(uuid)
|
|
1269
|
+
)
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
await Promise.all(operations);
|
|
1273
|
+
});
|
|
1011
1274
|
|
|
1012
1275
|
return Promise.resolve(true);
|
|
1013
1276
|
}
|
|
1014
1277
|
|
|
1015
1278
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
1279
|
+
if (this.getProtocolType(session) === 'V2') {
|
|
1280
|
+
await this.protocolV2Links.sendFlowControl(
|
|
1281
|
+
session,
|
|
1282
|
+
() => this.createProtocolV2Adapter(session),
|
|
1283
|
+
name,
|
|
1284
|
+
data
|
|
1285
|
+
);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1016
1288
|
await this.call(session, name, data);
|
|
1017
1289
|
}
|
|
1018
1290
|
|
|
@@ -1037,8 +1309,6 @@ export default class ReactNativeBleTransport {
|
|
|
1037
1309
|
`Device protocol has not been detected for ${uuid}`
|
|
1038
1310
|
);
|
|
1039
1311
|
}
|
|
1040
|
-
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1041
|
-
|
|
1042
1312
|
if (protocol === 'V2') {
|
|
1043
1313
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1044
1314
|
}
|
|
@@ -1064,7 +1334,25 @@ export default class ReactNativeBleTransport {
|
|
|
1064
1334
|
const transport = this.getCachedTransport(uuid);
|
|
1065
1335
|
const runPromise = createDeferred<string>();
|
|
1066
1336
|
runPromise.promise.catch(() => undefined);
|
|
1337
|
+
const supersededRunPromise = this.runPromise;
|
|
1338
|
+
if (supersededRunPromise) {
|
|
1339
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1340
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1341
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1342
|
+
// tear down the shared connection while another call is using it.
|
|
1343
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1344
|
+
}
|
|
1067
1345
|
this.runPromise = runPromise;
|
|
1346
|
+
this.runPromiseDeviceId = uuid;
|
|
1347
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1348
|
+
// only the call that still owns the slot may release it.
|
|
1349
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1350
|
+
if (this.runPromise === runPromise) {
|
|
1351
|
+
this.runPromise = null;
|
|
1352
|
+
this.runPromiseDeviceId = null;
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1068
1356
|
const messages = this._messages;
|
|
1069
1357
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1070
1358
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1090,6 +1378,9 @@ export default class ReactNativeBleTransport {
|
|
|
1090
1378
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1091
1379
|
} catch (e) {
|
|
1092
1380
|
onError(e);
|
|
1381
|
+
if (isWedgedWriteError(e)) {
|
|
1382
|
+
throw e;
|
|
1383
|
+
}
|
|
1093
1384
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1094
1385
|
}
|
|
1095
1386
|
}
|
|
@@ -1121,6 +1412,9 @@ export default class ReactNativeBleTransport {
|
|
|
1121
1412
|
}
|
|
1122
1413
|
} catch (e) {
|
|
1123
1414
|
onError(e);
|
|
1415
|
+
if (isWedgedWriteError(e)) {
|
|
1416
|
+
throw e;
|
|
1417
|
+
}
|
|
1124
1418
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1125
1419
|
}
|
|
1126
1420
|
}
|
|
@@ -1134,9 +1428,15 @@ export default class ReactNativeBleTransport {
|
|
|
1134
1428
|
if (name === 'EmmcFileWrite') {
|
|
1135
1429
|
await writeChunkedData(
|
|
1136
1430
|
buffers,
|
|
1137
|
-
data =>
|
|
1431
|
+
data =>
|
|
1432
|
+
this.writeBlePacket(
|
|
1433
|
+
uuid,
|
|
1434
|
+
data,
|
|
1435
|
+
payload => transport.writeWithRetry(payload),
|
|
1436
|
+
isCurrentOwner
|
|
1437
|
+
),
|
|
1138
1438
|
e => {
|
|
1139
|
-
|
|
1439
|
+
releaseOwnershipIfCurrent();
|
|
1140
1440
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1141
1441
|
}
|
|
1142
1442
|
);
|
|
@@ -1157,43 +1457,31 @@ export default class ReactNativeBleTransport {
|
|
|
1157
1457
|
// eslint-disable-next-line no-constant-condition
|
|
1158
1458
|
while (true) {
|
|
1159
1459
|
try {
|
|
1160
|
-
await
|
|
1460
|
+
await this.writeBlePacket(
|
|
1461
|
+
uuid,
|
|
1462
|
+
data,
|
|
1463
|
+
payload => transport.writeWithRetry(payload),
|
|
1464
|
+
isCurrentOwner
|
|
1465
|
+
);
|
|
1161
1466
|
return;
|
|
1162
1467
|
} catch (error) {
|
|
1163
1468
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1164
1469
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1165
1470
|
throw error;
|
|
1166
1471
|
}
|
|
1167
|
-
const
|
|
1168
|
-
const delayMs = shouldReconnect
|
|
1169
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1170
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1472
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1171
1473
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1172
1474
|
attempt: attempt + 1,
|
|
1173
1475
|
delayMs,
|
|
1174
|
-
reconnect: shouldReconnect,
|
|
1175
1476
|
error,
|
|
1176
1477
|
});
|
|
1177
|
-
if (shouldReconnect) {
|
|
1178
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1179
|
-
}
|
|
1180
1478
|
await delay(delayMs);
|
|
1181
1479
|
attempt += 1;
|
|
1182
|
-
if (shouldReconnect) {
|
|
1183
|
-
try {
|
|
1184
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1185
|
-
} catch (e) {
|
|
1186
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1187
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1188
|
-
throw e;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
1480
|
}
|
|
1193
1481
|
}
|
|
1194
1482
|
},
|
|
1195
1483
|
e => {
|
|
1196
|
-
|
|
1484
|
+
releaseOwnershipIfCurrent();
|
|
1197
1485
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1198
1486
|
}
|
|
1199
1487
|
);
|
|
@@ -1202,10 +1490,23 @@ export default class ReactNativeBleTransport {
|
|
|
1202
1490
|
const outData = o.toString('base64');
|
|
1203
1491
|
// Upload resources on low-end phones may OOM
|
|
1204
1492
|
try {
|
|
1205
|
-
|
|
1493
|
+
const shouldUseWriteWithResponse =
|
|
1494
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1495
|
+
await this.writeBlePacket(
|
|
1496
|
+
uuid,
|
|
1497
|
+
outData,
|
|
1498
|
+
payload =>
|
|
1499
|
+
shouldUseWriteWithResponse
|
|
1500
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1501
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1502
|
+
isCurrentOwner
|
|
1503
|
+
);
|
|
1206
1504
|
} catch (e) {
|
|
1207
1505
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1208
|
-
|
|
1506
|
+
releaseOwnershipIfCurrent();
|
|
1507
|
+
if (isWedgedWriteError(e)) {
|
|
1508
|
+
throw e;
|
|
1509
|
+
}
|
|
1209
1510
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1210
1511
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1211
1512
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1241,16 +1542,30 @@ export default class ReactNativeBleTransport {
|
|
|
1241
1542
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1242
1543
|
return check.call(jsonData);
|
|
1243
1544
|
} catch (e) {
|
|
1244
|
-
if (name === '
|
|
1245
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1545
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1546
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1246
1547
|
} else {
|
|
1247
1548
|
Log?.error('call error: ', e);
|
|
1248
1549
|
}
|
|
1550
|
+
const isProbeTimeout =
|
|
1551
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1552
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1553
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1554
|
+
// call is actively using.
|
|
1555
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1556
|
+
if (
|
|
1557
|
+
!isProbeTimeout &&
|
|
1558
|
+
!isStaleCall &&
|
|
1559
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1560
|
+
) {
|
|
1561
|
+
await this.disconnect(uuid);
|
|
1562
|
+
}
|
|
1249
1563
|
throw e;
|
|
1250
1564
|
} finally {
|
|
1251
1565
|
if (timeout) clearTimeout(timeout);
|
|
1252
1566
|
if (this.runPromise === runPromise) {
|
|
1253
1567
|
this.runPromise = null;
|
|
1568
|
+
this.runPromiseDeviceId = null;
|
|
1254
1569
|
}
|
|
1255
1570
|
}
|
|
1256
1571
|
}
|
|
@@ -1260,8 +1575,14 @@ export default class ReactNativeBleTransport {
|
|
|
1260
1575
|
}
|
|
1261
1576
|
|
|
1262
1577
|
async disconnect(session: string) {
|
|
1578
|
+
return this.runLifecycleOperation(session, () => this.disconnectUnlocked(session));
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private async disconnectUnlocked(session: string) {
|
|
1263
1582
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1264
1583
|
const transport = transportCache[session];
|
|
1584
|
+
const manager = this.blePlxManager;
|
|
1585
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1265
1586
|
|
|
1266
1587
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1267
1588
|
// from being triggered when we cancel the connection below
|
|
@@ -1289,60 +1610,232 @@ export default class ReactNativeBleTransport {
|
|
|
1289
1610
|
}
|
|
1290
1611
|
}
|
|
1291
1612
|
|
|
1292
|
-
// cancel the ble transaction
|
|
1293
|
-
if (session) {
|
|
1294
|
-
try {
|
|
1295
|
-
await this.blePlxManager?.cancelTransaction(session);
|
|
1296
|
-
} catch (e) {
|
|
1297
|
-
Log?.debug('resetSession: cancel transaction error (ignored): ', e?.message || e);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
// disconnect the device via the device object
|
|
1302
|
-
if (transport?.device) {
|
|
1303
|
-
try {
|
|
1304
|
-
await transport.device.cancelConnection();
|
|
1305
|
-
} catch (e) {
|
|
1306
|
-
Log?.debug('resetSession: device.cancelConnection error (ignored): ', e?.message || e);
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// disconnect the device via the ble manager
|
|
1311
|
-
try {
|
|
1312
|
-
await this.blePlxManager?.cancelDeviceConnection(session);
|
|
1313
|
-
} catch (e) {
|
|
1314
|
-
Log?.debug('resetSession: manager.cancelDeviceConnection error (ignored): ', e?.message || e);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
1613
|
// clear the transport cache
|
|
1318
|
-
if (transportCache[session]) {
|
|
1614
|
+
if (!transport || transportCache[session] === transport) {
|
|
1319
1615
|
delete transportCache[session];
|
|
1320
1616
|
}
|
|
1321
1617
|
this.deviceProtocol.delete(session);
|
|
1618
|
+
this.probingProtocols.delete(session);
|
|
1322
1619
|
this.deviceProtocolHints.delete(session);
|
|
1620
|
+
this.sessionProtocols.delete(session);
|
|
1621
|
+
this.protocolReprobeFailures.delete(session);
|
|
1323
1622
|
this.protocolV2Assemblers.delete(session);
|
|
1324
1623
|
this.resetProtocolV2Frames(session);
|
|
1325
1624
|
|
|
1326
1625
|
// emit the disconnect event
|
|
1327
1626
|
try {
|
|
1328
|
-
this.
|
|
1329
|
-
name: transport?.device?.name,
|
|
1330
|
-
id: session,
|
|
1331
|
-
connectId: session,
|
|
1332
|
-
});
|
|
1627
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1333
1628
|
} catch (e) {
|
|
1334
1629
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1335
1630
|
}
|
|
1631
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1632
|
+
this.monitorTokens.delete(session);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
await this.runNativeTeardown(session, manager, async () => {
|
|
1636
|
+
const operations: Promise<unknown>[] = [];
|
|
1637
|
+
if (manager) {
|
|
1638
|
+
operations.push(
|
|
1639
|
+
this.runBestEffortNativeOperation('disconnect: cancel transaction', () =>
|
|
1640
|
+
manager.cancelTransaction(session)
|
|
1641
|
+
)
|
|
1642
|
+
);
|
|
1643
|
+
operations.push(
|
|
1644
|
+
this.runBestEffortNativeOperation('disconnect: cancel device connection', () =>
|
|
1645
|
+
manager.cancelDeviceConnection(session)
|
|
1646
|
+
)
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
if (transport?.device) {
|
|
1650
|
+
operations.push(
|
|
1651
|
+
this.runBestEffortNativeOperation('disconnect: device cancel connection', () =>
|
|
1652
|
+
transport.device.cancelConnection()
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
await Promise.all(operations);
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1336
1659
|
// eslint-disable-next-line no-promise-executor-return
|
|
1337
1660
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1338
1661
|
}
|
|
1339
1662
|
|
|
1663
|
+
private async runNativeTeardown(
|
|
1664
|
+
uuid: string,
|
|
1665
|
+
manager: BlePlxManager | undefined,
|
|
1666
|
+
teardown: () => Promise<void>
|
|
1667
|
+
) {
|
|
1668
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1669
|
+
let timedOut = false;
|
|
1670
|
+
const pending = Promise.resolve()
|
|
1671
|
+
.then(teardown)
|
|
1672
|
+
.catch(error => {
|
|
1673
|
+
Log?.debug('BLE native teardown error (ignored): ', error?.message || error);
|
|
1674
|
+
});
|
|
1675
|
+
try {
|
|
1676
|
+
await Promise.race([
|
|
1677
|
+
pending,
|
|
1678
|
+
new Promise<void>(resolve => {
|
|
1679
|
+
timer = setTimeout(() => {
|
|
1680
|
+
timedOut = true;
|
|
1681
|
+
resolve();
|
|
1682
|
+
}, BLE_NATIVE_TEARDOWN_TIMEOUT_MS);
|
|
1683
|
+
}),
|
|
1684
|
+
]);
|
|
1685
|
+
} finally {
|
|
1686
|
+
if (timer) clearTimeout(timer);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
if (timedOut) {
|
|
1690
|
+
Log?.error('[ReactNativeBleTransport] BLE native teardown timed out:', uuid);
|
|
1691
|
+
if (this.blePlxManager === manager) {
|
|
1692
|
+
this.resetPlxManager();
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private runBestEffortNativeOperation(label: string, operation: () => Promise<unknown>) {
|
|
1698
|
+
return Promise.resolve()
|
|
1699
|
+
.then(operation)
|
|
1700
|
+
.catch(error => {
|
|
1701
|
+
Log?.debug(`${label} error (ignored): `, error?.message || error);
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
private async runLifecycleOperation<T>(uuid: string, operation: () => Promise<T>): Promise<T> {
|
|
1706
|
+
const previousOperation = this.lifecycleOperations.get(uuid) ?? Promise.resolve();
|
|
1707
|
+
let completeOperation!: () => void;
|
|
1708
|
+
const operationGate = new Promise<void>(resolve => {
|
|
1709
|
+
completeOperation = resolve;
|
|
1710
|
+
});
|
|
1711
|
+
const operationTail = previousOperation.catch(() => undefined).then(() => operationGate);
|
|
1712
|
+
this.lifecycleOperations.set(uuid, operationTail);
|
|
1713
|
+
|
|
1714
|
+
await previousOperation.catch(() => undefined);
|
|
1715
|
+
try {
|
|
1716
|
+
return await operation();
|
|
1717
|
+
} finally {
|
|
1718
|
+
completeOperation();
|
|
1719
|
+
if (this.lifecycleOperations.get(uuid) === operationTail) {
|
|
1720
|
+
this.lifecycleOperations.delete(uuid);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1340
1725
|
cancel() {
|
|
1341
1726
|
Log?.debug('transport-react-native transport cancel');
|
|
1342
1727
|
if (this.runPromise) {
|
|
1343
1728
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1344
1729
|
}
|
|
1345
1730
|
this.runPromise = null;
|
|
1731
|
+
this.runPromiseDeviceId = null;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1735
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1736
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1737
|
+
let timedOut = false;
|
|
1738
|
+
const pending = connect();
|
|
1739
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1740
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1741
|
+
pending.catch(() => undefined);
|
|
1742
|
+
try {
|
|
1743
|
+
const result = await Promise.race([
|
|
1744
|
+
pending,
|
|
1745
|
+
new Promise<never>((_, reject) => {
|
|
1746
|
+
timer = setTimeout(() => {
|
|
1747
|
+
timedOut = true;
|
|
1748
|
+
reject(
|
|
1749
|
+
ERRORS.TypedError(
|
|
1750
|
+
HardwareErrorCode.BleConnectedError,
|
|
1751
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1752
|
+
)
|
|
1753
|
+
);
|
|
1754
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1755
|
+
}),
|
|
1756
|
+
]);
|
|
1757
|
+
return result;
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1760
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1761
|
+
}
|
|
1762
|
+
throw error;
|
|
1763
|
+
} finally {
|
|
1764
|
+
if (timer) clearTimeout(timer);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1769
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1770
|
+
uuid: string,
|
|
1771
|
+
device: Device
|
|
1772
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1773
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1774
|
+
let timedOut = false;
|
|
1775
|
+
const pending = this.resolveCharacteristics(device);
|
|
1776
|
+
pending.catch(() => undefined);
|
|
1777
|
+
try {
|
|
1778
|
+
const result = await Promise.race([
|
|
1779
|
+
pending,
|
|
1780
|
+
new Promise<never>((_, reject) => {
|
|
1781
|
+
timer = setTimeout(() => {
|
|
1782
|
+
timedOut = true;
|
|
1783
|
+
reject(
|
|
1784
|
+
ERRORS.TypedError(
|
|
1785
|
+
HardwareErrorCode.BleConnectedError,
|
|
1786
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1787
|
+
)
|
|
1788
|
+
);
|
|
1789
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1790
|
+
}),
|
|
1791
|
+
]);
|
|
1792
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1793
|
+
return result;
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1796
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1797
|
+
}
|
|
1798
|
+
throw error;
|
|
1799
|
+
} finally {
|
|
1800
|
+
if (timer) clearTimeout(timer);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1806
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1807
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1808
|
+
*/
|
|
1809
|
+
private abandonStalledConnection(
|
|
1810
|
+
uuid: string,
|
|
1811
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1812
|
+
) {
|
|
1813
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1814
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1815
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1816
|
+
stage,
|
|
1817
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1821
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1822
|
+
});
|
|
1823
|
+
const stalled = transportCache[uuid];
|
|
1824
|
+
if (stalled) {
|
|
1825
|
+
delete transportCache[uuid];
|
|
1826
|
+
}
|
|
1827
|
+
this.deviceProtocol.delete(uuid);
|
|
1828
|
+
this.probingProtocols.delete(uuid);
|
|
1829
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1830
|
+
this.resetProtocolV2Frames(uuid);
|
|
1831
|
+
|
|
1832
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1833
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1834
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1835
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1836
|
+
this.resetPlxManager();
|
|
1837
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1838
|
+
}
|
|
1346
1839
|
}
|
|
1347
1840
|
|
|
1348
1841
|
private getCachedTransport(uuid: string) {
|
|
@@ -1353,6 +1846,137 @@ export default class ReactNativeBleTransport {
|
|
|
1353
1846
|
return transport;
|
|
1354
1847
|
}
|
|
1355
1848
|
|
|
1849
|
+
/**
|
|
1850
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1851
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1852
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1853
|
+
* cached and every later call would hang on it again.
|
|
1854
|
+
*/
|
|
1855
|
+
private async writeBlePacket(
|
|
1856
|
+
uuid: string,
|
|
1857
|
+
data: string,
|
|
1858
|
+
write: (payload: string) => Promise<unknown>,
|
|
1859
|
+
isCurrentOwner?: () => boolean
|
|
1860
|
+
) {
|
|
1861
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1862
|
+
let timedOut = false;
|
|
1863
|
+
try {
|
|
1864
|
+
await Promise.race([
|
|
1865
|
+
write(data),
|
|
1866
|
+
new Promise<never>((_, reject) => {
|
|
1867
|
+
timer = setTimeout(() => {
|
|
1868
|
+
timedOut = true;
|
|
1869
|
+
reject(
|
|
1870
|
+
ERRORS.TypedError(
|
|
1871
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1872
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1873
|
+
)
|
|
1874
|
+
);
|
|
1875
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1876
|
+
}),
|
|
1877
|
+
]);
|
|
1878
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (timedOut) {
|
|
1881
|
+
// A superseded call's late write must not tear down the link the current
|
|
1882
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1883
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1884
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1885
|
+
} else {
|
|
1886
|
+
this.tearDownWedgedLink(uuid);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
throw error;
|
|
1890
|
+
} finally {
|
|
1891
|
+
if (timer) clearTimeout(timer);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/**
|
|
1896
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1897
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1898
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1899
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1900
|
+
*/
|
|
1901
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1902
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1903
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1904
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1905
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
const wedged = transportCache[uuid];
|
|
1909
|
+
this.disconnect(uuid).catch(error => {
|
|
1910
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1911
|
+
});
|
|
1912
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1913
|
+
delete transportCache[uuid];
|
|
1914
|
+
}
|
|
1915
|
+
this.deviceProtocol.delete(uuid);
|
|
1916
|
+
this.probingProtocols.delete(uuid);
|
|
1917
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1918
|
+
this.resetProtocolV2Frames(uuid);
|
|
1919
|
+
|
|
1920
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1921
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1922
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1923
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1924
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1925
|
+
this.resetPlxManager();
|
|
1926
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
private resetPlxManager() {
|
|
1931
|
+
const manager = this.blePlxManager;
|
|
1932
|
+
this.blePlxManager = undefined;
|
|
1933
|
+
const reason = 'React Native BLE manager reset';
|
|
1934
|
+
// Destroying the shared manager invalidates every peripheral it owns. Notify
|
|
1935
|
+
// each cached session before clearing generations so Core cannot retain a
|
|
1936
|
+
// silently stale connection for an unrelated device.
|
|
1937
|
+
Object.entries(transportCache).forEach(([uuid, cachedTransport]) => {
|
|
1938
|
+
try {
|
|
1939
|
+
cachedTransport.disconnectSubscription?.remove();
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
Log?.debug('BLE manager reset disconnect subscription removal failed:', error);
|
|
1942
|
+
}
|
|
1943
|
+
cachedTransport.disconnectSubscription = undefined;
|
|
1944
|
+
try {
|
|
1945
|
+
cachedTransport.notifySubscription?.remove();
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
Log?.debug('BLE manager reset notify subscription removal failed:', error);
|
|
1948
|
+
}
|
|
1949
|
+
cachedTransport.notifySubscription = undefined;
|
|
1950
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1951
|
+
try {
|
|
1952
|
+
this.emitDeviceDisconnect(
|
|
1953
|
+
uuid,
|
|
1954
|
+
cachedTransport.device?.name,
|
|
1955
|
+
cachedTransport.monitorToken ?? this.monitorTokens.get(uuid)
|
|
1956
|
+
);
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
Log?.debug('BLE manager reset disconnect event failed:', error);
|
|
1959
|
+
}
|
|
1960
|
+
delete transportCache[uuid];
|
|
1961
|
+
});
|
|
1962
|
+
this.protocolV2Links.invalidateAllLinks(reason).catch(error => {
|
|
1963
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager link invalidation failed:', error);
|
|
1964
|
+
});
|
|
1965
|
+
this.deviceProtocol.clear();
|
|
1966
|
+
this.probingProtocols.clear();
|
|
1967
|
+
this.sessionProtocols.clear();
|
|
1968
|
+
this.protocolReprobeFailures.clear();
|
|
1969
|
+
this.writeTimeoutCounts.clear();
|
|
1970
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
1971
|
+
this.monitorTokens.clear();
|
|
1972
|
+
this.protocolV2Assemblers.clear();
|
|
1973
|
+
try {
|
|
1974
|
+
manager?.destroy();
|
|
1975
|
+
} catch (error) {
|
|
1976
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1356
1980
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1357
1981
|
return ERRORS.TypedError(
|
|
1358
1982
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1363,24 +1987,47 @@ export default class ReactNativeBleTransport {
|
|
|
1363
1987
|
private createProtocolDetectionError() {
|
|
1364
1988
|
return ERRORS.TypedError(
|
|
1365
1989
|
HardwareErrorCode.BleTimeoutError,
|
|
1366
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
1990
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1367
1991
|
);
|
|
1368
1992
|
}
|
|
1369
1993
|
|
|
1370
1994
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1995
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1996
|
+
this.probingProtocols.delete(uuid);
|
|
1997
|
+
}
|
|
1371
1998
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1372
1999
|
this.deviceProtocol.delete(uuid);
|
|
1373
2000
|
}
|
|
1374
2001
|
}
|
|
1375
2002
|
|
|
2003
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
2004
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
2005
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1376
2008
|
private async detectProtocol(
|
|
1377
2009
|
uuid: string,
|
|
1378
2010
|
expectedProtocol?: ProtocolType,
|
|
1379
|
-
protocolHint?: ProtocolType
|
|
2011
|
+
protocolHint?: ProtocolType,
|
|
2012
|
+
rebuildTransport?: () => Promise<void>
|
|
1380
2013
|
): Promise<ProtocolType> {
|
|
2014
|
+
// iOS still skips an extra V1 Initialize during acquire. Expected V2 must
|
|
2015
|
+
// Ping so USB-priority `link disabled` can surface instead of a later
|
|
2016
|
+
// unmapped RuntimeError.
|
|
2017
|
+
if (Platform.OS === 'ios' && expectedProtocol === 'V1') {
|
|
2018
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
2019
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
2020
|
+
deviceId: uuid,
|
|
2021
|
+
protocol: expectedProtocol,
|
|
2022
|
+
source: 'expected',
|
|
2023
|
+
});
|
|
2024
|
+
return expectedProtocol;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1381
2027
|
if (expectedProtocol === 'V1') {
|
|
1382
2028
|
if (await this.probeProtocolV1(uuid)) {
|
|
1383
2029
|
this.deviceProtocol.set(uuid, 'V1');
|
|
2030
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1384
2031
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1385
2032
|
deviceId: uuid,
|
|
1386
2033
|
protocol: 'V1',
|
|
@@ -1392,33 +2039,52 @@ export default class ReactNativeBleTransport {
|
|
|
1392
2039
|
}
|
|
1393
2040
|
|
|
1394
2041
|
if (expectedProtocol === 'V2') {
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
2042
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
2043
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
2044
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
2045
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
2046
|
+
deviceId: uuid,
|
|
2047
|
+
protocol: 'V2',
|
|
2048
|
+
source: 'expected',
|
|
2049
|
+
});
|
|
2050
|
+
return 'V2';
|
|
2051
|
+
}
|
|
2052
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1404
2053
|
}
|
|
1405
2054
|
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1409
|
-
const
|
|
2055
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
2056
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
2057
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
2058
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
2059
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1410
2060
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
2061
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
2062
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
2063
|
+
// only adds its timeout to every poll.
|
|
2064
|
+
const trustSessionProtocol =
|
|
2065
|
+
sessionProtocol !== undefined &&
|
|
2066
|
+
!protocolHint &&
|
|
2067
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2068
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1411
2069
|
|
|
1412
2070
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1413
2071
|
const protocol = probeOrder[i];
|
|
1414
2072
|
if (i > 0) {
|
|
1415
|
-
//
|
|
2073
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1416
2074
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
2075
|
+
if (!transportCache[uuid]) {
|
|
2076
|
+
if (!rebuildTransport) {
|
|
2077
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
2078
|
+
}
|
|
2079
|
+
await rebuildTransport();
|
|
2080
|
+
}
|
|
1417
2081
|
}
|
|
1418
2082
|
const detected =
|
|
1419
2083
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1420
2084
|
if (detected) {
|
|
1421
2085
|
this.deviceProtocol.set(uuid, protocol);
|
|
2086
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
2087
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1422
2088
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1423
2089
|
deviceId: uuid,
|
|
1424
2090
|
protocol,
|
|
@@ -1428,7 +2094,16 @@ export default class ReactNativeBleTransport {
|
|
|
1428
2094
|
}
|
|
1429
2095
|
}
|
|
1430
2096
|
|
|
2097
|
+
if (trustSessionProtocol) {
|
|
2098
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2099
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
2100
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
2101
|
+
} else {
|
|
2102
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1431
2105
|
this.deviceProtocol.delete(uuid);
|
|
2106
|
+
this.probingProtocols.delete(uuid);
|
|
1432
2107
|
throw this.createProtocolDetectionError();
|
|
1433
2108
|
}
|
|
1434
2109
|
|
|
@@ -1490,12 +2165,20 @@ export default class ReactNativeBleTransport {
|
|
|
1490
2165
|
}
|
|
1491
2166
|
|
|
1492
2167
|
try {
|
|
1493
|
-
this.
|
|
1494
|
-
|
|
2168
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2169
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
2170
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
2171
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2172
|
+
this.probingProtocols.delete(uuid);
|
|
1495
2173
|
return true;
|
|
1496
2174
|
} catch (error) {
|
|
1497
2175
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1498
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
2176
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2177
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2178
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2179
|
+
if (isWedgedWriteError(error)) {
|
|
2180
|
+
throw error;
|
|
2181
|
+
}
|
|
1499
2182
|
return false;
|
|
1500
2183
|
}
|
|
1501
2184
|
}
|
|
@@ -1505,7 +2188,7 @@ export default class ReactNativeBleTransport {
|
|
|
1505
2188
|
return false;
|
|
1506
2189
|
}
|
|
1507
2190
|
|
|
1508
|
-
this.
|
|
2191
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1509
2192
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1510
2193
|
const detected = await probeProtocolV2Helper({
|
|
1511
2194
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1520,6 +2203,8 @@ export default class ReactNativeBleTransport {
|
|
|
1520
2203
|
});
|
|
1521
2204
|
if (!detected) {
|
|
1522
2205
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2206
|
+
} else {
|
|
2207
|
+
this.probingProtocols.delete(uuid);
|
|
1523
2208
|
}
|
|
1524
2209
|
return detected;
|
|
1525
2210
|
}
|
|
@@ -1570,17 +2255,8 @@ export default class ReactNativeBleTransport {
|
|
|
1570
2255
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1571
2256
|
}
|
|
1572
2257
|
|
|
1573
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1574
|
-
this.protocolV2FrameQueues.clear();
|
|
1575
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1576
|
-
framePromise.reject(error);
|
|
1577
|
-
}
|
|
1578
|
-
this.protocolV2FramePromises.clear();
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
2258
|
private resetProtocolV2Frames(uuid: string) {
|
|
1582
|
-
this.
|
|
1583
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2259
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1584
2260
|
}
|
|
1585
2261
|
|
|
1586
2262
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -1609,19 +2285,95 @@ export default class ReactNativeBleTransport {
|
|
|
1609
2285
|
}
|
|
1610
2286
|
}
|
|
1611
2287
|
|
|
1612
|
-
private async
|
|
2288
|
+
private async writeProtocolV2Packet(
|
|
2289
|
+
uuid: string,
|
|
2290
|
+
transport: BleTransport,
|
|
2291
|
+
base64: string,
|
|
2292
|
+
context: ProtocolV2CallContext,
|
|
2293
|
+
assertCurrentGeneration: () => void
|
|
2294
|
+
) {
|
|
2295
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2296
|
+
platform: Platform.OS,
|
|
2297
|
+
highThroughput: context.highThroughput,
|
|
2298
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2299
|
+
characteristic: transport.writeCharacteristic,
|
|
2300
|
+
});
|
|
2301
|
+
let attempt = 0;
|
|
2302
|
+
for (;;) {
|
|
2303
|
+
assertCurrentGeneration();
|
|
2304
|
+
if (context.signal.aborted) {
|
|
2305
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2306
|
+
}
|
|
2307
|
+
try {
|
|
2308
|
+
await this.writeBlePacket(
|
|
2309
|
+
uuid,
|
|
2310
|
+
base64,
|
|
2311
|
+
payload =>
|
|
2312
|
+
shouldUseWriteWithResponse
|
|
2313
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2314
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2315
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2316
|
+
// tear down the link that the current generation is using.
|
|
2317
|
+
() => {
|
|
2318
|
+
try {
|
|
2319
|
+
assertCurrentGeneration();
|
|
2320
|
+
return !context.signal.aborted;
|
|
2321
|
+
} catch {
|
|
2322
|
+
return false;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
);
|
|
2326
|
+
assertCurrentGeneration();
|
|
2327
|
+
return;
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
if (
|
|
2330
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2331
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2332
|
+
) {
|
|
2333
|
+
throw error;
|
|
2334
|
+
}
|
|
2335
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2336
|
+
attempt += 1;
|
|
2337
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2338
|
+
name: context.messageName,
|
|
2339
|
+
attempt,
|
|
2340
|
+
delayMs,
|
|
2341
|
+
});
|
|
2342
|
+
await delay(delayMs);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
private async writeProtocolV2Frame(
|
|
2348
|
+
uuid: string,
|
|
2349
|
+
transport: BleTransport,
|
|
2350
|
+
frame: Uint8Array,
|
|
2351
|
+
context: ProtocolV2CallContext,
|
|
2352
|
+
assertCurrentGeneration: () => void
|
|
2353
|
+
) {
|
|
1613
2354
|
const tuning = getProtocolV2BleTuning();
|
|
1614
2355
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1615
2356
|
platform: Platform.OS,
|
|
1616
2357
|
iosPacketLength: tuning.iosPacketLength,
|
|
1617
2358
|
androidPacketLength: tuning.androidPacketLength,
|
|
1618
|
-
mtu:
|
|
2359
|
+
mtu: transport.mtuSize,
|
|
2360
|
+
});
|
|
2361
|
+
await writeProtocolV2BleFrame({
|
|
2362
|
+
frame,
|
|
2363
|
+
packetCapacity,
|
|
2364
|
+
assertActive: assertCurrentGeneration,
|
|
2365
|
+
signal: context.signal,
|
|
2366
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2367
|
+
wait: delay,
|
|
2368
|
+
writePacket: packet =>
|
|
2369
|
+
this.writeProtocolV2Packet(
|
|
2370
|
+
uuid,
|
|
2371
|
+
transport,
|
|
2372
|
+
Buffer.from(packet).toString('base64'),
|
|
2373
|
+
context,
|
|
2374
|
+
assertCurrentGeneration
|
|
2375
|
+
),
|
|
1619
2376
|
});
|
|
1620
|
-
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1621
|
-
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1622
|
-
const base64 = Buffer.from(chunk).toString('base64');
|
|
1623
|
-
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1624
|
-
}
|
|
1625
2377
|
}
|
|
1626
2378
|
|
|
1627
2379
|
private async callProtocolV2(
|
|
@@ -1634,19 +2386,45 @@ export default class ReactNativeBleTransport {
|
|
|
1634
2386
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1635
2387
|
}
|
|
1636
2388
|
|
|
1637
|
-
const callOptions =
|
|
1638
|
-
|
|
1639
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1640
|
-
};
|
|
1641
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2389
|
+
const callOptions = options;
|
|
2390
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
1642
2391
|
|
|
1643
|
-
if (
|
|
2392
|
+
if (highThroughputWrite) {
|
|
2393
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
1644
2394
|
const tuning = getProtocolV2BleTuning();
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2395
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2396
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2397
|
+
platform: Platform.OS,
|
|
2398
|
+
highThroughput: true,
|
|
2399
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2400
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
1649
2401
|
});
|
|
2402
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2403
|
+
platform: Platform.OS,
|
|
2404
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2405
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2406
|
+
mtu: currentTransport.mtuSize,
|
|
2407
|
+
});
|
|
2408
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2409
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2410
|
+
currentTransport.mtuSize
|
|
2411
|
+
)}:${packetCapacity}`;
|
|
2412
|
+
const loggedSignatures =
|
|
2413
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2414
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2415
|
+
loggedSignatures.add(logSignature);
|
|
2416
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2417
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2418
|
+
name,
|
|
2419
|
+
writeMode,
|
|
2420
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2421
|
+
packetCapacity,
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
if (highThroughputWrite) {
|
|
2427
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
1650
2428
|
}
|
|
1651
2429
|
|
|
1652
2430
|
try {
|
|
@@ -1660,6 +2438,90 @@ export default class ReactNativeBleTransport {
|
|
|
1660
2438
|
} catch (e) {
|
|
1661
2439
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1662
2440
|
throw e;
|
|
2441
|
+
} finally {
|
|
2442
|
+
if (highThroughputWrite) {
|
|
2443
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2449
|
+
const transport = this.getCachedTransport(uuid);
|
|
2450
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2451
|
+
|
|
2452
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2453
|
+
transport.device = refreshedDevice;
|
|
2454
|
+
transport.mtuSize =
|
|
2455
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2456
|
+
|
|
2457
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2458
|
+
throw ERRORS.TypedError(
|
|
2459
|
+
HardwareErrorCode.BleConnectedError,
|
|
2460
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2466
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2467
|
+
if (timerId !== undefined) {
|
|
2468
|
+
clearTimeout(timerId);
|
|
2469
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2474
|
+
if (Platform.OS !== 'android') return;
|
|
2475
|
+
|
|
2476
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2477
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2478
|
+
|
|
2479
|
+
const transport = transportCache[uuid];
|
|
2480
|
+
if (!transport) return;
|
|
2481
|
+
|
|
2482
|
+
try {
|
|
2483
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2484
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2485
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2486
|
+
priority: 'high',
|
|
2487
|
+
});
|
|
2488
|
+
} catch (error) {
|
|
2489
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2496
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2497
|
+
|
|
2498
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2499
|
+
const timerId = setTimeout(() => {
|
|
2500
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2501
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2502
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2503
|
+
);
|
|
2504
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2505
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2509
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2510
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
try {
|
|
2515
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2516
|
+
ConnectionPriority.Balanced
|
|
2517
|
+
);
|
|
2518
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2519
|
+
priority: 'balanced',
|
|
2520
|
+
});
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2523
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2524
|
+
});
|
|
1663
2525
|
}
|
|
1664
2526
|
}
|
|
1665
2527
|
|
|
@@ -1680,10 +2542,16 @@ export default class ReactNativeBleTransport {
|
|
|
1680
2542
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1681
2543
|
this.resetProtocolV2Frames(uuid);
|
|
1682
2544
|
},
|
|
1683
|
-
writeFrame: async (frame: Uint8Array) => {
|
|
2545
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1684
2546
|
assertCurrentGeneration();
|
|
1685
2547
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1686
|
-
await this.writeProtocolV2Frame(
|
|
2548
|
+
await this.writeProtocolV2Frame(
|
|
2549
|
+
uuid,
|
|
2550
|
+
currentTransport,
|
|
2551
|
+
frame,
|
|
2552
|
+
context,
|
|
2553
|
+
assertCurrentGeneration
|
|
2554
|
+
);
|
|
1687
2555
|
},
|
|
1688
2556
|
readFrame: async () => {
|
|
1689
2557
|
assertCurrentGeneration();
|
|
@@ -1694,6 +2562,7 @@ export default class ReactNativeBleTransport {
|
|
|
1694
2562
|
return rxFrame;
|
|
1695
2563
|
},
|
|
1696
2564
|
reset: (reason: string) => {
|
|
2565
|
+
if (this.monitorTokens.get(uuid) !== generation) return;
|
|
1697
2566
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1698
2567
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1699
2568
|
},
|
|
@@ -1708,6 +2577,6 @@ export default class ReactNativeBleTransport {
|
|
|
1708
2577
|
}
|
|
1709
2578
|
|
|
1710
2579
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1711
|
-
return this.
|
|
2580
|
+
return this.getActiveProtocol(path);
|
|
1712
2581
|
}
|
|
1713
2582
|
}
|