@onekeyfe/hd-core 1.2.0-alpha.97 → 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 (103) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +411 -86
  2. package/__tests__/check-firmware-release-protocol-v2.test.ts +199 -0
  3. package/__tests__/firmware-memory-host.test.ts +126 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +14 -2
  5. package/__tests__/firmware-update/firmware-host-binding.test.ts +9 -0
  6. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +573 -0
  7. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +150 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
  9. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  10. package/__tests__/firmware-update/firmware-update-plan.test.ts +293 -71
  11. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +53 -1
  12. package/__tests__/firmware-update/firmware-update-prepared-resource-executors.test.ts +326 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +2 -2
  14. package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
  15. package/__tests__/protocol-v2-resources.test.ts +148 -232
  16. package/__tests__/protocol-v2.test.ts +1326 -510
  17. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -3
  18. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  19. package/dist/api/CheckBLEFirmwareRelease.d.ts +3 -2
  20. package/dist/api/CheckBLEFirmwareRelease.d.ts.map +1 -1
  21. package/dist/api/CheckBootloaderRelease.d.ts +2 -1
  22. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  23. package/dist/api/CheckFirmwareRelease.d.ts +3 -2
  24. package/dist/api/CheckFirmwareRelease.d.ts.map +1 -1
  25. package/dist/api/FirmwareUpdateV2.d.ts +1 -0
  26. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  27. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  28. package/dist/api/FirmwareUpdateV4.d.ts +11 -9
  29. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  30. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  31. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  32. package/dist/api/firmware/FirmwareHostBinding.d.ts +1 -1
  33. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  34. package/dist/api/firmware/FirmwareMemoryHost.d.ts +22 -0
  35. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -0
  36. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts +11 -0
  37. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -0
  38. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  39. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +48 -1
  40. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  41. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +7 -1
  42. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  43. package/dist/api/firmware/getBinary.d.ts +0 -1
  44. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  45. package/dist/api/firmware/protocolV2Release.d.ts +33 -0
  46. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -0
  47. package/dist/data-manager/DataManager.d.ts +4 -3
  48. package/dist/data-manager/DataManager.d.ts.map +1 -1
  49. package/dist/index.d.ts +108 -114
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1825 -1006
  52. package/dist/protocols/protocol-v2/resources.d.ts +10 -28
  53. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  54. package/dist/types/api/checkAllFirmwareRelease.d.ts +20 -8
  55. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  56. package/dist/types/api/checkBLEFirmwareRelease.d.ts +2 -13
  57. package/dist/types/api/checkBLEFirmwareRelease.d.ts.map +1 -1
  58. package/dist/types/api/checkBootloaderRelease.d.ts +2 -6
  59. package/dist/types/api/checkBootloaderRelease.d.ts.map +1 -1
  60. package/dist/types/api/checkFirmwareRelease.d.ts +2 -13
  61. package/dist/types/api/checkFirmwareRelease.d.ts.map +1 -1
  62. package/dist/types/api/checkFirmwareTypeAvailable.d.ts +2 -2
  63. package/dist/types/api/checkFirmwareTypeAvailable.d.ts.map +1 -1
  64. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  65. package/dist/types/api/export.d.ts +1 -1
  66. package/dist/types/api/export.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdate.d.ts +4 -12
  68. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  71. package/dist/types/settings.d.ts +35 -36
  72. package/dist/types/settings.d.ts.map +1 -1
  73. package/package.json +4 -4
  74. package/src/api/CheckAllFirmwareRelease.ts +113 -80
  75. package/src/api/CheckBLEFirmwareRelease.ts +37 -5
  76. package/src/api/CheckBootloaderRelease.ts +35 -3
  77. package/src/api/CheckFirmwareRelease.ts +35 -7
  78. package/src/api/FirmwareUpdateV2.ts +231 -138
  79. package/src/api/FirmwareUpdateV3.ts +101 -56
  80. package/src/api/FirmwareUpdateV4.ts +936 -408
  81. package/src/api/UploadPortfolio.ts +18 -0
  82. package/src/api/device/DeviceUpdateBootloader.ts +37 -11
  83. package/src/api/firmware/FirmwareHostBinding.ts +26 -3
  84. package/src/api/firmware/FirmwareMemoryHost.ts +143 -0
  85. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +207 -0
  86. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
  87. package/src/api/firmware/FirmwareUpdatePlan.ts +295 -122
  88. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +49 -12
  89. package/src/api/firmware/protocolV2Release.ts +168 -0
  90. package/src/data-manager/DataManager.ts +35 -19
  91. package/src/index.ts +10 -0
  92. package/src/protocols/protocol-v2/resources.ts +157 -341
  93. package/src/types/api/checkAllFirmwareRelease.ts +27 -12
  94. package/src/types/api/checkBLEFirmwareRelease.ts +4 -13
  95. package/src/types/api/checkBootloaderRelease.ts +2 -6
  96. package/src/types/api/checkFirmwareRelease.ts +2 -13
  97. package/src/types/api/checkFirmwareTypeAvailable.ts +2 -2
  98. package/src/types/api/deviceUpdateBootloader.ts +1 -0
  99. package/src/types/api/export.ts +6 -1
  100. package/src/types/api/firmwareUpdate.ts +12 -17
  101. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  102. package/src/types/settings.ts +35 -61
  103. package/src/utils/deviceFeaturesUtils.ts +2 -2
