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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/__tests__/connectSettings.test.ts +47 -5
  2. package/__tests__/device-initialize-timeout.test.ts +56 -0
  3. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  4. package/__tests__/firmware-update/device-update-bootloader.test.ts +87 -0
  5. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  6. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  7. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  8. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  9. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  10. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +394 -0
  11. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  12. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +231 -0
  13. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  14. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +63 -0
  15. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  16. package/__tests__/protocol-v2.test.ts +371 -94
  17. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  18. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  19. package/dist/api/FirmwareUpdateV2.d.ts +10 -0
  20. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  21. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  22. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  23. package/dist/api/FirmwareUpdateV4.d.ts +25 -4
  24. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  25. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  26. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  27. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  28. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  29. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  30. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  31. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  32. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  33. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +8 -0
  34. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  35. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  36. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  37. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  38. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  39. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  40. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  41. package/dist/api/firmware/getBinary.d.ts +7 -0
  42. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  43. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  44. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  45. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  46. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  47. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  48. package/dist/data-manager/DataManager.d.ts +1 -0
  49. package/dist/data-manager/DataManager.d.ts.map +1 -1
  50. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  51. package/dist/device/Device.d.ts +1 -0
  52. package/dist/device/Device.d.ts.map +1 -1
  53. package/dist/index.d.ts +179 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +3020 -955
  56. package/dist/inject.d.ts.map +1 -1
  57. package/dist/lowLevelInject.d.ts.map +1 -1
  58. package/dist/topLevelInject.d.ts.map +1 -1
  59. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -0
  60. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  61. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  62. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  63. package/dist/types/api/export.d.ts +4 -1
  64. package/dist/types/api/export.d.ts.map +1 -1
  65. package/dist/types/api/firmwareUpdate.d.ts +68 -0
  66. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  67. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  68. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  69. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  70. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  71. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  72. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  73. package/dist/types/api/index.d.ts +9 -1
  74. package/dist/types/api/index.d.ts.map +1 -1
  75. package/dist/types/settings.d.ts +13 -0
  76. package/dist/types/settings.d.ts.map +1 -1
  77. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  78. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  79. package/package.json +4 -4
  80. package/src/api/CheckAllFirmwareRelease.ts +57 -6
  81. package/src/api/FirmwareUpdate.ts +13 -1
  82. package/src/api/FirmwareUpdateV2.ts +296 -121
  83. package/src/api/FirmwareUpdateV3.ts +263 -57
  84. package/src/api/FirmwareUpdateV4.ts +863 -243
  85. package/src/api/device/DeviceUpdateBootloader.ts +125 -6
  86. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  87. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  88. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  89. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +72 -1
  90. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  91. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  92. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +449 -0
  93. package/src/api/firmware/updateBootloader.ts +19 -4
  94. package/src/api/firmware/uploadFirmware.ts +140 -22
  95. package/src/data-manager/DataManager.ts +59 -54
  96. package/src/data-manager/connectSettings.ts +11 -0
  97. package/src/device/Device.ts +6 -1
  98. package/src/index.ts +5 -0
  99. package/src/inject.ts +22 -2
  100. package/src/lowLevelInject.ts +5 -1
  101. package/src/topLevelInject.ts +5 -1
  102. package/src/types/api/checkAllFirmwareRelease.ts +4 -0
  103. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  104. package/src/types/api/export.ts +18 -0
  105. package/src/types/api/firmwareUpdate.ts +75 -0
  106. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  107. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  108. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  109. package/src/types/api/index.ts +14 -0
  110. package/src/types/settings.ts +13 -0
  111. package/src/utils/deviceFeaturesCompat.ts +6 -0
@@ -5,15 +5,24 @@ import { UI_REQUEST } from '../../constants/ui-request';
5
5
  import { FirmwareUpdateTipMessage } from '../../events/ui-request';
6
6
  import { FirmwareUpdateBaseMethod } from '../firmware/FirmwareUpdateBaseMethod';
7
7
  import { getSysResourceBinary } from '../firmware/getBinary';
