@wodzik/cubecore 0.1.0
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/LICENSE +373 -0
- package/README.md +118 -0
- package/analyze/client.d.ts +18 -0
- package/analyze/client.js +42 -0
- package/analyze/index.d.ts +114 -0
- package/analyze/index.js +151 -0
- package/analyze/rotations.d.ts +3 -0
- package/analyze/rotations.js +15 -0
- package/analyze/roux.d.ts +67 -0
- package/analyze/roux.js +95 -0
- package/analyze/worker.d.ts +6 -0
- package/analyze/worker.js +31 -0
- package/analyze/zz.d.ts +47 -0
- package/analyze/zz.js +102 -0
- package/bld/index.d.ts +14 -0
- package/bld/index.js +14 -0
- package/bld/memo.d.ts +66 -0
- package/bld/memo.js +139 -0
- package/bld/scheme.d.ts +30 -0
- package/bld/scheme.js +35 -0
- package/bld/skin.d.ts +26 -0
- package/bld/skin.js +46 -0
- package/bld/tracker.d.ts +68 -0
- package/bld/tracker.js +145 -0
- package/bluetooth/clock.d.ts +22 -0
- package/bluetooth/clock.js +37 -0
- package/bluetooth/cubeSkins.d.ts +31 -0
- package/bluetooth/cubeSkins.js +32 -0
- package/bluetooth/gyro.d.ts +31 -0
- package/bluetooth/gyro.js +46 -0
- package/bluetooth/index.d.ts +5 -0
- package/bluetooth/index.js +5 -0
- package/bluetooth/session.d.ts +140 -0
- package/bluetooth/session.js +201 -0
- package/bluetooth/simulated.d.ts +52 -0
- package/bluetooth/simulated.js +80 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
- package/cfop/caseData.d.ts +16 -0
- package/cfop/caseData.js +88 -0
- package/cfop/cases.d.ts +42 -0
- package/cfop/cases.js +116 -0
- package/cfop/f2l.d.ts +28 -0
- package/cfop/f2l.js +88 -0
- package/cfop/f2lData.d.ts +15 -0
- package/cfop/f2lData.js +49 -0
- package/cfop/index.d.ts +41 -0
- package/cfop/index.js +110 -0
- package/cfop/slots.d.ts +2 -0
- package/cfop/slots.js +1 -0
- package/core/arrows.d.ts +24 -0
- package/core/arrows.js +22 -0
- package/core/bytes.d.ts +27 -0
- package/core/bytes.js +145 -0
- package/core/centers.d.ts +32 -0
- package/core/centers.js +83 -0
- package/core/checks.d.ts +53 -0
- package/core/checks.js +85 -0
- package/core/cubies.d.ts +45 -0
- package/core/cubies.js +169 -0
- package/core/frames.d.ts +61 -0
- package/core/frames.js +126 -0
- package/core/geometry.d.ts +58 -0
- package/core/geometry.js +132 -0
- package/core/index.d.ts +19 -0
- package/core/index.js +19 -0
- package/core/masks.d.ts +32 -0
- package/core/masks.js +39 -0
- package/core/method.d.ts +35 -0
- package/core/method.js +30 -0
- package/core/moves.d.ts +48 -0
- package/core/moves.js +88 -0
- package/core/notation.d.ts +48 -0
- package/core/notation.js +226 -0
- package/core/physical.d.ts +53 -0
- package/core/physical.js +99 -0
- package/core/practice.d.ts +62 -0
- package/core/practice.js +87 -0
- package/core/sequence.d.ts +90 -0
- package/core/sequence.js +157 -0
- package/core/stages.d.ts +90 -0
- package/core/stages.js +21 -0
- package/core/state.d.ts +55 -0
- package/core/state.js +140 -0
- package/core/stateCodec.d.ts +12 -0
- package/core/stateCodec.js +51 -0
- package/core/stream.d.ts +45 -0
- package/core/stream.js +75 -0
- package/core/tracker.d.ts +62 -0
- package/core/tracker.js +121 -0
- package/element/alg.d.ts +45 -0
- package/element/alg.js +150 -0
- package/element/base.d.ts +2 -0
- package/element/base.js +3 -0
- package/element/bld.d.ts +82 -0
- package/element/bld.js +222 -0
- package/element/index.d.ts +8 -0
- package/element/index.js +21 -0
- package/element/model.d.ts +36 -0
- package/element/model.js +64 -0
- package/element/player.d.ts +196 -0
- package/element/player.js +680 -0
- package/element/practice.d.ts +75 -0
- package/element/practice.js +150 -0
- package/element/scramble.d.ts +50 -0
- package/element/scramble.js +107 -0
- package/element/sequence.d.ts +126 -0
- package/element/sequence.js +254 -0
- package/element/styles.d.ts +32 -0
- package/element/styles.js +196 -0
- package/image/cache.d.ts +21 -0
- package/image/cache.js +66 -0
- package/image/index.d.ts +2 -0
- package/image/index.js +2 -0
- package/image/png-node.d.ts +6 -0
- package/image/png-node.js +10 -0
- package/image/svg.d.ts +36 -0
- package/image/svg.js +215 -0
- package/lbl/index.d.ts +10 -0
- package/lbl/index.js +31 -0
- package/methods/index.d.ts +20 -0
- package/methods/index.js +33 -0
- package/package.json +133 -0
- package/petrus/index.d.ts +11 -0
- package/petrus/index.js +27 -0
- package/react/index.d.ts +176 -0
- package/react/index.js +228 -0
- package/render/arrows.d.ts +68 -0
- package/render/arrows.js +190 -0
- package/render/build/attachments.d.ts +35 -0
- package/render/build/attachments.js +94 -0
- package/render/build/models.d.ts +9 -0
- package/render/build/models.js +27 -0
- package/render/build/tiles.d.ts +48 -0
- package/render/build/tiles.js +194 -0
- package/render/gltf.d.ts +10 -0
- package/render/gltf.js +106 -0
- package/render/index.d.ts +12 -0
- package/render/index.js +12 -0
- package/render/layers.d.ts +21 -0
- package/render/layers.js +24 -0
- package/render/pictures.d.ts +46 -0
- package/render/pictures.js +80 -0
- package/render/pieceModels.d.ts +38 -0
- package/render/pieceModels.js +43 -0
- package/render/renderer.d.ts +165 -0
- package/render/renderer.js +647 -0
- package/render/tile.d.ts +114 -0
- package/render/tile.js +390 -0
- package/render/viewports.d.ts +25 -0
- package/render/viewports.js +26 -0
- package/roux/cmll.d.ts +35 -0
- package/roux/cmll.js +121 -0
- package/roux/cmllData.d.ts +12 -0
- package/roux/cmllData.js +49 -0
- package/roux/index.d.ts +52 -0
- package/roux/index.js +175 -0
- package/skin/attachments.d.ts +82 -0
- package/skin/attachments.js +110 -0
- package/skin/index.d.ts +3 -0
- package/skin/index.js +3 -0
- package/skin/shapes.d.ts +112 -0
- package/skin/shapes.js +147 -0
- package/skin/skin.d.ts +911 -0
- package/skin/skin.js +412 -0
- package/solve/client.d.ts +58 -0
- package/solve/client.js +56 -0
- package/solve/handle.d.ts +6 -0
- package/solve/handle.js +38 -0
- package/solve/index.d.ts +8 -0
- package/solve/index.js +7 -0
- package/solve/lse.d.ts +42 -0
- package/solve/lse.js +151 -0
- package/solve/pieces.d.ts +18 -0
- package/solve/pieces.js +24 -0
- package/solve/protocol.d.ts +70 -0
- package/solve/protocol.js +16 -0
- package/solve/scramble.d.ts +76 -0
- package/solve/scramble.js +187 -0
- package/solve/stage.d.ts +108 -0
- package/solve/stage.js +532 -0
- package/solve/twophase.d.ts +36 -0
- package/solve/twophase.js +229 -0
- package/solve/worker.d.ts +6 -0
- package/solve/worker.js +15 -0
- package/timeline/codec.d.ts +23 -0
- package/timeline/codec.js +84 -0
- package/timeline/index.d.ts +7 -0
- package/timeline/index.js +7 -0
- package/timeline/recording.d.ts +19 -0
- package/timeline/recording.js +20 -0
- package/timeline/replay.d.ts +68 -0
- package/timeline/replay.js +145 -0
- package/timeline/segments.d.ts +31 -0
- package/timeline/segments.js +39 -0
- package/timeline/share.d.ts +30 -0
- package/timeline/share.js +62 -0
- package/timeline/stats.d.ts +34 -0
- package/timeline/stats.js +56 -0
- package/timeline/timings.d.ts +30 -0
- package/timeline/timings.js +54 -0
- package/zz/index.d.ts +29 -0
- package/zz/index.js +68 -0
- package/zz/seeds.d.ts +9 -0
- package/zz/seeds.js +29 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { Subject } from 'rxjs';
|
|
2
|
+
import { ModeOfOperation } from 'aes-js';
|
|
3
|
+
import { normalizeUuid } from '../attachment/normalize-uuid.js';
|
|
4
|
+
import { getCachedMacForDevice } from '../attachment/address-hints.js';
|
|
5
|
+
import { buildQiYiMacCandidatesFromName } from '../attachment/mac-candidates.js';
|
|
6
|
+
import { probeQiYiMac } from '../attachment/mac-probe-qiyi.js';
|
|
7
|
+
import { registerProtocol } from '../protocol.js';
|
|
8
|
+
import { CubieCube } from '../cubie-cube.js';
|
|
9
|
+
import { now, findCharacteristic, waitForAdvertisements, extractMacFromManufacturerData } from '../ble-utils.js';
|
|
10
|
+
import { writeGattCharacteristicValue } from '../../gatt-characteristic-write.js';
|
|
11
|
+
const UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
|
|
12
|
+
const SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
|
|
13
|
+
const CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
|
|
14
|
+
const QIYI_CIC_LIST = [0x0504];
|
|
15
|
+
const QIYI_KEY = [87, 177, 249, 171, 205, 90, 232, 167, 156, 185, 140, 231, 87, 140, 81, 8];
|
|
16
|
+
/** Kociemba facelet string for solved cube */
|
|
17
|
+
const QIYI_SOLVED_FACELETS = 'UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB';
|
|
18
|
+
function crc16modbus(data) {
|
|
19
|
+
let crc = 0xFFFF;
|
|
20
|
+
for (let i = 0; i < data.length; i++) {
|
|
21
|
+
crc ^= data[i];
|
|
22
|
+
for (let j = 0; j < 8; j++) {
|
|
23
|
+
crc = (crc & 0x1) > 0 ? (crc >> 1) ^ 0xa001 : crc >> 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return crc;
|
|
27
|
+
}
|
|
28
|
+
class QiYiEncrypter {
|
|
29
|
+
cipher;
|
|
30
|
+
constructor() {
|
|
31
|
+
this.cipher = new ModeOfOperation.ecb(new Uint8Array(QIYI_KEY));
|
|
32
|
+
}
|
|
33
|
+
encrypt(data) {
|
|
34
|
+
const result = [];
|
|
35
|
+
for (let i = 0; i < data.length; i += 16) {
|
|
36
|
+
const block = data.slice(i, i + 16);
|
|
37
|
+
const encrypted = this.cipher.encrypt(new Uint8Array(block));
|
|
38
|
+
for (let j = 0; j < 16; j++) {
|
|
39
|
+
result[i + j] = encrypted[j];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
decrypt(data) {
|
|
45
|
+
const result = [];
|
|
46
|
+
for (let i = 0; i < data.length; i += 16) {
|
|
47
|
+
const block = data.slice(i, i + 16);
|
|
48
|
+
const decrypted = this.cipher.decrypt(new Uint8Array(block));
|
|
49
|
+
for (let j = 0; j < 16; j++) {
|
|
50
|
+
result[i + j] = decrypted[j];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseFacelet(faceMsg) {
|
|
57
|
+
const ret = [];
|
|
58
|
+
for (let i = 0; i < 54; i++) {
|
|
59
|
+
ret.push("LRDUFB".charAt((faceMsg[i >> 1] >> ((i % 2) << 2)) & 0xf));
|
|
60
|
+
}
|
|
61
|
+
return ret.join("");
|
|
62
|
+
}
|
|
63
|
+
/** Device timestamps in bytes 3–6 and history slots: big-endian uint32 */
|
|
64
|
+
function readQiYiTimestampBE(msg, offset) {
|
|
65
|
+
return (msg[offset] << 24) | (msg[offset + 1] << 16) | (msg[offset + 2] << 8) | msg[offset + 3];
|
|
66
|
+
}
|
|
67
|
+
/** Collect primary + all 11 history slots (bytes 36..90) */
|
|
68
|
+
function collectQiYiStateChangeMoves(msg, headerTs) {
|
|
69
|
+
const out = [[msg[34], headerTs]];
|
|
70
|
+
for (let i = 0; i < 11; i++) {
|
|
71
|
+
const off = 36 + 5 * i;
|
|
72
|
+
if (off + 5 > msg.length)
|
|
73
|
+
break;
|
|
74
|
+
let allFf = true;
|
|
75
|
+
for (let j = 0; j < 5; j++) {
|
|
76
|
+
if (msg[off + j] !== 0xff) {
|
|
77
|
+
allFf = false;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (allFf)
|
|
82
|
+
continue;
|
|
83
|
+
const slotTs = readQiYiTimestampBE(msg, off);
|
|
84
|
+
const code = msg[off + 4];
|
|
85
|
+
out.push([code, slotTs]);
|
|
86
|
+
}
|
|
87
|
+
out.sort((a, b) => a[1] - b[1]);
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
return out.filter(([code, t]) => {
|
|
90
|
+
const k = `${code},${t}`;
|
|
91
|
+
if (seen.has(k))
|
|
92
|
+
return false;
|
|
93
|
+
seen.add(k);
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const QIYI_PROTOCOL = { id: 'qiyi', name: 'QiYi' };
|
|
98
|
+
class QiYiConnection {
|
|
99
|
+
deviceName;
|
|
100
|
+
deviceMAC;
|
|
101
|
+
protocol = QIYI_PROTOCOL;
|
|
102
|
+
capabilities = {
|
|
103
|
+
gyroscope: false,
|
|
104
|
+
battery: true,
|
|
105
|
+
facelets: true,
|
|
106
|
+
hardware: true,
|
|
107
|
+
reset: false
|
|
108
|
+
};
|
|
109
|
+
events$;
|
|
110
|
+
device;
|
|
111
|
+
cubeChrct = null;
|
|
112
|
+
encrypter;
|
|
113
|
+
curCubie = new CubieCube();
|
|
114
|
+
prevCubie = new CubieCube();
|
|
115
|
+
lastTs = 0;
|
|
116
|
+
lastBatteryLevel = null;
|
|
117
|
+
forceNextBatteryEmission = false;
|
|
118
|
+
writeChain = Promise.resolve();
|
|
119
|
+
constructor(device, mac) {
|
|
120
|
+
this.device = device;
|
|
121
|
+
this.deviceName = device.name || 'QiYi';
|
|
122
|
+
this.deviceMAC = mac;
|
|
123
|
+
this.events$ = new Subject();
|
|
124
|
+
this.encrypter = new QiYiEncrypter();
|
|
125
|
+
}
|
|
126
|
+
sendMessage(content) {
|
|
127
|
+
if (!this.cubeChrct)
|
|
128
|
+
return Promise.reject();
|
|
129
|
+
const ch = this.cubeChrct;
|
|
130
|
+
const run = async () => {
|
|
131
|
+
const msg = [0xfe];
|
|
132
|
+
msg.push(4 + content.length);
|
|
133
|
+
for (let i = 0; i < content.length; i++) {
|
|
134
|
+
msg.push(content[i]);
|
|
135
|
+
}
|
|
136
|
+
const crc = crc16modbus(msg);
|
|
137
|
+
msg.push(crc & 0xff, crc >> 8);
|
|
138
|
+
const npad = (16 - msg.length % 16) % 16;
|
|
139
|
+
for (let i = 0; i < npad; i++) {
|
|
140
|
+
msg.push(0);
|
|
141
|
+
}
|
|
142
|
+
const encMsg = this.encrypter.encrypt(msg);
|
|
143
|
+
await writeGattCharacteristicValue(ch, new Uint8Array(encMsg).buffer);
|
|
144
|
+
};
|
|
145
|
+
this.writeChain = this.writeChain.then(run, run);
|
|
146
|
+
return this.writeChain;
|
|
147
|
+
}
|
|
148
|
+
sendHello() {
|
|
149
|
+
const macBytes = this.deviceMAC.split(/[:-\s]+/).map(c => parseInt(c, 16));
|
|
150
|
+
const content = [0x00, 0x6b, 0x01, 0x00, 0x00, 0x22, 0x06, 0x00, 0x02, 0x08, 0x00];
|
|
151
|
+
for (let i = 5; i >= 0; i--) {
|
|
152
|
+
content.push(macBytes[i] || 0);
|
|
153
|
+
}
|
|
154
|
+
return this.sendMessage(content);
|
|
155
|
+
}
|
|
156
|
+
onCubeEvent = (event) => {
|
|
157
|
+
const value = event.target.value;
|
|
158
|
+
if (!value)
|
|
159
|
+
return;
|
|
160
|
+
const encMsg = [];
|
|
161
|
+
for (let i = 0; i < value.byteLength; i++) {
|
|
162
|
+
encMsg[i] = value.getUint8(i);
|
|
163
|
+
}
|
|
164
|
+
const msg = this.encrypter.decrypt(encMsg);
|
|
165
|
+
if (msg[0] === 0xCC && msg[1] === 0x10) {
|
|
166
|
+
this.handleQuaternionPacket(msg);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const trimmed = msg.slice(0, msg[1]);
|
|
170
|
+
if (trimmed.length < 3 || crc16modbus(trimmed) !== 0) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.parseCubeData(trimmed);
|
|
174
|
+
};
|
|
175
|
+
handleQuaternionPacket(msg) {
|
|
176
|
+
if (msg.length < 16)
|
|
177
|
+
return;
|
|
178
|
+
const expectedCrc = crc16modbus(msg.slice(0, 14));
|
|
179
|
+
const actualCrc = msg[14] | (msg[15] << 8);
|
|
180
|
+
if (expectedCrc !== actualCrc)
|
|
181
|
+
return;
|
|
182
|
+
if (!this.capabilities.gyroscope) {
|
|
183
|
+
this.capabilities.gyroscope = true;
|
|
184
|
+
}
|
|
185
|
+
const dv = new DataView(Uint8Array.from(msg).buffer);
|
|
186
|
+
const ax = dv.getInt16(6, false) / 1000;
|
|
187
|
+
const ay = dv.getInt16(8, false) / 1000;
|
|
188
|
+
const az = dv.getInt16(10, false) / 1000;
|
|
189
|
+
const aw = dv.getInt16(12, false) / 1000;
|
|
190
|
+
this.events$.next({
|
|
191
|
+
timestamp: now(),
|
|
192
|
+
type: "GYRO",
|
|
193
|
+
quaternion: {
|
|
194
|
+
x: ax,
|
|
195
|
+
y: -az,
|
|
196
|
+
z: ay,
|
|
197
|
+
w: aw
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
emitHardwareEvent() {
|
|
202
|
+
this.events$.next({
|
|
203
|
+
timestamp: now(),
|
|
204
|
+
type: "HARDWARE",
|
|
205
|
+
hardwareName: this.deviceName,
|
|
206
|
+
gyroSupported: this.capabilities.gyroscope
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
emitBatteryLevel(rawLevel, timestamp = now()) {
|
|
210
|
+
if (!Number.isFinite(rawLevel)) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const batteryLevel = Math.min(100, Math.max(0, Math.round(rawLevel)));
|
|
214
|
+
const forceEmission = this.forceNextBatteryEmission;
|
|
215
|
+
this.forceNextBatteryEmission = false;
|
|
216
|
+
if (!forceEmission && this.lastBatteryLevel === batteryLevel) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.lastBatteryLevel = batteryLevel;
|
|
220
|
+
this.events$.next({
|
|
221
|
+
timestamp,
|
|
222
|
+
type: 'BATTERY',
|
|
223
|
+
batteryLevel,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
parseCubeData(msg) {
|
|
227
|
+
const timestamp = now();
|
|
228
|
+
if (msg[0] !== 0xfe)
|
|
229
|
+
return;
|
|
230
|
+
const opcode = msg[2];
|
|
231
|
+
const ts = readQiYiTimestampBE(msg, 3);
|
|
232
|
+
if (opcode === 0x2) {
|
|
233
|
+
// Hello response — always ACK
|
|
234
|
+
this.sendMessage(msg.slice(2, 7)).catch(() => { });
|
|
235
|
+
const newFacelet = parseFacelet(msg.slice(7, 34));
|
|
236
|
+
this.events$.next({
|
|
237
|
+
timestamp,
|
|
238
|
+
type: "FACELETS",
|
|
239
|
+
facelets: newFacelet
|
|
240
|
+
});
|
|
241
|
+
this.emitBatteryLevel(msg[35], timestamp);
|
|
242
|
+
this.prevCubie.fromFacelet(newFacelet);
|
|
243
|
+
this.lastTs = ts;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (opcode === 0x3) {
|
|
247
|
+
const needsAck = msg.length > 91 && msg[91] !== 0;
|
|
248
|
+
if (needsAck) {
|
|
249
|
+
this.sendMessage(msg.slice(2, 7)).catch(() => { });
|
|
250
|
+
}
|
|
251
|
+
const candidates = collectQiYiStateChangeMoves(msg, ts);
|
|
252
|
+
const newMoves = candidates.filter(([code, moveTs]) => code >= 1 && code <= 12 && moveTs > this.lastTs);
|
|
253
|
+
for (let k = 0; k < newMoves.length; k++) {
|
|
254
|
+
const [code, moveTs] = newMoves[k];
|
|
255
|
+
const axis = [4, 1, 3, 0, 2, 5][(code - 1) >> 1];
|
|
256
|
+
const power = [0, 2][code & 1];
|
|
257
|
+
const m = axis * 3 + power;
|
|
258
|
+
const moveStr = ("URFDLB".charAt(axis) + " 2'".charAt(power)).trim();
|
|
259
|
+
CubieCube.CubeMult(this.prevCubie, CubieCube.moveCube[m], this.curCubie);
|
|
260
|
+
const facelet = this.curCubie.toFaceCube();
|
|
261
|
+
this.events$.next({
|
|
262
|
+
timestamp,
|
|
263
|
+
type: "MOVE",
|
|
264
|
+
face: axis,
|
|
265
|
+
direction: power === 0 ? 0 : 1,
|
|
266
|
+
move: moveStr,
|
|
267
|
+
localTimestamp: k === newMoves.length - 1 ? timestamp : null,
|
|
268
|
+
cubeTimestamp: Math.trunc(moveTs / 1.6)
|
|
269
|
+
});
|
|
270
|
+
this.events$.next({
|
|
271
|
+
timestamp,
|
|
272
|
+
type: "FACELETS",
|
|
273
|
+
facelets: facelet
|
|
274
|
+
});
|
|
275
|
+
const tmp = this.curCubie;
|
|
276
|
+
this.curCubie = this.prevCubie;
|
|
277
|
+
this.prevCubie = tmp;
|
|
278
|
+
}
|
|
279
|
+
if (newMoves.length > 0) {
|
|
280
|
+
this.lastTs = newMoves[newMoves.length - 1][1];
|
|
281
|
+
}
|
|
282
|
+
this.emitBatteryLevel(msg[35], timestamp);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (opcode === 0x4) {
|
|
286
|
+
// Sync confirmation: emit solved state; no ACK for op 4 in reference protocol.
|
|
287
|
+
if (msg[1] !== 38)
|
|
288
|
+
return;
|
|
289
|
+
this.events$.next({
|
|
290
|
+
timestamp,
|
|
291
|
+
type: "FACELETS",
|
|
292
|
+
facelets: QIYI_SOLVED_FACELETS
|
|
293
|
+
});
|
|
294
|
+
this.prevCubie.fromFacelet(QIYI_SOLVED_FACELETS);
|
|
295
|
+
this.lastTs = ts;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Unknown opcode: do not advance lastTs (avoids skewing move history filters).
|
|
299
|
+
}
|
|
300
|
+
onDisconnect = () => {
|
|
301
|
+
this.device.removeEventListener('gattserverdisconnected', this.onDisconnect);
|
|
302
|
+
this.lastBatteryLevel = null;
|
|
303
|
+
this.forceNextBatteryEmission = false;
|
|
304
|
+
this.events$.next({ timestamp: now(), type: "DISCONNECT" });
|
|
305
|
+
this.events$.complete();
|
|
306
|
+
};
|
|
307
|
+
async init() {
|
|
308
|
+
this.device.addEventListener('gattserverdisconnected', this.onDisconnect);
|
|
309
|
+
const gatt = await this.device.gatt.connect();
|
|
310
|
+
const service = await gatt.getPrimaryService(SERVICE_UUID);
|
|
311
|
+
const chrcts = await service.getCharacteristics();
|
|
312
|
+
this.cubeChrct = findCharacteristic(chrcts, CHRCT_UUID_CUBE);
|
|
313
|
+
if (!this.cubeChrct) {
|
|
314
|
+
throw new Error('[QiYi] Cannot find required characteristic');
|
|
315
|
+
}
|
|
316
|
+
this.cubeChrct.addEventListener('characteristicvaluechanged', this.onCubeEvent);
|
|
317
|
+
await this.cubeChrct.startNotifications();
|
|
318
|
+
await this.sendHello();
|
|
319
|
+
}
|
|
320
|
+
async sendCommand(command) {
|
|
321
|
+
if (command.type === "REQUEST_FACELETS" || command.type === "REQUEST_BATTERY") {
|
|
322
|
+
if (command.type === "REQUEST_BATTERY") {
|
|
323
|
+
this.forceNextBatteryEmission = true;
|
|
324
|
+
}
|
|
325
|
+
await this.sendHello();
|
|
326
|
+
}
|
|
327
|
+
else if (command.type === "REQUEST_HARDWARE") {
|
|
328
|
+
this.emitHardwareEvent();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async disconnect() {
|
|
332
|
+
if (this.cubeChrct) {
|
|
333
|
+
this.cubeChrct.removeEventListener('characteristicvaluechanged', this.onCubeEvent);
|
|
334
|
+
await this.cubeChrct.stopNotifications().catch(() => { });
|
|
335
|
+
this.cubeChrct = null;
|
|
336
|
+
}
|
|
337
|
+
this.lastBatteryLevel = null;
|
|
338
|
+
this.forceNextBatteryEmission = false;
|
|
339
|
+
this.device.removeEventListener('gattserverdisconnected', this.onDisconnect);
|
|
340
|
+
this.events$.next({ timestamp: now(), type: "DISCONNECT" });
|
|
341
|
+
this.events$.complete();
|
|
342
|
+
if (this.device.gatt?.connected) {
|
|
343
|
+
this.device.gatt.disconnect();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function parseQiYiMacFromMf(mfData) {
|
|
348
|
+
if (!mfData) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
return extractMacFromManufacturerData(mfData, QIYI_CIC_LIST, true);
|
|
352
|
+
}
|
|
353
|
+
async function connectQiYiDevice(device, macProvider, context) {
|
|
354
|
+
let mac = parseQiYiMacFromMf(context?.advertisementManufacturerData ?? null);
|
|
355
|
+
mac = mac || getCachedMacForDevice(device);
|
|
356
|
+
if (!mac && macProvider) {
|
|
357
|
+
const r = await macProvider(device, false);
|
|
358
|
+
if (r) {
|
|
359
|
+
mac = r;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!mac) {
|
|
363
|
+
const mfData = await waitForAdvertisements(device, context?.enableAddressSearch ? 8000 : 5000);
|
|
364
|
+
mac = parseQiYiMacFromMf(mfData);
|
|
365
|
+
}
|
|
366
|
+
if (!mac) {
|
|
367
|
+
const c = buildQiYiMacCandidatesFromName(device.name);
|
|
368
|
+
if (c.length === 1) {
|
|
369
|
+
mac = c[0];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!mac && context?.enableAddressSearch) {
|
|
373
|
+
const candidates = buildQiYiMacCandidatesFromName(device.name);
|
|
374
|
+
const timeoutMs = 3000;
|
|
375
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
376
|
+
if (context.signal?.aborted) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
context.onStatus?.(`Testing address (${i + 1}/${candidates.length})…`);
|
|
380
|
+
try {
|
|
381
|
+
if (await probeQiYiMac(device, candidates[i], {
|
|
382
|
+
timeoutMs,
|
|
383
|
+
signal: context.signal,
|
|
384
|
+
})) {
|
|
385
|
+
mac = candidates[i];
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
/* try next */
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!mac && macProvider) {
|
|
395
|
+
const r = await macProvider(device, true);
|
|
396
|
+
if (r) {
|
|
397
|
+
mac = r;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!mac) {
|
|
401
|
+
throw new Error('Unable to determine QiYi cube MAC address');
|
|
402
|
+
}
|
|
403
|
+
const conn = new QiYiConnection(device, mac);
|
|
404
|
+
await conn.init();
|
|
405
|
+
return conn;
|
|
406
|
+
}
|
|
407
|
+
const qiyiProtocol = {
|
|
408
|
+
nameFilters: [
|
|
409
|
+
{ namePrefix: "QY-QYSC" },
|
|
410
|
+
{ namePrefix: "XMD-TornadoV4-i" }
|
|
411
|
+
],
|
|
412
|
+
optionalServices: [SERVICE_UUID],
|
|
413
|
+
optionalManufacturerData: QIYI_CIC_LIST,
|
|
414
|
+
matchesDevice(device) {
|
|
415
|
+
const name = device.name || '';
|
|
416
|
+
return name.startsWith('QY-QYSC') || name.startsWith('XMD-TornadoV4-i');
|
|
417
|
+
},
|
|
418
|
+
gattAffinity(serviceUuids, _device) {
|
|
419
|
+
return serviceUuids.has(normalizeUuid(SERVICE_UUID)) ? 110 : 0;
|
|
420
|
+
},
|
|
421
|
+
connect: connectQiYiDevice
|
|
422
|
+
};
|
|
423
|
+
registerProtocol(qiyiProtocol);
|
|
424
|
+
export { qiyiProtocol };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/// <reference types="web-bluetooth" />
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
type SmartCubeMoveEvent = {
|
|
4
|
+
type: "MOVE";
|
|
5
|
+
face: number;
|
|
6
|
+
direction: number;
|
|
7
|
+
move: string;
|
|
8
|
+
localTimestamp: number | null;
|
|
9
|
+
cubeTimestamp: number | null;
|
|
10
|
+
};
|
|
11
|
+
type SmartCubeFaceletsEvent = {
|
|
12
|
+
type: "FACELETS";
|
|
13
|
+
facelets: string;
|
|
14
|
+
};
|
|
15
|
+
type SmartCubeGyroEvent = {
|
|
16
|
+
type: "GYRO";
|
|
17
|
+
quaternion: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
z: number;
|
|
21
|
+
w: number;
|
|
22
|
+
};
|
|
23
|
+
velocity?: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
z: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
type SmartCubeBatteryEvent = {
|
|
30
|
+
type: "BATTERY";
|
|
31
|
+
batteryLevel: number;
|
|
32
|
+
};
|
|
33
|
+
type SmartCubeProtocolInfo = {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
};
|
|
37
|
+
type SmartCubeHardwareEvent = {
|
|
38
|
+
type: "HARDWARE";
|
|
39
|
+
hardwareName?: string;
|
|
40
|
+
softwareVersion?: string;
|
|
41
|
+
hardwareVersion?: string;
|
|
42
|
+
productDate?: string;
|
|
43
|
+
gyroSupported?: boolean;
|
|
44
|
+
};
|
|
45
|
+
type SmartCubeDisconnectEvent = {
|
|
46
|
+
type: "DISCONNECT";
|
|
47
|
+
};
|
|
48
|
+
type SmartCubeEventMessage = SmartCubeMoveEvent | SmartCubeFaceletsEvent | SmartCubeGyroEvent | SmartCubeBatteryEvent | SmartCubeHardwareEvent | SmartCubeDisconnectEvent;
|
|
49
|
+
type SmartCubeEvent = {
|
|
50
|
+
timestamp: number;
|
|
51
|
+
} & SmartCubeEventMessage;
|
|
52
|
+
type SmartCubeCommand = {
|
|
53
|
+
type: "REQUEST_FACELETS";
|
|
54
|
+
} | {
|
|
55
|
+
type: "REQUEST_BATTERY";
|
|
56
|
+
} | {
|
|
57
|
+
type: "REQUEST_HARDWARE";
|
|
58
|
+
} | {
|
|
59
|
+
type: "REQUEST_RESET";
|
|
60
|
+
};
|
|
61
|
+
interface SmartCubeCapabilities {
|
|
62
|
+
gyroscope: boolean;
|
|
63
|
+
battery: boolean;
|
|
64
|
+
facelets: boolean;
|
|
65
|
+
hardware: boolean;
|
|
66
|
+
reset: boolean;
|
|
67
|
+
}
|
|
68
|
+
interface SmartCubeConnection {
|
|
69
|
+
readonly deviceName: string;
|
|
70
|
+
readonly deviceMAC: string;
|
|
71
|
+
readonly protocol: SmartCubeProtocolInfo;
|
|
72
|
+
readonly capabilities: SmartCubeCapabilities;
|
|
73
|
+
events$: Observable<SmartCubeEvent>;
|
|
74
|
+
sendCommand(command: SmartCubeCommand): Promise<void>;
|
|
75
|
+
disconnect(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
type MacAddressProvider = (device: BluetoothDevice, isFallbackCall?: boolean) => Promise<string | null>;
|
|
78
|
+
export type { SmartCubeEvent, SmartCubeEventMessage, SmartCubeMoveEvent, SmartCubeFaceletsEvent, SmartCubeGyroEvent, SmartCubeBatteryEvent, SmartCubeProtocolInfo, SmartCubeHardwareEvent, SmartCubeDisconnectEvent, SmartCubeCommand, SmartCubeCapabilities, SmartCubeConnection, MacAddressProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GanCubeMove } from './gan-cube-protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* Return current host clock timestamp with millisecond precision
|
|
4
|
+
* Use monotonic clock when available
|
|
5
|
+
* @returns Current host clock timestamp in milliseconds
|
|
6
|
+
*/
|
|
7
|
+
declare const now: () => number;
|
|
8
|
+
/**
|
|
9
|
+
* Use linear regression to fit timestamps reported by cube hardware with host device timestamps
|
|
10
|
+
* @param cubeMoves List representing window of cube moves to operate on
|
|
11
|
+
* @returns New copy of move list with fitted cubeTimestamp values
|
|
12
|
+
*/
|
|
13
|
+
declare function cubeTimestampLinearFit(cubeMoves: Array<GanCubeMove>): Array<GanCubeMove>;
|
|
14
|
+
/**
|
|
15
|
+
* Calculate time skew degree in percent between cube hardware and host device
|
|
16
|
+
* @param cubeMoves List representing window of cube moves to operate on
|
|
17
|
+
* @returns Time skew value in percent
|
|
18
|
+
*/
|
|
19
|
+
declare function cubeTimestampCalcSkew(cubeMoves: Array<GanCubeMove>): number;
|
|
20
|
+
/**
|
|
21
|
+
*
|
|
22
|
+
* Convert Corner/Edge Permutation/Orientation cube state to the Kociemba facelets representation string
|
|
23
|
+
*
|
|
24
|
+
* Example - solved state:
|
|
25
|
+
* cp = [0, 1, 2, 3, 4, 5, 6, 7]
|
|
26
|
+
* co = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
27
|
+
* ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
|
28
|
+
* eo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
29
|
+
* facelets = "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
|
|
30
|
+
* Example - state after F R moves made:
|
|
31
|
+
* cp = [0, 5, 2, 1, 7, 4, 6, 3]
|
|
32
|
+
* co = [1, 2, 0, 2, 1, 1, 0, 2]
|
|
33
|
+
* ep = [1, 9, 2, 3, 11, 8, 6, 7, 4, 5, 10, 0]
|
|
34
|
+
* eo = [1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]
|
|
35
|
+
* facelets = "UUFUUFLLFUUURRRRRRFFRFFDFFDRRBDDBDDBLLDLLDLLDLBBUBBUBB"
|
|
36
|
+
*
|
|
37
|
+
* @param cp Corner Permutation
|
|
38
|
+
* @param co Corner Orientation
|
|
39
|
+
* @param ep Egde Permutation
|
|
40
|
+
* @param eo Edge Orientation
|
|
41
|
+
* @returns Cube state in the Kociemba facelets representation string
|
|
42
|
+
*
|
|
43
|
+
*/
|
|
44
|
+
declare function toKociembaFacelets(cp: Array<number>, co: Array<number>, ep: Array<number>, eo: Array<number>): string;
|
|
45
|
+
export { now, cubeTimestampLinearFit, cubeTimestampCalcSkew, toKociembaFacelets, };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return current host clock timestamp with millisecond precision
|
|
3
|
+
* Use monotonic clock when available
|
|
4
|
+
* @returns Current host clock timestamp in milliseconds
|
|
5
|
+
*/
|
|
6
|
+
const now = typeof window != 'undefined' && typeof window.performance?.now == 'function' ?
|
|
7
|
+
() => Math.floor(window.performance.now()) :
|
|
8
|
+
typeof process != 'undefined' && typeof process.hrtime?.bigint == 'function' ?
|
|
9
|
+
() => Number(process.hrtime.bigint() / 1000000n) :
|
|
10
|
+
() => Date.now();
|
|
11
|
+
function linregress(X, Y) {
|
|
12
|
+
var sumX = 0;
|
|
13
|
+
var sumY = 0;
|
|
14
|
+
var sumXY = 0;
|
|
15
|
+
var sumXX = 0;
|
|
16
|
+
var sumYY = 0;
|
|
17
|
+
var n = 0;
|
|
18
|
+
for (var i = 0; i < X.length; i++) {
|
|
19
|
+
var x = X[i];
|
|
20
|
+
var y = Y[i];
|
|
21
|
+
if (x == null || y == null) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
n++;
|
|
25
|
+
sumX += x;
|
|
26
|
+
sumY += y;
|
|
27
|
+
sumXY += x * y;
|
|
28
|
+
sumXX += x * x;
|
|
29
|
+
sumYY += y * y;
|
|
30
|
+
}
|
|
31
|
+
var varX = n * sumXX - sumX * sumX;
|
|
32
|
+
var covXY = n * sumXY - sumX * sumY;
|
|
33
|
+
var slope = varX < 1e-3 ? 1 : covXY / varX;
|
|
34
|
+
var intercept = n < 1 ? 0 : sumY / n - slope * sumX / n;
|
|
35
|
+
return [slope, intercept];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Use linear regression to fit timestamps reported by cube hardware with host device timestamps
|
|
39
|
+
* @param cubeMoves List representing window of cube moves to operate on
|
|
40
|
+
* @returns New copy of move list with fitted cubeTimestamp values
|
|
41
|
+
*/
|
|
42
|
+
function cubeTimestampLinearFit(cubeMoves) {
|
|
43
|
+
var res = [];
|
|
44
|
+
// Calculate and fix timestamp values for missed and recovered cube moves.
|
|
45
|
+
if (cubeMoves.length >= 2) {
|
|
46
|
+
// 1st pass - tail-to-head, align missed move cube timestamps to next move -50ms
|
|
47
|
+
for (let i = cubeMoves.length - 1; i > 0; i--) {
|
|
48
|
+
if (cubeMoves[i].cubeTimestamp != null && cubeMoves[i - 1].cubeTimestamp == null)
|
|
49
|
+
cubeMoves[i - 1].cubeTimestamp = cubeMoves[i].cubeTimestamp - 50;
|
|
50
|
+
}
|
|
51
|
+
// 2nd pass - head-to-tail, align missed move cube timestamp to prev move +50ms
|
|
52
|
+
for (let i = 0; i < cubeMoves.length - 1; i++) {
|
|
53
|
+
if (cubeMoves[i].cubeTimestamp != null && cubeMoves[i + 1].cubeTimestamp == null)
|
|
54
|
+
cubeMoves[i + 1].cubeTimestamp = cubeMoves[i].cubeTimestamp + 50;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Apply linear regression to the cube timestamps
|
|
58
|
+
if (cubeMoves.length > 0) {
|
|
59
|
+
var [slope, intercept] = linregress(cubeMoves.map(m => m.cubeTimestamp), cubeMoves.map(m => m.localTimestamp));
|
|
60
|
+
var first = Math.round(slope * cubeMoves[0].cubeTimestamp + intercept);
|
|
61
|
+
cubeMoves.forEach(m => {
|
|
62
|
+
res.push({
|
|
63
|
+
face: m.face,
|
|
64
|
+
direction: m.direction,
|
|
65
|
+
move: m.move,
|
|
66
|
+
localTimestamp: m.localTimestamp,
|
|
67
|
+
cubeTimestamp: Math.round(slope * m.cubeTimestamp + intercept) - first
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return res;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Calculate time skew degree in percent between cube hardware and host device
|
|
75
|
+
* @param cubeMoves List representing window of cube moves to operate on
|
|
76
|
+
* @returns Time skew value in percent
|
|
77
|
+
*/
|
|
78
|
+
function cubeTimestampCalcSkew(cubeMoves) {
|
|
79
|
+
if (!cubeMoves.length)
|
|
80
|
+
return 0;
|
|
81
|
+
var [slope] = linregress(cubeMoves.map(m => m.localTimestamp), cubeMoves.map(m => m.cubeTimestamp));
|
|
82
|
+
return Math.round((slope - 1) * 100000) / 1000;
|
|
83
|
+
}
|
|
84
|
+
const CORNER_FACELET_MAP = [
|
|
85
|
+
[8, 9, 20], // URF
|
|
86
|
+
[6, 18, 38], // UFL
|
|
87
|
+
[0, 36, 47], // ULB
|
|
88
|
+
[2, 45, 11], // UBR
|
|
89
|
+
[29, 26, 15], // DFR
|
|
90
|
+
[27, 44, 24], // DLF
|
|
91
|
+
[33, 53, 42], // DBL
|
|
92
|
+
[35, 17, 51] // DRB
|
|
93
|
+
];
|
|
94
|
+
const EDGE_FACELET_MAP = [
|
|
95
|
+
[5, 10], // UR
|
|
96
|
+
[7, 19], // UF
|
|
97
|
+
[3, 37], // UL
|
|
98
|
+
[1, 46], // UB
|
|
99
|
+
[32, 16], // DR
|
|
100
|
+
[28, 25], // DF
|
|
101
|
+
[30, 43], // DL
|
|
102
|
+
[34, 52], // DB
|
|
103
|
+
[23, 12], // FR
|
|
104
|
+
[21, 41], // FL
|
|
105
|
+
[50, 39], // BL
|
|
106
|
+
[48, 14] // BR
|
|
107
|
+
];
|
|
108
|
+
/**
|
|
109
|
+
*
|
|
110
|
+
* Convert Corner/Edge Permutation/Orientation cube state to the Kociemba facelets representation string
|
|
111
|
+
*
|
|
112
|
+
* Example - solved state:
|
|
113
|
+
* cp = [0, 1, 2, 3, 4, 5, 6, 7]
|
|
114
|
+
* co = [0, 0, 0, 0, 0, 0, 0, 0]
|
|
115
|
+
* ep = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
|
116
|
+
* eo = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
117
|
+
* facelets = "UUUUUUUUURRRRRRRRRFFFFFFFFFDDDDDDDDDLLLLLLLLLBBBBBBBBB"
|
|
118
|
+
* Example - state after F R moves made:
|
|
119
|
+
* cp = [0, 5, 2, 1, 7, 4, 6, 3]
|
|
120
|
+
* co = [1, 2, 0, 2, 1, 1, 0, 2]
|
|
121
|
+
* ep = [1, 9, 2, 3, 11, 8, 6, 7, 4, 5, 10, 0]
|
|
122
|
+
* eo = [1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0]
|
|
123
|
+
* facelets = "UUFUUFLLFUUURRRRRRFFRFFDFFDRRBDDBDDBLLDLLDLLDLBBUBBUBB"
|
|
124
|
+
*
|
|
125
|
+
* @param cp Corner Permutation
|
|
126
|
+
* @param co Corner Orientation
|
|
127
|
+
* @param ep Egde Permutation
|
|
128
|
+
* @param eo Edge Orientation
|
|
129
|
+
* @returns Cube state in the Kociemba facelets representation string
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
function toKociembaFacelets(cp, co, ep, eo) {
|
|
133
|
+
var faces = "URFDLB";
|
|
134
|
+
var facelets = [];
|
|
135
|
+
for (let i = 0; i < 54; i++) {
|
|
136
|
+
facelets[i] = faces[~~(i / 9)];
|
|
137
|
+
}
|
|
138
|
+
for (let i = 0; i < 8; i++) {
|
|
139
|
+
for (let p = 0; p < 3; p++) {
|
|
140
|
+
facelets[CORNER_FACELET_MAP[i][(p + co[i]) % 3]] = faces[~~(CORNER_FACELET_MAP[cp[i]][p] / 9)];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (let i = 0; i < 12; i++) {
|
|
144
|
+
for (let p = 0; p < 2; p++) {
|
|
145
|
+
facelets[EDGE_FACELET_MAP[i][(p + eo[i]) % 2]] = faces[~~(EDGE_FACELET_MAP[ep[i]][p] / 9)];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return facelets.join('');
|
|
149
|
+
}
|
|
150
|
+
export { now, cubeTimestampLinearFit, cubeTimestampCalcSkew, toKociembaFacelets, };
|