@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,846 @@
|
|
|
1
|
+
import { now, toKociembaFacelets } from './utils.js';
|
|
2
|
+
import { GattWriteQueue } from './gan-write-queue.js';
|
|
3
|
+
import { writeGattCharacteristicValue } from './gatt-characteristic-write.js';
|
|
4
|
+
import { Subject } from 'rxjs';
|
|
5
|
+
;
|
|
6
|
+
/** Calculate sum of all numbers in array */
|
|
7
|
+
const sum = arr => arr.reduce((a, v) => a + v, 0);
|
|
8
|
+
/**
|
|
9
|
+
* Implementation of classic command/response connection with GAN Smart Cube device
|
|
10
|
+
*/
|
|
11
|
+
class GanCubeClassicConnection {
|
|
12
|
+
device;
|
|
13
|
+
commandCharacteristic;
|
|
14
|
+
stateCharacteristic;
|
|
15
|
+
encrypter;
|
|
16
|
+
driver;
|
|
17
|
+
events$;
|
|
18
|
+
validateDecrypted;
|
|
19
|
+
writeQueue = new GattWriteQueue();
|
|
20
|
+
disconnectOnce = false;
|
|
21
|
+
constructor(device, commandCharacteristic, stateCharacteristic, encrypter, driver, validateDecrypted) {
|
|
22
|
+
this.device = device;
|
|
23
|
+
this.commandCharacteristic = commandCharacteristic;
|
|
24
|
+
this.stateCharacteristic = stateCharacteristic;
|
|
25
|
+
this.encrypter = encrypter;
|
|
26
|
+
this.driver = driver;
|
|
27
|
+
this.validateDecrypted = validateDecrypted;
|
|
28
|
+
this.events$ = new Subject();
|
|
29
|
+
}
|
|
30
|
+
static async create(device, commandCharacteristic, stateCharacteristic, encrypter, driver, options) {
|
|
31
|
+
var conn = new GanCubeClassicConnection(device, commandCharacteristic, stateCharacteristic, encrypter, driver, options?.validateDecrypted);
|
|
32
|
+
conn.device.addEventListener('gattserverdisconnected', conn.onDisconnect);
|
|
33
|
+
conn.stateCharacteristic.addEventListener('characteristicvaluechanged', conn.onStateUpdate);
|
|
34
|
+
await conn.stateCharacteristic.startNotifications();
|
|
35
|
+
return conn;
|
|
36
|
+
}
|
|
37
|
+
get deviceName() {
|
|
38
|
+
return this.device.name || "GAN-XXXX";
|
|
39
|
+
}
|
|
40
|
+
get deviceMAC() {
|
|
41
|
+
return this.device.mac || "00:00:00:00:00:00";
|
|
42
|
+
}
|
|
43
|
+
async sendCommandMessage(message) {
|
|
44
|
+
var encryptedMessage = this.encrypter.encrypt(message);
|
|
45
|
+
return this.writeQueue.enqueue(() => writeGattCharacteristicValue(this.commandCharacteristic, encryptedMessage));
|
|
46
|
+
}
|
|
47
|
+
onStateUpdate = async (evt) => {
|
|
48
|
+
try {
|
|
49
|
+
var characteristic = evt.target;
|
|
50
|
+
var eventMessage = characteristic.value;
|
|
51
|
+
if (!eventMessage || eventMessage.byteLength < 16)
|
|
52
|
+
return;
|
|
53
|
+
var raw = new Uint8Array(eventMessage.buffer, eventMessage.byteOffset, eventMessage.byteLength);
|
|
54
|
+
var decryptedMessage = this.encrypter.decrypt(raw);
|
|
55
|
+
if (this.validateDecrypted && !this.validateDecrypted(decryptedMessage))
|
|
56
|
+
return;
|
|
57
|
+
var cubeEvents = await this.driver.handleStateEvent(this, decryptedMessage);
|
|
58
|
+
cubeEvents.forEach(e => this.events$.next(e));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore corrupt frame */
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
onDisconnect = async () => {
|
|
65
|
+
if (this.disconnectOnce)
|
|
66
|
+
return;
|
|
67
|
+
this.disconnectOnce = true;
|
|
68
|
+
this.device.removeEventListener('gattserverdisconnected', this.onDisconnect);
|
|
69
|
+
this.stateCharacteristic.removeEventListener('characteristicvaluechanged', this.onStateUpdate);
|
|
70
|
+
await this.stateCharacteristic.stopNotifications().catch(() => { });
|
|
71
|
+
if (!this.events$.closed) {
|
|
72
|
+
this.events$.next({ timestamp: now(), type: "DISCONNECT" });
|
|
73
|
+
this.events$.complete();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
async sendCubeCommand(command) {
|
|
77
|
+
var commandMessage = this.driver.createCommandMessage(command);
|
|
78
|
+
if (commandMessage) {
|
|
79
|
+
return this.sendCommandMessage(commandMessage);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async disconnect() {
|
|
83
|
+
await this.onDisconnect();
|
|
84
|
+
if (this.device.gatt?.connected) {
|
|
85
|
+
this.device.gatt?.disconnect();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* View for binary protocol messages allowing to retrieve from message arbitrary length bit words
|
|
91
|
+
*/
|
|
92
|
+
class GanProtocolMessageView {
|
|
93
|
+
bits;
|
|
94
|
+
constructor(message) {
|
|
95
|
+
this.bits = Array.from(message).map(byte => (byte + 0x100).toString(2).slice(1)).join('');
|
|
96
|
+
}
|
|
97
|
+
getBitWord(startBit, bitLength, littleEndian = false) {
|
|
98
|
+
if (bitLength <= 8) {
|
|
99
|
+
return parseInt(this.bits.slice(startBit, startBit + bitLength), 2);
|
|
100
|
+
}
|
|
101
|
+
else if (bitLength == 16 || bitLength == 32) {
|
|
102
|
+
let buf = new Uint8Array(bitLength / 8);
|
|
103
|
+
for (let i = 0; i < buf.length; i++) {
|
|
104
|
+
buf[i] = parseInt(this.bits.slice(8 * i + startBit, 8 * i + startBit + 8), 2);
|
|
105
|
+
}
|
|
106
|
+
let dv = new DataView(buf.buffer);
|
|
107
|
+
return bitLength == 16 ? dv.getUint16(0, littleEndian) : dv.getUint32(0, littleEndian);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new Error('Unsupproted bit word length');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Driver implementation for GAN Gen2 protocol, supported cubes:
|
|
116
|
+
* - GAN Mini ui FreePlay
|
|
117
|
+
* - GAN12 ui FreePlay
|
|
118
|
+
* - GAN12 ui
|
|
119
|
+
* - GAN356 i Carry S
|
|
120
|
+
* - GAN356 i Carry
|
|
121
|
+
* - GAN356 i 3
|
|
122
|
+
* - Monster Go 3Ai
|
|
123
|
+
*/
|
|
124
|
+
class GanGen2ProtocolDriver {
|
|
125
|
+
lastSerial = -1;
|
|
126
|
+
lastMoveTimestamp = 0;
|
|
127
|
+
cubeTimestamp = 0;
|
|
128
|
+
createCommandMessage(command) {
|
|
129
|
+
var msg = new Uint8Array(20).fill(0);
|
|
130
|
+
switch (command.type) {
|
|
131
|
+
case 'REQUEST_FACELETS':
|
|
132
|
+
msg[0] = 0x04;
|
|
133
|
+
break;
|
|
134
|
+
case 'REQUEST_HARDWARE':
|
|
135
|
+
msg[0] = 0x05;
|
|
136
|
+
break;
|
|
137
|
+
case 'REQUEST_BATTERY':
|
|
138
|
+
msg[0] = 0x09;
|
|
139
|
+
break;
|
|
140
|
+
case 'REQUEST_RESET':
|
|
141
|
+
msg.set([0x0A, 0x05, 0x39, 0x77, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
msg = undefined;
|
|
145
|
+
}
|
|
146
|
+
return msg;
|
|
147
|
+
}
|
|
148
|
+
async handleStateEvent(conn, eventMessage) {
|
|
149
|
+
var timestamp = now();
|
|
150
|
+
var cubeEvents = [];
|
|
151
|
+
var msg = new GanProtocolMessageView(eventMessage);
|
|
152
|
+
var eventType = msg.getBitWord(0, 4);
|
|
153
|
+
if (eventType == 0x01) { // GYRO
|
|
154
|
+
// Orientation Quaternion
|
|
155
|
+
let qw = msg.getBitWord(4, 16);
|
|
156
|
+
let qx = msg.getBitWord(20, 16);
|
|
157
|
+
let qy = msg.getBitWord(36, 16);
|
|
158
|
+
let qz = msg.getBitWord(52, 16);
|
|
159
|
+
// Angular Velocity
|
|
160
|
+
let vx = msg.getBitWord(68, 4);
|
|
161
|
+
let vy = msg.getBitWord(72, 4);
|
|
162
|
+
let vz = msg.getBitWord(76, 4);
|
|
163
|
+
cubeEvents.push({
|
|
164
|
+
type: "GYRO",
|
|
165
|
+
timestamp: timestamp,
|
|
166
|
+
quaternion: {
|
|
167
|
+
x: (1 - (qx >> 15) * 2) * (qx & 0x7FFF) / 0x7FFF,
|
|
168
|
+
y: (1 - (qy >> 15) * 2) * (qy & 0x7FFF) / 0x7FFF,
|
|
169
|
+
z: (1 - (qz >> 15) * 2) * (qz & 0x7FFF) / 0x7FFF,
|
|
170
|
+
w: (1 - (qw >> 15) * 2) * (qw & 0x7FFF) / 0x7FFF
|
|
171
|
+
},
|
|
172
|
+
velocity: {
|
|
173
|
+
x: (1 - (vx >> 3) * 2) * (vx & 0x7),
|
|
174
|
+
y: (1 - (vy >> 3) * 2) * (vy & 0x7),
|
|
175
|
+
z: (1 - (vz >> 3) * 2) * (vz & 0x7)
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else if (eventType == 0x02) { // MOVE
|
|
180
|
+
if (this.lastSerial != -1) { // Accept move events only after first facelets state event received
|
|
181
|
+
let serial = msg.getBitWord(4, 8);
|
|
182
|
+
let diff = Math.min((serial - this.lastSerial) & 0xFF, 7);
|
|
183
|
+
this.lastSerial = serial;
|
|
184
|
+
if (diff > 0) {
|
|
185
|
+
for (let i = diff - 1; i >= 0; i--) {
|
|
186
|
+
let face = msg.getBitWord(12 + 5 * i, 4);
|
|
187
|
+
let direction = msg.getBitWord(16 + 5 * i, 1);
|
|
188
|
+
let move = "URFDLB".charAt(face) + " '".charAt(direction);
|
|
189
|
+
let elapsed = msg.getBitWord(47 + 16 * i, 16);
|
|
190
|
+
if (elapsed == 0) { // In case of 16-bit cube timestamp register overflow
|
|
191
|
+
elapsed = timestamp - this.lastMoveTimestamp;
|
|
192
|
+
}
|
|
193
|
+
this.cubeTimestamp += elapsed;
|
|
194
|
+
cubeEvents.push({
|
|
195
|
+
type: "MOVE",
|
|
196
|
+
serial: (serial - i) & 0xFF,
|
|
197
|
+
timestamp: timestamp,
|
|
198
|
+
localTimestamp: i == 0 ? timestamp : null, // Missed and recovered events has no meaningfull local timestamps
|
|
199
|
+
cubeTimestamp: this.cubeTimestamp,
|
|
200
|
+
face: face,
|
|
201
|
+
direction: direction,
|
|
202
|
+
move: move.trim()
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
this.lastMoveTimestamp = timestamp;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (eventType == 0x04) { // FACELETS
|
|
210
|
+
let serial = msg.getBitWord(4, 8);
|
|
211
|
+
if (this.lastSerial == -1)
|
|
212
|
+
this.lastSerial = serial;
|
|
213
|
+
// Corner/Edge Permutation/Orientation
|
|
214
|
+
let cp = [];
|
|
215
|
+
let co = [];
|
|
216
|
+
let ep = [];
|
|
217
|
+
let eo = [];
|
|
218
|
+
// Corners
|
|
219
|
+
for (let i = 0; i < 7; i++) {
|
|
220
|
+
cp.push(msg.getBitWord(12 + i * 3, 3));
|
|
221
|
+
co.push(msg.getBitWord(33 + i * 2, 2));
|
|
222
|
+
}
|
|
223
|
+
cp.push(28 - sum(cp));
|
|
224
|
+
co.push((3 - (sum(co) % 3)) % 3);
|
|
225
|
+
// Edges
|
|
226
|
+
for (let i = 0; i < 11; i++) {
|
|
227
|
+
ep.push(msg.getBitWord(47 + i * 4, 4));
|
|
228
|
+
eo.push(msg.getBitWord(91 + i, 1));
|
|
229
|
+
}
|
|
230
|
+
ep.push(66 - sum(ep));
|
|
231
|
+
eo.push((2 - (sum(eo) % 2)) % 2);
|
|
232
|
+
cubeEvents.push({
|
|
233
|
+
type: "FACELETS",
|
|
234
|
+
serial: serial,
|
|
235
|
+
timestamp: timestamp,
|
|
236
|
+
facelets: toKociembaFacelets(cp, co, ep, eo),
|
|
237
|
+
state: {
|
|
238
|
+
CP: cp,
|
|
239
|
+
CO: co,
|
|
240
|
+
EP: ep,
|
|
241
|
+
EO: eo
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (eventType == 0x05) { // HARDWARE
|
|
246
|
+
let hwMajor = msg.getBitWord(8, 8);
|
|
247
|
+
let hwMinor = msg.getBitWord(16, 8);
|
|
248
|
+
let swMajor = msg.getBitWord(24, 8);
|
|
249
|
+
let swMinor = msg.getBitWord(32, 8);
|
|
250
|
+
let gyroSupported = msg.getBitWord(104, 1);
|
|
251
|
+
let hardwareName = '';
|
|
252
|
+
for (var i = 0; i < 8; i++) {
|
|
253
|
+
hardwareName += String.fromCharCode(msg.getBitWord(i * 8 + 40, 8));
|
|
254
|
+
}
|
|
255
|
+
cubeEvents.push({
|
|
256
|
+
type: "HARDWARE",
|
|
257
|
+
timestamp: timestamp,
|
|
258
|
+
hardwareName: hardwareName,
|
|
259
|
+
hardwareVersion: `${hwMajor}.${hwMinor}`,
|
|
260
|
+
softwareVersion: `${swMajor}.${swMinor}`,
|
|
261
|
+
gyroSupported: !!gyroSupported
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else if (eventType == 0x09) { // BATTERY
|
|
265
|
+
let batteryLevel = msg.getBitWord(8, 8);
|
|
266
|
+
cubeEvents.push({
|
|
267
|
+
type: "BATTERY",
|
|
268
|
+
timestamp: timestamp,
|
|
269
|
+
batteryLevel: Math.min(batteryLevel, 100)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else if (eventType == 0x0D) { // DISCONNECT
|
|
273
|
+
conn.disconnect();
|
|
274
|
+
}
|
|
275
|
+
return cubeEvents;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Driver implementation for GAN Gen3 protocol, supported cubes:
|
|
280
|
+
* - GAN356 i Carry 2
|
|
281
|
+
*/
|
|
282
|
+
class GanGen3ProtocolDriver {
|
|
283
|
+
serial = -1;
|
|
284
|
+
lastSerial = -1;
|
|
285
|
+
lastLocalTimestamp = null;
|
|
286
|
+
moveBuffer = [];
|
|
287
|
+
createCommandMessage(command) {
|
|
288
|
+
var msg = new Uint8Array(16).fill(0);
|
|
289
|
+
switch (command.type) {
|
|
290
|
+
case 'REQUEST_FACELETS':
|
|
291
|
+
msg.set([0x68, 0x01]);
|
|
292
|
+
break;
|
|
293
|
+
case 'REQUEST_HARDWARE':
|
|
294
|
+
msg.set([0x68, 0x04]);
|
|
295
|
+
break;
|
|
296
|
+
case 'REQUEST_BATTERY':
|
|
297
|
+
msg.set([0x68, 0x07]);
|
|
298
|
+
break;
|
|
299
|
+
case 'REQUEST_RESET':
|
|
300
|
+
msg.set([0x68, 0x05, 0x05, 0x39, 0x77, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0x00, 0x00, 0x00]);
|
|
301
|
+
break;
|
|
302
|
+
default:
|
|
303
|
+
msg = undefined;
|
|
304
|
+
}
|
|
305
|
+
return msg;
|
|
306
|
+
}
|
|
307
|
+
/** Private cube command for requesting move history */
|
|
308
|
+
async requestMoveHistory(conn, serial, count) {
|
|
309
|
+
var msg = new Uint8Array(16).fill(0);
|
|
310
|
+
// Move history response data is byte-aligned, and moves always starting with near-ceil odd serial number, regardless of requested.
|
|
311
|
+
// Adjust serial and count to get odd serial aligned history window with even number of moves inside.
|
|
312
|
+
if (serial % 2 == 0)
|
|
313
|
+
serial = (serial - 1) & 0xFF;
|
|
314
|
+
if (count % 2 == 1)
|
|
315
|
+
count++;
|
|
316
|
+
// Never overflow requested history window beyond the serial number cycle edge 255 -> 0.
|
|
317
|
+
// Because due to iCarry2 firmware bug the moves beyond the edge will be spoofed with 'D' (just zero bytes).
|
|
318
|
+
count = Math.min(count, serial + 1);
|
|
319
|
+
msg.set([0x68, 0x03, serial, 0, count, 0]);
|
|
320
|
+
return conn.sendCommandMessage(msg).catch(() => {
|
|
321
|
+
// We can safely suppress and ignore possible GATT write errors, requestMoveHistory command is automatically retried on next move event
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Evict move events from FIFO buffer until missing move event detected
|
|
326
|
+
* In case of missing move, and if connection is provided, submit request for move history to fill gap in buffer
|
|
327
|
+
*/
|
|
328
|
+
async evictMoveBuffer(conn) {
|
|
329
|
+
var evictedEvents = [];
|
|
330
|
+
while (this.moveBuffer.length > 0) {
|
|
331
|
+
let bufferHead = this.moveBuffer[0];
|
|
332
|
+
let diff = this.lastSerial == -1 ? 1 : (bufferHead.serial - this.lastSerial) & 0xFF;
|
|
333
|
+
if (diff > 1) {
|
|
334
|
+
if (conn) {
|
|
335
|
+
await this.requestMoveHistory(conn, bufferHead.serial, diff);
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
evictedEvents.push(this.moveBuffer.shift());
|
|
341
|
+
this.lastSerial = bufferHead.serial;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Probably something went wrong and buffer is no longer evicted, so forcibly disconnect the cube
|
|
345
|
+
if (conn && this.moveBuffer.length > 16) {
|
|
346
|
+
conn.disconnect();
|
|
347
|
+
}
|
|
348
|
+
return evictedEvents;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Check if circular serial number (modulo 256) fits into (start,end) serial number range.
|
|
352
|
+
* By default range is open, set closedStart / closedEnd to make it closed.
|
|
353
|
+
*/
|
|
354
|
+
isSerialInRange(start, end, serial, closedStart = false, closedEnd = false) {
|
|
355
|
+
return ((end - start) & 0xFF) >= ((serial - start) & 0xFF)
|
|
356
|
+
&& (closedStart || ((start - serial) & 0xFF) > 0)
|
|
357
|
+
&& (closedEnd || ((end - serial) & 0xFF) > 0);
|
|
358
|
+
}
|
|
359
|
+
/** Used to inject missed moves to FIFO buffer */
|
|
360
|
+
injectMissedMoveToBuffer(move) {
|
|
361
|
+
if (move.type == "MOVE") {
|
|
362
|
+
if (this.moveBuffer.length > 0) {
|
|
363
|
+
var bufferHead = this.moveBuffer[0];
|
|
364
|
+
// Skip if move event with the same serial already in the buffer
|
|
365
|
+
if (this.moveBuffer.some(e => e.type == "MOVE" && e.serial == move.serial))
|
|
366
|
+
return;
|
|
367
|
+
// Skip if move serial does not fit in range between last evicted event and event on buffer head, i.e. event must be one of missed
|
|
368
|
+
if (!this.isSerialInRange(this.lastSerial, bufferHead.serial, move.serial))
|
|
369
|
+
return;
|
|
370
|
+
// Move history events should be injected in reverse order, so just put suitable event on buffer head
|
|
371
|
+
if (move.serial == ((bufferHead.serial - 1) & 0xFF)) {
|
|
372
|
+
this.moveBuffer.unshift(move);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
// This case happens when lost move is recovered using periodic
|
|
377
|
+
// facelets state event, and being inserted into the empty buffer.
|
|
378
|
+
if (this.isSerialInRange(this.lastSerial, this.serial, move.serial, false, true)) {
|
|
379
|
+
this.moveBuffer.unshift(move);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Used in response to periodic facelets event to check if any moves missed */
|
|
385
|
+
async checkIfMoveMissed(conn) {
|
|
386
|
+
let diff = (this.serial - this.lastSerial) & 0xFF;
|
|
387
|
+
if (diff > 0) {
|
|
388
|
+
if (this.serial != 0) { // Constraint to avoid iCarry2 firmware bug with facelets state event at 255 move counter
|
|
389
|
+
let bufferHead = this.moveBuffer[0];
|
|
390
|
+
let startSerial = bufferHead ? bufferHead.serial : (this.serial + 1) & 0xFF;
|
|
391
|
+
await this.requestMoveHistory(conn, startSerial, diff + 1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async handleStateEvent(conn, eventMessage) {
|
|
396
|
+
var timestamp = now();
|
|
397
|
+
var cubeEvents = [];
|
|
398
|
+
var msg = new GanProtocolMessageView(eventMessage);
|
|
399
|
+
var magic = msg.getBitWord(0, 8);
|
|
400
|
+
var eventType = msg.getBitWord(8, 8);
|
|
401
|
+
var dataLength = msg.getBitWord(16, 8);
|
|
402
|
+
if (magic == 0x55 && dataLength > 0) {
|
|
403
|
+
if (eventType == 0x01) { // MOVE
|
|
404
|
+
if (this.lastSerial != -1) { // Accept move events only after first facelets state event received
|
|
405
|
+
this.lastLocalTimestamp = timestamp;
|
|
406
|
+
let cubeTimestamp = msg.getBitWord(24, 32, true);
|
|
407
|
+
let serial = this.serial = msg.getBitWord(56, 16, true);
|
|
408
|
+
let direction = msg.getBitWord(72, 2);
|
|
409
|
+
let face = [2, 32, 8, 1, 16, 4].indexOf(msg.getBitWord(74, 6));
|
|
410
|
+
let move = "URFDLB".charAt(face) + " '".charAt(direction);
|
|
411
|
+
// put move event into FIFO buffer
|
|
412
|
+
if (face >= 0) {
|
|
413
|
+
this.moveBuffer.push({
|
|
414
|
+
type: "MOVE",
|
|
415
|
+
serial: serial,
|
|
416
|
+
timestamp: timestamp,
|
|
417
|
+
localTimestamp: timestamp,
|
|
418
|
+
cubeTimestamp: cubeTimestamp,
|
|
419
|
+
face: face,
|
|
420
|
+
direction: direction,
|
|
421
|
+
move: move.trim()
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
// evict move events from FIFO buffer
|
|
425
|
+
cubeEvents = await this.evictMoveBuffer(conn);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else if (eventType == 0x06) { // MOVE_HISTORY
|
|
429
|
+
let startSerial = msg.getBitWord(24, 8);
|
|
430
|
+
let count = (dataLength - 1) * 2;
|
|
431
|
+
// inject missed moves into FIFO buffer
|
|
432
|
+
for (let i = 0; i < count; i++) {
|
|
433
|
+
let face = [1, 5, 3, 0, 4, 2].indexOf(msg.getBitWord(32 + 4 * i, 3));
|
|
434
|
+
let direction = msg.getBitWord(35 + 4 * i, 1);
|
|
435
|
+
if (face >= 0) {
|
|
436
|
+
let move = "URFDLB".charAt(face) + " '".charAt(direction);
|
|
437
|
+
this.injectMissedMoveToBuffer({
|
|
438
|
+
type: "MOVE",
|
|
439
|
+
serial: (startSerial - i) & 0xFF,
|
|
440
|
+
timestamp: timestamp,
|
|
441
|
+
localTimestamp: null, // Missed and recovered events has no meaningfull local timestamps
|
|
442
|
+
cubeTimestamp: null, // Cube hardware timestamp for missed move you should interpolate using cubeTimestampLinearFit
|
|
443
|
+
face: face,
|
|
444
|
+
direction: direction,
|
|
445
|
+
move: move.trim()
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
// evict move events from FIFO buffer
|
|
450
|
+
cubeEvents = await this.evictMoveBuffer();
|
|
451
|
+
}
|
|
452
|
+
else if (eventType == 0x02) { // FACELETS
|
|
453
|
+
let serial = this.serial = msg.getBitWord(24, 16, true);
|
|
454
|
+
// Also check and recovery missed moves using periodic facelets event sent by cube
|
|
455
|
+
if (this.lastSerial != -1) {
|
|
456
|
+
// Debounce the facelet event if there are active cube moves
|
|
457
|
+
if (this.lastLocalTimestamp != null && (timestamp - this.lastLocalTimestamp) > 500) {
|
|
458
|
+
await this.checkIfMoveMissed(conn);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (this.lastSerial == -1)
|
|
462
|
+
this.lastSerial = serial;
|
|
463
|
+
// Corner/Edge Permutation/Orientation
|
|
464
|
+
let cp = [];
|
|
465
|
+
let co = [];
|
|
466
|
+
let ep = [];
|
|
467
|
+
let eo = [];
|
|
468
|
+
// Corners
|
|
469
|
+
for (let i = 0; i < 7; i++) {
|
|
470
|
+
cp.push(msg.getBitWord(40 + i * 3, 3));
|
|
471
|
+
co.push(msg.getBitWord(61 + i * 2, 2));
|
|
472
|
+
}
|
|
473
|
+
cp.push(28 - sum(cp));
|
|
474
|
+
co.push((3 - (sum(co) % 3)) % 3);
|
|
475
|
+
// Edges
|
|
476
|
+
for (let i = 0; i < 11; i++) {
|
|
477
|
+
ep.push(msg.getBitWord(77 + i * 4, 4));
|
|
478
|
+
eo.push(msg.getBitWord(121 + i, 1));
|
|
479
|
+
}
|
|
480
|
+
ep.push(66 - sum(ep));
|
|
481
|
+
eo.push((2 - (sum(eo) % 2)) % 2);
|
|
482
|
+
cubeEvents.push({
|
|
483
|
+
type: "FACELETS",
|
|
484
|
+
serial: serial,
|
|
485
|
+
timestamp: timestamp,
|
|
486
|
+
facelets: toKociembaFacelets(cp, co, ep, eo),
|
|
487
|
+
state: {
|
|
488
|
+
CP: cp,
|
|
489
|
+
CO: co,
|
|
490
|
+
EP: ep,
|
|
491
|
+
EO: eo
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
else if (eventType == 0x07) { // HARDWARE
|
|
496
|
+
let swMajor = msg.getBitWord(72, 4);
|
|
497
|
+
let swMinor = msg.getBitWord(76, 4);
|
|
498
|
+
let hwMajor = msg.getBitWord(80, 4);
|
|
499
|
+
let hwMinor = msg.getBitWord(84, 4);
|
|
500
|
+
let hardwareName = '';
|
|
501
|
+
for (var i = 0; i < 5; i++) {
|
|
502
|
+
hardwareName += String.fromCharCode(msg.getBitWord(i * 8 + 32, 8));
|
|
503
|
+
}
|
|
504
|
+
cubeEvents.push({
|
|
505
|
+
type: "HARDWARE",
|
|
506
|
+
timestamp: timestamp,
|
|
507
|
+
hardwareName: hardwareName,
|
|
508
|
+
hardwareVersion: `${hwMajor}.${hwMinor}`,
|
|
509
|
+
softwareVersion: `${swMajor}.${swMinor}`,
|
|
510
|
+
gyroSupported: false
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
else if (eventType == 0x10) { // BATTERY
|
|
514
|
+
let batteryLevel = msg.getBitWord(24, 8);
|
|
515
|
+
cubeEvents.push({
|
|
516
|
+
type: "BATTERY",
|
|
517
|
+
timestamp: timestamp,
|
|
518
|
+
batteryLevel: Math.min(batteryLevel, 100)
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
else if (eventType == 0x11) { // DISCONNECT
|
|
522
|
+
conn.disconnect();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return cubeEvents;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Driver implementation for GAN Gen4 protocol, supported cubes:
|
|
530
|
+
* - GAN12 ui Maglev
|
|
531
|
+
* - GAN14 ui FreePlay
|
|
532
|
+
*/
|
|
533
|
+
class GanGen4ProtocolDriver {
|
|
534
|
+
serial = -1;
|
|
535
|
+
lastSerial = -1;
|
|
536
|
+
lastLocalTimestamp = null;
|
|
537
|
+
moveBuffer = [];
|
|
538
|
+
// Used to store partial result acquired from hardware info events
|
|
539
|
+
hwInfo = {};
|
|
540
|
+
/** Set when we have seen at least one 0xEC GYRO packet this connection (not cleared on REQUEST_HARDWARE). */
|
|
541
|
+
gyroObserved = false;
|
|
542
|
+
/** Set when a full HARDWARE snapshot was emitted for the current hwInfo assembly; reset when REQUEST_HARDWARE clears hwInfo. */
|
|
543
|
+
hardwareInfoEmitted = false;
|
|
544
|
+
createCommandMessage(command) {
|
|
545
|
+
var msg = new Uint8Array(20).fill(0);
|
|
546
|
+
switch (command.type) {
|
|
547
|
+
case 'REQUEST_FACELETS':
|
|
548
|
+
msg.set([0xDD, 0x04, 0x00, 0xED, 0x00, 0x00]);
|
|
549
|
+
break;
|
|
550
|
+
case 'REQUEST_HARDWARE':
|
|
551
|
+
this.hwInfo = {};
|
|
552
|
+
this.hardwareInfoEmitted = false;
|
|
553
|
+
msg.set([0xDF, 0x03, 0x00, 0x00, 0x00]);
|
|
554
|
+
break;
|
|
555
|
+
case 'REQUEST_BATTERY':
|
|
556
|
+
msg.set([0xDD, 0x04, 0x00, 0xEF, 0x00, 0x00]);
|
|
557
|
+
break;
|
|
558
|
+
case 'REQUEST_RESET':
|
|
559
|
+
msg.set([0xD2, 0x0D, 0x05, 0x39, 0x77, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0x00, 0x00, 0x00]);
|
|
560
|
+
break;
|
|
561
|
+
default:
|
|
562
|
+
msg = undefined;
|
|
563
|
+
}
|
|
564
|
+
return msg;
|
|
565
|
+
}
|
|
566
|
+
buildHardwareEvent(timestamp) {
|
|
567
|
+
return {
|
|
568
|
+
type: "HARDWARE",
|
|
569
|
+
timestamp: timestamp,
|
|
570
|
+
hardwareName: this.hwInfo[0xFC],
|
|
571
|
+
hardwareVersion: this.hwInfo[0xFE],
|
|
572
|
+
softwareVersion: this.hwInfo[0xFD],
|
|
573
|
+
productDate: this.hwInfo[0xFA],
|
|
574
|
+
gyroSupported: this.gyroObserved
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Private cube command for requesting move history */
|
|
578
|
+
async requestMoveHistory(conn, serial, count) {
|
|
579
|
+
var msg = new Uint8Array(20).fill(0);
|
|
580
|
+
// Move history response data is byte-aligned, and moves always starting with near-ceil odd serial number, regardless of requested.
|
|
581
|
+
// Adjust serial and count to get odd serial aligned history window with even number of moves inside.
|
|
582
|
+
if (serial % 2 == 0)
|
|
583
|
+
serial = (serial - 1) & 0xFF;
|
|
584
|
+
if (count % 2 == 1)
|
|
585
|
+
count++;
|
|
586
|
+
// Never overflow requested history window beyond the serial number cycle edge 255 -> 0.
|
|
587
|
+
// Because due to firmware bug the moves beyond the edge will be spoofed with 'D' (just zero bytes).
|
|
588
|
+
count = Math.min(count, serial + 1);
|
|
589
|
+
msg.set([0xD1, 0x04, serial, 0, count, 0]);
|
|
590
|
+
return conn.sendCommandMessage(msg).catch(() => {
|
|
591
|
+
// We can safely suppress and ignore possible GATT write errors, requestMoveHistory command is automatically retried on next move event
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Evict move events from FIFO buffer until missing move event detected
|
|
596
|
+
* In case of missing move, and if connection is provided, submit request for move history to fill gap in buffer
|
|
597
|
+
*/
|
|
598
|
+
async evictMoveBuffer(conn) {
|
|
599
|
+
var evictedEvents = [];
|
|
600
|
+
while (this.moveBuffer.length > 0) {
|
|
601
|
+
let bufferHead = this.moveBuffer[0];
|
|
602
|
+
let diff = this.lastSerial == -1 ? 1 : (bufferHead.serial - this.lastSerial) & 0xFF;
|
|
603
|
+
if (diff > 1) {
|
|
604
|
+
if (conn) {
|
|
605
|
+
await this.requestMoveHistory(conn, bufferHead.serial, diff);
|
|
606
|
+
}
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
evictedEvents.push(this.moveBuffer.shift());
|
|
611
|
+
this.lastSerial = bufferHead.serial;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Probably something went wrong and buffer is no longer evicted, so forcibly disconnect the cube
|
|
615
|
+
if (conn && this.moveBuffer.length > 16) {
|
|
616
|
+
conn.disconnect();
|
|
617
|
+
}
|
|
618
|
+
return evictedEvents;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Check if circular serial number (modulo 256) fits into (start,end) serial number range.
|
|
622
|
+
* By default range is open, set closedStart / closedEnd to make it closed.
|
|
623
|
+
*/
|
|
624
|
+
isSerialInRange(start, end, serial, closedStart = false, closedEnd = false) {
|
|
625
|
+
return ((end - start) & 0xFF) >= ((serial - start) & 0xFF)
|
|
626
|
+
&& (closedStart || ((start - serial) & 0xFF) > 0)
|
|
627
|
+
&& (closedEnd || ((end - serial) & 0xFF) > 0);
|
|
628
|
+
}
|
|
629
|
+
/** Used to inject missed moves to FIFO buffer */
|
|
630
|
+
injectMissedMoveToBuffer(move) {
|
|
631
|
+
if (move.type == "MOVE") {
|
|
632
|
+
if (this.moveBuffer.length > 0) {
|
|
633
|
+
var bufferHead = this.moveBuffer[0];
|
|
634
|
+
// Skip if move event with the same serial already in the buffer
|
|
635
|
+
if (this.moveBuffer.some(e => e.type == "MOVE" && e.serial == move.serial))
|
|
636
|
+
return;
|
|
637
|
+
// Skip if move serial does not fit in range between last evicted event and event on buffer head, i.e. event must be one of missed
|
|
638
|
+
if (!this.isSerialInRange(this.lastSerial, bufferHead.serial, move.serial))
|
|
639
|
+
return;
|
|
640
|
+
// Move history events should be injected in reverse order, so just put suitable event on buffer head
|
|
641
|
+
if (move.serial == ((bufferHead.serial - 1) & 0xFF)) {
|
|
642
|
+
this.moveBuffer.unshift(move);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
// This case happens when lost move is recovered using periodic
|
|
647
|
+
// facelets state event, and being inserted into the empty buffer.
|
|
648
|
+
if (this.isSerialInRange(this.lastSerial, this.serial, move.serial, false, true)) {
|
|
649
|
+
this.moveBuffer.unshift(move);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** Used in response to periodic facelets event to check if any moves missed */
|
|
655
|
+
async checkIfMoveMissed(conn) {
|
|
656
|
+
let diff = (this.serial - this.lastSerial) & 0xFF;
|
|
657
|
+
if (diff > 0) {
|
|
658
|
+
if (this.serial != 0) { // Constraint to avoid firmware bug with facelets state event at 255 move counter
|
|
659
|
+
let bufferHead = this.moveBuffer[0];
|
|
660
|
+
let startSerial = bufferHead ? bufferHead.serial : (this.serial + 1) & 0xFF;
|
|
661
|
+
await this.requestMoveHistory(conn, startSerial, diff + 1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async handleStateEvent(conn, eventMessage) {
|
|
666
|
+
var timestamp = now();
|
|
667
|
+
var cubeEvents = [];
|
|
668
|
+
var msg = new GanProtocolMessageView(eventMessage);
|
|
669
|
+
var eventType = msg.getBitWord(0, 8);
|
|
670
|
+
var dataLength = msg.getBitWord(8, 8);
|
|
671
|
+
if (eventType == 0x01) { // MOVE
|
|
672
|
+
if (this.lastSerial != -1) { // Accept move events only after first facelets state event received
|
|
673
|
+
// One BLE notification may contain multiple MOVE chunks (72 bits each). Only reading the first
|
|
674
|
+
// chunk drops later face turns (common on slice moves) until MOVE_HISTORY catches up
|
|
675
|
+
const msgBitLen = eventMessage.length * 8;
|
|
676
|
+
let off = 0;
|
|
677
|
+
while (off + 72 <= msgBitLen && msg.getBitWord(off, 8) === 0x01) {
|
|
678
|
+
let cubeTimestamp = msg.getBitWord(off + 16, 32, true);
|
|
679
|
+
let serial = msg.getBitWord(off + 48, 16, true);
|
|
680
|
+
this.serial = serial;
|
|
681
|
+
let direction = msg.getBitWord(off + 64, 2);
|
|
682
|
+
let face = [2, 32, 8, 1, 16, 4].indexOf(msg.getBitWord(off + 66, 6));
|
|
683
|
+
let move = "URFDLB".charAt(face) + " '".charAt(direction);
|
|
684
|
+
if (face < 0) {
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
this.moveBuffer.push({
|
|
688
|
+
type: "MOVE",
|
|
689
|
+
serial: serial,
|
|
690
|
+
timestamp: timestamp,
|
|
691
|
+
localTimestamp: timestamp,
|
|
692
|
+
cubeTimestamp: cubeTimestamp,
|
|
693
|
+
face: face,
|
|
694
|
+
direction: direction,
|
|
695
|
+
move: move.trim()
|
|
696
|
+
});
|
|
697
|
+
this.lastLocalTimestamp = timestamp;
|
|
698
|
+
off += 72;
|
|
699
|
+
}
|
|
700
|
+
// evict move events from FIFO buffer
|
|
701
|
+
cubeEvents = await this.evictMoveBuffer(conn);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else if (eventType == 0xD1) { // MOVE_HISTORY
|
|
705
|
+
let startSerial = msg.getBitWord(16, 8);
|
|
706
|
+
let count = (dataLength - 1) * 2;
|
|
707
|
+
// inject missed moves into FIFO buffer
|
|
708
|
+
for (let i = 0; i < count; i++) {
|
|
709
|
+
let face = [1, 5, 3, 0, 4, 2].indexOf(msg.getBitWord(24 + 4 * i, 3));
|
|
710
|
+
let direction = msg.getBitWord(27 + 4 * i, 1);
|
|
711
|
+
if (face >= 0) {
|
|
712
|
+
let move = "URFDLB".charAt(face) + " '".charAt(direction);
|
|
713
|
+
this.injectMissedMoveToBuffer({
|
|
714
|
+
type: "MOVE",
|
|
715
|
+
serial: (startSerial - i) & 0xFF,
|
|
716
|
+
timestamp: timestamp,
|
|
717
|
+
localTimestamp: null, // Missed and recovered events has no meaningfull local timestamps
|
|
718
|
+
cubeTimestamp: null, // Cube hardware timestamp for missed move you should interpolate using cubeTimestampLinearFit
|
|
719
|
+
face: face,
|
|
720
|
+
direction: direction,
|
|
721
|
+
move: move.trim()
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// evict move events from FIFO buffer
|
|
726
|
+
cubeEvents = await this.evictMoveBuffer();
|
|
727
|
+
}
|
|
728
|
+
else if (eventType == 0xED) { // FACELETS
|
|
729
|
+
let serial = this.serial = msg.getBitWord(16, 16, true);
|
|
730
|
+
// Also check and recovery missed moves using periodic facelets event sent by cube
|
|
731
|
+
if (this.lastSerial != -1) {
|
|
732
|
+
// Debounce the facelet event if there are active cube moves
|
|
733
|
+
if (this.lastLocalTimestamp != null && (timestamp - this.lastLocalTimestamp) > 500) {
|
|
734
|
+
await this.checkIfMoveMissed(conn);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (this.lastSerial == -1)
|
|
738
|
+
this.lastSerial = serial;
|
|
739
|
+
// Corner/Edge Permutation/Orientation
|
|
740
|
+
let cp = [];
|
|
741
|
+
let co = [];
|
|
742
|
+
let ep = [];
|
|
743
|
+
let eo = [];
|
|
744
|
+
// Corners
|
|
745
|
+
for (let i = 0; i < 7; i++) {
|
|
746
|
+
cp.push(msg.getBitWord(32 + i * 3, 3));
|
|
747
|
+
co.push(msg.getBitWord(53 + i * 2, 2));
|
|
748
|
+
}
|
|
749
|
+
cp.push(28 - sum(cp));
|
|
750
|
+
co.push((3 - (sum(co) % 3)) % 3);
|
|
751
|
+
// Edges
|
|
752
|
+
for (let i = 0; i < 11; i++) {
|
|
753
|
+
ep.push(msg.getBitWord(69 + i * 4, 4));
|
|
754
|
+
eo.push(msg.getBitWord(113 + i, 1));
|
|
755
|
+
}
|
|
756
|
+
ep.push(66 - sum(ep));
|
|
757
|
+
eo.push((2 - (sum(eo) % 2)) % 2);
|
|
758
|
+
cubeEvents.push({
|
|
759
|
+
type: "FACELETS",
|
|
760
|
+
serial: serial,
|
|
761
|
+
timestamp: timestamp,
|
|
762
|
+
facelets: toKociembaFacelets(cp, co, ep, eo),
|
|
763
|
+
state: {
|
|
764
|
+
CP: cp,
|
|
765
|
+
CO: co,
|
|
766
|
+
EP: ep,
|
|
767
|
+
EO: eo
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
else if (eventType >= 0xFA && eventType <= 0xFE) { // HARDWARE
|
|
772
|
+
switch (eventType) {
|
|
773
|
+
case 0xFA: // Product Date
|
|
774
|
+
let year = msg.getBitWord(24, 16, true);
|
|
775
|
+
let month = msg.getBitWord(40, 8);
|
|
776
|
+
let day = msg.getBitWord(48, 8);
|
|
777
|
+
this.hwInfo[eventType] = `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
|
|
778
|
+
break;
|
|
779
|
+
case 0xFC: // Hardware Name
|
|
780
|
+
this.hwInfo[eventType] = '';
|
|
781
|
+
for (var i = 0; i < dataLength - 1; i++) {
|
|
782
|
+
this.hwInfo[eventType] += String.fromCharCode(msg.getBitWord(i * 8 + 24, 8));
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
case 0xFD: // Software Version
|
|
786
|
+
let swMajor = msg.getBitWord(24, 4);
|
|
787
|
+
let swMinor = msg.getBitWord(28, 4);
|
|
788
|
+
this.hwInfo[eventType] = `${swMajor}.${swMinor}`;
|
|
789
|
+
break;
|
|
790
|
+
case 0xFE: // Hardware Version
|
|
791
|
+
let hwMajor = msg.getBitWord(24, 4);
|
|
792
|
+
let hwMinor = msg.getBitWord(28, 4);
|
|
793
|
+
this.hwInfo[eventType] = `${hwMajor}.${hwMinor}`;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
if (Object.keys(this.hwInfo).length == 4) { // All fields are populated
|
|
797
|
+
this.hardwareInfoEmitted = true;
|
|
798
|
+
cubeEvents.push(this.buildHardwareEvent(timestamp));
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
else if (eventType == 0xEC) { // GYRO
|
|
802
|
+
const firstGyroThisSession = !this.gyroObserved;
|
|
803
|
+
this.gyroObserved = true;
|
|
804
|
+
// Orientation Quaternion
|
|
805
|
+
let qw = msg.getBitWord(16, 16);
|
|
806
|
+
let qx = msg.getBitWord(32, 16);
|
|
807
|
+
let qy = msg.getBitWord(48, 16);
|
|
808
|
+
let qz = msg.getBitWord(64, 16);
|
|
809
|
+
// Angular Velocity
|
|
810
|
+
let vx = msg.getBitWord(80, 4);
|
|
811
|
+
let vy = msg.getBitWord(84, 4);
|
|
812
|
+
let vz = msg.getBitWord(88, 4);
|
|
813
|
+
cubeEvents.push({
|
|
814
|
+
type: "GYRO",
|
|
815
|
+
timestamp: timestamp,
|
|
816
|
+
quaternion: {
|
|
817
|
+
x: (1 - (qx >> 15) * 2) * (qx & 0x7FFF) / 0x7FFF,
|
|
818
|
+
y: (1 - (qy >> 15) * 2) * (qy & 0x7FFF) / 0x7FFF,
|
|
819
|
+
z: (1 - (qz >> 15) * 2) * (qz & 0x7FFF) / 0x7FFF,
|
|
820
|
+
w: (1 - (qw >> 15) * 2) * (qw & 0x7FFF) / 0x7FFF
|
|
821
|
+
},
|
|
822
|
+
velocity: {
|
|
823
|
+
x: (1 - (vx >> 3) * 2) * (vx & 0x7),
|
|
824
|
+
y: (1 - (vy >> 3) * 2) * (vy & 0x7),
|
|
825
|
+
z: (1 - (vz >> 3) * 2) * (vz & 0x7)
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
if (firstGyroThisSession && this.hardwareInfoEmitted && Object.keys(this.hwInfo).length == 4) {
|
|
829
|
+
cubeEvents.push(this.buildHardwareEvent(timestamp));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else if (eventType == 0xEF) { // BATTERY
|
|
833
|
+
let batteryLevel = msg.getBitWord(8 + dataLength * 8, 8);
|
|
834
|
+
cubeEvents.push({
|
|
835
|
+
type: "BATTERY",
|
|
836
|
+
timestamp: timestamp,
|
|
837
|
+
batteryLevel: Math.min(batteryLevel, 100)
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
else if (eventType == 0xEA) { // DISCONNECT
|
|
841
|
+
conn.disconnect();
|
|
842
|
+
}
|
|
843
|
+
return cubeEvents;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
export { GanCubeClassicConnection, GanGen2ProtocolDriver, GanGen3ProtocolDriver, GanGen4ProtocolDriver };
|