@onekeyfe/hd-core 1.2.2-alpha.100 → 1.2.2-alpha.3

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.
Files changed (69) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +13 -0
  3. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +0 -2
  4. package/__tests__/check-firmware-release-protocol-v2.test.ts +0 -2
  5. package/__tests__/device-lifecycle-events.test.ts +19 -0
  6. package/__tests__/deviceUploadNft.test.ts +68 -0
  7. package/__tests__/evmSignTypedData.test.ts +42 -0
  8. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +825 -12
  9. package/__tests__/open-wallet-session.test.ts +144 -37
  10. package/__tests__/pro2HostAssetPackage.test.ts +58 -0
  11. package/__tests__/protocol-v2-resources.test.ts +7 -4
  12. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  13. package/__tests__/protocol-v2.test.ts +720 -51
  14. package/dist/api/FirmwareUpdateV4.d.ts +4 -0
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  17. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  18. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  19. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  20. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  21. package/dist/api/firmware/getBinary.d.ts +2 -2
  22. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  23. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  24. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  25. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  26. package/dist/core/index.d.ts +1 -0
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/device/Device.d.ts +1 -1
  29. package/dist/device/Device.d.ts.map +1 -1
  30. package/dist/device/DeviceConnector.d.ts +1 -1
  31. package/dist/device/DeviceConnector.d.ts.map +1 -1
  32. package/dist/events/ui-request.d.ts +3 -0
  33. package/dist/events/ui-request.d.ts.map +1 -1
  34. package/dist/index.d.ts +21 -5
  35. package/dist/index.js +2288 -1439
  36. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  37. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  38. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  39. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  40. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  41. package/dist/types/settings.d.ts +2 -2
  42. package/dist/types/settings.d.ts.map +1 -1
  43. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  44. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  45. package/dist/utils/patch.d.ts +1 -1
  46. package/dist/utils/patch.d.ts.map +1 -1
  47. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  48. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  49. package/package.json +4 -4
  50. package/src/api/FirmwareUpdateV4.ts +434 -75
  51. package/src/api/OpenWalletSession.ts +39 -9
  52. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  53. package/src/api/evm/EVMSignTypedData.ts +1 -0
  54. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  55. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  56. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  57. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +27 -8
  58. package/src/api/protocol-v2/helpers.ts +8 -0
  59. package/src/core/index.ts +130 -14
  60. package/src/data/messages/messages-protocol-v2.json +1541 -1332
  61. package/src/device/Device.ts +3 -1
  62. package/src/device/DeviceConnector.ts +7 -3
  63. package/src/events/ui-request.ts +3 -0
  64. package/src/protocols/protocol-v2/resources.ts +9 -1
  65. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  66. package/src/protocols/protocol-v2/walletSession.ts +88 -50
  67. package/src/types/settings.ts +4 -2
  68. package/src/utils/deviceFeaturesUtils.ts +4 -0
  69. package/src/utils/pro2HostAssetPackage.ts +354 -0
@@ -1,4 +1,11 @@
1
- import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
1
+ import {
2
+ EDeviceType,
3
+ ERRORS,
4
+ HardwareError,
5
+ HardwareErrorCode,
6
+ isBleStaleBondHardwareError,
7
+ wait,
8
+ } from '@onekeyfe/hd-shared';
2
9
  import JSZip from 'jszip';
