@onekeyfe/hd-transport-react-native 1.2.0-alpha.11 → 1.2.0-alpha.111
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 -2
- 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 +114 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +744 -307
- package/dist/subscribeBleOn.d.ts.map +1 -1
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/jest.config.js +10 -0
- package/package.json +7 -6
- package/src/BleTransport.ts +10 -42
- package/src/__tests__/BleTransport.test.ts +114 -0
- package/src/__tests__/bleStrategy.test.ts +89 -6
- package/src/__tests__/connectTimeout.test.ts +292 -0
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +758 -46
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/bleStrategy.ts +26 -36
- package/src/constants.ts +9 -14
- package/src/index.ts +981 -326
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +1 -0
- package/src/types.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -5,34 +5,46 @@ 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
|
+
isPro2FindMyAdvertisementName,
|
|
33
|
+
} from '@onekeyfe/hd-shared';
|
|
24
34
|
|
|
25
35
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
26
36
|
import {
|
|
27
37
|
hasWritableCapability,
|
|
28
|
-
resolveBleWriteMode,
|
|
29
38
|
resolveProtocolV2PacketCapacity,
|
|
39
|
+
shouldRefreshNegotiatedMtu,
|
|
40
|
+
shouldWriteProtocolV2WithResponse,
|
|
30
41
|
} from './bleStrategy';
|
|
31
42
|
import { subscribeBleOn } from './subscribeBleOn';
|
|
32
43
|
import {
|
|
33
44
|
ANDROID_PACKET_LENGTH,
|
|
45
|
+
ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
34
46
|
IOS_PACKET_LENGTH,
|
|
35
|
-
|
|
47
|
+
IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
36
48
|
getBluetoothServiceUuids,
|
|
37
49
|
getInfosForServiceUuid,
|
|
38
50
|
isSameBleUuid,
|
|
@@ -56,24 +68,52 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
56
68
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
57
69
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
58
70
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
59
|
-
const FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS = 2000;
|
|
60
71
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
61
72
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
62
73
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
63
74
|
const ANDROID_GATT_CONGESTED_STATUS = 143;
|
|
64
75
|
|
|
65
|
-
type FirmwareUploadWriteRetryType = 'congested'
|
|
76
|
+
type FirmwareUploadWriteRetryType = 'congested';
|
|
66
77
|
type ResolvedBleCharacteristics = {
|
|
67
78
|
writeCharacteristic: Characteristic;
|
|
68
79
|
notifyCharacteristic: Characteristic;
|
|
69
80
|
};
|
|
70
81
|
|
|
82
|
+
const isAsciiWhitespace = (code: number) =>
|
|
83
|
+
code === 0x09 ||
|
|
84
|
+
code === 0x0a ||
|
|
85
|
+
code === 0x0b ||
|
|
86
|
+
code === 0x0c ||
|
|
87
|
+
code === 0x0d ||
|
|
88
|
+
code === 0x20;
|
|
89
|
+
|
|
90
|
+
const hasGattCongestedStatus = (text: string) => {
|
|
91
|
+
let searchFrom = 0;
|
|
92
|
+
while (searchFrom < text.length) {
|
|
93
|
+
const statusIndex = text.indexOf('status', searchFrom);
|
|
94
|
+
if (statusIndex < 0) return false;
|
|
95
|
+
|
|
96
|
+
let cursor = statusIndex + 'status'.length;
|
|
97
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
98
|
+
if (text[cursor] === ':' || text[cursor] === '=') {
|
|
99
|
+
cursor += 1;
|
|
100
|
+
while (cursor < text.length && isAsciiWhitespace(text.charCodeAt(cursor))) cursor += 1;
|
|
101
|
+
}
|
|
102
|
+
if (text.startsWith(String(ANDROID_GATT_CONGESTED_STATUS), cursor)) return true;
|
|
103
|
+
|
|
104
|
+
searchFrom = statusIndex + 'status'.length;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
};
|
|
108
|
+
|
|
71
109
|
const delay = (ms: number) =>
|
|
72
110
|
new Promise<void>(resolve => {
|
|
73
111
|
setTimeout(resolve, ms);
|
|
74
112
|
});
|
|
75
113
|
|
|
76
|
-
const getFirmwareUploadWriteRetryType = (
|
|
114
|
+
export const getFirmwareUploadWriteRetryType = (
|
|
115
|
+
error: unknown
|
|
116
|
+
): FirmwareUploadWriteRetryType | null => {
|
|
77
117
|
if (!error || typeof error !== 'object') return null;
|
|
78
118
|
const bleWriteError = error as {
|
|
79
119
|
androidErrorCode?: unknown;
|
|
@@ -84,13 +124,6 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
84
124
|
name?: unknown;
|
|
85
125
|
};
|
|
86
126
|
|
|
87
|
-
if (
|
|
88
|
-
bleWriteError.errorCode === BleErrorCode.DeviceDisconnected ||
|
|
89
|
-
bleWriteError.errorCode === BleErrorCode.CharacteristicNotFound
|
|
90
|
-
) {
|
|
91
|
-
return 'reconnectable';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
127
|
if (
|
|
95
128
|
bleWriteError.androidErrorCode === ANDROID_GATT_CONGESTED_STATUS ||
|
|
96
129
|
bleWriteError.status === ANDROID_GATT_CONGESTED_STATUS
|
|
@@ -101,39 +134,42 @@ const getFirmwareUploadWriteRetryType = (error: unknown): FirmwareUploadWriteRet
|
|
|
101
134
|
const text = [bleWriteError.reason, bleWriteError.message, bleWriteError.name]
|
|
102
135
|
.filter(value => typeof value === 'string')
|
|
103
136
|
.join(' ');
|
|
104
|
-
return
|
|
137
|
+
return text.includes('GATT_CONGESTED') || hasGattCongestedStatus(text) ? 'congested' : null;
|
|
105
138
|
};
|
|
106
139
|
|
|
107
140
|
const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, maxDelayMs = 1200) =>
|
|
108
141
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
109
|
-
const BLE_RESPONSE_TIMEOUT_MS = 30_000;
|
|
110
142
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
111
143
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
112
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
146
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
147
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
148
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
149
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
150
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
151
|
+
*/
|
|
152
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
153
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
154
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
155
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
156
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
157
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
158
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
159
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
160
|
+
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
113
161
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
114
162
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
115
|
-
const HIGH_VOLUME_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 6;
|
|
116
|
-
const HIGH_VOLUME_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 6 : 2;
|
|
117
|
-
const HIGH_VOLUME_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 20 : 8;
|
|
118
|
-
|
|
119
163
|
export type ProtocolV2BleTuning = {
|
|
120
164
|
iosPacketLength?: number;
|
|
121
165
|
androidPacketLength?: number;
|
|
122
|
-
highVolumeWriteBurstSize?: number;
|
|
123
|
-
highVolumeWritePauseMs?: number;
|
|
124
|
-
highVolumeWriteFlushDelayMs?: number;
|
|
125
|
-
highVolumeWriteWithResponse?: boolean;
|
|
126
166
|
};
|
|
127
167
|
|
|
128
168
|
type ResolvedProtocolV2BleTuning = Required<ProtocolV2BleTuning>;
|
|
129
169
|
|
|
130
170
|
const DEFAULT_PROTOCOL_V2_BLE_TUNING: ResolvedProtocolV2BleTuning = {
|
|
131
|
-
iosPacketLength:
|
|
132
|
-
androidPacketLength:
|
|
133
|
-
highVolumeWriteBurstSize: HIGH_VOLUME_WRITE_BURST_SIZE,
|
|
134
|
-
highVolumeWritePauseMs: HIGH_VOLUME_WRITE_PAUSE_MS,
|
|
135
|
-
highVolumeWriteFlushDelayMs: HIGH_VOLUME_WRITE_FLUSH_DELAY_MS,
|
|
136
|
-
highVolumeWriteWithResponse: false,
|
|
171
|
+
iosPacketLength: IOS_PROTOCOL_V2_PACKET_LENGTH,
|
|
172
|
+
androidPacketLength: ANDROID_PROTOCOL_V2_PACKET_LENGTH,
|
|
137
173
|
};
|
|
138
174
|
|
|
139
175
|
let protocolV2BleTuning: ResolvedProtocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
@@ -154,27 +190,13 @@ export function configureProtocolV2BleTuning(tuning: ProtocolV2BleTuning = {}) {
|
|
|
154
190
|
tuning.androidPacketLength,
|
|
155
191
|
protocolV2BleTuning.androidPacketLength
|
|
156
192
|
),
|
|
157
|
-
highVolumeWriteBurstSize: normalizePositiveInteger(
|
|
158
|
-
tuning.highVolumeWriteBurstSize,
|
|
159
|
-
protocolV2BleTuning.highVolumeWriteBurstSize
|
|
160
|
-
),
|
|
161
|
-
highVolumeWritePauseMs: normalizePositiveInteger(
|
|
162
|
-
tuning.highVolumeWritePauseMs,
|
|
163
|
-
protocolV2BleTuning.highVolumeWritePauseMs
|
|
164
|
-
),
|
|
165
|
-
highVolumeWriteFlushDelayMs: normalizePositiveInteger(
|
|
166
|
-
tuning.highVolumeWriteFlushDelayMs,
|
|
167
|
-
protocolV2BleTuning.highVolumeWriteFlushDelayMs
|
|
168
|
-
),
|
|
169
|
-
highVolumeWriteWithResponse:
|
|
170
|
-
tuning.highVolumeWriteWithResponse ?? protocolV2BleTuning.highVolumeWriteWithResponse,
|
|
171
193
|
};
|
|
172
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
194
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning configured', protocolV2BleTuning);
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
export function resetProtocolV2BleTuning() {
|
|
176
198
|
protocolV2BleTuning = { ...DEFAULT_PROTOCOL_V2_BLE_TUNING };
|
|
177
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
199
|
+
Log?.debug('[ReactNativeBleTransport] BLE tuning reset', protocolV2BleTuning);
|
|
178
200
|
}
|
|
179
201
|
|
|
180
202
|
export function getProtocolV2BleTuning() {
|
|
@@ -189,24 +211,60 @@ function getDeviceDisplayName(device?: Device | null) {
|
|
|
189
211
|
return device?.name || device?.localName || null;
|
|
190
212
|
}
|
|
191
213
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
214
|
+
const IOS_REQUEST_MTU = 247;
|
|
215
|
+
const ANDROID_REQUEST_MTU = 517;
|
|
216
|
+
const BLE_MTU_REFRESH_RETRY_DELAY_MS = 200;
|
|
217
|
+
const ANDROID_HIGH_PRIORITY_IDLE_MS = 1000;
|
|
195
218
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
getInfosForServiceUuid(serviceUuid, 'classic')
|
|
199
|
-
);
|
|
200
|
-
}
|
|
219
|
+
const getRequestedBleMtu = () =>
|
|
220
|
+
Platform.OS === 'android' ? ANDROID_REQUEST_MTU : IOS_REQUEST_MTU;
|
|
201
221
|
|
|
202
|
-
const
|
|
222
|
+
const BLE_NATIVE_CONNECT_TIMEOUT_MS = 3000;
|
|
203
223
|
|
|
204
224
|
const connectOptions: Record<string, unknown> = {
|
|
205
|
-
requestMTU:
|
|
206
|
-
timeout:
|
|
225
|
+
requestMTU: getRequestedBleMtu(),
|
|
226
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
207
227
|
refreshGatt: 'OnConnected',
|
|
208
228
|
};
|
|
209
229
|
|
|
230
|
+
/** Fallback connect options: drops requestMTU (the thing being worked around) but keeps the native budget. */
|
|
231
|
+
const fallbackConnectOptions: Record<string, unknown> = {
|
|
232
|
+
timeout: BLE_NATIVE_CONNECT_TIMEOUT_MS,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* JS backstop for connect. The native adapter applies its own 3s budget, but it
|
|
237
|
+
* schedules that timeout on its serial queue, so a busy queue (e.g. right after a
|
|
238
|
+
* firmware install tears the link down) can leave the promise unsettled — observed
|
|
239
|
+
* blocking a reconnect for 61s until the app-level timeout. Healthy connects finish
|
|
240
|
+
* inside the native budget, so this only fires when the native timeout did not.
|
|
241
|
+
*/
|
|
242
|
+
export const BLE_CONNECT_TIMEOUT_MS = BLE_NATIVE_CONNECT_TIMEOUT_MS * 2 + 2000;
|
|
243
|
+
/**
|
|
244
|
+
* Service discovery and characteristic resolution run after connect() succeeds, but
|
|
245
|
+
* CoreBluetooth schedules them on the same serial queue. If that queue is wedged by a
|
|
246
|
+
* device reboot, these calls can remain pending forever unless they have their own
|
|
247
|
+
* budget.
|
|
248
|
+
*/
|
|
249
|
+
export const BLE_GATT_SETUP_TIMEOUT_MS = 10_000;
|
|
250
|
+
/**
|
|
251
|
+
* How many times a known device may fail its own protocol before we probe the others
|
|
252
|
+
* again. Reconnect polling during a device reboot repeats this every few seconds, and
|
|
253
|
+
* probing Protocol V2 costs a 10s Ping timeout, so paying it on every attempt for a
|
|
254
|
+
* device we just spoke V1 to dominates the wait. A firmware update can legitimately
|
|
255
|
+
* change a device's protocol, so the shortcut has to expire rather than stick.
|
|
256
|
+
*/
|
|
257
|
+
export const PROTOCOL_REPROBE_FALLBACK_ATTEMPTS = 3;
|
|
258
|
+
/** BLE setup timeouts since the last successful setup before the manager is recreated. */
|
|
259
|
+
export const BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
260
|
+
const CONNECT_TIMEOUT_MESSAGE = 'BLE connect timeout after';
|
|
261
|
+
const isConnectTimeoutError = (error: unknown): boolean =>
|
|
262
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleConnectedError &&
|
|
263
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
264
|
+
(error as { message: string }).message.startsWith(CONNECT_TIMEOUT_MESSAGE);
|
|
265
|
+
const isNativeOperationTimeoutError = (error: unknown): boolean =>
|
|
266
|
+
(error as { errorCode?: unknown })?.errorCode === BleErrorCode.OperationTimedOut;
|
|
267
|
+
|
|
210
268
|
export type IOneKeyDevice = OneKeyDeviceInfoBase & Device;
|
|
211
269
|
|
|
212
270
|
const tryToGetConfiguration = (device: Device) => {
|
|
@@ -218,22 +276,32 @@ const tryToGetConfiguration = (device: Device) => {
|
|
|
218
276
|
return infos;
|
|
219
277
|
};
|
|
220
278
|
|
|
221
|
-
const
|
|
222
|
-
|
|
279
|
+
const requestNegotiatedMtu = async (
|
|
280
|
+
device: Device,
|
|
281
|
+
stage: 'connected' | 'servicesAndNotifyReady' | 'highThroughput',
|
|
282
|
+
attempt: number
|
|
283
|
+
) => {
|
|
284
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return device;
|
|
223
285
|
|
|
224
286
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
mtu: mtuDevice.mtu,
|
|
229
|
-
});
|
|
287
|
+
// iOS ignores the requested value but react-native-ble-plx returns a fresh
|
|
288
|
+
// Device snapshot whose MTU is derived from CoreBluetooth's maximum write length.
|
|
289
|
+
const mtuDevice = await device.requestMTU(getRequestedBleMtu());
|
|
230
290
|
return mtuDevice;
|
|
231
291
|
} catch (error) {
|
|
232
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
292
|
+
Log?.debug('[ReactNativeBleTransport] MTU refresh failed, continuing with current value', {
|
|
293
|
+
platform: Platform.OS,
|
|
294
|
+
stage,
|
|
295
|
+
attempt,
|
|
296
|
+
actual: device.mtu,
|
|
297
|
+
error: error instanceof Error ? error.message : String(error),
|
|
298
|
+
});
|
|
233
299
|
return device;
|
|
234
300
|
}
|
|
235
301
|
};
|
|
236
302
|
|
|
303
|
+
const resolveNegotiatedMtu = (device: Device) => requestNegotiatedMtu(device, 'connected', 0);
|
|
304
|
+
|
|
237
305
|
type IOBleErrorRemap = Error | BleError | null | undefined;
|
|
238
306
|
|
|
239
307
|
function remapError(error: IOBleErrorRemap) {
|
|
@@ -273,6 +341,8 @@ export default class ReactNativeBleTransport {
|
|
|
273
341
|
|
|
274
342
|
_messagesV2: ReturnType<typeof transport.parseConfigure> | undefined;
|
|
275
343
|
|
|
344
|
+
private protocolV2SchemaConfiguration: string | undefined;
|
|
345
|
+
|
|
276
346
|
name = 'ReactNativeBleTransport';
|
|
277
347
|
|
|
278
348
|
configured = false;
|
|
@@ -283,6 +353,8 @@ export default class ReactNativeBleTransport {
|
|
|
283
353
|
|
|
284
354
|
runPromise: Deferred<any> | null = null;
|
|
285
355
|
|
|
356
|
+
private runPromiseDeviceId: string | null = null;
|
|
357
|
+
|
|
286
358
|
emitter?: EventEmitter;
|
|
287
359
|
|
|
288
360
|
firmwareUploadWriteRecoveryIds = new Set<string>();
|
|
@@ -290,8 +362,28 @@ export default class ReactNativeBleTransport {
|
|
|
290
362
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
291
363
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
292
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
367
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
368
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
369
|
+
* transport that was never validated.
|
|
370
|
+
*/
|
|
371
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
372
|
+
|
|
373
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
374
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
375
|
+
|
|
376
|
+
/** BLE setup timeouts per device since the last complete characteristic resolution. */
|
|
377
|
+
private connectionSetupTimeoutCounts: Map<string, number> = new Map();
|
|
378
|
+
|
|
293
379
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
294
380
|
|
|
381
|
+
/** Protocol this device actually answered on, kept across reconnects of one session. */
|
|
382
|
+
private sessionProtocols: Map<string, ProtocolType> = new Map();
|
|
383
|
+
|
|
384
|
+
/** Consecutive detections that failed while trusting sessionProtocols. */
|
|
385
|
+
private protocolReprobeFailures: Map<string, number> = new Map();
|
|
386
|
+
|
|
295
387
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
296
388
|
|
|
297
389
|
private protocolV2FrameQueues: Map<string, Uint8Array[]> = new Map();
|
|
@@ -314,13 +406,21 @@ export default class ReactNativeBleTransport {
|
|
|
314
406
|
this.rejectProtocolV2Frames(uuid, new Error(reason));
|
|
315
407
|
Log?.debug('[ReactNativeBleTransport] Protocol V2 link invalidated:', uuid, reason);
|
|
316
408
|
if (reason.startsWith('Protocol V2 link-fatal error:')) {
|
|
317
|
-
await this.
|
|
409
|
+
await this.releaseNative(uuid, true);
|
|
318
410
|
}
|
|
319
411
|
},
|
|
320
412
|
});
|
|
321
413
|
|
|
322
414
|
private monitorTokens: Map<string, number> = new Map();
|
|
323
415
|
|
|
416
|
+
private disconnectEventTokens: Map<string, number> = new Map();
|
|
417
|
+
|
|
418
|
+
private protocolV2HighVolumeLogSignatures: Map<string, Set<string>> = new Map();
|
|
419
|
+
|
|
420
|
+
private androidHighPriorityDevices: Set<string> = new Set();
|
|
421
|
+
|
|
422
|
+
private androidPriorityResetTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
|
423
|
+
|
|
324
424
|
private nextMonitorToken = 1;
|
|
325
425
|
|
|
326
426
|
constructor(options: TransportOptions) {
|
|
@@ -339,11 +439,19 @@ export default class ReactNativeBleTransport {
|
|
|
339
439
|
}
|
|
340
440
|
|
|
341
441
|
configureProtocolV2(signedData: any) {
|
|
442
|
+
const configuration = typeof signedData === 'string' ? signedData : JSON.stringify(signedData);
|
|
443
|
+
if (this.protocolV2SchemaConfiguration === configuration) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const isReconfiguration = this.protocolV2SchemaConfiguration !== undefined;
|
|
342
448
|
this._messagesV2 = parseConfigure(signedData);
|
|
343
|
-
this.
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
|
|
449
|
+
this.protocolV2SchemaConfiguration = configuration;
|
|
450
|
+
if (isReconfiguration) {
|
|
451
|
+
this.protocolV2Links
|
|
452
|
+
.invalidateAllLinks('Protocol V2 schema reconfigured')
|
|
453
|
+
.catch(error => Log?.debug('Protocol V2 schema link cleanup failed:', error));
|
|
454
|
+
}
|
|
347
455
|
}
|
|
348
456
|
|
|
349
457
|
listen() {
|
|
@@ -373,29 +481,15 @@ export default class ReactNativeBleTransport {
|
|
|
373
481
|
}
|
|
374
482
|
}
|
|
375
483
|
|
|
376
|
-
let fallbackServiceUuid: string | undefined;
|
|
377
|
-
|
|
378
484
|
if (!infos) {
|
|
379
485
|
const services = await device.services();
|
|
380
486
|
Log?.debug(
|
|
381
487
|
'[ReactNativeBleTransport] Known OneKey service UUID not found, discovered services:',
|
|
382
488
|
services?.map(service => service.uuid)
|
|
383
489
|
);
|
|
384
|
-
|
|
385
|
-
const knownService = services.find(service =>
|
|
386
|
-
getInfosForServiceUuid(service.uuid, 'classic')
|
|
387
|
-
);
|
|
388
|
-
const fallbackService =
|
|
389
|
-
knownService ?? services.find(service => !isGenericBleService(service.uuid)) ?? services[0];
|
|
390
|
-
|
|
391
|
-
if (fallbackService) {
|
|
392
|
-
fallbackServiceUuid = fallbackService.uuid;
|
|
393
|
-
characteristics = await device.characteristicsForService(fallbackService.uuid);
|
|
394
|
-
Log?.debug('[ReactNativeBleTransport] Using fallback BLE service:', fallbackService.uuid);
|
|
395
|
-
}
|
|
396
490
|
}
|
|
397
491
|
|
|
398
|
-
if (!infos
|
|
492
|
+
if (!infos) {
|
|
399
493
|
try {
|
|
400
494
|
Log?.debug('cancel connection when service not found');
|
|
401
495
|
await device.cancelConnection();
|
|
@@ -405,9 +499,7 @@ export default class ReactNativeBleTransport {
|
|
|
405
499
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
406
500
|
}
|
|
407
501
|
|
|
408
|
-
const serviceUuid = infos
|
|
409
|
-
const writeUuid = infos?.writeUuid ?? '00000002-0000-1000-8000-00805f9b34fb';
|
|
410
|
-
const notifyUuid = infos?.notifyUuid ?? '00000003-0000-1000-8000-00805f9b34fb';
|
|
502
|
+
const { serviceUuid, writeUuid, notifyUuid } = infos;
|
|
411
503
|
|
|
412
504
|
if (!serviceUuid) {
|
|
413
505
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound);
|
|
@@ -457,6 +549,7 @@ export default class ReactNativeBleTransport {
|
|
|
457
549
|
|
|
458
550
|
attachDisconnectSubscription(transport: BleTransport, device: Device, uuid: string) {
|
|
459
551
|
transport.disconnectSubscription?.remove();
|
|
552
|
+
const { monitorToken } = transport;
|
|
460
553
|
transport.disconnectSubscription = device.onDisconnected(() => {
|
|
461
554
|
if (this.firmwareUploadWriteRecoveryIds.has(uuid)) {
|
|
462
555
|
Log?.debug('device disconnect ignored during FirmwareUpload write recovery: ', uuid);
|
|
@@ -466,18 +559,17 @@ export default class ReactNativeBleTransport {
|
|
|
466
559
|
Log?.debug('device disconnect ignored for stale transport: ', device?.id);
|
|
467
560
|
return;
|
|
468
561
|
}
|
|
562
|
+
if (this.monitorTokens.get(uuid) !== monitorToken) {
|
|
563
|
+
Log?.debug('device disconnect ignored for stale generation: ', device?.id);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
469
566
|
|
|
470
567
|
try {
|
|
471
568
|
Log?.debug('device disconnect: ', device?.id);
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
id: device?.id,
|
|
475
|
-
connectId: device?.id,
|
|
476
|
-
});
|
|
477
|
-
if (this.runPromise) {
|
|
569
|
+
this.emitDeviceDisconnect(uuid, device?.name, monitorToken);
|
|
570
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
478
571
|
const error = ERRORS.TypedError(HardwareErrorCode.BleConnectedError);
|
|
479
572
|
this.runPromise.reject(error);
|
|
480
|
-
this.rejectAllProtocolV2Frames(error);
|
|
481
573
|
}
|
|
482
574
|
} catch (e) {
|
|
483
575
|
Log?.debug('device disconnect error: ', e);
|
|
@@ -487,6 +579,22 @@ export default class ReactNativeBleTransport {
|
|
|
487
579
|
});
|
|
488
580
|
}
|
|
489
581
|
|
|
582
|
+
private emitDeviceDisconnect(uuid: string, name: string | null | undefined, token?: number) {
|
|
583
|
+
if (token === undefined || this.disconnectEventTokens.get(uuid) === token) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (this.monitorTokens.get(uuid) !== token) {
|
|
587
|
+
Log?.debug('device disconnect event ignored for stale generation: ', uuid);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.disconnectEventTokens.set(uuid, token);
|
|
591
|
+
this.emitter?.emit(TRANSPORT_EVENT.DEVICE_DISCONNECT, {
|
|
592
|
+
name,
|
|
593
|
+
id: uuid,
|
|
594
|
+
connectId: uuid,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
490
598
|
async reconnectFirmwareUploadTransport(uuid: string, transport: BleTransport) {
|
|
491
599
|
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
492
600
|
try {
|
|
@@ -499,22 +607,21 @@ export default class ReactNativeBleTransport {
|
|
|
499
607
|
const isConnected = await device.isConnected().catch(() => false);
|
|
500
608
|
if (!isConnected) {
|
|
501
609
|
try {
|
|
502
|
-
device = await device.connect(connectOptions);
|
|
610
|
+
device = await this.connectWithTimeout(uuid, () => device.connect(connectOptions));
|
|
503
611
|
} catch (e) {
|
|
504
612
|
if (
|
|
505
613
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
506
614
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
507
615
|
) {
|
|
508
|
-
device = await device.connect();
|
|
616
|
+
device = await this.connectWithTimeout(uuid, () => device.connect());
|
|
509
617
|
} else if (e.errorCode !== BleErrorCode.DeviceAlreadyConnected) {
|
|
510
618
|
throw e;
|
|
511
619
|
}
|
|
512
620
|
}
|
|
513
621
|
}
|
|
514
622
|
|
|
515
|
-
const { writeCharacteristic, notifyCharacteristic } =
|
|
516
|
-
device
|
|
517
|
-
);
|
|
623
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
624
|
+
await this.resolveCharacteristicsWithTimeout(uuid, device);
|
|
518
625
|
|
|
519
626
|
transport.device = device;
|
|
520
627
|
transport.writeCharacteristic = writeCharacteristic;
|
|
@@ -574,14 +681,13 @@ export default class ReactNativeBleTransport {
|
|
|
574
681
|
}
|
|
575
682
|
|
|
576
683
|
blePlxManager.startDeviceScan(
|
|
577
|
-
|
|
684
|
+
getBluetoothServiceUuids(),
|
|
578
685
|
{
|
|
579
686
|
allowDuplicates: true,
|
|
580
687
|
scanMode: ScanMode.LowLatency,
|
|
581
688
|
},
|
|
582
689
|
(error, device) => {
|
|
583
690
|
if (error) {
|
|
584
|
-
Log?.debug('ble scan manager: ', blePlxManager);
|
|
585
691
|
Log?.debug('ble scan error: ', error);
|
|
586
692
|
if (
|
|
587
693
|
[BleErrorCode.BluetoothPoweredOff, BleErrorCode.BluetoothInUnknownState].includes(
|
|
@@ -605,33 +711,23 @@ export default class ReactNativeBleTransport {
|
|
|
605
711
|
}
|
|
606
712
|
|
|
607
713
|
const displayName = getDeviceDisplayName(device);
|
|
714
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
715
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
716
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
717
|
+
const isFindMyPeripheral =
|
|
718
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
719
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
608
720
|
const isOneKey =
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
!!displayName && /onekey|bixinkey|pro\s*2|pro\b|touch|^k\d|^t\d/i.test(displayName);
|
|
614
|
-
|
|
615
|
-
if (shouldTraceCandidate) {
|
|
616
|
-
Log?.debug('[ReactNativeBleTransport] scan candidate', {
|
|
721
|
+
!isUnnamedIOSPeripheral &&
|
|
722
|
+
!isFindMyPeripheral &&
|
|
723
|
+
isOnekeyBluetoothDevice({
|
|
724
|
+
id: device?.id,
|
|
617
725
|
name: device?.name,
|
|
618
726
|
localName: device?.localName,
|
|
619
|
-
|
|
620
|
-
serviceUUIDs: device?.serviceUUIDs,
|
|
621
|
-
accepted: isOneKey,
|
|
727
|
+
serviceUuids: device?.serviceUUIDs,
|
|
622
728
|
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
729
|
if (isOneKey) {
|
|
626
|
-
Log?.debug('search device start ======================');
|
|
627
|
-
const { name, localName, id, serviceUUIDs } = device ?? {};
|
|
628
|
-
Log?.debug(
|
|
629
|
-
`device name: ${name ?? ''}\nlocalName: ${localName ?? ''}\nid: ${
|
|
630
|
-
id ?? ''
|
|
631
|
-
}\nserviceUUIDs: ${(serviceUUIDs ?? []).join(',')}`
|
|
632
|
-
);
|
|
633
730
|
addDevice(device as unknown as Device);
|
|
634
|
-
Log?.debug('search device end ======================\n');
|
|
635
731
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
636
732
|
Log?.debug('[ReactNativeBleTransport] Pro2-like BLE device was not accepted:', {
|
|
637
733
|
name: device?.name,
|
|
@@ -643,12 +739,32 @@ export default class ReactNativeBleTransport {
|
|
|
643
739
|
}
|
|
644
740
|
);
|
|
645
741
|
|
|
646
|
-
getConnectedDeviceIds(getBluetoothServiceUuids()).then(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
742
|
+
getConnectedDeviceIds(Platform.OS === 'ios' ? getBluetoothServiceUuids() : []).then(
|
|
743
|
+
devices => {
|
|
744
|
+
for (const device of devices) {
|
|
745
|
+
const localName =
|
|
746
|
+
'localName' in device && typeof device.localName === 'string'
|
|
747
|
+
? device.localName
|
|
748
|
+
: null;
|
|
749
|
+
const isFindMyPeripheral =
|
|
750
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
751
|
+
isPro2FindMyAdvertisementName(localName);
|
|
752
|
+
|
|
753
|
+
if (
|
|
754
|
+
!isFindMyPeripheral &&
|
|
755
|
+
isOnekeyBluetoothDevice({
|
|
756
|
+
id: device.id,
|
|
757
|
+
name: device.name,
|
|
758
|
+
localName,
|
|
759
|
+
serviceUuids: device.serviceUUIDs,
|
|
760
|
+
})
|
|
761
|
+
) {
|
|
762
|
+
Log?.debug('search connected peripheral: ', device.id);
|
|
763
|
+
addDevice(device as unknown as Device);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
650
766
|
}
|
|
651
|
-
|
|
767
|
+
);
|
|
652
768
|
|
|
653
769
|
const addDevice = (device: Device) => {
|
|
654
770
|
if (deviceList.every(d => d.id !== device.id)) {
|
|
@@ -662,6 +778,12 @@ export default class ReactNativeBleTransport {
|
|
|
662
778
|
name: displayName,
|
|
663
779
|
commType: 'ble',
|
|
664
780
|
} as IOneKeyDevice);
|
|
781
|
+
Log?.debug('[ReactNativeBleTransport] OneKey BLE device discovered', {
|
|
782
|
+
deviceId: device.id,
|
|
783
|
+
name: displayName,
|
|
784
|
+
serviceUUIDs: device.serviceUUIDs,
|
|
785
|
+
protocolHint,
|
|
786
|
+
});
|
|
665
787
|
}
|
|
666
788
|
};
|
|
667
789
|
|
|
@@ -672,6 +794,79 @@ export default class ReactNativeBleTransport {
|
|
|
672
794
|
});
|
|
673
795
|
}
|
|
674
796
|
|
|
797
|
+
private async installTransportForAcquire(
|
|
798
|
+
uuid: string,
|
|
799
|
+
device: Device,
|
|
800
|
+
characteristics?: ResolvedBleCharacteristics
|
|
801
|
+
) {
|
|
802
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
803
|
+
characteristics ?? (await this.resolveCharacteristicsWithTimeout(uuid, device));
|
|
804
|
+
const transport = new BleTransport(device, writeCharacteristic, notifyCharacteristic);
|
|
805
|
+
transport.mtuSize = typeof device.mtu === 'number' ? device.mtu : undefined;
|
|
806
|
+
const monitorToken = this.nextMonitorToken;
|
|
807
|
+
this.nextMonitorToken += 1;
|
|
808
|
+
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
809
|
+
transport.monitorToken = monitorToken;
|
|
810
|
+
transport.notifyTransactionId = notifyTransactionId;
|
|
811
|
+
this.monitorTokens.set(uuid, monitorToken);
|
|
812
|
+
transport.notifySubscription = this._monitorCharacteristic(
|
|
813
|
+
transport.notifyCharacteristic,
|
|
814
|
+
uuid,
|
|
815
|
+
monitorToken,
|
|
816
|
+
notifyTransactionId
|
|
817
|
+
);
|
|
818
|
+
transportCache[uuid] = transport;
|
|
819
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, new Set());
|
|
820
|
+
this.protocolV2Assemblers.set(
|
|
821
|
+
uuid,
|
|
822
|
+
new ProtocolV2FrameAssembler(PROTOCOL_V2_BLE_FRAME_MAX_BYTES)
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (Platform.OS === 'ios') {
|
|
826
|
+
await new Promise<void>(resolve => {
|
|
827
|
+
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
828
|
+
});
|
|
829
|
+
} else if (Platform.OS === 'android') {
|
|
830
|
+
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const initialMtu = transport.mtuSize;
|
|
834
|
+
let refreshAttempts = 0;
|
|
835
|
+
if (
|
|
836
|
+
(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
837
|
+
shouldRefreshNegotiatedMtu(transport.mtuSize)
|
|
838
|
+
) {
|
|
839
|
+
refreshAttempts += 1;
|
|
840
|
+
let refreshedDevice = await requestNegotiatedMtu(
|
|
841
|
+
transport.device,
|
|
842
|
+
'servicesAndNotifyReady',
|
|
843
|
+
1
|
|
844
|
+
);
|
|
845
|
+
transport.device = refreshedDevice;
|
|
846
|
+
transport.mtuSize =
|
|
847
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
848
|
+
|
|
849
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
850
|
+
await delay(BLE_MTU_REFRESH_RETRY_DELAY_MS);
|
|
851
|
+
refreshAttempts += 1;
|
|
852
|
+
refreshedDevice = await requestNegotiatedMtu(transport.device, 'servicesAndNotifyReady', 2);
|
|
853
|
+
transport.device = refreshedDevice;
|
|
854
|
+
transport.mtuSize =
|
|
855
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
Log?.debug('[ReactNativeBleTransport] BLE MTU ready', {
|
|
860
|
+
platform: Platform.OS,
|
|
861
|
+
requested: getRequestedBleMtu(),
|
|
862
|
+
initial: initialMtu,
|
|
863
|
+
actual: transport.mtuSize,
|
|
864
|
+
refreshAttempts,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return transport;
|
|
868
|
+
}
|
|
869
|
+
|
|
675
870
|
async acquire(input: BleAcquireInput) {
|
|
676
871
|
const { uuid, forceCleanRunPromise, expectedProtocol } = input;
|
|
677
872
|
|
|
@@ -705,8 +900,8 @@ export default class ReactNativeBleTransport {
|
|
|
705
900
|
if (forceCleanRunPromise && this.runPromise) {
|
|
706
901
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
707
902
|
this.runPromise.reject(error);
|
|
708
|
-
this.rejectAllProtocolV2Frames(error);
|
|
709
903
|
this.runPromise = null;
|
|
904
|
+
this.runPromiseDeviceId = null;
|
|
710
905
|
Log?.debug('Force clean Bluetooth run promise, forceCleanRunPromise: ', forceCleanRunPromise);
|
|
711
906
|
}
|
|
712
907
|
|
|
@@ -742,15 +937,22 @@ export default class ReactNativeBleTransport {
|
|
|
742
937
|
if (!device) {
|
|
743
938
|
Log?.debug('try to connect to device: ', uuid);
|
|
744
939
|
try {
|
|
745
|
-
device = await
|
|
940
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
941
|
+
blePlxManager.connectToDevice(uuid, connectOptions)
|
|
942
|
+
);
|
|
746
943
|
} catch (e) {
|
|
747
944
|
Log?.debug('try to connect to device has error: ', e);
|
|
945
|
+
if (isConnectTimeoutError(e)) {
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
748
948
|
if (
|
|
749
949
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
750
950
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
751
951
|
) {
|
|
752
952
|
Log?.debug('first try to reconnect without params');
|
|
753
|
-
device = await
|
|
953
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
954
|
+
blePlxManager.connectToDevice(uuid, fallbackConnectOptions)
|
|
955
|
+
);
|
|
754
956
|
} else if (e.errorCode === BleErrorCode.DeviceAlreadyConnected) {
|
|
755
957
|
Log?.debug('device already connected');
|
|
756
958
|
throw ERRORS.TypedError(HardwareErrorCode.BleAlreadyConnected);
|
|
@@ -766,26 +968,36 @@ export default class ReactNativeBleTransport {
|
|
|
766
968
|
|
|
767
969
|
if (!(await device.isConnected())) {
|
|
768
970
|
Log?.debug('not connected, try to connect to device: ', uuid);
|
|
971
|
+
const disconnectedDevice = device;
|
|
769
972
|
|
|
770
973
|
try {
|
|
771
|
-
device = await
|
|
974
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
975
|
+
disconnectedDevice.connect(connectOptions)
|
|
976
|
+
);
|
|
772
977
|
} catch (e) {
|
|
773
978
|
Log?.debug('not connected, try to connect to device has error: ', e);
|
|
979
|
+
if (isConnectTimeoutError(e)) {
|
|
980
|
+
throw e;
|
|
981
|
+
}
|
|
774
982
|
if (
|
|
775
983
|
e.errorCode === BleErrorCode.DeviceMTUChangeFailed ||
|
|
776
984
|
e.errorCode === BleErrorCode.OperationCancelled
|
|
777
985
|
) {
|
|
778
986
|
Log?.debug('second try to reconnect without params');
|
|
779
987
|
try {
|
|
780
|
-
device = await
|
|
988
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
989
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
990
|
+
);
|
|
781
991
|
} catch (e) {
|
|
782
992
|
Log?.debug('last try to reconnect error: ', e);
|
|
783
993
|
// last try to reconnect device if this issue exists
|
|
784
994
|
// https://github.com/dotintent/react-native-ble-plx/issues/426
|
|
785
995
|
if (e.errorCode === BleErrorCode.OperationCancelled) {
|
|
786
996
|
Log?.debug('last try to reconnect');
|
|
787
|
-
await
|
|
788
|
-
device = await
|
|
997
|
+
await disconnectedDevice.cancelConnection();
|
|
998
|
+
device = await this.connectWithTimeout(uuid, () =>
|
|
999
|
+
disconnectedDevice.connect(fallbackConnectOptions)
|
|
1000
|
+
);
|
|
789
1001
|
}
|
|
790
1002
|
}
|
|
791
1003
|
} else {
|
|
@@ -794,13 +1006,16 @@ export default class ReactNativeBleTransport {
|
|
|
794
1006
|
}
|
|
795
1007
|
}
|
|
796
1008
|
|
|
797
|
-
device = await
|
|
798
|
-
const
|
|
1009
|
+
device = await resolveNegotiatedMtu(device);
|
|
1010
|
+
const acquiredDevice = device;
|
|
1011
|
+
const { writeCharacteristic, notifyCharacteristic } =
|
|
1012
|
+
await this.resolveCharacteristicsWithTimeout(uuid, acquiredDevice);
|
|
799
1013
|
|
|
800
1014
|
const protocolHint = expectedProtocol
|
|
801
1015
|
? undefined
|
|
802
|
-
:
|
|
803
|
-
|
|
1016
|
+
: input.protocolHint ??
|
|
1017
|
+
this.deviceProtocolHints.get(uuid) ??
|
|
1018
|
+
inferProtocolHintFromDeviceName(getDeviceDisplayName(acquiredDevice));
|
|
804
1019
|
|
|
805
1020
|
// release transport before new transport instance
|
|
806
1021
|
await this.release(uuid, true);
|
|
@@ -808,45 +1023,30 @@ export default class ReactNativeBleTransport {
|
|
|
808
1023
|
this.deviceProtocolHints.set(uuid, protocolHint);
|
|
809
1024
|
}
|
|
810
1025
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
const monitorToken = this.nextMonitorToken;
|
|
816
|
-
this.nextMonitorToken += 1;
|
|
817
|
-
const notifyTransactionId = `${uuid}:notify:${monitorToken}`;
|
|
818
|
-
transport.monitorToken = monitorToken;
|
|
819
|
-
transport.notifyTransactionId = notifyTransactionId;
|
|
820
|
-
this.monitorTokens.set(uuid, monitorToken);
|
|
821
|
-
transport.notifySubscription = this._monitorCharacteristic(
|
|
822
|
-
transport.notifyCharacteristic,
|
|
823
|
-
uuid,
|
|
824
|
-
monitorToken,
|
|
825
|
-
notifyTransactionId
|
|
826
|
-
);
|
|
827
|
-
transportCache[uuid] = transport;
|
|
828
|
-
|
|
829
|
-
this.protocolV2Assemblers.set(uuid, new ProtocolV2FrameAssembler());
|
|
830
|
-
|
|
831
|
-
if (Platform.OS === 'ios') {
|
|
832
|
-
await new Promise<void>(resolve => {
|
|
833
|
-
setTimeout(resolve, IOS_NOTIFY_READY_DELAY_MS);
|
|
834
|
-
});
|
|
835
|
-
} else if (Platform.OS === 'android') {
|
|
836
|
-
await delay(ANDROID_NOTIFY_READY_DELAY_MS);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const protocolType = await this.detectProtocol(uuid, expectedProtocol, protocolHint);
|
|
840
|
-
|
|
841
|
-
this.emitter?.emit('device-connect', {
|
|
842
|
-
name: device.name,
|
|
843
|
-
id: device.id,
|
|
844
|
-
connectId: device.id,
|
|
1026
|
+
await this.installTransportForAcquire(uuid, acquiredDevice, {
|
|
1027
|
+
writeCharacteristic,
|
|
1028
|
+
notifyCharacteristic,
|
|
845
1029
|
});
|
|
846
1030
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1031
|
+
try {
|
|
1032
|
+
const protocolType = await this.detectProtocol(
|
|
1033
|
+
uuid,
|
|
1034
|
+
expectedProtocol,
|
|
1035
|
+
protocolHint,
|
|
1036
|
+
async () => {
|
|
1037
|
+
await this.installTransportForAcquire(uuid, acquiredDevice);
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
const currentTransport = transportCache[uuid];
|
|
1041
|
+
if (!currentTransport) {
|
|
1042
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1043
|
+
}
|
|
1044
|
+
this.attachDisconnectSubscription(currentTransport, currentTransport.device, uuid);
|
|
1045
|
+
return { uuid, protocolType };
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
await this.release(uuid, true);
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
850
1050
|
}
|
|
851
1051
|
|
|
852
1052
|
_monitorCharacteristic(
|
|
@@ -873,7 +1073,7 @@ export default class ReactNativeBleTransport {
|
|
|
873
1073
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
874
1074
|
return;
|
|
875
1075
|
}
|
|
876
|
-
if (this.
|
|
1076
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
877
1077
|
let errorCode:
|
|
878
1078
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
879
1079
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -896,7 +1096,7 @@ export default class ReactNativeBleTransport {
|
|
|
896
1096
|
this.rejectProtocolV2Frames(uuid, ERRORS.TypedError(errorCode));
|
|
897
1097
|
return;
|
|
898
1098
|
}
|
|
899
|
-
if (this.runPromise) {
|
|
1099
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
900
1100
|
let ERROR:
|
|
901
1101
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
902
1102
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -919,7 +1119,6 @@ export default class ReactNativeBleTransport {
|
|
|
919
1119
|
HardwareErrorCode.BleCharacteristicNotifyChangeFailure
|
|
920
1120
|
);
|
|
921
1121
|
this.runPromise.reject(notifyError);
|
|
922
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
923
1122
|
Log?.debug(
|
|
924
1123
|
`${HardwareErrorCode.BleCharacteristicNotifyChangeFailure} ${error.message} ${error.reason}`
|
|
925
1124
|
);
|
|
@@ -927,7 +1126,6 @@ export default class ReactNativeBleTransport {
|
|
|
927
1126
|
}
|
|
928
1127
|
const notifyError = ERRORS.TypedError(ERROR);
|
|
929
1128
|
this.runPromise.reject(notifyError);
|
|
930
|
-
this.rejectAllProtocolV2Frames(notifyError);
|
|
931
1129
|
Log?.debug(': monitor notify error, and has unreleased Promise', Error);
|
|
932
1130
|
}
|
|
933
1131
|
|
|
@@ -945,7 +1143,7 @@ export default class ReactNativeBleTransport {
|
|
|
945
1143
|
|
|
946
1144
|
try {
|
|
947
1145
|
const data = Buffer.from(c.value as string, 'base64');
|
|
948
|
-
const protocol = this.
|
|
1146
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
949
1147
|
if (!protocol) {
|
|
950
1148
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
951
1149
|
return;
|
|
@@ -972,14 +1170,16 @@ export default class ReactNativeBleTransport {
|
|
|
972
1170
|
// );
|
|
973
1171
|
bufferLength = 0;
|
|
974
1172
|
buffer = [];
|
|
975
|
-
this.
|
|
1173
|
+
if (this.runPromiseDeviceId === uuid) {
|
|
1174
|
+
this.runPromise?.resolve(value.toString('hex'));
|
|
1175
|
+
}
|
|
976
1176
|
}
|
|
977
1177
|
} catch (error) {
|
|
978
1178
|
Log?.debug('monitor data error: ', error);
|
|
979
1179
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
980
|
-
if (this.
|
|
1180
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
981
1181
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
982
|
-
} else {
|
|
1182
|
+
} else if (this.runPromiseDeviceId === uuid) {
|
|
983
1183
|
this.runPromise?.reject(notifyError);
|
|
984
1184
|
}
|
|
985
1185
|
}
|
|
@@ -989,13 +1189,18 @@ export default class ReactNativeBleTransport {
|
|
|
989
1189
|
}
|
|
990
1190
|
|
|
991
1191
|
async release(uuid: string, onclose = false) {
|
|
992
|
-
const transport = transportCache[uuid];
|
|
993
1192
|
await this.protocolV2Links.invalidateLink(uuid, 'React Native BLE transport released');
|
|
994
|
-
|
|
1193
|
+
return this.releaseNative(uuid, onclose);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
private async releaseNative(uuid: string, onclose = false) {
|
|
1197
|
+
const transport = transportCache[uuid];
|
|
1198
|
+
if (this.runPromise && this.runPromiseDeviceId === uuid) {
|
|
995
1199
|
const error = ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise);
|
|
996
1200
|
this.runPromise.reject(error);
|
|
997
1201
|
this.runPromise = null;
|
|
998
|
-
this.
|
|
1202
|
+
this.runPromiseDeviceId = null;
|
|
1203
|
+
this.rejectProtocolV2Frames(uuid, error);
|
|
999
1204
|
} else {
|
|
1000
1205
|
this.resetProtocolV2Frames(uuid);
|
|
1001
1206
|
}
|
|
@@ -1006,6 +1211,8 @@ export default class ReactNativeBleTransport {
|
|
|
1006
1211
|
return Promise.resolve(true);
|
|
1007
1212
|
}
|
|
1008
1213
|
|
|
1214
|
+
await this.restoreAndroidConnectionPriority(uuid, transport);
|
|
1215
|
+
|
|
1009
1216
|
if (transport) {
|
|
1010
1217
|
if (this.monitorTokens.get(uuid) === transport.monitorToken) {
|
|
1011
1218
|
this.monitorTokens.delete(uuid);
|
|
@@ -1035,8 +1242,11 @@ export default class ReactNativeBleTransport {
|
|
|
1035
1242
|
delete transportCache[uuid];
|
|
1036
1243
|
}
|
|
1037
1244
|
|
|
1245
|
+
this.protocolV2HighVolumeLogSignatures.delete(uuid);
|
|
1246
|
+
|
|
1038
1247
|
this.deviceProtocol.delete(uuid);
|
|
1039
|
-
|
|
1248
|
+
this.probingProtocols.delete(uuid);
|
|
1249
|
+
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1040
1250
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1041
1251
|
this.protocolV2Assemblers.delete(uuid);
|
|
1042
1252
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1075,33 +1285,11 @@ export default class ReactNativeBleTransport {
|
|
|
1075
1285
|
`Device protocol has not been detected for ${uuid}`
|
|
1076
1286
|
);
|
|
1077
1287
|
}
|
|
1078
|
-
// Upload resources on low-end phones may OOM
|
|
1079
|
-
if (name === 'ResourceUpdate' || name === 'ResourceAck') {
|
|
1080
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' data: ', {
|
|
1081
|
-
file_name: data?.file_name,
|
|
1082
|
-
hash: data?.hash,
|
|
1083
|
-
});
|
|
1084
|
-
} else if (LogBlockCommand.has(name)) {
|
|
1085
|
-
Log?.debug('transport-react-native', 'call-', ' name: ', name, ' protocol: ', protocol);
|
|
1086
|
-
} else {
|
|
1087
|
-
Log?.debug(
|
|
1088
|
-
'transport-react-native',
|
|
1089
|
-
'call-',
|
|
1090
|
-
' name: ',
|
|
1091
|
-
name,
|
|
1092
|
-
' data: ',
|
|
1093
|
-
data,
|
|
1094
|
-
' protocol: ',
|
|
1095
|
-
protocol
|
|
1096
|
-
);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
1288
|
if (protocol === 'V2') {
|
|
1100
1289
|
return this.callProtocolV2(uuid, name, data, options);
|
|
1101
1290
|
}
|
|
1102
1291
|
|
|
1103
1292
|
const forceRun = name === 'Initialize' || name === 'Cancel';
|
|
1104
|
-
Log?.debug('transport-react-native call this.runPromise', this.runPromise);
|
|
1105
1293
|
if (this.runPromise && !forceRun) {
|
|
1106
1294
|
throw ERRORS.TypedError(HardwareErrorCode.TransportCallInProgress);
|
|
1107
1295
|
}
|
|
@@ -1122,7 +1310,25 @@ export default class ReactNativeBleTransport {
|
|
|
1122
1310
|
const transport = this.getCachedTransport(uuid);
|
|
1123
1311
|
const runPromise = createDeferred<string>();
|
|
1124
1312
|
runPromise.promise.catch(() => undefined);
|
|
1313
|
+
const supersededRunPromise = this.runPromise;
|
|
1314
|
+
if (supersededRunPromise) {
|
|
1315
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1316
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1317
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1318
|
+
// tear down the shared connection while another call is using it.
|
|
1319
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1320
|
+
}
|
|
1125
1321
|
this.runPromise = runPromise;
|
|
1322
|
+
this.runPromiseDeviceId = uuid;
|
|
1323
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1324
|
+
// only the call that still owns the slot may release it.
|
|
1325
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1326
|
+
if (this.runPromise === runPromise) {
|
|
1327
|
+
this.runPromise = null;
|
|
1328
|
+
this.runPromiseDeviceId = null;
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1126
1332
|
const messages = this._messages;
|
|
1127
1333
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1128
1334
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1148,6 +1354,9 @@ export default class ReactNativeBleTransport {
|
|
|
1148
1354
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1149
1355
|
} catch (e) {
|
|
1150
1356
|
onError(e);
|
|
1357
|
+
if (isWedgedWriteError(e)) {
|
|
1358
|
+
throw e;
|
|
1359
|
+
}
|
|
1151
1360
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1152
1361
|
}
|
|
1153
1362
|
}
|
|
@@ -1179,6 +1388,9 @@ export default class ReactNativeBleTransport {
|
|
|
1179
1388
|
}
|
|
1180
1389
|
} catch (e) {
|
|
1181
1390
|
onError(e);
|
|
1391
|
+
if (isWedgedWriteError(e)) {
|
|
1392
|
+
throw e;
|
|
1393
|
+
}
|
|
1182
1394
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1183
1395
|
}
|
|
1184
1396
|
}
|
|
@@ -1192,21 +1404,26 @@ export default class ReactNativeBleTransport {
|
|
|
1192
1404
|
if (name === 'EmmcFileWrite') {
|
|
1193
1405
|
await writeChunkedData(
|
|
1194
1406
|
buffers,
|
|
1195
|
-
data =>
|
|
1407
|
+
data =>
|
|
1408
|
+
this.writeBlePacket(
|
|
1409
|
+
uuid,
|
|
1410
|
+
data,
|
|
1411
|
+
payload => transport.writeWithRetry(payload),
|
|
1412
|
+
isCurrentOwner
|
|
1413
|
+
),
|
|
1196
1414
|
e => {
|
|
1197
|
-
|
|
1415
|
+
releaseOwnershipIfCurrent();
|
|
1198
1416
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1199
1417
|
}
|
|
1200
1418
|
);
|
|
1201
1419
|
} else if (name === 'FirmwareUpload') {
|
|
1202
|
-
Log?.debug('[ReactNativeBleTransport]
|
|
1420
|
+
Log?.debug('[ReactNativeBleTransport] Firmware upload transport configured', {
|
|
1203
1421
|
packetCapacity: FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY,
|
|
1204
1422
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1205
1423
|
pauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1206
1424
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1207
1425
|
maxRetries: FIRMWARE_UPLOAD_WRITE_MAX_RETRIES,
|
|
1208
1426
|
});
|
|
1209
|
-
|
|
1210
1427
|
await writeFirmwareUploadChunkedData(
|
|
1211
1428
|
buffers,
|
|
1212
1429
|
async data => {
|
|
@@ -1216,43 +1433,31 @@ export default class ReactNativeBleTransport {
|
|
|
1216
1433
|
// eslint-disable-next-line no-constant-condition
|
|
1217
1434
|
while (true) {
|
|
1218
1435
|
try {
|
|
1219
|
-
await
|
|
1436
|
+
await this.writeBlePacket(
|
|
1437
|
+
uuid,
|
|
1438
|
+
data,
|
|
1439
|
+
payload => transport.writeWithRetry(payload),
|
|
1440
|
+
isCurrentOwner
|
|
1441
|
+
);
|
|
1220
1442
|
return;
|
|
1221
1443
|
} catch (error) {
|
|
1222
1444
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
1223
1445
|
if (!retryType || attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1224
1446
|
throw error;
|
|
1225
1447
|
}
|
|
1226
|
-
const
|
|
1227
|
-
const delayMs = shouldReconnect
|
|
1228
|
-
? FIRMWARE_UPLOAD_RECONNECT_RETRY_DELAY_MS
|
|
1229
|
-
: resolveFirmwareUploadRetryDelay(attempt);
|
|
1448
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
1230
1449
|
Log?.debug('[ReactNativeBleTransport] FirmwareUpload write retry:', {
|
|
1231
1450
|
attempt: attempt + 1,
|
|
1232
1451
|
delayMs,
|
|
1233
|
-
reconnect: shouldReconnect,
|
|
1234
1452
|
error,
|
|
1235
1453
|
});
|
|
1236
|
-
if (shouldReconnect) {
|
|
1237
|
-
this.firmwareUploadWriteRecoveryIds.add(uuid);
|
|
1238
|
-
}
|
|
1239
1454
|
await delay(delayMs);
|
|
1240
1455
|
attempt += 1;
|
|
1241
|
-
if (shouldReconnect) {
|
|
1242
|
-
try {
|
|
1243
|
-
await this.reconnectFirmwareUploadTransport(uuid, transport);
|
|
1244
|
-
} catch (e) {
|
|
1245
|
-
Log?.debug('[ReactNativeBleTransport] FirmwareUpload reconnect error:', e);
|
|
1246
|
-
if (attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES) {
|
|
1247
|
-
throw e;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
1456
|
}
|
|
1252
1457
|
}
|
|
1253
1458
|
},
|
|
1254
1459
|
e => {
|
|
1255
|
-
|
|
1460
|
+
releaseOwnershipIfCurrent();
|
|
1256
1461
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1257
1462
|
}
|
|
1258
1463
|
);
|
|
@@ -1260,12 +1465,24 @@ export default class ReactNativeBleTransport {
|
|
|
1260
1465
|
for (const o of buffers) {
|
|
1261
1466
|
const outData = o.toString('base64');
|
|
1262
1467
|
// Upload resources on low-end phones may OOM
|
|
1263
|
-
// this.Log.debug('send hex strting: ', o.toString('hex'));
|
|
1264
1468
|
try {
|
|
1265
|
-
|
|
1469
|
+
const shouldUseWriteWithResponse =
|
|
1470
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1471
|
+
await this.writeBlePacket(
|
|
1472
|
+
uuid,
|
|
1473
|
+
outData,
|
|
1474
|
+
payload =>
|
|
1475
|
+
shouldUseWriteWithResponse
|
|
1476
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
1477
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1478
|
+
isCurrentOwner
|
|
1479
|
+
);
|
|
1266
1480
|
} catch (e) {
|
|
1267
1481
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1268
|
-
|
|
1482
|
+
releaseOwnershipIfCurrent();
|
|
1483
|
+
if (isWedgedWriteError(e)) {
|
|
1484
|
+
throw e;
|
|
1485
|
+
}
|
|
1269
1486
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1270
1487
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1271
1488
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1298,7 +1515,6 @@ export default class ReactNativeBleTransport {
|
|
|
1298
1515
|
throw new Error('Returning data is not string.');
|
|
1299
1516
|
}
|
|
1300
1517
|
|
|
1301
|
-
Log?.debug('receive data: ', response);
|
|
1302
1518
|
const jsonData = ProtocolV1.decodeMessage(messages, response);
|
|
1303
1519
|
return check.call(jsonData);
|
|
1304
1520
|
} catch (e) {
|
|
@@ -1307,11 +1523,25 @@ export default class ReactNativeBleTransport {
|
|
|
1307
1523
|
} else {
|
|
1308
1524
|
Log?.error('call error: ', e);
|
|
1309
1525
|
}
|
|
1526
|
+
const isProbeTimeout =
|
|
1527
|
+
name === 'Initialize' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1528
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1529
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1530
|
+
// call is actively using.
|
|
1531
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1532
|
+
if (
|
|
1533
|
+
!isProbeTimeout &&
|
|
1534
|
+
!isStaleCall &&
|
|
1535
|
+
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1536
|
+
) {
|
|
1537
|
+
await this.disconnect(uuid);
|
|
1538
|
+
}
|
|
1310
1539
|
throw e;
|
|
1311
1540
|
} finally {
|
|
1312
1541
|
if (timeout) clearTimeout(timeout);
|
|
1313
1542
|
if (this.runPromise === runPromise) {
|
|
1314
1543
|
this.runPromise = null;
|
|
1544
|
+
this.runPromiseDeviceId = null;
|
|
1315
1545
|
}
|
|
1316
1546
|
}
|
|
1317
1547
|
}
|
|
@@ -1321,9 +1551,9 @@ export default class ReactNativeBleTransport {
|
|
|
1321
1551
|
}
|
|
1322
1552
|
|
|
1323
1553
|
async disconnect(session: string) {
|
|
1324
|
-
Log?.debug('transport-react-native transport resetSession: ', session);
|
|
1325
1554
|
await this.protocolV2Links.invalidateLink(session, 'React Native BLE transport disconnected');
|
|
1326
1555
|
const transport = transportCache[session];
|
|
1556
|
+
const monitorToken = transport?.monitorToken ?? this.monitorTokens.get(session);
|
|
1327
1557
|
|
|
1328
1558
|
// Clean up disconnect subscription first to prevent onDisconnected callback
|
|
1329
1559
|
// from being triggered when we cancel the connection below
|
|
@@ -1381,20 +1611,22 @@ export default class ReactNativeBleTransport {
|
|
|
1381
1611
|
delete transportCache[session];
|
|
1382
1612
|
}
|
|
1383
1613
|
this.deviceProtocol.delete(session);
|
|
1614
|
+
this.probingProtocols.delete(session);
|
|
1384
1615
|
this.deviceProtocolHints.delete(session);
|
|
1616
|
+
this.sessionProtocols.delete(session);
|
|
1617
|
+
this.protocolReprobeFailures.delete(session);
|
|
1385
1618
|
this.protocolV2Assemblers.delete(session);
|
|
1386
1619
|
this.resetProtocolV2Frames(session);
|
|
1387
1620
|
|
|
1388
1621
|
// emit the disconnect event
|
|
1389
1622
|
try {
|
|
1390
|
-
this.
|
|
1391
|
-
name: transport?.device?.name,
|
|
1392
|
-
id: session,
|
|
1393
|
-
connectId: session,
|
|
1394
|
-
});
|
|
1623
|
+
this.emitDeviceDisconnect(session, transport?.device?.name, monitorToken);
|
|
1395
1624
|
} catch (e) {
|
|
1396
1625
|
Log?.error('resetSession: emit disconnect event error: ', e);
|
|
1397
1626
|
}
|
|
1627
|
+
if (monitorToken !== undefined && this.monitorTokens.get(session) === monitorToken) {
|
|
1628
|
+
this.monitorTokens.delete(session);
|
|
1629
|
+
}
|
|
1398
1630
|
// eslint-disable-next-line no-promise-executor-return
|
|
1399
1631
|
await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
|
|
1400
1632
|
}
|
|
@@ -1405,6 +1637,114 @@ export default class ReactNativeBleTransport {
|
|
|
1405
1637
|
// this.runPromise.reject(new Error('Transport_CallCanceled'));
|
|
1406
1638
|
}
|
|
1407
1639
|
this.runPromise = null;
|
|
1640
|
+
this.runPromiseDeviceId = null;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** Run a native connect under the JS backstop budget. */
|
|
1644
|
+
private async connectWithTimeout<T>(uuid: string, connect: () => Promise<T>): Promise<T> {
|
|
1645
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1646
|
+
let timedOut = false;
|
|
1647
|
+
const pending = connect();
|
|
1648
|
+
// The abandoned attempt keeps running; swallow its late outcome so it cannot
|
|
1649
|
+
// surface as an unhandled rejection after we have already given up on it.
|
|
1650
|
+
pending.catch(() => undefined);
|
|
1651
|
+
try {
|
|
1652
|
+
const result = await Promise.race([
|
|
1653
|
+
pending,
|
|
1654
|
+
new Promise<never>((_, reject) => {
|
|
1655
|
+
timer = setTimeout(() => {
|
|
1656
|
+
timedOut = true;
|
|
1657
|
+
reject(
|
|
1658
|
+
ERRORS.TypedError(
|
|
1659
|
+
HardwareErrorCode.BleConnectedError,
|
|
1660
|
+
`BLE connect timeout after ${BLE_CONNECT_TIMEOUT_MS}ms for ${uuid}`
|
|
1661
|
+
)
|
|
1662
|
+
);
|
|
1663
|
+
}, BLE_CONNECT_TIMEOUT_MS);
|
|
1664
|
+
}),
|
|
1665
|
+
]);
|
|
1666
|
+
return result;
|
|
1667
|
+
} catch (error) {
|
|
1668
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1669
|
+
this.abandonStalledConnection(uuid, timedOut ? 'connect-backstop' : 'connect-native');
|
|
1670
|
+
}
|
|
1671
|
+
throw error;
|
|
1672
|
+
} finally {
|
|
1673
|
+
if (timer) clearTimeout(timer);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** Resolve the complete GATT shape under a budget so acquire() always settles. */
|
|
1678
|
+
private async resolveCharacteristicsWithTimeout(
|
|
1679
|
+
uuid: string,
|
|
1680
|
+
device: Device
|
|
1681
|
+
): Promise<ResolvedBleCharacteristics> {
|
|
1682
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1683
|
+
let timedOut = false;
|
|
1684
|
+
const pending = this.resolveCharacteristics(device);
|
|
1685
|
+
pending.catch(() => undefined);
|
|
1686
|
+
try {
|
|
1687
|
+
const result = await Promise.race([
|
|
1688
|
+
pending,
|
|
1689
|
+
new Promise<never>((_, reject) => {
|
|
1690
|
+
timer = setTimeout(() => {
|
|
1691
|
+
timedOut = true;
|
|
1692
|
+
reject(
|
|
1693
|
+
ERRORS.TypedError(
|
|
1694
|
+
HardwareErrorCode.BleConnectedError,
|
|
1695
|
+
`BLE GATT setup timeout after ${BLE_GATT_SETUP_TIMEOUT_MS}ms for ${uuid}`
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
}, BLE_GATT_SETUP_TIMEOUT_MS);
|
|
1699
|
+
}),
|
|
1700
|
+
]);
|
|
1701
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1702
|
+
return result;
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
if (timedOut || isNativeOperationTimeoutError(error)) {
|
|
1705
|
+
this.abandonStalledConnection(uuid, timedOut ? 'gatt-backstop' : 'gatt-native');
|
|
1706
|
+
}
|
|
1707
|
+
throw error;
|
|
1708
|
+
} finally {
|
|
1709
|
+
if (timer) clearTimeout(timer);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/**
|
|
1714
|
+
* Give up on a BLE setup operation the native layer did not settle. The abandoned
|
|
1715
|
+
* operation still owns native connection/GATT state that can poison the next attempt,
|
|
1716
|
+
* so it is cleared here without awaiting the same queue that stopped responding.
|
|
1717
|
+
*/
|
|
1718
|
+
private abandonStalledConnection(
|
|
1719
|
+
uuid: string,
|
|
1720
|
+
stage: 'connect-backstop' | 'connect-native' | 'gatt-backstop' | 'gatt-native'
|
|
1721
|
+
) {
|
|
1722
|
+
const timeouts = (this.connectionSetupTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1723
|
+
this.connectionSetupTimeoutCounts.set(uuid, timeouts);
|
|
1724
|
+
Log?.error('[ReactNativeBleTransport] BLE setup timed out:', uuid, {
|
|
1725
|
+
stage,
|
|
1726
|
+
setupTimeoutsSinceSuccess: timeouts,
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
this.blePlxManager?.cancelDeviceConnection(uuid).catch(() => {
|
|
1730
|
+
// Rejects with "Operation was cancelled" while merely connecting — expected.
|
|
1731
|
+
});
|
|
1732
|
+
const stalled = transportCache[uuid];
|
|
1733
|
+
if (stalled) {
|
|
1734
|
+
delete transportCache[uuid];
|
|
1735
|
+
}
|
|
1736
|
+
this.deviceProtocol.delete(uuid);
|
|
1737
|
+
this.probingProtocols.delete(uuid);
|
|
1738
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1739
|
+
this.resetProtocolV2Frames(uuid);
|
|
1740
|
+
|
|
1741
|
+
if (timeouts >= BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1742
|
+
// BleManager.destroy() force-rejects every promise the native queue abandoned —
|
|
1743
|
+
// the only JS-reachable way to settle them — and drops all cached peripherals.
|
|
1744
|
+
Log?.error('[ReactNativeBleTransport] BLE setup wedged repeatedly, resetting BLE manager');
|
|
1745
|
+
this.resetPlxManager();
|
|
1746
|
+
this.connectionSetupTimeoutCounts.delete(uuid);
|
|
1747
|
+
}
|
|
1408
1748
|
}
|
|
1409
1749
|
|
|
1410
1750
|
private getCachedTransport(uuid: string) {
|
|
@@ -1415,6 +1755,109 @@ export default class ReactNativeBleTransport {
|
|
|
1415
1755
|
return transport;
|
|
1416
1756
|
}
|
|
1417
1757
|
|
|
1758
|
+
/**
|
|
1759
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1760
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1761
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1762
|
+
* cached and every later call would hang on it again.
|
|
1763
|
+
*/
|
|
1764
|
+
private async writeBlePacket(
|
|
1765
|
+
uuid: string,
|
|
1766
|
+
data: string,
|
|
1767
|
+
write: (payload: string) => Promise<unknown>,
|
|
1768
|
+
isCurrentOwner?: () => boolean
|
|
1769
|
+
) {
|
|
1770
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1771
|
+
let timedOut = false;
|
|
1772
|
+
try {
|
|
1773
|
+
await Promise.race([
|
|
1774
|
+
write(data),
|
|
1775
|
+
new Promise<never>((_, reject) => {
|
|
1776
|
+
timer = setTimeout(() => {
|
|
1777
|
+
timedOut = true;
|
|
1778
|
+
reject(
|
|
1779
|
+
ERRORS.TypedError(
|
|
1780
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1781
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1782
|
+
)
|
|
1783
|
+
);
|
|
1784
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1785
|
+
}),
|
|
1786
|
+
]);
|
|
1787
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
if (timedOut) {
|
|
1790
|
+
// A superseded call's late write must not tear down the link the current
|
|
1791
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1792
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1793
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1794
|
+
} else {
|
|
1795
|
+
this.tearDownWedgedLink(uuid);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
throw error;
|
|
1799
|
+
} finally {
|
|
1800
|
+
if (timer) clearTimeout(timer);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1806
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1807
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1808
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1809
|
+
*/
|
|
1810
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1811
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1812
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1813
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1814
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1815
|
+
});
|
|
1816
|
+
|
|
1817
|
+
const wedged = transportCache[uuid];
|
|
1818
|
+
this.disconnect(uuid).catch(error => {
|
|
1819
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1820
|
+
});
|
|
1821
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1822
|
+
delete transportCache[uuid];
|
|
1823
|
+
}
|
|
1824
|
+
this.deviceProtocol.delete(uuid);
|
|
1825
|
+
this.probingProtocols.delete(uuid);
|
|
1826
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1827
|
+
this.resetProtocolV2Frames(uuid);
|
|
1828
|
+
|
|
1829
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1830
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1831
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1832
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1833
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1834
|
+
this.resetPlxManager();
|
|
1835
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private resetPlxManager() {
|
|
1840
|
+
const manager = this.blePlxManager;
|
|
1841
|
+
this.blePlxManager = undefined;
|
|
1842
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1843
|
+
Object.keys(transportCache).forEach(key => {
|
|
1844
|
+
delete transportCache[key];
|
|
1845
|
+
});
|
|
1846
|
+
this.deviceProtocol.clear();
|
|
1847
|
+
this.probingProtocols.clear();
|
|
1848
|
+
this.sessionProtocols.clear();
|
|
1849
|
+
this.protocolReprobeFailures.clear();
|
|
1850
|
+
this.writeTimeoutCounts.clear();
|
|
1851
|
+
this.connectionSetupTimeoutCounts.clear();
|
|
1852
|
+
this.monitorTokens.clear();
|
|
1853
|
+
this.protocolV2Assemblers.clear();
|
|
1854
|
+
try {
|
|
1855
|
+
manager?.destroy();
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1418
1861
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1419
1862
|
return ERRORS.TypedError(
|
|
1420
1863
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1430,55 +1873,115 @@ export default class ReactNativeBleTransport {
|
|
|
1430
1873
|
}
|
|
1431
1874
|
|
|
1432
1875
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1876
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1877
|
+
this.probingProtocols.delete(uuid);
|
|
1878
|
+
}
|
|
1433
1879
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1434
1880
|
this.deviceProtocol.delete(uuid);
|
|
1435
1881
|
}
|
|
1436
1882
|
}
|
|
1437
1883
|
|
|
1884
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1885
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1886
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1438
1889
|
private async detectProtocol(
|
|
1439
1890
|
uuid: string,
|
|
1440
1891
|
expectedProtocol?: ProtocolType,
|
|
1441
|
-
protocolHint?: ProtocolType
|
|
1892
|
+
protocolHint?: ProtocolType,
|
|
1893
|
+
rebuildTransport?: () => Promise<void>
|
|
1442
1894
|
): Promise<ProtocolType> {
|
|
1895
|
+
if (Platform.OS === 'ios' && expectedProtocol) {
|
|
1896
|
+
this.deviceProtocol.set(uuid, expectedProtocol);
|
|
1897
|
+
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1898
|
+
deviceId: uuid,
|
|
1899
|
+
protocol: expectedProtocol,
|
|
1900
|
+
source: 'expected',
|
|
1901
|
+
});
|
|
1902
|
+
return expectedProtocol;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1443
1905
|
if (expectedProtocol === 'V1') {
|
|
1444
1906
|
if (await this.probeProtocolV1(uuid)) {
|
|
1445
1907
|
this.deviceProtocol.set(uuid, 'V1');
|
|
1446
|
-
|
|
1908
|
+
this.sessionProtocols.set(uuid, 'V1');
|
|
1909
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1910
|
+
deviceId: uuid,
|
|
1911
|
+
protocol: 'V1',
|
|
1912
|
+
source: 'expected',
|
|
1913
|
+
});
|
|
1447
1914
|
return 'V1';
|
|
1448
1915
|
}
|
|
1449
1916
|
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1450
1917
|
}
|
|
1451
1918
|
|
|
1452
1919
|
if (expectedProtocol === 'V2') {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1920
|
+
if (await this.probeProtocolV2(uuid)) {
|
|
1921
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1922
|
+
this.sessionProtocols.set(uuid, 'V2');
|
|
1923
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1924
|
+
deviceId: uuid,
|
|
1925
|
+
protocol: 'V2',
|
|
1926
|
+
source: 'expected',
|
|
1927
|
+
});
|
|
1928
|
+
return 'V2';
|
|
1929
|
+
}
|
|
1930
|
+
throw this.createProtocolMismatchError(expectedProtocol);
|
|
1458
1931
|
}
|
|
1459
1932
|
|
|
1460
|
-
//
|
|
1461
|
-
//
|
|
1462
|
-
|
|
1463
|
-
const
|
|
1933
|
+
// Protocol must be actively probed after connection. Name, PID, and descriptors only
|
|
1934
|
+
// influence probe order; a V2 hint probes V2 first and falls back to V1.
|
|
1935
|
+
const sessionProtocol = this.sessionProtocols.get(uuid);
|
|
1936
|
+
const reprobeFailures = this.protocolReprobeFailures.get(uuid) ?? 0;
|
|
1937
|
+
const fullProbeOrder: ProtocolType[] =
|
|
1464
1938
|
protocolHint === 'V2' || this.deviceProtocol.get(uuid) === 'V2' ? ['V2', 'V1'] : ['V1', 'V2'];
|
|
1939
|
+
// A device that already answered on a protocol in this session keeps answering on
|
|
1940
|
+
// it; while it is rebooting nothing answers at all, so probing the other protocol
|
|
1941
|
+
// only adds its timeout to every poll.
|
|
1942
|
+
const trustSessionProtocol =
|
|
1943
|
+
sessionProtocol !== undefined &&
|
|
1944
|
+
!protocolHint &&
|
|
1945
|
+
reprobeFailures < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS;
|
|
1946
|
+
const probeOrder: ProtocolType[] = trustSessionProtocol ? [sessionProtocol] : fullProbeOrder;
|
|
1465
1947
|
|
|
1466
1948
|
for (let i = 0; i < probeOrder.length; i += 1) {
|
|
1467
1949
|
const protocol = probeOrder[i];
|
|
1468
1950
|
if (i > 0) {
|
|
1469
|
-
//
|
|
1951
|
+
// Reset subscriptions and buffers after a failed probe before trying another protocol.
|
|
1470
1952
|
await this.resetProbeStateAfterProtocolProbe(uuid, probeOrder[i - 1]);
|
|
1953
|
+
if (!transportCache[uuid]) {
|
|
1954
|
+
if (!rebuildTransport) {
|
|
1955
|
+
throw ERRORS.TypedError(HardwareErrorCode.TransportNotFound);
|
|
1956
|
+
}
|
|
1957
|
+
await rebuildTransport();
|
|
1958
|
+
}
|
|
1471
1959
|
}
|
|
1472
1960
|
const detected =
|
|
1473
1961
|
protocol === 'V1' ? await this.probeProtocolV1(uuid) : await this.probeProtocolV2(uuid);
|
|
1474
1962
|
if (detected) {
|
|
1475
1963
|
this.deviceProtocol.set(uuid, protocol);
|
|
1476
|
-
|
|
1964
|
+
this.sessionProtocols.set(uuid, protocol);
|
|
1965
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1966
|
+
Log?.debug('[ReactNativeBleTransport] protocol detected', {
|
|
1967
|
+
deviceId: uuid,
|
|
1968
|
+
protocol,
|
|
1969
|
+
source: 'probe',
|
|
1970
|
+
});
|
|
1477
1971
|
return protocol;
|
|
1478
1972
|
}
|
|
1479
1973
|
}
|
|
1480
1974
|
|
|
1975
|
+
if (trustSessionProtocol) {
|
|
1976
|
+
// Still silent on its own protocol: count it, and let the streak expire the
|
|
1977
|
+
// shortcut so a device that genuinely switched protocols is found again.
|
|
1978
|
+
this.protocolReprobeFailures.set(uuid, reprobeFailures + 1);
|
|
1979
|
+
} else {
|
|
1980
|
+
this.protocolReprobeFailures.delete(uuid);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1481
1983
|
this.deviceProtocol.delete(uuid);
|
|
1984
|
+
this.probingProtocols.delete(uuid);
|
|
1482
1985
|
throw this.createProtocolDetectionError();
|
|
1483
1986
|
}
|
|
1484
1987
|
|
|
@@ -1540,12 +2043,18 @@ export default class ReactNativeBleTransport {
|
|
|
1540
2043
|
}
|
|
1541
2044
|
|
|
1542
2045
|
try {
|
|
1543
|
-
this.
|
|
2046
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1544
2047
|
await this.callProtocolV1(uuid, 'Initialize', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
2048
|
+
this.probingProtocols.delete(uuid);
|
|
1545
2049
|
return true;
|
|
1546
2050
|
} catch (error) {
|
|
1547
2051
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1548
2052
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 Initialize probe failed:', error);
|
|
2053
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
2054
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
2055
|
+
if (isWedgedWriteError(error)) {
|
|
2056
|
+
throw error;
|
|
2057
|
+
}
|
|
1549
2058
|
return false;
|
|
1550
2059
|
}
|
|
1551
2060
|
}
|
|
@@ -1555,7 +2064,7 @@ export default class ReactNativeBleTransport {
|
|
|
1555
2064
|
return false;
|
|
1556
2065
|
}
|
|
1557
2066
|
|
|
1558
|
-
this.
|
|
2067
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1559
2068
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1560
2069
|
const detected = await probeProtocolV2Helper({
|
|
1561
2070
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1570,6 +2079,8 @@ export default class ReactNativeBleTransport {
|
|
|
1570
2079
|
});
|
|
1571
2080
|
if (!detected) {
|
|
1572
2081
|
this.clearProbeProtocol(uuid, 'V2');
|
|
2082
|
+
} else {
|
|
2083
|
+
this.probingProtocols.delete(uuid);
|
|
1573
2084
|
}
|
|
1574
2085
|
return detected;
|
|
1575
2086
|
}
|
|
@@ -1620,17 +2131,8 @@ export default class ReactNativeBleTransport {
|
|
|
1620
2131
|
this.getProtocolV2FrameQueue(uuid).push(frame);
|
|
1621
2132
|
}
|
|
1622
2133
|
|
|
1623
|
-
private rejectAllProtocolV2Frames(error: Error) {
|
|
1624
|
-
this.protocolV2FrameQueues.clear();
|
|
1625
|
-
for (const framePromise of this.protocolV2FramePromises.values()) {
|
|
1626
|
-
framePromise.reject(error);
|
|
1627
|
-
}
|
|
1628
|
-
this.protocolV2FramePromises.clear();
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
2134
|
private resetProtocolV2Frames(uuid: string) {
|
|
1632
|
-
this.
|
|
1633
|
-
this.protocolV2FramePromises.delete(uuid);
|
|
2135
|
+
this.rejectProtocolV2Frames(uuid, new Error(`Protocol V2 frame state reset for ${uuid}`));
|
|
1634
2136
|
}
|
|
1635
2137
|
|
|
1636
2138
|
private rejectProtocolV2Frames(uuid: string, error: Error) {
|
|
@@ -1659,49 +2161,95 @@ export default class ReactNativeBleTransport {
|
|
|
1659
2161
|
}
|
|
1660
2162
|
}
|
|
1661
2163
|
|
|
2164
|
+
private async writeProtocolV2Packet(
|
|
2165
|
+
uuid: string,
|
|
2166
|
+
transport: BleTransport,
|
|
2167
|
+
base64: string,
|
|
2168
|
+
context: ProtocolV2CallContext,
|
|
2169
|
+
assertCurrentGeneration: () => void
|
|
2170
|
+
) {
|
|
2171
|
+
const shouldUseWriteWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2172
|
+
platform: Platform.OS,
|
|
2173
|
+
highThroughput: context.highThroughput,
|
|
2174
|
+
requestedWithResponse: context.writeWithResponse,
|
|
2175
|
+
characteristic: transport.writeCharacteristic,
|
|
2176
|
+
});
|
|
2177
|
+
let attempt = 0;
|
|
2178
|
+
for (;;) {
|
|
2179
|
+
assertCurrentGeneration();
|
|
2180
|
+
if (context.signal.aborted) {
|
|
2181
|
+
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
2182
|
+
}
|
|
2183
|
+
try {
|
|
2184
|
+
await this.writeBlePacket(
|
|
2185
|
+
uuid,
|
|
2186
|
+
base64,
|
|
2187
|
+
payload =>
|
|
2188
|
+
shouldUseWriteWithResponse
|
|
2189
|
+
? transport.writeCharacteristic.writeWithResponse(payload)
|
|
2190
|
+
: transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
2191
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
2192
|
+
// tear down the link that the current generation is using.
|
|
2193
|
+
() => {
|
|
2194
|
+
try {
|
|
2195
|
+
assertCurrentGeneration();
|
|
2196
|
+
return !context.signal.aborted;
|
|
2197
|
+
} catch {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
);
|
|
2202
|
+
assertCurrentGeneration();
|
|
2203
|
+
return;
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
if (
|
|
2206
|
+
getFirmwareUploadWriteRetryType(error) !== 'congested' ||
|
|
2207
|
+
attempt >= FIRMWARE_UPLOAD_WRITE_MAX_RETRIES
|
|
2208
|
+
) {
|
|
2209
|
+
throw error;
|
|
2210
|
+
}
|
|
2211
|
+
const delayMs = resolveFirmwareUploadRetryDelay(attempt);
|
|
2212
|
+
attempt += 1;
|
|
2213
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 congested write retry:', {
|
|
2214
|
+
name: context.messageName,
|
|
2215
|
+
attempt,
|
|
2216
|
+
delayMs,
|
|
2217
|
+
});
|
|
2218
|
+
await delay(delayMs);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1662
2223
|
private async writeProtocolV2Frame(
|
|
2224
|
+
uuid: string,
|
|
1663
2225
|
transport: BleTransport,
|
|
1664
2226
|
frame: Uint8Array,
|
|
1665
|
-
|
|
2227
|
+
context: ProtocolV2CallContext,
|
|
2228
|
+
assertCurrentGeneration: () => void
|
|
1666
2229
|
) {
|
|
1667
2230
|
const tuning = getProtocolV2BleTuning();
|
|
1668
2231
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
1669
2232
|
platform: Platform.OS,
|
|
1670
2233
|
iosPacketLength: tuning.iosPacketLength,
|
|
1671
2234
|
androidPacketLength: tuning.androidPacketLength,
|
|
1672
|
-
mtu:
|
|
2235
|
+
mtu: transport.mtuSize,
|
|
2236
|
+
});
|
|
2237
|
+
await writeProtocolV2BleFrame({
|
|
2238
|
+
frame,
|
|
2239
|
+
packetCapacity,
|
|
2240
|
+
assertActive: assertCurrentGeneration,
|
|
2241
|
+
signal: context.signal,
|
|
2242
|
+
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
2243
|
+
wait: delay,
|
|
2244
|
+
writePacket: packet =>
|
|
2245
|
+
this.writeProtocolV2Packet(
|
|
2246
|
+
uuid,
|
|
2247
|
+
transport,
|
|
2248
|
+
Buffer.from(packet).toString('base64'),
|
|
2249
|
+
context,
|
|
2250
|
+
assertCurrentGeneration
|
|
2251
|
+
),
|
|
1673
2252
|
});
|
|
1674
|
-
const writeWithResponse =
|
|
1675
|
-
!!options?.writeWithResponse || (!!options?.highVolume && tuning.highVolumeWriteWithResponse);
|
|
1676
|
-
const writeMode = resolveBleWriteMode(
|
|
1677
|
-
transport.writeCharacteristic,
|
|
1678
|
-
writeWithResponse ? 'withResponse' : 'withoutResponse'
|
|
1679
|
-
);
|
|
1680
|
-
const shouldThrottle = !!options?.highVolume && writeMode === 'withoutResponse';
|
|
1681
|
-
let packetsWritten = 0;
|
|
1682
|
-
|
|
1683
|
-
for (let offset = 0; offset < frame.length; offset += packetCapacity) {
|
|
1684
|
-
const chunk = frame.slice(offset, offset + packetCapacity);
|
|
1685
|
-
const base64 = Buffer.from(chunk).toString('base64');
|
|
1686
|
-
if (writeMode === 'withResponse') {
|
|
1687
|
-
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1688
|
-
} else {
|
|
1689
|
-
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1690
|
-
}
|
|
1691
|
-
packetsWritten += 1;
|
|
1692
|
-
|
|
1693
|
-
if (
|
|
1694
|
-
shouldThrottle &&
|
|
1695
|
-
packetsWritten % tuning.highVolumeWriteBurstSize === 0 &&
|
|
1696
|
-
offset + packetCapacity < frame.length
|
|
1697
|
-
) {
|
|
1698
|
-
await delay(tuning.highVolumeWritePauseMs);
|
|
1699
|
-
}
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
if (shouldThrottle) {
|
|
1703
|
-
await delay(tuning.highVolumeWriteFlushDelayMs);
|
|
1704
|
-
}
|
|
1705
2253
|
}
|
|
1706
2254
|
|
|
1707
2255
|
private async callProtocolV2(
|
|
@@ -1714,26 +2262,45 @@ export default class ReactNativeBleTransport {
|
|
|
1714
2262
|
throw ERRORS.TypedError(HardwareErrorCode.TransportNotConfigured);
|
|
1715
2263
|
}
|
|
1716
2264
|
|
|
1717
|
-
const callOptions =
|
|
1718
|
-
|
|
1719
|
-
timeoutMs: options?.timeoutMs ?? BLE_RESPONSE_TIMEOUT_MS,
|
|
1720
|
-
};
|
|
1721
|
-
const highVolumeWrite = LogBlockCommand.has(name);
|
|
2265
|
+
const callOptions = options;
|
|
2266
|
+
const highThroughputWrite = isProtocolV2HighThroughputCall(name);
|
|
1722
2267
|
|
|
1723
|
-
if (
|
|
2268
|
+
if (highThroughputWrite) {
|
|
2269
|
+
await this.ensureProtocolV2HighThroughputMtu(uuid);
|
|
1724
2270
|
const tuning = getProtocolV2BleTuning();
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2271
|
+
const currentTransport = this.getCachedTransport(uuid);
|
|
2272
|
+
const writeWithResponse = shouldWriteProtocolV2WithResponse({
|
|
2273
|
+
platform: Platform.OS,
|
|
2274
|
+
highThroughput: true,
|
|
2275
|
+
requestedWithResponse: options?.writeWithResponse,
|
|
2276
|
+
characteristic: currentTransport.writeCharacteristic,
|
|
2277
|
+
});
|
|
2278
|
+
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
2279
|
+
platform: Platform.OS,
|
|
2280
|
+
iosPacketLength: tuning.iosPacketLength,
|
|
2281
|
+
androidPacketLength: tuning.androidPacketLength,
|
|
2282
|
+
mtu: currentTransport.mtuSize,
|
|
2283
|
+
});
|
|
2284
|
+
const writeMode = writeWithResponse ? 'withResponse' : 'withoutResponse';
|
|
2285
|
+
const logSignature = `${name}:${writeMode}:${String(
|
|
2286
|
+
currentTransport.mtuSize
|
|
2287
|
+
)}:${packetCapacity}`;
|
|
2288
|
+
const loggedSignatures =
|
|
2289
|
+
this.protocolV2HighVolumeLogSignatures.get(uuid) ?? new Set<string>();
|
|
2290
|
+
if (!loggedSignatures.has(logSignature)) {
|
|
2291
|
+
loggedSignatures.add(logSignature);
|
|
2292
|
+
this.protocolV2HighVolumeLogSignatures.set(uuid, loggedSignatures);
|
|
2293
|
+
Log?.debug('[ReactNativeBleTransport] Protocol V2 high-volume write configured', {
|
|
2294
|
+
name,
|
|
2295
|
+
writeMode,
|
|
2296
|
+
reportedMtu: currentTransport.mtuSize,
|
|
2297
|
+
packetCapacity,
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
if (highThroughputWrite) {
|
|
2303
|
+
await this.enableAndroidHighConnectionPriority(uuid);
|
|
1737
2304
|
}
|
|
1738
2305
|
|
|
1739
2306
|
try {
|
|
@@ -1747,6 +2314,90 @@ export default class ReactNativeBleTransport {
|
|
|
1747
2314
|
} catch (e) {
|
|
1748
2315
|
Log?.error('[ReactNativeBleTransport] Protocol V2 call error:', e);
|
|
1749
2316
|
throw e;
|
|
2317
|
+
} finally {
|
|
2318
|
+
if (highThroughputWrite) {
|
|
2319
|
+
this.scheduleAndroidBalancedConnectionPriority(uuid);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
private async ensureProtocolV2HighThroughputMtu(uuid: string) {
|
|
2325
|
+
const transport = this.getCachedTransport(uuid);
|
|
2326
|
+
if (!shouldRefreshNegotiatedMtu(transport.mtuSize)) return;
|
|
2327
|
+
|
|
2328
|
+
const refreshedDevice = await requestNegotiatedMtu(transport.device, 'highThroughput', 1);
|
|
2329
|
+
transport.device = refreshedDevice;
|
|
2330
|
+
transport.mtuSize =
|
|
2331
|
+
typeof refreshedDevice.mtu === 'number' ? refreshedDevice.mtu : transport.mtuSize;
|
|
2332
|
+
|
|
2333
|
+
if (shouldRefreshNegotiatedMtu(transport.mtuSize)) {
|
|
2334
|
+
throw ERRORS.TypedError(
|
|
2335
|
+
HardwareErrorCode.BleConnectedError,
|
|
2336
|
+
`Protocol V2 high-throughput BLE MTU unavailable: ${String(transport.mtuSize)}`
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
private clearAndroidPriorityResetTimer(uuid: string) {
|
|
2342
|
+
const timerId = this.androidPriorityResetTimers.get(uuid);
|
|
2343
|
+
if (timerId !== undefined) {
|
|
2344
|
+
clearTimeout(timerId);
|
|
2345
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
private async enableAndroidHighConnectionPriority(uuid: string) {
|
|
2350
|
+
if (Platform.OS !== 'android') return;
|
|
2351
|
+
|
|
2352
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2353
|
+
if (this.androidHighPriorityDevices.has(uuid)) return;
|
|
2354
|
+
|
|
2355
|
+
const transport = transportCache[uuid];
|
|
2356
|
+
if (!transport) return;
|
|
2357
|
+
|
|
2358
|
+
try {
|
|
2359
|
+
transport.device = await transport.device.requestConnectionPriority(ConnectionPriority.High);
|
|
2360
|
+
this.androidHighPriorityDevices.add(uuid);
|
|
2361
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2362
|
+
priority: 'high',
|
|
2363
|
+
});
|
|
2364
|
+
} catch (error) {
|
|
2365
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE high priority request failed', {
|
|
2366
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
private scheduleAndroidBalancedConnectionPriority(uuid: string) {
|
|
2372
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.has(uuid)) return;
|
|
2373
|
+
|
|
2374
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2375
|
+
const timerId = setTimeout(() => {
|
|
2376
|
+
this.androidPriorityResetTimers.delete(uuid);
|
|
2377
|
+
this.restoreAndroidConnectionPriority(uuid, transportCache[uuid]).catch(error =>
|
|
2378
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE priority restore failed', error)
|
|
2379
|
+
);
|
|
2380
|
+
}, ANDROID_HIGH_PRIORITY_IDLE_MS);
|
|
2381
|
+
this.androidPriorityResetTimers.set(uuid, timerId);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
private async restoreAndroidConnectionPriority(uuid: string, transport?: BleTransport) {
|
|
2385
|
+
this.clearAndroidPriorityResetTimer(uuid);
|
|
2386
|
+
if (Platform.OS !== 'android' || !this.androidHighPriorityDevices.delete(uuid) || !transport) {
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
try {
|
|
2391
|
+
transport.device = await transport.device.requestConnectionPriority(
|
|
2392
|
+
ConnectionPriority.Balanced
|
|
2393
|
+
);
|
|
2394
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE connection priority changed', {
|
|
2395
|
+
priority: 'balanced',
|
|
2396
|
+
});
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
Log?.debug('[ReactNativeBleTransport] Android BLE balanced priority request failed', {
|
|
2399
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2400
|
+
});
|
|
1750
2401
|
}
|
|
1751
2402
|
}
|
|
1752
2403
|
|
|
@@ -1767,12 +2418,16 @@ export default class ReactNativeBleTransport {
|
|
|
1767
2418
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1768
2419
|
this.resetProtocolV2Frames(uuid);
|
|
1769
2420
|
},
|
|
1770
|
-
writeFrame: async (frame: Uint8Array, context:
|
|
2421
|
+
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1771
2422
|
assertCurrentGeneration();
|
|
1772
2423
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1773
|
-
await this.writeProtocolV2Frame(
|
|
1774
|
-
|
|
1775
|
-
|
|
2424
|
+
await this.writeProtocolV2Frame(
|
|
2425
|
+
uuid,
|
|
2426
|
+
currentTransport,
|
|
2427
|
+
frame,
|
|
2428
|
+
context,
|
|
2429
|
+
assertCurrentGeneration
|
|
2430
|
+
);
|
|
1776
2431
|
},
|
|
1777
2432
|
readFrame: async () => {
|
|
1778
2433
|
assertCurrentGeneration();
|
|
@@ -1797,6 +2452,6 @@ export default class ReactNativeBleTransport {
|
|
|
1797
2452
|
}
|
|
1798
2453
|
|
|
1799
2454
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1800
|
-
return this.
|
|
2455
|
+
return this.getActiveProtocol(path);
|
|
1801
2456
|
}
|
|
1802
2457
|
}
|