@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,352 @@
1
+ import { Subject } from 'rxjs';
2
+ import { normalizeUuid } from '../attachment/normalize-uuid.js';
3
+ import { registerProtocol } from '../protocol.js';
4
+ import { CubieCube, SOLVED_FACELET } from '../cubie-cube.js';
5
+ import { now, findCharacteristic } from '../ble-utils.js';
6
+ import { MoyuV1Client, moyuStickersToFaceletString, MOYU_V1_SOLVED_STICKERS, } from './moyu-v1.js';
7
+ const UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
8
+ const SERVICE_UUID = '00001000' + UUID_SUFFIX;
9
+ const CHRCT_UUID_WRITE = '00001001' + UUID_SUFFIX;
10
+ const CHRCT_UUID_READ = '00001002' + UUID_SUFFIX;
11
+ const CHRCT_UUID_TURN = '00001003' + UUID_SUFFIX;
12
+ const CHRCT_UUID_GYRO = '00001004' + UUID_SUFFIX;
13
+ const FACE_ORDER_LEN = 6;
14
+ const DEVICE_FACE_TO_AXIS = [3, 4, 5, 1, 2, 0];
15
+ function normalizeQuaternion(q) {
16
+ const n = Math.hypot(q.w, q.x, q.y, q.z) || 1;
17
+ return { w: q.w / n, x: q.x / n, y: q.y / n, z: q.z / n };
18
+ }
19
+ const MOYU_MHC_PROTOCOL = { id: 'moyu-mhc', name: 'MoYu MHC' };
20
+ class MoyuMhcConnection {
21
+ deviceName;
22
+ deviceMAC;
23
+ protocol = MOYU_MHC_PROTOCOL;
24
+ capabilities;
25
+ events$;
26
+ device;
27
+ writeChrct = null;
28
+ readChrct = null;
29
+ turnChrct = null;
30
+ gyroChrct = null;
31
+ v1 = null;
32
+ faceStatus = [0, 0, 0, 0, 0, 0];
33
+ curCubie = new CubieCube();
34
+ prevCubie = new CubieCube();
35
+ lastBatteryLevel = null;
36
+ batteryInterval = null;
37
+ constructor(device) {
38
+ this.device = device;
39
+ this.deviceName = device.name || 'MHC';
40
+ this.deviceMAC = '';
41
+ this.events$ = new Subject();
42
+ this.capabilities = {
43
+ gyroscope: false,
44
+ battery: false,
45
+ facelets: false,
46
+ hardware: false,
47
+ reset: false,
48
+ };
49
+ }
50
+ onTurnEvent = (event) => {
51
+ const value = event.target.value;
52
+ if (!value)
53
+ return;
54
+ this.parseTurn(value);
55
+ };
56
+ onReadEvent = (event) => {
57
+ const value = event.target.value;
58
+ if (!value || !this.v1)
59
+ return;
60
+ this.v1.onReadNotification(value);
61
+ };
62
+ onGyroEvent = (event) => {
63
+ const e = event.target.value;
64
+ if (!e || e.byteLength < 20)
65
+ return;
66
+ const fw = e.getFloat32(4, true);
67
+ const fx = e.getFloat32(8, true);
68
+ const fy = e.getFloat32(12, true);
69
+ const fz = e.getFloat32(16, true);
70
+ const quaternion = normalizeQuaternion({
71
+ w: fw,
72
+ x: fx,
73
+ y: -fy,
74
+ z: fz,
75
+ });
76
+ const timestamp = now();
77
+ this.events$.next({
78
+ timestamp,
79
+ type: 'GYRO',
80
+ quaternion,
81
+ });
82
+ };
83
+ /**
84
+ * After each move, `prevCubie` holds the latest physical state (see parseTurn swap).
85
+ * Seed that cube from the device facelet string and align turn counters with gyro angles.
86
+ */
87
+ applyCubeStateFromDevice(stickers, angles) {
88
+ const facelet = moyuStickersToFaceletString(stickers);
89
+ this.prevCubie = new CubieCube();
90
+ const parsed = this.prevCubie.fromFacelet(facelet);
91
+ if (parsed === -1) {
92
+ this.prevCubie = new CubieCube();
93
+ this.curCubie = new CubieCube();
94
+ this.faceStatus = [0, 0, 0, 0, 0, 0];
95
+ return;
96
+ }
97
+ this.curCubie = new CubieCube();
98
+ for (let i = 0; i < FACE_ORDER_LEN; i++) {
99
+ this.faceStatus[i] = (angles[i] ?? 0) % 9;
100
+ }
101
+ }
102
+ parseTurn(data) {
103
+ const timestamp = now();
104
+ if (data.byteLength < 1)
105
+ return;
106
+ const nMoves = data.getUint8(0);
107
+ if (data.byteLength < 1 + nMoves * 6)
108
+ return;
109
+ for (let i = 0; i < nMoves; i++) {
110
+ const offset = 1 + i * 6;
111
+ let ts = data.getUint8(offset + 1) << 24
112
+ | data.getUint8(offset + 0) << 16
113
+ | data.getUint8(offset + 3) << 8
114
+ | data.getUint8(offset + 2);
115
+ ts = Math.round(ts / 65536 * 1000);
116
+ const face = data.getUint8(offset + 4);
117
+ if (face >= FACE_ORDER_LEN)
118
+ continue;
119
+ const dir = Math.round(data.getInt8(offset + 5) / 36);
120
+ const prevRot = this.faceStatus[face];
121
+ const curRot = this.faceStatus[face] + dir;
122
+ this.faceStatus[face] = (curRot + 9) % 9;
123
+ const axis = DEVICE_FACE_TO_AXIS[face];
124
+ let pow;
125
+ if (prevRot >= 5 && curRot <= 4) {
126
+ pow = 2;
127
+ }
128
+ else if (prevRot <= 4 && curRot >= 5) {
129
+ pow = 0;
130
+ }
131
+ else {
132
+ continue;
133
+ }
134
+ const m = axis * 3 + pow;
135
+ const moveStr = ("URFDLB".charAt(axis) + " 2'".charAt(pow)).trim();
136
+ CubieCube.CubeMult(this.prevCubie, CubieCube.moveCube[m], this.curCubie);
137
+ const faceletStr = this.curCubie.toFaceCube();
138
+ this.events$.next({
139
+ timestamp,
140
+ type: "MOVE",
141
+ face: axis,
142
+ direction: pow === 0 ? 0 : 1,
143
+ move: moveStr,
144
+ localTimestamp: timestamp,
145
+ cubeTimestamp: ts
146
+ });
147
+ this.events$.next({
148
+ timestamp,
149
+ type: "FACELETS",
150
+ facelets: faceletStr
151
+ });
152
+ const tmp = this.curCubie;
153
+ this.curCubie = this.prevCubie;
154
+ this.prevCubie = tmp;
155
+ }
156
+ }
157
+ emitBatteryLevel(rawLevel, timestamp = now(), forceEmission = false) {
158
+ if (!Number.isFinite(rawLevel)) {
159
+ return;
160
+ }
161
+ const batteryLevel = Math.min(100, Math.max(0, Math.round(rawLevel)));
162
+ if (!forceEmission && this.lastBatteryLevel === batteryLevel) {
163
+ return;
164
+ }
165
+ this.lastBatteryLevel = batteryLevel;
166
+ this.events$.next({
167
+ timestamp,
168
+ type: 'BATTERY',
169
+ batteryLevel,
170
+ });
171
+ }
172
+ async pollBattery() {
173
+ if (!this.v1) {
174
+ return;
175
+ }
176
+ try {
177
+ const b = await this.v1.getBatteryInfo();
178
+ this.emitBatteryLevel(b.value.percentage);
179
+ }
180
+ catch {
181
+ /* ignore failed optional commands */
182
+ }
183
+ }
184
+ onDisconnect = () => {
185
+ this.device.removeEventListener('gattserverdisconnected', this.onDisconnect);
186
+ this.lastBatteryLevel = null;
187
+ if (this.batteryInterval) {
188
+ clearInterval(this.batteryInterval);
189
+ this.batteryInterval = null;
190
+ }
191
+ this.events$.next({ timestamp: now(), type: "DISCONNECT" });
192
+ this.events$.complete();
193
+ };
194
+ updateCapabilities() {
195
+ const hasV1 = this.v1 !== null;
196
+ this.capabilities.gyroscope = this.gyroChrct !== null;
197
+ this.capabilities.battery = hasV1;
198
+ this.capabilities.facelets = hasV1;
199
+ this.capabilities.hardware = hasV1;
200
+ this.capabilities.reset = hasV1;
201
+ }
202
+ async init() {
203
+ this.device.addEventListener('gattserverdisconnected', this.onDisconnect);
204
+ const gatt = await this.device.gatt.connect();
205
+ const service = await gatt.getPrimaryService(SERVICE_UUID);
206
+ const chrcts = await service.getCharacteristics();
207
+ this.writeChrct = findCharacteristic(chrcts, CHRCT_UUID_WRITE);
208
+ this.readChrct = findCharacteristic(chrcts, CHRCT_UUID_READ);
209
+ this.turnChrct = findCharacteristic(chrcts, CHRCT_UUID_TURN);
210
+ this.gyroChrct = findCharacteristic(chrcts, CHRCT_UUID_GYRO);
211
+ if (this.writeChrct) {
212
+ this.v1 = new MoyuV1Client(this.writeChrct);
213
+ }
214
+ if (this.readChrct) {
215
+ this.readChrct.addEventListener('characteristicvaluechanged', this.onReadEvent);
216
+ await this.readChrct.startNotifications();
217
+ }
218
+ if (this.turnChrct) {
219
+ this.turnChrct.addEventListener('characteristicvaluechanged', this.onTurnEvent);
220
+ await this.turnChrct.startNotifications();
221
+ }
222
+ if (this.gyroChrct) {
223
+ this.gyroChrct.addEventListener('characteristicvaluechanged', this.onGyroEvent);
224
+ await this.gyroChrct.startNotifications();
225
+ }
226
+ this.updateCapabilities();
227
+ if (this.v1) {
228
+ await this.pollBattery();
229
+ this.batteryInterval = setInterval(() => {
230
+ void this.pollBattery();
231
+ }, 60_000);
232
+ try {
233
+ const st = await this.v1.getCubeState();
234
+ this.applyCubeStateFromDevice(st.stickers, st.angles);
235
+ const facelets = this.prevCubie.toFaceCube();
236
+ this.events$.next({
237
+ timestamp: now(),
238
+ type: 'FACELETS',
239
+ facelets,
240
+ });
241
+ }
242
+ catch {
243
+ this.events$.next({
244
+ timestamp: now(),
245
+ type: 'FACELETS',
246
+ facelets: SOLVED_FACELET,
247
+ });
248
+ }
249
+ }
250
+ else {
251
+ this.events$.next({
252
+ timestamp: now(),
253
+ type: 'FACELETS',
254
+ facelets: SOLVED_FACELET,
255
+ });
256
+ }
257
+ }
258
+ async sendCommand(command) {
259
+ if (!this.v1)
260
+ return;
261
+ const ts = now();
262
+ try {
263
+ if (command.type === 'REQUEST_FACELETS') {
264
+ const st = await this.v1.getCubeState();
265
+ this.applyCubeStateFromDevice(st.stickers, st.angles);
266
+ this.events$.next({
267
+ timestamp: ts,
268
+ type: 'FACELETS',
269
+ facelets: this.prevCubie.toFaceCube(),
270
+ });
271
+ }
272
+ else if (command.type === 'REQUEST_BATTERY') {
273
+ const b = await this.v1.getBatteryInfo();
274
+ this.emitBatteryLevel(b.value.percentage, ts, true);
275
+ }
276
+ else if (command.type === 'REQUEST_HARDWARE') {
277
+ const h = await this.v1.getHardwareInfo();
278
+ this.events$.next({
279
+ timestamp: ts,
280
+ type: 'HARDWARE',
281
+ softwareVersion: `${h.major}.${h.minor}.${h.patch}`,
282
+ hardwareVersion: `boot:${h.bootCount}`,
283
+ gyroSupported: this.capabilities.gyroscope,
284
+ });
285
+ }
286
+ else if (command.type === 'REQUEST_RESET') {
287
+ await this.v1.setCubeState(MOYU_V1_SOLVED_STICKERS, [0, 0, 0, 0, 0, 0]);
288
+ this.faceStatus = [0, 0, 0, 0, 0, 0];
289
+ this.curCubie = new CubieCube();
290
+ this.prevCubie = new CubieCube();
291
+ this.events$.next({
292
+ timestamp: ts,
293
+ type: 'FACELETS',
294
+ facelets: SOLVED_FACELET,
295
+ });
296
+ }
297
+ }
298
+ catch {
299
+ /* ignore failed optional commands */
300
+ }
301
+ }
302
+ async disconnect() {
303
+ if (this.readChrct) {
304
+ this.readChrct.removeEventListener('characteristicvaluechanged', this.onReadEvent);
305
+ await this.readChrct.stopNotifications().catch(() => { });
306
+ }
307
+ if (this.turnChrct) {
308
+ this.turnChrct.removeEventListener('characteristicvaluechanged', this.onTurnEvent);
309
+ await this.turnChrct.stopNotifications().catch(() => { });
310
+ }
311
+ if (this.gyroChrct) {
312
+ this.gyroChrct.removeEventListener('characteristicvaluechanged', this.onGyroEvent);
313
+ await this.gyroChrct.stopNotifications().catch(() => { });
314
+ }
315
+ this.lastBatteryLevel = null;
316
+ if (this.batteryInterval) {
317
+ clearInterval(this.batteryInterval);
318
+ this.batteryInterval = null;
319
+ }
320
+ this.readChrct = null;
321
+ this.turnChrct = null;
322
+ this.gyroChrct = null;
323
+ this.writeChrct = null;
324
+ this.v1 = null;
325
+ this.device.removeEventListener('gattserverdisconnected', this.onDisconnect);
326
+ this.events$.next({ timestamp: now(), type: "DISCONNECT" });
327
+ this.events$.complete();
328
+ if (this.device.gatt?.connected) {
329
+ this.device.gatt.disconnect();
330
+ }
331
+ }
332
+ }
333
+ const moyuMhcProtocol = {
334
+ nameFilters: [
335
+ { namePrefix: "MHC" }
336
+ ],
337
+ optionalServices: [SERVICE_UUID],
338
+ matchesDevice(device) {
339
+ const name = device.name || '';
340
+ return name.startsWith('MHC');
341
+ },
342
+ gattAffinity(serviceUuids, _device) {
343
+ return serviceUuids.has(normalizeUuid(SERVICE_UUID)) ? 110 : 0;
344
+ },
345
+ async connect(device, _macProvider, _context) {
346
+ const conn = new MoyuMhcConnection(device);
347
+ await conn.init();
348
+ return conn;
349
+ }
350
+ };
351
+ registerProtocol(moyuMhcProtocol);
352
+ export { moyuMhcProtocol };
@@ -0,0 +1,57 @@
1
+ /// <reference types="web-bluetooth" />
2
+ declare const MOYU_V1_SOLVED_STICKERS: number[][];
3
+ export interface MoyuV1CubeStatePayload {
4
+ readonly stickers: number[][];
5
+ readonly angles: number[];
6
+ }
7
+ export declare function moyuV1ParseCubeStatePayload(t: DataView): MoyuV1CubeStatePayload;
8
+ export declare function moyuV1EncodeCubeStatePayload(stickers: number[][], angles: number[]): Uint8Array;
9
+ /** Build 54-char URFDLB facelet string for CubieCube.fromFacelet. */
10
+ export declare function moyuStickersToFaceletString(stickers: number[][]): string;
11
+ export interface MoyuV1HardwareInfo {
12
+ readonly bootCount: number;
13
+ readonly major: number;
14
+ readonly minor: number;
15
+ readonly patch: number;
16
+ }
17
+ export interface MoyuV1BatteryInfo {
18
+ readonly charging: boolean;
19
+ readonly full: boolean;
20
+ readonly percentage: number;
21
+ readonly voltage: number;
22
+ }
23
+ export declare class MoyuV1Client {
24
+ private readonly writeCharacteristic;
25
+ private readonly idGen;
26
+ private readonly sendCountGen;
27
+ private incomplete;
28
+ private waiters;
29
+ constructor(writeCharacteristic: BluetoothRemoteGATTCharacteristic);
30
+ /** Call from 0x1002 notification handler. */
31
+ onReadNotification(dv: DataView): void;
32
+ private headerByte;
33
+ private sendRawRequest;
34
+ send(command: number, payload?: Uint8Array): Promise<{
35
+ sentAt: number;
36
+ receivedAt: number;
37
+ value: DataView;
38
+ }>;
39
+ getCubeState(): Promise<MoyuV1CubeStatePayload>;
40
+ setCubeState(stickers?: number[][], angles?: number[]): Promise<void>;
41
+ getBatteryInfo(): Promise<{
42
+ sentAt: number;
43
+ receivedAt: number;
44
+ value: MoyuV1BatteryInfo;
45
+ }>;
46
+ getHardwareInfo(): Promise<MoyuV1HardwareInfo>;
47
+ getTime(): Promise<{
48
+ sentAt: number;
49
+ receivedAt: number;
50
+ value: {
51
+ seconds: number;
52
+ counter: number;
53
+ };
54
+ }>;
55
+ setSleepState(state: number): Promise<void>;
56
+ }
57
+ export { MOYU_V1_SOLVED_STICKERS };
@@ -0,0 +1,243 @@
1
+ import { writeGattCharacteristicValue } from '../../gatt-characteristic-write.js';
2
+ /**
3
+ * MoYu BLE API v1: fragmented request/response on 0x1001 / 0x1002 and cube-state payload parsing.
4
+ */
5
+ const MOYU_V1_CMD_TIME = 1;
6
+ const MOYU_V1_CMD_HW = 2;
7
+ const MOYU_V1_CMD_BATTERY = 3;
8
+ const MOYU_V1_CMD_CUBE_STATE = 10;
9
+ const MOYU_V1_CMD_SLEEP = 6;
10
+ /** Sticker id 0–5 → center color letter (MoYu face order D,L,B,R,F,U). */
11
+ const STICKER_ID_TO_COLOR = 'DLBRFU';
12
+ /**
13
+ * Map MoYu face index × cell (0–8) to URFDLB linear facelet index (U 0–8, R 9–17, …)
14
+ */
15
+ const MOYU_CELL_TO_STD = [
16
+ [27, 28, 29, 30, 31, 32, 33, 34, 35],
17
+ [44, 43, 42, 41, 40, 39, 38, 37, 36],
18
+ [53, 52, 51, 50, 49, 48, 47, 46, 45],
19
+ [17, 16, 26, 14, 13, 12, 11, 10, 9],
20
+ [29, 25, 24, 23, 22, 21, 20, 19, 18],
21
+ [0, 1, 2, 3, 4, 5, 6, 7, 8],
22
+ ];
23
+ const MOYU_V1_SOLVED_STICKERS = [
24
+ [0, 0, 0, 0, 0, 0, 0, 0, 0],
25
+ [1, 1, 1, 1, 1, 1, 1, 1, 1],
26
+ [2, 2, 2, 2, 2, 2, 2, 2, 2],
27
+ [3, 3, 3, 3, 3, 3, 3, 3, 3],
28
+ [4, 4, 4, 4, 4, 4, 4, 4, 4],
29
+ [5, 5, 5, 5, 5, 5, 5, 5, 5],
30
+ ];
31
+ function parseIncomingPart(dv) {
32
+ const index = dv.getUint8(1) & 15;
33
+ const total = dv.getUint8(1) >> 4;
34
+ const payload = new Uint8Array(dv.buffer, dv.byteOffset + 2, Math.max(0, dv.byteLength - 2));
35
+ return { index, total, payload };
36
+ }
37
+ function mergeParts(parts) {
38
+ const sorted = [...parts].sort((a, b) => a.index - b.index);
39
+ const len = sorted.reduce((n, p) => n + p.payload.length, 0);
40
+ const out = new Uint8Array(len);
41
+ let o = 0;
42
+ for (const p of sorted) {
43
+ out.set(p.payload, o);
44
+ o += p.payload.length;
45
+ }
46
+ return new DataView(out.buffer);
47
+ }
48
+ function parseResponse(merged, timestamp) {
49
+ const header = merged.getUint8(0);
50
+ const command = header & 15;
51
+ const success = ((header >> 4) & 1) === 1;
52
+ const id = (header >> 5) & 7;
53
+ const payload = new DataView(merged.buffer, merged.byteOffset + 1, merged.byteLength - 1);
54
+ return { command, success, id, timestamp, payload };
55
+ }
56
+ function concatU8(a, b) {
57
+ const o = new Uint8Array(a.length + b.length);
58
+ o.set(a);
59
+ o.set(b, a.length);
60
+ return o;
61
+ }
62
+ class IdGenerator {
63
+ lastId = 0;
64
+ next() {
65
+ this.lastId = (this.lastId + 1) % 8;
66
+ return this.lastId;
67
+ }
68
+ }
69
+ class SendCountGenerator {
70
+ lastCount = -1;
71
+ next() {
72
+ this.lastCount = (this.lastCount + 1) % 256;
73
+ return this.lastCount;
74
+ }
75
+ }
76
+ export function moyuV1ParseCubeStatePayload(t) {
77
+ const stickers = [];
78
+ for (let s = 0; s < 6; s++) {
79
+ const r = [];
80
+ for (let a = 0; a < 9; a++) {
81
+ const o = t.getUint8(Math.floor((9 * s + a) / 2));
82
+ r.push((o >> ((9 * s + a) % 2 === 0 ? 0 : 4)) & 15);
83
+ }
84
+ stickers.push(r);
85
+ }
86
+ const angles = [];
87
+ for (let s = 0; s < 6; s++) {
88
+ const row = 27 + Math.floor(s / 2);
89
+ const a = t.getUint8(row);
90
+ angles.push((a >> (s % 2 === 0 ? 0 : 4)) & 15);
91
+ }
92
+ return { stickers, angles };
93
+ }
94
+ export function moyuV1EncodeCubeStatePayload(stickers, angles) {
95
+ const e = new Uint8Array(30);
96
+ const i = new DataView(e.buffer);
97
+ for (let s = 0; s < 6; s++) {
98
+ for (let r = 0; r < 9; r++) {
99
+ const lo = (9 * s + r) % 2 === 0;
100
+ const o = Math.floor((9 * s + r) / 2);
101
+ i.setUint8(o, i.getUint8(o) | ((15 & stickers[s][r]) << (lo ? 0 : 4)));
102
+ }
103
+ }
104
+ for (let s = 0; s < 6; s++) {
105
+ const lo = s % 2 === 0;
106
+ const row = 27 + Math.floor(s / 2);
107
+ i.setUint8(row, i.getUint8(row) | ((15 & angles[s]) << (lo ? 0 : 4)));
108
+ }
109
+ return e;
110
+ }
111
+ /** Build 54-char URFDLB facelet string for CubieCube.fromFacelet. */
112
+ export function moyuStickersToFaceletString(stickers) {
113
+ const chars = new Array(54).fill('?');
114
+ for (let face = 0; face < 6; face++) {
115
+ const row = MOYU_CELL_TO_STD[face];
116
+ const stickerRow = stickers[face];
117
+ for (let p = 0; p < 9; p++) {
118
+ const id = stickerRow[p] & 15;
119
+ const c = id < 6 ? STICKER_ID_TO_COLOR[id] : '?';
120
+ chars[row[p]] = c;
121
+ }
122
+ }
123
+ return chars.join('');
124
+ }
125
+ export class MoyuV1Client {
126
+ writeCharacteristic;
127
+ idGen = new IdGenerator();
128
+ sendCountGen = new SendCountGenerator();
129
+ incomplete = [];
130
+ waiters = [];
131
+ constructor(writeCharacteristic) {
132
+ this.writeCharacteristic = writeCharacteristic;
133
+ }
134
+ /** Call from 0x1002 notification handler. */
135
+ onReadNotification(dv) {
136
+ const part = parseIncomingPart(dv);
137
+ this.incomplete.push(part);
138
+ if (part.total <= 0 || part.index !== part.total - 1) {
139
+ return;
140
+ }
141
+ const merged = mergeParts(this.incomplete);
142
+ this.incomplete = [];
143
+ const receivedAt = Date.now();
144
+ const r = parseResponse(merged, receivedAt);
145
+ const idx = this.waiters.findIndex((w) => w.command === r.command && w.id === r.id);
146
+ if (idx < 0) {
147
+ return;
148
+ }
149
+ const w = this.waiters.splice(idx, 1)[0];
150
+ clearTimeout(w.timeout);
151
+ if (!r.success) {
152
+ w.reject(new Error(`MoYu v1 command ${r.command} failed`));
153
+ return;
154
+ }
155
+ w.resolve({ sentAt: w.sentAt, receivedAt, value: r.payload });
156
+ }
157
+ headerByte(command, hasPayload, id) {
158
+ return command | ((hasPayload ? 1 : 0) << 4) | (id << 5);
159
+ }
160
+ async sendRawRequest(body) {
161
+ const nParts = Math.ceil(body.length / 18);
162
+ if (nParts > 16)
163
+ throw new Error('Too many parts');
164
+ for (let i = 0; i < nParts; i++) {
165
+ const frame = new Uint8Array(20);
166
+ const v = new DataView(frame.buffer);
167
+ v.setUint8(0, this.sendCountGen.next());
168
+ v.setUint8(1, i | (nParts << 4));
169
+ const slice = body.subarray(18 * i, 18 * (i + 1));
170
+ frame.set(slice, 2);
171
+ await writeGattCharacteristicValue(this.writeCharacteristic, frame);
172
+ }
173
+ return Date.now();
174
+ }
175
+ async send(command, payload) {
176
+ const id = this.idGen.next();
177
+ const hasPayload = payload !== undefined;
178
+ const h = this.headerByte(command, hasPayload, id);
179
+ const first = new Uint8Array(1);
180
+ new DataView(first.buffer).setUint8(0, h);
181
+ const body = hasPayload && payload !== undefined ? concatU8(first, payload) : first;
182
+ let waiter;
183
+ const result = new Promise((resolve, reject) => {
184
+ const timeout = setTimeout(() => {
185
+ const i = this.waiters.findIndex((w) => w.command === command && w.id === id);
186
+ if (i >= 0)
187
+ this.waiters.splice(i, 1);
188
+ reject(new Error(`MoYu v1 command ${command} timeout`));
189
+ }, 5000);
190
+ waiter = { command, id, sentAt: 0, resolve, reject, timeout };
191
+ this.waiters.push(waiter);
192
+ });
193
+ waiter.sentAt = await this.sendRawRequest(body);
194
+ return result;
195
+ }
196
+ async getCubeState() {
197
+ const r = await this.send(MOYU_V1_CMD_CUBE_STATE);
198
+ return moyuV1ParseCubeStatePayload(r.value);
199
+ }
200
+ async setCubeState(stickers = MOYU_V1_SOLVED_STICKERS, angles = [0, 0, 0, 0, 0, 0]) {
201
+ const pl = moyuV1EncodeCubeStatePayload(stickers, angles);
202
+ await this.send(MOYU_V1_CMD_CUBE_STATE, pl);
203
+ }
204
+ async getBatteryInfo() {
205
+ const r = await this.send(MOYU_V1_CMD_BATTERY);
206
+ const t = r.value;
207
+ return {
208
+ sentAt: r.sentAt,
209
+ receivedAt: r.receivedAt,
210
+ value: {
211
+ charging: !!t.getUint8(0),
212
+ full: !!t.getUint8(1),
213
+ percentage: t.getUint16(2, true),
214
+ voltage: t.getInt32(4, true) / 1000,
215
+ },
216
+ };
217
+ }
218
+ async getHardwareInfo() {
219
+ const r = await this.send(MOYU_V1_CMD_HW);
220
+ const t = r.value;
221
+ return {
222
+ bootCount: t.getUint32(16, true),
223
+ major: t.getUint8(20),
224
+ minor: t.getUint8(21),
225
+ patch: t.getUint16(22, true),
226
+ };
227
+ }
228
+ async getTime() {
229
+ const r = await this.send(MOYU_V1_CMD_TIME);
230
+ const v = r.value;
231
+ return {
232
+ sentAt: r.sentAt,
233
+ receivedAt: r.receivedAt,
234
+ value: { seconds: v.getUint16(0, true), counter: v.getUint16(2, true) },
235
+ };
236
+ }
237
+ async setSleepState(state) {
238
+ const e = new Uint8Array(1);
239
+ new DataView(e.buffer).setUint8(0, state);
240
+ await this.send(MOYU_V1_CMD_SLEEP, e);
241
+ }
242
+ }
243
+ export { MOYU_V1_SOLVED_STICKERS };
@@ -0,0 +1,3 @@
1
+ import { SmartCubeProtocol } from '../protocol.js';
2
+ declare const moyu32Protocol: SmartCubeProtocol;
3
+ export { moyu32Protocol };