@wodzik/cubecore 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +118 -0
- package/analyze/client.d.ts +18 -0
- package/analyze/client.js +42 -0
- package/analyze/index.d.ts +114 -0
- package/analyze/index.js +151 -0
- package/analyze/rotations.d.ts +3 -0
- package/analyze/rotations.js +15 -0
- package/analyze/roux.d.ts +67 -0
- package/analyze/roux.js +95 -0
- package/analyze/worker.d.ts +6 -0
- package/analyze/worker.js +31 -0
- package/analyze/zz.d.ts +47 -0
- package/analyze/zz.js +102 -0
- package/bld/index.d.ts +14 -0
- package/bld/index.js +14 -0
- package/bld/memo.d.ts +66 -0
- package/bld/memo.js +139 -0
- package/bld/scheme.d.ts +30 -0
- package/bld/scheme.js +35 -0
- package/bld/skin.d.ts +26 -0
- package/bld/skin.js +46 -0
- package/bld/tracker.d.ts +68 -0
- package/bld/tracker.js +145 -0
- package/bluetooth/clock.d.ts +22 -0
- package/bluetooth/clock.js +37 -0
- package/bluetooth/cubeSkins.d.ts +31 -0
- package/bluetooth/cubeSkins.js +32 -0
- package/bluetooth/gyro.d.ts +31 -0
- package/bluetooth/gyro.js +46 -0
- package/bluetooth/index.d.ts +5 -0
- package/bluetooth/index.js +5 -0
- package/bluetooth/session.d.ts +140 -0
- package/bluetooth/session.js +201 -0
- package/bluetooth/simulated.d.ts +52 -0
- package/bluetooth/simulated.js +80 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
- package/cfop/caseData.d.ts +16 -0
- package/cfop/caseData.js +88 -0
- package/cfop/cases.d.ts +42 -0
- package/cfop/cases.js +116 -0
- package/cfop/f2l.d.ts +28 -0
- package/cfop/f2l.js +88 -0
- package/cfop/f2lData.d.ts +15 -0
- package/cfop/f2lData.js +49 -0
- package/cfop/index.d.ts +41 -0
- package/cfop/index.js +110 -0
- package/cfop/slots.d.ts +2 -0
- package/cfop/slots.js +1 -0
- package/core/arrows.d.ts +24 -0
- package/core/arrows.js +22 -0
- package/core/bytes.d.ts +27 -0
- package/core/bytes.js +145 -0
- package/core/centers.d.ts +32 -0
- package/core/centers.js +83 -0
- package/core/checks.d.ts +53 -0
- package/core/checks.js +85 -0
- package/core/cubies.d.ts +45 -0
- package/core/cubies.js +169 -0
- package/core/frames.d.ts +61 -0
- package/core/frames.js +126 -0
- package/core/geometry.d.ts +58 -0
- package/core/geometry.js +132 -0
- package/core/index.d.ts +19 -0
- package/core/index.js +19 -0
- package/core/masks.d.ts +32 -0
- package/core/masks.js +39 -0
- package/core/method.d.ts +35 -0
- package/core/method.js +30 -0
- package/core/moves.d.ts +48 -0
- package/core/moves.js +88 -0
- package/core/notation.d.ts +48 -0
- package/core/notation.js +226 -0
- package/core/physical.d.ts +53 -0
- package/core/physical.js +99 -0
- package/core/practice.d.ts +62 -0
- package/core/practice.js +87 -0
- package/core/sequence.d.ts +90 -0
- package/core/sequence.js +157 -0
- package/core/stages.d.ts +90 -0
- package/core/stages.js +21 -0
- package/core/state.d.ts +55 -0
- package/core/state.js +140 -0
- package/core/stateCodec.d.ts +12 -0
- package/core/stateCodec.js +51 -0
- package/core/stream.d.ts +45 -0
- package/core/stream.js +75 -0
- package/core/tracker.d.ts +62 -0
- package/core/tracker.js +121 -0
- package/element/alg.d.ts +45 -0
- package/element/alg.js +150 -0
- package/element/base.d.ts +2 -0
- package/element/base.js +3 -0
- package/element/bld.d.ts +82 -0
- package/element/bld.js +222 -0
- package/element/index.d.ts +8 -0
- package/element/index.js +21 -0
- package/element/model.d.ts +36 -0
- package/element/model.js +64 -0
- package/element/player.d.ts +196 -0
- package/element/player.js +680 -0
- package/element/practice.d.ts +75 -0
- package/element/practice.js +150 -0
- package/element/scramble.d.ts +50 -0
- package/element/scramble.js +107 -0
- package/element/sequence.d.ts +126 -0
- package/element/sequence.js +254 -0
- package/element/styles.d.ts +32 -0
- package/element/styles.js +196 -0
- package/image/cache.d.ts +21 -0
- package/image/cache.js +66 -0
- package/image/index.d.ts +2 -0
- package/image/index.js +2 -0
- package/image/png-node.d.ts +6 -0
- package/image/png-node.js +10 -0
- package/image/svg.d.ts +36 -0
- package/image/svg.js +215 -0
- package/lbl/index.d.ts +10 -0
- package/lbl/index.js +31 -0
- package/methods/index.d.ts +20 -0
- package/methods/index.js +33 -0
- package/package.json +133 -0
- package/petrus/index.d.ts +11 -0
- package/petrus/index.js +27 -0
- package/react/index.d.ts +176 -0
- package/react/index.js +228 -0
- package/render/arrows.d.ts +68 -0
- package/render/arrows.js +190 -0
- package/render/build/attachments.d.ts +35 -0
- package/render/build/attachments.js +94 -0
- package/render/build/models.d.ts +9 -0
- package/render/build/models.js +27 -0
- package/render/build/tiles.d.ts +48 -0
- package/render/build/tiles.js +194 -0
- package/render/gltf.d.ts +10 -0
- package/render/gltf.js +106 -0
- package/render/index.d.ts +12 -0
- package/render/index.js +12 -0
- package/render/layers.d.ts +21 -0
- package/render/layers.js +24 -0
- package/render/pictures.d.ts +46 -0
- package/render/pictures.js +80 -0
- package/render/pieceModels.d.ts +38 -0
- package/render/pieceModels.js +43 -0
- package/render/renderer.d.ts +165 -0
- package/render/renderer.js +647 -0
- package/render/tile.d.ts +114 -0
- package/render/tile.js +390 -0
- package/render/viewports.d.ts +25 -0
- package/render/viewports.js +26 -0
- package/roux/cmll.d.ts +35 -0
- package/roux/cmll.js +121 -0
- package/roux/cmllData.d.ts +12 -0
- package/roux/cmllData.js +49 -0
- package/roux/index.d.ts +52 -0
- package/roux/index.js +175 -0
- package/skin/attachments.d.ts +82 -0
- package/skin/attachments.js +110 -0
- package/skin/index.d.ts +3 -0
- package/skin/index.js +3 -0
- package/skin/shapes.d.ts +112 -0
- package/skin/shapes.js +147 -0
- package/skin/skin.d.ts +911 -0
- package/skin/skin.js +412 -0
- package/solve/client.d.ts +58 -0
- package/solve/client.js +56 -0
- package/solve/handle.d.ts +6 -0
- package/solve/handle.js +38 -0
- package/solve/index.d.ts +8 -0
- package/solve/index.js +7 -0
- package/solve/lse.d.ts +42 -0
- package/solve/lse.js +151 -0
- package/solve/pieces.d.ts +18 -0
- package/solve/pieces.js +24 -0
- package/solve/protocol.d.ts +70 -0
- package/solve/protocol.js +16 -0
- package/solve/scramble.d.ts +76 -0
- package/solve/scramble.js +187 -0
- package/solve/stage.d.ts +108 -0
- package/solve/stage.js +532 -0
- package/solve/twophase.d.ts +36 -0
- package/solve/twophase.js +229 -0
- package/solve/worker.d.ts +6 -0
- package/solve/worker.js +15 -0
- package/timeline/codec.d.ts +23 -0
- package/timeline/codec.js +84 -0
- package/timeline/index.d.ts +7 -0
- package/timeline/index.js +7 -0
- package/timeline/recording.d.ts +19 -0
- package/timeline/recording.js +20 -0
- package/timeline/replay.d.ts +68 -0
- package/timeline/replay.js +145 -0
- package/timeline/segments.d.ts +31 -0
- package/timeline/segments.js +39 -0
- package/timeline/share.d.ts +30 -0
- package/timeline/share.js +62 -0
- package/timeline/stats.d.ts +34 -0
- package/timeline/stats.js +56 -0
- package/timeline/timings.d.ts +30 -0
- package/timeline/timings.js +54 -0
- package/zz/index.d.ts +29 -0
- package/zz/index.js +68 -0
- package/zz/seeds.d.ts +9 -0
- package/zz/seeds.js +29 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as def from './gan-cube-definitions.js';
|
|
2
|
+
import { GanGen2CubeEncrypter, GanGen3CubeEncrypter, GanGen4CubeEncrypter } from './gan-cube-encrypter.js';
|
|
3
|
+
import { GanGen1CubeConnection } from './gan-gen1.js';
|
|
4
|
+
import { macStringToSaltOrThrow } from './gan-mac-salt.js';
|
|
5
|
+
import { isValidGanGen2Packet, isValidGanGen3Packet, isValidGanGen4Packet, } from './gan-gen234-packet-validate.js';
|
|
6
|
+
import { normalizeUuid } from './smartcube/attachment/normalize-uuid.js';
|
|
7
|
+
import { GanCubeClassicConnection, GanGen2ProtocolDriver, GanGen3ProtocolDriver, GanGen4ProtocolDriver } from './gan-cube-protocol.js';
|
|
8
|
+
/** Iterate over all known GAN cube CICs to find Manufacturer Specific Data */
|
|
9
|
+
function getManufacturerDataBytes(manufacturerData) {
|
|
10
|
+
// Workaround for Bluefy browser which may return raw DataView directly instead of Map
|
|
11
|
+
if (manufacturerData instanceof DataView) {
|
|
12
|
+
return new DataView(manufacturerData.buffer.slice(2, 11));
|
|
13
|
+
}
|
|
14
|
+
for (var id of def.GAN_CIC_LIST) {
|
|
15
|
+
if (manufacturerData.has(id)) {
|
|
16
|
+
return new DataView(manufacturerData.get(id).buffer.slice(0, 9));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
/** Extract MAC from last 6 bytes of Manufacturer Specific Data */
|
|
22
|
+
function extractMAC(manufacturerData) {
|
|
23
|
+
var mac = [];
|
|
24
|
+
var dataView = getManufacturerDataBytes(manufacturerData);
|
|
25
|
+
if (dataView && dataView.byteLength >= 6) {
|
|
26
|
+
for (let i = 1; i <= 6; i++) {
|
|
27
|
+
mac.push(dataView.getUint8(dataView.byteLength - i).toString(16).toUpperCase().padStart(2, "0"));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return mac.join(":");
|
|
31
|
+
}
|
|
32
|
+
/** If browser supports Web Bluetooth watchAdvertisements() API, try to retrieve MAC address automatically */
|
|
33
|
+
async function autoRetrieveMacAddress(device) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
if (typeof device.watchAdvertisements != 'function') {
|
|
36
|
+
resolve(null);
|
|
37
|
+
}
|
|
38
|
+
var abortController = new AbortController();
|
|
39
|
+
var onAdvEvent = (evt) => {
|
|
40
|
+
device.removeEventListener("advertisementreceived", onAdvEvent);
|
|
41
|
+
abortController.abort();
|
|
42
|
+
var mac = extractMAC(evt.manufacturerData);
|
|
43
|
+
resolve(mac || null);
|
|
44
|
+
};
|
|
45
|
+
var onAbort = () => {
|
|
46
|
+
device.removeEventListener("advertisementreceived", onAdvEvent);
|
|
47
|
+
abortController.abort();
|
|
48
|
+
resolve(null);
|
|
49
|
+
};
|
|
50
|
+
device.addEventListener("advertisementreceived", onAdvEvent);
|
|
51
|
+
device.watchAdvertisements({ signal: abortController.signal }).catch(onAbort);
|
|
52
|
+
setTimeout(onAbort, 5000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function hasGanGen1Profile(serviceUuids) {
|
|
56
|
+
const primary = normalizeUuid(def.GAN_GEN1_PRIMARY_SERVICE);
|
|
57
|
+
const deviceInfo = normalizeUuid(def.GAN_GEN1_DEVICE_INFO_SERVICE);
|
|
58
|
+
return serviceUuids.has(primary) && serviceUuids.has(deviceInfo);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Initiate new connection with the GAN Smart Cube device
|
|
62
|
+
* @param customMacAddressProvider Optional custom provider for cube MAC address
|
|
63
|
+
* @returns Object representing connection API and state
|
|
64
|
+
*/
|
|
65
|
+
async function connectGanCube(customMacAddressProvider) {
|
|
66
|
+
// Request user for the bluetooth device (popup selection dialog)
|
|
67
|
+
const nameFilters = [
|
|
68
|
+
{ namePrefix: "GAN" },
|
|
69
|
+
{ namePrefix: "MG" },
|
|
70
|
+
{ namePrefix: "AiCube" },
|
|
71
|
+
];
|
|
72
|
+
const cicFilters = def.GAN_CIC_LIST.map((companyIdentifier) => ({
|
|
73
|
+
manufacturerData: [{ companyIdentifier }],
|
|
74
|
+
}));
|
|
75
|
+
var device = await navigator.bluetooth.requestDevice({
|
|
76
|
+
filters: [...nameFilters, ...cicFilters],
|
|
77
|
+
optionalServices: [
|
|
78
|
+
def.GAN_GEN1_PRIMARY_SERVICE,
|
|
79
|
+
def.GAN_GEN1_DEVICE_INFO_SERVICE,
|
|
80
|
+
def.GAN_GEN2_SERVICE,
|
|
81
|
+
def.GAN_GEN3_SERVICE,
|
|
82
|
+
def.GAN_GEN4_SERVICE,
|
|
83
|
+
"0000180a-0000-1000-8000-00805f9b34fb",
|
|
84
|
+
"00001800-0000-1000-8000-00805f9b34fb",
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
// Connect to GATT and get primary services
|
|
88
|
+
var gatt = await device.gatt.connect();
|
|
89
|
+
var services = await gatt.getPrimaryServices();
|
|
90
|
+
const serviceUuidSet = new Set(services.map((s) => normalizeUuid(s.uuid)));
|
|
91
|
+
if (hasGanGen1Profile(serviceUuidSet)) {
|
|
92
|
+
return GanGen1CubeConnection.create(device);
|
|
93
|
+
}
|
|
94
|
+
// Retrieve cube MAC address needed for key salting
|
|
95
|
+
var mac = customMacAddressProvider && await customMacAddressProvider(device, false)
|
|
96
|
+
|| await autoRetrieveMacAddress(device)
|
|
97
|
+
|| customMacAddressProvider && await customMacAddressProvider(device, true);
|
|
98
|
+
if (!mac)
|
|
99
|
+
throw new Error('Unable to determine cube MAC address, connection is not possible!');
|
|
100
|
+
device.mac = mac;
|
|
101
|
+
// Create encryption salt from MAC address bytes placed in reverse order
|
|
102
|
+
var salt = macStringToSaltOrThrow(mac);
|
|
103
|
+
const g2 = normalizeUuid(def.GAN_GEN2_SERVICE);
|
|
104
|
+
const g3 = normalizeUuid(def.GAN_GEN3_SERVICE);
|
|
105
|
+
const g4 = normalizeUuid(def.GAN_GEN4_SERVICE);
|
|
106
|
+
let pick = null;
|
|
107
|
+
if (serviceUuidSet.has(g2))
|
|
108
|
+
pick = 'g2';
|
|
109
|
+
else if (serviceUuidSet.has(g3))
|
|
110
|
+
pick = 'g3';
|
|
111
|
+
else if (serviceUuidSet.has(g4))
|
|
112
|
+
pick = 'g4';
|
|
113
|
+
var conn = null;
|
|
114
|
+
// Resolve type of connected cube device and setup appropriate encryption / protocol driver
|
|
115
|
+
if (pick === 'g2') {
|
|
116
|
+
const service = await gatt.getPrimaryService(def.GAN_GEN2_SERVICE);
|
|
117
|
+
let commandCharacteristic = await service.getCharacteristic(def.GAN_GEN2_COMMAND_CHARACTERISTIC);
|
|
118
|
+
let stateCharacteristic = await service.getCharacteristic(def.GAN_GEN2_STATE_CHARACTERISTIC);
|
|
119
|
+
let key = device.name?.startsWith('AiCube') ? def.GAN_ENCRYPTION_KEYS[1] : def.GAN_ENCRYPTION_KEYS[0];
|
|
120
|
+
let encrypter = new GanGen2CubeEncrypter(new Uint8Array(key.key), new Uint8Array(key.iv), salt);
|
|
121
|
+
let driver = new GanGen2ProtocolDriver();
|
|
122
|
+
conn = await GanCubeClassicConnection.create(device, commandCharacteristic, stateCharacteristic, encrypter, driver, {
|
|
123
|
+
validateDecrypted: isValidGanGen2Packet,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
else if (pick === 'g3') {
|
|
127
|
+
const service = await gatt.getPrimaryService(def.GAN_GEN3_SERVICE);
|
|
128
|
+
let commandCharacteristic = await service.getCharacteristic(def.GAN_GEN3_COMMAND_CHARACTERISTIC);
|
|
129
|
+
let stateCharacteristic = await service.getCharacteristic(def.GAN_GEN3_STATE_CHARACTERISTIC);
|
|
130
|
+
let key = def.GAN_ENCRYPTION_KEYS[0];
|
|
131
|
+
let encrypter = new GanGen3CubeEncrypter(new Uint8Array(key.key), new Uint8Array(key.iv), salt);
|
|
132
|
+
let driver = new GanGen3ProtocolDriver();
|
|
133
|
+
conn = await GanCubeClassicConnection.create(device, commandCharacteristic, stateCharacteristic, encrypter, driver, {
|
|
134
|
+
validateDecrypted: isValidGanGen3Packet,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else if (pick === 'g4') {
|
|
138
|
+
const service = await gatt.getPrimaryService(def.GAN_GEN4_SERVICE);
|
|
139
|
+
let commandCharacteristic = await service.getCharacteristic(def.GAN_GEN4_COMMAND_CHARACTERISTIC);
|
|
140
|
+
let stateCharacteristic = await service.getCharacteristic(def.GAN_GEN4_STATE_CHARACTERISTIC);
|
|
141
|
+
let key = def.GAN_ENCRYPTION_KEYS[0];
|
|
142
|
+
let encrypter = new GanGen4CubeEncrypter(new Uint8Array(key.key), new Uint8Array(key.iv), salt);
|
|
143
|
+
let driver = new GanGen4ProtocolDriver();
|
|
144
|
+
conn = await GanCubeClassicConnection.create(device, commandCharacteristic, stateCharacteristic, encrypter, driver, {
|
|
145
|
+
validateDecrypted: isValidGanGen4Packet,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!conn)
|
|
149
|
+
throw new Error("Can't find target BLE services - wrong or unsupported cube device model");
|
|
150
|
+
return conn;
|
|
151
|
+
}
|
|
152
|
+
export { connectGanCube };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Observable } from 'rxjs';
|
|
2
|
+
/**
|
|
3
|
+
* GAN Smart Timer events/states
|
|
4
|
+
*/
|
|
5
|
+
declare enum GanTimerState {
|
|
6
|
+
/** Fired when timer is disconnected from bluetooth */
|
|
7
|
+
DISCONNECT = 0,
|
|
8
|
+
/** Grace delay is expired and timer is ready to start */
|
|
9
|
+
GET_SET = 1,
|
|
10
|
+
/** Hands removed from the timer before grace delay expired */
|
|
11
|
+
HANDS_OFF = 2,
|
|
12
|
+
/** Timer is running */
|
|
13
|
+
RUNNING = 3,
|
|
14
|
+
/** Timer is stopped, this event includes recorded time */
|
|
15
|
+
STOPPED = 4,
|
|
16
|
+
/** Timer is reset and idle */
|
|
17
|
+
IDLE = 5,
|
|
18
|
+
/** Hands are placed on the timer */
|
|
19
|
+
HANDS_ON = 6,
|
|
20
|
+
/** Timer moves to this state immediately after STOPPED */
|
|
21
|
+
FINISHED = 7
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Representation of time value
|
|
25
|
+
*/
|
|
26
|
+
interface GanTimerTime {
|
|
27
|
+
readonly minutes: number;
|
|
28
|
+
readonly seconds: number;
|
|
29
|
+
readonly milliseconds: number;
|
|
30
|
+
readonly asTimestamp: number;
|
|
31
|
+
toString(): string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Timer state event
|
|
35
|
+
*/
|
|
36
|
+
interface GanTimerEvent {
|
|
37
|
+
/** Current timer state */
|
|
38
|
+
state: GanTimerState;
|
|
39
|
+
/** Recorder time value in case of STOPPED event */
|
|
40
|
+
recordedTime?: GanTimerTime;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Representation of recorded in timer memory time values
|
|
44
|
+
*/
|
|
45
|
+
interface GanTimerRecordedTimes {
|
|
46
|
+
displayTime: GanTimerTime;
|
|
47
|
+
previousTimes: [GanTimerTime, GanTimerTime, GanTimerTime];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* GAN Timer connection object representing connection API and state
|
|
51
|
+
*/
|
|
52
|
+
interface GanTimerConnection {
|
|
53
|
+
/** RxJS Subject to subscribe for cube event messages */
|
|
54
|
+
events$: Observable<GanTimerEvent>;
|
|
55
|
+
/** Retrieve last time values recored by timer */
|
|
56
|
+
getRecordedTimes(): Promise<GanTimerRecordedTimes>;
|
|
57
|
+
/** Disconnect from timer */
|
|
58
|
+
disconnect(): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Construct time object
|
|
62
|
+
*/
|
|
63
|
+
declare function makeTime(min: number, sec: number, msec: number): GanTimerTime;
|
|
64
|
+
/**
|
|
65
|
+
* Construct time object from milliseconds timestamp
|
|
66
|
+
*/
|
|
67
|
+
declare function makeTimeFromTimestamp(timestamp: number): GanTimerTime;
|
|
68
|
+
/**
|
|
69
|
+
* Initiate new connection with the GAN Smart Timer device
|
|
70
|
+
* @returns Connection connection object representing connection API and state
|
|
71
|
+
*/
|
|
72
|
+
declare function connectGanTimer(): Promise<GanTimerConnection>;
|
|
73
|
+
export type { GanTimerConnection, GanTimerEvent, GanTimerTime, GanTimerRecordedTimes };
|
|
74
|
+
export { connectGanTimer, makeTime, makeTimeFromTimestamp, GanTimerState };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Subject } from 'rxjs';
|
|
2
|
+
// GAN Smart Timer bluetooth service and characteristic UUIDs
|
|
3
|
+
const GAN_TIMER_SERVICE = '0000fff0-0000-1000-8000-00805f9b34fb';
|
|
4
|
+
const GAN_TIMER_TIME_CHARACTERISTIC = '0000fff2-0000-1000-8000-00805f9b34fb';
|
|
5
|
+
const GAN_TIMER_STATE_CHARACTERISTIC = '0000fff5-0000-1000-8000-00805f9b34fb';
|
|
6
|
+
/**
|
|
7
|
+
* GAN Smart Timer events/states
|
|
8
|
+
*/
|
|
9
|
+
var GanTimerState;
|
|
10
|
+
(function (GanTimerState) {
|
|
11
|
+
/** Fired when timer is disconnected from bluetooth */
|
|
12
|
+
GanTimerState[GanTimerState["DISCONNECT"] = 0] = "DISCONNECT";
|
|
13
|
+
/** Grace delay is expired and timer is ready to start */
|
|
14
|
+
GanTimerState[GanTimerState["GET_SET"] = 1] = "GET_SET";
|
|
15
|
+
/** Hands removed from the timer before grace delay expired */
|
|
16
|
+
GanTimerState[GanTimerState["HANDS_OFF"] = 2] = "HANDS_OFF";
|
|
17
|
+
/** Timer is running */
|
|
18
|
+
GanTimerState[GanTimerState["RUNNING"] = 3] = "RUNNING";
|
|
19
|
+
/** Timer is stopped, this event includes recorded time */
|
|
20
|
+
GanTimerState[GanTimerState["STOPPED"] = 4] = "STOPPED";
|
|
21
|
+
/** Timer is reset and idle */
|
|
22
|
+
GanTimerState[GanTimerState["IDLE"] = 5] = "IDLE";
|
|
23
|
+
/** Hands are placed on the timer */
|
|
24
|
+
GanTimerState[GanTimerState["HANDS_ON"] = 6] = "HANDS_ON";
|
|
25
|
+
/** Timer moves to this state immediately after STOPPED */
|
|
26
|
+
GanTimerState[GanTimerState["FINISHED"] = 7] = "FINISHED";
|
|
27
|
+
})(GanTimerState || (GanTimerState = {}));
|
|
28
|
+
/**
|
|
29
|
+
* Construct time object
|
|
30
|
+
*/
|
|
31
|
+
function makeTime(min, sec, msec) {
|
|
32
|
+
return {
|
|
33
|
+
minutes: min,
|
|
34
|
+
seconds: sec,
|
|
35
|
+
milliseconds: msec,
|
|
36
|
+
asTimestamp: 60000 * min + 1000 * sec + msec,
|
|
37
|
+
toString: () => `${min.toString(10)}:${sec.toString(10).padStart(2, '0')}.${msec.toString(10).padStart(3, '0')}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Construct time object from raw event data
|
|
42
|
+
*/
|
|
43
|
+
function makeTimeFromRaw(data, offset) {
|
|
44
|
+
var min = data.getUint8(offset);
|
|
45
|
+
var sec = data.getUint8(offset + 1);
|
|
46
|
+
var msec = data.getUint16(offset + 2, true);
|
|
47
|
+
return makeTime(min, sec, msec);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Construct time object from milliseconds timestamp
|
|
51
|
+
*/
|
|
52
|
+
function makeTimeFromTimestamp(timestamp) {
|
|
53
|
+
var min = Math.trunc(timestamp / 60000);
|
|
54
|
+
var sec = Math.trunc(timestamp % 60000 / 1000);
|
|
55
|
+
var msec = Math.trunc(timestamp % 1000);
|
|
56
|
+
return makeTime(min, sec, msec);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Calculate ArrayBuffer checksum using CRC-16/CCIT-FALSE algorithm variation
|
|
60
|
+
*/
|
|
61
|
+
function crc16ccit(buff) {
|
|
62
|
+
var dataView = new DataView(buff);
|
|
63
|
+
var crc = 0xFFFF;
|
|
64
|
+
for (let i = 0; i < dataView.byteLength; ++i) {
|
|
65
|
+
crc ^= dataView.getUint8(i) << 8;
|
|
66
|
+
for (let j = 0; j < 8; ++j) {
|
|
67
|
+
crc = (crc & 0x8000) > 0 ? (crc << 1) ^ 0x1021 : crc << 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return crc & 0xFFFF;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ensure received timer event has valid data: check data magic and CRC
|
|
74
|
+
*/
|
|
75
|
+
function validateEventData(data) {
|
|
76
|
+
try {
|
|
77
|
+
if (data?.byteLength == 0 || data.getUint8(0) != 0xFE) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
var eventCRC = data.getUint16(data.byteLength - 2, true);
|
|
81
|
+
var calculatedCRC = crc16ccit(data.buffer.slice(2, data.byteLength - 2));
|
|
82
|
+
return eventCRC == calculatedCRC;
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Construct event object from raw data
|
|
90
|
+
*/
|
|
91
|
+
function buildTimerEvent(data) {
|
|
92
|
+
var evt = {
|
|
93
|
+
state: data.getUint8(3)
|
|
94
|
+
};
|
|
95
|
+
if (evt.state == GanTimerState.STOPPED) {
|
|
96
|
+
evt.recordedTime = makeTimeFromRaw(data, 4);
|
|
97
|
+
}
|
|
98
|
+
return evt;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Initiate new connection with the GAN Smart Timer device
|
|
102
|
+
* @returns Connection connection object representing connection API and state
|
|
103
|
+
*/
|
|
104
|
+
async function connectGanTimer() {
|
|
105
|
+
// Request user for the bluetooth device (popup selection dialog)
|
|
106
|
+
var device = await navigator.bluetooth.requestDevice({
|
|
107
|
+
filters: [
|
|
108
|
+
{ namePrefix: "GAN" },
|
|
109
|
+
{ namePrefix: "gan" },
|
|
110
|
+
{ namePrefix: "Gan" }
|
|
111
|
+
],
|
|
112
|
+
optionalServices: [GAN_TIMER_SERVICE]
|
|
113
|
+
});
|
|
114
|
+
// Connect to GATT server
|
|
115
|
+
var server = await device.gatt.connect();
|
|
116
|
+
// Connect to main timer service and characteristics
|
|
117
|
+
var service = await server.getPrimaryService(GAN_TIMER_SERVICE);
|
|
118
|
+
var timeCharacteristic = await service.getCharacteristic(GAN_TIMER_TIME_CHARACTERISTIC);
|
|
119
|
+
var stateCharacteristic = await service.getCharacteristic(GAN_TIMER_STATE_CHARACTERISTIC);
|
|
120
|
+
// Subscribe to value updates of the timer state characteristic
|
|
121
|
+
var eventSubject = new Subject();
|
|
122
|
+
var onStateChanged = async (evt) => {
|
|
123
|
+
var chr = evt.target;
|
|
124
|
+
var data = chr.value;
|
|
125
|
+
if (validateEventData(data)) {
|
|
126
|
+
eventSubject.next(buildTimerEvent(data));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
eventSubject.error("Invalid event data received from Timer");
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
stateCharacteristic.addEventListener('characteristicvaluechanged', onStateChanged);
|
|
133
|
+
stateCharacteristic.startNotifications();
|
|
134
|
+
// This action retrieves latest recorded times from timer
|
|
135
|
+
var getRecordedTimesAction = async () => {
|
|
136
|
+
var data = await timeCharacteristic.readValue();
|
|
137
|
+
return data?.byteLength >= 16 ?
|
|
138
|
+
Promise.resolve({
|
|
139
|
+
displayTime: makeTimeFromRaw(data, 0),
|
|
140
|
+
previousTimes: [makeTimeFromRaw(data, 4), makeTimeFromRaw(data, 8), makeTimeFromRaw(data, 12)]
|
|
141
|
+
}) : Promise.reject("Invalid time characteristic value received from Timer");
|
|
142
|
+
};
|
|
143
|
+
// Manual disconnect action
|
|
144
|
+
var disconnectAction = async () => {
|
|
145
|
+
device.removeEventListener('gattserverdisconnected', disconnectAction);
|
|
146
|
+
stateCharacteristic.removeEventListener('characteristicvaluechanged', onStateChanged);
|
|
147
|
+
await stateCharacteristic.stopNotifications().catch(() => { });
|
|
148
|
+
eventSubject.next({ state: GanTimerState.DISCONNECT });
|
|
149
|
+
eventSubject.complete();
|
|
150
|
+
if (server.connected) {
|
|
151
|
+
server.disconnect();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
device.addEventListener('gattserverdisconnected', disconnectAction);
|
|
155
|
+
return {
|
|
156
|
+
events$: eventSubject,
|
|
157
|
+
getRecordedTimes: getRecordedTimesAction,
|
|
158
|
+
disconnect: disconnectAction,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export { connectGanTimer, makeTime, makeTimeFromTimestamp, GanTimerState };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes GATT write operations (writeValueWithResponse / writeValueWithoutResponse) to avoid overlapping operations on some stacks.
|
|
3
|
+
*/
|
|
4
|
+
export class GattWriteQueue {
|
|
5
|
+
tail = Promise.resolve();
|
|
6
|
+
enqueue(fn) {
|
|
7
|
+
const run = this.tail.then(() => fn());
|
|
8
|
+
this.tail = run.then(() => { }, () => { });
|
|
9
|
+
return run;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { GattWriteQueue } from './gan-write-queue.js';
|
|
2
|
+
/**
|
|
3
|
+
* One in-flight GATT operation per connection on common stacks; overlapping writes throw
|
|
4
|
+
* NetworkError "GATT operation already in progress". Queue writes per device.
|
|
5
|
+
*/
|
|
6
|
+
const writeQueuesByDevice = new WeakMap();
|
|
7
|
+
function writeQueueForCharacteristic(characteristic) {
|
|
8
|
+
const device = characteristic.service.device;
|
|
9
|
+
let q = writeQueuesByDevice.get(device);
|
|
10
|
+
if (!q) {
|
|
11
|
+
q = new GattWriteQueue();
|
|
12
|
+
writeQueuesByDevice.set(device, q);
|
|
13
|
+
}
|
|
14
|
+
return q;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Writes using explicit Web Bluetooth APIs (writeValue is deprecated).
|
|
18
|
+
* Chooses write-with-response vs write-without-response from characteristic.properties.
|
|
19
|
+
* When both are supported, prefers write-with-response for reliable cross-platform command traffic.
|
|
20
|
+
*/
|
|
21
|
+
function writeGattCharacteristicValueNow(characteristic, value) {
|
|
22
|
+
const { write, writeWithoutResponse } = characteristic.properties;
|
|
23
|
+
if (!write && !writeWithoutResponse) {
|
|
24
|
+
return Promise.reject(new Error('Characteristic is not writable'));
|
|
25
|
+
}
|
|
26
|
+
if (writeWithoutResponse && !write) {
|
|
27
|
+
return characteristic.writeValueWithoutResponse(value);
|
|
28
|
+
}
|
|
29
|
+
if (write && !writeWithoutResponse) {
|
|
30
|
+
return characteristic.writeValueWithResponse(value);
|
|
31
|
+
}
|
|
32
|
+
return characteristic.writeValueWithResponse(value);
|
|
33
|
+
}
|
|
34
|
+
export function writeGattCharacteristicValue(characteristic, value) {
|
|
35
|
+
return writeQueueForCharacteristic(characteristic).enqueue(() => writeGattCharacteristicValueNow(characteristic, value));
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="web-bluetooth" />
|
|
2
|
+
export declare function getCachedMacForDevice(device: BluetoothDevice): string | null;
|
|
3
|
+
export declare function setCachedMacForDevice(device: BluetoothDevice, mac: string): void;
|
|
4
|
+
export declare function removeCachedMacForDevice(device: BluetoothDevice): void;
|
|
5
|
+
/**
|
|
6
|
+
* Wait for manufacturer data from advertisements (single shared listener).
|
|
7
|
+
* Merges all packets until timeout: the first BLE advertisement often has an empty
|
|
8
|
+
* manufacturerData map; MAC-bearing data appears on later frames.
|
|
9
|
+
*/
|
|
10
|
+
export declare function waitForManufacturerData(device: BluetoothDevice, timeoutMs?: number): Promise<BluetoothManufacturerData | null>;
|
|
11
|
+
/** GAN-style MAC from manufacturer data (last 6 bytes, reversed order in payload). */
|
|
12
|
+
export declare function macFromGanManufacturerData(mf: BluetoothManufacturerData | DataView): string | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as ganDef from '../../gan-cube-definitions.js';
|
|
2
|
+
const STORAGE_PREFIX = 'smartcube-ble-mac:';
|
|
3
|
+
export function getCachedMacForDevice(device) {
|
|
4
|
+
if (typeof localStorage === 'undefined') {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return localStorage.getItem(STORAGE_PREFIX + device.id);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function setCachedMacForDevice(device, mac) {
|
|
15
|
+
if (typeof localStorage === 'undefined') {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
localStorage.setItem(STORAGE_PREFIX + device.id, mac);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* ignore quota */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function removeCachedMacForDevice(device) {
|
|
26
|
+
if (typeof localStorage === 'undefined') {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
localStorage.removeItem(STORAGE_PREFIX + device.id);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* ignore */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function mergeManufacturerDataInto(acc, mf) {
|
|
37
|
+
if (!mf || typeof mf.keys !== 'function') {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const id of mf.keys()) {
|
|
41
|
+
const v = mf.get(id);
|
|
42
|
+
if (v) {
|
|
43
|
+
acc.set(id, new DataView(v.buffer.slice(v.byteOffset, v.byteOffset + v.byteLength)));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wait for manufacturer data from advertisements (single shared listener).
|
|
49
|
+
* Merges all packets until timeout: the first BLE advertisement often has an empty
|
|
50
|
+
* manufacturerData map; MAC-bearing data appears on later frames.
|
|
51
|
+
*/
|
|
52
|
+
export async function waitForManufacturerData(device, timeoutMs = 5000) {
|
|
53
|
+
if (typeof device.watchAdvertisements !== 'function') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const name = (device.name || '').trim();
|
|
57
|
+
/** MoYu32-style names rarely expose useful MF in the picker; keep connect snappy. */
|
|
58
|
+
const emptyFirstAdvExit = name.startsWith('WCU_');
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
const abortController = new AbortController();
|
|
61
|
+
const merged = new Map();
|
|
62
|
+
let sawAdvertisement = false;
|
|
63
|
+
let finished = false;
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
device.removeEventListener('advertisementreceived', onAdvEvent);
|
|
66
|
+
abortController.abort();
|
|
67
|
+
clearTimeout(maxTimer);
|
|
68
|
+
};
|
|
69
|
+
const finish = (value) => {
|
|
70
|
+
if (finished) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
finished = true;
|
|
74
|
+
cleanup();
|
|
75
|
+
resolve(value);
|
|
76
|
+
};
|
|
77
|
+
const onAdvEvent = (evt) => {
|
|
78
|
+
const adv = evt;
|
|
79
|
+
mergeManufacturerDataInto(merged, adv.manufacturerData ?? null);
|
|
80
|
+
const isFirstAdv = !sawAdvertisement;
|
|
81
|
+
sawAdvertisement = true;
|
|
82
|
+
if (merged.size > 0) {
|
|
83
|
+
finish(merged);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (emptyFirstAdvExit && isFirstAdv) {
|
|
87
|
+
finish(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const maxTimer = setTimeout(() => {
|
|
91
|
+
finish(merged.size > 0 ? merged : null);
|
|
92
|
+
}, timeoutMs);
|
|
93
|
+
device.addEventListener('advertisementreceived', onAdvEvent);
|
|
94
|
+
device.watchAdvertisements({ signal: abortController.signal }).catch(() => {
|
|
95
|
+
clearTimeout(maxTimer);
|
|
96
|
+
finish(null);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/** GAN-style MAC from manufacturer data (last 6 bytes, reversed order in payload). */
|
|
101
|
+
export function macFromGanManufacturerData(mf) {
|
|
102
|
+
function getBytes(manufacturerData) {
|
|
103
|
+
if (manufacturerData instanceof DataView) {
|
|
104
|
+
return new DataView(manufacturerData.buffer.slice(2, 11));
|
|
105
|
+
}
|
|
106
|
+
for (const id of ganDef.GAN_CIC_LIST) {
|
|
107
|
+
if (manufacturerData.has(id)) {
|
|
108
|
+
return new DataView(manufacturerData.get(id).buffer.slice(0, 9));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const dataView = getBytes(mf);
|
|
114
|
+
if (!dataView || dataView.byteLength < 6) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const mac = [];
|
|
118
|
+
for (let i = 1; i <= 6; i++) {
|
|
119
|
+
mac.push(dataView.getUint8(dataView.byteLength - i).toString(16).toUpperCase().padStart(2, '0'));
|
|
120
|
+
}
|
|
121
|
+
return mac.join(':');
|
|
122
|
+
}
|
package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SmartCubeProtocol } from '../protocol.js';
|
|
2
|
+
import type { DeviceSelectionMode } from './types.js';
|
|
3
|
+
export interface BuildRequestDeviceOptionsExtras {
|
|
4
|
+
deviceName?: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Build Web Bluetooth `requestDevice` options for the given mode.
|
|
8
|
+
* Manufacturer IDs appear in `filters` (OR with name filters) and in `optionalManufacturerData`
|
|
9
|
+
* so `watchAdvertisements` exposes manufacturer data when the device matched by name prefix.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildRequestDeviceOptions(protocols: SmartCubeProtocol[], mode: DeviceSelectionMode, extras?: BuildRequestDeviceOptionsExtras): RequestDeviceOptions;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mergeOptionalServiceUuids } from './default-optional-services.js';
|
|
2
|
+
import { extraScanFiltersForKnownDeviceName } from './device-name-scan-filters.js';
|
|
3
|
+
function mergeManufacturerCompanyIds(protocols) {
|
|
4
|
+
const all = new Set();
|
|
5
|
+
for (const p of protocols) {
|
|
6
|
+
if (p.optionalManufacturerData) {
|
|
7
|
+
for (const cic of p.optionalManufacturerData) {
|
|
8
|
+
all.add(cic);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return Array.from(all).sort((a, b) => a - b);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build Web Bluetooth `requestDevice` options for the given mode.
|
|
16
|
+
* Manufacturer IDs appear in `filters` (OR with name filters) and in `optionalManufacturerData`
|
|
17
|
+
* so `watchAdvertisements` exposes manufacturer data when the device matched by name prefix.
|
|
18
|
+
*/
|
|
19
|
+
export function buildRequestDeviceOptions(protocols, mode, extras) {
|
|
20
|
+
const protocolServices = [];
|
|
21
|
+
for (const p of protocols) {
|
|
22
|
+
for (const s of p.optionalServices) {
|
|
23
|
+
protocolServices.push(s);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const optionalServices = mergeOptionalServiceUuids(protocolServices);
|
|
27
|
+
const cics = mergeManufacturerCompanyIds(protocols);
|
|
28
|
+
const optionalManufacturerData = cics.length > 0 ? cics : undefined;
|
|
29
|
+
if (mode === 'any') {
|
|
30
|
+
return {
|
|
31
|
+
acceptAllDevices: true,
|
|
32
|
+
optionalServices,
|
|
33
|
+
optionalManufacturerData,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const filters = [];
|
|
37
|
+
for (const p of protocols) {
|
|
38
|
+
for (const f of p.nameFilters) {
|
|
39
|
+
filters.push(f);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const f of extraScanFiltersForKnownDeviceName(extras?.deviceName)) {
|
|
43
|
+
filters.push(f);
|
|
44
|
+
}
|
|
45
|
+
for (const companyIdentifier of cics) {
|
|
46
|
+
filters.push({
|
|
47
|
+
manufacturerData: [{ companyIdentifier }],
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
filters,
|
|
52
|
+
optionalServices,
|
|
53
|
+
optionalManufacturerData,
|
|
54
|
+
};
|
|
55
|
+
}
|