3
10
  import {
4
11
  DeviceRebootType,
@@ -44,6 +51,7 @@ import {
44
51
  getProtocolV2UnknownErrorText,
45
52
  isProtocolV2DeviceDisconnectedError,
46
53
  } from './protocol-v2/helpers';
54
+ import { isProtocolV2ResponseTimeout } from './helpers/protocolV2FileWrite';
47
55
  import {
48
56
  openFirmwareByteSource,
49
57
  readFirmwareByteSourceFully,
@@ -55,6 +63,7 @@ import {
55
63
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
56
64
  validateFirmwareUpdatePreparedPlan,
57
65
  } from './firmware/FirmwareUpdatePreparedPlan';
66
+
58
67
  import type {
59
68
  FirmwareArtifactReference,
60
69
  FirmwareUpdateV4Params,
@@ -84,6 +93,7 @@ const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
84
93
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
85
94
  const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
86
95
  const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
96
+ const PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS = 4;
87
97
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
88
98
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
89
99
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -182,6 +192,11 @@ const getProtocolV2DeviceTransferProgress = (
182
192
  type ProtocolV2FirmwareUpdateStatusTarget = {
183
193
  target_id: number | string;
184
194
  status?: number | string;
195
+ progress_percent?: number;
196
+ phase_info?: {
197
+ phase: number | string;
198
+ progress_percent: number;
199
+ };
185
200
  payload_version?: number;
186
201
  path?: string;
187
202
  };
@@ -359,12 +374,22 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
359
374
  );
360
375
  };
361
376
 
377
+ const isProtocolV2InstallResponseTimeout = (error: unknown) => {
378
+ if (!error || typeof error !== 'object') return false;
379
+ const { code } = error as { code?: number | string };
380
+ return (
381
+ code === 'response-timeout' ||
382
+ /\bresponse timeout\b/i.test(getProtocolV2UnknownErrorText(error))
383
+ );
384
+ };
385
+
362
386
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
363
- error instanceof HardwareError &&
364
- (error.errorCode === HardwareErrorCode.ActionCancelled ||
365
- error.errorCode === HardwareErrorCode.FirmwareError ||
366
- error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
367
- error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
387
+ isBleStaleBondHardwareError(error) ||
388
+ (error instanceof HardwareError &&
389
+ (error.errorCode === HardwareErrorCode.ActionCancelled ||
390
+ error.errorCode === HardwareErrorCode.FirmwareError ||
391
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
392
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE));
368
393
 
369
394
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
370
395
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
@@ -401,6 +426,18 @@ const normalizeProtocolV2TargetStatus = (
401
426
  return undefined;
402
427
  };
403
428
 
429
+ const PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE = new Map<
430
+ number | string,
431
+ 'prepare' | 'install' | 'verify'
432
+ >([
433
+ [0, 'prepare'],
434
+ [1, 'install'],
435
+ [2, 'verify'],
436
+ ['FW_MGMT_UPDATER_PHASE_PREPARE', 'prepare'],
437
+ ['FW_MGMT_UPDATER_PHASE_INSTALL', 'install'],
438
+ ['FW_MGMT_UPDATER_PHASE_VERIFY', 'verify'],
439
+ ]);
440
+
404
441
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
405
442
 
406
443
  const versionArrayToNumber = (version?: IVersionArray) => {
@@ -458,6 +495,11 @@ const toProtocolV2FiniteNumber = (value: unknown) => {
458
495
  return undefined;
459
496
  };
460
497
 
498
+ const normalizeProtocolV2ProgressPercent = (value: unknown) => {
499
+ const progress = toProtocolV2FiniteNumber(value);
500
+ return progress === undefined ? undefined : Math.min(100, Math.max(0, progress));
501
+ };
502
+
461
503
  export const isProtocolV2FirmwareFingerprintValid = (
462
504
  binary: ArrayBuffer | Uint8Array,
463
505
  fingerprint: string | undefined
@@ -530,6 +572,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
530
572
 
531
573
  private protocolV2InstallBaselineVersions = new Map<number, string>();
532
574
 
575
+ private protocolV2InstallNeedsReconnect = false;
576
+
577
+ private protocolV2InstallDisconnectObserved = false;
578
+
579
+ private protocolV2InstallTerminalSuccessObserved = false;
580
+
533
581
  private protocolV2LastRuntimeProbeFeatures?: Features;
534
582
 
535
583
  private protocolV2LastTransferProgress?: number;
@@ -1808,6 +1856,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1808
1856
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
1809
1857
  return true;
1810
1858
  } catch (error) {
1859
+ if (isBleStaleBondHardwareError(error)) {
1860
+ throw error;
1861
+ }
1811
1862
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1812
1863
  return false;
1813
1864
  }
@@ -1901,7 +1952,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1901
1952
  }
1902
1953
  lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
1903
1954
  } catch (error) {
1904
- if (this.isProtocolV2ReconnectIdentityError(error)) {
1955
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
1905
1956
  throw error;
1906
1957
  }
1907
1958
  shouldReconnect = true;
@@ -2183,6 +2234,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2183
2234
  });
2184
2235
  return processedSize + source.size;
2185
2236
  } catch (error) {
2237
+ if (isBleStaleBondHardwareError(error)) {
2238
+ throw error;
2239
+ }
2186
2240
  lastError = error;
2187
2241
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2188
2242
  await this.recoverProtocolV2FileTransfer();
@@ -2212,19 +2266,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2212
2266
  }
2213
2267
  }
2214
2268
 
