@onekeyfe/hd-transport-react-native 1.2.0-alpha.18 → 1.2.0-alpha.180
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 +137 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +950 -318
- 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 +942 -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 +1276 -302
- 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,13 +52,21 @@ 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';
|
|
44
58
|
import type EventEmitter from 'events';
|
|
45
59
|
import type { BleAcquireInput, TransportOptions } from './types';
|
|
46
60
|
|
|
61
|
+
type FirmwareInstallBleAcquireInput = BleAcquireInput & {
|
|
62
|
+
/**
|
|
63
|
+
* Reuse the already-verified protocol after an expected firmware-install
|
|
64
|
+
* disconnect. The install loader accepts status requests but may not answer
|
|
65
|
+
* the generic protocol probe used by a normal acquire.
|
|
66
|
+
*/
|
|
67
|
+
skipProtocolProbe?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
47
70
|
const { check, ProtocolV1, parseConfigure } = transport;
|
|
48
71
|
|
|
49
72
|
const Log = bleLogger;
|
|
@@ -53,24 +76,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
53
76
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
54
77
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
55
78
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
56
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
57
79
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
58
80
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
59
81
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
60
82
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
61
83
|
|
|
62
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
84
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
63
85
|
type ResolvedBleCharacteristics = {
|
|
64
86
|
writeCharacteristic: Characteristic;
|
|
65
87
|
notifyCharacteristic: Characteristic;
|
|
66
88
|
};
|
|
67
89
|
|
|
90
|
+
const isAsciiWhitespace = (code: number) =>
|
|
91
|
+
code === 0x09 ||
|
|
92
|
+
code === 0x0a ||
|
|
93
|
+
code === 0x0b ||
|
|
94
|
+
code === 0x0c ||
|
|
95
|
+
code === 0x0d ||
|
|
96
|
+
code === 0x20;
|
|
97
|
+
|
|
98
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
99
|
+
let searchFrom = 0;
|
|
100
|
+
while (searchFrom < text.length) {
|
|
101
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
102
|
+
if (statusIndex < 0) return false;
|
|
103
|
+
|
|
104
|
+
let cursor = statusIndex + 'status'.length;
|
|
105
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
106
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
107
|
+
cursor += 1;
|
|
108
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
109
|
+
}
|
|
110
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
111
|
+
|
|
112
|
+
searchFrom = statusIndex + 'status'.length;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
};
|
|
116
|
+
|
|
68
117
|
const delay = (ms: number) =>
|
|
69
118
|
new Promise<void>(resolve => {
|
|
70
119
|
setTimeout(resolve, ms);
|
|
71
120
|
});
|
|
72
121
|
|
|
73
|
-
const getFirmwareUploadWriteRetryType = (
|
|
122
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
123
|
+
error: unknown
|
|
124
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
74
125
|
if (!error || typeof error !== 'object') return null;
|
|
75
126
|
const bleWriteError = error as {
|
|
76
127
|
androidErrorCode?: unknown;
|
|
@@ -81,13 +132,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
81
132
|
name?: unknown;
|
|
82
133
|
};
|
|
83
134
|
|
|
84
|
-
if (
|
|
85
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
86
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
87
|
-
) {
|
|
88
|
-
return 'reconnectable';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
135
|
if (
|
|
92
136
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
93
137
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -98,15 +142,31 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
98
142
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
99
143
|
.filter(value => typeof value === 'string')
|
|
100
144
|
.join(' ');
|
|
101
|
-
return
|
|
145
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
102
146
|
};
|
|
103
147
|
|
|
104
148
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
105
149
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
106
|
-
const
|
|
107
|
-
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
150
|
+
const PROTOCOL_PROBE_TIMEOUT_MS = 3000;
|
|
108
151
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
154
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
155
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
156
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
157
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
158
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
159
|
+
*/
|
|
160
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
161
|
+
export const BLE_NATIVE_TEARDOWN_TIMEOUT_MS = 3_000;
|
|
162
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
163
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
164
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
165
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
166
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
167
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
168
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
169
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
110
170
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
111
171
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
112
172
|
export type ProtocolV2BleTuning = {
|
|
@@ -117,8 +177,8 @@ export type ProtocolV2BleTuning = {
|
|
|
117
177
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
118
178
|
|
|
119
179
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
120
|
-
iosPacketLength:
|
|
121
|
-
androidPacketLength:
|
|
180
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
181
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
122
182
|
};
|
|
123
183
|
|
|
124
184
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -152,32 +212,71 @@ export function getProtocolV2BleTuning() {
|
|
|
152
212
|
return { ...protocolV2BleTuning };
|
|
153
213
|
}
|
|
154
214
|
|
|
155
|
-
function inferProtocolHintFromDeviceName(name?: string | null): ProtocolType | undefined {
|
|
156
|
-
return /\bpro\s*2\b/i.test(name ?? '') ? 'V2' : undefined;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
215
|
function getDeviceDisplayName(device?: Device | null) {
|
|
160
216
|
return device?.name || device?.localName || null;
|
|
161
217
|
}
|
|
162
218
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
const IOS_REQUEST_MTU = 247;
|
|
220
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
221
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
222
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
166
223
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
170
|
-
);
|
|
171
|
-
}
|
|
224
|
+
const getRequestedBleMtu = () =>
|
|
225
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
172
226
|
|
|
173
|
-
const
|
|
227
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
174
228
|
|
|
175
229
|
const connectOptions: Record<string, unknown> = {
|
|
176
|
-
requestMTU:
|
|
177
|
-
timeout:
|
|
230
|
+
requestMTU: getRequestedBleMtu(),
|
|
231
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
178
232
|
refreshGatt: 'OnConnected',
|
|
179
233
|
};
|
|
180
234
|
|
|
235
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
236
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
237
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
242
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
243
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
244
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
245
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
246
|
+
*/
|
|
247
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
248
|
+
/**
|
|
249
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
250
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
251
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
252
|
+
* budget.
|
|
253
|
+
*/
|
|
254
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
255
|
+
/**
|
|
256
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
257
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
258
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
259
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
260
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
261
|
+
*/
|
|
262
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
263
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
264
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
265
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
266
|
+
export const BLE_SETUP_WEDGED_MESSAGE = 'BLE setup wedged repeatedly';
|
|
267
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
268
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
269
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
270
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
271
|
+
const isWedgedBleSetupError = (error: unknown): boolean =>
|
|
272
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.PollingTimeout &&
|
|
273
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
274
|
+
(error as { message: string }).message.startsWith(BLE_SETUP_WEDGED_MESSAGE);
|
|
275
|
+
const shouldRethrowBleSetupError = (error: unknown): boolean =>
|
|
276
|
+
isConnectTimeoutError(error) || isWedgedBleSetupError(error);
|
|
277
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
278
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
279
|
+
|
|
181
280
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
182
281
|
|
|
183
282
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -189,23 +288,32 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
189
288
|
return infos;
|
|
190
289
|
};
|
|
191
290
|
|
|
192
|
-
const
|
|
193
|
-
|
|
291
|
+
const requestNegotiatedMtu = async (
|
|
292
|
+
device: Device,
|
|
293
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
294
|
+
attempt: number
|
|
295
|
+
) => {
|
|
296
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
194
297
|
|
|
195
298
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
requested: ANDROID_REQUEST_MTU,
|
|
200
|
-
actual: mtuDevice.mtu,
|
|
201
|
-
});
|
|
299
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
300
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
301
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
202
302
|
return mtuDevice;
|
|
203
303
|
} catch (error) {
|
|
204
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
304
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
305
|
+
platform: Platform.OS,
|
|
306
|
+
stage,
|
|
307
|
+
attempt,
|
|
308
|
+
actual: device.mtu,
|
|
309
|
+
error: error instanceof Error ? error.message : String(error),
|
|
310
|
+
});
|
|
205
311
|
return device;
|
|
206
312
|
}
|
|
207
313
|
};
|
|
208
314
|
|
|
315
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
316
|
+
|
|
209
317
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
210
318
|
|
|
211
319
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -245,6 +353,8 @@ export default class ReactNativeBleTransport {
|
|
|
245
353
|
|
|
246
354
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
247
355
|
|
|
356
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
357
|
+
|
|
248
358
|
name = 'ReactNativeBleTransport';
|
|
249
359
|
|
|
250
360
|
configured = false;
|
|
@@ -255,6 +365,8 @@ export default class ReactNativeBleTransport {
|
|
|
255
365
|
|
|
256
366
|
runPromise: Deferred<any> | null = null;
|
|
257
367
|
|
|
368
|
+
private runPromiseDeviceId: string | null = null;
|
|
369
|
+
|
|
258
370
|
emitter?: EventEmitter;
|
|
259
371
|
|
|
260
372
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -262,8 +374,31 @@ export default class ReactNativeBleTransport {
|
|
|
262
374
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
263
375
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
264
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
379
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
380
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
381
|
+
* transport that was never validated.
|
|
382
|
+
*/
|
|
383
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
384
|
+
|
|
385
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
386
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
387
|
+
|
|
388
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
389
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
390
|
+
|
|
265
391
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
266
392
|
|
|
393
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
394
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
395
|
+
|
|
396
|
+
/** Endpoints that answered a V2 probe in this transport lifetime. Survives disconnect. */
|
|
397
|
+
private confirmedProtocolV2 = new Set<string>();
|
|
398
|
+
|
|
399
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
400
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
401
|
+
|
|
267
402
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
268
403
|
|
|
269
404
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -286,15 +421,26 @@ export default class ReactNativeBleTransport {
|
|
|
286
421
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
287
422
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
288
423
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
289
|
-
await this.
|
|
424
|
+
await this.releaseNative(uuid, true);
|
|
290
425
|
}
|
|
291
426
|
},
|
|
292
427
|
});
|
|
293
428
|
|
|
294
429
|
private monitorTokens: Map<string, number> = new Map();
|
|
295
430
|
|
|
431
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
432
|
+
|
|
433
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
434
|
+
|
|
435
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
436
|
+
|
|
437
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
438
|
+
|
|
296
439
|
private nextMonitorToken = 1;
|
|
297
440
|
|
|
441
|
+
/** Serializes transport lifecycle changes for the same physical device. */
|
|
442
|
+
private lifecycleOperations: Map<string, Promise<void>> = new Map();
|
|
443
|
+
|
|
298
444
|
constructor(options: TransportOptions) {
|
|
299
445
|
this.scanTimeout = options.scanTimeout ?? DEVICE_SCAN_TIMEOUT_MS;
|
|
300
446
|
}
|
|
@@ -311,10 +457,19 @@ export default class ReactNativeBleTransport {
|
|
|
311
457
|
}
|
|
312
458
|
|
|
313
459
|
configureProtocolV2(signedData: any) {
|
|
460
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
461
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
314
466
|
this._messagesV2 = parseConfigure(signedData);
|
|
315
|
-
this.
|
|
316
|
-
|
|
317
|
-
.
|
|
467
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
468
|
+
if (isReconfiguration) {
|
|
469
|
+
this.protocolV2Links
|
|
470
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
471
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
472
|
+
}
|
|
318
473
|
}
|
|
319
474
|
|
|
320
475
|
listen() {
|
|
@@ -344,29 +499,15 @@ export default class ReactNativeBleTransport {
|
|
|
344
499
|
}
|
|
345
500
|
}
|
|
346
501
|
|
|
347
|
-
let fallbackServiceUuid: string | undefined;
|
|
348
|
-
|
|
349
502
|
if (!infos) {
|
|
350
503
|
const services = await device.services();
|
|
351
504
|
Log?.debug(
|
|
352
505
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
353
506
|
services?.map(service => service.uuid)
|
|
354
507
|
);
|
|
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
508
|
}
|
|
368
509
|
|
|
369
|
-
if (!infos
|
|
510
|
+
if (!infos) {
|
|
370
511
|
try {
|
|
371
512
|
Log?.debug('cancel connection when service not found');
|
|
372
513
|
await device.cancelConnection();
|
|
@@ -376,9 +517,7 @@ export default class ReactNativeBleTransport {
|
|
|
376
517
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
377
518
|
}
|
|
378
519
|
|
|
379
|
-
const serviceUuid = infos
|
|
380
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
381
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
520
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
382
521
|
|
|
383
522
|
if (!serviceUuid) {
|
|
384
523
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -428,6 +567,7 @@ export default class ReactNativeBleTransport {
|
|
|
428
567
|
|
|
429
568
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
430
569
|
transport.disconnectSubscription?.remove();
|
|
570
|
+
const { monitorToken } = transport;
|
|
431
571
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
432
572
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
433
573
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -437,18 +577,17 @@ export default class ReactNativeBleTransport {
|
|
|
437
577
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
438
578
|
return;
|
|
439
579
|
}
|
|
580
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
581
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
440
584
|
|
|
441
585
|
try {
|
|
442
586
|
Log?.debug('device disconnect: ', device?.id);
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
id: device?.id,
|
|
446
|
-
connectId: device?.id,
|
|
447
|
-
});
|
|
448
|
-
if (this.runPromise) {
|
|
587
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
588
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
449
589
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
450
590
|
this.runPromise.reject(error);
|
|
451
|
-
this.rejectAllProtocolV2Frames(error);
|
|
452
591
|
}
|
|
453
592
|
} catch (e) {
|
|
454
593
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -458,6 +597,22 @@ export default class ReactNativeBleTransport {
|
|
|
458
597
|
});
|
|
459
598
|
}
|
|
460
599
|
|
|
600
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
601
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
605
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
609
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
610
|
+
name,
|
|
611
|
+
id: uuid,
|
|
612
|
+
connectId: uuid,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
461
616
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
462
617
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
463
618
|
try {
|
|
@@ -470,22 +625,21 @@ export default class ReactNativeBleTransport {
|
|
|
470
625
|
const isConnected = await device.isConnected().catch(() => false);
|
|
471
626
|
if (!isConnected) {
|
|
472
627
|
try {
|
|
473
|
-
device = await device.connect(connectOptions);
|
|
628
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
474
629
|
} catch (e) {
|
|
475
630
|
if (
|
|
476
631
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
477
632
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
478
633
|
) {
|
|
479
|
-
device = await device.connect();
|
|
634
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
480
635
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
481
636
|
throw e;
|
|
482
637
|
}
|
|
483
638
|
}
|
|
484
639
|
}
|
|
485
640
|
|
|
486
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
487
|
-
device
|
|
488
|
-
);
|
|
641
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
642
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
489
643
|
|
|
490
644
|
transport.device = device;
|
|
491
645
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -575,10 +729,19 @@ export default class ReactNativeBleTransport {
|
|
|
575
729
|
}
|
|
576
730
|
|
|
577
731
|
const displayName = getDeviceDisplayName(device);
|
|
732
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
733
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
734
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
578
735
|
const isOneKey =
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
736
|
+
!isUnnamedIOSPeripheral &&
|
|
737
|
+
isOnekeyBluetoothDevice({
|
|
738
|
+
id: device?.id,
|
|
739
|
+
name: device?.name,
|
|
740
|
+
localName: device?.localName,
|
|
741
|
+
// The native scan is already restricted to the OneKey communication service,
|
|
742
|
+
// but ble-plx permits the returned advertisement field to be null.
|
|
743
|
+
serviceUuids: device?.serviceUUIDs ?? getBluetoothServiceUuids(),
|
|
744
|
+
});
|
|
582
745
|
if (isOneKey) {
|
|
583
746
|
addDevice(device as unknown as Device);
|
|
584
747
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -595,10 +758,18 @@ export default class ReactNativeBleTransport {
|
|
|
595
758
|
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
596
759
|
devices => {
|
|
597
760
|
for (const device of devices) {
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
761
|
+
const localName =
|
|
762
|
+
'localName' in device && typeof device.localName === 'string'
|
|
763
|
+
? device.localName
|
|
764
|
+
: null;
|
|
765
|
+
if (
|
|
766
|
+
isOnekeyBluetoothDevice({
|
|
767
|
+
id: device.id,
|
|
768
|
+
name: device.name,
|
|
769
|
+
localName,
|
|
770
|
+
serviceUuids: device.serviceUUIDs,
|
|
771
|
+
})
|
|
772
|
+
) {
|
|
602
773
|
Log?.debug('search connected peripheral: ', device.id);
|
|
603
774
|
addDevice(device as unknown as Device);
|
|
604
775
|
}
|
|
@@ -609,10 +780,7 @@ export default class ReactNativeBleTransport {
|
|
|
609
780
|
const addDevice = (device: Device) => {
|
|
610
781
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
611
782
|
const displayName = getDeviceDisplayName(device) ?? 'Unknown BLE Device';
|
|
612
|
-
|
|
613
|
-
if (protocolHint) {
|
|
614
|
-
this.deviceProtocolHints.set(device.id, protocolHint);
|
|
615
|
-
}
|
|
783
|
+
|
|
616
784
|
deviceList.push({
|
|
617
785
|
...device,
|
|
618
786
|
name: displayName,
|
|
@@ -622,7 +790,6 @@ export default class ReactNativeBleTransport {
|
|
|
622
790
|
deviceId: device.id,
|
|
623
791
|
name: displayName,
|
|
624
792
|
serviceUUIDs: device.serviceUUIDs,
|
|
625
|
-
protocolHint,
|
|
626
793
|
});
|
|
627
794
|
}
|
|
628
795
|
};
|
|
@@ -634,32 +801,153 @@ export default class ReactNativeBleTransport {
|
|
|
634
801
|
});
|
|
635
802
|
}
|
|
636
803
|
|
|
637
|
-
async
|
|
638
|
-
|
|
804
|
+
private async installTransportForAcquire(
|
|
805
|
+
uuid: string,
|
|
806
|
+
device: Device,
|
|
807
|
+
characteristics?: ResolvedBleCharacteristics
|
|
808
|
+
) {
|
|
809
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
810
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
811
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
812
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
813
|
+
const monitorToken = this.nextMonitorToken;
|
|
814
|
+
this.nextMonitorToken += 1;
|
|
815
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
816
|
+
transport.monitorToken = monitorToken;
|
|
817
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
818
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
819
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
820
|
+
transport.notifyCharacteristic,
|
|
821
|
+
uuid,
|
|
822
|
+
monitorToken,
|
|
823
|
+
notifyTransactionId
|
|
824
|
+
);
|
|
825
|
+
transportCache[uuid] = transport;
|
|
826
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
827
|
+
this.protocolV2Assemblers.set(
|
|
828
|
+
uuid,
|
|
829
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
if (Platform.OS === 'ios') {
|
|
833
|
+
await new Promise<void>(resolve => {
|
|
834
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
835
|
+
});
|
|
836
|
+
} else if (Platform.OS === 'android') {
|
|
837
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const initialMtu = transport.mtuSize;
|
|
841
|
+
let refreshAttempts = 0;
|
|
842
|
+
if (
|
|
843
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
844
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
845
|
+
) {
|
|
846
|
+
refreshAttempts += 1;
|
|
847
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
848
|
+
transport.device,
|
|
849
|
+
'servicesAndNotifyReady',
|
|
850
|
+
1
|
|
851
|
+
);
|
|
852
|
+
transport.device = refreshedDevice;
|
|
853
|
+
transport.mtuSize =
|
|
854
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
855
|
+
|
|
856
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
857
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
858
|
+
refreshAttempts += 1;
|
|
859
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
860
|
+
transport.device = refreshedDevice;
|
|
861
|
+
transport.mtuSize =
|
|
862
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
867
|
+
platform: Platform.OS,
|
|
868
|
+
requested: getRequestedBleMtu(),
|
|
869
|
+
initial: initialMtu,
|
|
870
|
+
actual: transport.mtuSize,
|
|
871
|
+
refreshAttempts,
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return transport;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async acquire(input: FirmwareInstallBleAcquireInput) {
|
|
878
|
+
const { uuid } = input;
|
|
639
879
|
|
|
640
880
|
if (!uuid) {
|
|
641
881
|
throw ERRORS.TypedError(HardwareErrorCode.BleRequiredUUID);
|
|
642
882
|
}
|
|
643
883
|
|
|
884
|
+
return this.runLifecycleOperation(uuid, () => this.acquireUnlocked(input));
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private async acquireUnlocked(input: FirmwareInstallBleAcquireInput) {
|
|
888
|
+
const { uuid, forceCleanRunPromise, expectedProtocol, skipProtocolProbe } = input;
|
|
889
|
+
|
|
644
890
|
const cachedTransport = transportCache[uuid];
|
|
891
|
+
if (skipProtocolProbe && !cachedTransport && this.blePlxManager) {
|
|
892
|
+
Log?.debug(
|
|
893
|
+
'[ReactNativeBleTransport] refresh uncached BLE connection for firmware install:',
|
|
894
|
+
uuid
|
|
895
|
+
);
|
|
896
|
+
const manager = this.blePlxManager;
|
|
897
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
898
|
+
await this.runBestEffortNativeOperation(
|
|
899
|
+
'firmware install reconnect: cancel uncached device connection',
|
|
900
|
+
() => manager.cancelDeviceConnection(uuid)
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
645
904
|
if (cachedTransport) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
905
|
+
if (skipProtocolProbe) {
|
|
906
|
+
Log?.debug(
|
|
907
|
+
'[ReactNativeBleTransport] refresh cached BLE connection for firmware install:',
|
|
908
|
+
uuid
|
|
909
|
+
);
|
|
910
|
+
const manager = this.blePlxManager;
|
|
911
|
+
await this.releaseUnlocked(uuid, true);
|
|
912
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
913
|
+
const operations: Promise<unknown>[] = [];
|
|
914
|
+
if (manager) {
|
|
915
|
+
operations.push(
|
|
916
|
+
this.runBestEffortNativeOperation(
|
|
917
|
+
'firmware install reconnect: cancel device connection',
|
|
918
|
+
() => manager.cancelDeviceConnection(uuid)
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
operations.push(
|
|
923
|
+
this.runBestEffortNativeOperation(
|
|
924
|
+
'firmware install reconnect: device cancel connection',
|
|
925
|
+
() => cachedTransport.device.cancelConnection()
|
|
926
|
+
)
|
|
927
|
+
);
|
|
928
|
+
await Promise.all(operations);
|
|
929
|
+
});
|
|
930
|
+
} else {
|
|
931
|
+
const cachedProtocol = this.deviceProtocol.get(uuid);
|
|
932
|
+
const isCachedDeviceConnected = await cachedTransport.device
|
|
933
|
+
.isConnected()
|
|
934
|
+
.catch(() => false);
|
|
935
|
+
if (
|
|
936
|
+
isCachedDeviceConnected &&
|
|
937
|
+
cachedProtocol &&
|
|
938
|
+
(!expectedProtocol || cachedProtocol === expectedProtocol)
|
|
939
|
+
) {
|
|
940
|
+
Log?.debug('[ReactNativeBleTransport] reuse cached BLE transport:', uuid, cachedProtocol);
|
|
941
|
+
return { uuid, protocolType: cachedProtocol };
|
|
942
|
+
}
|
|
656
943
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
944
|
+
/**
|
|
945
|
+
* If the transport is not reusable due to a protocol mismatch or stale
|
|
946
|
+
* connection, clean it up before creating a new transport instance.
|
|
947
|
+
*/
|
|
948
|
+
Log?.debug('transport not reusable, will release: ', uuid);
|
|
949
|
+
await this.releaseUnlocked(uuid, true);
|
|
950
|
+
}
|
|
663
951
|
}
|
|
664
952
|
|
|
665
953
|
let device: Device | null = null;
|
|
@@ -667,8 +955,8 @@ export default class ReactNativeBleTransport {
|
|
|
667
955
|
if (forceCleanRunPromise && this.runPromise) {
|
|
668
956
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
669
957
|
this.runPromise.reject(error);
|
|
670
|
-
this.rejectAllProtocolV2Frames(error);
|
|
671
958
|
this.runPromise = null;
|
|
959
|
+
this.runPromiseDeviceId = null;
|
|
672
960
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
673
961
|
}
|
|
674
962
|
|
|
@@ -704,15 +992,22 @@ export default class ReactNativeBleTransport {
|
|
|
704
992
|
if (!device) {
|
|
705
993
|
Log?.debug('try to connect to device: ', uuid);
|
|
706
994
|
try {
|
|
707
|
-
device = await
|
|
995
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
996
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
997
|
+
);
|
|
708
998
|
} catch (e) {
|
|
709
999
|
Log?.debug('try to connect to device has error: ', e);
|
|
1000
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
1001
|
+
throw e;
|
|
1002
|
+
}
|
|
710
1003
|
if (
|
|
711
1004
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
712
1005
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
713
1006
|
) {
|
|
714
1007
|
Log?.debug('first try to reconnect without params');
|
|
715
|
-
device = await
|
|
1008
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1009
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
1010
|
+
);
|
|
716
1011
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
717
1012
|
Log?.debug('device already connected');
|
|
718
1013
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -728,26 +1023,36 @@ export default class ReactNativeBleTransport {
|
|
|
728
1023
|
|
|
729
1024
|
if (!(await device.isConnected())) {
|
|
730
1025
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
1026
|
+
const disconnectedDevice = device;
|
|
731
1027
|
|
|
732
1028
|
try {
|
|
733
|
-
device = await
|
|
1029
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1030
|
+
disconnectedDevice.connect(connectOptions)
|
|
1031
|
+
);
|
|
734
1032
|
} catch (e) {
|
|
735
1033
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
1034
|
+
if (shouldRethrowBleSetupError(e)) {
|
|
1035
|
+
throw e;
|
|
1036
|
+
}
|
|
736
1037
|
if (
|
|
737
1038
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
738
1039
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
739
1040
|
) {
|
|
740
1041
|
Log?.debug('second try to reconnect without params');
|
|
741
1042
|
try {
|
|
742
|
-
device = await
|
|
1043
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1044
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1045
|
+
);
|
|
743
1046
|
} catch (e) {
|
|
744
1047
|
Log?.debug('last try to reconnect error: ', e);
|
|
745
1048
|
// last try to reconnect device if this issue exists
|
|
746
1049
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
747
1050
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
748
1051
|
Log?.debug('last try to reconnect');
|
|
749
|
-
await
|
|
750
|
-
device = await
|
|
1052
|
+
await disconnectedDevice.cancelConnection();
|
|
1053
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
1054
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1055
|
+
);
|
|
751
1056
|
}
|
|
752
1057
|
}
|
|
753
1058
|
} else {
|
|
@@ -756,59 +1061,78 @@ export default class ReactNativeBleTransport {
|
|
|
756
1061
|
}
|
|
757
1062
|
}
|
|
758
1063
|
|
|
759
|
-
device = await
|
|
760
|
-
const
|
|
1064
|
+
device = await resolveNegotiatedMtu(device);
|
|
1065
|
+
const acquiredDevice = device;
|
|
1066
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1067
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
761
1068
|
|
|
762
1069
|
const protocolHint = expectedProtocol
|
|
763
1070
|
? undefined
|
|
764
|
-
: this.deviceProtocolHints.get(uuid)
|
|
765
|
-
inferProtocolHintFromDeviceName(getDeviceDisplayName(device));
|
|
1071
|
+
: input.protocolHint ?? this.deviceProtocolHints.get(uuid);
|
|
766
1072
|
|
|
767
1073
|
// release transport before new transport instance
|
|
768
|
-
await this.
|
|
1074
|
+
await this.releaseUnlocked(uuid, true);
|
|
769
1075
|
if (protocolHint) {
|
|
770
1076
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
771
1077
|
}
|
|
772
1078
|
|
|
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,
|
|
1079
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1080
|
+
writeCharacteristic,
|
|
1081
|
+
notifyCharacteristic,
|
|
807
1082
|
});
|
|
808
1083
|
|
|
809
|
-
|
|
1084
|
+
try {
|
|
1085
|
+
if (skipProtocolProbe) {
|
|
1086
|
+
if (!expectedProtocol) {
|
|
1087
|
+
throw ERRORS.TypedError(
|
|
1088
|
+
HardwareErrorCode.RuntimeError,
|
|
1089
|
+
'skipProtocolProbe requires an expected BLE protocol'
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
if (this.sessionProtocols.get(uuid) !== expectedProtocol) {
|
|
1093
|
+
throw ERRORS.TypedError(
|
|
1094
|
+
HardwareErrorCode.RuntimeError,
|
|
1095
|
+
'skipProtocolProbe requires a previously confirmed protocol for this BLE endpoint'
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1099
|
+
this.sessionProtocols.set(uuid, expectedProtocol);
|
|
1100
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1101
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected without probe', {
|
|
1102
|
+
deviceId: uuid,
|
|
1103
|
+
protocol: expectedProtocol,
|
|
1104
|
+
source: 'firmware-install-reconnect',
|
|
1105
|
+
});
|
|
1106
|
+
const currentTransport = transportCache[uuid];
|
|
1107
|
+
if (!currentTransport) {
|
|
1108
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1109
|
+
}
|
|
1110
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1111
|
+
return { uuid, protocolType: expectedProtocol };
|
|
1112
|
+
}
|
|
810
1113
|
|
|
811
|
-
|
|
1114
|
+
const protocolType = await this.detectProtocol(
|
|
1115
|
+
uuid,
|
|
1116
|
+
expectedProtocol,
|
|
1117
|
+
protocolHint,
|
|
1118
|
+
async () => {
|
|
1119
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1120
|
+
}
|
|
1121
|
+
);
|
|
1122
|
+
const currentTransport = transportCache[uuid];
|
|
1123
|
+
if (!currentTransport) {
|
|
1124
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1125
|
+
}
|
|
1126
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1127
|
+
return { uuid, protocolType };
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
if ((error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleDeviceBondError) {
|
|
1130
|
+
await this.disconnectUnlocked(uuid);
|
|
1131
|
+
} else {
|
|
1132
|
+
await this.releaseUnlocked(uuid, true);
|
|
1133
|
+
}
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
812
1136
|
}
|
|
813
1137
|
|
|
814
1138
|
_monitorCharacteristic(
|
|
@@ -835,7 +1159,7 @@ export default class ReactNativeBleTransport {
|
|
|
835
1159
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
836
1160
|
return;
|
|
837
1161
|
}
|
|
838
|
-
if (this.
|
|
1162
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
839
1163
|
let errorCode:
|
|
840
1164
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
841
1165
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -858,7 +1182,7 @@ export default class ReactNativeBleTransport {
|
|
|
858
1182
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
859
1183
|
return;
|
|
860
1184
|
}
|
|
861
|
-
if (this.runPromise) {
|
|
1185
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
862
1186
|
let ERROR:
|
|
863
1187
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
864
1188
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -881,7 +1205,6 @@ export default class ReactNativeBleTransport {
|
|
|
881
1205
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
882
1206
|
);
|
|
883
1207
|
this.runPromise.reject(notifyError);
|
|
884
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
885
1208
|
Log?.debug(
|
|
886
1209
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
887
1210
|
);
|
|
@@ -889,7 +1212,6 @@ export default class ReactNativeBleTransport {
|
|
|
889
1212
|
}
|
|
890
1213
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
891
1214
|
this.runPromise.reject(notifyError);
|
|
892
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
893
1215
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
894
1216
|
}
|
|
895
1217
|
|
|
@@ -907,7 +1229,7 @@ export default class ReactNativeBleTransport {
|
|
|
907
1229
|
|
|
908
1230
|
try {
|
|
909
1231
|
const data = Buffer.from(c.value as string, 'base64');
|
|
910
|
-
const protocol = this.
|
|
1232
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
911
1233
|
if (!protocol) {
|
|
912
1234
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
913
1235
|
return;
|
|
@@ -934,14 +1256,16 @@ export default class ReactNativeBleTransport {
|
|
|
934
1256
|
// );
|
|
935
1257
|
bufferLength = 0;
|
|
936
1258
|
buffer = [];
|
|
937
|
-
this.
|
|
1259
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1260
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1261
|
+
}
|
|
938
1262
|
}
|
|
939
1263
|
} catch (error) {
|
|
940
1264
|
Log?.debug('monitor data error: ', error);
|
|
941
1265
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
942
|
-
if (this.
|
|
1266
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
943
1267
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
944
|
-
} else {
|
|
1268
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
945
1269
|
this.runPromise?.reject(notifyError);
|
|
946
1270
|
}
|
|
947
1271
|
}
|
|
@@ -951,13 +1275,23 @@ export default class ReactNativeBleTransport {
|
|
|
951
1275
|
}
|
|
952
1276
|
|
|
953
1277
|
async release(uuid: string, onclose = false) {
|
|
954
|
-
|
|
1278
|
+
return this.runLifecycleOperation(uuid, () => this.releaseUnlocked(uuid, onclose));
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
private async releaseUnlocked(uuid: string, onclose = false) {
|
|
955
1282
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
956
|
-
|
|
1283
|
+
return this.releaseNative(uuid, onclose);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
1287
|
+
const transport = transportCache[uuid];
|
|
1288
|
+
const manager = this.blePlxManager;
|
|
1289
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
957
1290
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
958
1291
|
this.runPromise.reject(error);
|
|
959
1292
|
this.runPromise = null;
|
|
960
|
-
this.
|
|
1293
|
+
this.runPromiseDeviceId = null;
|
|
1294
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
961
1295
|
} else {
|
|
962
1296
|
this.resetProtocolV2Frames(uuid);
|
|
963
1297
|
}
|
|
@@ -985,34 +1319,56 @@ export default class ReactNativeBleTransport {
|
|
|
985
1319
|
);
|
|
986
1320
|
transport.notifySubscription?.remove();
|
|
987
1321
|
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
|
-
}
|
|
1322
|
+
if (transportCache[uuid] === transport) {
|
|
1323
|
+
delete transportCache[uuid];
|
|
995
1324
|
}
|
|
996
|
-
|
|
997
|
-
delete transportCache[uuid];
|
|
998
1325
|
}
|
|
999
1326
|
|
|
1327
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1328
|
+
|
|
1000
1329
|
this.deviceProtocol.delete(uuid);
|
|
1001
|
-
|
|
1330
|
+
this.probingProtocols.delete(uuid);
|
|
1331
|
+
// Confirmed protocol and caller hints stay in deviceProtocol / protocolHint.
|
|
1002
1332
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1003
1333
|
this.protocolV2Assemblers.delete(uuid);
|
|
1004
1334
|
this.resetProtocolV2Frames(uuid);
|
|
1005
1335
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1336
|
+
await this.runNativeTeardown(uuid, manager, async () => {
|
|
1337
|
+
const operations: Promise<unknown>[] = [
|
|
1338
|
+
this.runBestEffortNativeOperation('release: restore connection priority', () =>
|
|
1339
|
+
this.restoreAndroidConnectionPriority(uuid, transport)
|
|
1340
|
+
),
|
|
1341
|
+
];
|
|
1342
|
+
if (transport?.notifyTransactionId && manager) {
|
|
1343
|
+
operations.push(
|
|
1344
|
+
this.runBestEffortNativeOperation('release: cancel notify transaction', () =>
|
|
1345
|
+
manager.cancelTransaction(transport.notifyTransactionId as string)
|
|
1346
|
+
)
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
if (manager) {
|
|
1350
|
+
operations.push(
|
|
1351
|
+
this.runBestEffortNativeOperation('release: cancel transaction', () =>
|
|
1352
|
+
manager.cancelTransaction(uuid)
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
await Promise.all(operations);
|
|
1357
|
+
});
|
|
1011
1358
|
|
|
1012
1359
|
return Promise.resolve(true);
|
|
1013
1360
|
}
|
|
1014
1361
|
|
|
1015
1362
|
async post(session: string, name: string, data: Record<string, unknown>) {
|
|
1363
|
+
if (this.getProtocolType(session) === 'V2') {
|
|
1364
|
+
await this.protocolV2Links.sendFlowControl(
|
|
1365
|
+
session,
|
|
1366
|
+
() => this.createProtocolV2Adapter(session),
|
|
1367
|
+
name,
|
|
1368
|
+
data
|
|
1369
|
+
);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1016
1372
|
await this.call(session, name, data);
|
|
1017
1373
|
}
|
|
1018
1374
|
|
|
@@ -1037,8 +1393,6 @@ export default class ReactNativeBleTransport {
|
|
|
1037
1393
|
`Device protocol has not been detected for ${uuid}`
|
|
1038
1394
|
);
|
|
1039
1395
|
}
|
|
1040
|
-
Log?.debug('transport call', createTransportCallLog(name, protocol, data));
|
|
1041
|
-
|
|
1042
1396
|
if (protocol === 'V2') {
|
|
1043
1397
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1044
1398
|
}
|
|
@@ -1064,7 +1418,25 @@ export default class ReactNativeBleTransport {
|
|
|
1064
1418
|
const transport = this.getCachedTransport(uuid);
|
|
1065
1419
|
const runPromise = createDeferred<string>();
|
|
1066
1420
|
runPromise.promise.catch(() => undefined);
|
|
1421
|
+
const supersededRunPromise = this.runPromise;
|
|
1422
|
+
if (supersededRunPromise) {
|
|
1423
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1424
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1425
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1426
|
+
// tear down the shared connection while another call is using it.
|
|
1427
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1428
|
+
}
|
|
1067
1429
|
this.runPromise = runPromise;
|
|
1430
|
+
this.runPromiseDeviceId = uuid;
|
|
1431
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1432
|
+
// only the call that still owns the slot may release it.
|
|
1433
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1434
|
+
if (this.runPromise === runPromise) {
|
|
1435
|
+
this.runPromise = null;
|
|
1436
|
+
this.runPromiseDeviceId = null;
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1068
1440
|
const messages = this._messages;
|
|
1069
1441
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1070
1442
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1090,6 +1462,9 @@ export default class ReactNativeBleTransport {
|
|
|
1090
1462
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1091
1463
|
} catch (e) {
|
|
1092
1464
|
onError(e);
|
|
1465
|
+
if (isWedgedWriteError(e)) {
|
|
1466
|
+
throw e;
|
|
1467
|
+
}
|
|
1093
1468
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1094
1469
|
}
|
|
1095
1470
|
}
|
|
@@ -1121,6 +1496,9 @@ export default class ReactNativeBleTransport {
|
|
|
1121
1496
|
}
|
|
1122
1497
|
} catch (e) {
|
|
1123
1498
|
onError(e);
|
|
1499
|
+
if (isWedgedWriteError(e)) {
|
|
1500
|
+
throw e;
|
|
1501
|
+
}
|
|
1124
1502
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1125
1503
|
}
|
|
1126
1504
|
}
|
|
@@ -1134,9 +1512,15 @@ export default class ReactNativeBleTransport {
|
|
|
1134
1512
|
if (name === 'EmmcFileWrite') {
|
|
1135
1513
|
await writeChunkedData(
|
|
1136
1514
|
buffers,
|
|
1137
|
-
data =>
|
|
1515
|
+
data =>
|
|
1516
|
+
this.writeBlePacket(
|
|
1517
|
+
uuid,
|
|
1518
|
+
data,
|
|
1519
|
+
payload => transport.writeWithRetry(payload),
|
|
1520
|
+
isCurrentOwner
|
|
1521
|
+
),
|
|
1138
1522
|
e => {
|
|
1139
|
-
|
|
1523
|
+
releaseOwnershipIfCurrent();
|
|
1140
1524
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1141
1525
|
}
|
|
1142
1526
|
);
|
|
@@ -1157,43 +1541,31 @@ export default class ReactNativeBleTransport {
|
|
|
1157
1541
|
// eslint-disable-next-line no-constant-condition
|
|
1158
1542
|
while (true) {
|
|
1159
1543
|
try {
|
|
1160
|
-
await
|
|
1544
|
+
await this.writeBlePacket(
|
|
1545
|
+
uuid,
|
|
1546
|
+
data,
|
|
1547
|
+
payload => transport.writeWithRetry(payload),
|
|
1548
|
+
isCurrentOwner
|
|
1549
|
+
);
|
|
1161
1550
|
return;
|
|
1162
1551
|
} catch (error) {
|
|
1163
1552
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1164
1553
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1165
1554
|
throw error;
|
|
1166
1555
|
}
|
|
1167
|
-
const
|
|
1168
|
-
const delayMs = shouldReconnect
|
|
1169
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1170
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1556
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1171
1557
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1172
1558
|
attempt: attempt + 1,
|
|
1173
1559
|
delayMs,
|
|
1174
|
-
reconnect: shouldReconnect,
|
|
1175
1560
|
error,
|
|
1176
1561
|
});
|
|
1177
|
-
if (shouldReconnect) {
|
|
1178
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1179
|
-
}
|
|
1180
1562
|
await delay(delayMs);
|
|
1181
1563
|
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
1564
|
}
|
|
1193
1565
|
}
|
|
1194
1566
|
},
|
|
1195
1567
|
e => {
|
|
1196
|
-
|
|
1568
|
+
releaseOwnershipIfCurrent();
|
|
1197
1569
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1198
1570
|
}
|
|
1199
1571
|
);
|
|
@@ -1202,10 +1574,23 @@ export default class ReactNativeBleTransport {
|
|
|
1202
1574
|
const outData = o.toString('base64');
|
|
1203
1575
|
// Upload resources on low-end phones may OOM
|
|
1204
1576
|
try {
|
|
1205
|
-
|
|
1577
|
+
const shouldUseWriteWithResponse =
|
|
1578
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1579
|
+
await this.writeBlePacket(
|
|
1580
|
+
uuid,
|
|
1581
|
+
outData,
|
|
1582
|
+
payload =>
|
|
1583
|
+
shouldUseWriteWithResponse
|
|
1584
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1585
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1586
|
+
isCurrentOwner
|
|
1587
|
+
);
|
|
1206
1588
|
} catch (e) {
|
|
1207
1589
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1208
|
-
|
|
1590
|
+
releaseOwnershipIfCurrent();
|
|
1591
|
+
if (isWedgedWriteError(e)) {
|
|
1592
|
+
throw e;
|
|
1593
|
+
}
|
|
1209
1594
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1210
1595
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1211
1596
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1241,16 +1626,30 @@ export default class ReactNativeBleTransport {
|
|
|
1241
1626
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1242
1627
|
return check.call(jsonData);
|
|
1243
1628
|
} catch (e) {
|
|
1244
|
-
if (name === '
|
|
1245
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
1629
|
+
if (name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS) {
|
|
1630
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe call failed:', e);
|
|
1246
1631
|
} else {
|
|
1247
1632
|
Log?.error('call error: ', e);
|
|
1248
1633
|
}
|
|
1634
|
+
const isProbeTimeout =
|
|
1635
|
+
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1636
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1637
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1638
|
+
// call is actively using.
|
|
1639
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1640
|
+
if (
|
|
1641
|
+
!isProbeTimeout &&
|
|
1642
|
+
!isStaleCall &&
|
|
1643
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1644
|
+
) {
|
|
1645
|
+
await this.disconnect(uuid);
|
|
1646
|
+
}
|
|
1249
1647
|
throw e;
|
|
1250
1648
|
} finally {
|
|
1251
1649
|
if (timeout) clearTimeout(timeout);
|
|
1252
1650
|
if (this.runPromise === runPromise) {
|
|
1253
1651
|
this.runPromise = null;
|
|
1652
|
+
this.runPromiseDeviceId = null;
|
|
1254
1653
|
}
|
|
1255
1654
|
}
|
|
1256
1655
|
}
|
|
@@ -1260,8 +1659,14 @@ export default class ReactNativeBleTransport {
|
|
|
1260
1659
|
}
|
|
1261
1660
|
|
|
1262
1661
|
async disconnect(session: string) {
|
|
1662
|
+
return this.runLifecycleOperation(session, () => this.disconnectUnlocked(session));
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
private async disconnectUnlocked(session: string) {
|
|
1263
1666
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1264
1667
|
const transport = transportCache[session];
|
|
1668
|
+
const manager = this.blePlxManager;
|
|
1669
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1265
1670
|
|
|
1266
1671
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1267
1672
|
// from being triggered when we cancel the connection below
|
|
@@ -1289,60 +1694,253 @@ export default class ReactNativeBleTransport {
|
|
|
1289
1694
|
}
|
|
1290
1695
|
}
|
|
1291
1696
|
|
|
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
1697
|
// clear the transport cache
|
|
1318
|
-
if (transportCache[session]) {
|
|
1698
|
+
if (!transport || transportCache[session] === transport) {
|
|
1319
1699
|
delete transportCache[session];
|
|
1320
1700
|
}
|
|
1321
1701
|
this.deviceProtocol.delete(session);
|
|
1702
|
+
this.probingProtocols.delete(session);
|
|
1322
1703
|
this.deviceProtocolHints.delete(session);
|
|
1704
|
+
this.sessionProtocols.delete(session);
|
|
1705
|
+
this.protocolReprobeFailures.delete(session);
|
|
1323
1706
|
this.protocolV2Assemblers.delete(session);
|
|
1324
1707
|
this.resetProtocolV2Frames(session);
|
|
1325
1708
|
|
|
1326
1709
|
// emit the disconnect event
|
|
1327
1710
|
try {
|
|
1328
|
-
this.
|
|
1329
|
-
name: transport?.device?.name,
|
|
1330
|
-
id: session,
|
|
1331
|
-
connectId: session,
|
|
1332
|
-
});
|
|
1711
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1333
1712
|
} catch (e) {
|
|
1334
1713
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1335
1714
|
}
|
|
1715
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1716
|
+
this.monitorTokens.delete(session);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
await this.runNativeTeardown(session, manager, async () => {
|
|
1720
|
+
const operations: Promise<unknown>[] = [];
|
|
1721
|
+
if (manager) {
|
|
1722
|
+
operations.push(
|
|
1723
|
+
this.runBestEffortNativeOperation('disconnect: cancel transaction', () =>
|
|
1724
|
+
manager.cancelTransaction(session)
|
|
1725
|
+
)
|
|
1726
|
+
);
|
|
1727
|
+
operations.push(
|
|
1728
|
+
this.runBestEffortNativeOperation('disconnect: cancel device connection', () =>
|
|
1729
|
+
manager.cancelDeviceConnection(session)
|
|
1730
|
+
)
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
if (transport?.device) {
|
|
1734
|
+
operations.push(
|
|
1735
|
+
this.runBestEffortNativeOperation('disconnect: device cancel connection', () =>
|
|
1736
|
+
transport.device.cancelConnection()
|
|
1737
|
+
)
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
await Promise.all(operations);
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1336
1743
|
// eslint-disable-next-line no-promise-executor-return
|
|
1337
1744
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1338
1745
|
}
|
|
1339
1746
|
|
|
1747
|
+
private async runNativeTeardown(
|
|
1748
|
+
uuid: string,
|
|
1749
|
+
manager: BlePlxManager | undefined,
|
|
1750
|
+
teardown: () => Promise<void>
|
|
1751
|
+
) {
|
|
1752
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1753
|
+
let timedOut = false;
|
|
1754
|
+
const pending = Promise.resolve()
|
|
1755
|
+
.then(teardown)
|
|
1756
|
+
.catch(error => {
|
|
1757
|
+
Log?.debug('BLE native teardown error (ignored): ', error?.message || error);
|
|
1758
|
+
});
|
|
1759
|
+
try {
|
|
1760
|
+
await Promise.race([
|
|
1761
|
+
pending,
|
|
1762
|
+
new Promise<void>(resolve => {
|
|
1763
|
+
timer = setTimeout(() => {
|
|
1764
|
+
timedOut = true;
|
|
1765
|
+
resolve();
|
|
1766
|
+
}, BLE_NATIVE_TEARDOWN_TIMEOUT_MS);
|
|
1767
|
+
}),
|
|
1768
|
+
]);
|
|
1769
|
+
} finally {
|
|
1770
|
+
if (timer) clearTimeout(timer);
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (timedOut) {
|
|
1774
|
+
Log?.error('[ReactNativeBleTransport] BLE native teardown timed out:', uuid);
|
|
1775
|
+
if (this.blePlxManager === manager) {
|
|
1776
|
+
this.resetPlxManager();
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private runBestEffortNativeOperation(label: string, operation: () => Promise<unknown>) {
|
|
1782
|
+
return Promise.resolve()
|
|
1783
|
+
.then(operation)
|
|
1784
|
+
.catch(error => {
|
|
1785
|
+
Log?.debug(`${label} error (ignored): `, error?.message || error);
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
private async runLifecycleOperation<T>(uuid: string, operation: () => Promise<T>): Promise<T> {
|
|
1790
|
+
const previousOperation = this.lifecycleOperations.get(uuid) ?? Promise.resolve();
|
|
1791
|
+
let completeOperation!: () => void;
|
|
1792
|
+
const operationGate = new Promise<void>(resolve => {
|
|
1793
|
+
completeOperation = resolve;
|
|
1794
|
+
});
|
|
1795
|
+
const operationTail = previousOperation.catch(() => undefined).then(() => operationGate);
|
|
1796
|
+
this.lifecycleOperations.set(uuid, operationTail);
|
|
1797
|
+
|
|
1798
|
+
await previousOperation.catch(() => undefined);
|
|
1799
|
+
try {
|
|
1800
|
+
return await operation();
|
|
1801
|
+
} finally {
|
|
1802
|
+
completeOperation();
|
|
1803
|
+
if (this.lifecycleOperations.get(uuid) === operationTail) {
|
|
1804
|
+
this.lifecycleOperations.delete(uuid);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1340
1809
|
cancel() {
|
|
1341
1810
|
Log?.debug('transport-react-native transport cancel');
|
|
1342
1811
|
if (this.runPromise) {
|
|
1343
1812
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1344
1813
|
}
|
|
1345
1814
|
this.runPromise = null;
|
|
1815
|
+
this.runPromiseDeviceId = null;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1819
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1820
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1821
|
+
let timedOut = false;
|
|
1822
|
+
const pending = connect();
|
|
1823
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1824
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1825
|
+
pending.catch(() => undefined);
|
|
1826
|
+
try {
|
|
1827
|
+
const result = await Promise.race([
|
|
1828
|
+
pending,
|
|
1829
|
+
new Promise<never>((_, reject) => {
|
|
1830
|
+
timer = setTimeout(() => {
|
|
1831
|
+
timedOut = true;
|
|
1832
|
+
reject(
|
|
1833
|
+
ERRORS.TypedError(
|
|
1834
|
+
HardwareErrorCode.BleConnectedError,
|
|
1835
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1836
|
+
)
|
|
1837
|
+
);
|
|
1838
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1839
|
+
}),
|
|
1840
|
+
]);
|
|
1841
|
+
return result;
|
|
1842
|
+
} catch (error) {
|
|
1843
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1844
|
+
const resetManager = this.abandonStalledConnection(
|
|
1845
|
+
uuid,
|
|
1846
|
+
timedOut ? 'connect-backstop' : 'connect-native'
|
|
1847
|
+
);
|
|
1848
|
+
if (resetManager) {
|
|
1849
|
+
throw this.createWedgedBleSetupError();
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
throw error;
|
|
1853
|
+
} finally {
|
|
1854
|
+
if (timer) clearTimeout(timer);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1859
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1860
|
+
uuid: string,
|
|
1861
|
+
device: Device
|
|
1862
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1863
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1864
|
+
let timedOut = false;
|
|
1865
|
+
const pending = this.resolveCharacteristics(device);
|
|
1866
|
+
pending.catch(() => undefined);
|
|
1867
|
+
try {
|
|
1868
|
+
const result = await Promise.race([
|
|
1869
|
+
pending,
|
|
1870
|
+
new Promise<never>((_, reject) => {
|
|
1871
|
+
timer = setTimeout(() => {
|
|
1872
|
+
timedOut = true;
|
|
1873
|
+
reject(
|
|
1874
|
+
ERRORS.TypedError(
|
|
1875
|
+
HardwareErrorCode.BleConnectedError,
|
|
1876
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1877
|
+
)
|
|
1878
|
+
);
|
|
1879
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1880
|
+
}),
|
|
1881
|
+
]);
|
|
1882
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1883
|
+
return result;
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1886
|
+
const resetManager = this.abandonStalledConnection(
|
|
1887
|
+
uuid,
|
|
1888
|
+
timedOut ? 'gatt-backstop' : 'gatt-native'
|
|
1889
|
+
);
|
|
1890
|
+
if (resetManager) {
|
|
1891
|
+
throw this.createWedgedBleSetupError();
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
throw error;
|
|
1895
|
+
} finally {
|
|
1896
|
+
if (timer) clearTimeout(timer);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
/**
|
|
1901
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1902
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1903
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1904
|
+
* Returns true when the manager itself was reset so the caller can stop Core retries.
|
|
1905
|
+
*/
|
|
1906
|
+
private abandonStalledConnection(
|
|
1907
|
+
uuid: string,
|
|
1908
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1909
|
+
): boolean {
|
|
1910
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1911
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1912
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1913
|
+
stage,
|
|
1914
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1918
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1919
|
+
});
|
|
1920
|
+
const stalled = transportCache[uuid];
|
|
1921
|
+
if (stalled) {
|
|
1922
|
+
delete transportCache[uuid];
|
|
1923
|
+
}
|
|
1924
|
+
this.deviceProtocol.delete(uuid);
|
|
1925
|
+
this.probingProtocols.delete(uuid);
|
|
1926
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1927
|
+
this.resetProtocolV2Frames(uuid);
|
|
1928
|
+
|
|
1929
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1930
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1931
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1932
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1933
|
+
this.resetPlxManager();
|
|
1934
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
private createWedgedBleSetupError() {
|
|
1941
|
+
// PollingTimeout is not retried by connectDeviceForBle and already maps
|
|
1942
|
+
// to the App "connection failed" help text.
|
|
1943
|
+
return ERRORS.TypedError(HardwareErrorCode.PollingTimeout, BLE_SETUP_WEDGED_MESSAGE);
|
|
1346
1944
|
}
|
|
1347
1945
|
|
|
1348
1946
|
private getCachedTransport(uuid: string) {
|
|
@@ -1353,9 +1951,144 @@ export default class ReactNativeBleTransport {
|
|
|
1353
1951
|
return transport;
|
|
1354
1952
|
}
|
|
1355
1953
|
|
|
1356
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1956
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1957
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1958
|
+
* cached and every later call would hang on it again.
|
|
1959
|
+
*/
|
|
1960
|
+
private async writeBlePacket(
|
|
1961
|
+
uuid: string,
|
|
1962
|
+
data: string,
|
|
1963
|
+
write: (payload: string) => Promise<unknown>,
|
|
1964
|
+
isCurrentOwner?: () => boolean
|
|
1965
|
+
) {
|
|
1966
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1967
|
+
let timedOut = false;
|
|
1968
|
+
try {
|
|
1969
|
+
await Promise.race([
|
|
1970
|
+
write(data),
|
|
1971
|
+
new Promise<never>((_, reject) => {
|
|
1972
|
+
timer = setTimeout(() => {
|
|
1973
|
+
timedOut = true;
|
|
1974
|
+
reject(
|
|
1975
|
+
ERRORS.TypedError(
|
|
1976
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1977
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1978
|
+
)
|
|
1979
|
+
);
|
|
1980
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1981
|
+
}),
|
|
1982
|
+
]);
|
|
1983
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
if (timedOut) {
|
|
1986
|
+
// A superseded call's late write must not tear down the link the current
|
|
1987
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1988
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1989
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1990
|
+
} else {
|
|
1991
|
+
this.tearDownWedgedLink(uuid);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
throw error;
|
|
1995
|
+
} finally {
|
|
1996
|
+
if (timer) clearTimeout(timer);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
2002
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
2003
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
2004
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
2005
|
+
*/
|
|
2006
|
+
private tearDownWedgedLink(uuid: string) {
|
|
2007
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
2008
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
2009
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
2010
|
+
consecutiveWriteTimeouts: timeouts,
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
const wedged = transportCache[uuid];
|
|
2014
|
+
this.disconnect(uuid).catch(error => {
|
|
2015
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
2016
|
+
});
|
|
2017
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
2018
|
+
delete transportCache[uuid];
|
|
2019
|
+
}
|
|
2020
|
+
this.deviceProtocol.delete(uuid);
|
|
2021
|
+
this.probingProtocols.delete(uuid);
|
|
2022
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
2023
|
+
this.resetProtocolV2Frames(uuid);
|
|
2024
|
+
|
|
2025
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
2026
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
2027
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
2028
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
2029
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
2030
|
+
this.resetPlxManager();
|
|
2031
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
private resetPlxManager() {
|
|
2036
|
+
const manager = this.blePlxManager;
|
|
2037
|
+
this.blePlxManager = undefined;
|
|
2038
|
+
const reason = 'React Native BLE manager reset';
|
|
2039
|
+
// Destroying the shared manager invalidates every peripheral it owns. Notify
|
|
2040
|
+
// each cached session before clearing generations so Core cannot retain a
|
|
2041
|
+
// silently stale connection for an unrelated device.
|
|
2042
|
+
Object.entries(transportCache).forEach(([uuid, cachedTransport]) => {
|
|
2043
|
+
try {
|
|
2044
|
+
cachedTransport.disconnectSubscription?.remove();
|
|
2045
|
+
} catch (error) {
|
|
2046
|
+
Log?.debug('BLE manager reset disconnect subscription removal failed:', error);
|
|
2047
|
+
}
|
|
2048
|
+
cachedTransport.disconnectSubscription = undefined;
|
|
2049
|
+
try {
|
|
2050
|
+
cachedTransport.notifySubscription?.remove();
|
|
2051
|
+
} catch (error) {
|
|
2052
|
+
Log?.debug('BLE manager reset notify subscription removal failed:', error);
|
|
2053
|
+
}
|
|
2054
|
+
cachedTransport.notifySubscription = undefined;
|
|
2055
|
+
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
2056
|
+
try {
|
|
2057
|
+
this.emitDeviceDisconnect(
|
|
2058
|
+
uuid,
|
|
2059
|
+
cachedTransport.device?.name,
|
|
2060
|
+
cachedTransport.monitorToken ?? this.monitorTokens.get(uuid)
|
|
2061
|
+
);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
Log?.debug('BLE manager reset disconnect event failed:', error);
|
|
2064
|
+
}
|
|
2065
|
+
delete transportCache[uuid];
|
|
2066
|
+
});
|
|
2067
|
+
this.protocolV2Links.invalidateAllLinks(reason).catch(error => {
|
|
2068
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager link invalidation failed:', error);
|
|
2069
|
+
});
|
|
2070
|
+
this.deviceProtocol.clear();
|
|
2071
|
+
this.probingProtocols.clear();
|
|
2072
|
+
this.sessionProtocols.clear();
|
|
2073
|
+
this.confirmedProtocolV2.clear();
|
|
2074
|
+
this.protocolReprobeFailures.clear();
|
|
2075
|
+
this.writeTimeoutCounts.clear();
|
|
2076
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
2077
|
+
this.monitorTokens.clear();
|
|
2078
|
+
this.protocolV2Assemblers.clear();
|
|
2079
|
+
try {
|
|
2080
|
+
manager?.destroy();
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
private createProtocolMismatchError(expected: ProtocolType, uuid: string) {
|
|
2087
|
+
// A generic Ping miss is not a bond failure. Only a later miss after this
|
|
2088
|
+
// endpoint already answered V2, or a native encryption/pairing error, is.
|
|
2089
|
+
const isStaleV2Bond = expected === 'V2' && this.confirmedProtocolV2.has(uuid);
|
|
1357
2090
|
return ERRORS.TypedError(
|
|
1358
|
-
HardwareErrorCode.RuntimeError,
|
|
2091
|
+
isStaleV2Bond ? HardwareErrorCode.BleDeviceBondError : HardwareErrorCode.RuntimeError,
|
|
1359
2092
|
`Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`
|
|
1360
2093
|
);
|
|
1361
2094
|
}
|
|
@@ -1363,62 +2096,100 @@ export default class ReactNativeBleTransport {
|
|
|
1363
2096
|
private createProtocolDetectionError() {
|
|
1364
2097
|
return ERRORS.TypedError(
|
|
1365
2098
|
HardwareErrorCode.BleTimeoutError,
|
|
1366
|
-
'Unable to detect BLE protocol: device did not respond to Protocol V1
|
|
2099
|
+
'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping'
|
|
1367
2100
|
);
|
|
1368
2101
|
}
|
|
1369
2102
|
|
|
1370
2103
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
2104
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
2105
|
+
this.probingProtocols.delete(uuid);
|
|
2106
|
+
}
|
|
1371
2107
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1372
2108
|
this.deviceProtocol.delete(uuid);
|
|
1373
2109
|
}
|
|
1374
2110
|
}
|
|
1375
2111
|
|
|
2112
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
2113
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
2114
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
1376
2117
|
private async detectProtocol(
|
|
1377
2118
|
uuid: string,
|
|
1378
2119
|
expectedProtocol?: ProtocolType,
|
|
1379
|
-
protocolHint?: ProtocolType
|
|
2120
|
+
protocolHint?: ProtocolType,
|
|
2121
|
+
rebuildTransport?: () => Promise<void>
|
|
1380
2122
|
): Promise<ProtocolType> {
|
|
2123
|
+
// A declared V1 is taken at face value on every platform, as iOS has done
|
|
2124
|
+
// since protocol probing arrived: the caller reads the protocol off its own
|
|
2125
|
+
// device record, so the probe re-asks a question that is already answered
|
|
2126
|
+
// and costs a round trip on every acquire. Expected V2 must still Ping so
|
|
2127
|
+
// USB-priority `link disabled` surfaces here instead of as a later unmapped
|
|
2128
|
+
// RuntimeError. sessionProtocols is stamped too — the iOS path skipped it,
|
|
2129
|
+
// which left the firmware-install reconnect (skipProtocolProbe) without the
|
|
2130
|
+
// confirmed protocol it requires.
|
|
1381
2131
|
if (expectedProtocol === 'V1') {
|
|
1382
|
-
|
|
1383
|
-
|
|
2132
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
2133
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
2134
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
2135
|
+
deviceId: uuid,
|
|
2136
|
+
protocol: 'V1',
|
|
2137
|
+
source: 'expected',
|
|
2138
|
+
});
|
|
2139
|
+
return 'V1';
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (expectedProtocol === 'V2') {
|
|
2143
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
2144
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
2145
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
2146
|
+
this.confirmedProtocolV2.add(uuid);
|
|
1384
2147
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1385
2148
|
deviceId: uuid,
|
|
1386
|
-
protocol: '
|
|
2149
|
+
protocol: 'V2',
|
|
1387
2150
|
source: 'expected',
|
|
1388
2151
|
});
|
|
1389
|
-
return '
|
|
2152
|
+
return 'V2';
|
|
1390
2153
|
}
|
|
1391
|
-
throw this.createProtocolMismatchError(expectedProtocol);
|
|
2154
|
+
throw this.createProtocolMismatchError(expectedProtocol, uuid);
|
|
1392
2155
|
}
|
|
1393
2156
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
deviceId: uuid,
|
|
1400
|
-
protocol: 'V2',
|
|
1401
|
-
source: 'expected',
|
|
1402
|
-
});
|
|
1403
|
-
return 'V2';
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// 项目约束:协议判断必须在连接后主动探测,不能依赖设备名/PID/descriptor。
|
|
1407
|
-
// 设备名 hint(如 "Pro 2")只用于调整探测顺序:hint=V2 时先探 V2、失败回落 V1,
|
|
1408
|
-
// 不能作为最终结论。
|
|
1409
|
-
const probeOrder: ProtocolType[] =
|
|
2157
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
2158
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
2159
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
2160
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
2161
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1410
2162
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
2163
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
2164
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
2165
|
+
// only adds its timeout to every poll.
|
|
2166
|
+
const trustSessionProtocol =
|
|
2167
|
+
sessionProtocol !== undefined &&
|
|
2168
|
+
!protocolHint &&
|
|
2169
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
2170
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1411
2171
|
|
|
1412
2172
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1413
2173
|
const protocol = probeOrder[i];
|
|
1414
2174
|
if (i > 0) {
|
|
1415
|
-
//
|
|
2175
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1416
2176
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
2177
|
+
if (!transportCache[uuid]) {
|
|
2178
|
+
if (!rebuildTransport) {
|
|
2179
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
2180
|
+
}
|
|
2181
|
+
await rebuildTransport();
|
|
2182
|
+
}
|
|
1417
2183
|
}
|
|
1418
2184
|
const detected =
|
|
1419
2185
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1420
2186
|
if (detected) {
|
|
1421
2187
|
this.deviceProtocol.set(uuid, protocol);
|
|
2188
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
2189
|
+
if (protocol === 'V2') {
|
|
2190
|
+
this.confirmedProtocolV2.add(uuid);
|
|
2191
|
+
}
|
|
2192
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1422
2193
|
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1423
2194
|
deviceId: uuid,
|
|
1424
2195
|
protocol,
|
|
@@ -1428,7 +2199,16 @@ export default class ReactNativeBleTransport {
|
|
|
1428
2199
|
}
|
|
1429
2200
|
}
|
|
1430
2201
|
|
|
2202
|
+
if (trustSessionProtocol) {
|
|
2203
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
2204
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
2205
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
2206
|
+
} else {
|
|
2207
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
1431
2210
|
this.deviceProtocol.delete(uuid);
|
|
2211
|
+
this.probingProtocols.delete(uuid);
|
|
1432
2212
|
throw this.createProtocolDetectionError();
|
|
1433
2213
|
}
|
|
1434
2214
|
|
|
@@ -1490,12 +2270,20 @@ export default class ReactNativeBleTransport {
|
|
|
1490
2270
|
}
|
|
1491
2271
|
|
|
1492
2272
|
try {
|
|
1493
|
-
this.
|
|
1494
|
-
|
|
2273
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
2274
|
+
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
2275
|
+
// session before Core has a chance to restore a hidden wallet.
|
|
2276
|
+
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2277
|
+
this.probingProtocols.delete(uuid);
|
|
1495
2278
|
return true;
|
|
1496
2279
|
} catch (error) {
|
|
1497
2280
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1498
|
-
Log?.debug('[ReactNativeBleTransport] Protocol V1
|
|
2281
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
2282
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2283
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2284
|
+
if (isWedgedWriteError(error)) {
|
|
2285
|
+
throw error;
|
|
2286
|
+
}
|
|
1499
2287
|
return false;
|
|
1500
2288
|
}
|
|
1501
2289
|
}
|
|
@@ -1505,7 +2293,7 @@ export default class ReactNativeBleTransport {
|
|
|
1505
2293
|
return false;
|
|
1506
2294
|
}
|
|
1507
2295
|
|
|
1508
|
-
this.
|
|
2296
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1509
2297
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1510
2298
|
const detected = await probeProtocolV2Helper({
|
|
1511
2299
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1520,6 +2308,8 @@ export default class ReactNativeBleTransport {
|
|
|
1520
2308
|
});
|
|
1521
2309
|
if (!detected) {
|
|
1522
2310
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2311
|
+
} else {
|
|
2312
|
+
this.probingProtocols.delete(uuid);
|
|
1523
2313
|
}
|
|
1524
2314
|
return detected;
|
|
1525
2315
|
}
|
|
@@ -1570,17 +2360,8 @@ export default class ReactNativeBleTransport {
|
|
|
1570
2360
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1571
2361
|
}
|
|
1572
2362
|
|
|
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
2363
|
private resetProtocolV2Frames(uuid: string) {
|
|
1582
|
-
this.
|
|
1583
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2364
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1584
2365
|
}
|
|
1585
2366
|
|
|
1586
2367
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -1609,19 +2390,95 @@ export default class ReactNativeBleTransport {
|
|
|
1609
2390
|
}
|
|
1610
2391
|
}
|
|
1611
2392
|
|
|
1612
|
-
private async
|
|
2393
|
+
private async writeProtocolV2Packet(
|
|
2394
|
+
uuid: string,
|
|
2395
|
+
transport: BleTransport,
|
|
2396
|
+
base64: string,
|
|
2397
|
+
context: ProtocolV2CallContext,
|
|
2398
|
+
assertCurrentGeneration: () => void
|
|
2399
|
+
) {
|
|
2400
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2401
|
+
platform: Platform.OS,
|
|
2402
|
+
highThroughput: context.highThroughput,
|
|
2403
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2404
|
+
characteristic: transport.writeCharacteristic,
|
|
2405
|
+
});
|
|
2406
|
+
let attempt = 0;
|
|
2407
|
+
for (;;) {
|
|
2408
|
+
assertCurrentGeneration();
|
|
2409
|
+
if (context.signal.aborted) {
|
|
2410
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2411
|
+
}
|
|
2412
|
+
try {
|
|
2413
|
+
await this.writeBlePacket(
|
|
2414
|
+
uuid,
|
|
2415
|
+
base64,
|
|
2416
|
+
payload =>
|
|
2417
|
+
shouldUseWriteWithResponse
|
|
2418
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2419
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2420
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2421
|
+
// tear down the link that the current generation is using.
|
|
2422
|
+
() => {
|
|
2423
|
+
try {
|
|
2424
|
+
assertCurrentGeneration();
|
|
2425
|
+
return !context.signal.aborted;
|
|
2426
|
+
} catch {
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
);
|
|
2431
|
+
assertCurrentGeneration();
|
|
2432
|
+
return;
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
if (
|
|
2435
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2436
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2437
|
+
) {
|
|
2438
|
+
throw error;
|
|
2439
|
+
}
|
|
2440
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2441
|
+
attempt += 1;
|
|
2442
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2443
|
+
name: context.messageName,
|
|
2444
|
+
attempt,
|
|
2445
|
+
delayMs,
|
|
2446
|
+
});
|
|
2447
|
+
await delay(delayMs);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
private async writeProtocolV2Frame(
|
|
2453
|
+
uuid: string,
|
|
2454
|
+
transport: BleTransport,
|
|
2455
|
+
frame: Uint8Array,
|
|
2456
|
+
context: ProtocolV2CallContext,
|
|
2457
|
+
assertCurrentGeneration: () => void
|
|
2458
|
+
) {
|
|
1613
2459
|
const tuning = getProtocolV2BleTuning();
|
|
1614
2460
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1615
2461
|
platform: Platform.OS,
|
|
1616
2462
|
iosPacketLength: tuning.iosPacketLength,
|
|
1617
2463
|
androidPacketLength: tuning.androidPacketLength,
|
|
1618
|
-
mtu:
|
|
2464
|
+
mtu: transport.mtuSize,
|
|
2465
|
+
});
|
|
2466
|
+
await writeProtocolV2BleFrame({
|
|
2467
|
+
frame,
|
|
2468
|
+
packetCapacity,
|
|
2469
|
+
assertActive: assertCurrentGeneration,
|
|
2470
|
+
signal: context.signal,
|
|
2471
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2472
|
+
wait: delay,
|
|
2473
|
+
writePacket: packet =>
|
|
2474
|
+
this.writeProtocolV2Packet(
|
|
2475
|
+
uuid,
|
|
2476
|
+
transport,
|
|
2477
|
+
Buffer.from(packet).toString('base64'),
|
|
2478
|
+
context,
|
|
2479
|
+
assertCurrentGeneration
|
|
2480
|
+
),
|
|
1619
2481
|
});
|
|
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
2482
|
}
|
|
1626
2483
|
|
|
1627
2484
|
private async callProtocolV2(
|
|
@@ -1634,19 +2491,45 @@ export default class ReactNativeBleTransport {
|
|
|
1634
2491
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1635
2492
|
}
|
|
1636
2493
|
|
|
1637
|
-
const callOptions =
|
|
1638
|
-
|
|
1639
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1640
|
-
};
|
|
1641
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2494
|
+
const callOptions = options;
|
|
2495
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
1642
2496
|
|
|
1643
|
-
if (
|
|
2497
|
+
if (highThroughputWrite) {
|
|
2498
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
1644
2499
|
const tuning = getProtocolV2BleTuning();
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2500
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2501
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2502
|
+
platform: Platform.OS,
|
|
2503
|
+
highThroughput: true,
|
|
2504
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2505
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
2506
|
+
});
|
|
2507
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2508
|
+
platform: Platform.OS,
|
|
2509
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2510
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2511
|
+
mtu: currentTransport.mtuSize,
|
|
1649
2512
|
});
|
|
2513
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2514
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2515
|
+
currentTransport.mtuSize
|
|
2516
|
+
)}:${packetCapacity}`;
|
|
2517
|
+
const loggedSignatures =
|
|
2518
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2519
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2520
|
+
loggedSignatures.add(logSignature);
|
|
2521
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2522
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2523
|
+
name,
|
|
2524
|
+
writeMode,
|
|
2525
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2526
|
+
packetCapacity,
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
if (highThroughputWrite) {
|
|
2532
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
1650
2533
|
}
|
|
1651
2534
|
|
|
1652
2535
|
try {
|
|
@@ -1660,6 +2543,90 @@ export default class ReactNativeBleTransport {
|
|
|
1660
2543
|
} catch (e) {
|
|
1661
2544
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1662
2545
|
throw e;
|
|
2546
|
+
} finally {
|
|
2547
|
+
if (highThroughputWrite) {
|
|
2548
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2554
|
+
const transport = this.getCachedTransport(uuid);
|
|
2555
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2556
|
+
|
|
2557
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2558
|
+
transport.device = refreshedDevice;
|
|
2559
|
+
transport.mtuSize =
|
|
2560
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2561
|
+
|
|
2562
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2563
|
+
throw ERRORS.TypedError(
|
|
2564
|
+
HardwareErrorCode.BleConnectedError,
|
|
2565
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2571
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2572
|
+
if (timerId !== undefined) {
|
|
2573
|
+
clearTimeout(timerId);
|
|
2574
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2579
|
+
if (Platform.OS !== 'android') return;
|
|
2580
|
+
|
|
2581
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2582
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2583
|
+
|
|
2584
|
+
const transport = transportCache[uuid];
|
|
2585
|
+
if (!transport) return;
|
|
2586
|
+
|
|
2587
|
+
try {
|
|
2588
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2589
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2590
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2591
|
+
priority: 'high',
|
|
2592
|
+
});
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2595
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2601
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2602
|
+
|
|
2603
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2604
|
+
const timerId = setTimeout(() => {
|
|
2605
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2606
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2607
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2608
|
+
);
|
|
2609
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2610
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2614
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2615
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
try {
|
|
2620
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2621
|
+
ConnectionPriority.Balanced
|
|
2622
|
+
);
|
|
2623
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2624
|
+
priority: 'balanced',
|
|
2625
|
+
});
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2628
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2629
|
+
});
|
|
1663
2630
|
}
|
|
1664
2631
|
}
|
|
1665
2632
|
|
|
@@ -1680,10 +2647,16 @@ export default class ReactNativeBleTransport {
|
|
|
1680
2647
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1681
2648
|
this.resetProtocolV2Frames(uuid);
|
|
1682
2649
|
},
|
|
1683
|
-
writeFrame: async (frame: Uint8Array) => {
|
|
2650
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1684
2651
|
assertCurrentGeneration();
|
|
1685
2652
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1686
|
-
await this.writeProtocolV2Frame(
|
|
2653
|
+
await this.writeProtocolV2Frame(
|
|
2654
|
+
uuid,
|
|
2655
|
+
currentTransport,
|
|
2656
|
+
frame,
|
|
2657
|
+
context,
|
|
2658
|
+
assertCurrentGeneration
|
|
2659
|
+
);
|
|
1687
2660
|
},
|
|
1688
2661
|
readFrame: async () => {
|
|
1689
2662
|
assertCurrentGeneration();
|
|
@@ -1694,6 +2667,7 @@ export default class ReactNativeBleTransport {
|
|
|
1694
2667
|
return rxFrame;
|
|
1695
2668
|
},
|
|
1696
2669
|
reset: (reason: string) => {
|
|
2670
|
+
if (this.monitorTokens.get(uuid) !== generation) return;
|
|
1697
2671
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1698
2672
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
1699
2673
|
},
|
|
@@ -1708,6 +2682,6 @@ export default class ReactNativeBleTransport {
|
|
|
1708
2682
|
}
|
|
1709
2683
|
|
|
1710
2684
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1711
|
-
return this.
|
|
2685
|
+
return this.getActiveProtocol(path);
|
|
1712
2686
|
}
|
|
1713
2687
|
}
|