8
- import { updateBootloader } from '../firmware/uploadFirmware';
8
+ import { updateBootloader, updateBootloaderFromSource } from '../firmware/uploadFirmware';
9
9
  import { DeviceModelToTypes } from '../../types';
10
10
  import { DataManager } from '../../data-manager';
11
- import { checkBootloaderLength } from '../firmware/updateBootloader';
11
+ import { checkBootloaderLength, checkBootloaderSourceLength } from '../firmware/updateBootloader';
12
+ import { openFirmwareByteSource } from '../firmware/FirmwareArtifactSource';
13
+ import { resolveFirmwareUpdateHostBinding } from '../firmware/FirmwareHostBinding';
14
+ import {
15
+ assertFirmwareUpdatePreparedPlanBinding,
16
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
17
+ } from '../firmware/FirmwareUpdatePreparedPlan';
18
+ import { getDeviceType, getDeviceUUID } from '../../utils';
19
+ import { resolveDeviceBootloaderMode } from '../../utils/deviceFeaturesCompat';
12
20
 
13
21
  import type { DeviceUpdateBootloaderParams } from '../../types/api/deviceUpdateBootloader';
14
22
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
15
23
  import type { Device } from '../../device/Device';
16
24
  import type { Features } from '../../types';
25
+ import type { FirmwareByteSource } from '../firmware/FirmwareArtifactSource';
17
26
 
