@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.
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +66 -61
- package/__tests__/check-firmware-release-protocol-v2.test.ts +1 -1
- package/__tests__/firmware-memory-host.test.ts +18 -4
- package/__tests__/firmware-update/device-update-bootloader.test.ts +1 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +65 -2
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +61 -1
- package/__tests__/protocol-v2-firmware-targets.test.ts +16 -0
- package/__tests__/protocol-v2.test.ts +96 -0
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +3 -1
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
- package/dist/index.js +207 -54
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +6 -1
- package/src/api/FirmwareUpdateV2.ts +123 -65
- package/src/api/FirmwareUpdateV4.ts +54 -8
- package/src/api/firmware/FirmwareHostBinding.ts +10 -0
- package/src/api/firmware/FirmwareMemoryHost.ts +4 -3
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +122 -73
- package/src/api/firmware/FirmwareUpdatePlan.ts +18 -5
- 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
|
-
...(
|
|
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
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
}
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
(
|
|
146
|
+
Array.from(requestedTargets).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
|
|
145
147
|
);
|
|
146
|
-
if (params.se03Binary
|
|
147
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
+
entry.binary,
|
|
74
73
|
`${hostId}:entry:${artifactIndex}:${entryIndex}`
|
|
75
74
|
);
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
}
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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,
|