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

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 (79) 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 +86 -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__/logBlockEvent.test.ts +45 -122
  10. package/__tests__/open-wallet-session.test.ts +781 -69
  11. package/__tests__/pro2HostAssetPackage.test.ts +165 -0
  12. package/__tests__/protocol-v2-resources.test.ts +7 -4
  13. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -1
  14. package/__tests__/protocol-v2.test.ts +925 -97
  15. package/__tests__/protocolV2FileWrite.test.ts +40 -0
  16. package/dist/api/FirmwareUpdateV4.d.ts +5 -0
  17. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  18. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  19. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  20. package/dist/api/evm/EVMSignTypedData.d.ts.map +1 -1
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +2 -2
  24. package/dist/api/helpers/protocolV2FileWrite.d.ts +1 -0
  25. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  26. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  27. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  28. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  30. package/dist/core/RequestQueue.d.ts +2 -0
  31. package/dist/core/RequestQueue.d.ts.map +1 -1
  32. package/dist/core/index.d.ts +3 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/device/Device.d.ts +1 -1
  35. package/dist/device/Device.d.ts.map +1 -1
  36. package/dist/device/DeviceConnector.d.ts +1 -1
  37. package/dist/device/DeviceConnector.d.ts.map +1 -1
  38. package/dist/events/logBlockEvent.d.ts.map +1 -1
  39. package/dist/events/ui-request.d.ts +3 -0
  40. package/dist/events/ui-request.d.ts.map +1 -1
  41. package/dist/index.d.ts +21 -5
  42. package/dist/index.js +2511 -1538
  43. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +2 -0
  45. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  47. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  48. package/dist/types/settings.d.ts +2 -2
  49. package/dist/types/settings.d.ts.map +1 -1
  50. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  51. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  52. package/dist/utils/patch.d.ts +1 -1
  53. package/dist/utils/patch.d.ts.map +1 -1
  54. package/dist/utils/pro2HostAssetPackage.d.ts +8 -0
  55. package/dist/utils/pro2HostAssetPackage.d.ts.map +1 -0
  56. package/package.json +4 -4
  57. package/src/api/FirmwareUpdateV4.ts +474 -83
  58. package/src/api/OpenWalletSession.ts +39 -12
  59. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +5 -2
  60. package/src/api/evm/EVMSignTypedData.ts +1 -0
  61. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  62. package/src/api/helpers/protocolV2FileWrite.ts +11 -5
  63. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  64. package/src/api/protocol-v2/DeviceUploadNft.ts +31 -8
  65. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +33 -8
  66. package/src/api/protocol-v2/helpers.ts +8 -0
  67. package/src/core/RequestQueue.ts +16 -1
  68. package/src/core/index.ts +164 -26
  69. package/src/data/messages/messages-protocol-v2.json +1589 -1375
  70. package/src/device/Device.ts +3 -1
  71. package/src/device/DeviceConnector.ts +7 -3
  72. package/src/events/logBlockEvent.ts +6 -75
  73. package/src/events/ui-request.ts +3 -0
  74. package/src/protocols/protocol-v2/resources.ts +9 -1
  75. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +8 -1
  76. package/src/protocols/protocol-v2/walletSession.ts +223 -56
  77. package/src/types/settings.ts +4 -2
  78. package/src/utils/deviceFeaturesUtils.ts +4 -0
  79. package/src/utils/pro2HostAssetPackage.ts +400 -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,29 @@ 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
+
386
+ const getProtocolV2ErrorCode = (error: unknown): number | string | undefined => {
387
+ if (!error || typeof error !== 'object') return undefined;
388
+ const { errorCode, code } = error as { errorCode?: unknown; code?: unknown };
389
+ const causeCode = errorCode ?? code;
390
+ return typeof causeCode === 'number' || typeof causeCode === 'string' ? causeCode : undefined;
391
+ };
392
+
362
393
  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);
394
+ isBleStaleBondHardwareError(error) ||
395
+ (error instanceof HardwareError &&
396
+ (error.errorCode === HardwareErrorCode.ActionCancelled ||
397
+ error.errorCode === HardwareErrorCode.FirmwareError ||
398
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
399
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE));
368
400
 
369
401
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
370
402
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
@@ -401,6 +433,18 @@ const normalizeProtocolV2TargetStatus = (
401
433
  return undefined;
402
434
  };
403
435
 
436
+ const PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE = new Map<
437
+ number | string,
438
+ 'prepare' | 'install' | 'verify'
439
+ >([
440
+ [0, 'prepare'],
441
+ [1, 'install'],
442
+ [2, 'verify'],
443
+ ['FW_MGMT_UPDATER_PHASE_PREPARE', 'prepare'],
444
+ ['FW_MGMT_UPDATER_PHASE_INSTALL', 'install'],
445
+ ['FW_MGMT_UPDATER_PHASE_VERIFY', 'verify'],
446
+ ]);
447
+
404
448
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
405
449
 