18
27
  export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any> {
19
28
  init() {
@@ -43,17 +52,68 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
43
52
  return true;
44
53
  }
45
54
 
55
+ async updateBootloaderWithEmmcFileWriteFromSource(_device: Device, source: FirmwareByteSource) {
56
+ const filePath = '0:boot/bootloader.bin';
57
+
58
+ this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
59
+ await this.emmcCommonUpdateProcessFromSource({
60
+ source,
61
+ filePath,
62
+ });
63
+ this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
64
+ await this.reboot(RebootType.Normal);
65
+ this.postTipMessage(FirmwareUpdateTipMessage.UpdateBootloaderSuccess);
66
+ return true;
67
+ }
68
+
46
69
  async updateTouchBootloader({
47
70
  device,
48
71
  features,
49
72
  firmwareType,
73
+ binary: preparedBinary,
74
+ source: preparedSource,
50
75
  }: {
51
76
  device: Device;
52
77
  features?: Features;
53
78
  firmwareType: EFirmwareType;
79
+ binary?: ArrayBuffer;
80
+ source?: FirmwareByteSource;
54
81
  }) {
55
- let { binary } = this.payload;
82
+ if (preparedSource) {
83
+ if (!(await checkBootloaderSourceLength(preparedSource))) {
84
+ throw ERRORS.TypedError(HardwareErrorCode.CheckDownloadFileError);
85
+ }
86
+ if (features && device.isBootloader()) {
87
+ this.postTipMessage(FirmwareUpdateTipMessage.UpdateBootloader);
88
+ const result = await this.updateBootloaderWithEmmcFileWriteFromSource(
89
+ device,
90
+ preparedSource
91
+ );
92
+ return result;
93
+ }
94
+ if (features && !device.isBootloader()) {
95
+ await updateBootloaderFromSource(
96
+ this.device.getCommands().typedCall.bind(this.device.getCommands()),
97
+ this.postMessage,
98
+ device,
99
+ preparedSource
100
+ );
101
+ return true;
102
+ }
103
+ }
104
+
105
+ let binary = preparedBinary;
56
106
  if (!binary) {
107
+ if (DataManager.getSettings('firmwareManifestMode') === 'external-only') {
108
+ throw ERRORS.TypedError(
109
+ HardwareErrorCode.RuntimeError,
110
+ 'Bootloader must be prepared by the external firmware host',
111
+ {
112
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
113
+ artifactName: 'bootloader',
114
+ }
115
+ );
116
+ }
57
117
  this.postTipMessage(FirmwareUpdateTipMessage.CheckLatestUiResource);
58
118
  const resourceUrl = features
59
119
  ? DataManager.getBootloaderResource(features, firmwareType)
@@ -68,7 +128,7 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
68
128
  }
69
129
  }
70
130
 
71
- if (!checkBootloaderLength(binary)) {
131
+ if (!binary || !checkBootloaderLength(binary)) {
72
132
  throw ERRORS.TypedError(HardwareErrorCode.CheckDownloadFileError);
73
133
  }
74
134
 
@@ -76,7 +136,8 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
76
136
  if (features && device.isBootloader()) {
77
137
  // Use emmcFileWrite + reboot logic for bootloader mode
78
138
  this.postTipMessage(FirmwareUpdateTipMessage.UpdateBootloader);
79
- return this.updateBootloaderWithEmmcFileWrite(device, binary);
139
+ const result = await this.updateBootloaderWithEmmcFileWrite(device, binary);
140
+ return result;
80
141
  }
81
142
 
82
143
  if (features && !device.isBootloader()) {
@@ -96,13 +157,71 @@ export default class DeviceUpdateBootloader extends FirmwareUpdateBaseMethod<any
96
157
  const { features } = device;
97
158
 
98
159
  const payload = this.payload as DeviceUpdateBootloaderParams;
160
+ const hostBinding = payload.artifact
161
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
162
+ : undefined;
163
+ const executionParams = {
164
+ ...payload,
165
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
166
+ };
167
+ if (payload.artifact) {
168
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
169
+ preparedPlan: payload.preparedPlan,
170
+ deviceIdentity: getDeviceUUID(features) || undefined,
171
+ bootloaderMode: resolveDeviceBootloaderMode(features),
172
+ deviceModel: String(getDeviceType(features)),
173
+ });
174
+ assertFirmwareUpdatePreparedPlanBinding({
175
+ preparedPlan: payload.preparedPlan,
176
+ executor: 'v2',
177
+ scopeTargets: ['bootloader'],
178
+ bindings: [
179
+ {
180
+ target: 'bootloader',
181
+ artifact: payload.artifact,
182
+ },
183
+ ],
184
+ });
185
+ const source = await openFirmwareByteSource({
186
+ artifact: payload.artifact,
187
+ reader: executionParams.artifactReader,
188
+ });
189
+ if (!source) {
190
+ throw ERRORS.TypedError(
191
+ HardwareErrorCode.RuntimeError,
192
+ 'Bootloader artifact is not prepared'
193
+ );
194
+ }
195
+ try {
196
+ const deviceType = device.getCurrentDeviceType();
197
+ const deviceFirmwareType = device.getCurrentFirmwareType();
198
+ const firmwareType = payload.firmwareType ?? deviceFirmwareType;
199
+ if (DeviceModelToTypes.model_touch.includes(deviceType)) {
200
+ return await this.updateTouchBootloader({
201
+ device,
202
+ features,
203
+ firmwareType,
204
+ source,
205
+ });
206
+ }
207
+ return true;
208
+ } finally {
209
+ await source.close().catch(() => undefined);
210
+ }
211
+ }
99
212
 
213
+ const { binary } = payload;
100
214
  const deviceType = device.getCurrentDeviceType();
101
215
  const deviceFirmwareType = device.getCurrentFirmwareType();
102
216
  const firmwareType = payload.firmwareType ?? deviceFirmwareType;
103
217
 
104
218
  if (DeviceModelToTypes.model_touch.includes(deviceType)) {
105
- return this.updateTouchBootloader({ device, features, firmwareType });
219
+ return this.updateTouchBootloader({
220
+ device,
221
+ features,
222
+ firmwareType,
223
+ binary,
224
+ });
106
225
  }
107
226
 
108
227
  return Promise.resolve(true);
@@ -0,0 +1,278 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import type {
4
+ FirmwareArtifactReader,
5
+ FirmwareArtifactReference,
6
+ } from '../../types/api/firmwareUpdate';
7
+
8
+ export const MAX_FIRMWARE_ARTIFACT_READ_BYTES = 256 * 1024;
9
+
10
+ export interface FirmwareByteSource {
11
+ readonly size: number;
12
+ readAt(offset: number, length: number): Promise<ArrayBuffer>;
13
+ close(): Promise<void>;
14
+ }
15
+
16
+ const artifactError = (
17
+ code:
18
+ | 'FirmwareArtifactReceiptMismatch'
19
+ | 'FirmwareArtifactReaderInvalid'
20
+ | 'FirmwareArtifactReadOutOfBounds'
21
+ | 'FirmwareArtifactReadTooLarge',
22
+ message: string
23
+ ): never => {
24
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, {
25
+ firmwareUpdateCode: code,
26
+ });
27
+ };
28
+
29
+ const assertSafeSize = (value: number, name: string) => {
30
+ if (!Number.isSafeInteger(value) || value <= 0) {
31
+ artifactError('FirmwareArtifactReceiptMismatch', `${name} must be a positive safe integer`);
32
+ }
33
+ };
34
+
35
+ export const assertFirmwareArtifactReference = (
36
+ artifact: FirmwareArtifactReference
37
+ ): FirmwareArtifactReference => {
38
+ if (
39
+ !artifact ||
40
+ typeof artifact.artifactRef !== 'string' ||
41
+ artifact.artifactRef.length === 0 ||
42
+ artifact.artifactRef.length > 160 ||
43
+ artifact.artifactRef.includes('/') ||
44
+ artifact.artifactRef.includes('\\')
45
+ ) {
46
+ artifactError(
47
+ 'FirmwareArtifactReceiptMismatch',
48
+ 'Firmware artifactRef must be an opaque identifier'
49
+ );
50
+ }
51
+ assertSafeSize(artifact.size, 'Firmware artifact size');
52
+ if (typeof artifact.sha256 !== 'string' || !/^[a-f0-9]{64}$/i.test(artifact.sha256)) {
53
+ artifactError(
54
+ 'FirmwareArtifactReceiptMismatch',
55
+ 'Firmware artifact sha256 must be a 32-byte hexadecimal digest'
56
+ );
57
+ }
58
+ return artifact;
59
+ };
60
+
61
+ const assertFirmwareArtifactRead = (size: number, offset: number, length: number) => {
62
+ if (
63
+ !Number.isSafeInteger(offset) ||
64
+ offset < 0 ||
65
+ offset >= size ||
66
+ !Number.isSafeInteger(length) ||
67
+ length <= 0 ||
68
+ offset + length > size
69
+ ) {
70
+ artifactError(
71
+ 'FirmwareArtifactReadOutOfBounds',
72
+ `Firmware artifact read is outside size ${size}: offset=${offset}, length=${length}`
73
+ );
74
+ }
75
+ if (length > MAX_FIRMWARE_ARTIFACT_READ_BYTES) {
76
+ artifactError(
77
+ 'FirmwareArtifactReadTooLarge',
78
+ `Firmware artifact read length ${length} exceeds ${MAX_FIRMWARE_ARTIFACT_READ_BYTES}`
79
+ );
80
+ }
81
+ };
82
+
83
+ const createMemoryFirmwareByteSource = (binary: ArrayBuffer): FirmwareByteSource => {
84
+ let bytes: Uint8Array | null = new Uint8Array(binary);
85
+ const { byteLength: size } = binary;
86
+ assertSafeSize(size, 'Firmware binary size');
87
+ return {
88
+ size,
89
+ readAt: (offset, length) => {
90
+ if (!bytes) {
91
+ return Promise.reject(
92
+ ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Firmware byte source is closed', {
93
+ firmwareUpdateCode: 'FirmwareArtifactReaderInvalid',
94
+ })
95
+ );
96
+ }
97
+ assertFirmwareArtifactRead(size, offset, length);
98
+ return Promise.resolve(bytes.slice(offset, offset + length).buffer);
99
+ },
100
+ close: () => {
101
+ bytes = null;
102
+ return Promise.resolve();
103
+ },
104
+ };
105
+ };
106
+
107
+ class ReaderFirmwareByteSource implements FirmwareByteSource {
108
+ readonly size: number;
109
+
110
+ private closed = false;
111
+
112
+ private reading = false;
113
+
114
+ private constructor(
115
+ private readonly reader: FirmwareArtifactReader,
116
+ private readonly readerId: string,
117
+ size: number
118
+ ) {
119
+ this.size = size;
120
+ }
121
+
122
+ static async open(
123
+ artifact: FirmwareArtifactReference,
124
+ reader: FirmwareArtifactReader
125
+ ): Promise<ReaderFirmwareByteSource> {
126
+ const opened = await reader.open({ artifactRef: artifact.artifactRef });
127
+ if (
128
+ !opened ||
129
+ typeof opened.readerId !== 'string' ||
130
+ opened.readerId.length === 0 ||
131
+ !Number.isSafeInteger(opened.size) ||
132
+ opened.size !== artifact.size
133
+ ) {
134
+ if (opened?.readerId) {
135
+ await reader.close({ readerId: opened.readerId }).catch(() => undefined);
136
+ }
137
+ return artifactError(
138
+ 'FirmwareArtifactReceiptMismatch',
139
+ `Firmware artifact opened with size ${opened?.size ?? 'unknown'}, expected ${artifact.size}`
140
+ );
141
+ }
142
+ return new ReaderFirmwareByteSource(reader, opened.readerId, opened.size);
143
+ }
144
+
145
+ async readAt(offset: number, length: number): Promise<ArrayBuffer> {
146
+ if (this.closed) {
147
+ return artifactError('FirmwareArtifactReaderInvalid', 'Firmware artifact reader is closed');
148
+ }
149
+ if (this.reading) {
150
+ return artifactError(
151
+ 'FirmwareArtifactReaderInvalid',
152
+ 'Firmware artifact reader only permits one in-flight read'
153
+ );
154
+ }
155
+ assertFirmwareArtifactRead(this.size, offset, length);
156
+ this.reading = true;
157
+ try {
158
+ const result = await this.reader.read({
159
+ readerId: this.readerId,
160
+ offset,
161
+ length,
162
+ });
163
+ const expectedEof = offset + length === this.size;
164
+ if (
165
+ !result ||
166
+ !(result.data instanceof ArrayBuffer) ||
167
+ result.data.byteLength !== length ||
168
+ result.bytesRead !== length ||
169
+ result.eof !== expectedEof
170
+ ) {
171
+ return artifactError(
172
+ 'FirmwareArtifactReaderInvalid',
173
+ 'Firmware artifact reader returned an invalid read result'
174
+ );
175
+ }
176
+ return result.data;
177
+ } finally {
178
+ this.reading = false;
179
+ }
180
+ }
181
+
182
+ async close(): Promise<void> {
183
+ if (this.closed) {
184
+ return;
185
+ }
186
+ this.closed = true;
187
+ await this.reader.close({ readerId: this.readerId });
188
+ }
189
+ }
190
+
191
+ export const openFirmwareByteSource = async ({
192
+ binary,
193
+ artifact,
194
+ reader,
195
+ }: {
196
+ binary?: ArrayBuffer;
197
+ artifact?: FirmwareArtifactReference;
198
+ reader?: FirmwareArtifactReader;
199
+ }): Promise<FirmwareByteSource | null> => {
200
+ if (binary && artifact) {
201
+ return artifactError(
202
+ 'FirmwareArtifactReceiptMismatch',
203
+ 'Firmware input must not contain both binary and artifactRef'
204
+ );
205
+ }
206
+ if (binary) {
207
+ return createMemoryFirmwareByteSource(binary);
208
+ }
209
+ if (!artifact) {
210
+ return null;
211
+ }
212
+ assertFirmwareArtifactReference(artifact);
213
+ if (!reader) {
214
+ return artifactError(
215
+ 'FirmwareArtifactReaderInvalid',
216
+ 'Firmware artifact reader is required for artifactRef input'
217
+ );
218
+ }
219
+ return ReaderFirmwareByteSource.open(artifact, reader);
220
+ };
221
+
222
+ export const readFirmwareByteSourceFully = async (
223
+ source: FirmwareByteSource
224
+ ): Promise<ArrayBuffer> => {
225
+ const output = new Uint8Array(source.size);
226
+ let offset = 0;
227
+ while (offset < source.size) {
228
+ const length = Math.min(MAX_FIRMWARE_ARTIFACT_READ_BYTES, source.size - offset);
229
+ output.set(new Uint8Array(await source.readAt(offset, length)), offset);
230
+ offset += length;
231
+ }
232
+ return output.buffer;
233
+ };
234
+
235
+ export const writeFirmwareByteSource = async ({
236
+ source,
237
+ chunkSize,
238
+ write,
239
+ }: {
240
+ source: FirmwareByteSource;
241
+ chunkSize: number;
242
+ write: (input: {
243
+ data: ArrayBuffer;
244
+ sourceOffset: number;
245
+ length: number;
246
+ first: boolean;
247
+ }) => Promise<number>;
248
+ }) => {
249
+ if (
250
+ !Number.isSafeInteger(chunkSize) ||
251
+ chunkSize <= 0 ||
252
+ chunkSize > MAX_FIRMWARE_ARTIFACT_READ_BYTES
253
+ ) {
254
+ return artifactError(
255
+ 'FirmwareArtifactReadTooLarge',
256
+ `Firmware artifact chunk size must be between 1 and ${MAX_FIRMWARE_ARTIFACT_READ_BYTES}`
257
+ );
258
+ }
259
+ let offset = 0;
260
+ while (offset < source.size) {
261
+ const length = Math.min(chunkSize, source.size - offset);
262
+ const data = await source.readAt(offset, length);
263
+ const processed = await write({
264
+ data,
265
+ sourceOffset: offset,
266
+ length,
267
+ first: offset === 0,
268
+ });
269
+ if (processed !== length) {
270
+ return artifactError(
271
+ 'FirmwareArtifactReaderInvalid',
272
+ `Firmware artifact writer processed ${processed} bytes, expected ${length}`
273
+ );
274
+ }
275
+ offset += length;
276
+ }
277
+ return offset;
278
+ };
@@ -0,0 +1,100 @@
1
+ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ import type { FirmwareUpdateHostBinding } from '../../types/api/firmwareUpdate';
4
+
5
+ const bindingError = (message: string): never => {
6
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, message, {
7
+ firmwareUpdateCode: 'FirmwareArtifactReaderInvalid',
8
+ });
9
+ };
10
+
11
+ class FirmwareHostBindingRegistry {
12
+ private generation = 0;
13
+
14
+ private binding?: FirmwareUpdateHostBinding;
15
+
16
+ register(binding: FirmwareUpdateHostBinding): number {
17
+ if (
18
+ !binding ||
19
+ typeof binding.artifactReader?.open !== 'function' ||
20
+ typeof binding.artifactReader.read !== 'function' ||
21
+ typeof binding.artifactReader.close !== 'function'
22
+ ) {
23
+ return bindingError('Firmware host binding is invalid');
24
+ }
25
+ this.generation += 1;
26
+ this.binding = binding;
27
+ return this.generation;
28
+ }
29
+
30
+ unregister(generation?: number): boolean {
31
+ if (generation !== undefined && generation !== this.generation) {
32
+ return false;
33
+ }
34
+ this.generation += 1;
35
+ this.binding = undefined;
36
+ return true;
37
+ }
38
+
39
+ getGeneration(): number {
40
+ return this.generation;
41
+ }
42
+
43
+ resolve(generation: number): FirmwareUpdateHostBinding {
44
+ if (
45
+ !Number.isSafeInteger(generation) ||
46
+ generation <= 0 ||
47
+ generation !== this.generation ||
48
+ !this.binding
49
+ ) {
50
+ return bindingError(`Firmware host binding generation ${generation} is stale`);
51
+ }
52
+ const { binding } = this;
53
+ const assertCurrent = () => {
54
+ if (generation !== this.generation || binding !== this.binding) {
55
+ bindingError(`Firmware host binding generation ${generation} is stale`);
56
+ }
57
+ };
58
+ return {
59
+ artifactReader: {
60
+ open: async input => {
61
+ assertCurrent();
62
+ const opened = await binding.artifactReader.open(input);
63
+ try {
64
+ assertCurrent();
65
+ } catch (error) {
66
+ if (opened?.readerId) {
67
+ await binding.artifactReader
68
+ .close({ readerId: opened.readerId })
69
+ .catch(() => undefined);
70
+ }
71
+ throw error;
72
+ }
73
+ return opened;
74
+ },
75
+ read: async input => {
76
+ assertCurrent();
77
+ const result = await binding.artifactReader.read(input);
78
+ assertCurrent();
79
+ return result;
80
+ },
81
+ close: input => binding.artifactReader.close(input),
82
+ },
83
+ };
84
+ }
85
+ }
86
+
87
+ const firmwareHostBindingRegistry = new FirmwareHostBindingRegistry();
88
+
89
+ export const registerFirmwareUpdateHostBinding = (binding: FirmwareUpdateHostBinding): number =>
90
+ firmwareHostBindingRegistry.register(binding);
91
+
92
+ export const unregisterFirmwareUpdateHostBinding = (generation?: number): boolean =>
93
+ firmwareHostBindingRegistry.unregister(generation);
94
+
95
+ export const getFirmwareUpdateHostBindingGeneration = (): number =>
96
+ firmwareHostBindingRegistry.getGeneration();
97
+
98
+ export const resolveFirmwareUpdateHostBinding = (
99
+ generation: number | undefined
100
+ ): FirmwareUpdateHostBinding => firmwareHostBindingRegistry.resolve(generation ?? Number.NaN);
@@ -0,0 +1,12 @@
1
+ import { ERRORS, HardwareError, HardwareErrorCode } from '@onekeyfe/hd-shared';
2
+
3
+ export const normalizeFirmwarePreparationError = (error: unknown) => {
4
+ if (error instanceof HardwareError && error.params?.firmwareUpdateCode) {
5
+ return error;
6
+ }
7
+
8
+ return ERRORS.TypedError(
9
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
10
+ error instanceof Error ? error.message : String(error)
11
+ );
12
+ };
@@ -13,6 +13,7 @@ import { DeviceModelToTypes } from '../../types';
13
13
  import { DataManager } from '../../data-manager';
