@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.
Files changed (279) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +118 -0
  3. package/analyze/client.d.ts +18 -0
  4. package/analyze/client.js +42 -0
  5. package/analyze/index.d.ts +114 -0
  6. package/analyze/index.js +151 -0
  7. package/analyze/rotations.d.ts +3 -0
  8. package/analyze/rotations.js +15 -0
  9. package/analyze/roux.d.ts +67 -0
  10. package/analyze/roux.js +95 -0
  11. package/analyze/worker.d.ts +6 -0
  12. package/analyze/worker.js +31 -0
  13. package/analyze/zz.d.ts +47 -0
  14. package/analyze/zz.js +102 -0
  15. package/bld/index.d.ts +14 -0
  16. package/bld/index.js +14 -0
  17. package/bld/memo.d.ts +66 -0
  18. package/bld/memo.js +139 -0
  19. package/bld/scheme.d.ts +30 -0
  20. package/bld/scheme.js +35 -0
  21. package/bld/skin.d.ts +26 -0
  22. package/bld/skin.js +46 -0
  23. package/bld/tracker.d.ts +68 -0
  24. package/bld/tracker.js +145 -0
  25. package/bluetooth/clock.d.ts +22 -0
  26. package/bluetooth/clock.js +37 -0
  27. package/bluetooth/cubeSkins.d.ts +31 -0
  28. package/bluetooth/cubeSkins.js +32 -0
  29. package/bluetooth/gyro.d.ts +31 -0
  30. package/bluetooth/gyro.js +46 -0
  31. package/bluetooth/index.d.ts +5 -0
  32. package/bluetooth/index.js +5 -0
  33. package/bluetooth/session.d.ts +140 -0
  34. package/bluetooth/session.js +201 -0
  35. package/bluetooth/simulated.d.ts +52 -0
  36. package/bluetooth/simulated.js +80 -0
  37. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
  38. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
  39. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
  40. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
  41. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
  42. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
  43. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
  44. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
  45. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
  46. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
  47. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
  48. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
  49. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
  50. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
  51. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
  52. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
  53. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
  54. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
  55. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
  56. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
  57. package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
  58. package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
  59. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
  60. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
  61. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
  62. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
  63. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
  64. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
  65. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
  66. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
  67. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
  68. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
  69. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
  70. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
  71. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
  72. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
  73. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
  74. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
  75. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
  76. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
  77. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
  78. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
  79. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
  80. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
  81. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
  82. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
  83. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
  84. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
  85. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
  86. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
  87. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
  88. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
  89. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
  90. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
  91. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
  92. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
  93. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
  94. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
  95. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
  96. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
  97. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
  98. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
  99. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
  100. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
  101. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
  102. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
  103. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
  104. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
  105. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
  106. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
  107. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
  108. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
  109. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
  110. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
  111. package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
  112. package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
  113. package/cfop/caseData.d.ts +16 -0
  114. package/cfop/caseData.js +88 -0
  115. package/cfop/cases.d.ts +42 -0
  116. package/cfop/cases.js +116 -0
  117. package/cfop/f2l.d.ts +28 -0
  118. package/cfop/f2l.js +88 -0
  119. package/cfop/f2lData.d.ts +15 -0
  120. package/cfop/f2lData.js +49 -0
  121. package/cfop/index.d.ts +41 -0
  122. package/cfop/index.js +110 -0
  123. package/cfop/slots.d.ts +2 -0
  124. package/cfop/slots.js +1 -0
  125. package/core/arrows.d.ts +24 -0
  126. package/core/arrows.js +22 -0
  127. package/core/bytes.d.ts +27 -0
  128. package/core/bytes.js +145 -0
  129. package/core/centers.d.ts +32 -0
  130. package/core/centers.js +83 -0
  131. package/core/checks.d.ts +53 -0
  132. package/core/checks.js +85 -0
  133. package/core/cubies.d.ts +45 -0
  134. package/core/cubies.js +169 -0
  135. package/core/frames.d.ts +61 -0
  136. package/core/frames.js +126 -0
  137. package/core/geometry.d.ts +58 -0
  138. package/core/geometry.js +132 -0
  139. package/core/index.d.ts +19 -0
  140. package/core/index.js +19 -0
  141. package/core/masks.d.ts +32 -0
  142. package/core/masks.js +39 -0
  143. package/core/method.d.ts +35 -0
  144. package/core/method.js +30 -0
  145. package/core/moves.d.ts +48 -0
  146. package/core/moves.js +88 -0
  147. package/core/notation.d.ts +48 -0
  148. package/core/notation.js +226 -0
  149. package/core/physical.d.ts +53 -0
  150. package/core/physical.js +99 -0
  151. package/core/practice.d.ts +62 -0
  152. package/core/practice.js +87 -0
  153. package/core/sequence.d.ts +90 -0
  154. package/core/sequence.js +157 -0
  155. package/core/stages.d.ts +90 -0
  156. package/core/stages.js +21 -0
  157. package/core/state.d.ts +55 -0
  158. package/core/state.js +140 -0
  159. package/core/stateCodec.d.ts +12 -0
  160. package/core/stateCodec.js +51 -0
  161. package/core/stream.d.ts +45 -0
  162. package/core/stream.js +75 -0
  163. package/core/tracker.d.ts +62 -0
  164. package/core/tracker.js +121 -0
  165. package/element/alg.d.ts +45 -0
  166. package/element/alg.js +150 -0
  167. package/element/base.d.ts +2 -0
  168. package/element/base.js +3 -0
  169. package/element/bld.d.ts +82 -0
  170. package/element/bld.js +222 -0
  171. package/element/index.d.ts +8 -0
  172. package/element/index.js +21 -0
  173. package/element/model.d.ts +36 -0
  174. package/element/model.js +64 -0
  175. package/element/player.d.ts +196 -0
  176. package/element/player.js +680 -0
  177. package/element/practice.d.ts +75 -0
  178. package/element/practice.js +150 -0
  179. package/element/scramble.d.ts +50 -0
  180. package/element/scramble.js +107 -0
  181. package/element/sequence.d.ts +126 -0
  182. package/element/sequence.js +254 -0
  183. package/element/styles.d.ts +32 -0
  184. package/element/styles.js +196 -0
  185. package/image/cache.d.ts +21 -0
  186. package/image/cache.js +66 -0
  187. package/image/index.d.ts +2 -0
  188. package/image/index.js +2 -0
  189. package/image/png-node.d.ts +6 -0
  190. package/image/png-node.js +10 -0
  191. package/image/svg.d.ts +36 -0
  192. package/image/svg.js +215 -0
  193. package/lbl/index.d.ts +10 -0
  194. package/lbl/index.js +31 -0
  195. package/methods/index.d.ts +20 -0
  196. package/methods/index.js +33 -0
  197. package/package.json +133 -0
  198. package/petrus/index.d.ts +11 -0
  199. package/petrus/index.js +27 -0
  200. package/react/index.d.ts +176 -0
  201. package/react/index.js +228 -0
  202. package/render/arrows.d.ts +68 -0
  203. package/render/arrows.js +190 -0
  204. package/render/build/attachments.d.ts +35 -0
  205. package/render/build/attachments.js +94 -0
  206. package/render/build/models.d.ts +9 -0
  207. package/render/build/models.js +27 -0
  208. package/render/build/tiles.d.ts +48 -0
  209. package/render/build/tiles.js +194 -0
  210. package/render/gltf.d.ts +10 -0
  211. package/render/gltf.js +106 -0
  212. package/render/index.d.ts +12 -0
  213. package/render/index.js +12 -0
  214. package/render/layers.d.ts +21 -0
  215. package/render/layers.js +24 -0
  216. package/render/pictures.d.ts +46 -0
  217. package/render/pictures.js +80 -0
  218. package/render/pieceModels.d.ts +38 -0
  219. package/render/pieceModels.js +43 -0
  220. package/render/renderer.d.ts +165 -0
  221. package/render/renderer.js +647 -0
  222. package/render/tile.d.ts +114 -0
  223. package/render/tile.js +390 -0
  224. package/render/viewports.d.ts +25 -0
  225. package/render/viewports.js +26 -0
  226. package/roux/cmll.d.ts +35 -0
  227. package/roux/cmll.js +121 -0
  228. package/roux/cmllData.d.ts +12 -0
  229. package/roux/cmllData.js +49 -0
  230. package/roux/index.d.ts +52 -0
  231. package/roux/index.js +175 -0
  232. package/skin/attachments.d.ts +82 -0
  233. package/skin/attachments.js +110 -0
  234. package/skin/index.d.ts +3 -0
  235. package/skin/index.js +3 -0
  236. package/skin/shapes.d.ts +112 -0
  237. package/skin/shapes.js +147 -0
  238. package/skin/skin.d.ts +911 -0
  239. package/skin/skin.js +412 -0
  240. package/solve/client.d.ts +58 -0
  241. package/solve/client.js +56 -0
  242. package/solve/handle.d.ts +6 -0
  243. package/solve/handle.js +38 -0
  244. package/solve/index.d.ts +8 -0
  245. package/solve/index.js +7 -0
  246. package/solve/lse.d.ts +42 -0
  247. package/solve/lse.js +151 -0
  248. package/solve/pieces.d.ts +18 -0
  249. package/solve/pieces.js +24 -0
  250. package/solve/protocol.d.ts +70 -0
  251. package/solve/protocol.js +16 -0
  252. package/solve/scramble.d.ts +76 -0
  253. package/solve/scramble.js +187 -0
  254. package/solve/stage.d.ts +108 -0
  255. package/solve/stage.js +532 -0
  256. package/solve/twophase.d.ts +36 -0
  257. package/solve/twophase.js +229 -0
  258. package/solve/worker.d.ts +6 -0
  259. package/solve/worker.js +15 -0
  260. package/timeline/codec.d.ts +23 -0
  261. package/timeline/codec.js +84 -0
  262. package/timeline/index.d.ts +7 -0
  263. package/timeline/index.js +7 -0
  264. package/timeline/recording.d.ts +19 -0
  265. package/timeline/recording.js +20 -0
  266. package/timeline/replay.d.ts +68 -0
  267. package/timeline/replay.js +145 -0
  268. package/timeline/segments.d.ts +31 -0
  269. package/timeline/segments.js +39 -0
  270. package/timeline/share.d.ts +30 -0
  271. package/timeline/share.js +62 -0
  272. package/timeline/stats.d.ts +34 -0
  273. package/timeline/stats.js +56 -0
  274. package/timeline/timings.d.ts +30 -0
  275. package/timeline/timings.js +54 -0
  276. package/zz/index.d.ts +29 -0
  277. package/zz/index.js +68 -0
  278. package/zz/seeds.d.ts +9 -0
  279. 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 };