406
450
  const versionArrayToNumber = (version?: IVersionArray) => {
@@ -458,6 +502,11 @@ const toProtocolV2FiniteNumber = (value: unknown) => {
458
502
  return undefined;
459
503
  };
460
504
 
505
+ const normalizeProtocolV2ProgressPercent = (value: unknown) => {
506
+ const progress = toProtocolV2FiniteNumber(value);
507
+ return progress === undefined ? undefined : Math.min(100, Math.max(0, progress));
508
+ };
509
+
461
510
  export const isProtocolV2FirmwareFingerprintValid = (
462
511
  binary: ArrayBuffer | Uint8Array,
463
512
  fingerprint: string | undefined
@@ -530,12 +579,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
530
579
 
531
580
  private protocolV2InstallBaselineVersions = new Map<number, string>();
532
581
 
582
+ private protocolV2InstallNeedsReconnect = false;
583
+
584
+ private protocolV2InstallDisconnectObserved = false;
585
+
586
+ private protocolV2InstallTerminalSuccessObserved = false;
587
+
533
588
  private protocolV2LastRuntimeProbeFeatures?: Features;
534
589
 
535
590
  private protocolV2LastTransferProgress?: number;
536
591
 
537
592
  private protocolV2LastTransferProgressAt = 0;
538
593
 
594
+ private protocolV2LastTransferredBytes = 0;
595
+
539
596
  init() {
540
597
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
541
598
  this.requireDeviceMode = [];
@@ -1808,6 +1865,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1808
1865
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
1809
1866
  return true;
1810
1867
  } catch (error) {
1868
+ if (isBleStaleBondHardwareError(error)) {
1869
+ throw error;
1870
+ }
1811
1871
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1812
1872
  return false;
1813
1873
  }
@@ -1901,7 +1961,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1901
1961
  }
1902
1962
  lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
1903
1963
  } catch (error) {
1904
- if (this.isProtocolV2ReconnectIdentityError(error)) {
1964
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
1905
1965
  throw error;
1906
1966
  }
1907
1967
  shouldReconnect = true;
@@ -2063,6 +2123,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2063
2123
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2064
2124
  this.protocolV2LastTransferProgress = undefined;
2065
2125
  this.protocolV2LastTransferProgressAt = 0;
2126
+ this.protocolV2LastTransferredBytes = 0;
2127
+ const transferStartedAt = Date.now();
2066
2128
  let processedSize = 0;
2067
2129
  for (const resource of resourcesToSync) {
2068
2130
  // The bootloader keeps its live resource package mounted. FatFs rejects
@@ -2073,6 +2135,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2073
2135
  filePath: writePath,
2074
2136
  processedSize,
2075
2137
  totalSize,
2138
+ transferStartedAt,
2076
2139
  });
2077
2140
  await this.verifyProtocolV2StagedFile(writePath, resource.source.size);
2078
2141
  if (isProtocolV2BootResourcePackagePath(resource.devicePath)) {
@@ -2088,6 +2151,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2088
2151
  filePath,
2089
2152
  processedSize,
2090
2153
  totalSize,
2154
+ transferStartedAt,
2091
2155
  });
2092
2156
  await this.verifyProtocolV2StagedFile(filePath, item.source.size);
