@onekeyfe/hd-core 1.2.0-alpha.134 → 1.2.0-alpha.135

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 (31) hide show
  1. package/__tests__/DeviceCommands.test.ts +8 -1
  2. package/__tests__/device-lifecycle-events.test.ts +95 -2
  3. package/__tests__/device-settings.test.ts +167 -9
  4. package/__tests__/device-wallet-session-store.test.ts +21 -79
  5. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +37 -1
  6. package/__tests__/protocol-v2-unlock-policy.test.ts +59 -0
  7. package/__tests__/protocol-v2.test.ts +86 -21
  8. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  9. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  10. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  11. package/dist/api/device/DeviceVerify.d.ts +1 -0
  12. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  13. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  14. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  15. package/dist/core/index.d.ts +3 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/device/Device.d.ts +2 -0
  18. package/dist/device/Device.d.ts.map +1 -1
  19. package/dist/device/DeviceCommands.d.ts.map +1 -1
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.js +296 -247
  22. package/package.json +4 -4
  23. package/src/api/FirmwareUpdateV4.ts +25 -68
  24. package/src/api/device/DeviceChangePin.ts +4 -1
  25. package/src/api/device/DeviceSettings.ts +2 -16
  26. package/src/api/device/DeviceVerify.ts +9 -0
  27. package/src/api/device/DeviceWipe.ts +4 -1
  28. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +11 -0
  29. package/src/core/index.ts +67 -25
  30. package/src/device/Device.ts +45 -42
  31. package/src/device/DeviceCommands.ts +15 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/hd-core",
3
- "version": "1.2.0-alpha.134",
3
+ "version": "1.2.0-alpha.135",
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.0-alpha.134",
29
- "@onekeyfe/hd-transport": "1.2.0-alpha.134",
28
+ "@onekeyfe/hd-shared": "1.2.0-alpha.135",
29
+ "@onekeyfe/hd-transport": "1.2.0-alpha.135",
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": "47de2b0244584f6a541a84b0f94e4676bf63221a"
49
+ "gitHead": "321e900381de2c1a9c583e90e8abfc2f8dab8266"
50
50
  }
@@ -2,6 +2,7 @@ import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@on
2
2
  import JSZip from 'jszip';
3
3
  import {
4
4
  DeviceRebootType,
5
+ DeviceSessionPinType,
5
6
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
6
7
  PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
7
8
  PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
@@ -90,9 +91,8 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
90
91
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
91
92
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
92
93
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
93
- const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
94
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
94
95
  const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
95
- const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
96
96
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
97
97
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
98
98
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -106,6 +106,8 @@ const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
106
106
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
107
107
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
108
108
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
109
+ const PROTOCOL_V2_INSTALL_FAILED_CODE = 'FirmwareInstallFailed';
110
+ const PROTOCOL_V2_INSTALL_TIMEOUT_CODE = 'FirmwareInstallTimeout';
109
111
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
110
112
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
111
113
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
@@ -391,7 +393,8 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
391
393
 
392
394
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
393
395
  error instanceof HardwareError &&
394
- (error.errorCode === HardwareErrorCode.FirmwareError ||
396
+ (error.errorCode === HardwareErrorCode.ActionCancelled ||
397
+ error.errorCode === HardwareErrorCode.FirmwareError ||
395
398
  error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
396
399
  error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
397
400
 
@@ -599,6 +602,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
599
602
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
600
603
  this.requireDeviceMode = [];
601
604
  this.unlockPolicy = 'unlock-before-run';
605
+ // Protocol V2 device-management actions are not wallet-scoped, so either
606
+ // the main PIN or an Attach PIN may authorize them.
607
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
602
608
  this.useDevicePassphraseState = false;
603
609
  this.skipForceUpdateCheck = true;
604
610
 
@@ -1623,9 +1629,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1623
1629
  Math.floor(statusVersion / 0x100) % 0x100
1624
1630
  }.${statusVersion % 0x100}`;
1625
1631
  if (!observedVersion) {
1626
- if (this.protocolV2FinalStatusVerified) {
1632
+ const isUnobservableApplicationSlot =
1633
+ applicationTargets.length > 1 && (target === 'app_v1' || target === 'app_v2');
1634
+ if (this.protocolV2FinalStatusVerified || isUnobservableApplicationSlot) {
1627
1635
  Log.warn(
1628
- `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
1636
+ `Protocol V2 target ${target} has no observable final version; install completion is authoritative`
1629
1637
  );
1630
1638
  } else {
1631
1639
  throw ERRORS.TypedError(
@@ -2499,14 +2507,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2499
2507
  path: failedTarget.path,
2500
2508
  });