@@ -29,10 +29,12 @@ import { DataManager } from '../data-manager';
29
29
  import { DEVICE } from '../events';
30
30
  import { type FirmwareByteSource, openFirmwareByteSource } from './firmware/FirmwareArtifactSource';
31
31
  import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
32
+ import { readVerifiedPreparedResourceArchive } from './firmware/FirmwarePreparedResourceArchive';
32
33
  import {
33
34
  assertFirmwareUpdatePreparedPlanBinding,
34
35
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
35
- getFirmwareUpdateResourceName,
36
+ getFirmwareUpdatePreparedRawArtifact,
37
+ validateFirmwareUpdatePreparedPlan,
36
38
  } from './firmware/FirmwareUpdatePreparedPlan';
37
39
 
38
40
  import type { Features, KnownDevice } from '../types';
@@ -46,6 +48,7 @@ import type { FirmwareUpdatePreparedPlan } from '../types/api/firmwareUpdatePrep
46
48
  type Params = {
47
49
  binary?: ArrayBuffer;
48
50
  artifact?: FirmwareArtifactReference;
51
+ hostBindingGeneration?: number;
49
52
  resourceEntries?: Array<{
50
53
  entryName: string;
51
54
  artifact: FirmwareArtifactReference;
@@ -159,15 +162,17 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
159
162
  { name: 'firmwareType', type: 'string' },
160
163
  ]);
161
164
 
162
- if (
163
- 'binary' in payload &&
164
- payload.binary !== undefined &&
165
- 'artifact' in payload &&
166
- payload.artifact !== undefined
167
- ) {
165
+ const hasPreparedPlan = payload.preparedPlan !== undefined;
166
+ if (hasPreparedPlan && (payload.binary !== undefined || payload.version !== undefined)) {
168
167
  throw ERRORS.TypedError(
169
168
  HardwareErrorCode.CallMethodInvalidParameter,
170
- 'Firmware update binary and artifact are mutually exclusive'
169
+ 'Prepared firmware plans cannot be combined with legacy firmware inputs'
170
+ );
171
+ }
172
+ if (!hasPreparedPlan && payload.artifact !== undefined) {
173
+ throw ERRORS.TypedError(
174
+ HardwareErrorCode.CallMethodInvalidParameter,
175
+ 'Firmware artifacts require a prepared plan'
171
176
  );
172
177
  }
173
178
 
@@ -184,7 +189,7 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
184
189
  isUpdateBootloader: payload.isUpdateBootloader,
185
190
  };
186
191
 
