@onekeyfe/hd-core 1.2.0-alpha.166 → 1.2.0-alpha.168

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/core/index.ts CHANGED
@@ -415,13 +415,6 @@ const onCallDevice = async (
415
415
  if (method.payload?.onlyConnectBleDevice) {
416
416
  preWarmCallbackTask?.resolve();
417
417
  Log.debug('Call API - only connect ble device: ', device?.mainId);
418
- // This early return bypasses the normal-path bookkeeping at the end of the
419
- // call. Without it the task leaks and haunts every later queue snapshot
420
- // and cancel sweep (field log: a completed task lingered for 6 minutes),
421
- // and the request stays in the active maps, so repeated preconnects pile
422
- // up phantom work in diagnostics.
423
- completeMethodRequestContext(method);
424
- requestQueue.releaseTask(method.responseID);
425
418
  return createResponseMessage(method.responseID, true, null);
426
419
  }
427
420
 
@@ -961,60 +954,7 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
961
954
  * If the Bluetooth connection times out, retry up to 6 times
962
955
  * @param retryCount - Current retry count (default 0)
963
956
  */
964
- // device.acquire awaits a transport reply with no deadline of its own; a
965
- // transport that never settles (field case: Electron main lost an IPC reply,
966
- // "reply was never sent" after 5 minutes) hangs the call forever and cancel()
967
- // only takes effect at poll checkpoints. Race acquire against a deadline and
968
- // the caller's abort signal so the hang is bounded and cancel is immediate.
969
- const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;
970
-
971
- function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
972
- return new Promise<T>((resolve, reject) => {
973
- let settled = false;
974
- const settle = (fn: () => void) => {
975
- if (settled) return;
976
- settled = true;
977
- clearTimeout(deadline);
978
- abortSignal?.removeEventListener('abort', onAbort);
979
- fn();
980
- };
981
- const onAbort = () =>
982
- settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
983
- const deadline = setTimeout(
984
- () =>
985
- settle(() =>
986
- reject(
987
- ERRORS.TypedError(
988
- HardwareErrorCode.BleTimeoutError,
989
- `BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
990
- )
991
- )
992
- ),
993
- BLE_ACQUIRE_DEADLINE_MS
994
- );
995
- // Attach before any early return so a late settlement of acquirePromise
996
- // is always consumed — an abort or deadline must never leave the acquire
997
- // rejection unhandled.
998
- acquirePromise.then(
999
- value => settle(() => resolve(value)),
1000
- error => settle(() => reject(error))
1001
- );
1002
- if (abortSignal) {
1003
- if (abortSignal.aborted) {
1004
- onAbort();
1005
- return;
1006
- }
1007
- abortSignal.addEventListener('abort', onAbort);
1008
- }
1009
- });
1010
- }
1011
-
1012
- async function connectDeviceForBle(
1013
- method: BaseMethod,
1014
- device: Device,
1015
- abortSignal?: AbortSignal,
1016
- retryCount = 0
1017
- ) {
957
+ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
1018
958
  try {
1019
959
  if (device.wasInterruptedByUser()) {
1020
960
  throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
@@ -1028,50 +968,9 @@ async function connectDeviceForBle(
1028
968
  !device.commands ||
1029
969
  device.commands.disposed;
1030
970
  if (shouldAcquire) {
1031
- // The deadline/abort guards are scoped to the desktop electron
1032
- // transport: its IPC acquire is the only path with a proven
1033
- // never-settling failure mode, while react-native/lowlevel acquire may
1034
- // legitimately block on a user-driven system bonding prompt for longer
1035
- // than any sane deadline. Other envs keep the plain acquire unchanged.
1036
- const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
1037
- // A cancel landing during the retry backoff must not start a new acquire.
1038
- if (useAcquireGuards && abortSignal?.aborted) {
1039
- throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
1040
- }
1041
- if (!useAcquireGuards) {
1042
- await device.acquire(method.payload.connectProtocol, {
1043
- forceProtocolDetection: method.payload.forceProtocolDetection,
1044
- });
1045
- } else {
1046
- try {
1047
- await raceBleAcquire(
1048
- device.acquire(method.payload.connectProtocol, {
1049
- forceProtocolDetection: method.payload.forceProtocolDetection,
1050
- }),
1051
- abortSignal
1052
- );
1053
- } catch (err) {
1054
- // The abandoned acquire keeps running: raceBleAcquire only rejects
1055
- // its own wrapper. Supersede it so that, if it settles later, the
1056
- // generation guard in Device.acquire drops the link it built instead
1057
- // of committing mainId/commands over the newer attempt.
1058
- device.beginConnectionAttempt();
1059
- // A deadline hit also means the transport is wedged mid-acquire; drop
1060
- // the link we know about before the retry so it cold-connects instead
1061
- // of stacking a second connect onto the half-open one. `mainId` is
1062
- // unset while the first acquire is still pending, which is exactly
1063
- // the case the generation bump above covers.
1064
- if (
1065
- err.errorCode === HardwareErrorCode.BleTimeoutError &&
1066
- device.mainId &&
1067
- device.deviceConnector
1068
- ) {
1069
- await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
1070
- device.markTransportDisconnected();
1071
- }
1072
- throw err;
1073
- }
1074
- }
971
+ await device.acquire(method.payload.connectProtocol, {
972
+ forceProtocolDetection: method.payload.forceProtocolDetection,
973
+ });
1075
974
  }
1076
975
  if (method.payload?.onlyConnectBleDevice) {
1077
976
  if (shouldAcquire) {
@@ -1111,7 +1010,7 @@ async function connectDeviceForBle(
1111
1010
  const nextRetry = retryCount + 1;
1112
1011
  Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
1113
1012
  await wait(3000);
1114
- await connectDeviceForBle(method, device, abortSignal, nextRetry);
1013
+ await connectDeviceForBle(method, device, nextRetry);
1115
1014
  } else {
1116
1015
  throw err;
1117
1016
  }
@@ -1221,7 +1120,7 @@ const ensureConnected = async (
1221
1120
  if (tryCount === 1) {
1222
1121
  device.beginConnectionAttempt();
1223
1122
  }
1224
- await connectDeviceForBle(method, device, abortSignal);
1123
+ await connectDeviceForBle(method, device);
1225
1124
  }
1226
1125
  resolve(device);
1227
1126
  return;
@@ -6,12 +6,26 @@ import { ERRORS, HardwareErrorCode } from '@onekeyfe/hd-shared';
6
6
  export const PRO2_WALLPAPER_WIDTH = 604;
7
7
  export const PRO2_WALLPAPER_HEIGHT = 1024;
8
8
 
9
- export type Pro2WallpaperColorFormat = 'RGB565' | 'RGB565A8';
9
+ export type Pro2WallpaperColorFormat = 'RGB565' | 'RGB565A8' | 'I8';
10
+
11
+ export type Pro2WallpaperEncoding = 'rgb565' | 'i8-lz4';
10
12
 
11
13
  export type Pro2ImageAlphaMode = 'preserve' | 'black-background';
12
14
 
13
15
  const COLOR_FORMAT_RGB565 = 0x12;
14
16
  const COLOR_FORMAT_RGB565A8 = 0x14;
17
+ const COLOR_FORMAT_I8 = 0x0a;
18
+ const IMAGE_FLAG_COMPRESSED = 0x0008;
19
+ const IMAGE_COMPRESSION_LZ4 = 0x00000002;
20
+ const I8_PALETTE_SIZE = 256 * 4;
21
+ const I8_RED_LEVELS = 6;
22
+ const I8_GREEN_LEVELS = 7;
23
+ const I8_BLUE_LEVELS = 6;
24
+ const LZ4_MIN_MATCH = 4;
25
+ const LZ4_LAST_LITERALS = 5;
26
+ const LZ4_MATCH_FIND_LIMIT = 12;
27
+ const LZ4_HASH_BITS = 16;
28
+ const LZ4_HASH_MULTIPLIER = -1640531535;
15
29
 
16
30
  const RED_THRESHOLD = [
17
31
  1, 7, 3, 5, 0, 8, 2, 6, 7, 1, 5, 3, 8, 0, 6, 2, 3, 5, 0, 8, 2, 6, 1, 7, 5, 3, 8, 0, 6, 2, 7, 1, 0,
@@ -38,6 +52,184 @@ function align(value: number, boundary: number): number {
38
52
  return Math.ceil(value / boundary) * boundary;
39
53
  }
40
54
 
55
+ function writeLz4Length(output: Uint8Array, offset: number, length: number) {
56
+ let cursor = offset;
57
+ let remaining = length;
58
+ while (remaining >= 0xff) {
59
+ output[cursor] = 0xff;
60
+ cursor += 1;
61
+ remaining -= 0xff;
62
+ }
63
+ output[cursor] = remaining;
64
+ return cursor + 1;
65
+ }
66
+
67
+ function readUint32LittleEndian(data: Uint8Array, offset: number) {
68
+ return (
69
+ data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)
70
+ );
71
+ }
72
+
73
+ function getLz4Hash(sequence: number) {
74
+ return (Math.imul(sequence, LZ4_HASH_MULTIPLIER) >>> (32 - LZ4_HASH_BITS)) & 0xffff;
75
+ }
76
+
77
+ function compressLz4Block(input: Uint8Array) {
78
+ const output = new Uint8Array(input.byteLength + Math.floor(input.byteLength / 0xff) + 16);
79
+ const hashTable = new Int32Array(1 << LZ4_HASH_BITS);
80
+ hashTable.fill(-1);
81
+
82
+ let anchor = 0;
83
+ let inputOffset = 0;
84
+ let outputOffset = 0;
85
+ const matchFindEnd = input.byteLength - LZ4_MATCH_FIND_LIMIT;
86
+ const matchCopyEnd = input.byteLength - LZ4_LAST_LITERALS;
87
+
88
+ while (inputOffset <= matchFindEnd) {
89
+ const sequence = readUint32LittleEndian(input, inputOffset);
90
+ const hash = getLz4Hash(sequence);
91
+ const reference = hashTable[hash];
92
+ hashTable[hash] = inputOffset;
93
+
94
+ const matchOffset = inputOffset - reference;
95
+ const hasMatch =
96
+ reference >= 0 &&
97
+ matchOffset <= 0xffff &&
98
+ readUint32LittleEndian(input, reference) === sequence;
99
+ if (hasMatch) {
100
+ let matchLength = LZ4_MIN_MATCH;
101
+ while (
102
+ inputOffset + matchLength < matchCopyEnd &&
103
+ input[reference + matchLength] === input[inputOffset + matchLength]
104
+ ) {
105
+ matchLength += 1;
106
+ }
107
+
108
+ const literalLength = inputOffset - anchor;
109
+ const encodedMatchLength = matchLength - LZ4_MIN_MATCH;
110
+ const tokenOffset = outputOffset;
111
+ outputOffset += 1;
112
+ output[tokenOffset] =
113
+ (Math.min(literalLength, 0x0f) << 4) | Math.min(encodedMatchLength, 0x0f);
114
+
115
+ if (literalLength >= 0x0f) {
116
+ outputOffset = writeLz4Length(output, outputOffset, literalLength - 0x0f);
117
+ }
118
+ output.set(input.subarray(anchor, inputOffset), outputOffset);
119
+ outputOffset += literalLength;
120
+
121
+ output[outputOffset] = matchOffset & 0xff;
122
+ output[outputOffset + 1] = matchOffset >> 8;
123
+ outputOffset += 2;
124
+ if (encodedMatchLength >= 0x0f) {
125
+ outputOffset = writeLz4Length(output, outputOffset, encodedMatchLength - 0x0f);
126
+ }
127
+
128
+ const matchStart = inputOffset;
129
+ inputOffset += matchLength;
130
+ anchor = inputOffset;
131
+ for (
132
+ let cursor = Math.max(matchStart + 1, inputOffset - 2);
133
+ cursor < inputOffset;
134
+ cursor += 1
135
+ ) {
136
+ if (cursor <= matchFindEnd) {
137
+ hashTable[getLz4Hash(readUint32LittleEndian(input, cursor))] = cursor;
138
+ }
139
+ }
140
+ } else {
141
+ inputOffset += 1;
142
+ }
143
+ }
144
+
145
+ const literalLength = input.byteLength - anchor;
146
+ const tokenOffset = outputOffset;
147
+ outputOffset += 1;
148
+ output[tokenOffset] = Math.min(literalLength, 0x0f) << 4;
149
+ if (literalLength >= 0x0f) {
150
+ outputOffset = writeLz4Length(output, outputOffset, literalLength - 0x0f);
151
+ }
152
+ output.set(input.subarray(anchor), outputOffset);
153
+ outputOffset += literalLength;
154
+
155
+ return output.slice(0, outputOffset);
156
+ }
157
+
158
+ function quantizeChannel(value: number, levels: number) {
159
+ return Math.floor((value * (levels - 1) + 0x7f) / 0xff);
160
+ }
161
+
162
+ function expandChannel(value: number, levels: number) {
163
+ return Math.floor((value * 0xff + Math.floor((levels - 1) / 2)) / (levels - 1));
164
+ }
165
+
166
+ function encodePro2I8Lz4(options: {
167
+ width: number;
168
+ height: number;
169
+ rgba: Uint8Array | ArrayBuffer;
170
+ }): { data: Uint8Array; colorFormat: Pro2WallpaperColorFormat } {
171
+ const { width, height } = options;
172
+ if (!Number.isInteger(width) || width <= 0 || width > 0xffff) {
173
+ throw invalidParameter('Wallpaper width must be an integer between 1 and 65535.');
174
+ }
175
+ if (!Number.isInteger(height) || height <= 0 || height > 0xffff) {
176
+ throw invalidParameter('Wallpaper height must be an integer between 1 and 65535.');
177
+ }
178
+ const rgba = asBytes(options.rgba);
179
+ const expectedLength = width * height * 4;
180
+ if (rgba.byteLength !== expectedLength) {
181
+ throw invalidParameter(
182
+ `Wallpaper RGBA data length must be ${expectedLength} bytes, received ${rgba.byteLength}.`
183
+ );
184
+ }
185
+ const stride = width;
186
+ const rawData = new Uint8Array(I8_PALETTE_SIZE + stride * height);
187
+
188
+ for (let red = 0; red < I8_RED_LEVELS; red += 1) {
189
+ for (let green = 0; green < I8_GREEN_LEVELS; green += 1) {
190
+ for (let blue = 0; blue < I8_BLUE_LEVELS; blue += 1) {
191
+ const paletteIndex = (red * I8_GREEN_LEVELS + green) * I8_BLUE_LEVELS + blue;
192
+ const paletteOffset = paletteIndex * 4;
193
+ // LVGL stores its ARGB8888 palette as B, G, R, A bytes on little-endian devices.
194
+ rawData[paletteOffset] = expandChannel(blue, I8_BLUE_LEVELS);
195
+ rawData[paletteOffset + 1] = expandChannel(green, I8_GREEN_LEVELS);
196
+ rawData[paletteOffset + 2] = expandChannel(red, I8_RED_LEVELS);
197
+ rawData[paletteOffset + 3] = 0xff;
198
+ }
199
+ }
200
+ }
201
+
202
+ for (let pixel = 0; pixel < width * height; pixel += 1) {
203
+ const sourceOffset = pixel * 4;
204
+ const alpha = rgba[sourceOffset + 3];
205
+ const red = Math.round((rgba[sourceOffset] * alpha) / 0xff);
206
+ const green = Math.round((rgba[sourceOffset + 1] * alpha) / 0xff);
207
+ const blue = Math.round((rgba[sourceOffset + 2] * alpha) / 0xff);
208
+ const paletteIndex =
209
+ (quantizeChannel(red, I8_RED_LEVELS) * I8_GREEN_LEVELS +
210
+ quantizeChannel(green, I8_GREEN_LEVELS)) *
211
+ I8_BLUE_LEVELS +
212
+ quantizeChannel(blue, I8_BLUE_LEVELS);
213
+ rawData[I8_PALETTE_SIZE + pixel] = paletteIndex;
214
+ }
215
+
216
+ const compressed = compressLz4Block(rawData);
217
+ const data = new Uint8Array(24 + compressed.byteLength);
218
+ const view = new DataView(data.buffer);
219
+ data[0] = 0x19;
220
+ data[1] = COLOR_FORMAT_I8;
221
+ view.setUint16(2, IMAGE_FLAG_COMPRESSED, true);
222
+ view.setUint16(4, width, true);
223
+ view.setUint16(6, height, true);
224
+ view.setUint16(8, stride, true);
225
+ view.setUint16(10, 0, true);
226
+ view.setUint32(12, IMAGE_COMPRESSION_LZ4, true);
227
+ view.setUint32(16, compressed.byteLength, true);
228
+ view.setUint32(20, rawData.byteLength, true);
229
+ data.set(compressed, 24);
230
+ return { data, colorFormat: 'I8' };
231
+ }
232
+
41
233
  export function encodePro2Image(options: {
42
234
  width: number;
43
235
  height: number;
@@ -125,6 +317,10 @@ export function encodePro2Wallpaper(options: {
125
317
  width: number;
126
318
  height: number;
127
319
  rgba: Uint8Array | ArrayBuffer;
320
+ encoding?: Pro2WallpaperEncoding;
128
321
  }): { data: Uint8Array; colorFormat: Pro2WallpaperColorFormat } {
322
+ if (options.encoding === 'i8-lz4') {
323
+ return encodePro2I8Lz4(options);
324
+ }
129
325
  return encodePro2Image(options);
130
326
  }