2501
2509
  Log.error(`[FirmwareUpdateV4] firmware install failed target=${failedTargetDetails}`);
2502
- throw ERRORS.TypedError(
2503
- HardwareErrorCode.FirmwareError,
2504
- `Protocol V2 firmware target failed: target=${failedTarget.target_id} status=${
2505
- failedTarget.status ?? 'unknown'
2506
- } payloadVersion=${failedTarget.payload_version ?? 'unknown'} path=${
2507
- failedTarget.path ?? 'unknown'
2508
- }`
2509
- );
2510
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2511
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2512
+ });
2510
2513
  }
2511
2514
 
2512
2515
  const matchingTargets = statusTargets.filter(target =>
@@ -2520,13 +2523,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2520
2523
  seenTargetIds.has(targetId) ||
2521
2524
  (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2522
2525
  ) {
2523
- throw ERRORS.TypedError(
2524
- HardwareErrorCode.RuntimeError,
2525
- `Protocol V2 install status conflicts with target ${target.target_id}`,
2526
- {
2527
- firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2528
- }
2529
- );
2526
+ Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2527
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2528
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2529
+ });
2530
2530
  }
2531
2531
  seenTargetIds.add(targetId);
2532
2532
  }
@@ -2651,15 +2651,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2651
2651
  // Reconnecting here probes DeviceInfo, which loaders reject while installation is active.
2652
2652
  let shouldReconnect = false;
2653
2653
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2654
- let missingTargetStatusSince: number | undefined;
2655
- let missingTargetStatusKey: string | undefined;
2656
- let normalModeWithoutInstallEvidenceSince: number | undefined;
2657
2654
  let installEvidenceObserved = false;
2658
2655
  let currentInstallStatusObserved = false;
2659
- const resetMissingTargetStatusGrace = () => {
2660
- missingTargetStatusSince = undefined;
2661
- missingTargetStatusKey = undefined;
2662
- };
2663
2656
 
2664
2657
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2665
2658
  // A transport release caused by an explicit workflow cancellation must not
@@ -2687,7 +2680,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2687
2680
  );
2688
2681
  if (statusResponse.type === 'Success') {
2689
2682
  installEvidenceObserved = true;
2690
- resetMissingTargetStatusGrace();
2691
2683
  lastError = new Error(
2692
2684
  'Protocol V2 firmware install acknowledged; waiting for target status'
2693
2685
  );
@@ -2716,7 +2708,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2716
2708
  (!requireCurrentInstallStatus || currentInstallStatusObserved)
2717
2709
  ) {
2718
2710
  installEvidenceObserved = true;
2719
- normalModeWithoutInstallEvidenceSince = undefined;
2720
2711
  }
