@onekeyfe/hd-core 1.2.0-alpha.98 → 1.2.0-alpha.99

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 (29) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +66 -61
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +1 -1
  3. package/__tests__/firmware-memory-host.test.ts +18 -4
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +1 -0
  5. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
  6. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  7. package/__tests__/firmware-update/firmware-update-plan.test.ts +61 -1
  8. package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
  9. package/__tests__/protocol-v2.test.ts +96 -0
  10. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  11. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  12. package/dist/api/FirmwareUpdateV4.d.ts +3 -1
  13. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  14. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  15. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -1
  16. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  17. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  18. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
  19. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  20. package/dist/index.js +207 -54
  21. package/package.json +4 -4
  22. package/src/api/CheckAllFirmwareRelease.ts +6 -1
  23. package/src/api/FirmwareUpdateV2.ts +123 -65
  24. package/src/api/FirmwareUpdateV4.ts +54 -8
  25. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  26. package/src/api/firmware/FirmwareMemoryHost.ts +4 -3
  27. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
  28. package/src/api/firmware/FirmwareUpdatePlan.ts +18 -5
  29. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +22 -9
@@ -429,10 +429,15 @@ export default class CheckAllFirmwareRelease extends BaseMethod {
429
429
  const forceResourceUpdate =
430
430
  validatedForceUpdateTargets.includes('resource') ||
431
431
  validatedProtocolV2ForceUpdateTargets.includes('resource');
432
+ // A configured resource archive accompanies a real remote component update,
433
+ // but its mere availability must not turn an otherwise current device into a
434
+ // resource-only update. Explicit developer resource forcing remains supported.
435
+ const includeResourceUpdate =
436
+ forceResourceUpdate || (resourceSource !== undefined && detectedComponentTargets.length > 0);
432
437
  const targetsToUpdate = Array.from(
433
438
  new Set([
434
439
  ...componentTargetsToUpdate,
435
- ...(forceResourceUpdate ? (['resource'] as const) : []),
440
+ ...(includeResourceUpdate ? (['resource'] as const) : []),
436
441
  ])
437
442
  );
438
443
  let firmwareUpdatePlan: FirmwareUpdatePlan | undefined;
@@ -286,84 +286,142 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
286
286
  // check device goto bootloader mode
287
287
  let isFirstCheck = true;
288
288
  let checkCount = 0;
289
- // eslint-disable-next-line prefer-const
289
+ let hasPromptedWebDevice = false;
290
+ let isPromptingWebDevice = false;
291
+ let isFinished = false;
292
+ let intervalTimer: ReturnType<typeof setInterval> | undefined;
290
293
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
291
294
 
292
295
  const deviceType = this.device?.getCurrentDeviceType();
293
296
  const isTouchOrProDevice = deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro;
294
297
 
295
- const intervalTimer: ReturnType<typeof setInterval> | undefined = setInterval(
296
- async () => {
297
- checkCount += 1;
298
- Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
299
- if (isTouchOrProDevice && isFirstCheck) {
300
- isFirstCheck = false;
301
- Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] wait 3000ms');
302
- await wait(3000);
303
- }
298
+ const clearPollingTimers = () => {
299
+ if (intervalTimer !== undefined) {
300
+ clearInterval(intervalTimer);
301
+ intervalTimer = undefined;
302
+ }
303
+ if (timeoutTimer !== undefined) {
304
+ clearTimeout(timeoutTimer);
305
+ timeoutTimer = undefined;
306
+ }
307
+ };
304
308
 
305
- if (
306
- checkCount > 4 &&
307
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
308
- !this.payload.skipWebDevicePrompt
309
- ) {
310
- clearInterval(intervalTimer);
311
- clearTimeout(timeoutTimer);
309
+ const checkForBootloader = async (clearActiveTimers: boolean) => {
310
+ const found = await this._checkDeviceInBootloaderMode(
311
+ connectId,
312
+ clearActiveTimers ? intervalTimer : undefined,
313
+ clearActiveTimers ? timeoutTimer : undefined
314
+ );
315
+ if (found) {
316
+ isFinished = true;
317
+ clearPollingTimers();
318
+ }
319
+ return found;
320
+ };
312
321
 
313
- try {
314
- this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
315
- const confirmed = await this._promptDeviceInBootloaderForWebDevice();
316
- if (confirmed) {
317
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
318
- }
319
- } catch (e) {
320
- Log.log(
321
- 'FirmwareUpdateV2 [checkDeviceToBootloader] promptDeviceInBootloaderForWebDevice failed: ',
322
- e
323
- );
324
- this.checkPromise?.reject(e);
322
+ let startPolling: () => void = () => undefined;
323
+ const pollForBootloader = async () => {
324
+ if (isFinished || isPromptingWebDevice) return;
325
+ checkCount += 1;
326
+ Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
327
+ if (isTouchOrProDevice && isFirstCheck) {
328
+ isFirstCheck = false;
329
+ Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] wait 3000ms');
330
+ await wait(3000);
331
+ }
332
+
333
+ if (
334
+ checkCount > 4 &&
335
+ DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
336
+ !this.payload.skipWebDevicePrompt &&
337
+ !hasPromptedWebDevice &&
338
+ !isPromptingWebDevice
339
+ ) {
340
+ clearPollingTimers();
341
+ isPromptingWebDevice = true;
342
+ try {
343
+ this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
344
+ const confirmed = await this._promptDeviceInBootloaderForWebDevice();
345
+ hasPromptedWebDevice = true;
346
+ if (confirmed) {
347
+ await checkForBootloader(false);
348
+ }
349
+ // WebUSB enumeration can still be empty immediately after the chooser
350
+ // resolves. Resume a fresh bounded polling window instead of leaving the
351
+ // deferred check pending forever after the original timers were paused.
352
+ if (!isFinished) {
353
+ startPolling();
325
354
  }
326
- return;
355
+ } catch (e) {
356
+ isFinished = true;
357
+ clearPollingTimers();
358
+ Log.log(
359
+ 'FirmwareUpdateV2 [checkDeviceToBootloader] promptDeviceInBootloaderForWebDevice failed: ',
360
+ e
361
+ );
362
+ this.checkPromise?.reject(e);
363
+ } finally {
364
+ isPromptingWebDevice = false;
327
365
  }
366
+ return;
367
+ }
328
368
 
329
- if (isBleReconnect) {
330
- if (bleProbeInFlight) return;
331
- bleProbeInFlight = true;
332
- try {
333
- await this.device.deviceConnector?.acquire(
334
- this.device.originalDescriptor.id,
335
- null,
336
- true,
337
- this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
338
- );
339
- // Bound each probe so a request the rebooting device never received
340
- // frees the slot for the next tick instead of hanging into the
341
- // 30s reboot budget.
342
- await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
343
- if (this.device.isBootloader()) {
344
- clearInterval(intervalTimer);
345
- this.checkPromise?.resolve(true);
346
- }
347
- } catch (e) {
348
- // ignore error because of device is not connected
349
- Log.log('catch Bluetooth error when device is restarting: ', e);
350
- } finally {
351
- bleProbeInFlight = false;
369
+ if (isBleReconnect) {
370
+ if (bleProbeInFlight) return;
371
+ bleProbeInFlight = true;
372
+ try {
373
+ await this.device.deviceConnector?.acquire(
374
+ this.device.originalDescriptor.id,
375
+ null,
376
+ true,
377
+ this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
378
+ );
379
+ // Bound each probe so a request the rebooting device never received
380
+ // frees the slot for the next tick instead of hanging into the
381
+ // 30s reboot budget.
382
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
383
+ if (this.device.isBootloader()) {
384
+ isFinished = true;
385
+ clearPollingTimers();
386
+ this.checkPromise?.resolve(true);
352
387
  }
353
- } else {
354
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
388
+ } catch (e) {
389
+ // ignore error because of device is not connected
390
+ Log.log('catch Bluetooth error when device is restarting: ', e);
391
+ } finally {
392
+ bleProbeInFlight = false;
355
393
  }
356
- },
357
- isBleReconnect ? 3000 : 2000
358
- );
359
-
360
- // check goto bootloader mode timeout and throw error
361
- timeoutTimer = setTimeout(() => {
362
- if (this.checkPromise) {
363
- clearInterval(intervalTimer);
364
- this.checkPromise.reject(new Error());
394
+ } else {
395
+ await checkForBootloader(true);
365
396
  }
366
- }, 30000);
397
+ };
398
+
399
+ startPolling = () => {
400
+ if (isFinished) return;
401
+ clearPollingTimers();
402
+ intervalTimer = setInterval(
403
+ () => {
404
+ pollForBootloader().catch(error => {
405
+ if (isFinished) return;
406
+ isFinished = true;
407
+ clearPollingTimers();
408
+ this.checkPromise?.reject(error);
409
+ });
410
+ },
411
+ isBleReconnect ? 3000 : 2000
412
+ );
413
+ // Each automatic polling phase is bounded. Time spent in the browser's
414
+ // permission chooser is intentionally excluded from this deadline.
415
+ timeoutTimer = setTimeout(() => {
416
+ if (!isFinished && this.checkPromise) {
417
+ isFinished = true;
418
+ clearPollingTimers();
419
+ this.checkPromise.reject(new Error());
420
+ }
421
+ }, 30000);
422
+ };
423
+
424
+ startPolling();
367
425
  }
368
426
 
369
427
  private async _checkDeviceInBootloaderMode(
@@ -138,13 +138,19 @@ const getProtocolV2ZipEntrySizes = (entry: JSZip.JSZipObject) => {
138
138
 
139
139
  export function assertProtocolV2FirmwareTargetsSupported(
140
140
  deviceType: EDeviceType | string | undefined,
141
- params: FirmwareUpdateV4Params
141
+ params: FirmwareUpdateV4Params,
142
+ hasExplicitTargetSelection = false
142
143
  ) {
144
+ const requestedTargets = new Set(params.targetsToUpdate ?? []);
143
145
  const unsupportedTargets = new Set(
144
- (params.targetsToUpdate ?? []).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
146
+ Array.from(requestedTargets).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
145
147
  );
146
- if (params.se03Binary) unsupportedTargets.add('se03');
147
- if (params.se04Binary) unsupportedTargets.add('se04');
148
+ if (params.se03Binary && (!hasExplicitTargetSelection || requestedTargets.has('se03'))) {
149
+ unsupportedTargets.add('se03');
150
+ }
151
+ if (params.se04Binary && (!hasExplicitTargetSelection || requestedTargets.has('se04'))) {
152
+ unsupportedTargets.add('se04');
153
+ }
148
154
 
149
155
  if (!unsupportedTargets.size || deviceType === EDeviceType.Pro2) return;
150
156
 
@@ -559,6 +565,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
559
565
 
560
566
  private protocolV2ExpectedPath?: string;
561
567
 
568
+ private protocolV2HasExplicitTargetSelection = false;
569
+
562
570
  getSupportedProtocols() {
563
571
  return ['V2'] as const;
564
572
  }
@@ -584,6 +592,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
584
592
 
585
593
  const { payload } = this;
586
594
 
595
+ this.protocolV2HasExplicitTargetSelection = payload.targetsToUpdate !== undefined;
596
+
587
597
  if (typeof payload.retryCount !== 'number') {
588
598
  payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT;
589
599
  }
@@ -782,7 +792,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
782
792
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
783
793
  ? currentDeviceType
784
794
  : getDeviceType(deviceFeatures);
785
- assertProtocolV2FirmwareTargetsSupported(capabilityDeviceType, this.params);
795
+ assertProtocolV2FirmwareTargetsSupported(
796
+ capabilityDeviceType,
797
+ this.params,
798
+ this.protocolV2HasExplicitTargetSelection
799
+ );
786
800
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
787
801
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
788
802
  this.validateExpectedTargetVersions();
@@ -861,6 +875,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
861
875
  bootloaderBinary = remoteBinaries.bootloaderBinary;
862
876
  fwBinaryMap = remoteBinaries.fwBinaryMap;
863
877
  installItems = remoteBinaries.installItems;
878
+ } else {
879
+ const selectedInstallItems = this.filterProtocolV2LocalInstallItems(explicitInstallItems);
880
+ bootloaderBinary =
881
+ selectedInstallItems.find(item => item.kind === 'bootloader')?.binary ?? null;
882
+ fwBinaryMap = selectedInstallItems
883
+ .filter(item => item.kind === 'firmware')
884
+ .map(item => ({
885
+ fileName: item.fileName,
886
+ binary: item.binary,
887
+ targetId: item.targetId,
888
+ }));
864
889
  }
865
890
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
866
891
  } catch (err) {
@@ -1035,7 +1060,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1035
1060
  features: Features;
1036
1061
  firmwareType: EFirmwareType;
1037
1062
  }): Promise<FirmwareUpdateV4MemoryHost> {
1038
- const installItems = this.buildProtocolV2InstallItems({
1063
+ const availableInstallItems = this.buildProtocolV2InstallItems({
1039
1064
  bootloaderBinary: this.prepareBootloaderBinary(),
1040
1065
  fwBinaryMap: this.collectExplicitTargetBinaries(),
1041
1066
  });
@@ -1046,7 +1071,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1046
1071
  )
1047
1072
  );
1048
1073
  const localComponentTargets = new Set(
1049
- installItems.flatMap(item => {
1074
+ availableInstallItems.flatMap(item => {
1050
1075
  const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1051
1076
  return target ? [target] : [];
1052
1077
  })
@@ -1064,6 +1089,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1064
1089
  }
1065
1090
  );
1066
1091
  }
1092
+ const installItems = this.filterProtocolV2LocalInstallItems(availableInstallItems);
1067
1093
 
1068
1094
  const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
1069
1095
  [];
@@ -1165,7 +1191,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1165
1191
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1166
1192
  );
1167
1193
  }
1168
- const zip = await JSZip.loadAsync(binary);
1194
+ let zip: JSZip;
1195
+ try {
1196
+ zip = await JSZip.loadAsync(binary);
1197
+ } catch {
1198
+ throw ERRORS.TypedError(
1199
+ HardwareErrorCode.RuntimeError,
1200
+ 'Protocol V2 resource ZIP cannot be parsed',
1201
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1202
+ );
1203
+ }
1169
1204
  const zipEntries = Object.values(zip.files);
1170
1205
  if (
1171
1206
  zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
@@ -1698,6 +1733,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1698
1733
  .filter(target => !preparedTargets.has(target));
1699
1734
  }
1700
1735
 
1736
+ private filterProtocolV2LocalInstallItems(installItems: ProtocolV2InstallItem[]) {
1737
+ if (!this.protocolV2HasExplicitTargetSelection) {
1738
+ return installItems;
1739
+ }
1740
+ const requestedTargets = new Set(this.params.targetsToUpdate ?? []);
1741
+ return installItems.filter(item => {
1742
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1743
+ return target !== undefined && requestedTargets.has(target);
1744
+ });
1745
+ }
1746
+
1701
1747
  private buildProtocolV2InstallItems({
1702
1748
  bootloaderBinary,
1703
1749
  fwBinaryMap,
@@ -1,5 +1,7 @@
1
1
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
2
 
3
+ import { clearFirmwareUpdatePreparedPlanDeviceIdentityPin } from './FirmwareUpdatePreparedPlan';
4
+
3
5
  import type { FirmwareUpdateHostBinding } from '../../types/api/firmwareUpdate';
4
6
 
5
7
  const bindingError = (message: string): never => {
@@ -24,8 +26,12 @@ class FirmwareHostBindingRegistry {
24
26
  ) {
25
27
  return bindingError('Firmware host binding is invalid');
26
28
  }
29
+ const replacedPreparedPlanDigest = this.binding?.preparedPlanDigest;
27
30
  this.generation += 1;
28
31
  this.binding = binding;
32
+ if (replacedPreparedPlanDigest) {
33
+ clearFirmwareUpdatePreparedPlanDeviceIdentityPin(replacedPreparedPlanDigest);
34
+ }
29
35
  return this.generation;
30
36
  }
31
37
 
@@ -33,8 +39,12 @@ class FirmwareHostBindingRegistry {
33
39
  if (generation !== undefined && generation !== this.generation) {
34
40
  return false;
35
41
  }
42
+ const releasedPreparedPlanDigest = this.binding?.preparedPlanDigest;
36
43
  this.generation += 1;
37
44
  this.binding = undefined;
45
+ if (releasedPreparedPlanDigest) {
46
+ clearFirmwareUpdatePreparedPlanDeviceIdentityPin(releasedPreparedPlanDigest);
47
+ }
38
48
  return true;
39
49
  }
40
50
 
@@ -68,12 +68,13 @@ export function prepareFirmwareUpdateV4MemoryHost({
68
68
  );
69
69
  binaries.set(artifact.artifactRef, artifactBinary);
70
70
  const materializedEntries = input.materializedEntries?.map((entry, entryIndex) => {
71
- const entryBinary = new Uint8Array(entry.binary).slice();
72
71
  const entryArtifact = createReference(
73
- entryBinary.buffer as ArrayBuffer,
72
+ entry.binary,
74
73
  `${hostId}:entry:${artifactIndex}:${entryIndex}`
75
74
  );
76
- binaries.set(entryArtifact.artifactRef, entryBinary);
75
+ // Entry references are compact receipts for bytes that will be re-derived
76
+ // from the verified archive. Retaining another readable copy here doubles
77
+ // resource memory without adding an execution trust boundary.
77
78
  return {
78
79
  entryName: entry.entryName,
79
80
  artifact: entryArtifact,
@@ -167,92 +167,141 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
167
167
  let checkCount = 0;
168
168
  let hasPromptedWebDevice = false;
169
169
  let isPromptingWebDevice = false;
170
- // eslint-disable-next-line prefer-const
170
+ let isFinished = false;
171
+ let intervalTimer: ReturnType<typeof setInterval> | undefined;
171
172
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
172
173
 
173
174
  const isTouchOrProDevice =
174
175
  this?.device?.getCurrentDeviceType() === EDeviceType.Touch ||
175
176
  this?.device?.getCurrentDeviceType() === EDeviceType.Pro;
176
177
 
177
- const intervalTimer: ReturnType<typeof setInterval> | undefined = setInterval(
178
- async () => {
179
- checkCount += 1;
180
- Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
181
- if (isTouchOrProDevice && isFirstCheck) {
182
- isFirstCheck = false;
183
- Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] wait 3000ms');
184
- await wait(3000);
185
- }
178
+ const clearPollingTimers = () => {
179
+ if (intervalTimer !== undefined) {
180
+ clearInterval(intervalTimer);
181
+ intervalTimer = undefined;
182
+ }
183
+ if (timeoutTimer !== undefined) {
184
+ clearTimeout(timeoutTimer);
185
+ timeoutTimer = undefined;
186
+ }
187
+ };
186
188
 
187
- if (
188
- checkCount > 4 &&
189
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
190
- !this.payload.skipWebDevicePrompt &&
191
- !hasPromptedWebDevice &&
192
- !isPromptingWebDevice
193
- ) {
194
- clearInterval(intervalTimer);
195
- clearTimeout(timeoutTimer);
196
- isPromptingWebDevice = true;
197
- try {
198
- this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
199
- const confirmed = await this._promptDeviceInBootloaderForWebDevice();
200
- hasPromptedWebDevice = true;
201
- if (confirmed) {
202
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
203
- }
204
- } catch (e) {
205
- clearInterval(intervalTimer);
206
- clearTimeout(timeoutTimer);
207
- Log.log(
208
- 'FirmwareUpdateBaseMethod [checkDeviceToBootloader] _promptDeviceInBootloaderForWebDevice failed: ',
209
- e
210
- );
211
- this.checkPromise?.reject(e);
212
- } finally {
213
- isPromptingWebDevice = false;
189
+ const checkForBootloader = async (clearActiveTimers: boolean) => {
190
+ const found = await this._checkDeviceInBootloaderMode(
191
+ connectId,
192
+ clearActiveTimers ? intervalTimer : undefined,
193
+ clearActiveTimers ? timeoutTimer : undefined
194
+ );
195
+ if (found) {
196
+ isFinished = true;
197
+ clearPollingTimers();
198
+ }
199
+ return found;
200
+ };
201
+
202
+ let startPolling: () => void = () => undefined;
203
+ const pollForBootloader = async () => {
204
+ if (isFinished || isPromptingWebDevice) return;
205
+ checkCount += 1;
206
+ Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
207
+ if (isTouchOrProDevice && isFirstCheck) {
208
+ isFirstCheck = false;
209
+ Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] wait 3000ms');
210
+ await wait(3000);
211
+ }
212
+
213
+ if (
214
+ checkCount > 4 &&
215
+ DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
216
+ !this.payload.skipWebDevicePrompt &&
217
+ !hasPromptedWebDevice &&
218
+ !isPromptingWebDevice
219
+ ) {
220
+ clearPollingTimers();
221
+ isPromptingWebDevice = true;
222
+ try {
223
+ this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
224
+ const confirmed = await this._promptDeviceInBootloaderForWebDevice();
225
+ hasPromptedWebDevice = true;
226
+ if (confirmed) {
227
+ await checkForBootloader(false);
228
+ }
229
+ // WebUSB enumeration can still be empty immediately after the chooser
230
+ // resolves. Resume a fresh bounded polling window instead of leaving the
231
+ // deferred check pending forever after the original timers were paused.
232
+ if (!isFinished) {
233
+ startPolling();
214
234
  }
215
- return;
235
+ } catch (e) {
236
+ isFinished = true;
237
+ clearPollingTimers();
238
+ Log.log(
239
+ 'FirmwareUpdateBaseMethod [checkDeviceToBootloader] _promptDeviceInBootloaderForWebDevice failed: ',
240
+ e
241
+ );
242
+ this.checkPromise?.reject(e);
243
+ } finally {
244
+ isPromptingWebDevice = false;
216
245
  }
246
+ return;
247
+ }
217
248
 
218
- if (isBleReconnect) {
219
- if (bleProbeInFlight) return;
220
- bleProbeInFlight = true;
221
- try {
222
- await this.device.deviceConnector?.acquire(
223
- this.device.originalDescriptor.id,
224
- null,
225
- true,
226
- this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
227
- );
228
- // Bound each probe so a request the rebooting device never received
229
- // frees the slot for the next tick instead of hanging into the
230
- // 30s reboot budget.
231
- await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
232
- if (this.device.isBootloader()) {
233
- clearInterval(intervalTimer);
234
- this.checkPromise?.resolve(true);
235
- }
236
- } catch (e) {
237
- // ignore error because of device is not connected
238
- Log.log('catch Bluetooth error when device is restarting: ', e);
239
- } finally {
240
- bleProbeInFlight = false;
249
+ if (isBleReconnect) {
250
+ if (bleProbeInFlight) return;
251
+ bleProbeInFlight = true;
252
+ try {
253
+ await this.device.deviceConnector?.acquire(
254
+ this.device.originalDescriptor.id,
255
+ null,
256
+ true,
257
+ this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
258
+ );
259
+ // Bound each probe so a request the rebooting device never received
260
+ // frees the slot for the next tick instead of hanging into the
261
+ // 30s reboot budget.
262
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
263
+ if (this.device.isBootloader()) {
264
+ isFinished = true;
265
+ clearPollingTimers();
266
+ this.checkPromise?.resolve(true);
241
267
  }
242
- } else {
243
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
268
+ } catch (e) {
269
+ // ignore error because of device is not connected
270
+ Log.log('catch Bluetooth error when device is restarting: ', e);
271
+ } finally {
272
+ bleProbeInFlight = false;
244
273
  }
245
- },
246
- isBleReconnect ? 3000 : 2000
247
- );
248
-
249
- // check goto bootloader mode timeout and throw error
250
- timeoutTimer = setTimeout(() => {
251
- if (this.checkPromise) {
252
- clearInterval(intervalTimer);
253
- this.checkPromise.reject(new Error());
274
+ } else {
275
+ await checkForBootloader(true);
254
276
  }
255
- }, 30000);
277
+ };
278
+
279
+ startPolling = () => {
280
+ if (isFinished) return;
281
+ clearPollingTimers();
282
+ intervalTimer = setInterval(
283
+ () => {
284
+ pollForBootloader().catch(error => {
285
+ if (isFinished) return;
286
+ isFinished = true;
287
+ clearPollingTimers();
288
+ this.checkPromise?.reject(error);
289
+ });
290
+ },
291
+ isBleReconnect ? 3000 : 2000
292
+ );
293
+ // Each automatic polling phase is bounded. Time spent in the browser's
294
+ // permission chooser is intentionally excluded from this deadline.
295
+ timeoutTimer = setTimeout(() => {
296
+ if (!isFinished && this.checkPromise) {
297
+ isFinished = true;
298
+ clearPollingTimers();
299
+ this.checkPromise.reject(new Error());
300
+ }
301
+ }, 30000);
302
+ };
303
+
304
+ startPolling();
256
305
  }
257
306
 
258
307
  private async _checkDeviceInBootloaderMode(
@@ -216,15 +216,20 @@ export const validateProtocolV2FirmwareUpdateTargets = (
216
216
  if (value === undefined) {
217
217
  return [];
218
218
  }
219
+ if (!Array.isArray(value)) {
220
+ return planError('Protocol V2 firmware update targets are invalid');
221
+ }
222
+ const normalizedTargets = value.map(target =>
223
+ target === 'boot_resources' ? 'resource' : target
224
+ );
219
225
  if (
220
- !Array.isArray(value) ||
221
- value.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
222
- value.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
223
- new Set(value).size !== value.length
226
+ normalizedTargets.length > PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.size ||
227
+ normalizedTargets.some(target => !PROTOCOL_V2_FIRMWARE_UPDATE_TARGETS.has(target)) ||
228
+ new Set(normalizedTargets).size !== normalizedTargets.length
224
229
  ) {
225
230
  return planError('Protocol V2 firmware update targets are invalid');
226
231
  }
227
- return [...value] as FirmwareUpdateV4Target[];
232
+ return normalizedTargets as FirmwareUpdateV4Target[];
228
233
  };
229
234
 
230
235
  const assertExactKeys = (
@@ -916,6 +921,14 @@ export const buildFirmwareUpdatePlan = ({
916
921
  targetsToUpdate = [...new Set(artifacts.map(artifact => artifact.target))];
917
922
  }
918
923
 
924
+ if (
925
+ executor !== 'v4' &&
926
+ targetsToUpdate.includes('resource') &&
927
+ !targetsToUpdate.includes('firmware')
928
+ ) {
929
+ planError('Legacy resource updates require a firmware target');
930
+ }
931
+
919
932
  assertForcedTargetsRepresented({
920
933
  executor,
921
934
  forcedTargets: validatedForceTargets,