@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.
- package/__tests__/connectSettings.test.ts +47 -5
- package/__tests__/device-initialize-timeout.test.ts +56 -0
- package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
- package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
- package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
- package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
- package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
- package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
- package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
- package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
- package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
- package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
- package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
- package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
- package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
- package/__tests__/protocol-v2.test.ts +371 -94
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts +10 -0
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts +7 -3
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +25 -4
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
- package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
- package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
- package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
- package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
- package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
- package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
- package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/api/firmware/getBinary.d.ts +7 -0
- package/dist/api/firmware/getBinary.d.ts.map +1 -1
- package/dist/api/firmware/updateBootloader.d.ts +2 -0
- package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts +9 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
- package/dist/data-manager/DataManager.d.ts +1 -0
- package/dist/data-manager/DataManager.d.ts.map +1 -1
- package/dist/data-manager/connectSettings.d.ts.map +1 -1
- package/dist/device/Device.d.ts +1 -0
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/index.d.ts +179 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3020 -955
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
- package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
- package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
- package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
- package/dist/types/api/export.d.ts +4 -1
- package/dist/types/api/export.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdate.d.ts +68 -0
- package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
- package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
- package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
- package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
- package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +9 -1
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.d.ts.map +1 -1
- package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
- package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +57 -6
- package/src/api/FirmwareUpdate.ts +13 -1
- package/src/api/FirmwareUpdateV2.ts +296 -121
- package/src/api/FirmwareUpdateV3.ts +263 -57
- package/src/api/FirmwareUpdateV4.ts +863 -243
- package/src/api/device/DeviceUpdateBootloader.ts +125 -6
- package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
- package/src/api/firmware/FirmwareHostBinding.ts +100 -0
- package/src/api/firmware/FirmwarePreparationError.ts +12 -0
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
- package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
- package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
- package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
- package/src/api/firmware/updateBootloader.ts +19 -4
- package/src/api/firmware/uploadFirmware.ts +140 -22
- package/src/data-manager/DataManager.ts +59 -54
- package/src/data-manager/connectSettings.ts +11 -0
- package/src/device/Device.ts +6 -1
- package/src/index.ts +5 -0
- package/src/inject.ts +22 -2
- package/src/lowLevelInject.ts +5 -1
- package/src/topLevelInject.ts +5 -1
- package/src/types/api/checkAllFirmwareRelease.ts +4 -0
- package/src/types/api/deviceUpdateBootloader.ts +6 -0
- package/src/types/api/export.ts +18 -0
- package/src/types/api/firmwareUpdate.ts +75 -0
- package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
- package/src/types/api/firmwareUpdatePlan.ts +38 -0
- package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
- package/src/types/api/index.ts +14 -0
- package/src/types/settings.ts +13 -0
- package/src/utils/deviceFeaturesCompat.ts +6 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
|
|
2
2
|
import {
|
|
3
3
|
DeviceRebootType,
|
|
4
|
+
PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
|
|
4
5
|
PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
|
|
5
6
|
PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
|
|
6
7
|
} from '@onekeyfe/hd-transport';
|
|
@@ -8,7 +9,6 @@ import { sha256 } from '@noble/hashes/sha256';
|
|
|
8
9
|
|
|
9
10
|
import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
|
|
10
11
|
import { validateProtocolV2FilesystemPath } from './helpers/filesystemValidation';
|
|
11
|
-
import { writeProtocolV2File } from './helpers/protocolV2FileWrite';
|
|
12
12
|
import { validateParams } from './helpers/paramsValidator';
|
|
13
13
|
import {
|
|
14
14
|
LoggerNames,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getLogger,
|
|
20
20
|
} from '../utils';
|
|
21
21
|
import { getSysResourceBinary } from './firmware/getBinary';
|
|
22
|
+
import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
|
|
22
23
|
import { DataManager } from '../data-manager';
|
|
23
24
|
import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
|
|
24
25
|
import { DevicePool } from '../device/DevicePool';
|
|
@@ -37,11 +38,20 @@ import {
|
|
|
37
38
|
getProtocolV2UnknownErrorText,
|
|
38
39
|
isProtocolV2DeviceDisconnectedError,
|
|
39
40
|
} from './protocol-v2/helpers';
|
|
41
|
+
import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
|
|
42
|
+
import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
|
|
43
|
+
import {
|
|
44
|
+
assertFirmwareUpdatePreparedPlanBinding,
|
|
45
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity,
|
|
46
|
+
} from './firmware/FirmwareUpdatePreparedPlan';
|
|
40
47
|
|
|
41
|
-
import type {
|
|
48
|
+
import type {
|
|
49
|
+
FirmwareArtifactReference,
|
|
50
|
+
FirmwareUpdateV4Params,
|
|
51
|
+
FirmwareUpdateV4Target,
|
|
52
|
+
} from '../types/api/firmwareUpdate';
|
|
42
53
|
import type { EFirmwareType } from '@onekeyfe/hd-shared';
|
|
43
54
|
import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
|
|
44
|
-
import type { PROTO } from '../constants';
|
|
45
55
|
import type { TypedResponseMessage } from '../device/DeviceCommands';
|
|
46
56
|
import type {
|
|
47
57
|
Features,
|
|
@@ -51,6 +61,7 @@ import type {
|
|
|
51
61
|
IProtocolV2ResourceFile,
|
|
52
62
|
IVersionArray,
|
|
53
63
|
} from '../types';
|
|
64
|
+
import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
|
|
54
65
|
|
|
55
66
|
const Log = getLogger(LoggerNames.Method);
|
|
56
67
|
|
|
@@ -73,6 +84,7 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
|
|
|
73
84
|
const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
|
|
74
85
|
const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
|
|
75
86
|
const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
|
|
87
|
+
const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
|
|
76
88
|
const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
|
|
77
89
|
const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
|
|
78
90
|
const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
|
|
@@ -95,11 +107,6 @@ const getProtocolV2DeviceTransferProgress = (
|
|
|
95
107
|
return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
|
|
96
108
|
};
|
|
97
109
|
|
|
98
|
-
const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
|
|
99
|
-
const safeElapsedMs = Math.max(elapsedMs, 1);
|
|
100
|
-
return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
|
|
101
|
-
};
|
|
102
|
-
|
|
103
110
|
type ProtocolV2FirmwareUpdateStatusTarget = {
|
|
104
111
|
target_id: number | string;
|
|
105
112
|
status?: number | string;
|
|
@@ -113,17 +120,6 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
|
|
|
113
120
|
type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
|
|
114
121
|
kind: ProtocolV2RemoteComponentTarget['kind'];
|
|
115
122
|
};
|
|
116
|
-
type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
|
|
117
|
-
path: string;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
|
|
121
|
-
filePath: string;
|
|
122
|
-
processedSize?: number;
|
|
123
|
-
totalSize?: number;
|
|
124
|
-
onTransferredBytes?: (transferredBytes: number) => void;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
123
|
/** Protocol V2 resource file written independently to devicePath through FileWrite. */
|
|
128
124
|
type ProtocolV2ResourceBundleBinary = {
|
|
129
125
|
name: string;
|
|
@@ -141,6 +137,35 @@ type ProtocolV2RemoteComponentTarget = {
|
|
|
141
137
|
kind: 'bootloader' | 'firmware';
|
|
142
138
|
};
|
|
143
139
|
|
|
140
|
+
type ProtocolV2InstallSource = {
|
|
141
|
+
fileName: string;
|
|
142
|
+
source: FirmwareByteSource;
|
|
143
|
+
targetId: number;
|
|
144
|
+
kind: ProtocolV2RemoteComponentTarget['kind'];
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type ProtocolV2ResourceBundleSource = {
|
|
148
|
+
name: string;
|
|
149
|
+
source: FirmwareByteSource;
|
|
150
|
+
devicePath: string;
|
|
151
|
+
version?: IVersionArray;
|
|
152
|
+
payloadHash?: string;
|
|
153
|
+
headerHash?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type ProtocolV2ExecutionPhaseKind =
|
|
157
|
+
| 'resource-sync'
|
|
158
|
+
| 'bootloader-install'
|
|
159
|
+
| 'bootloader-verify'
|
|
160
|
+
| 'component-install'
|
|
161
|
+
| 'final-verify';
|
|
162
|
+
|
|
163
|
+
type ProtocolV2ExecutionPhase = {
|
|
164
|
+
kind: ProtocolV2ExecutionPhaseKind;
|
|
165
|
+
installSources: ProtocolV2InstallSource[];
|
|
166
|
+
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
167
|
+
};
|
|
168
|
+
|
|
144
169
|
type ProtocolV2OkppHeader = {
|
|
145
170
|
type: string;
|
|
146
171
|
version: IVersionArray;
|
|
@@ -243,6 +268,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
|
|
|
243
268
|
);
|
|
244
269
|
};
|
|
245
270
|
|
|
271
|
+
const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
|
|
272
|
+
error instanceof HardwareError &&
|
|
273
|
+
(error.errorCode === HardwareErrorCode.FirmwareError ||
|
|
274
|
+
error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
|
|
275
|
+
error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
|
|
276
|
+
|
|
246
277
|
const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
|
|
247
278
|
normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
|
|
248
279
|
|
|
@@ -280,6 +311,18 @@ const normalizeProtocolV2TargetStatus = (
|
|
|
280
311
|
|
|
281
312
|
const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
|
|
282
313
|
|
|
314
|
+
const versionArrayToNumber = (version?: IVersionArray) => {
|
|
315
|
+
if (!version) return undefined;
|
|
316
|
+
return version[0] * 0x10000 + version[1] * 0x100 + version[2];
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
|
|
320
|
+
const currentNumber = versionArrayToNumber(current);
|
|
321
|
+
const targetNumber = versionArrayToNumber(target);
|
|
322
|
+
if (currentNumber === undefined || targetNumber === undefined) return undefined;
|
|
323
|
+
return currentNumber - targetNumber;
|
|
324
|
+
};
|
|
325
|
+
|
|
283
326
|
const bytesToHex = (bytes: Uint8Array) =>
|
|
284
327
|
Array.from(bytes)
|
|
285
328
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
|
@@ -413,6 +456,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
413
456
|
return ['V2'] as const;
|
|
414
457
|
}
|
|
415
458
|
|
|
459
|
+
private protocolV2PreparedSources: FirmwareByteSource[] = [];
|
|
460
|
+
|
|
461
|
+
private protocolV2ExecutionInLoader = false;
|
|
462
|
+
|
|
463
|
+
private protocolV2CompletedTargetVersions = new Map<number, number>();
|
|
464
|
+
|
|
465
|
+
private protocolV2LatestFinalFeatures?: Features;
|
|
466
|
+
|
|
467
|
+
private protocolV2FinalStatusVerified = false;
|
|
468
|
+
|
|
416
469
|
init() {
|
|
417
470
|
this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
|
|
418
471
|
this.requireDeviceMode = [];
|
|
@@ -450,10 +503,62 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
450
503
|
{ name: 'firmwareType', type: 'string' },
|
|
451
504
|
{ name: 'targetsToUpdate', type: 'array', allowEmpty: true },
|
|
452
505
|
{ name: 'platform', type: 'string' },
|
|
506
|
+
{ name: 'expectedDeviceId', type: 'string' },
|
|
453
507
|
{ name: 'resourceFiles', type: 'array', allowEmpty: true },
|
|
454
508
|
]);
|
|
509
|
+
if (
|
|
510
|
+
payload.expectedDeviceId !== undefined &&
|
|
511
|
+
(payload.expectedDeviceId.length === 0 || payload.expectedDeviceId.length > 160)
|
|
512
|
+
) {
|
|
513
|
+
throw ERRORS.TypedError(
|
|
514
|
+
HardwareErrorCode.CallMethodInvalidParameter,
|
|
515
|
+
'Protocol V2 expected device identity is invalid'
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const hostBinding =
|
|
519
|
+
payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
|
|
520
|
+
? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
|
|
521
|
+
: undefined;
|
|
522
|
+
if (hostBinding) {
|
|
523
|
+
assertFirmwareUpdatePreparedPlanBinding({
|
|
524
|
+
preparedPlan: payload.preparedPlan,
|
|
525
|
+
executor: 'v4',
|
|
526
|
+
platform: payload.platform,
|
|
527
|
+
scopeTargets: [
|
|
528
|
+
'boot',
|
|
529
|
+
'app_v1',
|
|
530
|
+
'app_v2',
|
|
531
|
+
'coprocessor',
|
|
532
|
+
'resource',
|
|
533
|
+
'se01',
|
|
534
|
+
'se02',
|
|
535
|
+
'se03',
|
|
536
|
+
'se04',
|
|
537
|
+
],
|
|
538
|
+
bindings: [
|
|
539
|
+
...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
|
|
540
|
+
artifact
|
|
541
|
+
? [
|
|
542
|
+
{
|
|
543
|
+
target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
|
|
544
|
+
artifact,
|
|
545
|
+
},
|
|
546
|
+
]
|
|
547
|
+
: []
|
|
548
|
+
),
|
|
549
|
+
...(payload.resourceBundleArtifacts ?? []).map(
|
|
550
|
+
(entry: { name: string; artifact: FirmwareArtifactReference }) => ({
|
|
551
|
+
target: 'resource' as const,
|
|
552
|
+
logicalName: entry.name,
|
|
553
|
+
artifact: entry.artifact,
|
|
554
|
+
})
|
|
555
|
+
),
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
}
|
|
455
559
|
|
|
456
560
|
this.params = {
|
|
561
|
+
preparedPlan: payload.preparedPlan,
|
|
457
562
|
chunkSize: payload.chunkSize,
|
|
458
563
|
forcedUpdateRes: payload.forcedUpdateRes,
|
|
459
564
|
bootloaderBinary: payload.bootloaderBinary,
|
|
@@ -468,17 +573,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
468
573
|
resourceFiles: payload.resourceFiles,
|
|
469
574
|
firmwareType: payload.firmwareType,
|
|
470
575
|
targetsToUpdate: payload.targetsToUpdate,
|
|
576
|
+
expectedTargetVersions: payload.expectedTargetVersions,
|
|
471
577
|
platform: payload.platform,
|
|
578
|
+
expectedDeviceId: payload.expectedDeviceId,
|
|
579
|
+
artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
|
|
580
|
+
componentArtifacts: payload.componentArtifacts,
|
|
581
|
+
resourceBundleArtifacts: payload.resourceBundleArtifacts,
|
|
472
582
|
};
|
|
473
583
|
}
|
|
474
584
|
|
|
475
|
-
private getProtocolV2FirmwareChunkSize() {
|
|
585
|
+
private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
|
|
476
586
|
const payloadChunkSize = Number(this.params?.chunkSize);
|
|
477
587
|
const env = DataManager.getSettings('env');
|
|
478
588
|
const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
|
|
479
589
|
let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
|
|
480
590
|
if (isBle) {
|
|
481
|
-
maxChunkSize =
|
|
591
|
+
maxChunkSize =
|
|
592
|
+
direction === 'read'
|
|
593
|
+
? PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE
|
|
594
|
+
: PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
|
|
482
595
|
}
|
|
483
596
|
if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
|
|
484
597
|
return maxChunkSize;
|
|
@@ -499,8 +612,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
499
612
|
const deviceFeatures = await this.getProtocolV2DeviceFeatures();
|
|
500
613
|
const deviceFirmwareType = getFirmwareType(deviceFeatures);
|
|
501
614
|
const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
|
|
615
|
+
this.validateExpectedTargetVersions();
|
|
616
|
+
|
|
617
|
+
if (
|
|
618
|
+
(this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
|
|
619
|
+
this.params.resourceBundleArtifacts?.length
|
|
620
|
+
) {
|
|
621
|
+
return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
|
|
622
|
+
}
|
|
502
623
|
const needsRemoteResources =
|
|
503
624
|
!this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
|
|
625
|
+
const needsRemoteBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
|
|
504
626
|
|
|
505
627
|
let fwBinaryMap: ProtocolV2TargetBinary[] = [];
|
|
506
628
|
let bootloaderBinary: ArrayBuffer | null = null;
|
|
@@ -512,7 +634,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
512
634
|
fwBinaryMap = this.collectExplicitTargetBinaries();
|
|
513
635
|
bootloaderBinary = this.prepareBootloaderBinary();
|
|
514
636
|
const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
|
|
515
|
-
|
|
637
|
+
if (
|
|
638
|
+
(needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
|
|
639
|
+
(this.params.artifactReader ||
|
|
640
|
+
DataManager.getSettings('firmwareManifestMode') === 'external-only')
|
|
641
|
+
) {
|
|
642
|
+
throw ERRORS.TypedError(
|
|
643
|
+
HardwareErrorCode.RuntimeError,
|
|
644
|
+
'Protocol V2 firmware artifacts must be prepared by the external firmware host',
|
|
645
|
+
{
|
|
646
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
}
|
|
516
650
|
if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
|
|
517
651
|
// Remote updates must use a freshly fetched config before any reboot or file write.
|
|
518
652
|
await DataManager.forceReloadData({
|
|
@@ -539,7 +673,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
539
673
|
if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
|
|
540
674
|
throw err;
|
|
541
675
|
}
|
|
542
|
-
throw
|
|
676
|
+
throw normalizeFirmwarePreparationError(err);
|
|
543
677
|
}
|
|
544
678
|
|
|
545
679
|
if (
|
|
@@ -555,9 +689,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
555
689
|
);
|
|
556
690
|
}
|
|
557
691
|
|
|
558
|
-
const enteredBootloader = await this.enterProtocolV2BootloaderMode();
|
|
559
|
-
|
|
560
692
|
if (needsRemoteResources) {
|
|
693
|
+
const enteredBootloader = await this.enterProtocolV2BootloaderMode();
|
|
561
694
|
try {
|
|
562
695
|
const stableResources = await this.prepareProtocolV2ResourceBundles();
|
|
563
696
|
resourceBundles = [...(resourceBundles ?? []), ...(stableResources ?? [])];
|
|
@@ -573,25 +706,293 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
573
706
|
);
|
|
574
707
|
}
|
|
575
708
|
}
|
|
576
|
-
throw
|
|
709
|
+
throw normalizeFirmwarePreparationError(err);
|
|
577
710
|
}
|
|
578
711
|
}
|
|
579
712
|
|
|
580
|
-
|
|
713
|
+
return this.executeProtocolV2Update({
|
|
581
714
|
fwBinaryMap,
|
|
582
715
|
bootloaderBinary,
|
|
583
716
|
...(installItems ? { installItems } : undefined),
|
|
584
717
|
...(resourceBundles?.length ? { resourceBundles } : undefined),
|
|
585
718
|
});
|
|
719
|
+
}
|
|
586
720
|
|
|
587
|
-
|
|
588
|
-
await
|
|
721
|
+
private async openProtocolV2PreparedSource(artifact: FirmwareArtifactReference) {
|
|
722
|
+
const source = await openFirmwareByteSource({
|
|
723
|
+
artifact,
|
|
724
|
+
reader: this.params.artifactReader,
|
|
725
|
+
});
|
|
726
|
+
if (!source) {
|
|
727
|
+
throw ERRORS.TypedError(
|
|
728
|
+
HardwareErrorCode.RuntimeError,
|
|
729
|
+
`Firmware artifact ${artifact.artifactRef} is not prepared`,
|
|
730
|
+
{
|
|
731
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
this.protocolV2PreparedSources.push(source);
|
|
736
|
+
return source;
|
|
737
|
+
}
|
|
589
738
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
739
|
+
private async openProtocolV2MemorySource(binary: ArrayBuffer) {
|
|
740
|
+
const source = await openFirmwareByteSource({ binary });
|
|
741
|
+
if (!source) {
|
|
742
|
+
throw ERRORS.TypedError(
|
|
743
|
+
HardwareErrorCode.RuntimeError,
|
|
744
|
+
'Protocol V2 firmware binary is empty'
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
this.protocolV2PreparedSources.push(source);
|
|
748
|
+
return source;
|
|
749
|
+
}
|
|
593
750
|
|
|
594
|
-
|
|
751
|
+
private async closeProtocolV2PreparedSources() {
|
|
752
|
+
const sources = this.protocolV2PreparedSources.splice(0);
|
|
753
|
+
await Promise.all(sources.map(source => source.close().catch(() => undefined)));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private async prepareProtocolV2InstallSources(
|
|
757
|
+
firmwareType: EFirmwareType,
|
|
758
|
+
features: Features
|
|
759
|
+
): Promise<ProtocolV2InstallSource[]> {
|
|
760
|
+
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
761
|
+
if (!release) {
|
|
762
|
+
throw ERRORS.TypedError(
|
|
763
|
+
HardwareErrorCode.RuntimeError,
|
|
764
|
+
'Protocol V2 firmware release is unavailable'
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
const componentArtifacts = this.params.componentArtifacts ?? {};
|
|
768
|
+
const requestedTargets = new Set(
|
|
769
|
+
this.params.targetsToUpdate ?? (Object.keys(componentArtifacts) as FirmwareUpdateV4Target[])
|
|
770
|
+
);
|
|
771
|
+
const installSources: ProtocolV2InstallSource[] = [];
|
|
772
|
+
const preparedTargets = new Set<FirmwareUpdateV4Target>();
|
|
773
|
+
|
|
774
|
+
for (const [key, component] of this.getRemoteComponentEntries(release)) {
|
|
775
|
+
const target = this.getRemoteComponentTarget(key, component);
|
|
776
|
+
const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
|
|
777
|
+
if (updateTarget && updateTarget !== 'resource' && requestedTargets.has(updateTarget)) {
|
|
778
|
+
const artifact = componentArtifacts[updateTarget];
|
|
779
|
+
if (!artifact) {
|
|
780
|
+
throw ERRORS.TypedError(
|
|
781
|
+
HardwareErrorCode.RuntimeError,
|
|
782
|
+
`Protocol V2 ${updateTarget} artifact is not prepared`,
|
|
783
|
+
{
|
|
784
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
785
|
+
artifactName: updateTarget,
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
installSources.push({
|
|
790
|
+
...target,
|
|
791
|
+
source: await this.openProtocolV2PreparedSource(artifact),
|
|
792
|
+
});
|
|
793
|
+
preparedTargets.add(updateTarget);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const unknownTarget = Array.from(requestedTargets).find(
|
|
798
|
+
target => target !== 'resource' && !preparedTargets.has(target)
|
|
799
|
+
);
|
|
800
|
+
if (unknownTarget) {
|
|
801
|
+
throw ERRORS.TypedError(
|
|
802
|
+
HardwareErrorCode.RuntimeError,
|
|
803
|
+
`Protocol V2 release does not contain requested target ${unknownTarget}`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return installSources;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private async prepareProtocolV2ResourceSources(
|
|
810
|
+
firmwareType: EFirmwareType,
|
|
811
|
+
features: Features
|
|
812
|
+
): Promise<ProtocolV2ResourceBundleSource[]> {
|
|
813
|
+
const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
|
|
814
|
+
const resourceRequested =
|
|
815
|
+
this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
|
|
816
|
+
if (!resourceRequested) {
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
|
|
820
|
+
const descriptors = release?.resourceBundles ?? [];
|
|
821
|
+
const artifactByName = new Map(
|
|
822
|
+
preparedArtifacts.map(item => [item.name, item.artifact] as const)
|
|
823
|
+
);
|
|
824
|
+
const sources: ProtocolV2ResourceBundleSource[] = [];
|
|
825
|
+
for (const descriptor of descriptors) {
|
|
826
|
+
const artifact = artifactByName.get(descriptor.name);
|
|
827
|
+
if (!artifact) {
|
|
828
|
+
throw ERRORS.TypedError(
|
|
829
|
+
HardwareErrorCode.RuntimeError,
|
|
830
|
+
`Protocol V2 resource bundle ${descriptor.name} is not prepared`,
|
|
831
|
+
{
|
|
832
|
+
firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
|
|
833
|
+
artifactName: descriptor.name,
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const devicePath = validateProtocolV2FilesystemPath(
|
|
838
|
+
descriptor.devicePath,
|
|
839
|
+
'resourceBundles[].devicePath'
|
|
840
|
+
);
|
|
841
|
+
sources.push({
|
|
842
|
+
name: descriptor.name,
|
|
843
|
+
source: await this.openProtocolV2PreparedSource(artifact),
|
|
844
|
+
devicePath,
|
|
845
|
+
version: descriptor.version,
|
|
846
|
+
payloadHash: descriptor.payloadHash,
|
|
847
|
+
headerHash: descriptor.headerHash,
|
|
848
|
+
});
|
|
849
|
+
artifactByName.delete(descriptor.name);
|
|
850
|
+
}
|
|
851
|
+
if (artifactByName.size > 0) {
|
|
852
|
+
throw ERRORS.TypedError(
|
|
853
|
+
HardwareErrorCode.RuntimeError,
|
|
854
|
+
`Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
return sources;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
|
|
861
|
+
try {
|
|
862
|
+
this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
|
|
863
|
+
const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
|
|
864
|
+
const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
|
|
865
|
+
if (installSources.length === 0 && resourceSources.length === 0) {
|
|
866
|
+
throw ERRORS.TypedError(
|
|
867
|
+
HardwareErrorCode.FirmwareUpdateDownloadFailed,
|
|
868
|
+
'No firmware to update'
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
|
|
872
|
+
|
|
873
|
+
return await this.executeProtocolV2SourceUpdate({
|
|
874
|
+
installSources,
|
|
875
|
+
resourceSources,
|
|
876
|
+
});
|
|
877
|
+
} finally {
|
|
878
|
+
await this.closeProtocolV2PreparedSources();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private validateExpectedTargetVersions() {
|
|
883
|
+
const expected = this.params.expectedTargetVersions;
|
|
884
|
+
if (expected === undefined) return;
|
|
885
|
+
if (typeof expected !== 'object' || expected === null || Array.isArray(expected)) {
|
|
886
|
+
throw ERRORS.TypedError(
|
|
887
|
+
HardwareErrorCode.RuntimeError,
|
|
888
|
+
'Protocol V2 expected target versions are invalid'
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
for (const [target, version] of Object.entries(expected)) {
|
|
892
|
+
if (
|
|
893
|
+
!Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
|
|
894
|
+
target as FirmwareUpdateV4Target
|
|
895
|
+
) ||
|
|
896
|
+
typeof version !== 'string' ||
|
|
897
|
+
!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
|
|
898
|
+
version.length > 64
|
|
899
|
+
) {
|
|
900
|
+
throw ERRORS.TypedError(
|
|
901
|
+
HardwareErrorCode.RuntimeError,
|
|
902
|
+
'Protocol V2 expected target version is invalid'
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private getExpectedProtocolV2TargetVersion(targetId: number) {
|
|
909
|
+
const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(targetId);
|
|
910
|
+
const version = target ? this.params?.expectedTargetVersions?.[target] : undefined;
|
|
911
|
+
if (!version) return undefined;
|
|
912
|
+
const parts = version.split(/[+-]/u, 1)[0].split('.').map(Number);
|
|
913
|
+
if (
|
|
914
|
+
parts.length !== 3 ||
|
|
915
|
+
parts.some(part => !Number.isSafeInteger(part) || part < 0 || part > 0xff)
|
|
916
|
+
) {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
return parts[0] * 0x10000 + parts[1] * 0x100 + parts[2];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private assertExpectedProtocolV2Versions(targets?: readonly FirmwareUpdateV4Target[]) {
|
|
923
|
+
const expected = this.params?.expectedTargetVersions;
|
|
924
|
+
if (!expected) return;
|
|
925
|
+
const features = this.protocolV2LatestFinalFeatures;
|
|
926
|
+
const requestedTargets = this.getProtocolV2RequestedTargets();
|
|
927
|
+
const applicationTargets = requestedTargets.filter(
|
|
928
|
+
target => target === 'app_v1' || target === 'app_v2'
|
|
929
|
+
);
|
|
930
|
+
const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
|
|
931
|
+
boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
|
|
932
|
+
coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
|
|
933
|
+
se01: features?.se01Version ?? undefined,
|
|
934
|
+
se02: features?.se02Version ?? undefined,
|
|
935
|
+
se03: features?.se03Version ?? undefined,
|
|
936
|
+
se04: features?.se04Version ?? undefined,
|
|
937
|
+
};
|
|
938
|
+
if (features && applicationTargets.length === 1) {
|
|
939
|
+
visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
|
|
940
|
+
}
|
|
941
|
+
const expectedEntries = (
|
|
942
|
+
Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
|
|
943
|
+
).filter(([target]) => !targets || targets.includes(target));
|
|
944
|
+
for (const [target, expectedVersion] of expectedEntries) {
|
|
945
|
+
const targetId = Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.entries()).find(
|
|
946
|
+
([, mappedTarget]) => mappedTarget === target
|
|
947
|
+
)?.[0];
|
|
948
|
+
const statusVersion =
|
|
949
|
+
targetId === undefined ? undefined : this.protocolV2CompletedTargetVersions.get(targetId);
|
|
950
|
+
const observedVersion =
|
|
951
|
+
statusVersion === undefined
|
|
952
|
+
? visibleVersions[target]
|
|
953
|
+
: `${Math.floor(statusVersion / 0x10000) % 0x100}.${
|
|
954
|
+
Math.floor(statusVersion / 0x100) % 0x100
|
|
955
|
+
}.${statusVersion % 0x100}`;
|
|
956
|
+
if (!observedVersion) {
|
|
957
|
+
if (this.protocolV2FinalStatusVerified) {
|
|
958
|
+
Log.warn(
|
|
959
|
+
`Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
|
|
960
|
+
);
|
|
961
|
+
} else {
|
|
962
|
+
throw ERRORS.TypedError(
|
|
963
|
+
HardwareErrorCode.FirmwareVerificationFailed,
|
|
964
|
+
`Protocol V2 target ${target} has no observable final version after status fallback`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
} else if (observedVersion !== expectedVersion) {
|
|
968
|
+
throw ERRORS.TypedError(
|
|
969
|
+
HardwareErrorCode.FirmwareVerificationFailed,
|
|
970
|
+
`Protocol V2 target ${target} reached ${observedVersion}, expected ${expectedVersion}`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
|
|
977
|
+
if (this.params.targetsToUpdate?.length) {
|
|
978
|
+
return [...new Set(this.params.targetsToUpdate)];
|
|
979
|
+
}
|
|
980
|
+
const targets = new Set<FirmwareUpdateV4Target>();
|
|
981
|
+
Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
|
|
982
|
+
targets.add(target as FirmwareUpdateV4Target)
|
|
983
|
+
);
|
|
984
|
+
if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
|
|
985
|
+
targets.add('resource');
|
|
986
|
+
}
|
|
987
|
+
if (this.params.bootloaderBinary) targets.add('boot');
|
|
988
|
+
if (this.params.applicationP1Binary) targets.add('app_v1');
|
|
989
|
+
if (this.params.applicationP2Binary) targets.add('app_v2');
|
|
990
|
+
if (this.params.coprocessorBinary) targets.add('coprocessor');
|
|
991
|
+
if (this.params.se01Binary) targets.add('se01');
|
|
992
|
+
if (this.params.se02Binary) targets.add('se02');
|
|
993
|
+
if (this.params.se03Binary) targets.add('se03');
|
|
994
|
+
if (this.params.se04Binary) targets.add('se04');
|
|
995
|
+
return Array.from(targets);
|
|
595
996
|
}
|
|
596
997
|
|
|
597
998
|
private async getProtocolV2DeviceFeatures(): Promise<Features> {
|
|
@@ -635,8 +1036,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
635
1036
|
private async captureProtocolV2PhysicalIdentity() {
|
|
636
1037
|
const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
|
|
637
1038
|
const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
|
|
638
|
-
const path = this.device.originalDescriptor
|
|
639
|
-
|
|
1039
|
+
const path = this.device.originalDescriptor?.path?.trim() || undefined;
|
|
1040
|
+
if (this.params?.preparedPlan) {
|
|
1041
|
+
assertFirmwareUpdatePreparedPlanDeviceIdentity({
|
|
1042
|
+
preparedPlan: this.params.preparedPlan,
|
|
1043
|
+
deviceIdentity: serialNumber,
|
|
1044
|
+
});
|
|
1045
|
+
} else if (this.params?.expectedDeviceId) {
|
|
1046
|
+
assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
|
|
1047
|
+
} else {
|
|
1048
|
+
assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
|
|
1049
|
+
}
|
|
640
1050
|
this.protocolV2ExpectedSerialNumber = serialNumber;
|
|
641
1051
|
this.protocolV2ExpectedPath = path;
|
|
642
1052
|
}
|
|
@@ -920,38 +1330,88 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
920
1330
|
};
|
|
921
1331
|
}
|
|
922
1332
|
|
|
923
|
-
private
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const transferTransport = this.getProtocolV2FirmwareTransferTransport();
|
|
1333
|
+
private getProtocolV2ResourceFilePath(path: string) {
|
|
1334
|
+
if (path.startsWith('vol')) return path;
|
|
1335
|
+
if (path.startsWith('/')) return `vol0:${path}`;
|
|
1336
|
+
return `vol0:/${path}`;
|
|
1337
|
+
}
|
|
929
1338
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1339
|
+
private async readProtocolV2DeviceFileHeader(path: string) {
|
|
1340
|
+
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
1341
|
+
const filePath = this.getProtocolV2ResourceFilePath(path);
|
|
1342
|
+
const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
|
|
1343
|
+
path: filePath,
|
|
1344
|
+
});
|
|
1345
|
+
const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
|
|
1346
|
+
if (
|
|
1347
|
+
!pathInfoRes.message?.exist ||
|
|
1348
|
+
pathInfoRes.message?.directory ||
|
|
1349
|
+
fileSize === undefined ||
|
|
1350
|
+
fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
|
|
1351
|
+
) {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
933
1354
|
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1355
|
+
const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
|
|
1356
|
+
const chunks: Uint8Array[] = [];
|
|
1357
|
+
let offset = 0;
|
|
1358
|
+
while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
|
|
1359
|
+
const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
|
|
1360
|
+
const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
|
|
1361
|
+
file: {
|
|
1362
|
+
path: filePath,
|
|
1363
|
+
offset,
|
|
1364
|
+
total_size: 0,
|
|
1365
|
+
},
|
|
1366
|
+
chunk_len: readLen,
|
|
1367
|
+
ui_percentage: undefined,
|
|
945
1368
|
});
|
|
1369
|
+
const data = toProtocolV2Bytes(res.message?.data);
|
|
1370
|
+
if (data.byteLength === 0) return null;
|
|
1371
|
+
chunks.push(data);
|
|
1372
|
+
offset += data.byteLength;
|
|
946
1373
|
}
|
|
947
1374
|
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
);
|
|
954
|
-
return
|
|
1375
|
+
const headerBytes = new Uint8Array(offset);
|
|
1376
|
+
let cursor = 0;
|
|
1377
|
+
chunks.forEach(chunk => {
|
|
1378
|
+
headerBytes.set(chunk, cursor);
|
|
1379
|
+
cursor += chunk.byteLength;
|
|
1380
|
+
});
|
|
1381
|
+
return parseProtocolV2OkppHeader(headerBytes);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** Compare a prepared okpkg header when its manifest supplies version or hash metadata. */
|
|
1385
|
+
private async isProtocolV2ResourceBundleUpToDate(
|
|
1386
|
+
bundle: Pick<
|
|
1387
|
+
ProtocolV2ResourceBundleSource,
|
|
1388
|
+
'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
|
|
1389
|
+
>
|
|
1390
|
+
): Promise<boolean> {
|
|
1391
|
+
if (this.params?.forcedUpdateRes) return false;
|
|
1392
|
+
if (!bundle.version && !bundle.payloadHash) return false;
|
|
1393
|
+
|
|
1394
|
+
try {
|
|
1395
|
+
const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
|
|
1396
|
+
if (!header) return false;
|
|
1397
|
+
|
|
1398
|
+
if (bundle.version) {
|
|
1399
|
+
const cmp = compareProtocolV2Versions(header.version, bundle.version);
|
|
1400
|
+
if (cmp === undefined || cmp !== 0) return false;
|
|
1401
|
+
}
|
|
1402
|
+
if (bundle.payloadHash) {
|
|
1403
|
+
const expected = normalizeProtocolV2Hex(bundle.payloadHash);
|
|
1404
|
+
if (expected && header.payloadHash !== expected) return false;
|
|
1405
|
+
}
|
|
1406
|
+
if (bundle.headerHash) {
|
|
1407
|
+
const expected = normalizeProtocolV2Hex(bundle.headerHash);
|
|
1408
|
+
if (expected && header.headerHash !== expected) return false;
|
|
1409
|
+
}
|
|
1410
|
+
return true;
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
955
1415
|
}
|
|
956
1416
|
|
|
957
1417
|
private isProtocolV2BootloaderMode() {
|
|
@@ -983,10 +1443,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
983
1443
|
// It rejects DeviceRebootType.Bootloader, so reuse the current connection.
|
|
984
1444
|
if (this.isProtocolV2RomloaderMode()) {
|
|
985
1445
|
Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
|
|
1446
|
+
this.protocolV2ExecutionInLoader = true;
|
|
986
1447
|
return false;
|
|
987
1448
|
}
|
|
988
1449
|
if (this.isProtocolV2BootloaderMode()) {
|
|
989
1450
|
Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
|
|
1451
|
+
this.protocolV2ExecutionInLoader = true;
|
|
990
1452
|
this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
|
|
991
1453
|
return false;
|
|
992
1454
|
}
|
|
@@ -996,6 +1458,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
996
1458
|
await this.protocolV2Reboot(DeviceRebootType.Bootloader);
|
|
997
1459
|
await wait(1000);
|
|
998
1460
|
await this.waitForProtocolV2BootloaderMode();
|
|
1461
|
+
this.protocolV2ExecutionInLoader = true;
|
|
999
1462
|
this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
|
|
1000
1463
|
return true;
|
|
1001
1464
|
} catch (error) {
|
|
@@ -1091,6 +1554,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1091
1554
|
return entries;
|
|
1092
1555
|
}
|
|
1093
1556
|
|
|
1557
|
+
private buildProtocolV2ExecutionPhases({
|
|
1558
|
+
installSources,
|
|
1559
|
+
resourceSources,
|
|
1560
|
+
}: {
|
|
1561
|
+
installSources: ProtocolV2InstallSource[];
|
|
1562
|
+
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1563
|
+
}): ProtocolV2ExecutionPhase[] {
|
|
1564
|
+
const phases: ProtocolV2ExecutionPhase[] = [];
|
|
1565
|
+
if (resourceSources.length > 0) {
|
|
1566
|
+
phases.push({
|
|
1567
|
+
kind: 'resource-sync',
|
|
1568
|
+
installSources: [],
|
|
1569
|
+
resourceSources,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
|
|
1573
|
+
if (bootloaderSources.length > 0) {
|
|
1574
|
+
phases.push(
|
|
1575
|
+
{
|
|
1576
|
+
kind: 'bootloader-install',
|
|
1577
|
+
installSources: bootloaderSources,
|
|
1578
|
+
resourceSources: [],
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
kind: 'bootloader-verify',
|
|
1582
|
+
installSources: [],
|
|
1583
|
+
resourceSources: [],
|
|
1584
|
+
}
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
const componentSources = installSources.filter(source => source.kind !== 'bootloader');
|
|
1588
|
+
if (componentSources.length > 0) {
|
|
1589
|
+
phases.push({
|
|
1590
|
+
kind: 'component-install',
|
|
1591
|
+
installSources: componentSources,
|
|
1592
|
+
resourceSources: [],
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
phases.push({
|
|
1596
|
+
kind: 'final-verify',
|
|
1597
|
+
installSources: [],
|
|
1598
|
+
resourceSources: [],
|
|
1599
|
+
});
|
|
1600
|
+
return phases;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
private async executeProtocolV2Phases({
|
|
1604
|
+
installSources,
|
|
1605
|
+
resourceSources,
|
|
1606
|
+
}: {
|
|
1607
|
+
installSources: ProtocolV2InstallSource[];
|
|
1608
|
+
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1609
|
+
}) {
|
|
1610
|
+
const phases = this.buildProtocolV2ExecutionPhases({
|
|
1611
|
+
installSources,
|
|
1612
|
+
resourceSources,
|
|
1613
|
+
});
|
|
1614
|
+
for (const phase of phases) {
|
|
1615
|
+
if (phase.kind === 'final-verify') {
|
|
1616
|
+
return this.completeProtocolV2FinalVerification();
|
|
1617
|
+
}
|
|
1618
|
+
if (phase.kind === 'resource-sync') {
|
|
1619
|
+
await this.enterProtocolV2BootloaderMode();
|
|
1620
|
+
await this.executeProtocolV2TransferPhase(phase);
|
|
1621
|
+
} else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
|
|
1622
|
+
await this.enterProtocolV2BootloaderMode();
|
|
1623
|
+
await this.executeProtocolV2TransferPhase(phase);
|
|
1624
|
+
await this.exitProtocolV2BootloaderToNormal();
|
|
1625
|
+
} else if (phase.kind === 'bootloader-verify') {
|
|
1626
|
+
await this.waitForProtocolV2FinalFeatures();
|
|
1627
|
+
this.assertExpectedProtocolV2Versions(['boot']);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
throw ERRORS.TypedError(
|
|
1631
|
+
HardwareErrorCode.RuntimeError,
|
|
1632
|
+
'Protocol V2 execution has no final verification phase'
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
private async executeProtocolV2SourceUpdate({
|
|
1637
|
+
installSources,
|
|
1638
|
+
resourceSources,
|
|
1639
|
+
}: {
|
|
1640
|
+
installSources: ProtocolV2InstallSource[];
|
|
1641
|
+
resourceSources: ProtocolV2ResourceBundleSource[];
|
|
1642
|
+
}) {
|
|
1643
|
+
return this.executeProtocolV2Phases({
|
|
1644
|
+
installSources,
|
|
1645
|
+
resourceSources,
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1094
1649
|
private async executeProtocolV2Update({
|
|
1095
1650
|
fwBinaryMap,
|
|
1096
1651
|
bootloaderBinary,
|
|
@@ -1102,104 +1657,168 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1102
1657
|
installItems?: ProtocolV2InstallItem[];
|
|
1103
1658
|
resourceBundles?: ProtocolV2ResourceBundleBinary[];
|
|
1104
1659
|
}) {
|
|
1105
|
-
const
|
|
1660
|
+
const memoryInstallItems =
|
|
1106
1661
|
installItems ??
|
|
1107
1662
|
this.buildProtocolV2InstallItems({
|
|
1108
|
-
bootloaderBinary: bootloaderBinary ?? null,
|
|
1109
1663
|
fwBinaryMap: fwBinaryMap ?? [],
|
|
1664
|
+
bootloaderBinary: bootloaderBinary ?? null,
|
|
1110
1665
|
});
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const
|
|
1121
|
-
resourceBundles
|
|
1122
|
-
|
|
1666
|
+
try {
|
|
1667
|
+
const installSources = await Promise.all(
|
|
1668
|
+
memoryInstallItems.map(async item => ({
|
|
1669
|
+
fileName: item.fileName,
|
|
1670
|
+
source: await this.openProtocolV2MemorySource(item.binary),
|
|
1671
|
+
targetId: item.targetId,
|
|
1672
|
+
kind: item.kind,
|
|
1673
|
+
}))
|
|
1674
|
+
);
|
|
1675
|
+
const resourceSources = await Promise.all(
|
|
1676
|
+
(resourceBundles ?? []).map(async bundle => ({
|
|
1677
|
+
name: bundle.name,
|
|
1678
|
+
source: await this.openProtocolV2MemorySource(bundle.binary),
|
|
1679
|
+
devicePath: bundle.devicePath,
|
|
1680
|
+
}))
|
|
1123
1681
|
);
|
|
1124
|
-
|
|
1125
|
-
|
|
1682
|
+
return await this.executeProtocolV2Phases({
|
|
1683
|
+
installSources,
|
|
1684
|
+
resourceSources,
|
|
1685
|
+
});
|
|
1686
|
+
} finally {
|
|
1687
|
+
await this.closeProtocolV2PreparedSources();
|
|
1126
1688
|
}
|
|
1689
|
+
}
|
|
1127
1690
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1691
|
+
private async executeProtocolV2TransferPhase({
|
|
1692
|
+
installSources,
|
|
1693
|
+
resourceSources,
|
|
1694
|
+
}: ProtocolV2ExecutionPhase) {
|
|
1695
|
+
let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
|
|
1696
|
+
const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
|
|
1697
|
+
for (const resource of resourceSources) {
|
|
1698
|
+
if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
|
|
1699
|
+
Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
|
|
1700
|
+
} else {
|
|
1701
|
+
resourcesToSync.push(resource);
|
|
1702
|
+
totalSize += resource.source.size;
|
|
1133
1703
|
}
|
|
1134
|
-
return;
|
|
1135
1704
|
}
|
|
1136
|
-
let transferredSize = processedSize;
|
|
1137
|
-
const transferStartTime = Date.now();
|
|
1138
|
-
const transferTransport = this.getProtocolV2FirmwareTransferTransport();
|
|
1139
|
-
const chunkSize = this.getProtocolV2FirmwareChunkSize();
|
|
1140
|
-
const onTransferredBytes = (bytes: number) => {
|
|
1141
|
-
transferredSize = bytes;
|
|
1142
|
-
};
|
|
1143
|
-
Log.log(
|
|
1144
|
-
`[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
|
|
1145
|
-
);
|
|
1146
|
-
|
|
1147
|
-
const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
|
|
1148
1705
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
processedSize
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
onTransferredBytes,
|
|
1161
|
-
});
|
|
1162
|
-
transferredSize = processedSize;
|
|
1163
|
-
await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
|
|
1164
|
-
|
|
1165
|
-
stagedInstallTargets.push({
|
|
1166
|
-
...item,
|
|
1167
|
-
path: filePath,
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1706
|
+
this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
|
|
1707
|
+
let processedSize = 0;
|
|
1708
|
+
for (const resource of resourcesToSync) {
|
|
1709
|
+
processedSize = await this.protocolV2SourceUpdateProcess({
|
|
1710
|
+
source: resource.source,
|
|
1711
|
+
filePath: resource.devicePath,
|
|
1712
|
+
processedSize,
|
|
1713
|
+
totalSize,
|
|
1714
|
+
});
|
|
1715
|
+
await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
|
|
1716
|
+
}
|
|
1170
1717
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1718
|
+
const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
|
|
1719
|
+
for (const item of installSources) {
|
|
1720
|
+
const filePath = this.getProtocolV2InstallItemStagingPath(item);
|
|
1721
|
+
processedSize = await this.protocolV2SourceUpdateProcess({
|
|
1722
|
+
source: item.source,
|
|
1723
|
+
filePath,
|
|
1724
|
+
processedSize,
|
|
1725
|
+
totalSize,
|
|
1726
|
+
});
|
|
1727
|
+
await this.verifyProtocolV2StagedFile(filePath, item.source.size);
|
|
1728
|
+
stagedInstallTargets.push({
|
|
1729
|
+
targetId: item.targetId,
|
|
1730
|
+
path: filePath,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1174
1733
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
);
|
|
1181
|
-
} catch (error) {
|
|
1182
|
-
const elapsedMs = Date.now() - transferStartTime;
|
|
1183
|
-
Log.warn(
|
|
1184
|
-
`[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
|
|
1185
|
-
elapsedMs / 1000
|
|
1186
|
-
).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
|
|
1187
|
-
);
|
|
1188
|
-
throw error;
|
|
1734
|
+
if (totalSize > 0) {
|
|
1735
|
+
this.postProgressMessage(100, 'transferData');
|
|
1736
|
+
}
|
|
1737
|
+
if (stagedInstallTargets.length === 0) {
|
|
1738
|
+
return;
|
|
1189
1739
|
}
|
|
1190
1740
|
|
|
1191
1741
|
this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
|
|
1192
|
-
|
|
1193
|
-
const allTargets = stagedInstallTargets.map(item => ({
|
|
1742
|
+
const targets = stagedInstallTargets.map(item => ({
|
|
1194
1743
|
target_id: item.targetId,
|
|
1195
1744
|
path: item.path,
|
|
1196
1745
|
}));
|
|
1197
|
-
|
|
1198
|
-
await this.
|
|
1199
|
-
|
|
1746
|
+
await this.protocolV2StartFirmwareUpdate({ targets });
|
|
1747
|
+
await this.waitForProtocolV2FirmwareUpdateComplete(targets);
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
private async protocolV2SourceUpdateProcess({
|
|
1751
|
+
source,
|
|
1752
|
+
filePath,
|
|
1753
|
+
processedSize,
|
|
1754
|
+
totalSize,
|
|
1755
|
+
}: {
|
|
1756
|
+
source: FirmwareByteSource;
|
|
1757
|
+
filePath: string;
|
|
1758
|
+
processedSize: number;
|
|
1759
|
+
totalSize: number;
|
|
1760
|
+
}) {
|
|
1761
|
+
let lastError: unknown;
|
|
1762
|
+
for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
|
|
1763
|
+
try {
|
|
1764
|
+
const transferStartedAt = Date.now();
|
|
1765
|
+
await writeFirmwareByteSource({
|
|
1766
|
+
source,
|
|
1767
|
+
chunkSize: this.getProtocolV2FirmwareChunkSize(),
|
|
1768
|
+
write: async ({ data, sourceOffset, length, first }) => {
|
|
1769
|
+
const chunkEnd = sourceOffset + length;
|
|
1770
|
+
const deviceProgress = getProtocolV2DeviceTransferProgress(
|
|
1771
|
+
processedSize + sourceOffset,
|
|
1772
|
+
processedSize + chunkEnd,
|
|
1773
|
+
totalSize
|
|
1774
|
+
);
|
|
1775
|
+
const response = await this.fileWriteChunk(
|
|
1776
|
+
filePath,
|
|
1777
|
+
source.size,
|
|
1778
|
+
sourceOffset,
|
|
1779
|
+
data,
|
|
1780
|
+
first,
|
|
1781
|
+
deviceProgress
|
|
1782
|
+
);
|
|
1783
|
+
const rawProcessedByte = response.message.processed_byte;
|
|
1784
|
+
const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
|
|
1785
|
+
if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
|
|
1786
|
+
throw ERRORS.TypedError(
|
|
1787
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
1788
|
+
`invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
const transferredBytes = processedSize + chunkEnd;
|
|
1792
|
+
const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
|
|
1793
|
+
this.postProgressMessage(
|
|
1794
|
+
Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
|
|
1795
|
+
'transferData',
|
|
1796
|
+
{
|
|
1797
|
+
transferredBytes,
|
|
1798
|
+
totalBytes: totalSize,
|
|
1799
|
+
rateBytesPerSecond:
|
|
1800
|
+
elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
|
|
1801
|
+
elapsedMs,
|
|
1802
|
+
}
|
|
1803
|
+
);
|
|
1804
|
+
return length;
|
|
1805
|
+
},
|
|
1806
|
+
});
|
|
1807
|
+
return processedSize + source.size;
|
|
1808
|
+
} catch (error) {
|
|
1809
|
+
lastError = error;
|
|
1810
|
+
if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
|
|
1811
|
+
await this.recoverProtocolV2FileTransfer();
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
throw ERRORS.TypedError(
|
|
1816
|
+
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
1817
|
+
`transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
|
|
1818
|
+
);
|
|
1200
1819
|
}
|
|
1201
1820
|
|
|
1202
|
-
private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
|
|
1821
|
+
private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
|
|
1203
1822
|
return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
|
|
1204
1823
|
}
|
|
1205
1824
|
|
|
@@ -1218,7 +1837,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1218
1837
|
|
|
1219
1838
|
private assertProtocolV2TargetStatus(
|
|
1220
1839
|
statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
|
|
1221
|
-
expectedTargetIds: Set<number
|
|
1840
|
+
expectedTargetIds: Set<number>,
|
|
1841
|
+
expectedPaths = new Map<number, string>()
|
|
1222
1842
|
) {
|
|
1223
1843
|
Log.log(
|
|
1224
1844
|
`[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
|
|
@@ -1249,17 +1869,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1249
1869
|
const matchingTargets = statusTargets.filter(target =>
|
|
1250
1870
|
expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
|
|
1251
1871
|
);
|
|
1872
|
+
const seenTargetIds = new Set<number>();
|
|
1873
|
+
for (const target of matchingTargets) {
|
|
1874
|
+
const targetId = normalizeProtocolV2TargetId(target.target_id);
|
|
1875
|
+
if (
|
|
1876
|
+
targetId === undefined ||
|
|
1877
|
+
seenTargetIds.has(targetId) ||
|
|
1878
|
+
(target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
|
|
1879
|
+
) {
|
|
1880
|
+
throw ERRORS.TypedError(
|
|
1881
|
+
HardwareErrorCode.RuntimeError,
|
|
1882
|
+
`Protocol V2 install status conflicts with target ${target.target_id}`,
|
|
1883
|
+
{
|
|
1884
|
+
firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
|
|
1885
|
+
}
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
seenTargetIds.add(targetId);
|
|
1889
|
+
}
|
|
1890
|
+
const completedTargets = matchingTargets.filter(target =>
|
|
1891
|
+
isProtocolV2TargetStatusFinished(target.status)
|
|
1892
|
+
);
|
|
1252
1893
|
const completedTargetIds = new Set<number>();
|
|
1253
|
-
|
|
1894
|
+
completedTargets.forEach(target => {
|
|
1254
1895
|
const targetId = normalizeProtocolV2TargetId(target.target_id);
|
|
1255
|
-
if (targetId !== undefined
|
|
1896
|
+
if (targetId !== undefined) {
|
|
1256
1897
|
completedTargetIds.add(targetId);
|
|
1257
1898
|
}
|
|
1258
1899
|
});
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1900
|
+
if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
|
|
1901
|
+
for (const target of completedTargets) {
|
|
1902
|
+
const targetId = normalizeProtocolV2TargetId(target.target_id);
|
|
1903
|
+
const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
|
|
1904
|
+
if (targetId === undefined) {
|
|
1905
|
+
throw ERRORS.TypedError(
|
|
1906
|
+
HardwareErrorCode.RuntimeError,
|
|
1907
|
+
'Protocol V2 completed target identity is invalid'
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
const expectedVersion = this.getExpectedProtocolV2TargetVersion(targetId);
|
|
1911
|
+
if (expectedVersion !== undefined && payloadVersion !== undefined) {
|
|
1912
|
+
if (payloadVersion !== expectedVersion) {
|
|
1913
|
+
throw ERRORS.TypedError(
|
|
1914
|
+
HardwareErrorCode.FirmwareVerificationFailed,
|
|
1915
|
+
`Protocol V2 target ${targetId} reached payload version ${payloadVersion}, expected ${expectedVersion}`
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (payloadVersion !== undefined) {
|
|
1920
|
+
this.protocolV2CompletedTargetVersions.set(targetId, payloadVersion);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1263
1923
|
this.postProgressMessage(100, 'installingFirmware');
|
|
1264
1924
|
return true;
|
|
1265
1925
|
}
|
|
@@ -1297,7 +1957,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1297
1957
|
private async waitForProtocolV2FirmwareUpdateComplete(
|
|
1298
1958
|
targets: Array<{ target_id: number; path: string }>
|
|
1299
1959
|
) {
|
|
1960
|
+
this.protocolV2FinalStatusVerified = false;
|
|
1300
1961
|
const expectedTargetIds = new Set(targets.map(target => target.target_id));
|
|
1962
|
+
const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
|
|
1301
1963
|
const startTime = Date.now();
|
|
1302
1964
|
let lastError: unknown;
|
|
1303
1965
|
let shouldReconnect = true;
|
|
@@ -1332,7 +1994,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1332
1994
|
);
|
|
1333
1995
|
const statusTargets = (statusResponse.message.records ??
|
|
1334
1996
|
[]) as ProtocolV2FirmwareUpdateStatusTarget[];
|
|
1335
|
-
if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
|
|
1997
|
+
if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
|
|
1998
|
+
this.protocolV2FinalStatusVerified = true;
|
|
1336
1999
|
return;
|
|
1337
2000
|
}
|
|
1338
2001
|
|
|
@@ -1380,10 +2043,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1380
2043
|
lastError = new Error('Protocol V2 firmware targets are still installing');
|
|
1381
2044
|
}
|
|
1382
2045
|
} catch (error) {
|
|
1383
|
-
if (
|
|
1384
|
-
error instanceof HardwareError &&
|
|
1385
|
-
error.errorCode === HardwareErrorCode.FirmwareError
|
|
1386
|
-
) {
|
|
2046
|
+
if (isProtocolV2TerminalInstallStatusError(error)) {
|
|
1387
2047
|
throw error;
|
|
1388
2048
|
}
|
|
1389
2049
|
// Only consecutive full status dumps can prove that a record is absent.
|
|
@@ -1423,7 +2083,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1423
2083
|
if (this.isProtocolV2ReconnectIdentityError(error)) {
|
|
1424
2084
|
throw error;
|
|
1425
2085
|
}
|
|
1426
|
-
if (error
|
|
2086
|
+
if (isProtocolV2TerminalInstallStatusError(error)) {
|
|
2087
|
+
throw error;
|
|
2088
|
+
}
|
|
2089
|
+
if (
|
|
2090
|
+
error instanceof HardwareError &&
|
|
2091
|
+
error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
|
|
2092
|
+
) {
|
|
1427
2093
|
throw error;
|
|
1428
2094
|
}
|
|
1429
2095
|
shouldReconnect = true;
|
|
@@ -1454,6 +2120,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1454
2120
|
Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
|
|
1455
2121
|
}
|
|
1456
2122
|
await this.protocolV2Reboot(DeviceRebootType.Normal);
|
|
2123
|
+
this.protocolV2ExecutionInLoader = false;
|
|
1457
2124
|
}
|
|
1458
2125
|
|
|
1459
2126
|
private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
|
|
@@ -1476,6 +2143,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1476
2143
|
const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
|
|
1477
2144
|
const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
|
|
1478
2145
|
const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
|
|
2146
|
+
this.protocolV2LatestFinalFeatures = features;
|
|
1479
2147
|
if (firmwareVersion === '0.0.0') {
|
|
1480
2148
|
Log.warn(
|
|
1481
2149
|
'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
|
|
@@ -1489,6 +2157,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1489
2157
|
};
|
|
1490
2158
|
}
|
|
1491
2159
|
|
|
2160
|
+
private async completeProtocolV2FinalVerification() {
|
|
2161
|
+
if (this.protocolV2ExecutionInLoader || this.isProtocolV2BootloaderMode()) {
|
|
2162
|
+
await this.exitProtocolV2BootloaderToNormal();
|
|
2163
|
+
}
|
|
2164
|
+
const versions = await this.waitForProtocolV2FinalFeatures();
|
|
2165
|
+
this.assertExpectedProtocolV2Versions();
|
|
2166
|
+
this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
|
|
2167
|
+
DevicePool.resetState();
|
|
2168
|
+
return versions;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1492
2171
|
private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
|
|
1493
2172
|
const startTime = Date.now();
|
|
1494
2173
|
let lastError: unknown;
|
|
@@ -1613,99 +2292,40 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
|
|
|
1613
2292
|
return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
|
|
1614
2293
|
}
|
|
1615
2294
|
|
|
1616
|
-
private async
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
throw ERRORS.TypedError(
|
|
1635
|
-
HardwareErrorCode.EmmcFileWriteFirmwareError,
|
|
1636
|
-
`transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
|
|
1637
|
-
);
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
private async protocolV2WriteWholeFile({
|
|
1641
|
-
payload,
|
|
1642
|
-
filePath,
|
|
1643
|
-
processedSize,
|
|
1644
|
-
totalSize,
|
|
1645
|
-
onTransferredBytes,
|
|
1646
|
-
}: ProtocolV2FileTransferParams) {
|
|
1647
|
-
const chunkSize = this.getProtocolV2FirmwareChunkSize();
|
|
1648
|
-
const getUploadProgress = (fileOffset: number) => {
|
|
1649
|
-
if (totalSize !== undefined && processedSize !== undefined) {
|
|
1650
|
-
return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
|
|
1651
|
-
}
|
|
1652
|
-
return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
|
|
1653
|
-
};
|
|
1654
|
-
|
|
1655
|
-
try {
|
|
1656
|
-
await writeProtocolV2File({
|
|
1657
|
-
commands: this.device.getCommands(),
|
|
1658
|
-
path: filePath,
|
|
1659
|
-
data: payload,
|
|
1660
|
-
totalSize: payload.byteLength,
|
|
1661
|
-
chunkSize,
|
|
1662
|
-
overwrite: true,
|
|
1663
|
-
append: false,
|
|
1664
|
-
writeWithResponse: true,
|
|
1665
|
-
maxChunkRetries: 0,
|
|
1666
|
-
getUiPercentage: ({ offset, chunkLength }) =>
|
|
1667
|
-
getProtocolV2DeviceTransferProgress(
|
|
1668
|
-
(processedSize ?? 0) + offset,
|
|
1669
|
-
(processedSize ?? 0) + offset + chunkLength,
|
|
1670
|
-
totalSize ?? payload.byteLength
|
|
1671
|
-
),
|
|
1672
|
-
onProgress: progress => {
|
|
1673
|
-
const transferredBytes = (processedSize ?? 0) + progress.transferredBytes;
|
|
1674
|
-
onTransferredBytes?.(transferredBytes);
|
|
1675
|
-
this.postProgressMessage(getUploadProgress(progress.transferredBytes), 'transferData', {
|
|
1676
|
-
transferredBytes,
|
|
1677
|
-
totalBytes: totalSize ?? payload.byteLength,
|
|
1678
|
-
rateBytesPerSecond: progress.rateBytesPerSecond,
|
|
1679
|
-
elapsedMs: progress.elapsedMs,
|
|
1680
|
-
});
|
|
2295
|
+
private async fileWriteChunk(
|
|
2296
|
+
filePath: string,
|
|
2297
|
+
totalFileSize: number,
|
|
2298
|
+
offset: number,
|
|
2299
|
+
chunk: ArrayBuffer | Buffer,
|
|
2300
|
+
overwrite: boolean,
|
|
2301
|
+
progress: number | null
|
|
2302
|
+
): Promise<TypedResponseMessage<'FilesystemFile'>> {
|
|
2303
|
+
const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
|
|
2304
|
+
const writeRes = await typedCall(
|
|
2305
|
+
'FilesystemFileWrite',
|
|
2306
|
+
'FilesystemFile',
|
|
2307
|
+
{
|
|
2308
|
+
file: {
|
|
2309
|
+
path: filePath,
|
|
2310
|
+
offset,
|
|
2311
|
+
total_size: totalFileSize,
|
|
2312
|
+
data: chunk,
|
|
1681
2313
|
},
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
2314
|
+
overwrite,
|
|
2315
|
+
append: false,
|
|
2316
|
+
ui_percentage: progress ?? undefined,
|
|
2317
|
+
},
|
|
2318
|
+
{ writeWithResponse: true }
|
|
2319
|
+
);
|
|
2320
|
+
if (writeRes.type !== 'FilesystemFile') {
|
|
2321
|
+
if ((writeRes as any).type === 'CallMethodError') {
|
|
2322
|
+
if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
|
|
2323
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
|
|
2324
|
+
}
|
|
1690
2325
|
}
|
|
1691
|
-
throw error;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
private getProtocolV2FirmwareTransferTransport() {
|
|
1698
|
-
const env = DataManager.getSettings('env');
|
|
1699
|
-
if (env && DataManager.isBleConnect(env)) {
|
|
1700
|
-
return 'BLE';
|
|
1701
|
-
}
|
|
1702
|
-
if (
|
|
1703
|
-
env &&
|
|
1704
|
-
(DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
|
|
1705
|
-
) {
|
|
1706
|
-
return 'WebUSB';
|
|
2326
|
+
throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
|
|
1707
2327
|
}
|
|
1708
|
-
return
|
|
2328
|
+
return writeRes;
|
|
1709
2329
|
}
|
|
1710
2330
|
|
|
1711
2331
|
private async recoverProtocolV2FileTransfer() {
|