14
14
  import { BaseMethod } from '../BaseMethod';
15
15
  import { DEVICE } from '../../events';
16
+ import { writeFirmwareByteSource } from './FirmwareArtifactSource';
16
17
 
17
18
  import type {
18
19
  FirmwareProgress,
@@ -24,11 +25,22 @@ import type { RebootType } from '@onekeyfe/hd-transport';
24
25
  import type { Deferred } from '@onekeyfe/hd-shared';
25
26
  import type { KnownDevice } from '../../types';
26
27
  import type { TypedResponseMessage } from '../../device/DeviceCommands';
28
+ import type { FirmwareByteSource } from './FirmwareArtifactSource';
27
29
 
28
30
  const Log = getLogger(LoggerNames.Method);
29
31
  const SESSION_ERROR = 'session not found';
30
32
  const FIRMWARE_UPDATE_CONFIRM = 'Firmware install confirmed';
31
33
 
34
+ /**
35
+ * Response timeout for each Initialize probe while polling a rebooting device over BLE.
36
+ * Overlap safety comes from the poll's in-flight guard, not from this value. Keep it
37
+ * well below the 30s reboot budget so several attempts fit: a probe written into the
38
+ * dying pre-reboot connection is never answered, and its timeout tears that stale link
39
+ * down so the next tick reconnects fresh. Raise it only if a device model is proven to
40
+ * answer bootloader Initialize slower than this on a fresh connection.
41
+ */
42
+ export const BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS = 5000;
43
+
32
44
  type FirmwareTransferMetrics = Pick<
33
45
  FirmwareProgress['payload'],
34
46
  'transferredBytes' | 'totalBytes' | 'rateBytesPerSecond' | 'elapsedMs'
@@ -143,6 +155,10 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
143
155
  this.checkPromise = createDeferred();
144
156
  const env = DataManager.getSettings('env');
145
157
  const isBleReconnect = connectId && DataManager.isBleConnect(env);
158
+ // Tracks the in-flight BLE probe so interval ticks never overlap: a superseded
159
+ // Initialize left pending on the V1 transport is exactly what later times out
160
+ // and used to tear down the connection mid-firmware-upload.
161
+ let bleProbeInFlight = false;
146
162
 
147
163
  Log.log('FirmwareUpdateBaseMethod [checkDeviceToBootloader] isBleReconnect: ', isBleReconnect);
148
164
 
@@ -198,6 +214,8 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
198
214
  }
