@onekeyfe/hd-core 1.2.6-alpha.0 → 1.2.6-alpha.2

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.
@@ -4,8 +4,11 @@ import CheckAllFirmwareRelease, {
4
4
  buildProtocolV2FirmwareRelease,
5
5
  } from '../src/api/CheckAllFirmwareRelease';
6
6
  import { DataManager } from '../src/data-manager';
7
+ import { mapProtocolV2DeviceInfoToState } from '../src/device/DeviceStateMapper';
8
+ import { DeviceStateStore } from '../src/device/DeviceStateStore';
7
9
  import { createCoreApi } from '../src/inject';
8
10
 
11
+ import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
9
12
  import type { CoreApi } from '../src/types/api';
10
13
  import type { DeviceStateVersions, IFirmwareReleaseInfo } from '../src/types';
11
14
 
@@ -290,6 +293,67 @@ describe('checkAllFirmwareRelease Protocol V2 support', () => {
290
293
  expect(result.targetsToUpdate).toEqual(['app_v1', 'app_v2']);
291
294
  });
292
295
 
296
+ test('does not repeat a P2 update after normal firmware follows an older loader P2 reading', () => {
297
+ const store = new DeviceStateStore();
298
+ store.update(
299
+ mapProtocolV2DeviceInfoToState(
300
+ {
301
+ main_mcu: {
302
+ application: { version: '1.0.2' },
303
+ application_data: { version: '1.0.2', build_id: 'old-p2', hash: [0xbb] },
304
+ },
305
+ } as ProtocolV2DeviceInfo,
306
+ 'bootloader'
307
+ ),
308
+ 'device-info'
309
+ );
310
+ const { state } = store.update(
311
+ mapProtocolV2DeviceInfoToState(
312
+ {
313
+ main_mcu: {
314
+ application: { version: '1.0.3', hash: [0xaa] },
315
+ application_data: { version: '1.0.2', build_id: 'old-p2', hash: [0xbb] },
316
+ },
317
+ } as ProtocolV2DeviceInfo,
318
+ 'normal'
319
+ ),
320
+ 'device-info'
321
+ );
322
+ const packageSet: IFirmwareReleaseInfo = {
323
+ ...release,
324
+ installOrder: ['applicationP1', 'applicationP2'],
325
+ components: {
326
+ applicationP1: {
327
+ target: 'APPLICATION_P1',
328
+ version: [1, 0, 3],
329
+ url: 'https://example.com/p1.okpkg',
330
+ },
331
+ applicationP2: {
332
+ target: 'APPLICATION_P2',
333
+ version: [1, 0, 3],
334
+ url: 'https://example.com/p2.okpkg',
335
+ },
336
+ },
337
+ };
338
+
339
+ expect(state.versions.applicationP2).toBeNull();
340
+ expect(state.verification?.applicationP2BuildId).toBeNull();
341
+ expect(state.verification?.applicationP2Hash).toBeNull();
342
+ expect(
343
+ buildProtocolV2FirmwareRelease({
344
+ currentVersions: state.versions,
345
+ currentVerification: state.verification,
346
+ checkFirmwareHash: true,
347
+ remotePayloadHashes: {
348
+ applicationP1: 'aa'.repeat(64),
349
+ applicationP2: 'cc'.repeat(64),
350
+ },
351
+ firmwareType: EFirmwareType.Universal,
352
+ release: packageSet,
353
+ }).targetsToUpdate
354
+ ).toEqual([]);
355
+ });
356
+
293
357
  test('does not read vol0 resource inventory while Protocol V2 is in Application mode', async () => {
294
358
  const method = new CheckAllFirmwareRelease({
295
359
  id: 1,
@@ -110,7 +110,7 @@ describe('DeviceStateMapper', () => {
110
110
  expect(patch.identity?.firmwareType).toBe(EFirmwareType.Universal);
111
111
  });
112
112
 
113
- test('maps Protocol V2 DeviceInfo without inventing runtime status', () => {
113
+ test('maps Protocol V2 bootloader DeviceInfo including P2', () => {
114
114
  const patch = mapProtocolV2DeviceInfoToState(
115
115
  {
116
116
  protocol_version: 2,
@@ -130,7 +130,7 @@ describe('DeviceStateMapper', () => {
130
130
  application: { version: '3.0.0', build_id: 'se1-build', hash: [0x05, 0x06] },
131
131
  },
132
132
  } as ProtocolV2DeviceInfo,
133
- 'normal'
133
+ 'bootloader'
134
134
  );
135
135
 
136
136
  expect(patch.identity).toMatchObject({
@@ -157,9 +157,33 @@ describe('DeviceStateMapper', () => {
157
157
  se01: { type: 'THD89', state: 'APP' },
158
158
  });
159
159
  expect(patch).toMatchObject({ protocolVersion: 2 });
160
- expect(patch.status?.unlocked).toBeUndefined();
160
+ expect(patch.status?.unlocked).toBeNull();
161
161
  });
162
162
 
163
+ test.each([undefined, { version: '1.0.2', build_id: 'old-p2', hash: [0x01, 0x02] }])(
164
+ 'clears cached loader P2 version and verification when normal firmware reports P1',
165
+ applicationData => {
166
+ const patch = mapProtocolV2DeviceInfoToState(
167
+ {
168
+ main_mcu: {
169
+ application: { version: '1.0.3' },
170
+ application_data: applicationData,
171
+ },
172
+ } as ProtocolV2DeviceInfo,
173
+ 'normal'
174
+ );
175
+
176
+ expect(patch.versions).toMatchObject({
177
+ applicationP1: '1.0.3',
178
+ applicationP2: null,
179
+ });
180
+ expect(patch.verification).toMatchObject({
181
+ applicationP2BuildId: null,
182
+ applicationP2Hash: null,
183
+ });
184
+ }
185
+ );
186
+
163
187
  test('maps the Protocol V2 Neo device type without treating it as Pro2', () => {
164
188
  const patch = mapProtocolV2DeviceInfoToState({
165
189
  hw: { Device_type: DeviceType.NEO, serial_no: 'NEO-SERIAL-1' },
@@ -3,6 +3,8 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
3
  import FirmwareUpdateV4 from '../../src/api/FirmwareUpdateV4';
4
4
 
5
5
  import type { Device } from '../../src/device/Device';
6
+ import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
7
+ import type { Features } from '../../src/types';
6
8
 
7
9
  jest.mock('../../src/data/config', () => ({
8
10
  DEFAULT_DOMAIN: 'https://example.com/',
@@ -833,6 +835,304 @@ describe('FirmwareUpdateV4 install polling', () => {
833
835
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
834
836
  });
835
837
 
838
+ test.each([
839
+ { scenario: 'missed progress after reconnect', updatedAt: 0, loaderBeforeUpdate: false },
840
+ { scenario: 'a reboot on the reconnected link', updatedAt: 1000, loaderBeforeUpdate: true },
841
+ { scenario: 'completion at the deadline', updatedAt: 600_000, loaderBeforeUpdate: false },
842
+ {
843
+ scenario: 'completion at the deadline without a disconnect',
844
+ updatedAt: 600_000,
845
+ loaderBeforeUpdate: false,
846
+ skipReconnect: true,
847
+ },
848
+ {
849
+ scenario: 'a reboot with an unavailable status endpoint',
850
+ updatedAt: 1000,
851
+ loaderBeforeUpdate: true,
852
+ statusUnavailable: true,
853
+ },
854
+ {
855
+ scenario: 'a different physical device after reconnect',
856
+ updatedAt: 0,
857
+ loaderBeforeUpdate: false,
858
+ differentDevice: true,
859
+ },
860
+ {
861
+ scenario: 'unchanged versions despite cached P2',
862
+ updatedAt: Infinity,
863
+ loaderBeforeUpdate: false,
864
+ },
865
+ {
866
+ scenario: 'normal firmware P1 updated while P2 is unobservable',
867
+ updatedAt: Infinity,
868
+ p1UpdatedAt: 0,
869
+ loaderBeforeUpdate: false,
870
+ },
871
+ {
872
+ scenario: 'P2-only install cannot be inferred from an already updated P1',
873
+ updatedAt: Infinity,
874
+ p1UpdatedAt: 0,
875
+ loaderBeforeUpdate: false,
876
+ p2Only: true,
877
+ },
878
+ {
879
+ scenario: 'normal firmware P1 updated after in-progress status disappears',
880
+ updatedAt: 3000,
881
+ p1UpdatedAt: 0,
882
+ statusInProgressAtStart: true,
883
+ loaderBeforeUpdate: false,
884
+ },
885
+ ])(
886
+ 'checks fresh versions when BLE status is unavailable: $scenario',
887
+ async ({
888
+ updatedAt,
889
+ p1UpdatedAt,
890
+ p2Only,
891
+ statusInProgressAtStart,
892
+ loaderBeforeUpdate,
893
+ skipReconnect,
894
+ statusUnavailable,
895
+ differentDevice,
896
+ }) => {
897
+ let now = 0;
898
+ jest.spyOn(Date, 'now').mockImplementation(() => now);
899
+ jest.spyOn(global, 'setTimeout').mockImplementation(((
900
+ callback: () => void,
901
+ delay: number
902
+ ) => {
903
+ now += delay;
904
+ callback();
905
+ return 0;
906
+ }) as typeof setTimeout);
907
+ const method = new FirmwareUpdateV4({
908
+ id: 1,
909
+ payload: { method: 'firmwareUpdateV4', connectId: 'pro2-ble' },
910
+ });
911
+ const targets = [
912
+ { target_id: 4, path: 'vol0:/application_p1.bin' },
913
+ { target_id: 5, path: 'vol0:/application_p2.bin' },
914
+ ].filter(target => !p2Only || target.target_id === 5);
915
+ let statusPolls = 0;
916
+ const statusGet = jest.fn().mockImplementation(() => {
917
+ if (statusUnavailable) throw new Error('unsupported message');
918
+ if (statusInProgressAtStart && statusPolls++ === 0) {
919
+ return {
920
+ type: 'DeviceFirmwareUpdateStatus',
921
+ message: { records: [{ ...targets[targets.length - 1], status: 1 }] },
922
+ };
923
+ }
924
+ return { type: 'DeviceFirmwareUpdateStatus', message: { records: [] } };
925
+ });
926
+ let lastDeviceInfo: ProtocolV2DeviceInfo | undefined;
927
+ const deviceInfoGet = jest.fn().mockImplementation(() => {
928
+ lastDeviceInfo = {
929
+ hw: { serial_no: differentDevice ? 'pro2-other' : 'pro2-test' },
930
+ main_mcu: {
931
+ application: { version: now >= (p1UpdatedAt ?? updatedAt) ? '1.0.2' : '1.0.1' },
932
+ application_data: { version: now >= updatedAt ? '1.0.2' : '1.0.1' },
933
+ },
934
+ };
935
+ return { type: 'DeviceInfo', message: lastDeviceInfo };
936
+ });
937
+ const typedCall = jest.fn().mockImplementation((type: string) => {
938
+ if (type === 'DeviceFirmwareUpdateStatusGet') return statusGet();
939
+ if (type === 'DeviceInfoGet') return deviceInfoGet();
940
+ throw new Error(`Unexpected command: ${type}`);
941
+ });
942
+ let finalFeatures: Features | undefined;
943
+ const probeProtocolV2RuntimeState = jest
944
+ .fn()
945
+ .mockImplementation(
946
+ (
947
+ info: ProtocolV2DeviceInfo,
948
+ _timeout: number,
949
+ options?: { forceRuntimeContextRefresh?: boolean }
950
+ ) => {
951
+ const normal =
952
+ options?.forceRuntimeContextRefresh && (!loaderBeforeUpdate || now >= updatedAt);
953
+ finalFeatures = {
954
+ mode: normal ? 'normal' : 'bootloader',
955
+ bootloaderMode: !normal,
956
+ firmwareVersion: info.main_mcu?.application?.version,
957
+ } as Features;
958
+ return finalFeatures;
959
+ }
960
+ );
961
+ method.device = {
962
+ originalDescriptor: { path: 'pro2-ble' },
963
+ getCommands: () => ({ typedCall }),
964
+ // A retained P2 field is deliberately newer than the live pre-install version.
965
+ state: { versions: { applicationP2: '1.0.2' } },
966
+ probeProtocolV2RuntimeState,
967
+ setCancelableAction: jest.fn(),
968
+ } as unknown as Device;
969
+ method.postProgressMessage = jest.fn();
970
+ const firmwareUpdate = method as unknown as {
971
+ isBleReconnect: () => boolean;
972
+ protocolV2InstallNeedsReconnect: boolean;
973
+ protocolV2ExpectedSerialNumber: string;
974
+ protocolV2InstallBaselineVersions: Map<number, string>;
975
+ protocolV2LatestFinalFeatures?: Features;
976
+ protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
977
+ params: { expectedTargetVersions: { app_v1: string; app_v2: string } };
978
+ waitForProtocolV2FirmwareUpdateComplete: (
979
+ value: typeof targets,
980
+ requireCurrentInstallStatus: boolean
981
+ ) => Promise<void>;
982
+ reconnectProtocolV2Device: () => Promise<void>;
983
+ assertExpectedProtocolV2Versions: () => void;
984
+ };
985
+ firmwareUpdate.isBleReconnect = () => true;
986
+ firmwareUpdate.protocolV2InstallNeedsReconnect = !skipReconnect;
987
+ firmwareUpdate.protocolV2ExpectedSerialNumber = 'pro2-test';
988
+ firmwareUpdate.protocolV2InstallBaselineVersions = new Map([
989
+ [4, '1.0.1'],
990
+ [5, '1.0.1'],
991
+ ]);
992
+ firmwareUpdate.params = { expectedTargetVersions: { app_v1: '1.0.2', app_v2: '1.0.2' } };
993
+ firmwareUpdate.reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
994
+
995
+ const polling = firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
996
+ if (differentDevice) {
997
+ await expect(polling).rejects.toThrow('physical identity mismatch');
998
+ expect(probeProtocolV2RuntimeState).not.toHaveBeenCalled();
999
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
1000
+ return;
1001
+ }
1002
+ let completionAt = updatedAt;
1003
+ if (p2Only) {
1004
+ completionAt = Infinity;
1005
+ } else if (!loaderBeforeUpdate) {
1006
+ completionAt = Math.min(updatedAt, p1UpdatedAt ?? Infinity);
1007
+ }
1008
+ if (Number.isFinite(completionAt)) {
1009
+ await polling;
1010
+ expect(now).toBe(Math.max(completionAt, statusInProgressAtStart ? 1000 : 0));
1011
+ firmwareUpdate.protocolV2LatestFinalFeatures = finalFeatures;
1012
+ firmwareUpdate.protocolV2LatestFinalDeviceInfo = lastDeviceInfo;
1013
+ expect(() => firmwareUpdate.assertExpectedProtocolV2Versions()).not.toThrow();
1014
+ firmwareUpdate.params.expectedTargetVersions.app_v1 = '1.0.3';
1015
+ expect(() => firmwareUpdate.assertExpectedProtocolV2Versions()).toThrow('expected 1.0.3');
1016
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
1017
+ } else {
1018
+ await expect(polling).rejects.toMatchObject({
1019
+ params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
1020
+ });
1021
+ expect(now).toBe(600_000);
1022
+ expect(method.postProgressMessage).not.toHaveBeenCalled();
1023
+ }
1024
+ expect(deviceInfoGet).toHaveBeenCalledTimes(
1025
+ skipReconnect ? 1 : now / 1000 + 1 - (statusInProgressAtStart ? 1 : 0)
1026
+ );
1027
+ expect(probeProtocolV2RuntimeState).toHaveBeenLastCalledWith(
1028
+ expect.anything(),
1029
+ expect.any(Number),
1030
+ { forceRuntimeContextRefresh: true }
1031
+ );
1032
+ expect(typedCall).toHaveBeenCalledWith(
1033
+ 'DeviceInfoGet',
1034
+ 'DeviceInfo',
1035
+ expect.objectContaining({
1036
+ targets: {
1037
+ hw: true,
1038
+ main_mcu: true,
1039
+ coprocessor: true,
1040
+ se1: true,
1041
+ se2: true,
1042
+ se3: true,
1043
+ se4: true,
1044
+ },
1045
+ }),
1046
+ expect.anything()
1047
+ );
1048
+ }
1049
+ );
1050
+
1051
+ test.each([
1052
+ {
1053
+ scenario: 'finishes after five minutes',
1054
+ terminalAt: 360_000,
1055
+ terminalStatus: 2,
1056
+ error: undefined,
1057
+ },
1058
+ {
1059
+ scenario: 'finishes at the deadline',
1060
+ terminalAt: 600_000,
1061
+ terminalStatus: 2,
1062
+ error: undefined,
1063
+ },
1064
+ {
1065
+ scenario: 'remains in progress',
1066
+ terminalAt: Infinity,
1067
+ terminalStatus: 1,
1068
+ error: 'FirmwareInstallTimeout',
1069
+ },
1070
+ {
1071
+ scenario: 'fails at the deadline',
1072
+ terminalAt: 600_000,
1073
+ terminalStatus: 3,
1074
+ error: 'FirmwareInstallFailed',
1075
+ },
1076
+ {
1077
+ scenario: 'is cancelled at the deadline',
1078
+ terminalAt: 600_000,
1079
+ terminalStatus: -1,
1080
+ error: 'cancelled',
1081
+ },
1082
+ ])(
1083
+ 'polls installation for ten minutes: $scenario',
1084
+ async ({ terminalAt, terminalStatus, error }) => {
1085
+ let now = 0;
1086
+ jest.spyOn(Date, 'now').mockImplementation(() => now);
1087
+ jest.spyOn(global, 'setTimeout').mockImplementation(((
1088
+ callback: () => void,
1089
+ delay: number
1090
+ ) => {
1091
+ now += delay;
1092
+ callback();
1093
+ return 0;
1094
+ }) as typeof setTimeout);
1095
+ const method = new FirmwareUpdateV4({
1096
+ id: 1,
1097
+ payload: { method: 'firmwareUpdateV4', connectId: 'pro2-ble' },
1098
+ });
1099
+ const targets = [{ target_id: 4, path: 'vol0:/application_p1.bin' }];
1100
+ const typedCall = jest.fn().mockImplementation(() => {
1101
+ if (now >= terminalAt && terminalStatus === -1) {
1102
+ throw ERRORS.TypedError(HardwareErrorCode.ActionCancelled);
1103
+ }
1104
+ return {
1105
+ type: 'DeviceFirmwareUpdateStatus',
1106
+ message: { records: [{ ...targets[0], status: now >= terminalAt ? terminalStatus : 1 }] },
1107
+ };
1108
+ });
1109
+ method.device = { getCommands: () => ({ typedCall }) } as unknown as Device;
1110
+ method.postProgressMessage = jest.fn();
1111
+ const firmwareUpdate = method as unknown as {
1112
+ isBleReconnect: () => boolean;
1113
+ waitForProtocolV2FirmwareUpdateComplete: (
1114
+ value: typeof targets,
1115
+ requireCurrentInstallStatus: boolean
1116
+ ) => Promise<void>;
1117
+ };
1118
+ firmwareUpdate.isBleReconnect = () => true;
1119
+ const polling = firmwareUpdate.waitForProtocolV2FirmwareUpdateComplete(targets, true);
1120
+ if (error) {
1121
+ await expect(polling).rejects.toMatchObject(
1122
+ error === 'cancelled'
1123
+ ? { errorCode: HardwareErrorCode.ActionCancelled }
1124
+ : { params: { firmwareUpdateCode: error } }
1125
+ );
1126
+ expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
1127
+ } else {
1128
+ await polling;
1129
+ expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
1130
+ }
1131
+ expect(now).toBe(Math.min(terminalAt, 600_000));
1132
+ expect(typedCall).toHaveBeenCalledTimes(now / 1000 + 1);
1133
+ }
1134
+ );
1135
+
836
1136
  test('does not send Request when Stage is rejected', async () => {
837
1137
  const method = new FirmwareUpdateV4({
838
1138
  id: 1,
@@ -6328,7 +6328,7 @@ describe('Protocol V2 firmware update targets', () => {
6328
6328
  expect(typedCall).toHaveBeenCalledTimes(3);
6329
6329
  });
6330
6330
 
6331
- test('waits five minutes before rejecting normal mode without install evidence', async () => {
6331
+ test('waits ten minutes before rejecting normal mode without install evidence', async () => {
6332
6332
  const method = new FirmwareUpdateV4({
6333
6333
  id: 1,
6334
6334
  payload: {
@@ -6366,7 +6366,7 @@ describe('Protocol V2 firmware update targets', () => {
6366
6366
  });
6367
6367
 
6368
6368
  expect(method.postProgressMessage).not.toHaveBeenCalledWith(100, 'installingFirmware');
6369
- expect(typedCall).toHaveBeenCalledTimes(5);
6369
+ expect(typedCall).toHaveBeenCalledTimes(11);
6370
6370
  });
6371
6371
 
6372
6372
  test('preserves multi-app completion evidence when App mode replaces the status endpoint', async () => {
@@ -6381,6 +6381,7 @@ describe('Protocol V2 firmware update targets', () => {
6381
6381
  },
6382
6382
  },
6383
6383
  });
6384
+ method.init();
6384
6385
  const typedCall = jest
6385
6386
  .fn()
6386
6387
  .mockRejectedValue(new Error('Failure: Handler not registered for this message'));
@@ -6410,7 +6411,9 @@ describe('Protocol V2 firmware update targets', () => {
6410
6411
  ])
6411
6412
  ).resolves.toBeUndefined();
6412
6413
 
6413
- expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
6414
+ expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
6415
+ forceRuntimeContextRefresh: true,
6416
+ });
6414
6417
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
6415
6418
  expect(Array.from((method as any).protocolV2CompletedTargetIds)).toEqual([4, 5]);
6416
6419
 
@@ -6535,7 +6538,9 @@ describe('Protocol V2 firmware update targets', () => {
6535
6538
  (method as any).device = stubDevice({ probeProtocolV2RuntimeState });
6536
6539
 
6537
6540
  await expect((method as any).probeProtocolV2NormalMode(deviceInfo)).resolves.toBe(expected);
6538
- expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000);
6541
+ expect(probeProtocolV2RuntimeState).toHaveBeenCalledWith(deviceInfo, 5000, {
6542
+ forceRuntimeContextRefresh: true,
6543
+ });
6539
6544
  });
6540
6545
 
6541
6546
  test('keeps polling when the firmware status handler is missing in loader mode', async () => {
@@ -6735,7 +6740,7 @@ describe('Protocol V2 firmware update targets', () => {
6735
6740
  expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
6736
6741
  });
6737
6742
 
6738
- test('uses the firmware version for P1 and the optional DeviceInfo version for P2', () => {
6743
+ test('uses bootloader DeviceInfo for the P2 version', () => {
6739
6744
  const method = new FirmwareUpdateV4({
6740
6745
  id: 1,
6741
6746
  payload: {
@@ -6753,6 +6758,9 @@ describe('Protocol V2 firmware update targets', () => {
6753
6758
  major_version: 1,
6754
6759
  minor_version: 0,
6755
6760
  patch_version: 0,
6761
+ mode: 'bootloader',
6762
+ bootloaderMode: true,
6763
+ firmwareVersion: '1.0.0',
6756
6764
  };
6757
6765
  (method as any).protocolV2LatestFinalDeviceInfo = {
6758
6766
  main_mcu: {
@@ -6764,7 +6772,36 @@ describe('Protocol V2 firmware update targets', () => {
6764
6772
  expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
6765
6773
  });
6766
6774
 
6767
- test('rejects a mismatched final DeviceInfo application slot version', () => {
6775
+ test('uses P1 after normal boot even when DeviceInfo retains an old P2 version', () => {
6776
+ const method = new FirmwareUpdateV4({
6777
+ id: 1,
6778
+ payload: {
6779
+ method: 'firmwareUpdateV4',
6780
+ platform: 'desktop',
6781
+ targetsToUpdate: ['app_v1', 'app_v2'],
6782
+ expectedTargetVersions: { app_v1: '2.0.0', app_v2: '2.0.0' },
6783
+ },
6784
+ });
6785
+ method.init();
6786
+ (method as any).protocolV2LatestFinalFeatures = {
6787
+ major_version: 2,
6788
+ minor_version: 0,
6789
+ patch_version: 0,
6790
+ mode: 'normal',
6791
+ bootloaderMode: false,
6792
+ };
6793
+ (method as any).protocolV2LatestFinalDeviceInfo = {
6794
+ main_mcu: {
6795
+ application: { version: '2.0.0' },
6796
+ application_data: { version: '1.0.1' },
6797
+ },
6798
+ };
6799
+ (method as any).protocolV2CompletedTargetVersions = new Map([[5, 0x20000]]);
6800
+
6801
+ expect(() => (method as any).assertExpectedProtocolV2Versions()).not.toThrow();
6802
+ });
6803
+
6804
+ test('rejects a mismatched bootloader DeviceInfo application slot version', () => {
6768
6805
  const method = new FirmwareUpdateV4({
6769
6806
  id: 1,
6770
6807
  payload: {
@@ -6782,6 +6819,9 @@ describe('Protocol V2 firmware update targets', () => {
6782
6819
  major_version: 1,
6783
6820
  minor_version: 0,
6784
6821
  patch_version: 0,
6822
+ mode: 'bootloader',
6823
+ bootloaderMode: true,
6824
+ firmwareVersion: '1.0.0',
6785
6825
  };
6786
6826
  (method as any).protocolV2LatestFinalDeviceInfo = {
6787
6827
  main_mcu: {
@@ -6888,7 +6928,7 @@ describe('Protocol V2 firmware update targets', () => {
6888
6928
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'installingFirmware');
6889
6929
  });
6890
6930
 
6891
- test('keeps polling missing target records until the five-minute timeout', async () => {
6931
+ test('keeps polling missing target records until the ten-minute timeout', async () => {
6892
6932
  const method = new FirmwareUpdateV4({
6893
6933
  id: 1,
6894
6934
  payload: {
@@ -6926,7 +6966,7 @@ describe('Protocol V2 firmware update targets', () => {
6926
6966
  message: 'Protocol V2 firmware install timed out',
6927
6967
  params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
6928
6968
  });
6929
- expect(typedCall).toHaveBeenCalledTimes(5);
6969
+ expect(typedCall).toHaveBeenCalledTimes(11);
6930
6970
  });
6931
6971
 
6932
6972
  test('keeps polling incomplete records after a Pro2 reboot interruption', async () => {
@@ -6982,7 +7022,7 @@ describe('Protocol V2 firmware update targets', () => {
6982
7022
  expect(typedCall).toHaveBeenCalledTimes(4);
6983
7023
  });
6984
7024
 
6985
- test('uses a five-minute Pro2 install status window', async () => {
7025
+ test('uses a ten-minute Pro2 install status window with a final status check', async () => {
6986
7026
  const method = new FirmwareUpdateV4({
6987
7027
  id: 1,
6988
7028
  payload: {
@@ -6992,7 +7032,13 @@ describe('Protocol V2 firmware update targets', () => {
6992
7032
  jest
6993
7033
  .spyOn(Date, 'now')
6994
7034
  .mockReturnValueOnce(0)
6995
- .mockReturnValue(5 * 60 * 1000);
7035
+ .mockReturnValue(10 * 60 * 1000);
7036
+ const typedCall = jest.fn().mockResolvedValue({
7037
+ type: 'DeviceFirmwareUpdateStatus',
7038
+ message: { records: [{ target_id: 4, status: 1 }] },
7039
+ });
7040
+ method.device = stubDevice({ getCommands: () => ({ typedCall }) });
7041
+ method.postProgressMessage = jest.fn();
6996
7042
 
6997
7043
  await expect(
6998
7044
  (method as any).waitForProtocolV2FirmwareUpdateComplete([
@@ -7003,6 +7049,7 @@ describe('Protocol V2 firmware update targets', () => {
7003
7049
  message: 'Protocol V2 firmware install timed out',
7004
7050
  params: { firmwareUpdateCode: 'FirmwareInstallTimeout' },
7005
7051
  });
7052
+ expect(typedCall).toHaveBeenCalledTimes(1);
7006
7053
  });
7007
7054
 
7008
7055
  test('uses a 90-second Pro2 bootloader reconnect window', async () => {
@@ -8914,6 +8961,54 @@ describe('Protocol V2 firmware update targets', () => {
8914
8961
  });
8915
8962
  });
8916
8963
 
8964
+ test.each([
8965
+ ['react-native', 10_000],
8966
+ ['webusb', undefined],
8967
+ ] as const)('uses the native firmware write timeout in %s', async (env, timeoutMs) => {
8968
+ const getSettingsSpy = jest.spyOn(DataManager, 'getSettings').mockReturnValue(env);
8969
+ const method = new FirmwareUpdateV4({
8970
+ id: 1,
8971
+ payload: {
8972
+ method: 'firmwareUpdateV4',
8973
+ },
8974
+ });
8975
+ const typedCall = jest.fn().mockResolvedValue({
8976
+ type: 'FilesystemFile',
8977
+ message: { processed_byte: 1 },
8978
+ });
8979
+ (method as any).device = stubDevice({
8980
+ getCommands: () => ({ typedCall }),
8981
+ });
8982
+
8983
+ try {
8984
+ await (method as any).fileWriteChunk(
8985
+ 'vol0:/application_p1.bin',
8986
+ 1,
8987
+ 0,
8988
+ new Uint8Array([1]),
8989
+ true,
8990
+ 1
8991
+ );
8992
+ } finally {
8993
+ getSettingsSpy.mockRestore();
8994
+ }
8995
+
8996
+ expect(typedCall).toHaveBeenCalledWith(
8997
+ 'FilesystemFileWrite',
8998
+ 'FilesystemFile',
8999
+ expect.objectContaining({
9000
+ file: expect.objectContaining({
9001
+ path: 'vol0:/application_p1.bin',
9002
+ offset: 0,
9003
+ }),
9004
+ }),
9005
+ expect.objectContaining({
9006
+ timeoutMs,
9007
+ writeWithResponse: false,
9008
+ })
9009
+ );
9010
+ });
9011
+
8917
9012
  test('does not retry or wrap a stale BLE bond during a V4 file transfer', async () => {
8918
9013
  const method = new FirmwareUpdateV4({
8919
9014
  id: 1,
@@ -54,14 +54,6 @@ describe('SolSignOffchainMessage', () => {
54
54
  });
55
55
  });
56
56
 
57
- test('maps omitted required signers to an empty repeated field', () => {
58
- const method = createMethod({ requiredSigners: undefined });
59
-
60
- method.init();
61
-
62
- expect(method.params.required_signers).toEqual([]);
63
- });
64
-
65
57
  test.each([
66
58
  { requiredSigners: ['11'], error: '32-byte hex public key' },
67
59
  { requiredSigners: [signerB, signerA], error: 'strictly sorted and unique' },
@@ -16,6 +16,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
16
16
  private protocolV2CompletedTargetIds;
17
17
  private protocolV2LatestFinalFeatures?;
18
18
  private protocolV2LatestFinalDeviceInfo?;
19
+ private protocolV2InitialDeviceInfo?;
19
20
  private protocolV2InstallBaselineVersions;
20
21
  private protocolV2InstallNeedsReconnect;
21
22
  private protocolV2InstallDisconnectObserved;
@@ -1 +1 @@
1
- {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"FirmwareUpdateV4.d.ts","sourceRoot":"","sources":["../../src/api/FirmwareUpdateV4.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceStateMapper.d.ts","sourceRoot":"","sources":["../../src/device/DeviceStateMapper.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,aAAa,EAEb,cAAc,EACd,YAAY,EACZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAEV,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAsDxE,eAAO,MAAM,kBAAkB,aAAc,QAAQ,KAAG,gBAqEtD,CAAC;AAEH,eAAO,MAAM,4BAA4B,uBACnB,cAAc,KACjC,gBAA0F,CAAC;AAc9F,eAAO,MAAM,kCAAkC,aAAc,cAAc,KAAG,gBAgE7E,CAAC;AAEF,eAAO,MAAM,8BAA8B,SACnC,oBAAoB,SACpB,eAAe,KACpB,gBAuGF,CAAC;AAEF,eAAO,MAAM,gCAAgC,WAAY,YAAY,KAAG,gBActE,CAAC;AAEH,eAAO,MAAM,uBAAuB,aAAc,aAAa,KAAG,gBAmBjE,CAAC;AAEF,eAAO,MAAM,wBAAwB,aAAc,cAAc,KAAG,gBA0BnE,CAAC;AAEF,eAAO,MAAM,6BAA6B,aAAc,oBAAoB,KAAG,cAe3E,CAAC"}
1
+ {"version":3,"file":"DeviceStateMapper.d.ts","sourceRoot":"","sources":["../../src/device/DeviceStateMapper.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,aAAa,EAEb,cAAc,EACd,YAAY,EACZ,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,EAEV,eAAe,EACf,gBAAgB,EAChB,QAAQ,EACR,cAAc,EACf,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAsDxE,eAAO,MAAM,kBAAkB,aAAc,QAAQ,KAAG,gBAqEtD,CAAC;AAEH,eAAO,MAAM,4BAA4B,uBACnB,cAAc,KACjC,gBAA0F,CAAC;AAc9F,eAAO,MAAM,kCAAkC,aAAc,cAAc,KAAG,gBAgE7E,CAAC;AAEF,eAAO,MAAM,8BAA8B,SACnC,oBAAoB,SACpB,eAAe,KACpB,gBAyGF,CAAC;AAEF,eAAO,MAAM,gCAAgC,WAAY,YAAY,KAAG,gBActE,CAAC;AAEH,eAAO,MAAM,uBAAuB,aAAc,aAAa,KAAG,gBAmBjE,CAAC;AAEF,eAAO,MAAM,wBAAwB,aAAc,cAAc,KAAG,gBA0BnE,CAAC;AAEF,eAAO,MAAM,6BAA6B,aAAc,oBAAoB,KAAG,cAe3E,CAAC"}
package/dist/index.js CHANGED
@@ -44899,7 +44899,7 @@ const mapProtocolV2DeviceInfoToState = (info, mode = 'unknown') => {
44899
44899
  versions: definedEntries({
44900
44900
  firmware: imageVersion((_d = info.main_mcu) === null || _d === void 0 ? void 0 : _d.application),
44901
44901
  applicationP1: imageVersion((_e = info.main_mcu) === null || _e === void 0 ? void 0 : _e.application),
44902
- applicationP2: imageVersion((_f = info.main_mcu) === null || _f === void 0 ? void 0 : _f.application_data),
44902
+ applicationP2: mode === 'normal' ? null : imageVersion((_f = info.main_mcu) === null || _f === void 0 ? void 0 : _f.application_data),
44903
44903
  bootloader: imageVersion((_g = info.main_mcu) === null || _g === void 0 ? void 0 : _g.bootloader),
44904
44904
  board: imageVersion((_h = info.main_mcu) === null || _h === void 0 ? void 0 : _h.romloader),
44905
44905
  ble: imageVersion((_j = info.coprocessor) === null || _j === void 0 ? void 0 : _j.application),
@@ -44917,8 +44917,8 @@ const mapProtocolV2DeviceInfoToState = (info, mode = 'unknown') => {
44917
44917
  firmwareHash: imageHash((_u = info.main_mcu) === null || _u === void 0 ? void 0 : _u.application),
44918
44918
  applicationP1BuildId: imageBuildId((_v = info.main_mcu) === null || _v === void 0 ? void 0 : _v.application),
44919
44919
  applicationP1Hash: imageHash((_w = info.main_mcu) === null || _w === void 0 ? void 0 : _w.application),
44920
- applicationP2BuildId: imageBuildId((_x = info.main_mcu) === null || _x === void 0 ? void 0 : _x.application_data),
44921
- applicationP2Hash: imageHash((_y = info.main_mcu) === null || _y === void 0 ? void 0 : _y.application_data),
44920
+ applicationP2BuildId: mode === 'normal' ? null : imageBuildId((_x = info.main_mcu) === null || _x === void 0 ? void 0 : _x.application_data),
44921
+ applicationP2Hash: mode === 'normal' ? null : imageHash((_y = info.main_mcu) === null || _y === void 0 ? void 0 : _y.application_data),
44922
44922
  bootloaderBuildId: imageBuildId((_z = info.main_mcu) === null || _z === void 0 ? void 0 : _z.bootloader),
44923
44923
  bootloaderHash: imageHash((_0 = info.main_mcu) === null || _0 === void 0 ? void 0 : _0.bootloader),
44924
44924
  boardBuildId: imageBuildId((_1 = info.main_mcu) === null || _1 === void 0 ? void 0 : _1.romloader),
@@ -52412,7 +52412,8 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
52412
52412
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
52413
52413
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
52414
52414
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
52415
- const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
52415
+ const PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT = 10 * 1000;
52416
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 10 * 60 * 1000;
52416
52417
  const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
52417
52418
  const PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS = 4;
52418
52419
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
@@ -52891,8 +52892,7 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
52891
52892
  return __awaiter(this, void 0, void 0, function* () {
52892
52893
  yield this.captureProtocolV2PhysicalIdentity();
52893
52894
  const deviceFeatures = yield this.getProtocolV2DeviceFeatures();
52894
- this.protocolV2InstallBaselineVersions =
52895
- this.getProtocolV2ObservableTargetVersions(deviceFeatures);
52895
+ this.protocolV2InstallBaselineVersions = this.getProtocolV2ObservableTargetVersions(deviceFeatures, this.protocolV2InitialDeviceInfo);
52896
52896
  this.protocolV2LastRuntimeProbeFeatures = undefined;
52897
52897
  const currentDeviceType = this.device.getCurrentDeviceType();
52898
52898
  const capabilityDeviceType = currentDeviceType === hdShared.EDeviceType.Pro2 || currentDeviceType === hdShared.EDeviceType.Neo
@@ -53297,7 +53297,9 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
53297
53297
  app_v1: features
53298
53298
  ? getDeviceFirmwareVersion(features).join('.')
53299
53299
  : (_c = (_b = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.main_mcu) === null || _b === void 0 ? void 0 : _b.application) === null || _c === void 0 ? void 0 : _c.version,
53300
- app_v2: (_e = (_d = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.main_mcu) === null || _d === void 0 ? void 0 : _d.application_data) === null || _e === void 0 ? void 0 : _e.version,
53300
+ app_v2: (features === null || features === void 0 ? void 0 : features.mode) === 'normal'
53301
+ ? getDeviceFirmwareVersion(features).join('.')
53302
+ : (_e = (_d = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.main_mcu) === null || _d === void 0 ? void 0 : _d.application_data) === null || _e === void 0 ? void 0 : _e.version,
53301
53303
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
53302
53304
  se01: (_f = features === null || features === void 0 ? void 0 : features.se01Version) !== null && _f !== void 0 ? _f : undefined,
53303
53305
  se02: (_g = features === null || features === void 0 ? void 0 : features.se02Version) !== null && _g !== void 0 ? _g : undefined,
@@ -53402,11 +53404,16 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
53402
53404
  }
53403
53405
  this.protocolV2ExpectedSerialNumber = serialNumber;
53404
53406
  this.protocolV2ExpectedPath = path;
53407
+ this.protocolV2InitialDeviceInfo = deviceInfo;
53405
53408
  });
53406
53409
  }
53407
- verifyProtocolV2ReconnectIdentity() {
53410
+ verifyProtocolV2ReconnectIdentity(request) {
53408
53411
  return __awaiter(this, void 0, void 0, function* () {
53409
- const deviceInfo = yield this.requestProtocolV2PhysicalIdentity();
53412
+ const deviceInfo = yield requestProtocolV2DeviceInfo({
53413
+ commands: this.device.getCommands(),
53414
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
53415
+ request,
53416
+ });
53410
53417
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
53411
53418
  return deviceInfo;
53412
53419
  });
@@ -54158,12 +54165,18 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54158
54165
  });
54159
54166
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
54160
54167
  }
54161
- getProtocolV2ObservableTargetVersions(features) {
54168
+ getProtocolV2ObservableTargetVersions(features, deviceInfo) {
54169
+ var _a, _b;
54162
54170
  const versions = new Map();
54163
54171
  versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, getDeviceBootloaderVersion(features).join('.'));
54164
54172
  const applicationVersion = getDeviceFirmwareVersion(features).join('.');
54165
54173
  versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
54166
- versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
54174
+ const applicationP2Version = features.mode === 'normal'
54175
+ ? applicationVersion
54176
+ : (_b = (_a = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.main_mcu) === null || _a === void 0 ? void 0 : _a.application_data) === null || _b === void 0 ? void 0 : _b.version;
54177
+ if (applicationP2Version) {
54178
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationP2Version);
54179
+ }
54167
54180
  versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, getDeviceBLEFirmwareVersion(features).join('.'));
54168
54181
  const secureElementVersions = [
54169
54182
  [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, features.se01Version],
@@ -54177,17 +54190,31 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54177
54190
  });
54178
54191
  return versions;
54179
54192
  }
54180
- hasProtocolV2InstallVersionChanged(expectedTargetIds) {
54193
+ hasProtocolV2InstallVersionChanged(expectedTargetIds, deviceInfo) {
54181
54194
  if (!this.protocolV2LastRuntimeProbeFeatures)
54182
54195
  return false;
54183
- const currentVersions = this.getProtocolV2ObservableTargetVersions(this.protocolV2LastRuntimeProbeFeatures);
54184
- return Array.from(expectedTargetIds).some(targetId => {
54185
- const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
54186
- const currentVersion = currentVersions.get(targetId);
54187
- return (previousVersion !== undefined &&
54188
- currentVersion !== undefined &&
54189
- previousVersion !== currentVersion);
54190
- });
54196
+ const currentVersions = this.getProtocolV2ObservableTargetVersions(this.protocolV2LastRuntimeProbeFeatures, deviceInfo);
54197
+ const p1TargetId = ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1;
54198
+ const p2TargetId = ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2;
54199
+ return (expectedTargetIds.size > 0 &&
54200
+ Array.from(expectedTargetIds).every(targetId => {
54201
+ var _a, _b, _c;
54202
+ if (targetId === p2TargetId && ((_a = this.protocolV2LastRuntimeProbeFeatures) === null || _a === void 0 ? void 0 : _a.mode) === 'normal') {
54203
+ if (!expectedTargetIds.has(p1TargetId))
54204
+ return false;
54205
+ const previousP1Version = this.protocolV2InstallBaselineVersions.get(p1TargetId);
54206
+ const currentP1Version = currentVersions.get(p1TargetId);
54207
+ return (previousP1Version !== undefined &&
54208
+ currentP1Version !== undefined &&
54209
+ previousP1Version !== currentP1Version &&
54210
+ currentP1Version === ((_c = (_b = this.params) === null || _b === void 0 ? void 0 : _b.expectedTargetVersions) === null || _c === void 0 ? void 0 : _c.app_v2));
54211
+ }
54212
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
54213
+ const currentVersion = currentVersions.get(targetId);
54214
+ return (previousVersion !== undefined &&
54215
+ currentVersion !== undefined &&
54216
+ previousVersion !== currentVersion);
54217
+ }));
54191
54218
  }
54192
54219
  recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds) {
54193
54220
  expectedTargetIds.forEach(targetId => this.protocolV2CompletedTargetIds.add(targetId));
@@ -54224,8 +54251,9 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54224
54251
  finishedStatusSnapshotKey = undefined;
54225
54252
  finishedStatusSnapshotPolls = 0;
54226
54253
  };
54227
- while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
54254
+ for (;;) {
54228
54255
  this.throwIfAborted();
54256
+ const isFinalStatusCheck = Date.now() - startTime >= PROTOCOL_V2_INSTALL_TIMEOUT;
54229
54257
  try {
54230
54258
  if (shouldReconnect) {
54231
54259
  const isBleInstallReconnect = this.isBleReconnect();
@@ -54347,17 +54375,13 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54347
54375
  lastError = new Error('Protocol V2 firmware status is stale; waiting for the current install to start');
54348
54376
  }
54349
54377
  if (statusTargets.length === 0 &&
54350
- !currentDeviceInfo &&
54351
- bleInstallLinkReady &&
54352
- installEvidenceObserved) {
54353
- currentDeviceInfo = yield this.verifyProtocolV2ReconnectIdentity();
54378
+ (currentDeviceInfo || bleInstallLinkReady || isFinalStatusCheck)) {
54379
+ currentDeviceInfo = yield this.verifyProtocolV2ReconnectIdentity(PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST);
54354
54380
  deviceInfo = currentDeviceInfo;
54355
- }
54356
- if (statusTargets.length === 0 && currentDeviceInfo) {
54357
54381
  const isNormalMode = yield this.probeProtocolV2NormalMode(currentDeviceInfo);
54358
54382
  if (isNormalMode &&
54359
54383
  (installEvidenceObserved ||
54360
- this.hasProtocolV2InstallVersionChanged(expectedTargetIds))) {
54384
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds, currentDeviceInfo))) {
54361
54385
  Log$6.log('[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete');
54362
54386
  this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
54363
54387
  this.postProgressMessage(100, 'installingFirmware');
@@ -54379,24 +54403,19 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54379
54403
  }
54380
54404
  }
54381
54405
  catch (error) {
54382
- if (isProtocolV2TerminalInstallStatusError(error)) {
54406
+ if (this.isProtocolV2ReconnectIdentityError(error) ||
54407
+ isProtocolV2TerminalInstallStatusError(error)) {
54383
54408
  throw error;
54384
54409
  }
54385
54410
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
54386
- if (!currentDeviceInfo) {
54387
- if (!bleInstallLinkReady) {
54388
- throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Protocol V2 device identity is unavailable during install polling');
54389
- }
54390
- deviceInfo = yield this.verifyProtocolV2ReconnectIdentity();
54391
- }
54392
- const reconnectDeviceInfo = currentDeviceInfo !== null && currentDeviceInfo !== void 0 ? currentDeviceInfo : deviceInfo;
54393
- if (!reconnectDeviceInfo) {
54411
+ if (!currentDeviceInfo && !bleInstallLinkReady && !isFinalStatusCheck) {
54394
54412
  throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Protocol V2 device identity is unavailable during install polling');
54395
54413
  }
54396
- const isNormalMode = yield this.probeProtocolV2NormalMode(reconnectDeviceInfo);
54414
+ deviceInfo = yield this.verifyProtocolV2ReconnectIdentity(PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST);
54415
+ const isNormalMode = yield this.probeProtocolV2NormalMode(deviceInfo);
54397
54416
  if (isNormalMode &&
54398
54417
  (installEvidenceObserved ||
54399
- this.hasProtocolV2InstallVersionChanged(expectedTargetIds))) {
54418
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds, deviceInfo))) {
54400
54419
  Log$6.log('[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot');
54401
54420
  this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
54402
54421
  this.postProgressMessage(100, 'installingFirmware');
@@ -54437,6 +54456,8 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54437
54456
  bleInstallLinkReady = false;
54438
54457
  Log$6.log('Protocol V2 firmware install device readiness probe failed: ', error);
54439
54458
  }
54459
+ if (isFinalStatusCheck)
54460
+ break;
54440
54461
  yield hdShared.wait(1000);
54441
54462
  }
54442
54463
  Log$6.error(`[FirmwareUpdateV4] install timed out after ${PROTOCOL_V2_INSTALL_TIMEOUT / 1000}s: ${this.normalizeErrorMessage(lastError)}`);
@@ -54467,7 +54488,7 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54467
54488
  }
54468
54489
  probeProtocolV2NormalMode(deviceInfo) {
54469
54490
  return __awaiter(this, void 0, void 0, function* () {
54470
- const features = yield this.device.probeProtocolV2RuntimeState(deviceInfo, PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT);
54491
+ const features = yield this.device.probeProtocolV2RuntimeState(deviceInfo, PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT, { forceRuntimeContextRefresh: true });
54471
54492
  this.protocolV2LastRuntimeProbeFeatures = features;
54472
54493
  return this.isProtocolV2ApplicationMode(features);
54473
54494
  });
@@ -54627,6 +54648,7 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54627
54648
  var _a;
54628
54649
  return __awaiter(this, void 0, void 0, function* () {
54629
54650
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
54651
+ const env = DataManager.getSettings('env');
54630
54652
  const writeRes = yield typedCall('FilesystemFileWrite', 'FilesystemFile', {
54631
54653
  file: {
54632
54654
  path: filePath,
@@ -54638,6 +54660,7 @@ class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod {
54638
54660
  append: false,
54639
54661
  ui_percentage: progress !== null && progress !== void 0 ? progress : undefined,
54640
54662
  }, {
54663
+ timeoutMs: env === 'react-native' ? PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT : undefined,
54641
54664
  writeWithResponse: false,
54642
54665
  onWriteCompleted: () => undefined,
54643
54666
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.6-alpha.0",
3
+ "version": "1.2.6-alpha.2",
4
4
  "description": "Core processes and APIs for communicating with OneKey hardware devices.",
5
5
  "author": "OneKey",
6
6
  "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
25
25
  "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
26
26
  },
27
27
  "dependencies": {
28
- "@onekeyfe/hd-shared": "1.2.6-alpha.0",
29
- "@onekeyfe/hd-transport": "1.2.6-alpha.0",
28
+ "@onekeyfe/hd-shared": "1.2.6-alpha.2",
29
+ "@onekeyfe/hd-transport": "1.2.6-alpha.2",
30
30
  "axios": "1.15.2",
31
31
  "bignumber.js": "^9.0.2",
32
32
  "buffer": "^6.0.3",
@@ -46,5 +46,5 @@
46
46
  "@types/w3c-web-usb": "^1.0.10",
47
47
  "@types/web-bluetooth": "^0.0.21"
48
48
  },
49
- "gitHead": "9aa91a0780c3169164da575deadf5d256013d416"
49
+ "gitHead": "c01014c750c5c0bc4f6bff2e07b2fee985893f59"
50
50
  }
@@ -70,7 +70,7 @@ import type {
70
70
  FirmwareUpdateV4Target,
71
71
  } from '../types/api/firmwareUpdate';
72
72
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
73
- import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
73
+ import type { DeviceInfoGet, ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
74
74
  import type { TypedResponseMessage } from '../device/DeviceCommands';
75
75
  import type {
76
76
  Features,
@@ -91,7 +91,9 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
91
91
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
92
92
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
93
93
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
94
- const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
94
+ // React Native invalidates a timed-out BLE link, and V4 can safely replay the staging file.
95
+ const PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT = 10 * 1000;
96
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 10 * 60 * 1000;
95
97
  const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
96
98
  const PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS = 4;
97
99
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
@@ -577,6 +579,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
577
579
 
578
580
  private protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
579
581
 
582
+ private protocolV2InitialDeviceInfo?: ProtocolV2DeviceInfo;
583
+
580
584
  private protocolV2InstallBaselineVersions = new Map<number, string>();
581
585
 
582
586
  private protocolV2InstallNeedsReconnect = false;
@@ -814,8 +818,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
814
818
  private async runProtocolV2() {
815
819
  await this.captureProtocolV2PhysicalIdentity();
816
820
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
817
- this.protocolV2InstallBaselineVersions =
818
- this.getProtocolV2ObservableTargetVersions(deviceFeatures);
821
+ this.protocolV2InstallBaselineVersions = this.getProtocolV2ObservableTargetVersions(
822
+ deviceFeatures,
823
+ this.protocolV2InitialDeviceInfo
824
+ );
819
825
  this.protocolV2LastRuntimeProbeFeatures = undefined;
820
826
  const currentDeviceType = this.device.getCurrentDeviceType();
821
827
  const capabilityDeviceType =
@@ -1382,7 +1388,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1382
1388
  app_v1: features
1383
1389
  ? getDeviceFirmwareVersion(features).join('.')
1384
1390
  : deviceInfo?.main_mcu?.application?.version,
1385
- app_v2: deviceInfo?.main_mcu?.application_data?.version,
1391
+ // Production boot verifies P2 against P1 before normal firmware can run.
1392
+ app_v2:
1393
+ features?.mode === 'normal'
1394
+ ? getDeviceFirmwareVersion(features).join('.')
1395
+ : deviceInfo?.main_mcu?.application_data?.version,
1386
1396
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
1387
1397
  se01: features?.se01Version ?? undefined,
1388
1398
  se02: features?.se02Version ?? undefined,
@@ -1511,10 +1521,15 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1511
1521
  }
1512
1522
  this.protocolV2ExpectedSerialNumber = serialNumber;
1513
1523
  this.protocolV2ExpectedPath = path;
1524
+ this.protocolV2InitialDeviceInfo = deviceInfo;
1514
1525
  }
1515
1526
 
1516
- private async verifyProtocolV2ReconnectIdentity() {
1517
- const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
1527
+ private async verifyProtocolV2ReconnectIdentity(request?: DeviceInfoGet) {
1528
+ const deviceInfo = await requestProtocolV2DeviceInfo({
1529
+ commands: this.device.getCommands(),
1530
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1531
+ request,
1532
+ });
1518
1533
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1519
1534
  return deviceInfo;
1520
1535
  }
@@ -2505,7 +2520,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2505
2520
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2506
2521
  }
2507
2522
 
2508
- private getProtocolV2ObservableTargetVersions(features: Features) {
2523
+ private getProtocolV2ObservableTargetVersions(
2524
+ features: Features,
2525
+ deviceInfo?: ProtocolV2DeviceInfo
2526
+ ) {
2509
2527
  const versions = new Map<number, string>();
2510
2528
  versions.set(
2511
2529
  ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
@@ -2513,7 +2531,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2513
2531
  );
2514
2532
  const applicationVersion = getDeviceFirmwareVersion(features).join('.');
2515
2533
  versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
2516
- versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
2534
+ const applicationP2Version =
2535
+ features.mode === 'normal'
2536
+ ? applicationVersion
2537
+ : deviceInfo?.main_mcu?.application_data?.version;
2538
+ if (applicationP2Version) {
2539
+ versions.set(
2540
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2,
2541
+ applicationP2Version
2542
+ );
2543
+ }
2517
2544
  versions.set(
2518
2545
  ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
2519
2546
  getDeviceBLEFirmwareVersion(features).join('.')
@@ -2530,20 +2557,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2530
2557
  return versions;
2531
2558
  }
2532
2559
 
2533
- private hasProtocolV2InstallVersionChanged(expectedTargetIds: Set<number>) {
2560
+ private hasProtocolV2InstallVersionChanged(
2561
+ expectedTargetIds: Set<number>,
2562
+ deviceInfo: ProtocolV2DeviceInfo
2563
+ ) {
2534
2564
  if (!this.protocolV2LastRuntimeProbeFeatures) return false;
2535
2565
  const currentVersions = this.getProtocolV2ObservableTargetVersions(
2536
- this.protocolV2LastRuntimeProbeFeatures
2566
+ this.protocolV2LastRuntimeProbeFeatures,
2567
+ deviceInfo
2568
+ );
2569
+ const p1TargetId = ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1;
2570
+ const p2TargetId = ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2;
2571
+ return (
2572
+ expectedTargetIds.size > 0 &&
2573
+ Array.from(expectedTargetIds).every(targetId => {
2574
+ if (targetId === p2TargetId && this.protocolV2LastRuntimeProbeFeatures?.mode === 'normal') {
2575
+ // P1 must change in this install; a pre-existing P1 version cannot prove a P2-only update.
2576
+ if (!expectedTargetIds.has(p1TargetId)) return false;
2577
+ const previousP1Version = this.protocolV2InstallBaselineVersions.get(p1TargetId);
2578
+ const currentP1Version = currentVersions.get(p1TargetId);
2579
+ return (
2580
+ previousP1Version !== undefined &&
2581
+ currentP1Version !== undefined &&
2582
+ previousP1Version !== currentP1Version &&
2583
+ currentP1Version === this.params?.expectedTargetVersions?.app_v2
2584
+ );
2585
+ }
2586
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2587
+ const currentVersion = currentVersions.get(targetId);
2588
+ return (
2589
+ previousVersion !== undefined &&
2590
+ currentVersion !== undefined &&
2591
+ previousVersion !== currentVersion
2592
+ );
2593
+ })
2537
2594
  );
2538
- return Array.from(expectedTargetIds).some(targetId => {
2539
- const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2540
- const currentVersion = currentVersions.get(targetId);
2541
- return (
2542
- previousVersion !== undefined &&
2543
- currentVersion !== undefined &&
2544
- previousVersion !== currentVersion
2545
- );
2546
- });
2547
2595
  }
2548
2596
 
2549
2597
  private recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds: Set<number>) {
@@ -2588,10 +2636,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2588
2636
  finishedStatusSnapshotPolls = 0;
2589
2637
  };
2590
2638
 
2591
- while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2639
+ for (;;) {
2592
2640
  // A transport release caused by an explicit workflow cancellation must not
2593
2641
  // be mistaken for the expected device reboot during installation.
2594
2642
  this.throwIfAborted();
2643
+ // Read once more at the deadline so a completed reboot is not reported as a timeout.
2644
+ const isFinalStatusCheck = Date.now() - startTime >= PROTOCOL_V2_INSTALL_TIMEOUT;
2595
2645
  try {
2596
2646
  if (shouldReconnect) {
2597
2647
  const isBleInstallReconnect = this.isBleReconnect();
@@ -2750,22 +2800,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2750
2800
 
2751
2801
  if (
2752
2802
  statusTargets.length === 0 &&
2753
- !currentDeviceInfo &&
2754
- bleInstallLinkReady &&
2755
- installEvidenceObserved
2803
+ (currentDeviceInfo || bleInstallLinkReady || isFinalStatusCheck)
2756
2804
  ) {
2757
- // BLE install reconnect skips generic probes because loaders may not answer Ping.
2758
- // Restore the verified identity only after current-install evidence disappears.
2759
- currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2805
+ // Installation can finish while BLE is disconnected, hiding all progress.
2806
+ // Refresh identity and target versions even without observed install status.
2807
+ currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity(
2808
+ PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST
2809
+ );
2760
2810
  deviceInfo = currentDeviceInfo;
2761
- }
2762
-
2763
- if (statusTargets.length === 0 && currentDeviceInfo) {
2764
2811
  const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2765
2812
  if (
2766
2813
  isNormalMode &&
2767
2814
  (installEvidenceObserved ||
2768
- this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2815
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds, currentDeviceInfo))
2769
2816
  ) {
2770
2817
  Log.log(
2771
2818
  '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
@@ -2795,33 +2842,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2795
2842
  }
2796
2843
  }
2797
2844
  } catch (error) {
2798
- if (isProtocolV2TerminalInstallStatusError(error)) {
2845
+ if (
2846
+ this.isProtocolV2ReconnectIdentityError(error) ||
2847
+ isProtocolV2TerminalInstallStatusError(error)
2848
+ ) {
2799
2849
  throw error;
2800
2850
  }
2801
- // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
2802
- // missing endpoint as completion only after the runtime probe confirms App mode.
2851
+ // Some firmware builds do not expose install status. Require a fresh runtime
2852
+ // probe and current-install evidence or changed versions before accepting completion.
2803
2853
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2804
- if (!currentDeviceInfo) {
2805
- if (!bleInstallLinkReady) {
2806
- throw ERRORS.TypedError(
2807
- HardwareErrorCode.RuntimeError,
2808
- 'Protocol V2 device identity is unavailable during install polling'
2809
- );
2810
- }
2811
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2812
- }
2813
- const reconnectDeviceInfo = currentDeviceInfo ?? deviceInfo;
2814
- if (!reconnectDeviceInfo) {
2854
+ if (!currentDeviceInfo && !bleInstallLinkReady && !isFinalStatusCheck) {
2815
2855
  throw ERRORS.TypedError(
2816
2856
  HardwareErrorCode.RuntimeError,
2817
2857
  'Protocol V2 device identity is unavailable during install polling'
2818
2858
  );
2819
2859
  }
2820
- const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2860
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity(
2861
+ PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST
2862
+ );
2863
+ const isNormalMode = await this.probeProtocolV2NormalMode(deviceInfo);
2821
2864
  if (
2822
2865
  isNormalMode &&
2823
2866
  (installEvidenceObserved ||
2824
- this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2867
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds, deviceInfo))
2825
2868
  ) {
2826
2869
  Log.log(
2827
2870
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
@@ -2871,6 +2914,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2871
2914
  bleInstallLinkReady = false;
2872
2915
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2873
2916
  }
2917
+ if (isFinalStatusCheck) break;
2874
2918
  await wait(1000);
2875
2919
  }
2876
2920
 
@@ -2909,7 +2953,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2909
2953
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
2910
2954
  const features = await this.device.probeProtocolV2RuntimeState(
2911
2955
  deviceInfo,
2912
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2956
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2957
+ { forceRuntimeContextRefresh: true }
2913
2958
  );
2914
2959
  this.protocolV2LastRuntimeProbeFeatures = features;
2915
2960
  return this.isProtocolV2ApplicationMode(features);
@@ -3132,6 +3177,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3132
3177
  progress: number | null
3133
3178
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
3134
3179
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
3180
+ const env = DataManager.getSettings('env');
3135
3181
  const writeRes = await typedCall(
3136
3182
  'FilesystemFileWrite',
3137
3183
  'FilesystemFile',
@@ -3147,6 +3193,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3147
3193
  ui_percentage: progress ?? undefined,
3148
3194
  },
3149
3195
  {
3196
+ timeoutMs: env === 'react-native' ? PROTOCOL_V2_FILE_WRITE_RESPONSE_TIMEOUT : undefined,
3150
3197
  writeWithResponse: false,
3151
3198
  onWriteCompleted: () => undefined,
3152
3199
  }
@@ -271,7 +271,8 @@ export const mapProtocolV2DeviceInfoToState = (
271
271
  versions: definedEntries({
272
272
  firmware: imageVersion(info.main_mcu?.application),
273
273
  applicationP1: imageVersion(info.main_mcu?.application),
274
- applicationP2: imageVersion(info.main_mcu?.application_data),
274
+ // The running application does not report P2; discard a loader-era version.
275
+ applicationP2: mode === 'normal' ? null : imageVersion(info.main_mcu?.application_data),
275
276
  bootloader: imageVersion(info.main_mcu?.bootloader),
276
277
  board: imageVersion(info.main_mcu?.romloader),
277
278
  ble: imageVersion(info.coprocessor?.application),
@@ -289,8 +290,9 @@ export const mapProtocolV2DeviceInfoToState = (
289
290
  firmwareHash: imageHash(info.main_mcu?.application),
290
291
  applicationP1BuildId: imageBuildId(info.main_mcu?.application),
291
292
  applicationP1Hash: imageHash(info.main_mcu?.application),
292
- applicationP2BuildId: imageBuildId(info.main_mcu?.application_data),
293
- applicationP2Hash: imageHash(info.main_mcu?.application_data),
293
+ applicationP2BuildId:
294
+ mode === 'normal' ? null : imageBuildId(info.main_mcu?.application_data),
295
+ applicationP2Hash: mode === 'normal' ? null : imageHash(info.main_mcu?.application_data),
294
296
  bootloaderBuildId: imageBuildId(info.main_mcu?.bootloader),
295
297
  bootloaderHash: imageHash(info.main_mcu?.bootloader),
296
298
  boardBuildId: imageBuildId(info.main_mcu?.romloader),