187
- if ('version' in payload) {
192
+ if (!hasPreparedPlan && 'version' in payload) {
188
193
  this.params = {
189
194
  ...this.params,
190
195
  version: payload.version,
@@ -192,16 +197,25 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
192
197
  };
193
198
  }
194
199
 
195
- if ('binary' in payload) {
200
+ if (!hasPreparedPlan && 'binary' in payload) {
196
201
  this.params = {
197
202
  ...this.params,
198
203
  binary: payload.binary,
199
204
  };
200
205
  }
201
206
 
202
- if ('artifact' in payload) {
203
- const hostBinding = resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration);
207
+ if (hasPreparedPlan) {
208
+ const preparedPlan = validateFirmwareUpdatePreparedPlan(payload.preparedPlan);
209
+ const hostBinding = resolveFirmwareUpdateHostBinding(
210
+ payload.hostBindingGeneration,
211
+ preparedPlan.preparedPlanDigest
212
+ );
204
213
  const target = payload.isUpdateBootloader ? 'bootloader' : payload.updateType;
214
+ const plannedArtifact = getFirmwareUpdatePreparedRawArtifact({
215
+ preparedPlan,
216
+ target,
217
+ role: target,
218
+ }).artifact;
205
219
  const resourceBindings = (payload.resourceEntries ?? []).map(
206
220
  (entry: { entryName: string; artifact: FirmwareArtifactReference }) => ({
207
221
  target: 'resource' as const,
@@ -210,18 +224,21 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
210
224
  })
211
225
  );
212
226
  assertFirmwareUpdatePreparedPlanBinding({
213
- preparedPlan: payload.preparedPlan,
227
+ preparedPlan,
214
228
  executor: 'v2',
215
229
  platform: payload.platform,
216
- scopeTargets: [target, ...(target === 'firmware' ? (['resource'] as const) : [])],
217
- bindings: [{ target, artifact: payload.artifact }, ...resourceBindings],
230
+ scopeTargets: [
231
+ target,
232
+ ...(target === 'firmware' && resourceBindings.length ? (['resource'] as const) : []),
233
+ ],
234
+ bindings: [{ target, artifact: payload.artifact ?? plannedArtifact }, ...resourceBindings],
218
235
  });
219
236
  this.params = {
220
237
  ...this.params,
221
- preparedPlan: payload.preparedPlan,
222
- artifact: payload.artifact,
223
- resourceEntries: payload.resourceEntries,
238
+ preparedPlan,
239
+ artifact: plannedArtifact,
224
240
  artifactReader: hostBinding.artifactReader,
241
+ firmwareType: preparedPlan.firmwareType,
225
242
  };
226
243
  }
227
244
  }
@@ -269,84 +286,142 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
269
286
  // check device goto bootloader mode
270
287
  let isFirstCheck = true;
271
288
  let checkCount = 0;
272
- // 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;
273
293
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
274
294
 
275
295
  const deviceType = this.device?.getCurrentDeviceType();
276
296
  const isTouchOrProDevice = deviceType === EDeviceType.Touch || deviceType === EDeviceType.Pro;
277
297
 
278
- const intervalTimer: ReturnType<typeof setInterval> | undefined = setInterval(
279
- async () => {
280
- checkCount += 1;
281
- Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] isFirstCheck: ', isFirstCheck);
282
- if (isTouchOrProDevice && isFirstCheck) {
283
- isFirstCheck = false;
284
- Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] wait 3000ms');
285
- await wait(3000);
286
- }
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
+ };
287
308
 
288
- if (
289
- checkCount > 4 &&
290
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
291
- !this.payload.skipWebDevicePrompt
292
- ) {
293
- clearInterval(intervalTimer);
294
- 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
+ };
295
321
 
296
- try {
297
- this.postTipMessage(FirmwareUpdateTipMessage.SelectDeviceInBootloaderForWebDevice);
298
- const confirmed = await this._promptDeviceInBootloaderForWebDevice();
299
- if (confirmed) {
300
- await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
301
- }
302
- } catch (e) {
303
- Log.log(
304
- 'FirmwareUpdateV2 [checkDeviceToBootloader] promptDeviceInBootloaderForWebDevice failed: ',
305
- e
306
- );
307
- 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();
308
354
  }
309
- 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;
310
365
  }
366
+ return;
367
+ }
311
368
 