2093
2157
  stagedInstallTargets.push({
@@ -2097,7 +2161,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2097
2161
  }
2098
2162
 
2099
2163
  if (totalSize > 0) {
2100
- this.postProgressMessage(100, 'transferData');
2164
+ const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2165
+ this.postProgressMessage(100, 'transferData', {
2166
+ transferredBytes: totalSize,
2167
+ totalBytes: totalSize,
2168
+ rateBytesPerSecond: elapsedMs > 0 ? Math.round((totalSize / elapsedMs) * 1000) : undefined,
2169
+ elapsedMs,
2170
+ });
2101
2171
  }
2102
2172
  if (stagedInstallTargets.length === 0) {
2103
2173
  return;
@@ -2122,20 +2192,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2122
2192
  filePath,
2123
2193
  processedSize,
2124
2194
  totalSize,
2195
+ transferStartedAt = Date.now(),
2125
2196
  }: {
2126
2197
  source: FirmwareByteSource;
2127
2198
  filePath: string;
2128
2199
  processedSize: number;
2129
2200
  totalSize: number;
2201
+ transferStartedAt?: number;
2130
2202
  }) {
2131
2203
  let lastError: unknown;
2132
2204
  for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
2205
+ this.throwIfAborted();
2133
2206
  try {
2134
- const transferStartedAt = Date.now();
2135
2207
  await writeFirmwareByteSource({
2136
2208
  source,
2137
2209
  chunkSize: this.getProtocolV2FirmwareChunkSize('write', filePath),
2138
2210
  write: async ({ data, sourceOffset, length, first }) => {
2211
+ this.throwIfAborted();
2139
2212
  const chunkEnd = sourceOffset + length;
2140
2213
  const deviceProgress = getProtocolV2DeviceTransferProgress(
2141
2214
  processedSize + sourceOffset,
@@ -2163,18 +2236,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2163
2236
  const elapsedMs = Math.max(now - transferStartedAt, 0);
2164
2237
  const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2165
2238
  const shouldPostProgress =
2166
- progress !== this.protocolV2LastTransferProgress ||
2167
- now - this.protocolV2LastTransferProgressAt >=
2168
- PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2169
- chunkEnd === source.size;
2239
+ transferredBytes >= this.protocolV2LastTransferredBytes &&
2240
+ (progress !== this.protocolV2LastTransferProgress ||
2241
+ now - this.protocolV2LastTransferProgressAt >=
2242
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2243
+ chunkEnd === source.size);
2170
2244
  if (shouldPostProgress) {
2171
2245
  this.protocolV2LastTransferProgress = progress;
2172
2246
  this.protocolV2LastTransferProgressAt = now;
2247
+ this.protocolV2LastTransferredBytes = transferredBytes;
2173
2248
  this.postProgressMessage(progress, 'transferData', {
2174
2249
  transferredBytes,
2175
2250
  totalBytes: totalSize,
2176
2251
  rateBytesPerSecond:
2177
- elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2252
+ elapsedMs > 0 ? Math.round((transferredBytes / elapsedMs) * 1000) : undefined,
2178
2253
  elapsedMs,
2179
2254
  });
2180
2255
  }
@@ -2183,15 +2258,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2183
2258
  });
2184
2259
  return processedSize + source.size;
2185
2260
  } catch (error) {
2261
+ this.throwIfAborted();
2262
+ if (isBleStaleBondHardwareError(error)) {
2263
+ throw error;
2264
+ }
2186
2265
  lastError = error;
2187
2266
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2188
2267
  await this.recoverProtocolV2FileTransfer();
2268
+ this.throwIfAborted();
2189
2269
  }
2190
2270
  }
2191
2271
  }
2272
+ const causeCode = getProtocolV2ErrorCode(lastError);
2192
2273
  throw ERRORS.TypedError(
2193
2274
  HardwareErrorCode.EmmcFileWriteFirmwareError,
2194
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
2275
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`,
2276
+ causeCode === undefined ? undefined : { causeCode }
2195
2277
  );
2196
2278
  }
2197
2279
 
@@ -2212,19 +2294,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2212
2294
  }
2213
2295
  }
2214
2296
 
2297
+ private collectProtocolV2LiveTargetIds(
2298
+ statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2299
+ expectedTargetIds: Set<number>,
2300
+ liveTargetIds: Set<number>
2301
+ ) {
2302
+ statusTargets.forEach(target => {
2303
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2304
+ if (
2305
+ targetId !== undefined &&
2306
+ expectedTargetIds.has(targetId) &&
2307
+ isProtocolV2TargetStatusInProgress(target.status)
2308
+ ) {
2309
+ liveTargetIds.add(targetId);
2310
+ }
2311
+ });
2312
+ }
2313
+
2215
2314
  private assertProtocolV2TargetStatus(
2216
2315
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2217
2316
  expectedTargetIds: Set<number>,
2218
- expectedPaths = new Map<number, string>()
2317
+ expectedPaths?: Map<number, string>,
2318
+ liveTargetIds?: Set<number>
2219
2319
  ) {
2320
+ const resolvedExpectedPaths = expectedPaths ?? new Map<number, string>();
2220
2321
  Log.log(
2221
2322
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
2222
2323
  );
2223
- const failedTarget = statusTargets.find(
2224
- target =>
2225
- expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
2226
- isProtocolV2TargetStatusFailed(target.status)
2227
- );
2324
+ const failedTarget = statusTargets.find(target => {
2325
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2326
+ return (
2327
+ expectedTargetIds.has(targetId) &&
2328
+ isProtocolV2TargetStatusFailed(target.status) &&
2329
+ (!liveTargetIds || liveTargetIds.has(targetId))
2330
+ );
2331
+ });
2228
2332
  if (failedTarget) {
2229
2333
  const failedTargetDetails = JSON.stringify({
2230
2334
  targetId: failedTarget.target_id,
@@ -2241,6 +2345,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2241
2345
  }
2242
2346
  );
2243
2347
  }
2348
+ if (liveTargetIds) {
2349
+ const staleFailedTargets = statusTargets.filter(target => {
2350
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2351
+ return (
2352
+ expectedTargetIds.has(targetId) &&
2353
+ isProtocolV2TargetStatusFailed(target.status) &&
2354
+ !liveTargetIds.has(targetId)
2355
+ );
2356
+ });
2357
+ if (staleFailedTargets.length > 0) {
2358
+ Log.log(
2359
+ `[FirmwareUpdateV4] ignoring stale failed firmware status until those targets start: ${JSON.stringify(
2360
+ staleFailedTargets.map(target => ({
2361
+ targetId: target.target_id,
2362
+ status: target.status,
2363
+ path: target.path,
2364
+ }))
2365
+ )}`
2366
+ );
2367
+ }
2368
+ }
2244
2369
 
2245
2370
  const matchingTargets = statusTargets.filter(target =>
2246
2371
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
@@ -2248,10 +2373,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2248
2373
  const seenTargetIds = new Set<number>();
2249
2374
  for (const target of matchingTargets) {
2250
2375
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2376
+ const pathConflictsWithCurrentInstall =
2377
+ targetId !== undefined &&
2378
+ Boolean(target.path) &&
2379
+ Boolean(resolvedExpectedPaths.get(targetId)) &&
2380
+ target.path !== resolvedExpectedPaths.get(targetId) &&
2381
+ (!liveTargetIds || liveTargetIds.has(targetId));
2251
2382
  if (
2252
2383
  targetId === undefined ||
2253
2384
  seenTargetIds.has(targetId) ||
2254
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2385
+ pathConflictsWithCurrentInstall
2255
2386
  ) {
2256
2387
  Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2257
2388
  throw ERRORS.TypedError(
@@ -2264,9 +2395,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2264
2395
  }
2265
2396
  seenTargetIds.add(targetId);
2266
2397
  }
2267
- const completedTargets = matchingTargets.filter(target =>
2268
- isProtocolV2TargetStatusFinished(target.status)
2269
- );
2398
+ const completedTargets = matchingTargets.filter(target => {
2399
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2400
+ return (
2401
+ isProtocolV2TargetStatusFinished(target.status) &&
2402
+ (!liveTargetIds || (targetId !== undefined && liveTargetIds.has(targetId)))
2403
+ );
2404
+ });
2270
2405
  const completedTargetIds = new Set<number>();
2271
2406
  completedTargets.forEach(target => {
2272
2407
  const targetId = normalizeProtocolV2TargetId(target.target_id);
@@ -2303,16 +2438,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2303
2438
  }
2304
2439
 
2305
2440
  if (expectedTargetIds.size > 0 && matchingTargets.length > 0) {
2306
- const hasInProgressTarget = matchingTargets.some(target =>
2307
- isProtocolV2TargetStatusInProgress(target.status)
2308
- );
2441
+ let detailedProgress = completedTargetIds.size * 100;
2442
+ let installProgressMetadata:
2443
+ | {
2444
+ installTargetId: number;
2445
+ installPhase: 'prepare' | 'install' | 'verify';
2446
+ installPhaseProgress: number;
2447
+ }
2448
+ | undefined;
2449
+ let hasInProgressTarget = false;
2450
+ matchingTargets.forEach(target => {
2451
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2452
+ if (
2453
+ targetId === undefined ||
2454
+ completedTargetIds.has(targetId) ||
2455
+ !isProtocolV2TargetStatusInProgress(target.status) ||
2456
+ (liveTargetIds && !liveTargetIds.has(targetId))
2457
+ ) {
2458
+ return;
2459
+ }
2460
+ hasInProgressTarget = true;
2461
+ detailedProgress += normalizeProtocolV2ProgressPercent(target.progress_percent) ?? 0;
2462
+ const installPhase = target.phase_info
2463
+ ? PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE.get(target.phase_info.phase)
2464
+ : undefined;
2465
+ const installPhaseProgress = normalizeProtocolV2ProgressPercent(
2466
+ target.phase_info?.progress_percent
2467
+ );
2468
+ if (installPhase && installPhaseProgress !== undefined && !installProgressMetadata) {
2469
+ installProgressMetadata = {
2470
+ installTargetId: targetId,
2471
+ installPhase,
2472
+ installPhaseProgress,
2473
+ };
2474
+ }
2475
+ });
2309
2476
  const completedProgress = Math.floor(
2310
2477
  (completedTargetIds.size / expectedTargetIds.size) * 100
2311
2478
  );
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');
2479
+ const reportedProgress = Math.floor(detailedProgress / expectedTargetIds.size);
2480
+ const progress = Math.min(
2481
+ 99,
2482
+ Math.max(completedProgress, reportedProgress, hasInProgressTarget ? 1 : 0)
2483
+ );
2484
+ if (installProgressMetadata) {
2485
+ this.postProgressMessage(progress, 'installingFirmware', installProgressMetadata);
2486
+ } else {
2487
+ this.postProgressMessage(progress, 'installingFirmware');
2488
+ }
2316
2489
  }
2317
2490
 
2318
2491
  return false;
@@ -2383,14 +2556,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2383
2556
  ) {
2384
2557
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2385
2558
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2559
+ const isBleInstall = this.isBleReconnect();
2386
2560
  const startTime = Date.now();
2387
2561
  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;
2562
+ // The loader may release either USB or BLE as installation starts. Recovery reconnects
2563
+ // directly to status polling without generic Ping or DeviceInfo probes.
2564
+ let shouldReconnect = this.protocolV2InstallNeedsReconnect;
2565
+ this.protocolV2InstallNeedsReconnect = false;
2391
2566
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2392
- let installEvidenceObserved = false;
2567
+ let bleInstallLinkReady = false;
2568
+ let installStatusDisconnectObserved = isBleInstall && this.protocolV2InstallDisconnectObserved;
2569
+ this.protocolV2InstallDisconnectObserved = false;
2570
+ let installReconnectObserved = false;
2571
+ let finishedStatusSnapshotKey: string | undefined;
2572
+ let finishedStatusSnapshotPolls = 0;
2573
+ let fastFinishedInstallProgressReported = false;
2574
+ let installEvidenceObserved = this.protocolV2InstallTerminalSuccessObserved;
2393
2575
  let currentInstallStatusObserved = false;
2576
+ const liveTargetIds = new Set<number>();
2577
+ const recordBleInstallDisconnect = (error: unknown) => {
2578
+ if (
2579
+ !isBleInstall ||
2580
+ !isProtocolV2DeviceDisconnectedError(error) ||
2581
+ isProtocolV2InstallResponseTimeout(error)
2582
+ ) {
2583
+ return;
2584
+ }
2585
+ installStatusDisconnectObserved = true;
2586
+ installReconnectObserved = false;
2587
+ finishedStatusSnapshotKey = undefined;
2588
+ finishedStatusSnapshotPolls = 0;
2589
+ };
2394
2590
 
2395
2591
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2396
2592
  // A transport release caused by an explicit workflow cancellation must not
@@ -2398,43 +2594,40 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2398
2594
  this.throwIfAborted();
2399
2595
  try {
2400
2596
  if (shouldReconnect) {
2401
- await this.reconnectProtocolV2Device();
2402
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2597
+ const isBleInstallReconnect = this.isBleReconnect();
2598
+ await this.reconnectProtocolV2Device({ skipProtocolProbe: true });
2599
+ bleInstallLinkReady = isBleInstallReconnect;
2600
+ deviceInfo = isBleInstallReconnect
2601
+ ? undefined
2602
+ : await this.verifyProtocolV2ReconnectIdentity();
2603
+ if (isBleInstallReconnect && installStatusDisconnectObserved) {
2604
+ installReconnectObserved = true;
2605
+ }
2403
2606
  shouldReconnect = false;
2404
2607
  }
2405
- const currentDeviceInfo = deviceInfo;
2608
+ let currentDeviceInfo = deviceInfo;
2406
2609
  try {
2407
2610
  const statusResponse = await this.device.getCommands().typedCall(
2408
2611
  'DeviceFirmwareUpdateStatusGet',
2409
- ['DeviceFirmwareUpdateStatus', 'Success'],
2612
+ 'DeviceFirmwareUpdateStatus',
2410
2613
  {
2411
2614
  fields: {
2412
2615
  status: true,
2616
+ progress_percent: true,
2617
+ phase_info: true,
2413
2618
  payload_version: true,
2414
2619
  path: true,
2415
2620
  },
2416
2621
  },
2417
2622
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2418
2623
  );
2419
- if (statusResponse.type === 'Success') {
2624
+ if (this.protocolV2InstallTerminalSuccessObserved) {
2420
2625
  installEvidenceObserved = true;
2421
- lastError = new Error(
2422
- 'Protocol V2 firmware install acknowledged; waiting for target status'
2423
- );
2424
2626
  }
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) {
2627
+ const statusTargets = (statusResponse.message.records ??
2628
+ []) as ProtocolV2FirmwareUpdateStatusTarget[];
2629
+ this.collectProtocolV2LiveTargetIds(statusTargets, expectedTargetIds, liveTargetIds);
2630
+ if (liveTargetIds.size > 0) {
2438
2631
  currentInstallStatusObserved = true;
2439
2632
  }
2440
2633
  const hasMatchingTargetStatus = statusTargets.some(target => {
@@ -2451,18 +2644,103 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2451
2644
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2452
2645
  return targetId !== undefined && expectedTargetIds.has(targetId);
2453
2646
  });
2647
+ const matchingFinishedTargetIds = new Set<number>();
2648
+ const matchingFinishedStatus = matchingStatusTargets
2649
+ .map(target => {
2650
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2651
+ const expectedPath = targetId === undefined ? undefined : expectedPaths.get(targetId);
2652
+ const expectedVersion =
2653
+ targetId === undefined
2654
+ ? undefined
2655
+ : this.getExpectedProtocolV2TargetVersion(targetId);
2656
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
2657
+ if (
2658
+ targetId === undefined ||
2659
+ matchingFinishedTargetIds.has(targetId) ||
2660
+ !isProtocolV2TargetStatusFinished(target.status) ||
2661
+ (Boolean(target.path) && Boolean(expectedPath) && target.path !== expectedPath) ||
2662
+ (expectedVersion !== undefined &&
2663
+ payloadVersion !== undefined &&
2664
+ payloadVersion !== expectedVersion)
2665
+ ) {
2666
+ return undefined;
2667
+ }
2668
+ matchingFinishedTargetIds.add(targetId);
2669
+ return [targetId, target.path ?? null, payloadVersion ?? null] as const;
2670
+ })
2671
+ .filter((target): target is readonly [number, string | null, number | null] => !!target)
2672
+ .sort(([leftTargetId], [rightTargetId]) => leftTargetId - rightTargetId);
2673
+ const hasCompleteMatchingFinishedStatus =
2674
+ matchingFinishedStatus.length === expectedTargetIds.size &&
2675
+ matchingFinishedTargetIds.size === expectedTargetIds.size &&
2676
+ expectedTargetIds.size > 0;
2677
+ if (
2678
+ requireCurrentInstallStatus &&
2679
+ installReconnectObserved &&
2680
+ hasCompleteMatchingFinishedStatus
2681
+ ) {
2682
+ const nextSnapshotKey = JSON.stringify(matchingFinishedStatus);
2683
+ if (nextSnapshotKey === finishedStatusSnapshotKey) {
2684
+ finishedStatusSnapshotPolls += 1;
2685
+ } else {
2686
+ finishedStatusSnapshotKey = nextSnapshotKey;
2687
+ finishedStatusSnapshotPolls = 1;
2688
+ }
2689
+ } else {
2690
+ finishedStatusSnapshotKey = undefined;
2691
+ finishedStatusSnapshotPolls = 0;
2692
+ }
2693
+ if (
2694
+ requireCurrentInstallStatus &&
2695
+ installReconnectObserved &&
2696
+ hasCompleteMatchingFinishedStatus &&
2697
+ !currentInstallStatusObserved &&
2698
+ !this.protocolV2InstallTerminalSuccessObserved &&
2699
+ !fastFinishedInstallProgressReported
2700
+ ) {
2701
+ // A fast BLE install may finish while disconnected, so IN_PROGRESS can be hidden.
2702
+ // A status response timeout alone is not evidence that the install was accepted.
2703
+ this.postProgressMessage(1, 'installingFirmware');
2704
+ fastFinishedInstallProgressReported = true;
2705
+ }
2706
+ const finishedAfterDisconnectObserved =
2707
+ finishedStatusSnapshotPolls >= PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS;
2708
+ const hasCurrentInstallEvidence =
2709
+ currentInstallStatusObserved ||
2710
+ this.protocolV2InstallTerminalSuccessObserved ||
2711
+ finishedAfterDisconnectObserved;
2712
+ if (hasCurrentInstallEvidence) {
2713
+ installEvidenceObserved = true;
2714
+ }
2454
2715
  const shouldVerifyTargetCompletion =
2455
- !requireCurrentInstallStatus || currentInstallStatusObserved;
2716
+ !requireCurrentInstallStatus || hasCurrentInstallEvidence;
2456
2717
  if (
2457
2718
  shouldVerifyTargetCompletion &&
2458
- this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)
2719
+ this.assertProtocolV2TargetStatus(
2720
+ statusTargets,
2721
+ expectedTargetIds,
2722
+ expectedPaths,
2723
+ requireCurrentInstallStatus &&
2724
+ !this.protocolV2InstallTerminalSuccessObserved &&
2725
+ !finishedAfterDisconnectObserved
2726
+ ? liveTargetIds
2727
+ : undefined
2728
+ )
2459
2729
  ) {
2460
2730
  return;
2461
2731
  }
2462
2732
 
2733
+ if (!hasCurrentInstallEvidence) {
2734
+ // A successful status poll clears DeviceCommands' cancel action. Keep cancellation
2735
+ // available while UpdateRequest is still waiting for confirmation on the device.
2736
+ // Bootloader does not register the protocol Cancel message. Keep the
2737
+ // operation locally cancellable; interruptionFromUser() disposes the link.
2738
+ this.device.setCancelableAction(() => Promise.resolve());
2739
+ }
2740
+
2463
2741
  if (
2464
2742
  requireCurrentInstallStatus &&
2465
- !currentInstallStatusObserved &&
2743
+ !hasCurrentInstallEvidence &&
2466
2744
  matchingStatusTargets.length > 0
2467
2745
  ) {
2468
2746
  lastError = new Error(
@@ -2470,6 +2748,18 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2470
2748
  );
2471
2749
  }
2472
2750
 
2751
+ if (
2752
+ statusTargets.length === 0 &&
2753
+ !currentDeviceInfo &&
2754
+ bleInstallLinkReady &&
2755
+ installEvidenceObserved
2756
+ ) {
2757
+ // BLE install reconnect skips generic probes because loaders may not answer Ping.
2758
+ // Restore the verified identity only after current-install evidence disappears.
2759
+ currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2760
+ deviceInfo = currentDeviceInfo;
2761
+ }
2762
+
2473
2763
  if (statusTargets.length === 0 && currentDeviceInfo) {
2474
2764
  const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2475
2765
  if (
@@ -2487,11 +2777,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2487
2777
  }
2488
2778
 
2489
2779
  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
- }
2780
+ lastError = new Error(
2781
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2782
+ );
2495
2783
  } else {
2496
2784
  const missingTargetIds = this.getProtocolV2MissingTargetIds(
2497
2785
  statusTargets,
@@ -2514,12 +2802,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2514
2802
  // missing endpoint as completion only after the runtime probe confirms App mode.
2515
2803
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2516
2804
  if (!currentDeviceInfo) {
2805
+ if (!bleInstallLinkReady) {
2806
+ throw ERRORS.TypedError(
2807
+ HardwareErrorCode.RuntimeError,
2808
+ 'Protocol V2 device identity is unavailable during install polling'
2809
+ );
2810
+ }
2811
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2812
+ }
2813
+ const reconnectDeviceInfo = currentDeviceInfo ?? deviceInfo;
2814
+ if (!reconnectDeviceInfo) {
2517
2815
  throw ERRORS.TypedError(
2518
2816
  HardwareErrorCode.RuntimeError,
2519
2817
  'Protocol V2 device identity is unavailable during install polling'
2520
2818
  );
2521
2819
  }
2522
- const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2820
+ const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2523
2821
  if (
2524
2822
  isNormalMode &&
2525
2823
  (installEvidenceObserved ||
@@ -2542,8 +2840,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2542
2840
  );
2543
2841
  }
2544
2842
  } else {
2843
+ recordBleInstallDisconnect(error);
2545
2844
  shouldReconnect = true;
2546
2845
  deviceInfo = undefined;
2846
+ bleInstallLinkReady = false;
2547
2847
  lastError = error;
2548
2848
  Log.log(
2549
2849
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -2565,8 +2865,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2565
2865
  ) {
2566
2866
  throw error;
2567
2867
  }
2868
+ recordBleInstallDisconnect(error);
2568
2869
  shouldReconnect = true;
2569
2870
  deviceInfo = undefined;
2871
+ bleInstallLinkReady = false;
2570
2872
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2571
2873
  }
2572
2874
  await wait(1000);
@@ -2595,6 +2897,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2595
2897
  return;
2596
2898
  }
2597
2899
  } catch (error) {
2900
+ if (isBleStaleBondHardwareError(error)) {
2901
+ throw error;
2902
+ }
2598
2903
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2599
2904
  }
2600
2905
  await this.protocolV2Reboot(DeviceRebootType.Normal);
@@ -2686,7 +2991,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2686
2991
  'Protocol V2 device is still in bootloader mode'
2687
2992
  );
2688
2993
  } catch (error) {
2689
- if (this.isProtocolV2ReconnectIdentityError(error)) {
2994
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2690
2995
  throw error;
2691
2996
  }
2692
2997
  shouldReconnect = true;
@@ -2704,19 +3009,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2704
3009
  );
2705
3010
  }
2706
3011
 
2707
- private async reconnectProtocolV2Device() {
3012
+ private async reconnectProtocolV2Device(options?: { skipProtocolProbe?: boolean }) {
2708
3013
  if (this.isBleReconnect()) {
2709
- await this.acquireProtocolV2BleDevice();
3014
+ await this.acquireProtocolV2BleDevice(options?.skipProtocolProbe);
2710
3015
  return;
2711
3016
  }
2712
3017
 
2713
3018
  const deviceDiff = await this.device.deviceConnector?.enumerate();
2714
3019
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
3020
+ const env = DataManager.getSettings('env');
3021
+ const isWebUsb = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
3022
+
3023
+ if (isWebUsb && options?.skipProtocolProbe) {
3024
+ const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
3025
+ const expectedPath = this.protocolV2ExpectedPath?.trim();
3026
+ const reconnectDescriptor = devicesDescriptor.find(descriptor => {
3027
+ const descriptorPath = descriptor.path?.trim();
3028
+ return (
3029
+ (!!expectedSerialNumber && descriptorPath === expectedSerialNumber) ||
3030
+ (!!expectedPath && descriptorPath === expectedPath)
3031
+ );
3032
+ });
3033
+ if (!reconnectDescriptor?.path) {
3034
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
3035
+ }
3036
+ this.device.updateDescriptor(
3037
+ {
3038
+ ...reconnectDescriptor,
3039
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
3040
+ },
3041
+ true
3042
+ );
3043
+ const mainId = await this.device.deviceConnector?.acquire(
3044
+ reconnectDescriptor.path,
3045
+ null,
3046
+ undefined,
3047
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3048
+ undefined,
3049
+ undefined,
3050
+ true
3051
+ );
3052
+ this.device.mainId = typeof mainId === 'string' ? mainId : reconnectDescriptor.path;
3053
+ this.device.commands.disposed = false;
3054
+ this.device.getCommands().mainId = this.device.mainId;
3055
+ return;
3056
+ }
2715
3057
 
2716
- if (
2717
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2718
- devicesDescriptor.length === 1
2719
- ) {
3058
+ if (isWebUsb && devicesDescriptor.length === 1) {
2720
3059
  const descriptor = devicesDescriptor[0];
2721
3060
  if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2722
3061
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
@@ -2834,13 +3173,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2834
3173
  await wait(2000);
2835
3174
  }
2836
3175
 
2837
- private async acquireProtocolV2BleDevice() {
2838
- await this.device.deviceConnector?.acquire(
2839
- this.device.originalDescriptor.id,
3176
+ private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {
3177
+ const connector = this.device.deviceConnector;
3178
+ const expectedId = this.device.originalDescriptor.id;
3179
+ if (!skipProtocolProbe) {
3180
+ await connector?.acquire(expectedId, null, true, PROTOCOL_V2_CONNECT_PROTOCOL);
3181
+ return;
3182
+ }
3183
+ await connector?.acquire(
3184
+ expectedId,
2840
3185
  null,
2841
3186
  true,
2842
- PROTOCOL_V2_CONNECT_PROTOCOL
3187
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3188
+ undefined,
3189
+ undefined,
3190
+ skipProtocolProbe
2843
3191
  );
3192
+ this.device.commands.disposed = false;
3193
+ this.device.getCommands().mainId = expectedId;
2844
3194
  }
2845
3195
 
2846
3196
  private async protocolV2StartFirmwareUpdate({
@@ -2849,9 +3199,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2849
3199
  targets: Array<{ target_id: number; path: string }>;
2850
3200
  }) {
2851
3201
  this.protocolV2LastRuntimeProbeFeatures = undefined;
3202
+ this.protocolV2InstallNeedsReconnect = false;
3203
+ this.protocolV2InstallDisconnectObserved = false;
3204
+ this.protocolV2InstallTerminalSuccessObserved = false;
2852
3205
  const commands = this.device.getCommands();
2853
3206
  await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
2854
- this.device.setCancelableAction(() => commands.cancelDevice());
3207
+ // Bootloader does not register the protocol Cancel message. Keep the
3208
+ // operation locally cancellable; interruptionFromUser() disposes the link.
3209
+ this.device.setCancelableAction(() => Promise.resolve());
2855
3210
  const interaction = this.device.createProtocolV2UiPhaseMetadata('button', 'start');
2856
3211
  this.postMessage(
2857
3212
  createUiMessage(UI_REQUEST.REQUEST_BUTTON, {
@@ -2864,19 +3219,55 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2864
3219
  ...(interaction ? { interaction } : {}),
2865
3220
  })
2866
3221
  );
2867
- await commands.call('DeviceFirmwareUpdateRequest', {}, { returnAfterWrite: true });
3222
+
3223
+ try {
3224
+ await commands.call(
3225
+ 'DeviceFirmwareUpdateRequest',
3226
+ {},
3227
+ {
3228
+ returnAfterWrite: true,
3229
+ expectedTypes: ['Success'],
3230
+ onResponseAfterWrite: response => {
3231
+ if (response.type !== 'Success') return;
3232
+ this.protocolV2InstallTerminalSuccessObserved = true;
3233
+ this.device.clearCancelableAction();
3234
+ Log.log('[FirmwareUpdateV4] firmware install completed by device response');
3235
+ },
3236
+ }
3237
+ );
3238
+ Log.log('[FirmwareUpdateV4] firmware install request written; continue with status polling');
3239
+ } catch (error) {
3240
+ this.throwIfAborted();
3241
+ if (!isProtocolV2DeviceDisconnectedError(error)) {
3242
+ throw error;
3243
+ }
3244
+ Log.log(
3245
+ '[FirmwareUpdateV4] transport released while writing install request; continue status polling'
3246
+ );
3247
+ this.protocolV2InstallNeedsReconnect = true;
3248
+ this.protocolV2InstallDisconnectObserved = !isProtocolV2InstallResponseTimeout(error);
3249
+ }
2868
3250
  }
2869
3251
 
2870
3252
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
2871
3253
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2872
3254
  try {
2873
- const res = await typedCall('DeviceReboot', 'Success', {
2874
- reboot_type: rebootType,
2875
- });
3255
+ const res = await typedCall(
3256
+ 'DeviceReboot',
3257
+ 'Success',
3258
+ {
3259
+ reboot_type: rebootType,
3260
+ },
3261
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
3262
+ );
2876
3263
  this.device.markProtocolV2Reboot(rebootType);
2877
3264
  return res.message;
2878
3265
  } catch (error) {
2879
- if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) {
3266
+ if (
3267
+ isProtocolV2DeviceDisconnectedError(error) ||
3268
+ isProtocolV2ReconnectProbeError(error) ||
3269
+ isProtocolV2ResponseTimeout(error)
3270
+ ) {
2880
3271
  this.device.markProtocolV2Reboot(rebootType);
2881
3272
  return { message: 'Device rebooted successfully' };
2882
3273
  }