2269
+ private collectProtocolV2LiveTargetIds(
2270
+ statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2271
+ expectedTargetIds: Set<number>,
2272
+ liveTargetIds: Set<number>
2273
+ ) {
2274
+ statusTargets.forEach(target => {
2275
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2276
+ if (
2277
+ targetId !== undefined &&
2278
+ expectedTargetIds.has(targetId) &&
2279
+ isProtocolV2TargetStatusInProgress(target.status)
2280
+ ) {
2281
+ liveTargetIds.add(targetId);
2282
+ }
2283
+ });
2284
+ }
2285
+
2215
2286
  private assertProtocolV2TargetStatus(
2216
2287
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2217
2288
  expectedTargetIds: Set<number>,
2218
- expectedPaths = new Map<number, string>()
2289
+ expectedPaths?: Map<number, string>,
2290
+ liveTargetIds?: Set<number>
2219
2291
  ) {
2292
+ const resolvedExpectedPaths = expectedPaths ?? new Map<number, string>();
2220
2293
  Log.log(
2221
2294
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
2222
2295
  );
2223
- const failedTarget = statusTargets.find(
2224
- target =>
2225
- expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
2226
- isProtocolV2TargetStatusFailed(target.status)
2227
- );
2296
+ const failedTarget = statusTargets.find(target => {
2297
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2298
+ return (
2299
+ expectedTargetIds.has(targetId) &&
2300
+ isProtocolV2TargetStatusFailed(target.status) &&
2301
+ (!liveTargetIds || liveTargetIds.has(targetId))
2302
+ );
2303
+ });
2228
2304
  if (failedTarget) {
2229
2305
  const failedTargetDetails = JSON.stringify({
2230
2306
  targetId: failedTarget.target_id,
@@ -2241,6 +2317,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2241
2317
  }
2242
2318
  );
2243
2319
  }
2320
+ if (liveTargetIds) {
2321
+ const staleFailedTargets = statusTargets.filter(target => {
2322
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2323
+ return (
2324
+ expectedTargetIds.has(targetId) &&
2325
+ isProtocolV2TargetStatusFailed(target.status) &&
2326
+ !liveTargetIds.has(targetId)
2327
+ );
2328
+ });
2329
+ if (staleFailedTargets.length > 0) {
2330
+ Log.log(
2331
+ `[FirmwareUpdateV4] ignoring stale failed firmware status until those targets start: ${JSON.stringify(
2332
+ staleFailedTargets.map(target => ({
2333
+ targetId: target.target_id,
2334
+ status: target.status,
2335
+ path: target.path,
2336
+ }))
2337
+ )}`
2338
+ );
2339
+ }
2340
+ }
2244
2341
 
2245
2342
  const matchingTargets = statusTargets.filter(target =>
2246
2343
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
@@ -2248,10 +2345,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2248
2345
  const seenTargetIds = new Set<number>();
2249
2346
  for (const target of matchingTargets) {
2250
2347
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2348
+ const pathConflictsWithCurrentInstall =
2349
+ targetId !== undefined &&
2350
+ Boolean(target.path) &&
2351
+ Boolean(resolvedExpectedPaths.get(targetId)) &&
2352
+ target.path !== resolvedExpectedPaths.get(targetId) &&
2353
+ (!liveTargetIds || liveTargetIds.has(targetId));
2251
2354
  if (
2252
2355
  targetId === undefined ||
2253
2356
  seenTargetIds.has(targetId) ||
2254
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2357
+ pathConflictsWithCurrentInstall
2255
2358
  ) {
2256
2359
  Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2257
2360
  throw ERRORS.TypedError(
@@ -2264,9 +2367,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2264
2367
  }
2265
2368
  seenTargetIds.add(targetId);
2266
2369
  }
2267
- const completedTargets = matchingTargets.filter(target =>
2268
- isProtocolV2TargetStatusFinished(target.status)
2269
- );
2370
+ const completedTargets = matchingTargets.filter(target => {
2371
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2372
+ return (
2373
+ isProtocolV2TargetStatusFinished(target.status) &&
2374
+ (!liveTargetIds || (targetId !== undefined && liveTargetIds.has(targetId)))
2375
+ );
2376
+ });
2270
2377
  const completedTargetIds = new Set<number>();
2271
2378
  completedTargets.forEach(target => {
2272
2379
  const targetId = normalizeProtocolV2TargetId(target.target_id);
@@ -2303,16 +2410,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2303
2410
  }
2304
2411
 
2305
2412
  if (expectedTargetIds.size > 0 && matchingTargets.length > 0) {
2306
- const hasInProgressTarget = matchingTargets.some(target =>
2307
- isProtocolV2TargetStatusInProgress(target.status)
2308
- );
2413
+ let detailedProgress = completedTargetIds.size * 100;
2414
+ let installProgressMetadata:
2415
+ | {
2416
+ installTargetId: number;
2417
+ installPhase: 'prepare' | 'install' | 'verify';
2418
+ installPhaseProgress: number;
2419
+ }
2420
+ | undefined;
2421
+ let hasInProgressTarget = false;
2422
+ matchingTargets.forEach(target => {
2423
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2424
+ if (
2425
+ targetId === undefined ||
2426
+ completedTargetIds.has(targetId) ||
2427
+ !isProtocolV2TargetStatusInProgress(target.status) ||
2428
+ (liveTargetIds && !liveTargetIds.has(targetId))
2429
+ ) {
2430
+ return;
2431
+ }
2432
+ hasInProgressTarget = true;
2433
+ detailedProgress += normalizeProtocolV2ProgressPercent(target.progress_percent) ?? 0;
2434
+ const installPhase = target.phase_info
2435
+ ? PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE.get(target.phase_info.phase)
2436
+ : undefined;
2437
+ const installPhaseProgress = normalizeProtocolV2ProgressPercent(
2438
+ target.phase_info?.progress_percent
2439
+ );
2440
+ if (installPhase && installPhaseProgress !== undefined && !installProgressMetadata) {
2441
+ installProgressMetadata = {
2442
+ installTargetId: targetId,
2443
+ installPhase,
2444
+ installPhaseProgress,
2445
+ };
2446
+ }
2447
+ });
2309
2448
  const completedProgress = Math.floor(
2310
2449
  (completedTargetIds.size / expectedTargetIds.size) * 100
2311
2450
  );
2312
- // The protocol exposes no per-target percentage, so report coarse progress by
2313
- // completed targets and use 1% once work starts to keep the UI responsive.
2314
- const progress = Math.min(99, Math.max(completedProgress, hasInProgressTarget ? 1 : 0));
2315
- this.postProgressMessage(progress, 'installingFirmware');
2451
+ const reportedProgress = Math.floor(detailedProgress / expectedTargetIds.size);
2452
+ const progress = Math.min(
2453
+ 99,
2454
+ Math.max(completedProgress, reportedProgress, hasInProgressTarget ? 1 : 0)
2455
+ );
2456
+ if (installProgressMetadata) {
2457
+ this.postProgressMessage(progress, 'installingFirmware', installProgressMetadata);
2458
+ } else {
2459
+ this.postProgressMessage(progress, 'installingFirmware');
2460
+ }
2316
2461
  }
2317
2462
 
2318
2463
  return false;
@@ -2383,14 +2528,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2383
2528
  ) {
2384
2529
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2385
2530
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2531
+ const isBleInstall = this.isBleReconnect();
2386
2532
  const startTime = Date.now();
2387
2533
  let lastError: unknown;
2388
- // DeviceFirmwareUpdateRequest leaves the current loader link usable for status polling.
2389
- // Reconnecting here probes DeviceInfo, which loaders reject while installation is active.
2390
- let shouldReconnect = false;
2534
+ // The loader may release either USB or BLE as installation starts. Recovery reconnects
2535
+ // directly to status polling without generic Ping or DeviceInfo probes.
2536
+ let shouldReconnect = this.protocolV2InstallNeedsReconnect;
2537
+ this.protocolV2InstallNeedsReconnect = false;
2391
2538
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2392
- let installEvidenceObserved = false;
2539
+ let bleInstallLinkReady = false;
2540
+ let installStatusDisconnectObserved = isBleInstall && this.protocolV2InstallDisconnectObserved;
2541
+ this.protocolV2InstallDisconnectObserved = false;
2542
+ let installReconnectObserved = false;
2543
+ let finishedStatusSnapshotKey: string | undefined;
2544
+ let finishedStatusSnapshotPolls = 0;
2545
+ let fastFinishedInstallProgressReported = false;
2546
+ let installEvidenceObserved = this.protocolV2InstallTerminalSuccessObserved;
2393
2547
  let currentInstallStatusObserved = false;
2548
+ const liveTargetIds = new Set<number>();
2549
+ const recordBleInstallDisconnect = (error: unknown) => {
2550
+ if (
2551
+ !isBleInstall ||
2552
+ !isProtocolV2DeviceDisconnectedError(error) ||
2553
+ isProtocolV2InstallResponseTimeout(error)
2554
+ ) {
2555
+ return;
2556
+ }
2557
+ installStatusDisconnectObserved = true;
2558
+ installReconnectObserved = false;
2559
+ finishedStatusSnapshotKey = undefined;
2560
+ finishedStatusSnapshotPolls = 0;
2561
+ };
2394
2562
 
2395
2563
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2396
2564
  // A transport release caused by an explicit workflow cancellation must not
@@ -2398,43 +2566,40 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2398
2566
  this.throwIfAborted();
2399
2567
  try {
2400
2568
  if (shouldReconnect) {
2401
- await this.reconnectProtocolV2Device();
2402
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2569
+ const isBleInstallReconnect = this.isBleReconnect();
2570
+ await this.reconnectProtocolV2Device({ skipProtocolProbe: true });
2571
+ bleInstallLinkReady = isBleInstallReconnect;
2572
+ deviceInfo = isBleInstallReconnect
2573
+ ? undefined
2574
+ : await this.verifyProtocolV2ReconnectIdentity();
2575
+ if (isBleInstallReconnect && installStatusDisconnectObserved) {
2576
+ installReconnectObserved = true;
2577
+ }
2403
2578
  shouldReconnect = false;
2404
2579
  }
2405
- const currentDeviceInfo = deviceInfo;
2580
+ let currentDeviceInfo = deviceInfo;
2406
2581
  try {
2407
2582
  const statusResponse = await this.device.getCommands().typedCall(
2408
2583
  'DeviceFirmwareUpdateStatusGet',
2409
- ['DeviceFirmwareUpdateStatus', 'Success'],
2584
+ 'DeviceFirmwareUpdateStatus',
2410
2585
  {
2411
2586
  fields: {
2412
2587
  status: true,
2588
+ progress_percent: true,
2589
+ phase_info: true,
2413
2590
  payload_version: true,
2414
2591
  path: true,
2415
2592
  },
2416
2593
  },
2417
2594
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2418
2595
  );
2419
- if (statusResponse.type === 'Success') {
2596
+ if (this.protocolV2InstallTerminalSuccessObserved) {
2420
2597
  installEvidenceObserved = true;
2421
- lastError = new Error(
2422
- 'Protocol V2 firmware install acknowledged; waiting for target status'
2423
- );
2424
2598
  }
2425
- const statusTargets =
2426
- statusResponse.type === 'DeviceFirmwareUpdateStatus'
2427
- ? ((statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[])
2428
- : [];
2429
- const hasInProgressTarget = statusTargets.some(target => {
2430
- const targetId = normalizeProtocolV2TargetId(target.target_id);
2431
- return (
2432
- targetId !== undefined &&
2433
- expectedTargetIds.has(targetId) &&
2434
- isProtocolV2TargetStatusInProgress(target.status)
2435
- );
2436
- });
2437
- if (hasInProgressTarget) {
2599
+ const statusTargets = (statusResponse.message.records ??
2600
+ []) as ProtocolV2FirmwareUpdateStatusTarget[];
2601
+ this.collectProtocolV2LiveTargetIds(statusTargets, expectedTargetIds, liveTargetIds);
2602
+ if (liveTargetIds.size > 0) {
2438
2603
  currentInstallStatusObserved = true;
2439
2604
  }
2440
2605
  const hasMatchingTargetStatus = statusTargets.some(target => {
@@ -2451,18 +2616,101 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2451
2616
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2452
2617
  return targetId !== undefined && expectedTargetIds.has(targetId);
2453
2618
  });
2619
+ const matchingFinishedTargetIds = new Set<number>();
2620
+ const matchingFinishedStatus = matchingStatusTargets
2621
+ .map(target => {
2622
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2623
+ const expectedPath = targetId === undefined ? undefined : expectedPaths.get(targetId);
2624
+ const expectedVersion =
2625
+ targetId === undefined
2626
+ ? undefined
2627
+ : this.getExpectedProtocolV2TargetVersion(targetId);
2628
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
2629
+ if (
2630
+ targetId === undefined ||
2631
+ matchingFinishedTargetIds.has(targetId) ||
2632
+ !isProtocolV2TargetStatusFinished(target.status) ||
2633
+ (Boolean(target.path) && Boolean(expectedPath) && target.path !== expectedPath) ||
2634
+ (expectedVersion !== undefined &&
2635
+ payloadVersion !== undefined &&
2636
+ payloadVersion !== expectedVersion)
2637
+ ) {
2638
+ return undefined;
2639
+ }
2640
+ matchingFinishedTargetIds.add(targetId);
2641
+ return [targetId, target.path ?? null, payloadVersion ?? null] as const;
2642
+ })
2643
+ .filter((target): target is readonly [number, string | null, number | null] => !!target)
2644
+ .sort(([leftTargetId], [rightTargetId]) => leftTargetId - rightTargetId);
2645
+ const hasCompleteMatchingFinishedStatus =
2646
+ matchingFinishedStatus.length === expectedTargetIds.size &&
2647
+ matchingFinishedTargetIds.size === expectedTargetIds.size &&
2648
+ expectedTargetIds.size > 0;
2649
+ if (
2650
+ requireCurrentInstallStatus &&
2651
+ installReconnectObserved &&
2652
+ hasCompleteMatchingFinishedStatus
2653
+ ) {
2654
+ const nextSnapshotKey = JSON.stringify(matchingFinishedStatus);
2655
+ if (nextSnapshotKey === finishedStatusSnapshotKey) {
2656
+ finishedStatusSnapshotPolls += 1;
2657
+ } else {
2658
+ finishedStatusSnapshotKey = nextSnapshotKey;
2659
+ finishedStatusSnapshotPolls = 1;
2660
+ }
2661
+ } else {
2662
+ finishedStatusSnapshotKey = undefined;
2663
+ finishedStatusSnapshotPolls = 0;
2664
+ }
2665
+ if (
2666
+ requireCurrentInstallStatus &&
2667
+ installReconnectObserved &&
2668
+ hasCompleteMatchingFinishedStatus &&
2669
+ !currentInstallStatusObserved &&
2670
+ !this.protocolV2InstallTerminalSuccessObserved &&
2671
+ !fastFinishedInstallProgressReported
2672
+ ) {
2673
+ // A fast BLE install may finish while disconnected, so IN_PROGRESS can be hidden.
2674
+ // A status response timeout alone is not evidence that the install was accepted.
2675
+ this.postProgressMessage(1, 'installingFirmware');
2676
+ fastFinishedInstallProgressReported = true;
2677
+ }
2678
+ const finishedAfterDisconnectObserved =
2679
+ finishedStatusSnapshotPolls >= PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS;
2680
+ const hasCurrentInstallEvidence =
2681
+ currentInstallStatusObserved ||
2682
+ this.protocolV2InstallTerminalSuccessObserved ||
2683
+ finishedAfterDisconnectObserved;
2684
+ if (hasCurrentInstallEvidence) {
2685
+ installEvidenceObserved = true;
2686
+ }
2454
2687
  const shouldVerifyTargetCompletion =
2455
- !requireCurrentInstallStatus || currentInstallStatusObserved;
2688
+ !requireCurrentInstallStatus || hasCurrentInstallEvidence;
2456
2689
  if (
2457
2690
  shouldVerifyTargetCompletion &&
2458
- this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)
2691
+ this.assertProtocolV2TargetStatus(
2692
+ statusTargets,
2693
+ expectedTargetIds,
2694
+ expectedPaths,
2695
+ requireCurrentInstallStatus &&
2696
+ !this.protocolV2InstallTerminalSuccessObserved &&
2697
+ !finishedAfterDisconnectObserved
2698
+ ? liveTargetIds
2699
+ : undefined
2700
+ )
2459
2701
  ) {
2460
2702
  return;
2461
2703
  }
2462
2704
 
2705
+ if (!hasCurrentInstallEvidence) {
2706
+ // A successful status poll clears DeviceCommands' cancel action. Keep cancellation
2707
+ // available while UpdateRequest is still waiting for confirmation on the device.
2708
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
2709
+ }
2710
+
2463
2711
  if (
2464
2712
  requireCurrentInstallStatus &&
2465
- !currentInstallStatusObserved &&
2713
+ !hasCurrentInstallEvidence &&
2466
2714
  matchingStatusTargets.length > 0
2467
2715
  ) {
2468
2716
  lastError = new Error(
@@ -2470,6 +2718,18 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2470
2718
  );
2471
2719
  }
2472
2720
 
2721
+ if (
2722
+ statusTargets.length === 0 &&
2723
+ !currentDeviceInfo &&
2724
+ bleInstallLinkReady &&
2725
+ installEvidenceObserved
2726
+ ) {
2727
+ // BLE install reconnect skips generic probes because loaders may not answer Ping.
2728
+ // Restore the verified identity only after current-install evidence disappears.
2729
+ currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2730
+ deviceInfo = currentDeviceInfo;
2731
+ }
2732
+
2473
2733
  if (statusTargets.length === 0 && currentDeviceInfo) {
2474
2734
  const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2475
2735
  if (
@@ -2487,11 +2747,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2487
2747
  }
2488
2748
 
2489
2749
  if (statusTargets.length === 0) {
2490
- if (statusResponse.type !== 'Success') {
2491
- lastError = new Error(
2492
- 'Protocol V2 firmware update is waiting for user confirmation or target status'
2493
- );
2494
- }
2750
+ lastError = new Error(
2751
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2752
+ );
2495
2753
  } else {
2496
2754
  const missingTargetIds = this.getProtocolV2MissingTargetIds(
2497
2755
  statusTargets,
@@ -2514,12 +2772,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2514
2772
  // missing endpoint as completion only after the runtime probe confirms App mode.
2515
2773
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2516
2774
  if (!currentDeviceInfo) {
2775
+ if (!bleInstallLinkReady) {
2776
+ throw ERRORS.TypedError(
2777
+ HardwareErrorCode.RuntimeError,
2778
+ 'Protocol V2 device identity is unavailable during install polling'
2779
+ );
2780
+ }
2781
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2782
+ }
2783
+ const reconnectDeviceInfo = currentDeviceInfo ?? deviceInfo;
2784
+ if (!reconnectDeviceInfo) {
2517
2785
  throw ERRORS.TypedError(
2518
2786
  HardwareErrorCode.RuntimeError,
2519
2787
  'Protocol V2 device identity is unavailable during install polling'
2520
2788
  );
2521
2789
  }
2522
- const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2790
+ const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2523
2791
  if (
2524
2792
  isNormalMode &&
2525
2793
  (installEvidenceObserved ||
@@ -2542,8 +2810,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2542
2810
  );
2543
2811
  }
2544
2812
  } else {
2813
+ recordBleInstallDisconnect(error);
2545
2814
  shouldReconnect = true;
2546
2815
  deviceInfo = undefined;
2816
+ bleInstallLinkReady = false;
2547
2817
  lastError = error;
2548
2818
  Log.log(
2549
2819
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -2565,8 +2835,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2565
2835
  ) {
2566
2836
  throw error;
2567
2837
  }
2838
+ recordBleInstallDisconnect(error);
2568
2839
  shouldReconnect = true;
2569
2840
  deviceInfo = undefined;
2841
+ bleInstallLinkReady = false;
2570
2842
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2571
2843
  }
2572
2844
  await wait(1000);
@@ -2595,6 +2867,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2595
2867
  return;
2596
2868
  }
2597
2869
  } catch (error) {
2870
+ if (isBleStaleBondHardwareError(error)) {
2871
+ throw error;
2872
+ }
2598
2873
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2599
2874
  }
2600
2875
  await this.protocolV2Reboot(DeviceRebootType.Normal);
@@ -2686,7 +2961,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2686
2961
  'Protocol V2 device is still in bootloader mode'
2687
2962
  );
2688
2963
  } catch (error) {
2689
- if (this.isProtocolV2ReconnectIdentityError(error)) {
2964
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2690
2965
  throw error;
2691
2966
  }
2692
2967
  shouldReconnect = true;
@@ -2704,19 +2979,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2704
2979
  );
2705
2980
  }
2706
2981
 
2707
- private async reconnectProtocolV2Device() {
2982
+ private async reconnectProtocolV2Device(options?: { skipProtocolProbe?: boolean }) {
2708
2983
  if (this.isBleReconnect()) {
2709
- await this.acquireProtocolV2BleDevice();
2984
+ await this.acquireProtocolV2BleDevice(options?.skipProtocolProbe);
2710
2985
  return;
2711
2986
  }
2712
2987
 
2713
2988
  const deviceDiff = await this.device.deviceConnector?.enumerate();
2714
2989
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
2990
+ const env = DataManager.getSettings('env');
2991
+ const isWebUsb = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
2992
+
2993
+ if (isWebUsb && options?.skipProtocolProbe) {
2994
+ const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
2995
+ const expectedPath = this.protocolV2ExpectedPath?.trim();
2996
+ const reconnectDescriptor = devicesDescriptor.find(descriptor => {
2997
+ const descriptorPath = descriptor.path?.trim();
2998
+ return (
2999
+ (!!expectedSerialNumber && descriptorPath === expectedSerialNumber) ||
3000
+ (!!expectedPath && descriptorPath === expectedPath)
3001
+ );
3002
+ });
3003
+ if (!reconnectDescriptor?.path) {
3004
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
3005
+ }
3006
+ this.device.updateDescriptor(
3007
+ {
3008
+ ...reconnectDescriptor,
3009
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
3010
+ },
3011
+ true
3012
+ );
3013
+ const mainId = await this.device.deviceConnector?.acquire(
3014
+ reconnectDescriptor.path,
3015
+ null,
3016
+ undefined,
3017
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3018
+ undefined,
3019
+ undefined,
3020
+ true
3021
+ );
3022
+ this.device.mainId = typeof mainId === 'string' ? mainId : reconnectDescriptor.path;
3023
+ this.device.commands.disposed = false;
3024
+ this.device.getCommands().mainId = this.device.mainId;
3025
+ return;
3026
+ }
2715
3027
 
2716
- if (
2717
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2718
- devicesDescriptor.length === 1
2719
- ) {
3028
+ if (isWebUsb && devicesDescriptor.length === 1) {
2720
3029
  const descriptor = devicesDescriptor[0];
2721
3030
  if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2722
3031
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
@@ -2834,13 +3143,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2834
3143
  await wait(2000);
2835
3144
  }
2836
3145
 
2837
- private async acquireProtocolV2BleDevice() {
2838
- await this.device.deviceConnector?.acquire(
2839
- this.device.originalDescriptor.id,
3146
+ private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {
3147
+ const connector = this.device.deviceConnector;
3148
+ const expectedId = this.device.originalDescriptor.id;
3149
+ if (!skipProtocolProbe) {
3150
+ await connector?.acquire(expectedId, null, true, PROTOCOL_V2_CONNECT_PROTOCOL);
3151
+ return;
3152
+ }
3153
+ await connector?.acquire(
3154
+ expectedId,
2840
3155
  null,
2841
3156
  true,
2842
- PROTOCOL_V2_CONNECT_PROTOCOL
3157
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3158
+ undefined,
3159
+ undefined,
3160
+ skipProtocolProbe
2843
3161
  );
3162
+ this.device.commands.disposed = false;
3163
+ this.device.getCommands().mainId = expectedId;
2844
3164
  }
2845
3165
 
2846
3166
  private async protocolV2StartFirmwareUpdate({
@@ -2849,9 +3169,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2849
3169
  targets: Array<{ target_id: number; path: string }>;
2850
3170
  }) {
2851
3171
  this.protocolV2LastRuntimeProbeFeatures = undefined;
3172
+ this.protocolV2InstallNeedsReconnect = false;
3173
+ this.protocolV2InstallDisconnectObserved = false;
3174
+ this.protocolV2InstallTerminalSuccessObserved = false;
2852
3175
  const commands = this.device.getCommands();
2853
3176
  await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
2854
- this.device.setCancelableAction(() => commands.cancelDevice());
3177
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
2855
3178
  const interaction = this.device.createProtocolV2UiPhaseMetadata('button', 'start');
2856
3179
  this.postMessage(
2857
3180
  createUiMessage(UI_REQUEST.REQUEST_BUTTON, {
@@ -2864,19 +3187,55 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2864
3187
  ...(interaction ? { interaction } : {}),
2865
3188
  })
2866
3189
  );
2867
- await commands.call('DeviceFirmwareUpdateRequest', {}, { returnAfterWrite: true });
3190
+
3191
+ try {
3192
+ await commands.call(
3193
+ 'DeviceFirmwareUpdateRequest',
3194
+ {},
3195
+ {
3196
+ returnAfterWrite: true,
3197
+ expectedTypes: ['Success'],
3198
+ onResponseAfterWrite: response => {
3199
+ if (response.type !== 'Success') return;
3200
+ this.protocolV2InstallTerminalSuccessObserved = true;
3201
+ this.device.clearCancelableAction();
3202
+ Log.log('[FirmwareUpdateV4] firmware install completed by device response');
3203
+ },
3204
+ }
3205
+ );
3206
+ Log.log('[FirmwareUpdateV4] firmware install request written; continue with status polling');
3207
+ } catch (error) {
3208
+ this.throwIfAborted();
3209
+ if (!isProtocolV2DeviceDisconnectedError(error)) {
3210
+ throw error;
3211
+ }
3212
+ Log.log(
3213
+ '[FirmwareUpdateV4] transport released while writing install request; continue status polling'
3214
+ );
3215
+ this.protocolV2InstallNeedsReconnect = true;
3216
+ this.protocolV2InstallDisconnectObserved = !isProtocolV2InstallResponseTimeout(error);
3217
+ }
2868
3218
  }
2869
3219
 
2870
3220
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
2871
3221
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2872
3222
  try {
2873
- const res = await typedCall('DeviceReboot', 'Success', {
2874
- reboot_type: rebootType,
2875
- });
3223
+ const res = await typedCall(
3224
+ 'DeviceReboot',
3225
+ 'Success',
3226
+ {
3227
+ reboot_type: rebootType,
3228
+ },
3229
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
3230
+ );
2876
3231
  this.device.markProtocolV2Reboot(rebootType);
2877
3232
  return res.message;
2878
3233
  } catch (error) {
2879
- if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) {
3234
+ if (
3235
+ isProtocolV2DeviceDisconnectedError(error) ||
3236
+ isProtocolV2ReconnectProbeError(error) ||
3237
+ isProtocolV2ResponseTimeout(error)
3238
+ ) {
2880
3239
  this.device.markProtocolV2Reboot(rebootType);
2881
3240
  return { message: 'Device rebooted successfully' };
2882
3241
  }