@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,38 @@
|
|
|
1
|
+
/// <reference types="web-bluetooth" />
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import type { GanCubeCommand, GanCubeConnection, GanCubeEvent } from './gan-cube-protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* GAN 356i “API v1”: primary service `fff0` + Device Information for key derivation.
|
|
6
|
+
*/
|
|
7
|
+
export declare class GanGen1CubeConnection implements GanCubeConnection {
|
|
8
|
+
readonly deviceMAC = "";
|
|
9
|
+
readonly events$: Subject<GanCubeEvent>;
|
|
10
|
+
private readonly encrypter;
|
|
11
|
+
private readonly device;
|
|
12
|
+
private readonly chrState;
|
|
13
|
+
private readonly chrMoves;
|
|
14
|
+
private readonly chrFacelets;
|
|
15
|
+
private readonly chrBattery;
|
|
16
|
+
private readonly chrGyroNotify;
|
|
17
|
+
private polling;
|
|
18
|
+
private prevMoveCnt;
|
|
19
|
+
private movePollTicks;
|
|
20
|
+
private batteryPollTicks;
|
|
21
|
+
private pollFailures;
|
|
22
|
+
private teardown;
|
|
23
|
+
private lastBatteryLevel;
|
|
24
|
+
private forceNextBatteryEmission;
|
|
25
|
+
private readonly onGattDisconnected;
|
|
26
|
+
private constructor();
|
|
27
|
+
get deviceName(): string;
|
|
28
|
+
static create(device: BluetoothDevice): Promise<GanGen1CubeConnection>;
|
|
29
|
+
private onGyroNotify;
|
|
30
|
+
private emitBatteryLevel;
|
|
31
|
+
private readBattery;
|
|
32
|
+
private readInitialState;
|
|
33
|
+
private schedulePoll;
|
|
34
|
+
private pollLoop;
|
|
35
|
+
private handleDisconnect;
|
|
36
|
+
sendCubeCommand(command: GanCubeCommand): Promise<void>;
|
|
37
|
+
disconnect(): Promise<void>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { AES } from 'aes-js';
|
|
2
|
+
import { decompressFromEncodedURIComponent } from 'lz-string';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
import * as def from './gan-cube-definitions.js';
|
|
5
|
+
import { now } from './utils.js';
|
|
6
|
+
class GanGen1Aes {
|
|
7
|
+
aes;
|
|
8
|
+
constructor(keyBytes) {
|
|
9
|
+
this.aes = new AES([...keyBytes]);
|
|
10
|
+
}
|
|
11
|
+
decrypt(data) {
|
|
12
|
+
if (data.length < 16)
|
|
13
|
+
throw new Error('Invalid data length');
|
|
14
|
+
const t = Array.from(data);
|
|
15
|
+
if (t.length > 16) {
|
|
16
|
+
const i = t.length - 16;
|
|
17
|
+
const n = this.aes.decrypt(t.slice(i, i + 16));
|
|
18
|
+
for (let r = 0; r < 16; r++)
|
|
19
|
+
t[r + i] = n[r];
|
|
20
|
+
}
|
|
21
|
+
const s = this.aes.decrypt(t.slice(0, 16));
|
|
22
|
+
for (let r = 0; r < 16; r++)
|
|
23
|
+
t[r] = s[r];
|
|
24
|
+
return new Uint8Array(t);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function deriveGen1Key(fwVersion, hw) {
|
|
28
|
+
const idx = (fwVersion >> 8) & 255;
|
|
29
|
+
const compressed = def.GAN_GEN1_COMPRESSED_KEYS[idx] ?? def.GAN_GEN1_COMPRESSED_KEYS[0];
|
|
30
|
+
if (!compressed)
|
|
31
|
+
return null;
|
|
32
|
+
const json = decompressFromEncodedURIComponent(compressed);
|
|
33
|
+
if (!json)
|
|
34
|
+
return null;
|
|
35
|
+
let arr;
|
|
36
|
+
try {
|
|
37
|
+
arr = JSON.parse(json);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(arr) || arr.length < 6)
|
|
43
|
+
return null;
|
|
44
|
+
if (hw.byteLength < 6)
|
|
45
|
+
return null;
|
|
46
|
+
for (let s = 0; s < 6; s++) {
|
|
47
|
+
arr[s] = (arr[s] + hw.getUint8(5 - s)) & 255;
|
|
48
|
+
}
|
|
49
|
+
if (arr.length < 16)
|
|
50
|
+
return null;
|
|
51
|
+
return new Uint8Array(arr.slice(0, 16));
|
|
52
|
+
}
|
|
53
|
+
function gyroFromState(t) {
|
|
54
|
+
if (t.length < 6)
|
|
55
|
+
return null;
|
|
56
|
+
let x0 = t[0] | (t[1] << 8);
|
|
57
|
+
let x1 = t[2] | (t[3] << 8);
|
|
58
|
+
let x2 = t[4] | (t[5] << 8);
|
|
59
|
+
if (x0 > 32767)
|
|
60
|
+
x0 -= 65536;
|
|
61
|
+
if (x1 > 32767)
|
|
62
|
+
x1 -= 65536;
|
|
63
|
+
if (x2 > 32767)
|
|
64
|
+
x2 -= 65536;
|
|
65
|
+
const r = x0 / 16384;
|
|
66
|
+
const s = x1 / 16384;
|
|
67
|
+
const a = x2 / 16384;
|
|
68
|
+
const o = 1 - r * r - s * s - a * a;
|
|
69
|
+
return { x: r, y: a, z: -s, w: o > 0 ? Math.sqrt(o) : 0 };
|
|
70
|
+
}
|
|
71
|
+
function parseGen1Facelets(t) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (let i = 0; i < t.length - 2; i += 3) {
|
|
74
|
+
const n = (t[1 ^ i] << 16) | (t[(i + 1) ^ 1] << 8) | t[(i + 2) ^ 1];
|
|
75
|
+
for (let r = 21; r >= 0; r -= 3) {
|
|
76
|
+
out.push('URFDLB'.charAt((n >> r) & 7));
|
|
77
|
+
if (r === 12)
|
|
78
|
+
out.push('URFDLB'.charAt(i / 3));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out.join('');
|
|
82
|
+
}
|
|
83
|
+
/** Placeholder cubelets state when gen1 only supplies a facelet string. */
|
|
84
|
+
const GEN1_SOLVED_STATE = {
|
|
85
|
+
CP: [0, 1, 2, 3, 4, 5, 6, 7],
|
|
86
|
+
CO: [0, 0, 0, 0, 0, 0, 0, 0],
|
|
87
|
+
EP: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
88
|
+
EO: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* GAN 356i “API v1”: primary service `fff0` + Device Information for key derivation.
|
|
92
|
+
*/
|
|
93
|
+
export class GanGen1CubeConnection {
|
|
94
|
+
deviceMAC = '';
|
|
95
|
+
events$;
|
|
96
|
+
encrypter;
|
|
97
|
+
device;
|
|
98
|
+
chrState;
|
|
99
|
+
chrMoves;
|
|
100
|
+
chrFacelets;
|
|
101
|
+
chrBattery;
|
|
102
|
+
chrGyroNotify;
|
|
103
|
+
polling = false;
|
|
104
|
+
prevMoveCnt = -1;
|
|
105
|
+
movePollTicks = 0;
|
|
106
|
+
batteryPollTicks = 0;
|
|
107
|
+
pollFailures = 0;
|
|
108
|
+
teardown = false;
|
|
109
|
+
lastBatteryLevel = null;
|
|
110
|
+
forceNextBatteryEmission = false;
|
|
111
|
+
onGattDisconnected;
|
|
112
|
+
constructor(device, encrypter, events$, chrState, chrMoves, chrFacelets, chrBattery, chrGyroNotify) {
|
|
113
|
+
this.device = device;
|
|
114
|
+
this.encrypter = encrypter;
|
|
115
|
+
this.events$ = events$;
|
|
116
|
+
this.chrState = chrState;
|
|
117
|
+
this.chrMoves = chrMoves;
|
|
118
|
+
this.chrFacelets = chrFacelets;
|
|
119
|
+
this.chrBattery = chrBattery;
|
|
120
|
+
this.chrGyroNotify = chrGyroNotify;
|
|
121
|
+
this.onGattDisconnected = () => void this.handleDisconnect();
|
|
122
|
+
this.device.addEventListener('gattserverdisconnected', this.onGattDisconnected);
|
|
123
|
+
}
|
|
124
|
+
get deviceName() {
|
|
125
|
+
return this.device.name || 'GAN356i v1';
|
|
126
|
+
}
|
|
127
|
+
static async create(device) {
|
|
128
|
+
const gatt = device.gatt;
|
|
129
|
+
if (!gatt?.connected) {
|
|
130
|
+
throw new Error('GATT must be connected before GAN gen1 setup');
|
|
131
|
+
}
|
|
132
|
+
const deviceInfo = await gatt.getPrimaryService(def.GAN_GEN1_DEVICE_INFO_SERVICE);
|
|
133
|
+
const fwChar = await deviceInfo.getCharacteristic(def.GAN_GEN1_CHR_FIRMWARE);
|
|
134
|
+
const hwChar = await deviceInfo.getCharacteristic(def.GAN_GEN1_CHR_HARDWARE);
|
|
135
|
+
const fw = await fwChar.readValue();
|
|
136
|
+
const n = fw.byteLength >= 3
|
|
137
|
+
? (fw.getUint8(0) << 16) | (fw.getUint8(1) << 8) | fw.getUint8(2)
|
|
138
|
+
: 0;
|
|
139
|
+
if (!(n > 65543 && (16776704 & n) === 65536)) {
|
|
140
|
+
throw new Error(`Invalid firmware version: 0x${n.toString(16)}`);
|
|
141
|
+
}
|
|
142
|
+
const hwRaw = await hwChar.readValue();
|
|
143
|
+
const keyArr = deriveGen1Key(n, hwRaw);
|
|
144
|
+
if (!keyArr) {
|
|
145
|
+
throw new Error('Invalid encryption key');
|
|
146
|
+
}
|
|
147
|
+
const encrypter = new GanGen1Aes(keyArr);
|
|
148
|
+
const primary = await gatt.getPrimaryService(def.GAN_GEN1_PRIMARY_SERVICE);
|
|
149
|
+
const chrState = await primary.getCharacteristic(def.GAN_GEN1_CHR_STATE);
|
|
150
|
+
const chrMoves = await primary.getCharacteristic(def.GAN_GEN1_CHR_MOVES);
|
|
151
|
+
const chrFacelets = await primary.getCharacteristic(def.GAN_GEN1_CHR_FACELETS);
|
|
152
|
+
const chrBattery = await primary.getCharacteristic(def.GAN_GEN1_CHR_BATTERY);
|
|
153
|
+
const chrGyroNotify = await primary.getCharacteristic(def.GAN_GEN1_CHR_GYRO_NOTIFY);
|
|
154
|
+
const events$ = new Subject();
|
|
155
|
+
const conn = new GanGen1CubeConnection(device, encrypter, events$, chrState, chrMoves, chrFacelets, chrBattery, chrGyroNotify);
|
|
156
|
+
chrGyroNotify.addEventListener('characteristicvaluechanged', conn.onGyroNotify);
|
|
157
|
+
await chrGyroNotify.startNotifications();
|
|
158
|
+
await conn.readInitialState();
|
|
159
|
+
await conn.readBattery();
|
|
160
|
+
conn.polling = true;
|
|
161
|
+
conn.schedulePoll(0);
|
|
162
|
+
return conn;
|
|
163
|
+
}
|
|
164
|
+
onGyroNotify = (evt) => {
|
|
165
|
+
try {
|
|
166
|
+
const chr = evt.target;
|
|
167
|
+
const e = chr.value;
|
|
168
|
+
if (!e || e.byteLength < 16)
|
|
169
|
+
return;
|
|
170
|
+
const dec = this.encrypter.decrypt(new Uint8Array(e.buffer, e.byteOffset, e.byteLength));
|
|
171
|
+
const q = gyroFromState(dec);
|
|
172
|
+
if (q) {
|
|
173
|
+
this.events$.next({ type: 'GYRO', timestamp: now(), quaternion: q });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* ignore corrupt frame */
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
emitBatteryLevel(rawLevel, timestamp = now()) {
|
|
181
|
+
if (!Number.isFinite(rawLevel)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const batteryLevel = Math.min(100, Math.max(0, Math.round(rawLevel)));
|
|
185
|
+
const forceEmission = this.forceNextBatteryEmission;
|
|
186
|
+
this.forceNextBatteryEmission = false;
|
|
187
|
+
if (!forceEmission && this.lastBatteryLevel === batteryLevel) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.lastBatteryLevel = batteryLevel;
|
|
191
|
+
this.events$.next({
|
|
192
|
+
type: 'BATTERY',
|
|
193
|
+
batteryLevel,
|
|
194
|
+
timestamp,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async readBattery() {
|
|
198
|
+
try {
|
|
199
|
+
const e = await this.chrBattery.readValue();
|
|
200
|
+
const t = this.encrypter.decrypt(new Uint8Array(e.buffer, e.byteOffset, e.byteLength));
|
|
201
|
+
if (t.length < 8)
|
|
202
|
+
return;
|
|
203
|
+
this.emitBatteryLevel(t[7]);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
/* ignore */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async readInitialState() {
|
|
210
|
+
try {
|
|
211
|
+
const e = await this.chrFacelets.readValue();
|
|
212
|
+
const t = this.encrypter.decrypt(new Uint8Array(e.buffer, e.byteOffset, e.byteLength));
|
|
213
|
+
this.events$.next({
|
|
214
|
+
timestamp: now(),
|
|
215
|
+
type: 'FACELETS',
|
|
216
|
+
serial: 0,
|
|
217
|
+
facelets: parseGen1Facelets(t),
|
|
218
|
+
state: {
|
|
219
|
+
CP: [...GEN1_SOLVED_STATE.CP],
|
|
220
|
+
CO: [...GEN1_SOLVED_STATE.CO],
|
|
221
|
+
EP: [...GEN1_SOLVED_STATE.EP],
|
|
222
|
+
EO: [...GEN1_SOLVED_STATE.EO],
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* ignore */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
schedulePoll(delayMs) {
|
|
231
|
+
if (!this.polling || this.teardown)
|
|
232
|
+
return;
|
|
233
|
+
setTimeout(() => void this.pollLoop(), delayMs);
|
|
234
|
+
}
|
|
235
|
+
async pollLoop() {
|
|
236
|
+
if (!this.polling || this.teardown)
|
|
237
|
+
return;
|
|
238
|
+
try {
|
|
239
|
+
const e = await this.chrState.readValue();
|
|
240
|
+
const t = this.encrypter.decrypt(new Uint8Array(e.buffer, e.byteOffset, e.byteLength));
|
|
241
|
+
this.pollFailures = 0;
|
|
242
|
+
const q = gyroFromState(t);
|
|
243
|
+
if (q) {
|
|
244
|
+
this.events$.next({ type: 'GYRO', timestamp: now(), quaternion: q });
|
|
245
|
+
}
|
|
246
|
+
const moveCnt = t[12];
|
|
247
|
+
if (this.prevMoveCnt === -1) {
|
|
248
|
+
this.prevMoveCnt = moveCnt;
|
|
249
|
+
}
|
|
250
|
+
else if (moveCnt !== this.prevMoveCnt) {
|
|
251
|
+
let o = (moveCnt - this.prevMoveCnt) & 255;
|
|
252
|
+
if (o > 6)
|
|
253
|
+
o = 6;
|
|
254
|
+
const moves = [];
|
|
255
|
+
for (let l = 0; l < 6; l++) {
|
|
256
|
+
const u = t[13 + l];
|
|
257
|
+
moves.unshift('URFDLB'.charAt(~~(u / 3)) + " 2'".charAt(u % 3));
|
|
258
|
+
}
|
|
259
|
+
const moveData = await this.chrMoves.readValue();
|
|
260
|
+
const mt = this.encrypter.decrypt(new Uint8Array(moveData.buffer, moveData.byteOffset, moveData.byteLength));
|
|
261
|
+
const stamps = [];
|
|
262
|
+
for (let r = 0; r < 9; r++) {
|
|
263
|
+
stamps.unshift(mt[2 * r + 1] | (mt[2 * r + 2] << 8));
|
|
264
|
+
}
|
|
265
|
+
const ts = now();
|
|
266
|
+
for (let r = o - 1; r >= 0; r--) {
|
|
267
|
+
const d = moves[r]?.trim();
|
|
268
|
+
if (!d)
|
|
269
|
+
continue;
|
|
270
|
+
const f = 'URFDLB'.indexOf(d[0]);
|
|
271
|
+
const h = d.endsWith("'") ? 1 : 0;
|
|
272
|
+
this.events$.next({
|
|
273
|
+
timestamp: ts,
|
|
274
|
+
type: 'MOVE',
|
|
275
|
+
serial: (moveCnt - r) & 255,
|
|
276
|
+
face: f,
|
|
277
|
+
direction: h,
|
|
278
|
+
move: d,
|
|
279
|
+
cubeTimestamp: stamps[r] ?? null,
|
|
280
|
+
localTimestamp: ts,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
this.prevMoveCnt = moveCnt;
|
|
284
|
+
}
|
|
285
|
+
this.movePollTicks++;
|
|
286
|
+
if (this.movePollTicks >= 50) {
|
|
287
|
+
this.movePollTicks = 0;
|
|
288
|
+
await this.readInitialState();
|
|
289
|
+
}
|
|
290
|
+
this.batteryPollTicks += 30;
|
|
291
|
+
if (this.batteryPollTicks >= 60_000) {
|
|
292
|
+
this.batteryPollTicks = 0;
|
|
293
|
+
await this.readBattery();
|
|
294
|
+
}
|
|
295
|
+
this.schedulePoll(30);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
this.pollFailures++;
|
|
299
|
+
const wait = Math.min(500 * 2 ** Math.min(this.pollFailures, 4), 2000);
|
|
300
|
+
this.schedulePoll(wait);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async handleDisconnect() {
|
|
304
|
+
if (this.teardown)
|
|
305
|
+
return;
|
|
306
|
+
this.teardown = true;
|
|
307
|
+
this.polling = false;
|
|
308
|
+
this.lastBatteryLevel = null;
|
|
309
|
+
this.forceNextBatteryEmission = false;
|
|
310
|
+
this.device.removeEventListener('gattserverdisconnected', this.onGattDisconnected);
|
|
311
|
+
try {
|
|
312
|
+
this.chrGyroNotify.removeEventListener('characteristicvaluechanged', this.onGyroNotify);
|
|
313
|
+
await this.chrGyroNotify.stopNotifications().catch(() => { });
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* ignore */
|
|
317
|
+
}
|
|
318
|
+
this.events$.next({ timestamp: now(), type: 'DISCONNECT' });
|
|
319
|
+
this.events$.complete();
|
|
320
|
+
}
|
|
321
|
+
async sendCubeCommand(command) {
|
|
322
|
+
switch (command.type) {
|
|
323
|
+
case 'REQUEST_BATTERY':
|
|
324
|
+
this.forceNextBatteryEmission = true;
|
|
325
|
+
await this.readBattery();
|
|
326
|
+
break;
|
|
327
|
+
case 'REQUEST_FACELETS':
|
|
328
|
+
await this.readInitialState();
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async disconnect() {
|
|
335
|
+
await this.handleDisconnect();
|
|
336
|
+
if (this.device.gatt?.connected) {
|
|
337
|
+
this.device.gatt.disconnect();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural checks on decrypted GAN gen2/3/4 payloads (after AES).
|
|
3
|
+
*/
|
|
4
|
+
export declare class GanBitReader {
|
|
5
|
+
private readonly bits;
|
|
6
|
+
constructor(message: Uint8Array | readonly number[]);
|
|
7
|
+
getBitWord(offset: number, bitLength: number, littleEndian?: boolean): number;
|
|
8
|
+
}
|
|
9
|
+
export declare function isValidGanGen2Packet(e: Uint8Array | number[]): boolean;
|
|
10
|
+
export declare function isValidGanGen3Packet(e: Uint8Array | number[]): boolean;
|
|
11
|
+
export declare function isValidGanGen4Packet(e: Uint8Array | number[]): boolean;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural checks on decrypted GAN gen2/3/4 payloads (after AES).
|
|
3
|
+
*/
|
|
4
|
+
export class GanBitReader {
|
|
5
|
+
bits;
|
|
6
|
+
constructor(message) {
|
|
7
|
+
this.bits = Array.from(message, (b) => (b + 256).toString(2).slice(1)).join('');
|
|
8
|
+
}
|
|
9
|
+
getBitWord(offset, bitLength, littleEndian = false) {
|
|
10
|
+
if (bitLength <= 8) {
|
|
11
|
+
return parseInt(this.bits.slice(offset, offset + bitLength), 2);
|
|
12
|
+
}
|
|
13
|
+
if (bitLength === 16 || bitLength === 32) {
|
|
14
|
+
const buf = new Uint8Array(bitLength / 8);
|
|
15
|
+
for (let i = 0; i < buf.length; i++) {
|
|
16
|
+
buf[i] = parseInt(this.bits.slice(8 * i + offset, 8 * i + offset + 8), 2);
|
|
17
|
+
}
|
|
18
|
+
const dv = new DataView(buf.buffer);
|
|
19
|
+
return bitLength === 16 ? dv.getUint16(0, littleEndian) : dv.getUint32(0, littleEndian);
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Invalid BitWord size');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function isValidGanGen2Packet(e) {
|
|
25
|
+
if (!e || e.length < 16)
|
|
26
|
+
return false;
|
|
27
|
+
try {
|
|
28
|
+
const t = new GanBitReader(e);
|
|
29
|
+
const i = t.getBitWord(0, 4);
|
|
30
|
+
if (![1, 2, 4, 5, 9, 13].includes(i))
|
|
31
|
+
return false;
|
|
32
|
+
if (i === 1) {
|
|
33
|
+
const a = t.getBitWord(4, 16);
|
|
34
|
+
const b = t.getBitWord(20, 16);
|
|
35
|
+
const c = t.getBitWord(36, 16);
|
|
36
|
+
const d = t.getBitWord(52, 16);
|
|
37
|
+
if (a === 0 && b === 0 && c === 0 && d === 0)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
else if (i === 2) {
|
|
41
|
+
for (let j = 0; j < 7; j++) {
|
|
42
|
+
const v = t.getBitWord(12 + 5 * j, 4);
|
|
43
|
+
if (v > 5)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (i === 4) {
|
|
48
|
+
let s = 0;
|
|
49
|
+
for (let j = 0; j < 7; j++)
|
|
50
|
+
s += t.getBitWord(12 + 3 * j, 3);
|
|
51
|
+
if (s > 28)
|
|
52
|
+
return false;
|
|
53
|
+
let x = 0;
|
|
54
|
+
for (let j = 0; j < 11; j++)
|
|
55
|
+
x += t.getBitWord(47 + 4 * j, 4);
|
|
56
|
+
if (x > 66)
|
|
57
|
+
return false;
|
|
58
|
+
// The cube transmits 11 of 12 EOs; the 12th is reconstructed from the
|
|
59
|
+
// EO parity invariant. The 11-bit window can sum to either parity, so
|
|
60
|
+
// there is nothing to validate here without knowing the 12th bit.
|
|
61
|
+
}
|
|
62
|
+
else if (i === 9) {
|
|
63
|
+
if (t.getBitWord(8, 8) > 100)
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
else if (i === 5) {
|
|
67
|
+
for (let j = 0; j < 8; j++) {
|
|
68
|
+
const v = t.getBitWord(8 * j + 40, 8);
|
|
69
|
+
if (v !== 0 && (v < 32 || v > 126))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function isValidGanGen3Packet(e) {
|
|
80
|
+
if (!e || e.length < 16)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const t = new GanBitReader(e);
|
|
84
|
+
const i = t.getBitWord(0, 8);
|
|
85
|
+
const n = t.getBitWord(8, 8);
|
|
86
|
+
const r = t.getBitWord(16, 8);
|
|
87
|
+
if (i !== 85 || r === 0 || ![1, 2, 6, 7, 16, 17].includes(n))
|
|
88
|
+
return false;
|
|
89
|
+
if (n === 1) {
|
|
90
|
+
const f = t.getBitWord(74, 6);
|
|
91
|
+
if ([2, 32, 8, 1, 16, 4].indexOf(f) < 0)
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function isValidGanGen4Packet(e) {
|
|
101
|
+
if (!e || e.length < 16)
|
|
102
|
+
return false;
|
|
103
|
+
try {
|
|
104
|
+
const t = new GanBitReader(e);
|
|
105
|
+
const i = t.getBitWord(0, 8);
|
|
106
|
+
if (![1, 209, 237, 236, 239, 234, 250, 251, 252, 253, 254].includes(i))
|
|
107
|
+
return false;
|
|
108
|
+
if (i === 1) {
|
|
109
|
+
const f = t.getBitWord(66, 6);
|
|
110
|
+
if ([2, 32, 8, 1, 16, 4].indexOf(f) < 0)
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 6-byte MAC as reversed salt for GAN gen2–4 AES; throws if invalid. */
|
|
2
|
+
export function macStringToSaltOrThrow(mac) {
|
|
3
|
+
const parts = mac.split(/[:-\s]+/).filter(Boolean);
|
|
4
|
+
if (parts.length !== 6) {
|
|
5
|
+
throw new Error('GAN gen2–4 requires a valid 6-byte Bluetooth MAC (e.g. aa:bb:cc:dd:ee:ff).');
|
|
6
|
+
}
|
|
7
|
+
const bytes = parts.map((c) => {
|
|
8
|
+
const n = parseInt(c, 16);
|
|
9
|
+
if (!Number.isFinite(n) || n < 0 || n > 255) {
|
|
10
|
+
throw new Error('Invalid MAC address segment.');
|
|
11
|
+
}
|
|
12
|
+
return n;
|
|
13
|
+
});
|
|
14
|
+
return new Uint8Array(bytes.reverse());
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="web-bluetooth" />
|
|
2
|
+
import { GanCubeConnection, GanCubeCommand, GanCubeEvent, GanCubeMove } from './gan-cube-protocol.js';
|
|
3
|
+
/**
|
|
4
|
+
* Type representing function interface to implement custom MAC address provider
|
|
5
|
+
* @param device Current BluetoothDevice selected by user.
|
|
6
|
+
* @param isFallbackCall Flag indicating this is final and last resort call for MAC address.
|
|
7
|
+
* If this flag is not set, custom provider can return null instead of MAC,
|
|
8
|
+
* in such case library will try to read MAC automatically.
|
|
9
|
+
*/
|
|
10
|
+
type MacAddressProvider = (device: BluetoothDevice, isFallbackCall?: boolean) => Promise<string | null>;
|
|
11
|
+
/**
|
|
12
|
+
* Initiate new connection with the GAN Smart Cube device
|
|
13
|
+
* @param customMacAddressProvider Optional custom provider for cube MAC address
|
|
14
|
+
* @returns Object representing connection API and state
|
|
15
|
+
*/
|
|
16
|
+
declare function connectGanCube(customMacAddressProvider?: MacAddressProvider): Promise<GanCubeConnection>;
|
|
17
|
+
export type { MacAddressProvider, GanCubeConnection, GanCubeCommand, GanCubeEvent, GanCubeMove };
|
|
18
|
+
export { connectGanCube };
|