@onekeyfe/hd-core 1.2.0-alpha.66 → 1.2.0-alpha.68

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 (111) hide show
  1. package/__tests__/connectSettings.test.ts +47 -5
  2. package/__tests__/device-initialize-timeout.test.ts +56 -0
  3. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  5. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  6. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  7. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  11. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  12. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +371 -94
  17. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  20. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  22. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts +25 -4
  24. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  25. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  26. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  28. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  31. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  32. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  36. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  37. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  38. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  39. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  40. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  41. package/dist/api/firmware/getBinary.d.ts +7 -0
  42. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  43. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  44. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  45. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  46. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/data-manager/DataManager.d.ts +1 -0
  49. package/dist/data-manager/DataManager.d.ts.map +1 -1
  50. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/index.d.ts +179 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3020 -955
  56. package/dist/inject.d.ts.map +1 -1
  57. package/dist/lowLevelInject.d.ts.map +1 -1
  58. package/dist/topLevelInject.d.ts.map +1 -1
  59. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  60. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  61. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  62. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  63. package/dist/types/api/export.d.ts +4 -1
  64. package/dist/types/api/export.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  66. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  68. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  71. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  72. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  73. package/dist/types/api/index.d.ts +9 -1
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/settings.d.ts +13 -0
  76. package/dist/types/settings.d.ts.map +1 -1
  77. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  78. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  79. package/package.json +4 -4
  80. package/src/api/CheckAllFirmwareRelease.ts +57 -6
  81. package/src/api/FirmwareUpdate.ts +13 -1
  82. package/src/api/FirmwareUpdateV2.ts +296 -121
  83. package/src/api/FirmwareUpdateV3.ts +263 -57
  84. package/src/api/FirmwareUpdateV4.ts +863 -243
  85. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  86. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  87. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  88. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  89. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  90. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  91. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  92. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  93. package/src/api/firmware/updateBootloader.ts +19 -4
  94. package/src/api/firmware/uploadFirmware.ts +140 -22
  95. package/src/data-manager/DataManager.ts +59 -54
  96. package/src/data-manager/connectSettings.ts +11 -0
  97. package/src/device/Device.ts +6 -1
  98. package/src/index.ts +5 -0
  99. package/src/inject.ts +22 -2
  100. package/src/lowLevelInject.ts +5 -1
  101. package/src/topLevelInject.ts +5 -1
  102. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  103. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  104. package/src/types/api/export.ts +18 -0
  105. package/src/types/api/firmwareUpdate.ts +75 -0
  106. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  107. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  108. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  109. package/src/types/api/index.ts +14 -0
  110. package/src/types/settings.ts +13 -0
  111. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -1,6 +1,13 @@