312
- if (isBleReconnect) {
313
- if (bleProbeInFlight) return;
314
- bleProbeInFlight = true;
315
- try {
316
- await this.device.deviceConnector?.acquire(
317
- this.device.originalDescriptor.id,
318
- null,
319
- true,
320
- this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
321
- );
322
- // Bound each probe so a request the rebooting device never received
323
- // frees the slot for the next tick instead of hanging into the
324
- // 30s reboot budget.
325
- await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
326
- if (this.device.isBootloader()) {
327
- clearInterval(intervalTimer);
328
- this.checkPromise?.resolve(true);
329
- }
330
- } catch (e) {
331
- // ignore error because of device is not connected
332
- Log.log('catch Bluetooth error when device is restarting: ', e);
333
- } finally {
334
- 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);
335
387
  }
336
- } else {
337
- 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;
338
393
  }
339
- },
340
- isBleReconnect ? 3000 : 2000
341
- );
342
-
343
- // check goto bootloader mode timeout and throw error
344
- timeoutTimer = setTimeout(() => {
345
- if (this.checkPromise) {
346
- clearInterval(intervalTimer);
347
- this.checkPromise.reject(new Error());
394
+ } else {
395
+ await checkForBootloader(true);
348
396
  }
349
- }, 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();
350
425
  }
351
426
 
352
427
  private async _checkDeviceInBootloaderMode(
@@ -531,6 +606,18 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
531
606
  }
532
607
  };
533
608
 