199
215
 
200
216
  if (isBleReconnect) {
217
+ if (bleProbeInFlight) return;
218
+ bleProbeInFlight = true;
201
219
  try {
202
220
  await this.device.deviceConnector?.acquire(
203
221
  this.device.originalDescriptor.id,
@@ -205,7 +223,10 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
205
223
  true,
206
224
  this.payload.connectProtocol ?? this.device.originalDescriptor.protocolType
207
225
  );
208
- await this.device.initialize();
226
+ // Bound each probe so a request the rebooting device never received
227
+ // frees the slot for the next tick instead of hanging into the
228
+ // 30s reboot budget.
229
+ await this.device.initialize({ timeoutMs: BOOTLOADER_POLL_INITIALIZE_TIMEOUT_MS });
209
230
  if (this.device.isBootloader()) {
210
231
  clearInterval(intervalTimer);
211
232
  this.checkPromise?.resolve(true);
@@ -213,6 +234,8 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
213
234
  } catch (e) {
214
235
  // ignore error because of device is not connected
215
236
  Log.log('catch Bluetooth error when device is restarting: ', e);
237
+ } finally {
238
+ bleProbeInFlight = false;
216
239
  }
217
240
  } else {
218
241
  await this._checkDeviceInBootloaderMode(connectId, intervalTimer, timeoutTimer);
@@ -394,6 +417,54 @@ export class FirmwareUpdateBaseMethod<Params> extends BaseMethod<Params> {
394
417
  return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
395
418
  }
396
419
 
420
+ async emmcCommonUpdateProcessFromSource({
421
+ source,
422
+ filePath,
423
+ processedSize,
424
+ totalSize,
425
+ }: {
426
+ source: FirmwareByteSource;
427
+ filePath: string;
428
+ processedSize?: number;
429
+ totalSize?: number;
430
+ }) {
431
+ if (!filePath.startsWith('0:')) {
432
+ throw new Error('filePath must start with 0:');
433
+ }
434
+ const env = DataManager.getSettings('env');
435
+ const chunkSize = 1024 * (DataManager.isBleConnect(env) ? 16 : 128);
436
+
437
+ await writeFirmwareByteSource({
438
+ source,
439
+ chunkSize,
440
+ write: async ({ data, sourceOffset, length, first }) => {
441
+ const progress =
442
+ totalSize !== undefined && processedSize !== undefined
443
+ ? Math.min(Math.ceil(((processedSize + sourceOffset + length) / totalSize) * 100), 99)
444
+ : Math.min(Math.ceil(((sourceOffset + length) / source.size) * 100), 99);
445
+ const writeRes = await this.emmcFileWriteWithRetry(
446
+ filePath,
447
+ length,
448
+ sourceOffset,
449
+ data,
450
+ first,
451
+ progress
452
+ );
453
+ if (!writeRes) {
454
+ throw ERRORS.TypedError(
455
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
456
+ 'transfer data error'
457
+ );
458
+ }
459
+ const processedBytes = Number(writeRes.message.processed_byte);
460
+ this.postProgressMessage(progress, 'transferData');
461
+ return Number.isFinite(processedBytes) ? processedBytes : length;
462
+ },
463
+ });
464
+
465
+ return totalSize !== undefined ? (processedSize ?? 0) + source.size : 0;
466
+ }
467
+
397
468
  async emmcFileWriteWithRetry(
398
469
  filePath: string,
399
470
  chunkLength: number,