@onekeyfe/hardware-cli 1.2.0-alpha.33 → 1.2.0-alpha.35
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/cli.d.ts +59 -0
- package/dist/cli.js +129 -117
- package/dist/deviceSelection.d.ts +1 -1
- package/dist/transports/nobleBlePlugin.js +26 -39
- package/package.json +6 -6
- package/src/__tests__/firmware-update-v4-command.test.ts +82 -1
- package/src/__tests__/noble-ble-plugin.test.ts +142 -1
- package/src/cli.ts +138 -130
- package/src/deviceSelection.ts +1 -1
- package/src/transports/nobleBlePlugin.ts +34 -45
package/dist/cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { createSDK } from './sdk';
|
|
2
3
|
declare const program: Command;
|
|
3
4
|
export declare function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble'): 90000 | undefined;
|
|
4
5
|
/**
|
|
@@ -11,6 +12,26 @@ export declare function getLegacyFirmwareConnectTimeout(transport: 'usb' | 'ble'
|
|
|
11
12
|
* After this, globalOpts.passphraseState is set and getCommonParams will include it.
|
|
12
13
|
*/
|
|
13
14
|
export declare function prepareSession(sdk: typeof import('@onekeyfe/hd-common-connect-sdk').default, globalOpts: Record<string, any>): Promise<string | undefined>;
|
|
15
|
+
/**
|
|
16
|
+
* Unified command runner. All `.action(...)` handlers should wrap their body
|
|
17
|
+
* in this helper so every command goes through the same lifecycle:
|
|
18
|
+
*
|
|
19
|
+
* 1. (optional) create SDK
|
|
20
|
+
* 2. (optional) prepareSession — unlock + keychain preload + passphraseState
|
|
21
|
+
* 3. run the handler (which calls outputResult on success)
|
|
22
|
+
* 4. report uncaught errors as a structured failure result
|
|
23
|
+
* 5. dispose SDK
|
|
24
|
+
* 6. let Node exit naturally after all SDK resources are released
|
|
25
|
+
*
|
|
26
|
+
* This fixes three previous bugs:
|
|
27
|
+
* - Most signing commands skipped prepareSession, so keychain sessions
|
|
28
|
+
* were never actually reused.
|
|
29
|
+
* - outputResult scheduled process.exit(200ms) before dispose completed,
|
|
30
|
+
* racing USB release on slower transports.
|
|
31
|
+
* - Errors thrown from SDK calls leaked as stack traces instead of the
|
|
32
|
+
* CLI's structured `{ success: false, payload: { error } }` shape.
|
|
33
|
+
*/
|
|
34
|
+
type AnySdk = Awaited<ReturnType<typeof createSDK>>;
|
|
14
35
|
export declare function buildWallpaperUploadMetrics({ totalBytes, transferredBytes, startedAt, endedAt, lastProgress, }: {
|
|
15
36
|
totalBytes: number;
|
|
16
37
|
transferredBytes: number;
|
|
@@ -24,4 +45,42 @@ export declare function buildWallpaperUploadMetrics({ totalBytes, transferredByt
|
|
|
24
45
|
transferKiBPerSecond: number | null;
|
|
25
46
|
lastProgress: number;
|
|
26
47
|
};
|
|
48
|
+
export declare function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries, }: {
|
|
49
|
+
sdk: AnySdk;
|
|
50
|
+
globalOpts: Record<string, any>;
|
|
51
|
+
params: ReturnType<typeof buildFirmwareUpdateV4Params>;
|
|
52
|
+
retries?: number;
|
|
53
|
+
}): Promise<unknown>;
|
|
54
|
+
declare function buildFirmwareUpdateV4Params(opts: {
|
|
55
|
+
chunkSize?: string;
|
|
56
|
+
resourceBundle?: string[];
|
|
57
|
+
romloader?: string;
|
|
58
|
+
bootloader?: string;
|
|
59
|
+
applicationP1?: string;
|
|
60
|
+
applicationP2?: string;
|
|
61
|
+
coprocessor?: string;
|
|
62
|
+
se01?: string;
|
|
63
|
+
se02?: string;
|
|
64
|
+
se03?: string;
|
|
65
|
+
se04?: string;
|
|
66
|
+
forcedUpdateRes?: boolean;
|
|
67
|
+
}): {
|
|
68
|
+
platform: "desktop";
|
|
69
|
+
connectProtocol: "V2";
|
|
70
|
+
chunkSize: number | undefined;
|
|
71
|
+
forcedUpdateRes: boolean | undefined;
|
|
72
|
+
resourceBundleFiles: {
|
|
73
|
+
binary: ArrayBuffer;
|
|
74
|
+
devicePath: string;
|
|
75
|
+
}[] | undefined;
|
|
76
|
+
romloaderBinary: ArrayBuffer | undefined;
|
|
77
|
+
bootloaderBinary: ArrayBuffer | undefined;
|
|
78
|
+
applicationP1Binary: ArrayBuffer | undefined;
|
|
79
|
+
applicationP2Binary: ArrayBuffer | undefined;
|
|
80
|
+
coprocessorBinary: ArrayBuffer | undefined;
|
|
81
|
+
se01Binary: ArrayBuffer | undefined;
|
|
82
|
+
se02Binary: ArrayBuffer | undefined;
|
|
83
|
+
se03Binary: ArrayBuffer | undefined;
|
|
84
|
+
se04Binary: ArrayBuffer | undefined;
|
|
85
|
+
};
|
|
27
86
|
export { program };
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.program = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
|
|
4
|
+
exports.program = exports.runFirmwareUpdateV4WithRetry = exports.buildWallpaperUploadMetrics = exports.prepareSession = exports.getLegacyFirmwareConnectTimeout = void 0;
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
7
|
const commander_1 = require("commander");
|
|
@@ -633,7 +633,14 @@ sessionCmd
|
|
|
633
633
|
});
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
|
-
const device = searchResult.payload
|
|
636
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult.payload, globalOpts.connectId);
|
|
637
|
+
if (!device) {
|
|
638
|
+
outputResult(globalOpts, {
|
|
639
|
+
success: false,
|
|
640
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
637
644
|
const connectId = device.connectId || globalOpts.connectId;
|
|
638
645
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
639
646
|
// device session, which a locked device will reject with an obscure
|
|
@@ -678,8 +685,7 @@ sessionCmd
|
|
|
678
685
|
.description('Clear cached device session')
|
|
679
686
|
.action(() => runCommand({}, async ({ sdk, globalOpts }) => {
|
|
680
687
|
const searchResult = await sdk.searchDevices();
|
|
681
|
-
const device =
|
|
682
|
-
searchResult?.payload?.[0];
|
|
688
|
+
const device = (0, deviceSelection_1.selectSearchDevice)(searchResult?.payload ?? [], globalOpts.connectId);
|
|
683
689
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
684
690
|
if (deviceId) {
|
|
685
691
|
await (0, session_1.clearSessionFromKeychain)(deviceId);
|
|
@@ -1069,128 +1075,134 @@ async function runFirmwareUpdateV4WithRetry({ sdk, globalOpts, params, retries,
|
|
|
1069
1075
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1070
1076
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1071
1077
|
let currentSdk = sdk;
|
|
1072
|
-
let lastResult;
|
|
1073
1078
|
let retried = false;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
? undefined
|
|
1088
|
-
: globalOpts.connectId;
|
|
1089
|
-
const onUiEvent = (message) => {
|
|
1090
|
-
if (!message || typeof message !== 'object')
|
|
1091
|
-
return;
|
|
1092
|
-
const messageType = message.type;
|
|
1093
|
-
const payload = getFirmwareUpdatePayload(message);
|
|
1094
|
-
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1095
|
-
const tipMessage = payload?.data?.message;
|
|
1096
|
-
if (typeof tipMessage === 'string') {
|
|
1097
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1098
|
-
}
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1102
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1103
|
-
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1107
|
-
return;
|
|
1108
|
-
const progress = Number(payload.progress);
|
|
1109
|
-
if (!Number.isFinite(progress))
|
|
1110
|
-
return;
|
|
1111
|
-
if (payload.progressType === 'transferData') {
|
|
1112
|
-
progressEvents += 1;
|
|
1113
|
-
lastProgress = Math.max(lastProgress, progress);
|
|
1114
|
-
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1115
|
-
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1116
|
-
progressType: 'transfer',
|
|
1117
|
-
progress,
|
|
1118
|
-
payload,
|
|
1119
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1120
|
-
});
|
|
1121
|
-
if (progress >= 100) {
|
|
1122
|
-
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1123
|
-
}
|
|
1124
|
-
return;
|
|
1125
|
-
}
|
|
1126
|
-
if (payload.progressType === 'installingFirmware') {
|
|
1127
|
-
installProgressEvents += 1;
|
|
1128
|
-
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1129
|
-
installStartedAt ?? (installStartedAt = Date.now());
|
|
1130
|
-
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1131
|
-
progressType: 'install',
|
|
1132
|
-
progress,
|
|
1133
|
-
payload,
|
|
1134
|
-
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1135
|
-
});
|
|
1136
|
-
if (progress >= 100) {
|
|
1137
|
-
installEndedAt ?? (installEndedAt = Date.now());
|
|
1138
|
-
}
|
|
1079
|
+
let attempt = 1;
|
|
1080
|
+
let { connectId } = globalOpts;
|
|
1081
|
+
if (globalOpts.transport === 'usb') {
|
|
1082
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1083
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1084
|
+
scope: 'runtime',
|
|
1085
|
+
connectProtocol: 'V2',
|
|
1086
|
+
retryCount: 0,
|
|
1087
|
+
});
|
|
1088
|
+
if (isSuccessResult(probeResult))
|
|
1089
|
+
break;
|
|
1090
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1091
|
+
return probeResult;
|
|
1139
1092
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
currentSdk
|
|
1093
|
+
retried = true;
|
|
1094
|
+
process.stderr.write(`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`);
|
|
1095
|
+
await (0, sdk_1.disposeSDK)();
|
|
1096
|
+
await new Promise(resolve => {
|
|
1097
|
+
setTimeout(resolve, 3000);
|
|
1098
|
+
});
|
|
1099
|
+
currentSdk = await (0, sdk_1.createSDK)(globalOpts);
|
|
1100
|
+
if (globalOpts.connectId)
|
|
1101
|
+
connectId = undefined;
|
|
1147
1102
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1103
|
+
}
|
|
1104
|
+
let progressEvents = 0;
|
|
1105
|
+
let lastProgress = -1;
|
|
1106
|
+
let transferStartedAt;
|
|
1107
|
+
let transferEndedAt;
|
|
1108
|
+
let installProgressEvents = 0;
|
|
1109
|
+
let lastInstallProgress = -1;
|
|
1110
|
+
let installStartedAt;
|
|
1111
|
+
let installEndedAt;
|
|
1112
|
+
let lastPrintedTransferProgress = -10;
|
|
1113
|
+
let lastPrintedInstallProgress = -10;
|
|
1114
|
+
const totalStartedAt = Date.now();
|
|
1115
|
+
const onUiEvent = (message) => {
|
|
1116
|
+
if (!message || typeof message !== 'object')
|
|
1117
|
+
return;
|
|
1118
|
+
const messageType = message.type;
|
|
1119
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1120
|
+
if (messageType === hd_core_1.UI_REQUEST.FIRMWARE_TIP) {
|
|
1121
|
+
const tipMessage = payload?.data?.message;
|
|
1122
|
+
if (typeof tipMessage === 'string') {
|
|
1123
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1150
1126
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
totalStartedAt,
|
|
1156
|
-
transferStartedAt,
|
|
1157
|
-
transferEndedAt,
|
|
1158
|
-
installStartedAt,
|
|
1159
|
-
installEndedAt,
|
|
1160
|
-
progressEvents,
|
|
1161
|
-
lastProgress,
|
|
1162
|
-
installProgressEvents,
|
|
1163
|
-
lastInstallProgress,
|
|
1164
|
-
retried,
|
|
1165
|
-
});
|
|
1166
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1167
|
-
const payload = (lastResult.payload ?? {});
|
|
1168
|
-
lastResult = {
|
|
1169
|
-
...lastResult,
|
|
1170
|
-
payload: {
|
|
1171
|
-
...payload,
|
|
1172
|
-
metrics,
|
|
1173
|
-
},
|
|
1174
|
-
};
|
|
1127
|
+
if (messageType === hd_core_1.UI_REQUEST.REQUEST_BUTTON) {
|
|
1128
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1129
|
+
process.stderr.write(`[onekey-hw] Please confirm the firmware update on your device${code}.\n`);
|
|
1130
|
+
return;
|
|
1175
1131
|
}
|
|
1176
|
-
if (
|
|
1177
|
-
return
|
|
1132
|
+
if (messageType !== hd_core_1.UI_REQUEST.FIRMWARE_PROGRESS || !payload)
|
|
1133
|
+
return;
|
|
1134
|
+
const progress = Number(payload.progress);
|
|
1135
|
+
if (!Number.isFinite(progress))
|
|
1136
|
+
return;
|
|
1137
|
+
if (payload.progressType === 'transferData') {
|
|
1138
|
+
progressEvents += 1;
|
|
1139
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1140
|
+
transferStartedAt ?? (transferStartedAt = Date.now());
|
|
1141
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1142
|
+
progressType: 'transfer',
|
|
1143
|
+
progress,
|
|
1144
|
+
payload,
|
|
1145
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1146
|
+
});
|
|
1147
|
+
if (progress >= 100) {
|
|
1148
|
+
transferEndedAt ?? (transferEndedAt = Date.now());
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1178
1151
|
}
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1152
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1153
|
+
installProgressEvents += 1;
|
|
1154
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1155
|
+
installStartedAt ?? (installStartedAt = Date.now());
|
|
1156
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1157
|
+
progressType: 'install',
|
|
1158
|
+
progress,
|
|
1159
|
+
payload,
|
|
1160
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1161
|
+
});
|
|
1162
|
+
if (progress >= 100) {
|
|
1163
|
+
installEndedAt ?? (installEndedAt = Date.now());
|
|
1164
|
+
}
|
|
1183
1165
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1166
|
+
};
|
|
1167
|
+
currentSdk.on(hd_core_1.UI_EVENT, onUiEvent);
|
|
1168
|
+
let result;
|
|
1169
|
+
try {
|
|
1170
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1171
|
+
}
|
|
1172
|
+
finally {
|
|
1173
|
+
currentSdk.off?.(hd_core_1.UI_EVENT, onUiEvent);
|
|
1174
|
+
}
|
|
1175
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1176
|
+
installEndedAt = Date.now();
|
|
1177
|
+
}
|
|
1178
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1179
|
+
attempt,
|
|
1180
|
+
maxAttempts,
|
|
1181
|
+
totalBytes,
|
|
1182
|
+
totalStartedAt,
|
|
1183
|
+
transferStartedAt,
|
|
1184
|
+
transferEndedAt,
|
|
1185
|
+
installStartedAt,
|
|
1186
|
+
installEndedAt,
|
|
1187
|
+
progressEvents,
|
|
1188
|
+
lastProgress,
|
|
1189
|
+
installProgressEvents,
|
|
1190
|
+
lastInstallProgress,
|
|
1191
|
+
retried,
|
|
1192
|
+
});
|
|
1193
|
+
if (result && typeof result === 'object') {
|
|
1194
|
+
const payload = (result.payload ?? {});
|
|
1195
|
+
return {
|
|
1196
|
+
...result,
|
|
1197
|
+
payload: {
|
|
1198
|
+
...payload,
|
|
1199
|
+
metrics,
|
|
1200
|
+
},
|
|
1201
|
+
};
|
|
1191
1202
|
}
|
|
1192
|
-
return
|
|
1203
|
+
return result;
|
|
1193
1204
|
}
|
|
1205
|
+
exports.runFirmwareUpdateV4WithRetry = runFirmwareUpdateV4WithRetry;
|
|
1194
1206
|
function buildFirmwareUpdateV4Params(opts) {
|
|
1195
1207
|
const params = {
|
|
1196
1208
|
platform: 'desktop',
|
|
@@ -3,13 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createNobleBlePlugin = void 0;
|
|
4
4
|
const hd_shared_1 = require("@onekeyfe/hd-shared");
|
|
5
5
|
const ONEKEY_SERVICE_UUIDS = [hd_shared_1.ONEKEY_SERVICE_UUID];
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
10
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
11
|
-
'0001',
|
|
12
|
-
]);
|
|
6
|
+
const ONEKEY_SERVICE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_SERVICE_UUID);
|
|
7
|
+
const ONEKEY_WRITE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
8
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
13
9
|
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
14
10
|
const DEVICE_SCAN_TIMEOUT = 8000;
|
|
15
11
|
const CONNECTION_TIMEOUT = 8000;
|
|
@@ -24,24 +20,14 @@ const connectedDevices = new Map();
|
|
|
24
20
|
const deviceCharacteristics = new Map();
|
|
25
21
|
const notificationStates = new Map();
|
|
26
22
|
const notificationGenerations = new Map();
|
|
27
|
-
function getBleUuidKey(uuid) {
|
|
28
|
-
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
29
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
30
|
-
}
|
|
31
|
-
function isGenericBleService(uuid) {
|
|
32
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
33
|
-
}
|
|
34
|
-
function hasOneKeyAdvertisementService(peripheral) {
|
|
35
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
36
|
-
return serviceUuids.some(uuid => {
|
|
37
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
38
|
-
return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
39
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
23
|
function isOneKeyPeripheral(peripheral) {
|
|
43
|
-
const
|
|
44
|
-
return (0, hd_shared_1.
|
|
24
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
25
|
+
return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
|
|
26
|
+
(0, hd_shared_1.isOnekeyBluetoothDevice)({
|
|
27
|
+
id: peripheral.id,
|
|
28
|
+
localName: peripheral.advertisement?.localName,
|
|
29
|
+
serviceUuids,
|
|
30
|
+
}));
|
|
45
31
|
}
|
|
46
32
|
function enqueueNotification(deviceId, generation, data) {
|
|
47
33
|
const state = notificationStates.get(deviceId);
|
|
@@ -171,7 +157,7 @@ async function scanDevices(targetDeviceId) {
|
|
|
171
157
|
const onDiscover = (peripheral) => {
|
|
172
158
|
if (targetDeviceId && peripheral.id !== targetDeviceId)
|
|
173
159
|
return;
|
|
174
|
-
if (!
|
|
160
|
+
if (!isOneKeyPeripheral(peripheral))
|
|
175
161
|
return;
|
|
176
162
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
177
163
|
found.set(peripheral.id, peripheral);
|
|
@@ -220,13 +206,7 @@ async function discoverCharacteristics(peripheral) {
|
|
|
220
206
|
resolve(discoveredServices);
|
|
221
207
|
});
|
|
222
208
|
});
|
|
223
|
-
|
|
224
|
-
if (!service) {
|
|
225
|
-
service =
|
|
226
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
227
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
228
|
-
services[0];
|
|
229
|
-
}
|
|
209
|
+
const service = services.find(s => (0, hd_shared_1.matchesKnownBleUuid)(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
230
210
|
if (!service) {
|
|
231
211
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
232
212
|
}
|
|
@@ -243,11 +223,10 @@ async function discoverCharacteristics(peripheral) {
|
|
|
243
223
|
let writeCharacteristic;
|
|
244
224
|
let notifyCharacteristic;
|
|
245
225
|
for (const characteristic of characteristics) {
|
|
246
|
-
|
|
247
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
226
|
+
if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
248
227
|
writeCharacteristic = characteristic;
|
|
249
228
|
}
|
|
250
|
-
else if (
|
|
229
|
+
else if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
251
230
|
notifyCharacteristic = characteristic;
|
|
252
231
|
}
|
|
253
232
|
}
|
|
@@ -335,17 +314,25 @@ function createNobleBlePlugin() {
|
|
|
335
314
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
|
|
336
315
|
}
|
|
337
316
|
await connectPeripheral(peripheral);
|
|
338
|
-
|
|
339
|
-
const notificationState = createNotificationState(uuid);
|
|
317
|
+
let characteristics;
|
|
340
318
|
try {
|
|
319
|
+
characteristics = await discoverCharacteristics(peripheral);
|
|
320
|
+
const notificationState = createNotificationState(uuid);
|
|
341
321
|
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
322
|
+
connectedDevices.set(uuid, peripheral);
|
|
323
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
342
324
|
}
|
|
343
325
|
catch (error) {
|
|
344
326
|
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
327
|
+
if (characteristics) {
|
|
328
|
+
characteristics.notify.removeAllListeners('data');
|
|
329
|
+
await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
|
|
330
|
+
}
|
|
331
|
+
if (peripheral.state !== 'disconnected') {
|
|
332
|
+
await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
|
|
333
|
+
}
|
|
345
334
|
throw error;
|
|
346
335
|
}
|
|
347
|
-
connectedDevices.set(uuid, peripheral);
|
|
348
|
-
deviceCharacteristics.set(uuid, characteristics);
|
|
349
336
|
},
|
|
350
337
|
async disconnect(uuid) {
|
|
351
338
|
await disconnectDevice(uuid);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hardware-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.35",
|
|
4
4
|
"description": "OneKey hardware wallet CLI for testing device communication",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"test": "jest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.
|
|
35
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
36
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
37
|
-
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.
|
|
34
|
+
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.35",
|
|
35
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.35",
|
|
36
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.35",
|
|
37
|
+
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.35",
|
|
38
38
|
"@stoprocent/noble": "2.3.16",
|
|
39
39
|
"commander": "^12.0.0"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "638bd33030686c4a3321e4feb4924c3f9f5c81a8"
|
|
42
42
|
}
|
|
@@ -1,6 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSDK, disposeSDK } from '../sdk';
|
|
2
|
+
import { program, runFirmwareUpdateV4WithRetry } from '../cli';
|
|
3
|
+
|
|
4
|
+
jest.mock('../sdk', () => ({
|
|
5
|
+
createSDK: jest.fn(),
|
|
6
|
+
disposeSDK: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const transientProbeFailure = {
|
|
10
|
+
success: false,
|
|
11
|
+
payload: {
|
|
12
|
+
error: 'Device protocol mismatch: expected V2; device did not respond to expected protocol',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createSdkMock = () => ({
|
|
17
|
+
getDeviceState: jest.fn(),
|
|
18
|
+
firmwareUpdateV4: jest.fn(),
|
|
19
|
+
on: jest.fn(),
|
|
20
|
+
off: jest.fn(),
|
|
21
|
+
});
|
|
2
22
|
|
|
3
23
|
describe('firmware-update-v4 CLI command', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
4
32
|
test('exposes firmware-update-v4 as the formal command', () => {
|
|
5
33
|
const command = program.commands.find(item => item.name() === 'firmware-update-v4');
|
|
6
34
|
|
|
@@ -16,4 +44,57 @@ describe('firmware-update-v4 CLI command', () => {
|
|
|
16
44
|
false
|
|
17
45
|
);
|
|
18
46
|
});
|
|
47
|
+
|
|
48
|
+
test('retries only the read-only USB probe before starting the firmware update', async () => {
|
|
49
|
+
const firstSdk = createSdkMock();
|
|
50
|
+
const retrySdk = createSdkMock();
|
|
51
|
+
firstSdk.getDeviceState.mockResolvedValue(transientProbeFailure);
|
|
52
|
+
retrySdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
53
|
+
retrySdk.firmwareUpdateV4.mockResolvedValue({ success: true, payload: {} });
|
|
54
|
+
jest.mocked(createSDK).mockResolvedValue(retrySdk as never);
|
|
55
|
+
jest.mocked(disposeSDK).mockResolvedValue(undefined);
|
|
56
|
+
jest.spyOn(global, 'setTimeout').mockImplementation(callback => {
|
|
57
|
+
callback();
|
|
58
|
+
return 0 as never;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
62
|
+
sdk: firstSdk as never,
|
|
63
|
+
globalOpts: { transport: 'usb', connectId: 'stale-connect-id' },
|
|
64
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
65
|
+
retries: 1,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(firstSdk.firmwareUpdateV4).not.toHaveBeenCalled();
|
|
69
|
+
expect(disposeSDK).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(retrySdk.getDeviceState).toHaveBeenCalledWith(undefined, {
|
|
71
|
+
scope: 'runtime',
|
|
72
|
+
connectProtocol: 'V2',
|
|
73
|
+
retryCount: 0,
|
|
74
|
+
});
|
|
75
|
+
expect(retrySdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(result).toMatchObject({ success: true, payload: { metrics: { attempt: 2 } } });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('does not replay firmwareUpdateV4 after the read-only probe succeeds', async () => {
|
|
80
|
+
const sdk = createSdkMock();
|
|
81
|
+
sdk.getDeviceState.mockResolvedValue({ success: true, payload: { protocol: 'V2' } });
|
|
82
|
+
sdk.firmwareUpdateV4.mockResolvedValue(transientProbeFailure);
|
|
83
|
+
|
|
84
|
+
const result = await runFirmwareUpdateV4WithRetry({
|
|
85
|
+
sdk: sdk as never,
|
|
86
|
+
globalOpts: { transport: 'usb', connectId: 'pro2-connect-id' },
|
|
87
|
+
params: { applicationP1Binary: new ArrayBuffer(1) } as never,
|
|
88
|
+
retries: 2,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(sdk.getDeviceState).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(sdk.firmwareUpdateV4).toHaveBeenCalledTimes(1);
|
|
93
|
+
expect(disposeSDK).not.toHaveBeenCalled();
|
|
94
|
+
expect(createSDK).not.toHaveBeenCalled();
|
|
95
|
+
expect(result).toMatchObject({
|
|
96
|
+
success: false,
|
|
97
|
+
payload: { error: transientProbeFailure.payload.error, metrics: { attempt: 1 } },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
19
100
|
});
|
|
@@ -33,12 +33,13 @@ const createPeripheral = (id: string) => {
|
|
|
33
33
|
state: 'connected',
|
|
34
34
|
advertisement: {
|
|
35
35
|
localName: `OneKey Pro 2 ${id}`,
|
|
36
|
-
serviceUuids: ['
|
|
36
|
+
serviceUuids: ['0001'],
|
|
37
37
|
},
|
|
38
38
|
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
39
|
connect: jest.fn(callback => callback()),
|
|
40
40
|
disconnect: jest.fn(callback => callback()),
|
|
41
41
|
},
|
|
42
|
+
service,
|
|
42
43
|
write,
|
|
43
44
|
notify,
|
|
44
45
|
};
|
|
@@ -46,10 +47,74 @@ const createPeripheral = (id: string) => {
|
|
|
46
47
|
|
|
47
48
|
describe('Noble BLE plugin notification routing', () => {
|
|
48
49
|
afterEach(() => {
|
|
50
|
+
jest.useRealTimers();
|
|
49
51
|
jest.resetModules();
|
|
50
52
|
jest.clearAllMocks();
|
|
51
53
|
});
|
|
52
54
|
|
|
55
|
+
test('does not enumerate Find My advertisements that expose FFFD', async () => {
|
|
56
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
57
|
+
const oneKey = createPeripheral('onekey-device');
|
|
58
|
+
const findMy = createPeripheral('find-my-device');
|
|
59
|
+
findMy.peripheral.advertisement.localName = 'Find My';
|
|
60
|
+
findMy.peripheral.advertisement.serviceUuids = ['fffd'];
|
|
61
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
62
|
+
state: string;
|
|
63
|
+
startScanning: jest.Mock;
|
|
64
|
+
stopScanning: jest.Mock;
|
|
65
|
+
};
|
|
66
|
+
noble.state = 'poweredOn';
|
|
67
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
68
|
+
callback?.();
|
|
69
|
+
noble.emit('discover', findMy.peripheral);
|
|
70
|
+
noble.emit('discover', oneKey.peripheral);
|
|
71
|
+
});
|
|
72
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
73
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
74
|
+
|
|
75
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
76
|
+
const plugin = createNobleBlePlugin();
|
|
77
|
+
await plugin.init();
|
|
78
|
+
const devicesPromise = plugin.enumerate();
|
|
79
|
+
await Promise.resolve();
|
|
80
|
+
jest.runAllTimers();
|
|
81
|
+
await Promise.resolve();
|
|
82
|
+
|
|
83
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
84
|
+
expect.objectContaining({ id: 'onekey-device' }),
|
|
85
|
+
]);
|
|
86
|
+
jest.useRealTimers();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('does not enumerate a name-only candidate without the communication service', async () => {
|
|
90
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
91
|
+
const nameOnly = createPeripheral('name-only-device');
|
|
92
|
+
nameOnly.peripheral.advertisement.serviceUuids = [];
|
|
93
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
94
|
+
state: string;
|
|
95
|
+
startScanning: jest.Mock;
|
|
96
|
+
stopScanning: jest.Mock;
|
|
97
|
+
};
|
|
98
|
+
noble.state = 'poweredOn';
|
|
99
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
100
|
+
callback?.();
|
|
101
|
+
noble.emit('discover', nameOnly.peripheral);
|
|
102
|
+
});
|
|
103
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
104
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
105
|
+
|
|
106
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
107
|
+
const plugin = createNobleBlePlugin();
|
|
108
|
+
await plugin.init();
|
|
109
|
+
const devicesPromise = plugin.enumerate();
|
|
110
|
+
await Promise.resolve();
|
|
111
|
+
jest.runAllTimers();
|
|
112
|
+
await Promise.resolve();
|
|
113
|
+
|
|
114
|
+
await expect(devicesPromise).resolves.toEqual([]);
|
|
115
|
+
jest.useRealTimers();
|
|
116
|
+
});
|
|
117
|
+
|
|
53
118
|
test('routes notifications to the receiver waiting for the same device', async () => {
|
|
54
119
|
const deviceA = createPeripheral('device-a');
|
|
55
120
|
const deviceB = createPeripheral('device-b');
|
|
@@ -112,6 +177,82 @@ describe('Noble BLE plugin notification routing', () => {
|
|
|
112
177
|
expect(result).toBe('completed');
|
|
113
178
|
});
|
|
114
179
|
|
|
180
|
+
test('disconnects an untracked peripheral when service discovery fails', async () => {
|
|
181
|
+
const device = createPeripheral('device-a');
|
|
182
|
+
device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
|
|
183
|
+
callback(new Error('service discovery failed'))
|
|
184
|
+
);
|
|
185
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
186
|
+
state: string;
|
|
187
|
+
startScanning: jest.Mock;
|
|
188
|
+
stopScanning: jest.Mock;
|
|
189
|
+
};
|
|
190
|
+
noble.state = 'poweredOn';
|
|
191
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
192
|
+
callback?.();
|
|
193
|
+
noble.emit('discover', device.peripheral);
|
|
194
|
+
});
|
|
195
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
196
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
197
|
+
|
|
198
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
199
|
+
const plugin = createNobleBlePlugin();
|
|
200
|
+
await plugin.init();
|
|
201
|
+
|
|
202
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('service discovery failed');
|
|
203
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('rejects a vendor-specific service containing the OneKey short UUID', async () => {
|
|
207
|
+
const device = createPeripheral('device-a');
|
|
208
|
+
device.service.uuid = 'abcd0001-1234-5678-9012-abcdefabcdef';
|
|
209
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
210
|
+
state: string;
|
|
211
|
+
startScanning: jest.Mock;
|
|
212
|
+
stopScanning: jest.Mock;
|
|
213
|
+
};
|
|
214
|
+
noble.state = 'poweredOn';
|
|
215
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
216
|
+
callback?.();
|
|
217
|
+
noble.emit('discover', device.peripheral);
|
|
218
|
+
});
|
|
219
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
220
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
221
|
+
|
|
222
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
223
|
+
const plugin = createNobleBlePlugin();
|
|
224
|
+
await plugin.init();
|
|
225
|
+
|
|
226
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('No BLE service found');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('unsubscribes and disconnects an untracked peripheral when notification setup fails', async () => {
|
|
230
|
+
const device = createPeripheral('device-a');
|
|
231
|
+
device.notify.subscribe.mockImplementation(callback =>
|
|
232
|
+
callback(new Error('notification setup failed'))
|
|
233
|
+
);
|
|
234
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
235
|
+
state: string;
|
|
236
|
+
startScanning: jest.Mock;
|
|
237
|
+
stopScanning: jest.Mock;
|
|
238
|
+
};
|
|
239
|
+
noble.state = 'poweredOn';
|
|
240
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
241
|
+
callback?.();
|
|
242
|
+
noble.emit('discover', device.peripheral);
|
|
243
|
+
});
|
|
244
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
245
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
246
|
+
|
|
247
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
248
|
+
const plugin = createNobleBlePlugin();
|
|
249
|
+
await plugin.init();
|
|
250
|
+
|
|
251
|
+
await expect(plugin.connect('device-a')).rejects.toThrow('notification setup failed');
|
|
252
|
+
expect(device.notify.unsubscribe).toHaveBeenCalled();
|
|
253
|
+
expect(device.peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
115
256
|
test('uses withoutResponse for normal and high-volume writes', async () => {
|
|
116
257
|
const device = createPeripheral('device-a');
|
|
117
258
|
const noble = new EventEmitter() as EventEmitter & {
|
package/src/cli.ts
CHANGED
|
@@ -808,7 +808,17 @@ sessionCmd
|
|
|
808
808
|
});
|
|
809
809
|
return;
|
|
810
810
|
}
|
|
811
|
-
const device =
|
|
811
|
+
const device = selectSearchDevice(
|
|
812
|
+
searchResult.payload as Array<SearchDevice & { features?: Features }>,
|
|
813
|
+
globalOpts.connectId
|
|
814
|
+
);
|
|
815
|
+
if (!device) {
|
|
816
|
+
outputResult(globalOpts, {
|
|
817
|
+
success: false,
|
|
818
|
+
payload: { error: 'No matching device found', code: 'NO_DEVICE' },
|
|
819
|
+
});
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
812
822
|
const connectId = device.connectId || globalOpts.connectId;
|
|
813
823
|
|
|
814
824
|
// 2. Unlock if locked — getPassphraseState below talks to a live
|
|
@@ -860,8 +870,10 @@ sessionCmd
|
|
|
860
870
|
.action(() =>
|
|
861
871
|
runCommand({}, async ({ sdk, globalOpts }) => {
|
|
862
872
|
const searchResult = await sdk.searchDevices();
|
|
863
|
-
const device =
|
|
864
|
-
(searchResult?.payload as
|
|
873
|
+
const device = selectSearchDevice(
|
|
874
|
+
(searchResult?.payload as Array<SearchDevice & { features?: Features }>) ?? [],
|
|
875
|
+
globalOpts.connectId
|
|
876
|
+
);
|
|
865
877
|
const deviceId = device?.deviceId || device?.features?.device_id;
|
|
866
878
|
if (deviceId) {
|
|
867
879
|
await clearSessionFromKeychain(deviceId);
|
|
@@ -1433,7 +1445,7 @@ function buildFirmwareUpdateV4Metrics({
|
|
|
1433
1445
|
};
|
|
1434
1446
|
}
|
|
1435
1447
|
|
|
1436
|
-
async function runFirmwareUpdateV4WithRetry({
|
|
1448
|
+
export async function runFirmwareUpdateV4WithRetry({
|
|
1437
1449
|
sdk,
|
|
1438
1450
|
globalOpts,
|
|
1439
1451
|
params,
|
|
@@ -1447,147 +1459,143 @@ async function runFirmwareUpdateV4WithRetry({
|
|
|
1447
1459
|
const totalBytes = getFirmwareUpdateV4TotalBytes(params);
|
|
1448
1460
|
const maxAttempts = Math.max((retries ?? 2) + 1, 1);
|
|
1449
1461
|
let currentSdk = sdk;
|
|
1450
|
-
let lastResult: unknown;
|
|
1451
1462
|
let retried = false;
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
const connectId =
|
|
1466
|
-
retried && globalOpts.transport === 'usb' && globalOpts.connectId
|
|
1467
|
-
? undefined
|
|
1468
|
-
: globalOpts.connectId;
|
|
1469
|
-
|
|
1470
|
-
const onUiEvent = (message: unknown) => {
|
|
1471
|
-
if (!message || typeof message !== 'object') return;
|
|
1472
|
-
const messageType = (message as { type?: string }).type;
|
|
1473
|
-
const payload = getFirmwareUpdatePayload(message);
|
|
1474
|
-
|
|
1475
|
-
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1476
|
-
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1477
|
-
if (typeof tipMessage === 'string') {
|
|
1478
|
-
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1479
|
-
}
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1484
|
-
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1485
|
-
process.stderr.write(
|
|
1486
|
-
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1487
|
-
);
|
|
1488
|
-
return;
|
|
1463
|
+
let attempt = 1;
|
|
1464
|
+
let { connectId } = globalOpts;
|
|
1465
|
+
|
|
1466
|
+
if (globalOpts.transport === 'usb') {
|
|
1467
|
+
for (; attempt <= maxAttempts; attempt += 1) {
|
|
1468
|
+
const probeResult = await currentSdk.getDeviceState(connectId, {
|
|
1469
|
+
scope: 'runtime',
|
|
1470
|
+
connectProtocol: 'V2',
|
|
1471
|
+
retryCount: 0,
|
|
1472
|
+
});
|
|
1473
|
+
if (isSuccessResult(probeResult)) break;
|
|
1474
|
+
if (attempt >= maxAttempts || !isProtocolV2UsbProbeTransientResult(probeResult)) {
|
|
1475
|
+
return probeResult;
|
|
1489
1476
|
}
|
|
1490
1477
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1504
|
-
});
|
|
1505
|
-
if (progress >= 100) {
|
|
1506
|
-
transferEndedAt ??= Date.now();
|
|
1507
|
-
}
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1478
|
+
retried = true;
|
|
1479
|
+
process.stderr.write(
|
|
1480
|
+
`[onekey-hw] Protocol V2 USB probe was transient; retrying read-only probe (${attempt}/${maxAttempts})...\n`
|
|
1481
|
+
);
|
|
1482
|
+
await disposeSDK();
|
|
1483
|
+
await new Promise(resolve => {
|
|
1484
|
+
setTimeout(resolve, 3000);
|
|
1485
|
+
});
|
|
1486
|
+
currentSdk = await createSDK(globalOpts);
|
|
1487
|
+
if (globalOpts.connectId) connectId = undefined;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1510
1490
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1491
|
+
let progressEvents = 0;
|
|
1492
|
+
let lastProgress = -1;
|
|
1493
|
+
let transferStartedAt: number | undefined;
|
|
1494
|
+
let transferEndedAt: number | undefined;
|
|
1495
|
+
let installProgressEvents = 0;
|
|
1496
|
+
let lastInstallProgress = -1;
|
|
1497
|
+
let installStartedAt: number | undefined;
|
|
1498
|
+
let installEndedAt: number | undefined;
|
|
1499
|
+
let lastPrintedTransferProgress = -10;
|
|
1500
|
+
let lastPrintedInstallProgress = -10;
|
|
1501
|
+
const totalStartedAt = Date.now();
|
|
1502
|
+
|
|
1503
|
+
const onUiEvent = (message: unknown) => {
|
|
1504
|
+
if (!message || typeof message !== 'object') return;
|
|
1505
|
+
const messageType = (message as { type?: string }).type;
|
|
1506
|
+
const payload = getFirmwareUpdatePayload(message);
|
|
1507
|
+
|
|
1508
|
+
if (messageType === UI_REQUEST.FIRMWARE_TIP) {
|
|
1509
|
+
const tipMessage = (payload?.data as { message?: unknown } | undefined)?.message;
|
|
1510
|
+
if (typeof tipMessage === 'string') {
|
|
1511
|
+
process.stderr.write(`[onekey-hw] Firmware: ${tipMessage}\n`);
|
|
1524
1512
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1528
|
-
try {
|
|
1529
|
-
lastResult = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1530
|
-
} finally {
|
|
1531
|
-
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1532
|
-
}
|
|
1533
|
-
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1534
|
-
installEndedAt = Date.now();
|
|
1513
|
+
return;
|
|
1535
1514
|
}
|
|
1536
1515
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
transferEndedAt,
|
|
1544
|
-
installStartedAt,
|
|
1545
|
-
installEndedAt,
|
|
1546
|
-
progressEvents,
|
|
1547
|
-
lastProgress,
|
|
1548
|
-
installProgressEvents,
|
|
1549
|
-
lastInstallProgress,
|
|
1550
|
-
retried,
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
if (lastResult && typeof lastResult === 'object') {
|
|
1554
|
-
const payload = ((lastResult as { payload?: unknown }).payload ?? {}) as Record<
|
|
1555
|
-
string,
|
|
1556
|
-
unknown
|
|
1557
|
-
>;
|
|
1558
|
-
lastResult = {
|
|
1559
|
-
...(lastResult as Record<string, unknown>),
|
|
1560
|
-
payload: {
|
|
1561
|
-
...payload,
|
|
1562
|
-
metrics,
|
|
1563
|
-
},
|
|
1564
|
-
};
|
|
1516
|
+
if (messageType === UI_REQUEST.REQUEST_BUTTON) {
|
|
1517
|
+
const code = typeof payload?.code === 'string' ? ` (${payload.code})` : '';
|
|
1518
|
+
process.stderr.write(
|
|
1519
|
+
`[onekey-hw] Please confirm the firmware update on your device${code}.\n`
|
|
1520
|
+
);
|
|
1521
|
+
return;
|
|
1565
1522
|
}
|
|
1566
1523
|
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1524
|
+
if (messageType !== UI_REQUEST.FIRMWARE_PROGRESS || !payload) return;
|
|
1525
|
+
const progress = Number(payload.progress);
|
|
1526
|
+
if (!Number.isFinite(progress)) return;
|
|
1527
|
+
|
|
1528
|
+
if (payload.progressType === 'transferData') {
|
|
1529
|
+
progressEvents += 1;
|
|
1530
|
+
lastProgress = Math.max(lastProgress, progress);
|
|
1531
|
+
transferStartedAt ??= Date.now();
|
|
1532
|
+
lastPrintedTransferProgress = maybePrintFirmwareProgress({
|
|
1533
|
+
progressType: 'transfer',
|
|
1534
|
+
progress,
|
|
1535
|
+
payload,
|
|
1536
|
+
lastPrintedProgress: lastPrintedTransferProgress,
|
|
1537
|
+
});
|
|
1538
|
+
if (progress >= 100) {
|
|
1539
|
+
transferEndedAt ??= Date.now();
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1569
1542
|
}
|
|
1570
1543
|
|
|
1571
|
-
if (
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1544
|
+
if (payload.progressType === 'installingFirmware') {
|
|
1545
|
+
installProgressEvents += 1;
|
|
1546
|
+
lastInstallProgress = Math.max(lastInstallProgress, progress);
|
|
1547
|
+
installStartedAt ??= Date.now();
|
|
1548
|
+
lastPrintedInstallProgress = maybePrintFirmwareProgress({
|
|
1549
|
+
progressType: 'install',
|
|
1550
|
+
progress,
|
|
1551
|
+
payload,
|
|
1552
|
+
lastPrintedProgress: lastPrintedInstallProgress,
|
|
1553
|
+
});
|
|
1554
|
+
if (progress >= 100) {
|
|
1555
|
+
installEndedAt ??= Date.now();
|
|
1556
|
+
}
|
|
1577
1557
|
}
|
|
1558
|
+
};
|
|
1578
1559
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
);
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1560
|
+
currentSdk.on(UI_EVENT, onUiEvent);
|
|
1561
|
+
let result: unknown;
|
|
1562
|
+
try {
|
|
1563
|
+
result = await currentSdk.firmwareUpdateV4(connectId, params);
|
|
1564
|
+
} finally {
|
|
1565
|
+
currentSdk.off?.(UI_EVENT, onUiEvent);
|
|
1566
|
+
}
|
|
1567
|
+
if (installStartedAt !== undefined && installEndedAt === undefined) {
|
|
1568
|
+
installEndedAt = Date.now();
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const metrics = buildFirmwareUpdateV4Metrics({
|
|
1572
|
+
attempt,
|
|
1573
|
+
maxAttempts,
|
|
1574
|
+
totalBytes,
|
|
1575
|
+
totalStartedAt,
|
|
1576
|
+
transferStartedAt,
|
|
1577
|
+
transferEndedAt,
|
|
1578
|
+
installStartedAt,
|
|
1579
|
+
installEndedAt,
|
|
1580
|
+
progressEvents,
|
|
1581
|
+
lastProgress,
|
|
1582
|
+
installProgressEvents,
|
|
1583
|
+
lastInstallProgress,
|
|
1584
|
+
retried,
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
if (result && typeof result === 'object') {
|
|
1588
|
+
const payload = ((result as { payload?: unknown }).payload ?? {}) as Record<string, unknown>;
|
|
1589
|
+
return {
|
|
1590
|
+
...(result as Record<string, unknown>),
|
|
1591
|
+
payload: {
|
|
1592
|
+
...payload,
|
|
1593
|
+
metrics,
|
|
1594
|
+
},
|
|
1595
|
+
};
|
|
1588
1596
|
}
|
|
1589
1597
|
|
|
1590
|
-
return
|
|
1598
|
+
return result;
|
|
1591
1599
|
}
|
|
1592
1600
|
|
|
1593
1601
|
function buildFirmwareUpdateV4Params(opts: {
|
package/src/deviceSelection.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ERRORS,
|
|
3
3
|
HardwareErrorCode,
|
|
4
|
+
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
4
5
|
ONEKEY_SERVICE_UUID,
|
|
5
|
-
|
|
6
|
+
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
7
|
+
createKnownBleUuidAliases,
|
|
8
|
+
hasOnekeyCommunicationService,
|
|
9
|
+
isOnekeyBluetoothDevice,
|
|
10
|
+
matchesKnownBleUuid,
|
|
6
11
|
} from '@onekeyfe/hd-shared';
|
|
7
12
|
|
|
8
13
|
import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
|
|
@@ -39,13 +44,9 @@ type NobleNotificationState = {
|
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
46
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
47
|
-
'0001',
|
|
48
|
-
]);
|
|
47
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
48
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
49
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
49
50
|
|
|
50
51
|
const BLUETOOTH_INIT_TIMEOUT = 10_000;
|
|
51
52
|
const DEVICE_SCAN_TIMEOUT = 8_000;
|
|
@@ -63,29 +64,16 @@ const deviceCharacteristics = new Map<string, CharacteristicPair>();
|
|
|
63
64
|
const notificationStates = new Map<string, NobleNotificationState>();
|
|
64
65
|
const notificationGenerations = new Map<string, number>();
|
|
65
66
|
|
|
66
|
-
function getBleUuidKey(uuid?: string | null) {
|
|
67
|
-
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
68
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isGenericBleService(uuid?: string | null) {
|
|
72
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function hasOneKeyAdvertisementService(peripheral: Peripheral) {
|
|
76
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
77
|
-
return serviceUuids.some(uuid => {
|
|
78
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
79
|
-
return (
|
|
80
|
-
NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
81
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey)
|
|
82
|
-
);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
67
|
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
87
|
-
const
|
|
88
|
-
return
|
|
68
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
69
|
+
return (
|
|
70
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
71
|
+
isOnekeyBluetoothDevice({
|
|
72
|
+
id: peripheral.id,
|
|
73
|
+
localName: peripheral.advertisement?.localName,
|
|
74
|
+
serviceUuids,
|
|
75
|
+
})
|
|
76
|
+
);
|
|
89
77
|
}
|
|
90
78
|
|
|
91
79
|
function enqueueNotification(deviceId: string, generation: number, data: Buffer) {
|
|
@@ -230,7 +218,7 @@ async function scanDevices(targetDeviceId?: string) {
|
|
|
230
218
|
|
|
231
219
|
const onDiscover = (peripheral: Peripheral) => {
|
|
232
220
|
if (targetDeviceId && peripheral.id !== targetDeviceId) return;
|
|
233
|
-
if (!
|
|
221
|
+
if (!isOneKeyPeripheral(peripheral)) return;
|
|
234
222
|
|
|
235
223
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
236
224
|
found.set(peripheral.id, peripheral);
|
|
@@ -285,13 +273,7 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
285
273
|
});
|
|
286
274
|
});
|
|
287
275
|
|
|
288
|
-
|
|
289
|
-
if (!service) {
|
|
290
|
-
service =
|
|
291
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
292
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
293
|
-
services[0];
|
|
294
|
-
}
|
|
276
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
295
277
|
if (!service) {
|
|
296
278
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
297
279
|
}
|
|
@@ -310,10 +292,9 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
310
292
|
let writeCharacteristic: Characteristic | undefined;
|
|
311
293
|
let notifyCharacteristic: Characteristic | undefined;
|
|
312
294
|
for (const characteristic of characteristics) {
|
|
313
|
-
|
|
314
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
295
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
315
296
|
writeCharacteristic = characteristic;
|
|
316
|
-
} else if (
|
|
297
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
317
298
|
notifyCharacteristic = characteristic;
|
|
318
299
|
}
|
|
319
300
|
}
|
|
@@ -437,16 +418,24 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
437
418
|
}
|
|
438
419
|
|
|
439
420
|
await connectPeripheral(peripheral);
|
|
440
|
-
|
|
441
|
-
const notificationState = createNotificationState(uuid);
|
|
421
|
+
let characteristics: CharacteristicPair | undefined;
|
|
442
422
|
try {
|
|
423
|
+
characteristics = await discoverCharacteristics(peripheral);
|
|
424
|
+
const notificationState = createNotificationState(uuid);
|
|
443
425
|
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
426
|
+
connectedDevices.set(uuid, peripheral);
|
|
427
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
444
428
|
} catch (error) {
|
|
445
429
|
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
430
|
+
if (characteristics) {
|
|
431
|
+
characteristics.notify.removeAllListeners('data');
|
|
432
|
+
await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
|
|
433
|
+
}
|
|
434
|
+
if (peripheral.state !== 'disconnected') {
|
|
435
|
+
await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
|
|
436
|
+
}
|
|
446
437
|
throw error;
|
|
447
438
|
}
|
|
448
|
-
connectedDevices.set(uuid, peripheral);
|
|
449
|
-
deviceCharacteristics.set(uuid, characteristics);
|
|
450
439
|
},
|
|
451
440
|
|
|
452
441
|
async disconnect(uuid: string) {
|