2721
2712
  const matchingStatusTargets = statusTargets.filter(target => {
2722
2713
  const targetId = normalizeProtocolV2TargetId(target.target_id);
@@ -2758,7 +2749,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2758
2749
  }
2759
2750
 
2760
2751
  if (statusTargets.length === 0) {
2761
- resetMissingTargetStatusGrace();
2762
2752
  if (statusResponse.type !== 'Success') {
2763
2753
  lastError = new Error(
2764
2754
  'Protocol V2 firmware update is waiting for user confirmation or target status'
@@ -2770,30 +2760,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2770
2760
  expectedTargetIds
2771
2761
  );
2772
2762
  if (missingTargetIds.length > 0) {
2773
- const now = Date.now();
2774
2763
  const missingKey = missingTargetIds.join(',');
2775
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2776
- missingTargetStatusSince = now;
2777
- missingTargetStatusKey = missingKey;
2778
- } else if (
2779
- now - missingTargetStatusSince >=
2780
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2781
- ) {
2782
- const reportedTargetIds = statusTargets
2783
- .map(target => normalizeProtocolV2TargetId(target.target_id))
2784
- .filter((targetId): targetId is number => targetId !== undefined);
2785
- throw ERRORS.TypedError(
2786
- HardwareErrorCode.FirmwareError,
2787
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2788
- ','
2789
- )}`
2790
- );
2791
- }
2792
2764
  lastError = new Error(
2793
2765
  `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2794
2766
  );
2795
2767
  } else {
2796
- resetMissingTargetStatusGrace();
2797
2768
  lastError = new Error('Protocol V2 firmware targets are still installing');
2798
2769
  }
2799
2770
  }
@@ -2801,9 +2772,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2801
2772
  if (isProtocolV2TerminalInstallStatusError(error)) {
2802
2773
  throw error;
2803
2774
  }
2804
- // Only consecutive full status dumps can prove that a record is absent.
2805
- // A reboot or transport interruption starts a fresh grace window.
2806
- resetMissingTargetStatusGrace();
2807
2775
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
2808
2776
  // missing endpoint as completion only after the runtime probe confirms App mode.
2809
2777
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
@@ -2826,22 +2794,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2826
2794
  return;
2827
2795
  }
2828
2796
  if (isNormalMode) {
2829
- const now = Date.now();
2830
- normalModeWithoutInstallEvidenceSince ??= now;
2831
- if (
2832
- now - normalModeWithoutInstallEvidenceSince >=
2833
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2834
- ) {
2835
- throw ERRORS.TypedError(
2836
- HardwareErrorCode.FirmwareError,
2837
- 'Protocol V2 device returned to normal mode without install ACK, target status, or version change'
2838
- );
2839
- }
2840
2797
  lastError = new Error(
2841
2798
  'Protocol V2 device is in normal mode but installation is not yet confirmed'
2842
2799
  );
2843
2800
  } else {
2844
- normalModeWithoutInstallEvidenceSince = undefined;
2845
2801
  lastError = new Error(
2846
2802
  'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2847
2803
  );
@@ -2872,18 +2828,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2872
2828
  }
2873
2829
  shouldReconnect = true;
2874
2830
  deviceInfo = undefined;
2875
- resetMissingTargetStatusGrace();
2876
2831
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2877
2832
  }
2878
2833
  await wait(1000);
2879
2834
  }
2880
2835
 
