@onekeyfe/hd-transport-react-native 1.2.0-alpha.5 → 1.2.0-alpha.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BleTransport.d.ts +1 -3
- package/dist/BleTransport.d.ts.map +1 -1
- package/dist/bleStrategy.d.ts +0 -2
- package/dist/bleStrategy.d.ts.map +1 -1
- package/dist/constants.d.ts +0 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/index.d.ts +21 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +379 -372
- 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 +2 -43
- package/src/__tests__/BleTransport.test.ts +41 -0
- package/src/__tests__/bleStrategy.test.ts +1 -6
- package/src/__tests__/constants.test.ts +20 -0
- package/src/__tests__/protocolV2Link.test.ts +520 -0
- package/src/__tests__/staleCallTimeout.test.ts +211 -0
- package/src/bleStrategy.ts +0 -25
- package/src/constants.ts +7 -13
- package/src/index.ts +452 -391
- package/src/subscribeBleOn.ts +0 -2
- package/src/transportLog.ts +1 -0
- package/src/types.ts +1 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import ReactNativeBleTransport from '../index';
|
|
4
|
+
|
|
5
|
+
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
6
|
+
|
|
7
|
+
jest.mock(
|
|
8
|
+
'react-native',
|
|
9
|
+
() => ({
|
|
10
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
11
|
+
PermissionsAndroid: {
|
|
12
|
+
PERMISSIONS: {},
|
|
13
|
+
RESULTS: {},
|
|
14
|
+
request: jest.fn(),
|
|
15
|
+
requestMultiple: jest.fn(),
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
{ virtual: true }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
22
|
+
BleATTErrorCode: { InvalidHandle: 1 },
|
|
23
|
+
BleError: Error,
|
|
24
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
25
|
+
BleManager: jest.fn(),
|
|
26
|
+
ScanMode: { LowLatency: 2 },
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
30
|
+
__esModule: true,
|
|
31
|
+
default: {
|
|
32
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
33
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
34
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
const UUID = 'stale-timeout-device';
|
|
39
|
+
|
|
40
|
+
const flush = () =>
|
|
41
|
+
new Promise(resolve => {
|
|
42
|
+
setImmediate(resolve);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function createHarness() {
|
|
46
|
+
const t = new ReactNativeBleTransport({});
|
|
47
|
+
t.configure(messages);
|
|
48
|
+
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
49
|
+
const writeWithoutResponse = jest.fn(() => Promise.resolve());
|
|
50
|
+
const fakeBleTransport = {
|
|
51
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
52
|
+
writeWithRetry: jest.fn(() => Promise.resolve()),
|
|
53
|
+
};
|
|
54
|
+
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
55
|
+
const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
|
|
56
|
+
return { t, disconnectSpy, writeWithoutResponse };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('Protocol V1 stale call timeout', () => {
|
|
60
|
+
beforeAll(() => {
|
|
61
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
jest.useRealTimers();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
jest.clearAllTimers();
|
|
70
|
+
jest.restoreAllMocks();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('superseded Initialize timeout does not tear down the shared transport', async () => {
|
|
74
|
+
const { t, disconnectSpy } = createHarness();
|
|
75
|
+
|
|
76
|
+
// Initialize #1: written while the device reboots, never answered.
|
|
77
|
+
const firstErrors: unknown[] = [];
|
|
78
|
+
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
79
|
+
first.catch(e => firstErrors.push(e));
|
|
80
|
+
await flush();
|
|
81
|
+
|
|
82
|
+
// Initialize #2 (forceRun) supersedes #1 three seconds later.
|
|
83
|
+
jest.advanceTimersByTime(3000);
|
|
84
|
+
const secondErrors: unknown[] = [];
|
|
85
|
+
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
86
|
+
second.catch(e => secondErrors.push(e));
|
|
87
|
+
await flush();
|
|
88
|
+
|
|
89
|
+
// The device answers Initialize #2 (settle its deferred at the transport seam).
|
|
90
|
+
expect(t.runPromise).not.toBeNull();
|
|
91
|
+
t.runPromise?.reject(new Error('settled by device response'));
|
|
92
|
+
await flush();
|
|
93
|
+
|
|
94
|
+
// FirmwareUpload is now the active call on the same transport, awaiting its response.
|
|
95
|
+
const uploadErrors: unknown[] = [];
|
|
96
|
+
const upload = t.call(UUID, 'FirmwareUpload', { payload: Buffer.alloc(300) });
|
|
97
|
+
upload.catch(e => uploadErrors.push(e));
|
|
98
|
+
await flush();
|
|
99
|
+
jest.advanceTimersByTime(100); // firmware upload flush delay
|
|
100
|
+
await flush();
|
|
101
|
+
|
|
102
|
+
// Initialize #1's 25s response timer elapses while the upload is in flight.
|
|
103
|
+
jest.advanceTimersByTime(25000);
|
|
104
|
+
await flush();
|
|
105
|
+
|
|
106
|
+
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
107
|
+
expect(firstErrors).toHaveLength(1);
|
|
108
|
+
expect(uploadErrors).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('forceRun supersede settles the previous pending call immediately', async () => {
|
|
112
|
+
const { t } = createHarness();
|
|
113
|
+
|
|
114
|
+
const firstErrors: Array<{ errorCode?: unknown }> = [];
|
|
115
|
+
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
116
|
+
first.catch(e => firstErrors.push(e));
|
|
117
|
+
await flush();
|
|
118
|
+
|
|
119
|
+
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
120
|
+
second.catch(() => undefined);
|
|
121
|
+
await flush();
|
|
122
|
+
|
|
123
|
+
expect(firstErrors).toHaveLength(1);
|
|
124
|
+
expect(firstErrors[0]?.errorCode).toBe(HardwareErrorCode.BleForceCleanRunPromise);
|
|
125
|
+
|
|
126
|
+
t.runPromise?.reject(new Error('settle second call'));
|
|
127
|
+
await flush();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('late write failure of a superseded call keeps the successor as owner', async () => {
|
|
131
|
+
const { t, disconnectSpy } = createHarness();
|
|
132
|
+
|
|
133
|
+
// First call's write hangs; its promise is controlled by the test.
|
|
134
|
+
let rejectFirstWrite: ((e: Error) => void) | undefined;
|
|
135
|
+
const fakeBleTransport = {
|
|
136
|
+
writeCharacteristic: {
|
|
137
|
+
writeWithoutResponse: jest
|
|
138
|
+
.fn()
|
|
139
|
+
.mockImplementationOnce(
|
|
140
|
+
() =>
|
|
141
|
+
new Promise((_resolve, reject) => {
|
|
142
|
+
rejectFirstWrite = reject;
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
.mockImplementation(() => Promise.resolve()),
|
|
146
|
+
},
|
|
147
|
+
writeWithRetry: jest.fn(() => Promise.resolve()),
|
|
148
|
+
};
|
|
149
|
+
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
150
|
+
|
|
151
|
+
const firstErrors: unknown[] = [];
|
|
152
|
+
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
153
|
+
first.catch(e => firstErrors.push(e));
|
|
154
|
+
await flush();
|
|
155
|
+
|
|
156
|
+
// forceRun successor takes ownership while the first call is stuck writing.
|
|
157
|
+
const secondErrors: unknown[] = [];
|
|
158
|
+
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 5000 });
|
|
159
|
+
second.catch(e => secondErrors.push(e));
|
|
160
|
+
await flush();
|
|
161
|
+
|
|
162
|
+
// The first call's write now fails late; it must not clear the successor's slot.
|
|
163
|
+
rejectFirstWrite?.(new Error('late write failure'));
|
|
164
|
+
await flush();
|
|
165
|
+
|
|
166
|
+
expect(t.runPromise).not.toBeNull();
|
|
167
|
+
|
|
168
|
+
// The successor's genuine timeout must still tear the connection down.
|
|
169
|
+
jest.advanceTimersByTime(5000);
|
|
170
|
+
await flush();
|
|
171
|
+
|
|
172
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
173
|
+
expect(secondErrors).toHaveLength(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('orphan timer left behind by cancel() must not disconnect the transport', async () => {
|
|
177
|
+
const { t, disconnectSpy } = createHarness();
|
|
178
|
+
|
|
179
|
+
// cancel() nulls the ownership slot without settling the deferred, so the
|
|
180
|
+
// call's response timer stays armed (reachable via DeviceCommands.dispose).
|
|
181
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
182
|
+
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
183
|
+
p.catch(e => errors.push(e));
|
|
184
|
+
await flush();
|
|
185
|
+
|
|
186
|
+
t.cancel();
|
|
187
|
+
|
|
188
|
+
jest.advanceTimersByTime(5000);
|
|
189
|
+
await flush();
|
|
190
|
+
|
|
191
|
+
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
192
|
+
expect(errors).toHaveLength(1);
|
|
193
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('timeout on the active call still disconnects the transport', async () => {
|
|
197
|
+
const { t, disconnectSpy } = createHarness();
|
|
198
|
+
|
|
199
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
200
|
+
const p = t.call(UUID, 'GetFeatures', {}, { timeoutMs: 5000 });
|
|
201
|
+
p.catch(e => errors.push(e));
|
|
202
|
+
await flush();
|
|
203
|
+
|
|
204
|
+
jest.advanceTimersByTime(5000);
|
|
205
|
+
await flush();
|
|
206
|
+
|
|
207
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(errors).toHaveLength(1);
|
|
209
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
210
|
+
});
|
|
211
|
+
});
|
package/src/bleStrategy.ts
CHANGED
|
@@ -7,35 +7,10 @@ export type BleWriteCapability = {
|
|
|
7
7
|
isWritableWithoutResponse?: boolean | null;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export type BleWriteMode = 'withResponse' | 'withoutResponse';
|
|
11
|
-
|
|
12
10
|
export function hasWritableCapability(characteristic: BleWriteCapability) {
|
|
13
11
|
return !!(characteristic.isWritableWithResponse || characteristic.isWritableWithoutResponse);
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
export function resolveBleWriteMode(
|
|
17
|
-
characteristic: BleWriteCapability,
|
|
18
|
-
preferredMode: BleWriteMode = 'withoutResponse'
|
|
19
|
-
): BleWriteMode {
|
|
20
|
-
if (preferredMode === 'withoutResponse' && characteristic.isWritableWithoutResponse) {
|
|
21
|
-
return 'withoutResponse';
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (preferredMode === 'withResponse' && characteristic.isWritableWithResponse) {
|
|
25
|
-
return 'withResponse';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (characteristic.isWritableWithoutResponse) {
|
|
29
|
-
return 'withoutResponse';
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (characteristic.isWritableWithResponse) {
|
|
33
|
-
return 'withResponse';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return preferredMode;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
14
|
export function resolveProtocolV2PacketCapacity({
|
|
40
15
|
platform,
|
|
41
16
|
iosPacketLength = IOS_PACKET_LENGTH,
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createKnownBleUuidAliases, matchesKnownBleUuid } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
1
3
|
export const IOS_PACKET_LENGTH = 128;
|
|
2
4
|
export const ANDROID_PACKET_LENGTH = 192;
|
|
3
5
|
export const ANDROID_DEFAULT_MTU = 23;
|
|
@@ -40,7 +42,9 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
|
|
|
40
42
|
const service =
|
|
41
43
|
services[serviceUuid] ??
|
|
42
44
|
Object.values(services).find(
|
|
43
|
-
item =>
|
|
45
|
+
item =>
|
|
46
|
+
normalizeBleUuid(item.serviceUuid) === normalizedServiceUuid ||
|
|
47
|
+
matchesKnownBleUuid(serviceUuid, createKnownBleUuidAliases(item.serviceUuid))
|
|
44
48
|
);
|
|
45
49
|
if (!service) {
|
|
46
50
|
return null;
|
|
@@ -51,17 +55,7 @@ export const getInfosForServiceUuid = (serviceUuid: string, deviceType: 'classic
|
|
|
51
55
|
export const normalizeBleUuid = (uuid?: string | null) =>
|
|
52
56
|
(uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
53
57
|
|
|
54
|
-
export const getBleUuidKey = (uuid?: string | null) => {
|
|
55
|
-
const normalized = normalizeBleUuid(uuid);
|
|
56
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
58
|
export const isSameBleUuid = (left?: string | null, right?: string | null) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
normalizedLeft === normalizedRight ||
|
|
65
|
-
(getBleUuidKey(left) !== '' && getBleUuidKey(left) === getBleUuidKey(right))
|
|
66
|
-
);
|
|
59
|
+
if (!left || !right) return false;
|
|
60
|
+
return matchesKnownBleUuid(left, createKnownBleUuidAliases(right));
|
|
67
61
|
};
|