609
+ const preparedResourceRequested =
610
+ !params.isUpdateBootloader &&
611
+ params.updateType === 'firmware' &&
612
+ (params.preparedPlan?.targetsToUpdate.includes('resource') ?? false);
613
+ if (preparedResourceRequested && device.isBootloader()) {
614
+ throw ERRORS.TypedError(
615
+ HardwareErrorCode.RuntimeError,
616
+ 'Prepared firmware resources require the device application mode',
617
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared', artifactName: 'resource' }
618
+ );
619
+ }
620
+
534
621
  if (!device.isBootloader() && features) {
535
622
  const serialNo = device.getCurrentSerialNo();
536
623
  // should go to bootloader mode manually
@@ -541,72 +628,78 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
541
628
  // All network-backed input must be ready before the first device mutation.
542
629
  await acquireFirmwareSource();
543
630
 
544
- // check & upgrade firmware resource
545
- if (this.isSupportResourceUpdate(params.updateType)) {
631
+ // PreparedPlan 资源只依赖获批 ZIP;live release 仅保留给非 prepared 旧流程。
632
+ if (preparedResourceRequested) {
633
+ if (!this.isSupportResourceUpdate(params.updateType)) {
634
+ throw ERRORS.TypedError(
635
+ HardwareErrorCode.RuntimeError,
636
+ 'Prepared firmware resources are not supported by this device',
637
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared', artifactName: 'resource' }
638
+ );
639
+ }
546
640
  this.postTipMessage('CheckLatestUiResource');
547
- const resourceUrl = DataManager.getSysResourcesLatestRelease({
548
- features,
549
- forcedUpdateRes: params.forcedUpdateRes,
550
- firmwareType,
641
+ this.postTipMessage('DownloadLatestUiResource');
642
+ const verifiedEntries = await readVerifiedPreparedResourceArchive({
643
+ preparedPlan: params.preparedPlan as FirmwareUpdatePreparedPlan,
644
+ reader: params.artifactReader,
551
645
  });
552
- if (resourceUrl) {
553
- this.postTipMessage('DownloadLatestUiResource');
554
- if (params.resourceEntries?.length) {
555
- const sources: Array<{
556
- entryName: string;
557
- source: FirmwareByteSource;
558
- }> = [];
559
- try {
560
- for (const entry of params.resourceEntries) {
561
- const resourceName = getFirmwareUpdateResourceName(entry.entryName);
562
- const source = await openFirmwareByteSource({
563
- artifact: entry.artifact,
564
- reader: params.artifactReader,
565
- });
566
- if (!source) {
567
- throw new Error('Firmware resource entry is not prepared');
568
- }
569
- sources.push({ entryName: resourceName, source });
570
- }
571
- await updateResourcesFromSources(
572
- this.device.getCommands().typedCall.bind(this.device.getCommands()),
573
- this.postMessage,
574
- device,
575
- sources.map(entry => ({
576
- entryName: entry.entryName,
577
- source: entry.source,
578
- }))
579
- );
580
- } finally {
581
- await Promise.all(
582
- sources.map(entry => entry.source.close().catch(() => undefined))
583
- );
584
- }
585
- this.postTipMessage('DownloadLatestUiResourceSuccess');
586
- } else {
587
- if (
588
- params.artifactReader ||
589
- DataManager.getSettings('firmwareManifestMode') === 'external-only'
590
- ) {
646
+ const sources: Array<{ entryName: string; source: FirmwareByteSource }> = [];
647
+ try {
648
+ for (const entry of verifiedEntries) {
649
+ const source = await openFirmwareByteSource({ binary: entry.binary });
650
+ if (!source) {
591
651
  throw ERRORS.TypedError(
592
652
  HardwareErrorCode.RuntimeError,
593
- 'Firmware resource must be prepared by the external firmware host',
653
+ `Firmware resource entry ${entry.entryName} is empty`,
594
654
  {
595
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
655
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
596
656
  artifactName: 'resource',
597
657
  }
598
658
  );
599
659
  }
600
- const resourceBinary: ArrayBuffer | Buffer = (await getSysResourceBinary(resourceUrl))
601
- .binary;
602
- this.postTipMessage('DownloadLatestUiResourceSuccess');
603
- await updateResources(
604
- this.device.getCommands().typedCall.bind(this.device.getCommands()),
605
- this.postMessage,
606
- device,
607
- resourceBinary
660
+ sources.push({ entryName: entry.entryName, source });
661
+ }
662
+ await updateResourcesFromSources(
663
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
664
+ this.postMessage,
665
+ device,
666
+ sources
667
+ );
668
+ } finally {
669
+ await Promise.all(sources.map(entry => entry.source.close().catch(() => undefined)));
670
+ }
671
+ this.postTipMessage('DownloadLatestUiResourceSuccess');
672
+ } else if (this.isSupportResourceUpdate(params.updateType)) {
673
+ this.postTipMessage('CheckLatestUiResource');
674
+ const resourceUrl = DataManager.getSysResourcesLatestRelease({
675
+ features,
676
+ forcedUpdateRes: params.forcedUpdateRes,
677
+ firmwareType,
678
+ });
679
+ if (resourceUrl) {
680
+ this.postTipMessage('DownloadLatestUiResource');
681
+ if (
682
+ params.artifactReader ||
683
+ DataManager.getSettings('firmwareManifestMode') === 'external-only'
684
+ ) {
685
+ throw ERRORS.TypedError(
686
+ HardwareErrorCode.RuntimeError,
687
+ 'Firmware resource must be prepared by the external firmware host',
688
+ {
689
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
690
+ artifactName: 'resource',
691
+ }
608
692
  );
609
693
  }
694
+ const resourceBinary: ArrayBuffer | Buffer = (await getSysResourceBinary(resourceUrl))
695
+ .binary;
696
+ this.postTipMessage('DownloadLatestUiResourceSuccess');
697
+ await updateResources(
698
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
699
+ this.postMessage,
700
+ device,
701
+ resourceBinary
702
+ );
610
703
  }
611
704
  }
612
705
 
@@ -21,10 +21,12 @@ import { DEVICE } from '../events';
21
21
  import { buildProtocolV1FeaturesPayload } from '../deviceProfile';
22
22
  import { openFirmwareByteSource } from './firmware/FirmwareArtifactSource';
23
23
  import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
24
+ import { readVerifiedPreparedResourceArchive } from './firmware/FirmwarePreparedResourceArchive';
24
25
  import {
25
26
  assertFirmwareUpdatePreparedPlanBinding,
26
27
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
27
- getFirmwareUpdateResourceName,
28
+ getFirmwareUpdatePreparedRawArtifact,
29
+ validateFirmwareUpdatePreparedPlan,
28
30
  } from './firmware/FirmwareUpdatePreparedPlan';
29
31
 
30
32
  import type {
@@ -77,54 +79,97 @@ export default class FirmwareUpdateV3 extends FirmwareUpdateBaseMethod<FirmwareU
77
79
  { name: 'firmwareType', type: 'string' },
78
80
  { name: 'platform', type: 'string' },
79
81
  ]);
80
- const hostBinding =
81
- payload.artifacts || payload.preparedPlan
82
- ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
83
- : undefined;
84
- if (hostBinding) {
82
+ const hasPreparedPlan = payload.preparedPlan !== undefined;
83
+ if (!hasPreparedPlan && payload.artifacts !== undefined) {
84
+ throw ERRORS.TypedError(
85
+ HardwareErrorCode.CallMethodInvalidParameter,
86
+ 'Firmware artifacts require a prepared plan'
87
+ );
88
+ }
89
+ const preparedPlan = hasPreparedPlan
90
+ ? validateFirmwareUpdatePreparedPlan(payload.preparedPlan)
91
+ : undefined;
92
+ const hasLegacyInputs = [
93
+ payload.bleVersion,
94
+ payload.bleBinary,
95
+ payload.firmwareVersion,
96
+ payload.firmwareBinary,
97
+ payload.resourceBinary,
98
+ payload.bootloaderVersion,
99
+ payload.bootloaderBinary,
100
+ ].some(input => input !== undefined);
101
+ if (preparedPlan && hasLegacyInputs) {
102
+ throw ERRORS.TypedError(
103
+ HardwareErrorCode.CallMethodInvalidParameter,
104
+ 'Prepared firmware plans cannot be combined with legacy firmware inputs'
105
+ );
106
+ }
107
+ const artifactReader = preparedPlan
108
+ ? resolveFirmwareUpdateHostBinding(
109
+ payload.hostBindingGeneration,
110
+ preparedPlan.preparedPlanDigest
111
+ ).artifactReader
112
+ : payload.artifactReader;
113
+ const preparedArtifacts: NonNullable<FirmwareUpdateV3Params['artifacts']> = {};
114
+ if (preparedPlan) {
115
+ const componentTargets = ['bootloader', 'firmware', 'ble'] as const;
116
+ const supportedTargets = new Set<string>([...componentTargets, 'resource']);
117
+ const unsupportedTarget = preparedPlan.targetsToUpdate.find(
118
+ target => !supportedTargets.has(target)
119
+ );
120
+ if (unsupportedTarget) {
121
+ throw ERRORS.TypedError(
122
+ HardwareErrorCode.RuntimeError,
123
+ `Firmware prepared plan target ${unsupportedTarget} is not supported by V3`,
124
+ { firmwareUpdateCode: 'FirmwarePreparedPlanInvalid' }
125
+ );
126
+ }
127
+ for (const target of componentTargets) {
128
+ if (preparedPlan.targetsToUpdate.includes(target)) {
129
+ preparedArtifacts[target] = getFirmwareUpdatePreparedRawArtifact({
130
+ preparedPlan,
131
+ target,
132
+ role: target,
133
+ }).artifact;
134
+ }
135
+ }
136
+ const resourceBindings = (payload.artifacts?.resourceEntries ?? []).map(
137
+ (entry: { entryName: string; artifact: FirmwareArtifactReference }) => ({
138
+ target: 'resource' as const,
139
+ entryName: entry.entryName,
140
+ artifact: entry.artifact,
141
+ })
142
+ );
85
143
  assertFirmwareUpdatePreparedPlanBinding({
86
- preparedPlan: payload.preparedPlan,
144
+ preparedPlan,
87
145
  executor: 'v3',
88
146
  platform: payload.platform,
89
- scopeTargets: ['bootloader', 'firmware', 'resource', 'ble'],
147
+ scopeTargets: [
148
+ 'bootloader',
149
+ 'firmware',
150
+ 'ble',
151
+ ...(resourceBindings.length ? (['resource'] as const) : []),
152
+ ],
90
153
  bindings: [
91
- ...(payload.artifacts?.bootloader
92
- ? [
93
- {
94
- target: 'bootloader' as const,
95
- artifact: payload.artifacts.bootloader,
96
- },
97
- ]
98
- : []),
99
- ...(payload.artifacts?.firmware
100
- ? [
101
- {
102
- target: 'firmware' as const,
103
- artifact: payload.artifacts.firmware,
104
- },
105
- ]
106
- : []),
107
- ...(payload.artifacts?.ble
108
- ? [
109
- {
110
- target: 'ble' as const,
111
- artifact: payload.artifacts.ble,
112
- },
113
- ]
114
- : []),
115
- ...(payload.artifacts?.resourceEntries ?? []).map(
116
- (entry: { entryName: string; artifact: FirmwareArtifactReference }) => ({
117
- target: 'resource' as const,
118
- entryName: entry.entryName,
119
- artifact: entry.artifact,
120
- })
121
- ),
154
+ ...componentTargets.flatMap(target => {
155
+ const plannedArtifact = preparedArtifacts[target];
156
+ const suppliedArtifact = payload.artifacts?.[target];
157
+ return plannedArtifact || suppliedArtifact
158
+ ? [
159
+ {
160
+ target,
161
+ artifact: suppliedArtifact ?? (plannedArtifact as FirmwareArtifactReference),
162
+ },
163
+ ]
164
+ : [];
165
+ }),
166
+ ...resourceBindings,
122
167
  ],
123
168
  });
124
169
  }
125
170
 
126
171
  this.params = {
127
- preparedPlan: payload.preparedPlan,
172
+ preparedPlan,
128
173
  bleBinary: payload.bleBinary,
129
174
  firmwareBinary: payload.firmwareBinary,
130
175
  forcedUpdateRes: payload.forcedUpdateRes,
@@ -133,10 +178,10 @@ export default class FirmwareUpdateV3 extends FirmwareUpdateBaseMethod<FirmwareU
133
178
  bootloaderBinary: payload.bootloaderBinary,
134
179
  firmwareVersion: payload.firmwareVersion,
135
180
  resourceBinary: payload.resourceBinary,
136
- firmwareType: payload.firmwareType,
181
+ firmwareType: preparedPlan?.firmwareType ?? payload.firmwareType,
137
182
  platform: payload.platform,
138
- artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
139
- artifacts: payload.artifacts,
183
+ artifactReader,
184
+ artifacts: preparedPlan ? preparedArtifacts : undefined,
140
185
  };
141
186
  }
142
187
 
@@ -263,25 +308,25 @@ export default class FirmwareUpdateV3 extends FirmwareUpdateBaseMethod<FirmwareU
263
308
  }
264
309
 
265
310
  private async prepareResourceInput(firmwareType: EFirmwareType) {
266
- const preparedEntries = this.params.artifacts?.resourceEntries;
267
- if (preparedEntries?.length && this.params.resourceBinary) {
268
- throw ERRORS.TypedError(
269
- HardwareErrorCode.RuntimeError,
270
- 'Firmware resource input must not contain both archive binary and prepared entries'
271
- );
272
- }
273
- if (preparedEntries?.length) {
311
+ if (this.params.preparedPlan?.targetsToUpdate.includes('resource')) {
312
+ const verifiedEntries = await readVerifiedPreparedResourceArchive({
313
+ preparedPlan: this.params.preparedPlan,
314
+ reader: this.params.artifactReader,
315
+ });
274
316
  const resourceEntries: Array<{ entryName: string; source: FirmwareByteSource }> = [];
275
- for (const entry of preparedEntries) {
276
- const resourceName = getFirmwareUpdateResourceName(entry.entryName);
277
- const source = await this.openArtifactSource(undefined, entry.artifact);
317
+ for (const entry of verifiedEntries) {
318
+ const source = await this.openArtifactSource(entry.binary, undefined);
278
319
  if (!source) {
279
320
  throw ERRORS.TypedError(
280
321
  HardwareErrorCode.RuntimeError,
281
- `Firmware resource entry ${entry.entryName} is not prepared`
322
+ `Firmware resource entry ${entry.entryName} is empty`,
323
+ {
324
+ firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch',
325
+ artifactName: 'resource',
326
+ }
282
327
  );
283
328
  }
284
- resourceEntries.push({ entryName: resourceName, source });
329
+ resourceEntries.push({ entryName: entry.entryName, source });
285
330
  }
286
331
  return {
287
332
  resourceBinary: null,