@vscubing/cubing
Version:
A collection of JavaScript cubing libraries.
1,599 lines (1,581 loc) • 46.3 kB
JavaScript
import {
binaryComponentsToReid3x3x3,
twizzleBinaryToBinaryComponents
} from "../chunks/chunk-RGIZKMSM.js";
import {
cube3x3x3,
puzzles
} from "../chunks/chunk-H3JHWQ36.js";
import {
experimental3x3x3KPuzzle
} from "../chunks/chunk-7ASPZCMY.js";
import {
KPattern
} from "../chunks/chunk-4IUILNFM.js";
import {
Alg,
Move,
experimentalAppendMove,
keyToMove
} from "../chunks/chunk-T3WO4S5D.js";
// src/cubing/bluetooth/debug.ts
var DEBUG_LOGGING_ENABLED = false;
function enableDebugLogging(enable) {
DEBUG_LOGGING_ENABLED = enable;
}
function debugLog(...args) {
if (!DEBUG_LOGGING_ENABLED) {
return;
}
if (console.info) {
console.info(...args);
} else {
console.log(...args);
}
}
// src/cubing/bluetooth/transformer.ts
import { Quaternion } from "three/src/math/Quaternion.js";
import { Vector3 } from "three/src/math/Vector3.js";
function maxAxis(v) {
const maxVal = Math.max(Math.abs(v.x), Math.abs(v.y), Math.abs(v.z));
switch (maxVal) {
case v.x:
return "x";
case -v.x:
return "-x";
case v.y:
return "y";
case -v.y:
return "-y";
case v.z:
return "z";
case -v.z:
return "-z";
default:
throw new Error("Uh-oh.");
}
}
var s2 = Math.sqrt(0.5);
var m = {
"y z": new Quaternion(0, 0, 0, 1),
"-z y": new Quaternion(s2, 0, 0, s2),
"x z": new Quaternion(0, 0, -s2, s2),
"-x z": new Quaternion(0, 0, s2, s2)
};
var BasicRotationTransformer = class {
// private reorientQuat = new Quaternion();
transformAlgLeaf(_algLeafEvent) {
}
transformOrientation(orientationEvent) {
const { x, y, z, w } = orientationEvent.quaternion;
const quat = new Quaternion(x, y, z, w);
const U = new Vector3(0, 1, 0);
const F = new Vector3(0, 0, 1);
const maxU = maxAxis(U.applyQuaternion(quat));
const maxF = maxAxis(F.applyQuaternion(quat));
const oriQuat = m[`${maxU} ${maxF}`] || m["y z"];
console.log(quat);
console.log(oriQuat);
const q2 = quat.premultiply(oriQuat);
console.log(q2);
orientationEvent.quaternion = quat;
console.log(orientationEvent.quaternion);
}
};
// src/cubing/bluetooth/smart-puzzle/bluetooth-puzzle.ts
var BluetoothPuzzle = class extends EventTarget {
transformers = [];
listeners = [];
// TODO: type
orientationListeners = [];
// TODO: Can we make this reutrn (async) on success?
// TODO: require subclasses to implement this?
async getPattern() {
throw new Error("cannot get pattern");
}
addAlgLeafListener(listener) {
this.listeners.push(listener);
}
addOrientationListener(listener) {
this.orientationListeners.push(listener);
}
experimentalAddBasicRotationTransformer() {
this.transformers.push(new BasicRotationTransformer());
}
dispatchAlgLeaf(algLeaf) {
for (const transformer of this.transformers) {
transformer.transformAlgLeaf(algLeaf);
}
for (const l of this.listeners) {
l(algLeaf);
}
}
dispatchOrientation(orientationEvent) {
for (const transformer of this.transformers) {
transformer.transformOrientation(orientationEvent);
}
const { x, y, z, w } = orientationEvent.quaternion;
orientationEvent.quaternion = {
x,
y,
z,
w
};
for (const l of this.orientationListeners) {
l(orientationEvent);
}
}
};
// src/cubing/bluetooth/keyboard.ts
var KeyboardPuzzle = class extends BluetoothPuzzle {
// (e: KeyboardEvent) => Promise<void>;
// TODO: Decide on the right arguments.
// TODO: support tying the puzzle to a TwistyPlayer.
constructor(target, puzzle = "3x3x3") {
super();
this.target = target;
this.listener = this.onKeyDown.bind(this);
target.addEventListener("keydown", this.listener);
this.keyMappingAndPatternPromise = this.setPuzzleInternal(puzzle);
}
keyMappingAndPatternPromise;
listener;
name() {
return "Keyboard Input";
}
async setPuzzleInternal(puzzle) {
const puzzleLoader = await (async () => typeof puzzle === "string" ? (await import("../puzzles/index.js")).puzzles[puzzle] : puzzle)();
const kpuzzle = await (async () => puzzleLoader.kpuzzle())();
return Promise.all([puzzleLoader.keyMapping?.(), kpuzzle.defaultPattern()]);
}
disconnect() {
this.target.removeEventListener("keydown", this.listener);
}
async getPattern() {
return (await this.keyMappingAndPatternPromise)[1];
}
async onKeyDown(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
this.keyMappingAndPatternPromise = (async () => {
const [keyMapping, pattern] = await this.keyMappingAndPatternPromise;
const algLeaf = keyToMove(keyMapping, e);
let newPattern;
if (algLeaf) {
newPattern = pattern.applyAlg(new Alg([algLeaf]));
this.dispatchAlgLeaf({
latestAlgLeaf: algLeaf,
timeStamp: e.timeStamp,
pattern: newPattern
});
e.preventDefault();
}
return [keyMapping, newPattern ?? pattern];
})();
}
};
async function debugKeyboardConnect(target = window, puzzle = "3x3x3") {
return new KeyboardPuzzle(target, puzzle);
}
// src/cubing/bluetooth/connect/index.ts
function requestOptions(configs, acceptAllDevices = false) {
const options = acceptAllDevices ? {
acceptAllDevices: true,
optionalServices: []
} : {
filters: [],
optionalServices: []
};
for (const config of configs) {
if (!acceptAllDevices) {
options.filters = options.filters.concat(config.filters);
}
options.optionalServices = options.optionalServices.concat(
config.optionalServices
);
}
debugLog({ requestOptions: options });
return options;
}
var consecutiveFailures = 0;
var MAX_FAILURES_BEFORE_ACCEPT_ALL_FALLBACK = 2;
async function bluetoothConnect(configs, options = {}) {
debugLog("Attempting to pair.");
let device;
try {
let acceptAllDevices = options.acceptAllDevices;
if (!acceptAllDevices && consecutiveFailures >= MAX_FAILURES_BEFORE_ACCEPT_ALL_FALLBACK) {
console.info(
`The last ${MAX_FAILURES_BEFORE_ACCEPT_ALL_FALLBACK} Bluetooth puzzle connection attempts failed. This time, the Bluetooth prompt will show all possible devices.`
);
acceptAllDevices = true;
}
device = await navigator.bluetooth.requestDevice(
requestOptions(configs, acceptAllDevices)
);
consecutiveFailures = 0;
} catch (e) {
consecutiveFailures++;
throw e;
}
debugLog("Device:", device);
if (typeof device.gatt === "undefined") {
return Promise.reject("Device did not have a GATT server.");
}
const server = await device.gatt.connect();
debugLog("Server:", server);
const name = server.device?.name || "";
for (const config of configs) {
for (const prefix of config.prefixes) {
if (name?.startsWith(prefix)) {
return config.connect(server, device);
}
}
}
throw Error("Unknown Bluetooth devive.");
}
// src/cubing/bluetooth/smart-puzzle/gan.ts
import { Quaternion as Quaternion2 } from "three/src/math/Quaternion.js";
// src/cubing/vendor/public-domain/unsafe-raw-aes/unsafe-raw-aes.ts
var blockSize = 16;
var zeros = new Uint8Array(blockSize);
var paddingBlockPlaintext = new Uint8Array(
new Array(blockSize).fill(blockSize)
);
var AES_CBC = "AES-CBC";
async function importKey(keyBytes) {
return await crypto.subtle.importKey("raw", keyBytes, AES_CBC, true, [
"encrypt",
"decrypt"
]);
}
async function unsafeEncryptBlockWithIV(key, plaintextBlock, iv) {
const cryptoResult = await window.crypto.subtle.encrypt(
{
name: AES_CBC,
iv
},
key,
plaintextBlock
);
return cryptoResult.slice(0, blockSize);
}
async function unsafeDecryptBlock(key, ciphertextBlock) {
const paddingBlock = await unsafeEncryptBlockWithIV(
key,
paddingBlockPlaintext,
ciphertextBlock
);
const cbcCiphertext = new Uint8Array(2 * blockSize);
cbcCiphertext.set(new Uint8Array(ciphertextBlock), 0);
cbcCiphertext.set(new Uint8Array(paddingBlock), blockSize);
const cryptoResult = await window.crypto.subtle.decrypt(
{
name: AES_CBC,
iv: zeros
},
key,
cbcCiphertext
);
return cryptoResult.slice(0, blockSize);
}
// src/cubing/bluetooth/smart-puzzle/gan.ts
var DEFAULT_INTERVAL_MS = 150;
var MAX_LATEST_MOVES = 6;
var ganMoveToBlockMove = {
0: new Move("U"),
2: new Move("U", -1),
3: new Move("R"),
5: new Move("R", -1),
6: new Move("F"),
8: new Move("F", -1),
9: new Move("D"),
11: new Move("D", -1),
12: new Move("L"),
14: new Move("L", -1),
15: new Move("B"),
17: new Move("B", -1)
};
var homeQuatInverse = null;
function probablyDecodedCorrectly(data) {
return data[13] < 18 && data[14] < 18 && data[15] < 18 && data[16] < 18 && data[17] < 18 && data[18] < 18;
}
var key10 = new Uint8Array([
198,
202,
21,
223,
79,
110,
19,
182,
119,
13,
230,
89,
58,
175,
186,
162
]);
var key11 = new Uint8Array([
67,
226,
91,
214,
125,
220,
120,
216,
7,
96,
163,
218,
130,
60,
1,
241
]);
async function decryptPattern(data, aesKey) {
if (aesKey === null) {
return data;
}
const copy = new Uint8Array(data);
copy.set(new Uint8Array(await unsafeDecryptBlock(aesKey, copy.slice(3))), 3);
copy.set(
new Uint8Array(await unsafeDecryptBlock(aesKey, copy.slice(0, 16))),
0
);
if (probablyDecodedCorrectly(copy)) {
return copy;
}
throw new Error("Invalid Gan cube pattern");
}
var PhysicalState = class _PhysicalState {
constructor(dataView, timeStamp) {
this.dataView = dataView;
this.timeStamp = timeStamp;
this.arr = new Uint8Array(dataView.buffer);
if (this.arr.length !== this.arrLen) {
throw new Error("Unexpected array length");
}
}
static async read(characteristic, aesKey) {
const value = await decryptPattern(
new Uint8Array((await characteristic.readValue()).buffer),
aesKey
);
const timeStamp = Date.now();
return new _PhysicalState(new DataView(value.buffer), timeStamp);
}
arr;
arrLen = 19;
rotQuat() {
let x = this.dataView.getInt16(0, true) / 16384;
let y = this.dataView.getInt16(2, true) / 16384;
let z = this.dataView.getInt16(4, true) / 16384;
[x, y, z] = [-y, z, -x];
const wSquared = 1 - (x * x + y * y + z * z);
const w = wSquared > 0 ? Math.sqrt(wSquared) : 0;
const quat = new Quaternion2(x, y, z, w);
if (!homeQuatInverse) {
homeQuatInverse = quat.clone().invert();
}
return quat.clone().multiply(homeQuatInverse.clone());
}
// Loops from 255 to 0.
moveCounter() {
return this.arr[12];
}
numMovesSince(previousMoveCounter) {
return this.moveCounter() - previousMoveCounter & 255;
}
// Due to the design of the Gan356i protocol, it's common to query for the
// latest physical state and find 0 moves have been performed since the last
// query. Therefore, it's useful to allow 0 as an argument.
latestMoves(n) {
if (n < 0 || n > MAX_LATEST_MOVES) {
throw new Error(`Must ask for 0 to 6 latest moves. (Asked for ${n})`);
}
return Array.from(this.arr.slice(19 - n, 19)).map(
(i) => ganMoveToBlockMove[i]
);
}
debugInfo() {
return {
arr: this.arr
};
}
};
var UUIDs = {
ganCubeService: "0000fff0-0000-1000-8000-00805f9b34fb",
physicalStateCharacteristic: "0000fff5-0000-1000-8000-00805f9b34fb",
actualAngleAndBatteryCharacteristic: "0000fff7-0000-1000-8000-00805f9b34fb",
faceletStatus1Characteristic: "0000fff2-0000-1000-8000-00805f9b34fb",
faceletStatus2Characteristic: "0000fff3-0000-1000-8000-00805f9b34fb",
infoService: "0000180a-0000-1000-8000-00805f9b34fb",
systemIDCharacteristic: "00002a23-0000-1000-8000-00805f9b34fb",
versionCharacteristic: "00002a28-0000-1000-8000-00805f9b34fb"
};
var commands = {
reset: new Uint8Array([
0,
0,
36,
0,
73,
146,
36,
73,
109,
146,
219,
182,
73,
146,
182,
36,
109,
219
])
};
function buf2hex(buffer) {
return Array.prototype.map.call(
new Uint8Array(buffer),
(x) => `00${x.toString(16)}`.slice(-2)
).join(" ");
}
var reidEdgeOrder = "UF UR UB UL DF DR DB DL FR FL BR BL".split(" ");
var reidCornerOrder = "UFR URB UBL ULF DRF DFL DLB DBR".split(" ");
function rotateLeft(s, i) {
return s.slice(i) + s.slice(0, i);
}
var pieceMap = {};
reidEdgeOrder.forEach((edge, idx) => {
for (let i = 0; i < 2; i++) {
pieceMap[rotateLeft(edge, i)] = { piece: idx, orientation: i };
}
});
reidCornerOrder.forEach((corner, idx) => {
for (let i = 0; i < 3; i++) {
pieceMap[rotateLeft(corner, i)] = { piece: idx, orientation: i };
}
});
var gan356iCornerMappings = [
[0, 21, 15],
[5, 13, 47],
[7, 45, 39],
[2, 37, 23],
[29, 10, 16],
[31, 18, 32],
[26, 34, 40],
[24, 42, 8]
];
var gan356iEdgeMappings = [
[1, 22],
[3, 14],
[6, 46],
[4, 38],
[30, 17],
[27, 9],
[25, 41],
[28, 33],
[19, 12],
[20, 35],
[44, 11],
[43, 36]
];
var faceOrder = "URFDLB";
async function getKey(server) {
const infoService = await server.getPrimaryService(UUIDs.infoService);
const versionCharacteristic = await infoService.getCharacteristic(
UUIDs.versionCharacteristic
);
const versionBuffer = new Uint8Array(
(await versionCharacteristic.readValue()).buffer
);
const versionValue = ((versionBuffer[0] << 8) + versionBuffer[1] << 8) + versionBuffer[2];
if (versionValue < 65544) {
return null;
}
const keyXor = versionValue < 65792 ? key10 : key11;
const systemIDCharacteristic = await infoService.getCharacteristic(
UUIDs.systemIDCharacteristic
);
const systemID = new Uint8Array(
(await systemIDCharacteristic.readValue()).buffer
).reverse();
const key = new Uint8Array(keyXor);
for (let i = 0; i < systemID.length; i++) {
key[i] = (key[i] + systemID[i]) % 256;
}
return importKey(key);
}
var GanCube = class _GanCube extends BluetoothPuzzle {
constructor(kpuzzle, service, server, physicalStateCharacteristic, lastMoveCounter, aesKey) {
super();
this.kpuzzle = kpuzzle;
this.service = service;
this.server = server;
this.physicalStateCharacteristic = physicalStateCharacteristic;
this.lastMoveCounter = lastMoveCounter;
this.aesKey = aesKey;
this.pattern = kpuzzle.defaultPattern();
this.startTrackingMoves();
}
// We have to perform async operations before we call the constructor.
static async connect(server) {
const ganCubeService = await server.getPrimaryService(UUIDs.ganCubeService);
debugLog("Service:", ganCubeService);
const physicalStateCharacteristic = await ganCubeService.getCharacteristic(
UUIDs.physicalStateCharacteristic
);
debugLog("Characteristic:", physicalStateCharacteristic);
const aesKey = await getKey(server);
const initialMoveCounter = (await PhysicalState.read(physicalStateCharacteristic, aesKey)).moveCounter();
debugLog("Initial Move Counter:", initialMoveCounter);
const cube = new _GanCube(
await puzzles["3x3x3"].kpuzzle(),
ganCubeService,
server,
physicalStateCharacteristic,
initialMoveCounter,
aesKey
);
return cube;
}
INTERVAL_MS = DEFAULT_INTERVAL_MS;
intervalHandle = null;
pattern;
cachedFaceletStatus1Characteristic;
cachedFaceletStatus2Characteristic;
cachedActualAngleAndBatteryCharacteristic;
name() {
return this.server.device.name;
}
disconnect() {
this.server.disconnect();
}
startTrackingMoves() {
this.intervalHandle = window.setInterval(
this.intervalHandler.bind(this),
this.INTERVAL_MS
);
}
stopTrackingMoves() {
if (!this.intervalHandle) {
throw new Error("Not tracking moves!");
}
clearInterval(this.intervalHandle);
this.intervalHandle = null;
}
// TODO: Can we ever receive async responses out of order?
async intervalHandler() {
const physicalState = await PhysicalState.read(
this.physicalStateCharacteristic,
this.aesKey
);
let numInterveningMoves = physicalState.numMovesSince(this.lastMoveCounter);
if (numInterveningMoves > MAX_LATEST_MOVES) {
debugLog(
`Too many moves! Dropping ${numInterveningMoves - MAX_LATEST_MOVES} moves`
);
numInterveningMoves = MAX_LATEST_MOVES;
}
for (const move of physicalState.latestMoves(numInterveningMoves)) {
this.pattern = this.pattern.applyMove(move);
this.dispatchAlgLeaf({
latestAlgLeaf: move,
timeStamp: physicalState.timeStamp,
debug: physicalState.debugInfo(),
pattern: this.pattern
// quaternion: physicalState.rotQuat(),
});
}
this.dispatchOrientation({
timeStamp: physicalState.timeStamp,
quaternion: physicalState.rotQuat()
});
this.lastMoveCounter = physicalState.moveCounter();
}
async getBattery() {
return new Uint8Array(
await this.readActualAngleAndBatteryCharacteristic()
)[7];
}
async getPattern() {
const arr = await decryptPattern(
new Uint8Array(await this.readFaceletStatus1Characteristic()),
this.aesKey
);
const stickers = [];
for (let i = 0; i < 18; i += 3) {
let v = ((arr[i ^ 1] << 8) + arr[i + 1 ^ 1] << 8) + arr[i + 2 ^ 1];
for (let j = 0; j < 8; j++) {
stickers.push(v & 7);
v >>= 3;
}
}
const patternData = {
CORNERS: {
pieces: [],
orientation: []
},
EDGES: {
pieces: [],
orientation: []
},
CENTERS: {
pieces: [0, 1, 2, 3, 4, 5],
orientation: [0, 0, 0, 0, 0, 0],
orientationMod: [1, 1, 1, 1, 1, 1]
// TODO
}
};
for (const cornerMapping of gan356iCornerMappings) {
const pieceInfo = pieceMap[cornerMapping.map((i) => faceOrder[stickers[i]]).join("")];
patternData["CORNERS"].pieces.push(pieceInfo.piece);
patternData["CORNERS"].orientation.push(pieceInfo.orientation);
}
for (const edgeMapping of gan356iEdgeMappings) {
const pieceInfo = pieceMap[edgeMapping.map((i) => faceOrder[stickers[i]]).join("")];
patternData["EDGES"].pieces.push(pieceInfo.piece);
patternData["EDGES"].orientation.push(pieceInfo.orientation);
}
return new KPattern(this.kpuzzle, patternData);
}
async faceletStatus1Characteristic() {
this.cachedFaceletStatus1Characteristic = this.cachedFaceletStatus1Characteristic || this.service.getCharacteristic(UUIDs.faceletStatus1Characteristic);
return this.cachedFaceletStatus1Characteristic;
}
async faceletStatus2Characteristic() {
this.cachedFaceletStatus2Characteristic = this.cachedFaceletStatus2Characteristic || this.service.getCharacteristic(UUIDs.faceletStatus2Characteristic);
return this.cachedFaceletStatus2Characteristic;
}
async actualAngleAndBatteryCharacteristic() {
this.cachedActualAngleAndBatteryCharacteristic = this.cachedActualAngleAndBatteryCharacteristic || this.service.getCharacteristic(UUIDs.actualAngleAndBatteryCharacteristic);
return this.cachedActualAngleAndBatteryCharacteristic;
}
async reset() {
const faceletStatus1Characteristic = await this.faceletStatus1Characteristic();
await faceletStatus1Characteristic.writeValue(commands["reset"]);
}
async readFaceletStatus1Characteristic() {
const faceletStatus1Characteristic = await this.faceletStatus1Characteristic();
return (await faceletStatus1Characteristic.readValue()).buffer;
}
async readFaceletStatus2Characteristic() {
const faceletStatus2Characteristic = await this.faceletStatus2Characteristic();
return buf2hex((await faceletStatus2Characteristic.readValue()).buffer);
}
async readActualAngleAndBatteryCharacteristic() {
const actualAngleAndBatteryCharacteristic = await this.actualAngleAndBatteryCharacteristic();
return (await actualAngleAndBatteryCharacteristic.readValue()).buffer;
}
// TODO
// private onphysicalStateCharacteristicChanged(event: any): void {
// var val = event.target.value;
// debugLog(val);
// }
};
var ganConfig = {
connect: GanCube.connect.bind(GanCube),
prefixes: ["GAN"],
filters: [{ namePrefix: "GAN" }],
optionalServices: [UUIDs.ganCubeService, UUIDs.infoService]
};
// src/cubing/bluetooth/smart-puzzle/giiker.ts
var MESSAGE_LENGTH = 20;
var UUIDs2 = {
cubeService: "0000aadb-0000-1000-8000-00805f9b34fb",
cubeCharacteristic: "0000aadc-0000-1000-8000-00805f9b34fb"
};
function giikerMoveToAlgMove(face, amount) {
switch (amount) {
case 3: {
amount = -1;
break;
}
case 9: {
debugLog("Encountered 9", face, amount);
amount = -2;
break;
}
}
const family = ["?", "B", "D", "L", "U", "R", "F"][face];
return new Move(family, amount);
}
function giikerStateStr(giikerState) {
let str = "";
str += giikerState.slice(0, 8).join(".");
str += "\n";
str += giikerState.slice(8, 16).join(".");
str += "\n";
str += giikerState.slice(16, 28).join(".");
str += "\n";
str += giikerState.slice(28, 32).join(".");
str += "\n";
str += giikerState.slice(32, 40).join(".");
return str;
}
var Reid333SolvedCenters = {
pieces: [0, 1, 2, 3, 4, 5],
orientation: [0, 0, 0, 0, 0, 0],
orientationMod: [1, 1, 1, 1, 1, 1]
// TODO
};
var epGiiKERtoReid333 = [4, 8, 0, 9, 5, 1, 3, 7, 6, 10, 2, 11];
var epReid333toGiiKER = [2, 5, 10, 6, 0, 4, 8, 7, 1, 3, 9, 11];
var preEO = [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0];
var postEO = [1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0];
var cpGiiKERtoReid333 = [4, 0, 3, 5, 7, 1, 2, 6];
var cpReid333toGiiKER = [1, 5, 6, 2, 0, 3, 7, 4];
var preCO = [1, 2, 1, 2, 2, 1, 2, 1];
var postCO = [2, 1, 2, 1, 1, 2, 1, 2];
var coFlip = [-1, 1, -1, 1, 1, -1, 1, -1];
function getNibble(val, i) {
if (i % 2 === 1) {
return val[i / 2 | 0] % 16;
}
return 0 | val[i / 2 | 0] / 16;
}
function probablyEncrypted(data) {
return data[18] === 167;
}
var lookup = [
176,
81,
104,
224,
86,
137,
237,
119,
38,
26,
193,
161,
210,
126,
150,
81,
93,
13,
236,
249,
89,
235,
88,
24,
113,
81,
214,
131,
130,
199,
2,
169,
39,
165,
171,
41
];
function decryptState(data) {
const offset1 = getNibble(data, 38);
const offset2 = getNibble(data, 39);
const output = new Uint8Array(MESSAGE_LENGTH);
for (let i = 0; i < MESSAGE_LENGTH; i++) {
output[i] = data[i] + lookup[offset1 + i] + lookup[offset2 + i];
}
return output;
}
async function decodeState(data) {
if (!probablyEncrypted(data)) {
return data;
}
return decryptState(data);
}
var GiiKERCube = class _GiiKERCube extends BluetoothPuzzle {
constructor(server, cubeCharacteristic, originalValue) {
super();
this.server = server;
this.cubeCharacteristic = cubeCharacteristic;
this.originalValue = originalValue;
}
static async connect(server) {
const cubeService = await server.getPrimaryService(UUIDs2.cubeService);
debugLog("Service:", cubeService);
const cubeCharacteristic = await cubeService.getCharacteristic(
UUIDs2.cubeCharacteristic
);
debugLog("Characteristic:", cubeCharacteristic);
const originalValue = await decodeState(
new Uint8Array((await cubeCharacteristic.readValue()).buffer)
);
debugLog("Original value:", originalValue);
const cube = new _GiiKERCube(server, cubeCharacteristic, originalValue);
await cubeCharacteristic.startNotifications();
cubeCharacteristic.addEventListener(
"characteristicvaluechanged",
cube.onCubeCharacteristicChanged.bind(cube)
);
return cube;
}
name() {
return this.server.device.name;
}
disconnect() {
this.server.disconnect();
}
async getPattern() {
return this.toReid333(
new Uint8Array((await this.cubeCharacteristic.readValue()).buffer)
);
}
getBit(val, i) {
const n = i / 8 | 0;
const shift = 7 - i % 8;
return val[n] >> shift & 1;
}
toReid333(val) {
const patternData = {
EDGES: {
pieces: new Array(12),
orientation: new Array(12)
},
CORNERS: {
pieces: new Array(8),
orientation: new Array(8)
},
CENTERS: Reid333SolvedCenters
};
for (let i = 0; i < 12; i++) {
const gi = epReid333toGiiKER[i];
patternData["EDGES"].pieces[i] = epGiiKERtoReid333[getNibble(val, gi + 16) - 1];
patternData["EDGES"].orientation[i] = this.getBit(val, gi + 112) ^ preEO[patternData["EDGES"].pieces[i]] ^ postEO[i];
}
for (let i = 0; i < 8; i++) {
const gi = cpReid333toGiiKER[i];
patternData["CORNERS"].pieces[i] = cpGiiKERtoReid333[getNibble(val, gi) - 1];
patternData["CORNERS"].orientation[i] = (getNibble(val, gi + 8) * coFlip[gi] + preCO[patternData["CORNERS"].pieces[i]] + postCO[i]) % 3;
}
return new KPattern(experimental3x3x3KPuzzle, patternData);
}
async onCubeCharacteristicChanged(event) {
const val = await decodeState(new Uint8Array(event.target.value.buffer));
debugLog(val);
debugLog(val);
if (this.isRepeatedInitialValue(val)) {
debugLog("Skipping repeated initial value.");
return;
}
const giikerState = [];
for (let i = 0; i < MESSAGE_LENGTH; i++) {
giikerState.push(Math.floor(val[i] / 16));
giikerState.push(val[i] % 16);
}
debugLog(giikerState);
const str = giikerStateStr(giikerState);
debugLog(str);
this.dispatchAlgLeaf({
latestAlgLeaf: giikerMoveToAlgMove(giikerState[32], giikerState[33]),
timeStamp: event.timeStamp,
debug: {
stateStr: str
},
pattern: this.toReid333(val)
});
}
isRepeatedInitialValue(val) {
if (typeof this.originalValue === "undefined") {
throw new Error("GiiKERCube has uninitialized original value.");
}
if (this.originalValue === null) {
return false;
}
const originalValue = this.originalValue;
this.originalValue = null;
debugLog("Comparing against original value.");
for (let i = 0; i < MESSAGE_LENGTH - 2; i++) {
if (originalValue[i] !== val[i]) {
debugLog("Different at index ", i);
return false;
}
}
return true;
}
};
var giiKERConfig = {
connect: GiiKERCube.connect.bind(GiiKERCube),
prefixes: ["Gi", "", "Mi", "Hi-"],
// Hack
filters: [
// Known prefixes: GiC, GiS (3x3x3), Gi2 (2x2x2)
// Suspected prefixes GiY, Gi3
{ namePrefix: "Gi" },
{ namePrefix: "Mi" },
{ namePrefix: "Hi-" },
{ services: ["0000aadb-0000-1000-8000-00805f9b34fb"] },
{ services: ["0000aaaa-0000-1000-8000-00805f9b34fb"] },
{ services: ["0000fe95-0000-1000-8000-00805f9b34fb"] }
],
optionalServices: [
// "00001530-1212-efde-1523-785feabcd123",
// "0000aaaa-0000-1000-8000-00805f9b34fb",
UUIDs2.cubeService
// "0000180f-0000-1000-8000-00805f9b34fb",
// "0000180a-0000-1000-8000-00805f9b34fb"
]
};
// src/cubing/bluetooth/smart-puzzle/gocube.ts
import { Quaternion as Quaternion3 } from "three/src/math/Quaternion.js";
var UUIDs3 = {
goCubeService: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
goCubeStateCharacteristic: "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
};
function buf2hex2(buffer) {
return Array.prototype.map.call(
new Uint8Array(buffer),
(x) => `00${x.toString(16)}`.slice(-2)
).join(" ");
}
function bufferToString(buffer) {
const byteView = new Uint8Array(buffer);
let str = "";
for (const charCode of byteView) {
str += String.fromCharCode(charCode);
}
return str;
}
var moveMap = [
new Move("B", 1),
new Move("B", -1),
new Move("F", 1),
new Move("F", -1),
new Move("U", 1),
new Move("U", -1),
new Move("D", 1),
new Move("D", -1),
new Move("R", 1),
new Move("R", -1),
new Move("L", 1),
new Move("L", -1)
];
var GoCube = class _GoCube extends BluetoothPuzzle {
constructor(server, goCubeStateCharacteristic) {
super();
this.server = server;
this.goCubeStateCharacteristic = goCubeStateCharacteristic;
}
// We have to perform async operations before we call the constructor.
static async connect(server) {
const service = await server.getPrimaryService(UUIDs3.goCubeService);
debugLog({ service });
const goCubeStateCharacteristic = await service.getCharacteristic(
UUIDs3.goCubeStateCharacteristic
);
debugLog({ goCubeStateCharacteristic });
const cube = new _GoCube(server, goCubeStateCharacteristic);
await goCubeStateCharacteristic.startNotifications();
goCubeStateCharacteristic.addEventListener(
"characteristicvaluechanged",
cube.onCubeCharacteristicChanged.bind(cube)
);
return cube;
}
// public async getState(): Promise<PuzzleState> {
// return new Promise((resolve, reject) => {
// this.resolve = (value: any) => {
// resolve(buf2hex(value.buffer) as any);
// };
// this.goCubeStateCharacteristic.startNotifications();
// });
// }
recorded = [];
homeQuatInverse = null;
lastRawQuat = new Quaternion3(0, 0, 0, 1);
currentQuat = new Quaternion3(0, 0, 0, 1);
lastTarget = new Quaternion3(0, 0, 0, 1);
alg = new Alg();
disconnect() {
this.server.disconnect();
}
reset() {
this.resetAlg();
this.resetOrientation();
}
resetAlg(alg) {
this.alg = alg || new Alg();
}
resetOrientation() {
this.homeQuatInverse = this.lastRawQuat.clone().invert();
this.currentQuat = new Quaternion3(0, 0, 0, 1);
this.lastTarget = new Quaternion3(0, 0, 0, 1);
}
name() {
return this.server.device.name;
}
onCubeCharacteristicChanged(event) {
const buffer = event.target.value;
this.recorded.push([event.timeStamp, buf2hex2(buffer.buffer)]);
if (buffer.byteLength < 16) {
for (let i = 3; i < buffer.byteLength - 4; i += 2) {
const move = moveMap[buffer.getUint8(i)];
this.alg = experimentalAppendMove(this.alg, move);
this.dispatchAlgLeaf({
latestAlgLeaf: moveMap[buffer.getUint8(i)],
timeStamp: event.timeStamp,
debug: {
stateStr: buf2hex2(buffer.buffer)
}
});
}
} else {
const coords = bufferToString(
buffer.buffer.slice(3, buffer.byteLength - 3)
).split("#").map((s) => parseInt(s, 10) / 16384);
const quat = new Quaternion3(coords[0], coords[1], coords[2], coords[3]);
this.lastRawQuat = quat.clone();
if (!this.homeQuatInverse) {
this.homeQuatInverse = quat.clone().invert();
}
const targetQuat = quat.clone().multiply(this.homeQuatInverse.clone());
targetQuat.y = -targetQuat.y;
this.lastTarget.slerp(targetQuat, 0.5);
this.currentQuat.rotateTowards(this.lastTarget, rotateTowardsRate);
this.dispatchOrientation({
quaternion: this.currentQuat,
timeStamp: event.timeStamp
});
}
}
};
var rotateTowardsRate = 0.5;
var goCubeConfig = {
connect: GoCube.connect.bind(GoCube),
prefixes: ["GoCube", "Rubik"],
filters: [{ namePrefix: "GoCube" }, { namePrefix: "Rubik" }],
optionalServices: [UUIDs3.goCubeService]
};
// src/cubing/bluetooth/smart-puzzle/endianness.ts
function flipBitOrder(v, numBits) {
let result = 0;
for (let i = 0; i < numBits; i++) {
const shiftLeft = numBits - 1 - 2 * i;
const unShiftedBit = v & 1 << i;
result += shiftLeft < 0 ? unShiftedBit >> -shiftLeft : unShiftedBit << shiftLeft;
}
return result;
}
// src/cubing/bluetooth/smart-puzzle/Heykube.ts
var UUIDs4 = {
heykubeService: "b46a791a-8273-4fc1-9e67-94d3dc2aac1c",
stateCharacteristic: "a2f41a4e-0e31-4bbc-9389-4253475481fb",
batteryCharacteristic: "fd51b3ba-99c7-49c6-9f85-5644ff56a378"
};
var HeykubeCube = class _HeykubeCube extends BluetoothPuzzle {
constructor(_kpuzzle, _service, device, server, stateCharacteristic) {
super();
this.server = server;
this.stateCharacteristic = stateCharacteristic;
device.addEventListener(
"gattserverdisconnected",
this.onDisconnect.bind(this)
);
this.stateCharacteristic.startNotifications();
this.startTrackingMoves();
}
// We have to perform async operations before we call the constructor.
static async connect(server, device) {
const service = await server.getPrimaryService(UUIDs4.heykubeService);
debugLog("Service:", service);
const stateCharacteristic = await service.getCharacteristic(
UUIDs4.stateCharacteristic
);
debugLog("Characteristic:", stateCharacteristic);
const cube = new _HeykubeCube(
await puzzles["3x3x3"].kpuzzle(),
service,
device,
server,
stateCharacteristic
);
return cube;
}
name() {
return this.server.device.name;
}
disconnect() {
this.server.disconnect();
}
onDisconnect() {
this.dispatchEvent(new CustomEvent("disconnect"));
}
startTrackingMoves() {
this.stateCharacteristic.addEventListener(
"characteristicvaluechanged",
(e) => this.onStateCharacteristic(e)
);
}
// public stopTrackingMoves(): void {}
// public async getBattery(): Promise<number> {
// return new Uint8Array(
// await this.readActualAngleAndBatteryCharacteristic(),
// )[7];
// }srcElement: BluetoothRemoteGATTCharacteristic
onStateCharacteristic(event) {
const state = this.decodeState(event.target.value);
this.dispatchAlgLeaf({
latestAlgLeaf: state.latestMove,
timeStamp: event.timeStamp,
pattern: state.pattern
});
}
decodeState(dv) {
const moves = [
new Move("U"),
new Move("U'"),
new Move("B"),
new Move("B'"),
new Move("F"),
new Move("F'"),
null,
null,
new Move("L"),
new Move("L'"),
new Move("D"),
new Move("D'"),
new Move("R"),
new Move("R'")
// null,
// null,
];
const b2 = new Uint8Array(dv.byteLength);
for (let i = 0; i < dv.byteLength; i++) {
b2[i] = flipBitOrder(dv.getUint8(i), 8);
}
const components1 = twizzleBinaryToBinaryComponents(
b2.slice(0, 11)
);
const components2 = {
epLex: flipBitOrder(components1.epLex, 29),
eoMask: flipBitOrder(components1.eoMask, 12),
cpLex: flipBitOrder(components1.cpLex, 16),
coMask: flipBitOrder(components1.coMask, 13),
poIdxL: 0,
poIdxU: 7,
moSupport: 1,
// TODO
moMask: 0
};
return {
pattern: binaryComponentsToReid3x3x3(components2),
latestMove: moves[b2[20] & 15]
};
}
async getPattern() {
const b1 = await this.stateCharacteristic.readValue();
return this.decodeState(b1).pattern;
}
};
var heykubeConfig = {
connect: HeykubeCube.connect.bind(HeykubeCube),
prefixes: ["HEYKUBE"],
filters: [{ namePrefix: "HEYKUBE" }],
optionalServices: [UUIDs4.heykubeService]
};
// src/cubing/bluetooth/smart-puzzle/connect.ts
var smartPuzzleConfigs = [
ganConfig,
goCubeConfig,
heykubeConfig,
giiKERConfig
// GiiKER must be last, due to Xiaomi naming. TODO: enforce this using tests.
];
async function connectSmartPuzzle(options) {
return bluetoothConnect(smartPuzzleConfigs, options);
}
// src/cubing/bluetooth/smart-robot/GanRobot.ts
function buf2hex3(buffer) {
return Array.prototype.map.call(
new Uint8Array(buffer),
(x) => `00${x.toString(16)}`.slice(-2)
).join(" ");
}
var MAX_NIBBLES_PER_WRITE = 18 * 2;
var QUANTUM_TURN_DURATION_MS = 150;
var DOUBLE_TURN_DURATION_MS = 250;
var U_D_SWAP = new Alg("F B R2 L2 B' F'");
var U_D_UNSWAP = U_D_SWAP.invert();
var F_B_SWAP = new Alg("U D R2 L2 D' U'");
var F_B_UNSWAP = F_B_SWAP.invert();
var UUIDs5 = {
ganRobotService: "0000fff0-0000-1000-8000-00805f9b34fb",
statusCharacteristic: "0000fff2-0000-1000-8000-00805f9b34fb",
moveCharacteristic: "0000fff3-0000-1000-8000-00805f9b34fb"
};
var moveMap2 = {
R: 0,
R2: 1,
"R2'": 1,
"R'": 2,
F: 3,
F2: 4,
"F2'": 4,
"F'": 5,
D: 6,
D2: 7,
"D2'": 7,
"D'": 8,
L: 9,
L2: 10,
"L2'": 10,
"L'": 11,
B: 12,
B2: 13,
"B2'": 13,
"B'": 14
};
var moveMapX = {
R: 0,
R2: 1,
"R2'": 1,
"R'": 2,
U: 3,
U2: 4,
"U2'": 4,
"U'": 5,
F: 6,
F2: 7,
"F2'": 7,
"F'": 8,
L: 9,
L2: 10,
"L2'": 10,
"L'": 11,
D: 12,
D2: 13,
"D2'": 13,
"D'": 14
};
function isDoubleTurnNibble(nibble) {
return nibble % 3 === 1;
}
function nibbleDuration(nibble) {
return isDoubleTurnNibble(nibble) ? DOUBLE_TURN_DURATION_MS : QUANTUM_TURN_DURATION_MS;
}
function throwInvalidAlgNode(algNode) {
console.error("invalid alg node", algNode, algNode.toString());
throw new Error("invalid alg node!");
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
var GanRobot = class _GanRobot extends EventTarget {
constructor(_service, server, device, statusCharacteristic, moveCharacteristic) {
super();
this.server = server;
this.statusCharacteristic = statusCharacteristic;
this.moveCharacteristic = moveCharacteristic;
device.addEventListener(
"gattserverdisconnected",
this.onDisconnect.bind(this)
);
}
experimentalDebugOnSend = null;
experimentalDebugLog = () => {
};
// Because our Bluetooth connection code is set up not to know what kind of device is connecting, we put these options directly on the class.
experimentalOptions = {
xAngle: false,
singleMoveFixHack: false,
bufferQueue: 0,
postSleep: 0
};
// We have to perform async operations before we call the constructor.
static async connect(server, device) {
const ganTimerService = await server.getPrimaryService(
UUIDs5.ganRobotService
);
const statusCharacteristic = await ganTimerService.getCharacteristic(
UUIDs5.statusCharacteristic
);
const moveCharacteristic = await ganTimerService.getCharacteristic(
UUIDs5.moveCharacteristic
);
const timer = new _GanRobot(
ganTimerService,
server,
device,
statusCharacteristic,
moveCharacteristic
);
return timer;
}
name() {
return this.server.device.name;
}
disconnect() {
this.server.disconnect();
}
onDisconnect() {
this.dispatchEvent(new CustomEvent("disconnect"));
}
algNodeToNibble(algNode) {
const move = algNode.as(Move);
if (!move) {
throwInvalidAlgNode(algNode);
}
const nibble = (this.experimentalOptions.xAngle ? moveMapX : moveMap2)[move.toString()] ?? null;
if (nibble === null) {
throwInvalidAlgNode(move);
}
return nibble;
}
async writeNibbles(nibbles) {
if (nibbles.length > MAX_NIBBLES_PER_WRITE) {
throw new Error(
`Can only write ${MAX_NIBBLES_PER_WRITE} nibbles at a time!`
);
}
const bytes = new Uint8Array(18);
let i;
for (i = 0; i < nibbles.length; i++) {
const byteIdx = Math.floor(i / 2);
bytes[byteIdx] += nibbles[i];
if (i % 2 === 0) {
bytes[byteIdx] *= 16;
}
}
if (nibbles.length % 2 === 1) {
bytes[Math.ceil(nibbles.length / 2) - 1] += 15;
}
for (let i2 = Math.ceil(nibbles.length / 2); i2 < 18; i2++) {
bytes[i2] = 255;
}
let sleepDuration = 0;
for (const nibble of nibbles) {
sleepDuration += nibbleDuration(nibble);
}
this.experimentalDebugLog("WRITING:", buf2hex3(bytes));
await this.moveCharacteristic.writeValue(bytes);
await sleep(sleepDuration * 0.75);
while ((await this.getStatus()).movesRemaining > 0) {
}
await sleep(this.experimentalOptions.postSleep);
}
async getStatus() {
const statusBytes = new Uint8Array(
(await this.statusCharacteristic.readValue()).buffer
);
this.experimentalDebugLog("moves remaining:", statusBytes[0]);
return {
movesRemaining: statusBytes[0]
};
}
locked = false;
processQueue() {
}
moveQueue = new Alg();
// TODO: Don't let this resolve until the move is done?
async queueMoves(moves) {
this.moveQueue = this.moveQueue.concat(moves).experimentalSimplify({
puzzleSpecificSimplifyOptions: cube3x3x3.puzzleSpecificSimplifyOptions
});
if (!this.locked) {
try {
this.locked = true;
if (this.moveQueue.experimentalNumChildAlgNodes() === 1) {
await sleep(this.experimentalOptions.bufferQueue);
}
while (this.moveQueue.experimentalNumChildAlgNodes() > 0) {
let algNodes = Array.from(this.moveQueue.childAlgNodes());
if (this.experimentalOptions.singleMoveFixHack && algNodes.length === 1) {
const move = algNodes[0].as(Move);
if (move) {
if (move.amount === 2) {
algNodes = [
move.modified({ amount: 1 }),
move.modified({ amount: 1 })
];
} else {
algNodes = [
move.modified({ amount: -move.amount }),
move.modified({ amount: 2 })
];
}
}
}
const splicedAlgNodes = algNodes.splice(
0,
MAX_NIBBLES_PER_WRITE
);
const nibbles = splicedAlgNodes.map(
this.algNodeToNibble.bind(this)
);
const sending = new Alg(splicedAlgNodes);
this.experimentalDebugLog("SENDING", sending.toString());
if (this.experimentalDebugOnSend) {
this.experimentalDebugOnSend(sending);
}
const write = this.writeNibbles(nibbles);
this.moveQueue = new Alg(algNodes);
await write;
}
} finally {
this.locked = false;
}
}
}
async applyMoves(moves) {
for (const move of moves) {
const str = move.toString();
if (str in (this.experimentalOptions.xAngle ? moveMapX : moveMap2)) {
await this.queueMoves(new Alg([move]));
} else if (move.family === (this.experimentalOptions.xAngle ? "B" : "U")) {
await Promise.all([
this.queueMoves(
this.experimentalOptions.xAngle ? F_B_SWAP : U_D_SWAP
),
this.queueMoves(
new Alg([
move.modified({
family: this.experimentalOptions.xAngle ? "F" : "D"
})
]).concat(
this.experimentalOptions.xAngle ? F_B_UNSWAP : U_D_UNSWAP
)
)
]);
}
}
}
};
var ganTimerConfig = {
connect: GanRobot.connect.bind(GanRobot),
prefixes: ["GAN"],
filters: [{ namePrefix: "GAN" }],
optionalServices: [UUIDs5.ganRobotService]
};
// src/cubing/bluetooth/smart-robot/index.ts
var smartRobotConfigs = [ganTimerConfig];
async function connectSmartRobot(options) {
return bluetoothConnect(smartRobotConfigs, options);
}
// src/cubing/bluetooth/smart-timer/GanTimer.ts
var UUIDs6 = {
ganTimerService: "0000fff0-0000-1000-8000-00805f9b34fb",
timeCharacteristic: "0000fff2-0000-1000-8000-00805f9b34fb"
};
var GanTimer = class _GanTimer extends EventTarget {
constructor(_service, server, device, timeCharacteristic) {
super();
this.server = server;
this.timeCharacteristic = timeCharacteristic;
this.startPolling();
console.log(server);
device.addEventListener(
"gattserverdisconnected",
this.onDisconnect.bind(this)
);
}
polling = false;
previousDetail = null;
// We have to perform async operations before we call the constructor.
static async connect(server, device) {
const ganTimerService = await server.getPrimaryService(
UUIDs6.ganTimerService
);
console.log("Service:", ganTimerService);
const timeCharacteristic = await ganTimerService.getCharacteristic(
UUIDs6.timeCharacteristic
);
console.log("Characteristic:", timeCharacteristic);
const timer = new _GanTimer(
ganTimerService,
server,
device,
timeCharacteristic
);
return timer;
}
disconnect() {
this.server.disconnect();
}
async poll() {
if (!this.polling) {
return;
}
const value = await this.getTimeCharacteristic();
const detail = {
currentTime: this.decodeTimeMs(value.slice(0, 4)),
latestTimes: [
this.decodeTimeMs(value.slice(4, 8)),
this.decodeTimeMs(value.slice(8, 12)),
this.decodeTimeMs(value.slice(12, 16))
]
};
if (detail.currentTime === 0) {
if (this.previousDetail && this.previousDetail.currentTime !== 0) {
this.dispatchEvent(new CustomEvent("reset"));
}
}
if (detail.currentTime !== 0 && this.previousDetail) {
if (this.previousDetail.currentTime === 0) {
this.dispatchEvent(new CustomEvent("start"));
}
if (detail.currentTime !== this.previousDetail.currentTime) {
this.dispatchEvent(new CustomEvent("update", { detail }));
if (detail.currentTime === detail.latestTimes[0] && detail.latestTimes[1] === this.previousDetail.latestTimes[0] && detail.latestTimes[2] === this.previousDetail.latestTimes[1]) {
this.dispatchEvent(new CustomEvent("stop", { detail }));
}
}
}
this.previousDetail = detail;
this.poll();
}
onDisconnect() {
this.dispatchEvent(new CustomEvent("disconnect"));
}
async getTimeCharacteristic() {
return new Uint8Array((await this.timeCharacteristic.readValue()).buffer);
}
async getTime() {
const value = await this.getTimeCharacteristic();
return this.decodeTimeMs(value.slice(0, 4));
}
decodeTimeMs(bytes) {
return (bytes[0] * 60 + bytes[1]) * 1e3 + bytes[2] + bytes[3] * 256;
}
startPolling() {
this.polling = true;
this.poll();
}
stopPolling() {
this.polling = false;
}
};
var ganTimerConfig2 = {
connect: GanTimer.connect.bind(GanTimer),
prefixes: ["GAN"],
filters: [{ namePrefix: "GAN" }],
optionalServices: [UUIDs6.ganTimerService]
};
// src/cubing/bluetooth/smart-timer/index.ts
var smartTimerConfigs = [ganTimerConfig2];
async function connectSmartTimer(options) {
return bluetoothConnect(smartTimerConfigs, options);
}
export {
GanCube,
GiiKERCube,
GoCube,
KeyboardPuzzle,
connectSmartPuzzle,
connectSmartRobot,
connectSmartTimer,
debugKeyboardConnect,
enableDebugLogging
};
//# sourceMappingURL=index.js.map