1
- import { EDeviceType, EFirmwareType, ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
1
+ import {
2
+ EDeviceType,
3
+ EFirmwareType,
4
+ ERRORS,
5
+ HardwareError,
6
+ HardwareErrorCode,
7
+ } from '@onekeyfe/hd-shared';
2
8
  import semver from 'semver';
3
9
 
10
+ import { BaseMethod } from './BaseMethod';
4
11
  import { UI_REQUEST } from '../constants/ui-request';
5
12
  import { DataManager } from '../data-manager';
6
13
  import {
@@ -8,13 +15,22 @@ import {
8
15
  getBootloaderReleaseInfo,
9
16
  getFirmwareReleaseInfo,
10
17
  } from './firmware/releaseHelper';
18
+ import {
19
+ buildFirmwareUpdatePlan,
20
+ validateFirmwareUpdatePlanForceTargets,
21
+ } from './firmware/FirmwareUpdatePlan';
11
22
  import { getBridgeReleaseInfo } from '../utils/bridgeUpdate';
12
- import { getDeviceFirmwareVersion, getDeviceType, getFirmwareType } from '../utils';
13
- import { BaseMethod } from './BaseMethod';
14
23
  import {
15
24
  buildProtocolV2ResourceUpdatePlan,
16
25
  readProtocolV2ResourceInventory,
17
26
  } from '../protocols/protocol-v2/resources';
27
+ import {
28
+ LoggerNames,
29
+ getDeviceFirmwareVersion,
30
+ getDeviceType,
31
+ getFirmwareType,
32
+ getLogger,
33
+ } from '../utils';
18
34
 
19
35
  import type {
20
36
  AllFirmwareRelease,
@@ -30,6 +46,7 @@ import type {
30
46
  IProtocolV2FirmwareComponent,
31
47
  IProtocolV2FirmwareComponentTarget,
32
48
  } from '../types';
49
+ import type { FirmwareUpdatePlan } from '../types/api/firmwareUpdatePlan';
33
50
 
34
51
  const UPDATE_TARGET_BY_COMPONENT_TARGET: Readonly<
35
52
  Partial<Record<IProtocolV2FirmwareComponentTarget, FirmwareUpdateV4Target>>
@@ -252,6 +269,8 @@ export function buildProtocolV2FirmwareRelease({
252
269
  };
253
270
  }
254
271
 
272
+ const Log = getLogger(LoggerNames.Method);
273
+
255
274
  export default class CheckAllFirmwareRelease extends BaseMethod {
256
275
  getSupportedProtocols() {
257
276
  return ['V1', 'V2'] as const;
@@ -269,8 +288,13 @@ export default class CheckAllFirmwareRelease extends BaseMethod {
269
288
  }
270
289
 
271
290
  const { features } = this.device;
272
- const { checkBridgeRelease, firmwareType: firmwareTypeParams } = this
273
- .payload as CheckAllFirmwareReleaseParams;
291
+ const {
292
+ checkBridgeRelease,
293
+ firmwareType: firmwareTypeParams,
294
+ platform,
295
+ forceUpdateTargets,
296
+ } = this.payload as CheckAllFirmwareReleaseParams;
297
+ const validatedForceUpdateTargets = validateFirmwareUpdatePlanForceTargets(forceUpdateTargets);
274
298
 
275
299
  if (!features) {
276
300
  return Promise.resolve(null);
@@ -301,6 +325,32 @@ export default class CheckAllFirmwareRelease extends BaseMethod {
301
325
  firmwareType,
302
326
  });
303
327
  const bleFirmwareReleaseInfo = getBleFirmwareReleaseInfo(features);
328
+ let firmwareUpdatePlan: FirmwareUpdatePlan | undefined;
329
+ try {
330
+ firmwareUpdatePlan = buildFirmwareUpdatePlan({
331
+ features,
332
+ firmwareType,
333
+ platform: platform ?? 'web',
334
+ firmware: firmwareRelease,
335
+ ble: bleFirmwareReleaseInfo,
336
+ bootloader: bootloaderRelease,
337
+ forceUpdateTargets: validatedForceUpdateTargets,
338
+ });
339
+ } catch (error) {
340
+ if (
341
+ validatedForceUpdateTargets.length > 0 ||
342
+ !(error instanceof HardwareError) ||
343
+ error.params?.firmwareUpdateCode !== 'FirmwarePlanInvalid'
344
+ ) {
345
+ throw error;
346
+ }
347
+ // A Plan is an optional external-host capability. Release checks remain
348
+ // available for legacy and recovery flows when a Plan cannot be built.
349
+ Log.warn(
350
+ '[CheckAllFirmwareRelease] Optional firmware Plan is unavailable; using the legacy release result'
351
+ );
352
+ firmwareUpdatePlan = undefined;
353
+ }
304
354
 
305
355
  return {
306
356
  firmware: firmwareRelease,
@@ -315,7 +365,8 @@ export default class CheckAllFirmwareRelease extends BaseMethod {
315
365
  }
316
366
  : undefined,
317
367
  features,
318
- } as AllFirmwareRelease;
368
+ firmwareUpdatePlan,
369
+ } as unknown as AllFirmwareRelease;
319
370
  }
320
371
 
321
372
  private async runProtocolV2(): Promise<AllFirmwareRelease> {
@@ -10,6 +10,7 @@ import { UI_REQUEST } from '../constants/ui-request';
10
10
  import { BaseMethod } from './BaseMethod';
11
11
  import { validateParams } from './helpers/paramsValidator';
12
12
  import { getBinary } from './firmware/getBinary';
13
+ import { BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS } from './firmware/FirmwareUpdateBaseMethod';
13
14
  import { uploadFirmware } from './firmware/uploadFirmware';
14
15
  import { createUiMessage } from '../events';
15
16
  import { DeviceModelToTypes, type KnownDevice } from '../types';
@@ -78,6 +79,10 @@ export default class FirmwareUpdate extends BaseMethod<Params> {
78
79
  this.checkPromise = createDeferred();
79
80
  const env = DataManager.getSettings('env');
80
81
  const isBleReconnect = connectId && DataManager.isBleConnect(env);
82
+ // Tracks the in-flight BLE probe so interval ticks never overlap: a superseded
83
+ // Initialize left pending on the V1 transport is exactly what later times out
84
+ // and used to tear down the connection mid-firmware-upload.
85
+ let bleProbeInFlight = false;
81
86
 
82
87
  Log.log('FirmwareUpdate [checkDeviceToBootloader] isBleReconnect: ', isBleReconnect);
83
88
 
@@ -85,6 +90,8 @@ export default class FirmwareUpdate extends BaseMethod<Params> {
85
90
  const intervalTimer: ReturnType<typeof setInterval> | undefined = setInterval(
86
91
  async () => {
87
92
  if (isBleReconnect) {
93
+ if (bleProbeInFlight) return;
94
+ bleProbeInFlight = true;
88
95
  try {
89
96
  await this.device.deviceConnector?.acquire(
90
97
  this.device.originalDescriptor.id,
@@ -92,7 +99,10 @@ export default class FirmwareUpdate extends BaseMethod<Params> {
92
99
  true,
93
100
  this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
94
101
  );
95
- await this.device.initialize();
102
+ // Bound each probe so a request the rebooting device never received
103
+ // frees the slot for the next tick instead of hanging into the
104
+ // 30s reboot budget.
105
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
96
106
  if (this.device.isBootloader()) {
97
107
  clearInterval(intervalTimer);
98
108
  this.checkPromise?.resolve(true);
@@ -100,6 +110,8 @@ export default class FirmwareUpdate extends BaseMethod<Params> {
100
110
  } catch (e) {
101
111
  // ignore error because of device is not connected
102
112
  Log.log('catch Bluetooth error when device is restarting: ', e);
113
+ } finally {
114
+ bleProbeInFlight = false;
103
115
  }
104
116
  } else {
105
117
  const deviceDiff = await this.device.deviceConnector?.enumerate();
@@ -14,18 +14,45 @@ import { BaseMethod } from './BaseMethod';
14
14
  import { validateParams } from './helpers/paramsValidator';
15
15
  import { DevicePool } from '../device/DevicePool';
16
16
  import { getBinary, getInfo, getSysResourceBinary } from './firmware/getBinary';
17
- import { updateResources, uploadFirmware } from './firmware/uploadFirmware';
18
- import { LoggerNames, getLogger, wait } from '../utils';
17
+ import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
18
+ import {
19
+ updateResources,
20
+ updateResourcesFromSources,
21
+ uploadFirmwareFromSource,
22
+ } from './firmware/uploadFirmware';
23
+ import { BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS } from './firmware/FirmwareUpdateBaseMethod';
24
+ import { LoggerNames, getDeviceType, getDeviceUUID, getLogger, wait } from '../utils';
25
+ import { resolveDeviceBootloaderMode } from '../utils/deviceFeaturesCompat';
19
26
  import { FirmwareUpdateTipMessage, createUiMessage } from '../events/ui-request';
20
27
  import { DeviceModelToTypes } from '../types';
21
28
  import { DataManager } from '../data-manager';
22
29
  import { DEVICE } from '../events';
30
+ import { type FirmwareByteSource, openFirmwareByteSource } from './firmware/FirmwareArtifactSource';
31
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
32
+ import {
33
+ assertFirmwareUpdatePreparedPlanBinding,
34
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
35
+ getFirmwareUpdateResourceName,
36
+ } from './firmware/FirmwareUpdatePreparedPlan';
23
37
 
24
38
  import type { Features, KnownDevice } from '../types';
25
39
  import type { FirmwareBinary } from './firmware/getBinary';
40
+ import type {
41
+ FirmwareArtifactReader,
42
+ FirmwareArtifactReference,
43
+ } from '../types/api/firmwareUpdate';
44
+ import type { FirmwareUpdatePreparedPlan } from '../types/api/firmwareUpdatePreparedPlan';
26
45
 
27
46
  type Params = {
28
47
  binary?: ArrayBuffer;
48
+ artifact?: FirmwareArtifactReference;
49
+ resourceEntries?: Array<{
50
+ entryName: string;
51
+ artifact: FirmwareArtifactReference;
52
+ }>;
53
+ artifactReader?: FirmwareArtifactReader;
54
+ preparedPlan?: FirmwareUpdatePreparedPlan;
55
+ platform?: FirmwareUpdatePreparedPlan['platform'];
29
56
  version?: number[];
30
57
  updateType: 'firmware' | 'ble';
31
58
  forcedUpdateRes?: boolean;
@@ -106,6 +133,13 @@ const normalizeFirmwareBinary = (binary: unknown): FirmwareBinary | undefined =>
106
133
  return normalized.buffer;
107
134
  };
108
135
 
136
+ const toArrayBuffer = (binary: FirmwareBinary): ArrayBuffer => {
137
+ if (binary instanceof ArrayBuffer) {
138
+ return binary;
139
+ }
140
+ return new Uint8Array(binary.buffer, binary.byteOffset, binary.byteLength).slice().buffer;
141
+ };
142
+
109
143
  export default class FirmwareUpdateV2 extends BaseMethod<Params> {
110
144
  checkPromise: Deferred<any> | null = null;
111
145
 
@@ -152,6 +186,32 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
152
186
  binary: payload.binary,
153
187
  };
154
188
  }
189
+
190
+ if ('artifact' in payload) {
191
+ const hostBinding = resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration);
192
+ const target = payload.isUpdateBootloader ? 'bootloader' : payload.updateType;
193
+ const resourceBindings = (payload.resourceEntries ?? []).map(
194
+ (entry: { entryName: string; artifact: FirmwareArtifactReference }) => ({
195
+ target: 'resource' as const,
196
+ entryName: entry.entryName,
197
+ artifact: entry.artifact,
198
+ })
199
+ );
200
+ assertFirmwareUpdatePreparedPlanBinding({
201
+ preparedPlan: payload.preparedPlan,
202
+ executor: 'v2',
203
+ platform: payload.platform,
204
+ scopeTargets: [target, ...(target === 'firmware' ? (['resource'] as const) : [])],
205
+ bindings: [{ target, artifact: payload.artifact }, ...resourceBindings],
206
+ });
207
+ this.params = {
208
+ ...this.params,
209
+ preparedPlan: payload.preparedPlan,
210
+ artifact: payload.artifact,
211
+ resourceEntries: payload.resourceEntries,
212
+ artifactReader: hostBinding.artifactReader,
213
+ };
214
+ }
155
215
  }
156
216
 
157
217
  postTipMessage = (message: string) => {
@@ -187,6 +247,10 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
187
247
  this.checkPromise = createDeferred();
188
248
  const env = DataManager.getSettings('env');
189
249
  const isBleReconnect = connectId && DataManager.isBleConnect(env);
250
+ // Tracks the in-flight BLE probe so interval ticks never overlap: a superseded
251
+ // Initialize left pending on the V1 transport is exactly what later times out
252
+ // and used to tear down the connection mid-firmware-upload.
253
+ let bleProbeInFlight = false;
190
254
 
191
255
  Log.log('FirmwareUpdateV2 [checkDeviceToBootloader] isBleReconnect: ', isBleReconnect);
192
256
 
@@ -234,6 +298,8 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
234
298
  }
235
299
 
236
300
  if (isBleReconnect) {
301
+ if (bleProbeInFlight) return;
302
+ bleProbeInFlight = true;
237
303
  try {
238
304
  await this.device.deviceConnector?.acquire(
239
305
  this.device.originalDescriptor.id,
@@ -241,7 +307,10 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
241
307
  true,
242
308
  this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
243
309
  );
244
- await this.device.initialize();
310
+ // Bound each probe so a request the rebooting device never received
311
+ // frees the slot for the next tick instead of hanging into the
312
+ // 30s reboot budget.
313
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
245
314
  if (this.device.isBootloader()) {
246
315
  clearInterval(intervalTimer);
247
316
  this.checkPromise?.resolve(true);
@@ -249,6 +318,8 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
249
318
  } catch (e) {
250
319
  // ignore error because of device is not connected
251
320
  Log.log('catch Bluetooth error when device is restarting: ', e);
321
+ } finally {
322
+ bleProbeInFlight = false;
252
323
  }
253
324
  } else {
254
325
  await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
@@ -344,151 +415,255 @@ export default class FirmwareUpdateV2 extends BaseMethod<Params> {
344
415
  const { features, commands } = device;
345
416
  const deviceType = device.getCurrentDeviceType();
346
417
 
418
+ // Protocol V2 (Pro2) uses DeviceFirmwareUpdate and must not enter this legacy flow.
419
+ if (device.isProtocolV2()) {
420
+ throw ERRORS.TypedError(
421
+ HardwareErrorCode.RuntimeError,
422
+ 'Protocol V2 firmware update must use firmwareUpdateV4'
423
+ );
424
+ }
425
+ if (params.preparedPlan) {
426
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
427
+ preparedPlan: params.preparedPlan,
428
+ deviceIdentity: getDeviceUUID(features) || undefined,
429
+ bootloaderMode: resolveDeviceBootloaderMode(features),
430
+ deviceModel: String(getDeviceType(features)),
431
+ });
432
+ }
433
+
347
434
  const deviceFirmwareType = device.getCurrentFirmwareType();
348
435
  const firmwareType = params.firmwareType ?? deviceFirmwareType;
349
436
 
350
437
  this.checkVersionForCopyTouchResource(features, firmwareType);
351
438
 
352
- let preparedBinary: FirmwareBinary | undefined;
353
- const acquireFirmwareBinary = async (): Promise<FirmwareBinary> => {
354
- try {
355
- if (preparedBinary) {
356
- return preparedBinary;
357
- }
358
-
359
- if (params.binary !== undefined) {
360
- preparedBinary = normalizeFirmwareBinary(params.binary);
361
- if (!preparedBinary) {
362
- throw new Error('firmware binary is empty or invalid');
439
+ let preparedSource: FirmwareByteSource | undefined;
440
+ try {
441
+ const acquireFirmwareSource = async (): Promise<FirmwareByteSource> => {
442
+ try {
443
+ if (preparedSource) {
444
+ return preparedSource;
363
445
  }
364
- return preparedBinary;
365
- }
366
446
 
367
- if (!device.features) {
368
- throw ERRORS.TypedError(
369
- HardwareErrorCode.RuntimeError,
370
- 'no features found for this device'
371
- );
372
- }
373
-
374
- this.postTipMessage('DownloadFirmware');
375
- const firmware = await getBinary({
376
- features: device.features,
377
- version: params.version,
378
- updateType: params.updateType,
379
- isUpdateBootloader: params.isUpdateBootloader,
380
- firmwareType,
381
- requestOptions: FIRMWARE_DOWNLOAD_REQUEST_OPTIONS,
382
- });
383
- preparedBinary = normalizeFirmwareBinary(firmware.binary);
384
- if (!preparedBinary) {
385
- throw new Error('downloaded firmware binary is empty or invalid');
386
- }
387
- this.postTipMessage('DownloadFirmwareSuccess');
388
- return preparedBinary;
389
- } catch (err) {
390
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
391
- }
392
- };
447
+ if (params.binary !== undefined) {
448
+ const binary = normalizeFirmwareBinary(params.binary);
449
+ if (!binary) {
450
+ throw new Error('firmware binary is empty or invalid');
451
+ }
452
+ const source = await openFirmwareByteSource({
453
+ binary: toArrayBuffer(binary),
454
+ });
455
+ if (!source) {
456
+ throw new Error('firmware binary is empty or invalid');
457
+ }
458
+ preparedSource = source;
459
+ return source;
460
+ }
393
461
 
394
- if (!device.isBootloader() && features) {
395
- const serialNo = device.getCurrentSerialNo();
396
- // should go to bootloader mode manually
397
- if (this.isEnteredManuallyBoot()) {
398
- return Promise.reject(ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateManuallyEnterBoot));
399
- }
462
+ if (params.artifact) {
463
+ const source = await openFirmwareByteSource({
464
+ artifact: params.artifact,
465
+ reader: params.artifactReader,
466
+ });
467
+ if (!source) {
468
+ throw new Error('firmware artifact is not prepared');
469
+ }
470
+ preparedSource = source;
471
+ return source;
472
+ }
400
473
 
401
- // check & upgrade firmware resource
402
- if (this.isSupportResourceUpdate(params.updateType)) {
403
- this.postTipMessage('CheckLatestUiResource');
404
- const resourceUrl = DataManager.getSysResourcesLatestRelease({
405
- features,
406
- forcedUpdateRes: params.forcedUpdateRes,
407
- firmwareType,
408
- });
409
- if (resourceUrl) {
410
- this.postTipMessage('DownloadLatestUiResource');
411
- const resource = await getSysResourceBinary(resourceUrl);
412
- this.postTipMessage('DownloadLatestUiResourceSuccess');
413
- if (resource) {
414
- await updateResources(
415
- this.device.getCommands().typedCall.bind(this.device.getCommands()),
416
- this.postMessage,
417
- device,
418
- resource.binary
474
+ if (!device.features) {
475
+ throw ERRORS.TypedError(
476
+ HardwareErrorCode.RuntimeError,
477
+ 'no features found for this device'
419
478
  );
420
479
  }
421
- }
422
- }
423
-
424
- // check if the device commands has been disposed
425
- this.device?.commands?.checkDisposed();
426
480
 
427
- // A failed firmware download must leave the device in normal mode.
428
- await acquireFirmwareBinary();
481
+ if (
482
+ params.artifactReader ||
483
+ DataManager.getSettings('firmwareManifestMode') === 'external-only'
484
+ ) {
485
+ throw ERRORS.TypedError(
486
+ HardwareErrorCode.RuntimeError,
487
+ 'Firmware must be prepared by the external firmware host',
488
+ {
489
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
490
+ artifactName: 'firmware',
491
+ }
492
+ );
493
+ }
429
494
 
430
- // The request may outlive the current transport command instance.
431
- this.device?.commands?.checkDisposed();
495
+ this.postTipMessage('DownloadFirmware');
496
+ const firmware = await getBinary({
497
+ features: device.features,
498
+ version: params.version,
499
+ updateType: params.updateType,
500
+ isUpdateBootloader: params.isUpdateBootloader,
501
+ firmwareType,
502
+ requestOptions: FIRMWARE_DOWNLOAD_REQUEST_OPTIONS,
503
+ });
504
+ const binary = normalizeFirmwareBinary(firmware.binary);
505
+ if (!binary) {
506
+ throw new Error('downloaded firmware binary is empty or invalid');
507
+ }
508
+ const source = await openFirmwareByteSource({
509
+ binary: toArrayBuffer(binary),
510
+ });
511
+ if (!source) {
512
+ throw new Error('downloaded firmware binary is empty or invalid');
513
+ }
514
+ preparedSource = source;
515
+ this.postTipMessage('DownloadFirmwareSuccess');
516
+ return source;
517
+ } catch (err) {
518
+ throw normalizeFirmwarePreparationError(err);
519
+ }
520
+ };
432
521
 
433
- // auto go to bootloader mode
434
- try {
435
- this.postTipMessage('AutoRebootToBootloader');
436
- const bootRes = await commands.typedCall('DeviceBackToBoot', 'Success');
437
- // @ts-expect-error
438
- if (bootRes.type === 'CallMethodError') {
439
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure);
522
+ if (!device.isBootloader() && features) {
523
+ const serialNo = device.getCurrentSerialNo();
524
+ // should go to bootloader mode manually
525
+ if (this.isEnteredManuallyBoot()) {
526
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateManuallyEnterBoot);
440
527
  }
441
- this.postTipMessage('GoToBootloaderSuccess');
442
- this.checkDeviceToBootloader(this.payload.connectId);
443
528
 
444
- // force clean classic device cache so that the device can initialize again
445
- if (DeviceModelToTypes.model_classic.includes(deviceType)) {
446
- DevicePool.clearDeviceCache(serialNo);
529
+ // All network-backed input must be ready before the first device mutation.
530
+ await acquireFirmwareSource();
531
+
532
+ // check & upgrade firmware resource
533
+ if (this.isSupportResourceUpdate(params.updateType)) {
534
+ this.postTipMessage('CheckLatestUiResource');
535
+ const resourceUrl = DataManager.getSysResourcesLatestRelease({
536
+ features,
537
+ forcedUpdateRes: params.forcedUpdateRes,
538
+ firmwareType,
539
+ });
540
+ if (resourceUrl) {
541
+ this.postTipMessage('DownloadLatestUiResource');
542
+ if (params.resourceEntries?.length) {
543
+ const sources: Array<{
544
+ entryName: string;
545
+ source: FirmwareByteSource;
546
+ }> = [];
547
+ try {
548
+ for (const entry of params.resourceEntries) {
549
+ const resourceName = getFirmwareUpdateResourceName(entry.entryName);
550
+ const source = await openFirmwareByteSource({
551
+ artifact: entry.artifact,
552
+ reader: params.artifactReader,
553
+ });
554
+ if (!source) {
555
+ throw new Error('Firmware resource entry is not prepared');
556
+ }
557
+ sources.push({ entryName: resourceName, source });
558
+ }
559
+ await updateResourcesFromSources(
560
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
561
+ this.postMessage,
562
+ device,
563
+ sources.map(entry => ({
564
+ entryName: entry.entryName,
565
+ source: entry.source,
566
+ }))
567
+ );
568
+ } finally {
569
+ await Promise.all(
570
+ sources.map(entry => entry.source.close().catch(() => undefined))
571
+ );
572
+ }
573
+ this.postTipMessage('DownloadLatestUiResourceSuccess');
574
+ } else {
575
+ if (
576
+ params.artifactReader ||
577
+ DataManager.getSettings('firmwareManifestMode') === 'external-only'
578
+ ) {
579
+ throw ERRORS.TypedError(
580
+ HardwareErrorCode.RuntimeError,
581
+ 'Firmware resource must be prepared by the external firmware host',
582
+ {
583
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
584
+ artifactName: 'resource',
585
+ }
586
+ );
587
+ }
588
+ const resourceBinary: ArrayBuffer | Buffer = (await getSysResourceBinary(resourceUrl))
589
+ .binary;
590
+ this.postTipMessage('DownloadLatestUiResourceSuccess');
591
+ await updateResources(
592
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
593
+ this.postMessage,
594
+ device,
595
+ resourceBinary
596
+ );
597
+ }
598
+ }
447
599
  }
448
- delete DevicePool.devicesCache[''];
449
- await this.checkPromise?.promise;
450
- this.checkPromise = null;
451
600
 
452
- // check if the device commands has been disposed
601
+ // The request may outlive the current transport command instance.
453
602
  this.device?.commands?.checkDisposed();
454
603
 
455
- /**
456
- * Touch 1 with bootloader v2.5.0 issue: BLE chip need more time for looking up name, here change the delay time to 3000ms after rebooting.
457
- */
458
- const isTouch = DeviceModelToTypes.model_touch.includes(deviceType);
459
- await wait(isTouch ? 3000 : 1500);
460
- } catch (e) {
461
- if (e instanceof HardwareError) {
462
- return Promise.reject(e);
604
+ // auto go to bootloader mode
605
+ try {
606
+ this.postTipMessage('AutoRebootToBootloader');
607
+ const bootRes = await commands.typedCall('DeviceBackToBoot', 'Success');
608
+ // @ts-expect-error
609
+ if (bootRes.type === 'CallMethodError') {
610
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure);
611
+ }
612
+ this.postTipMessage('GoToBootloaderSuccess');
613
+ this.checkDeviceToBootloader(this.payload.connectId);
614
+
615
+ // force clean classic device cache so that the device can initialize again
616
+ if (DeviceModelToTypes.model_classic.includes(deviceType)) {
617
+ DevicePool.clearDeviceCache(serialNo);
618
+ }
619
+ delete DevicePool.devicesCache[''];
620
+ await this.checkPromise?.promise;
621
+ this.checkPromise = null;
622
+
623
+ // check if the device commands has been disposed
624
+ this.device?.commands?.checkDisposed();
625
+
626
+ /**
627
+ * Touch 1 with bootloader v2.5.0 issue: BLE chip need more time for looking up name, here change the delay time to 3000ms after rebooting.
628
+ */
629
+ const isTouch = DeviceModelToTypes.model_touch.includes(deviceType);
630
+ await wait(isTouch ? 3000 : 1500);
631
+ } catch (e) {
632
+ if (e instanceof HardwareError) {
633
+ return Promise.reject(e);
634
+ }
635
+ console.log('auto go to bootloader mode failed: ', e);
636
+ return Promise.reject(
637
+ ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure)
638
+ );
463
639
  }
464
- console.log('auto go to bootloader mode failed: ', e);
465
- return Promise.reject(
466
- ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure)
467
- );
468
640
  }
469
- }
470
641
 
471
- // Devices already in bootloader mode still acquire through the same helper.
472
- const binary = await acquireFirmwareBinary();
642
+ // Devices already in bootloader mode still acquire through the same helper.
643
+ const source = await acquireFirmwareSource();
473
644
 
474
- // check if the device commands has been disposed
475
- this.device?.commands?.checkDisposed();
645
+ // check if the device commands has been disposed
646
+ this.device?.commands?.checkDisposed();
476
647
 
477
- await this.device.acquire();
648
+ await this.device.acquire();
478
649
 
479
- const response = await uploadFirmware(
480
- params.updateType,
481
- this.device.getCommands().typedCall.bind(this.device.getCommands()),
482
- this.postMessage,
483
- device,
484
- { payload: binary, rebootOnSuccess: true },
485
- params.isUpdateBootloader
486
- );
650
+ const response = await uploadFirmwareFromSource(
651
+ params.updateType,
652
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
653
+ this.postMessage,
654
+ device,
655
+ source,
656
+ true,
657
+ params.isUpdateBootloader
658
+ );
487
659
 
488
- if (this.connectId) {
489
- DevicePool.clearDeviceCache(this.connectId);
490
- }
660
+ if (this.connectId) {
661
+ DevicePool.clearDeviceCache(this.connectId);
662
+ }
491
663
 
492
- return response;
664
+ return response;
665
+ } finally {
666
+ await preparedSource?.close().catch(() => undefined);
667
+ }
493
668
  }
494
669
  }