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