2881
- throw ERRORS.TypedError(
2882
- HardwareErrorCode.RuntimeError,
2883
- `Protocol V2 firmware update status not complete within ${
2836
+ Log.error(
2837
+ `[FirmwareUpdateV4] install timed out after ${
2884
2838
  PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2885
2839
  }s: ${this.normalizeErrorMessage(lastError)}`
2886
2840
  );
2841
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareError, undefined, {
2842
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_TIMEOUT_CODE,
2843
+ });
2887
2844
  }
2888
2845
 
2889
2846
  private async exitProtocolV2BootloaderToNormal() {
@@ -1,4 +1,4 @@
1
- import { DeviceSettingsPage } from '@onekeyfe/hd-transport';
1
+ import { DeviceSessionPinType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
2
2
 
3
3
  import { BaseMethod } from '../BaseMethod';
4
4
  import { validateParams } from '../helpers/paramsValidator';
@@ -13,6 +13,9 @@ export default class DeviceChangePin extends BaseMethod<ChangePin> {
13
13
 
14
14
  init() {
15
15
  this.unlockPolicy = 'unlock-before-run';
16
+ // Protocol V2 device-management actions are not wallet-scoped, so either
17
+ // the main PIN or an Attach PIN may authorize them.
18
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
16
19
  this.useDevicePassphraseState = false;
17
20
 
18
21
  // check payload
@@ -168,8 +168,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
168
168
  async run() {
169
169
  try {
170
170
  if (this.device.isProtocolV2()) {
171
- const refreshStatusAndSettings = () =>
172
- this.device.getDeviceState({ refreshSections: ['status', 'settings'] });
171
+ const refreshStatusAndSettings = () => this.device.refreshProtocolV2SettingsAfterMutation();
173
172
  assertSettingsSupported(this.payload, DEVICE_SETTINGS_V1_ONLY_FIELDS, 'Protocol V2');
174
173
  const capabilities = getDeviceSettingsCapabilities(
175
174
  this.device.getCurrentDeviceType(),
@@ -201,20 +200,7 @@ export default class DeviceSettings extends BaseMethod<ApplySettings> {
201
200
  const res = await this.device.commands.typedCall('DeviceSettingsPageShow', 'Success', {
202
201
  page: DeviceSettingsPage.DevicePassphrase,
203
202
  });
204
- const updated = await refreshStatusAndSettings();
205
- const lockedAfterDisabling =
206
- requestedPassphrase === false &&
207
- current.status.unlocked === true &&
208
- updated.status.unlocked === false;
209
- if (
210
- updated.status.passphraseProtection !== requestedPassphrase &&
211
- !lockedAfterDisabling
212
- ) {
213
- throw TypedError(
214
- HardwareErrorCode.RuntimeError,
215
- 'Protocol V2 passphrase setting did not reach the requested value.'
216
- );
217
- }
203
+ await refreshStatusAndSettings();
218
204
  return res.message;
219
205
  }
220
206
  if (requestedAirgap !== undefined) {
@@ -1,5 +1,6 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
2
  import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
3
+ import { DeviceSessionPinType } from '@onekeyfe/hd-transport';
3
4
  import { bytesToHex } from '@noble/hashes/utils';
4
5
 
5
6
  import { formatAnyHex } from '../helpers/hexUtils';
@@ -11,7 +12,15 @@ import type { BixinVerifyDeviceRequest } from '@onekeyfe/hd-transport';
11
12
  import type { DeviceVerifySignature } from '../../types';
12
13
 
13
14
  export default class DeviceVerify extends BaseMethod<BixinVerifyDeviceRequest> {
15
+ getSupportedProtocols() {
16
+ return ['V1', 'V2'] as const;
17
+ }
18
+
14
19
  init() {
20
+ this.unlockPolicy = 'unlock-before-run';
21
+ // Protocol V2 device-management actions are not wallet-scoped, so either
22
+ // the main PIN or an Attach PIN may authorize them.
23
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
15
24
  this.useDevicePassphraseState = false;
16
25
 
17
26
  // check payload
@@ -1,4 +1,4 @@
1
- import { DeviceSettingsPage } from '@onekeyfe/hd-transport';
1
+ import { DeviceSessionPinType, DeviceSettingsPage } from '@onekeyfe/hd-transport';
2
2
 
3
3
  import { BaseMethod } from '../BaseMethod';
4
4
 
@@ -11,6 +11,9 @@ export default class DeviceWipe extends BaseMethod<WipeDevice> {
11
11
 
12
12
  init() {
13
13
  this.unlockPolicy = 'unlock-before-run';
14
+ // Protocol V2 device-management actions are not wallet-scoped, so either
15
+ // the main PIN or an Attach PIN may authorize them.
16
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
14
17
  this.useDevicePassphraseState = false;
15
18
  this.protocolV2UiInteraction = {
16
19
  request: 'button',
@@ -8,6 +8,7 @@ import { invalidParameter } from '../helpers/filesystemValidation';
8
8
  import { writeProtocolV2File } from '../helpers/protocolV2FileWrite';
9
9
  import { UI_REQUEST, createUiMessage } from '../../events/ui-request';
10
10
  import { supportsProtocolV2Message } from '../../protocols/protocol-v2/features';
11
+ import { LoggerNames, getLogger } from '../../utils';
11
12
  import {
12
13
  PRO2_WALLPAPER_HEIGHT,
13
14
  PRO2_WALLPAPER_WIDTH,
@@ -33,6 +34,7 @@ const SAFE_FILE_NAME = /^[A-Za-z0-9_-]+(?:\.bin)?$/;
33
34
  const DEVICE_SETTINGS_SET_MESSAGE_TYPE = 60412;
34
35
  const FILESYSTEM_FILE_WRITE_MESSAGE_TYPE = 60805;
35
36
  const FILESYSTEM_DIR_MAKE_MESSAGE_TYPE = 60809;
37
+ const Log = getLogger(LoggerNames.Method);
36
38
 
37
39
  function normalizeFileName(fileName: string | undefined, data: Uint8Array): string {
38
40
  if (fileName !== undefined && (!fileName || !SAFE_FILE_NAME.test(fileName))) {
@@ -143,6 +145,15 @@ export default class DeviceUploadWallpaper extends BaseMethod<DeviceUploadWallpa
143
145
  const response = await this.device.commands.typedCall('DeviceSettingsSet', 'Success', {
144
146
  settings: { wallpaper_path: this.path },
145
147
  });
148
+ try {
149
+ await this.device.refreshProtocolV2SettingsAfterMutation();
150
+ } catch (error) {
151
+ // The wallpaper is already applied. A transient read-back failure must not
152
+ // make callers retry the completed file upload.
153
+ Log.warn('Protocol V2 wallpaper settings refresh failed after apply', {
154
+ error: error instanceof Error ? error.message : String(error),
155
+ });
156
+ }
146
157
  return {
147
158
  path: this.path,
148
159
  size: encoded.data.byteLength,
package/src/core/index.ts CHANGED
@@ -83,6 +83,7 @@ import type { BaseMethod } from '../api/BaseMethod';
83
83
 
84
84
  const Log = getLogger(LoggerNames.Core);
85
85
  const PRE_INITIALIZE_TTL_MS = 60 * 1000;
86
+ const PRE_PENDING_CALL_TIMEOUT_MS = 15 * 1000;
86
87
 
87
88
  // Dedup/coalesce state for "pre-warm signal" methods (isPreWarmSignal),
88
89
  // keyed by getPreWarmKey(): coalesce in-flight, skip if warmed within TTL.
@@ -296,26 +297,35 @@ const handlePreWarmSignal = async (
296
297
  }
297
298
  };
298
299
 
299
- const waitWithTimeout = async (promise: Promise<any>, timeout: number) => {
300
- const timeoutPromise = new Promise((_, reject) => {
301
- setTimeout(() => reject(new Error('Request timeout')), timeout);
302
- });
303
- return Promise.race([promise, timeoutPromise]);
304
- };
305
-
306
300
  const waitForPendingPromise = async (
307
- getPrePendingCallPromise: () => Promise<void> | undefined,
308
- removePrePendingCallPromise?: (promise: Promise<void> | undefined) => void
301
+ connectId: string,
302
+ getPrePendingCallPromise: (connectId: string) => Promise<void> | undefined,
303
+ removePrePendingCallPromise?: (connectId: string, promise: Promise<void>) => void
309
304
  ) => {
310
- const pendingPromise = getPrePendingCallPromise();
305
+ const pendingPromise = getPrePendingCallPromise(connectId);
311
306
  if (pendingPromise) {
312
307
  Log.debug('pre pending call promise before call method, wait for it');
308
+ let timer: ReturnType<typeof setTimeout> | undefined;
309
+ let timedOut = false;
313
310
  try {
314
- await waitWithTimeout(pendingPromise, 5 * 1000);
311
+ await Promise.race([
312
+ pendingPromise,
313
+ new Promise<void>(resolve => {
314
+ timer = setTimeout(() => {
315
+ timedOut = true;
316
+ resolve();
317
+ }, PRE_PENDING_CALL_TIMEOUT_MS);
318
+ }),
319
+ ]);
315
320
  } catch (error) {
316
- // ignore timeout error
321
+ // Cancellation is best-effort; the transport teardown owns recovery.
322
+ } finally {
323
+ if (timer) clearTimeout(timer);
324
+ removePrePendingCallPromise?.(connectId, pendingPromise);
325
+ }
326
+ if (timedOut) {
327
+ Log.warn('pre pending call promise timed out before call method', { connectId });
317
328
  }
318
- removePrePendingCallPromise?.(pendingPromise);
319
329
  Log.debug('pre pending call promise before call method done');
320
330
  }
321
331
  };
@@ -327,7 +337,7 @@ const onCallDevice = async (
327
337
  ): Promise<any> => {
328
338
  let messageResponse: any;
329
339
 
330
- const { requestQueue, getPrePendingCallPromise, setPrePendingCallPromise } = context;
340
+ const { requestQueue, getPrePendingCallPromise, removePrePendingCallPromise } = context;
331
341
 
332
342
  updateMethodRequestContext(method, { status: 'running' });
333
343
 
@@ -355,7 +365,11 @@ const onCallDevice = async (
355
365
  await context.waitForCallbackTasks(method.connectId);
356
366
  }
357
367
 
358
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
368
+ await waitForPendingPromise(
369
+ method.connectId ?? '',
370
+ getPrePendingCallPromise,
371
+ removePrePendingCallPromise
372
+ );
359
373
 
360
374
  const task = requestQueue.createTask(method);
361
375
 
@@ -454,7 +468,11 @@ const onCallDevice = async (
454
468
  await context.waitForCallbackTasks(method.connectId, preWarmCallbackTask);
455
469
  }
456
470
 
457
- await waitForPendingPromise(getPrePendingCallPromise, setPrePendingCallPromise);
471
+ await waitForPendingPromise(
472
+ method.connectId ?? '',
473
+ getPrePendingCallPromise,
474
+ removePrePendingCallPromise
475
+ );
458
476
 
459
477
  const inner = async (): Promise<void> => {
460
478
  // Protocol is established from an active device response during acquire/initialize.
@@ -906,6 +924,16 @@ function isRetryableBleProtocolV2ProbeError(method: BaseMethod, error: unknown)
906
924
  );
907
925
  }
908
926
 
927
+ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unknown) {
928
+ const typedError = error as { errorCode?: unknown; message?: unknown };
929
+ return (
930
+ method.payload.connectProtocol === 'V2' &&
931
+ typedError?.errorCode === HardwareErrorCode.RuntimeError &&
932
+ typeof typedError.message === 'string' &&
933
+ typedError.message.includes('Device protocol has not been detected')
934
+ );
935
+ }
936
+
909
937
  /**
910
938
  * If the Bluetooth connection times out, retry up to 6 times
911
939
  * @param retryCount - Current retry count (default 0)
@@ -943,12 +971,13 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
943
971
  DevicePool.emitter.emit(DEVICE.CONNECT, device);
944
972
  }
945
973
  } catch (err) {
974
+ const requiresColdReconnect = isMissingDetectedProtocolV2Error(method, err);
946
975
  // Device.run()'s REQUIRE_DISCONNECT handling never sees acquire/initialize
947
976
  // failures, so with keep-alive a wedged link would be reused by every retry
948
977
  // (field case: Initialize timing out at 25s per attempt, forever). Drop it
949
978
  // here so the next retry cold-connects.
950
979
  if (
951
- ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) &&
980
+ (ERROR_CODES_REQUIRE_DISCONNECT.includes(err.errorCode) || requiresColdReconnect) &&
952
981
  device.mainId &&
953
982
  device.deviceConnector
954
983
  ) {
@@ -961,11 +990,12 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
961
990
  if (
962
991
  (err.errorCode === HardwareErrorCode.BleTimeoutError ||
963
992
  err.errorCode === HardwareErrorCode.BleConnectedError ||
964
- isRetryableBleProtocolV2ProbeError(method, err)) &&
993
+ isRetryableBleProtocolV2ProbeError(method, err) ||
994
+ requiresColdReconnect) &&
965
995
  retryCount < 6
966
996
  ) {
967
997
  const nextRetry = retryCount + 1;
968
- Log.debug(`Bluetooth connect timeout and will retry, retry count: ${nextRetry}`);
998
+ Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
969
999
  await wait(3000);
970
1000
  await connectDeviceForBle(method, device, nextRetry);
971
1001
  } else {
@@ -1177,7 +1207,10 @@ export const cancel = (context: CoreContext, connectId?: string) => {
1177
1207
  if (task && task.method?.device) {
1178
1208
  if (!canceledDevices.includes(task.method.device)) {
1179
1209
  const { device } = task.method;
1180
- setPrePendingCallPromise(device?.interruptionFromUser());
1210
+ setPrePendingCallPromise(
1211
+ task.method.connectId ?? connectId,
1212
+ device.interruptionFromUser()
1213
+ );
1181
1214
  canceledDevices.push(device);
1182
1215
  }
1183
1216
  requestQueue.rejectRequest(
@@ -1531,7 +1564,7 @@ export default class Core extends EventEmitter {
1531
1564
  private disposePromise?: Promise<void>;
1532
1565
 
1533
1566
  // background task
1534
- private prePendingCallPromise: Promise<void> | undefined;
1567
+ private prePendingCallPromises = new Map<string, Promise<void>>();
1535
1568
 
1536
1569
  private methodSynchronize = getSynchronize();
1537
1570
 
@@ -1548,9 +1581,18 @@ export default class Core extends EventEmitter {
1548
1581
  tracingContext: this.tracingContext,
1549
1582
  requestQueue: this.requestQueue,
1550
1583
  methodSynchronize: this.methodSynchronize,
1551
- getPrePendingCallPromise: () => this.prePendingCallPromise,
1552
- setPrePendingCallPromise: (promise: Promise<void> | undefined) => {
1553
- this.prePendingCallPromise = promise;
1584
+ getPrePendingCallPromise: (connectId: string) => this.prePendingCallPromises.get(connectId),
1585
+ setPrePendingCallPromise: (connectId: string, promise?: Promise<void>) => {
1586
+ if (!promise) {
1587
+ this.prePendingCallPromises.delete(connectId);
1588
+ return;
1589
+ }
1590
+ this.prePendingCallPromises.set(connectId, promise);
1591
+ },
1592
+ removePrePendingCallPromise: (connectId: string, promise: Promise<void>) => {
1593
+ if (this.prePendingCallPromises.get(connectId) === promise) {
1594
+ this.prePendingCallPromises.delete(connectId);
1595
+ }
1554
1596
  },
1555
1597
  // callback 任务管理
1556
1598
  registerCallbackTask: (connectId: string, callbackPromise: Deferred<any>) => {
@@ -1665,7 +1707,7 @@ export default class Core extends EventEmitter {
1665
1707
  preWarmInflight.clear();
1666
1708
  preWarmDoneAt.clear();
1667
1709
  preConnectCache = { passphraseState: undefined };
1668
- this.prePendingCallPromise = undefined;
1710
+ this.prePendingCallPromises.clear();
1669
1711
  this.removeAllListeners();
1670
1712
  cleanupSdkInstance(this.sdkInstanceId);
1671
1713
  if (_core === this) _core = undefined;