智能蓝牙魔方解析
很早之前,开发过一个魔方游戏,也一直想用智能蓝牙来连接,可惜教程什么的太少了,如何连接,如何解密与收取数据,一直是个难题,unity又没有全平台的蓝牙api,想了很久还是web的ble好用,参考的提取的主要是Cstimer简化实现,之前在blbl有问过原作者是如何解析的,他说那部分是老外写的,具体是如何实现的,他也不知,那时我用phpstudy边注释边console.log只知道有54个面,怎么用的不知,怎么写的不知,反正就是一痛苦面具。
一、蓝牙Web Bluetooth
用的是奇艺的智能魔方,它很便宜,所以用的是它,主要用的教程是:通过 JavaScript 与蓝牙设备通信,还有就是全面的Web_Bluetooth_API,这个可以让你很好的入门这个简单的,当网站使用 navigator.bluetooth.requestDevice 请求访问附近的设备时,浏览器会向用户显示设备选择器,用户可以在其中选择一台设备或取消请求,这个函数接受一个用于定义过滤条件的必需对象,这些过滤器用于仅返回与某些已通告的蓝牙 GATT 服务和/或设备名称匹配的设备,此外为了安全必需用户手动触发,下面就双击事件案例:
window.addEventListener("dblclick", async () =>{ // 双击事件
console.log("请求 BLE 设备");
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
//optionalServices: [SERVICE_UUID] // 这里加上你要访问的所有 service UUID
});
console.log('设备:', device.name);
});
二、魔方训练Cstimer
这是训练魔方的网站,它的源码在github上开源了,很实用可以点一下start,有时真的即使代码放你面前,可是你就是看不懂的崩感,就要是不了解它,看它我也花了好长时间才懂,主要用到的代码是cstimer/src/js/hardware/qiyucube.js,代码如下所示:
execMain(function() {
var _gatt;
var _service;
var _deviceName;
var _chrct_cube;
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
var SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
var CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
var QIYI_CIC_LIST = [0x0504];
var decoder = null;
var deviceMac = null;
var KEYS = ['NoDg7ANAjGkEwBYCc0xQnADAVgkzGAzHNAGyRTanQi5QIFyHrjQMQgsC6QA'];
function initMac(forcePrompt, isWrongKey) {
var defaultMac = null;
if (/^(QY-QYSC|XMD-TornadoV4-i)-.-[0-9A-F]{4}$/.exec(_deviceName)) {
defaultMac = 'CC:A3:00:00:' + _deviceName.slice(-4, -2) + ':' + _deviceName.slice(-2);
}
deviceMac = giikerutil.reqMacAddr(forcePrompt, isWrongKey, deviceMac, defaultMac);
}
function crc16modbus(data) {
var crc = 0xFFFF;
for (var i = 0; i < data.length; i++) {
crc ^= data[i];
for (var j = 0; j < 8; j++) {
crc = (crc & 0x1) > 0 ? (crc >> 1) ^ 0xa001 : crc >> 1;
}
}
return crc;
}
// content: [u8, u8, ..]
function sendMessage(content) {
if (!_chrct_cube || DEBUGBL) {
return DEBUGBL ? Promise.resolve() : Promise.reject();
}
var msg = [0xfe];
msg.push(4 + content.length); // length = 1 (op) + cont.length + 2 (crc)
for (var i = 0; i < content.length; i++) {
msg.push(content[i]);
}
var crc = crc16modbus(msg);
msg.push(crc & 0xff, crc >> 8);
var npad = (16 - msg.length % 16) % 16;
for (var i = 0; i < npad; i++) {
msg.push(0);
}
var encMsg = [];
decoder = decoder || $.aes128(JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[0])));
for (var i = 0; i < msg.length; i += 16) {
var block = msg.slice(i, i + 16);
decoder.encrypt(block);
for (var j = 0; j < 16; j++) {
encMsg[i + j] = block[j];
}
}
giikerutil.log('[qiyicube] send message to cube', msg, encMsg);
return _chrct_cube.writeValue(new Uint8Array(encMsg).buffer);
}
function sendHello(mac) {
if (!mac) {
return Promise.reject('empty mac');
}
var content = [0x00, 0x6b, 0x01, 0x00, 0x00, 0x22, 0x06, 0x00, 0x02, 0x08, 0x00];
for (var i = 5; i >= 0; i--) {
content.push(parseInt(mac.slice(i * 3, i * 3 + 2), 16));
}
return sendMessage(content);
}
function getManufacturerDataBytes(mfData) {
if (mfData instanceof DataView) { // this is workaround for Bluefy browser
return new DataView(mfData.buffer.slice(2));
}
for (var id of QIYI_CIC_LIST) {
if (mfData.has(id)) {
giikerutil.log('[qiyicube] found Manufacturer Data under CIC = 0x' + id.toString(16).padStart(4, '0'));
return mfData.get(id);
}
}
giikerutil.log('[qiyicube] Looks like this cube has new unknown CIC');
}
function init(device) {
clear();
_deviceName = device.name.trim();
giikerutil.log('[qiyicube] start init device');
return GiikerCube.waitForAdvs().then(function(mfData) {
var dataView = getManufacturerDataBytes(mfData);
if (dataView && dataView.byteLength >= 6) {
var mac = [];
for (var i = 5; i >= 0; i--) {
mac.push((dataView.getUint8(i) + 0x100).toString(16).slice(1));
}
return Promise.resolve(mac.join(':'));
}
return Promise.reject(-3);
}).then(function(mac) {
giikerutil.log('[qiyicube] init, found cube bluetooth hardware MAC = ' + mac);
deviceMac = mac;
}, function(err) {
giikerutil.log('[qiyicube] init, unable to automatically determine cube MAC, error code = ' + err);
}).then(function() {
return device.gatt.connect();
}).then(function(gatt) {
_gatt = gatt;
return gatt.getPrimaryService(SERVICE_UUID);
}).then(function(service) {
_service = service;
giikerutil.log('[qiyicube] got primary service', SERVICE_UUID);
return _service.getCharacteristics();
}).then(function(chrcts) {
giikerutil.log('[qiyicube] find chrcts', chrcts);
_chrct_cube = GiikerCube.findUUID(chrcts, CHRCT_UUID_CUBE);
}).then(function() {
_chrct_cube.addEventListener('characteristicvaluechanged', onCubeEvent);
return _chrct_cube.startNotifications();
}).then(function() {
initMac(true);
return sendHello(deviceMac);
});
}
function onCubeEvent(event) {
var value = event.target.value;
var encMsg = [];
for (var i = 0; i < value.byteLength; i++) {
encMsg[i] = value.getUint8(i);
}
giikerutil.log('[qiyicube] receive enc data', encMsg);
decoder = decoder || $.aes128(JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[0])));
var msg = [];
for (var i = 0; i < encMsg.length; i += 16) {
var block = encMsg.slice(i, i + 16);
decoder.decrypt(block);
for (var j = 0; j < 16; j++) {
msg[i + j] = block[j];
}
}
giikerutil.log('[qiyicube] decrypted msg', msg);
msg = msg.slice(0, msg[1]);
if (msg.length < 3 || crc16modbus(msg) != 0) {
giikerutil.log('[qiyicube] crc checked error');
return;
}
parseCubeData(msg);
}
var curCubie = new mathlib.CubieCube();
var prevCubie = new mathlib.CubieCube();
var prevMoves = [];
var lastTs = 0;
var batteryLevel = 0;
function parseCubeData(msg) {
var locTime = $.now();
if (msg[0] != 0xfe) {
giikerutil.log('[qiyicube] error cube data', msg);
return;
}
var opcode = msg[2];
var ts = (msg[3] << 24 | msg[4] << 16 | msg[5] << 8 | msg[6]);
if (opcode == 0x2) { // cube hello
batteryLevel = msg[35];
sendMessage(msg.slice(2, 7));
var newFacelet = parseFacelet(msg.slice(7, 34));
GiikerCube.callback(newFacelet, [], [Math.trunc(ts / 1.6), locTime], _deviceName);
prevCubie.fromFacelet(newFacelet);
if (newFacelet != kernel.getProp('giiSolved', mathlib.SOLVED_FACELET)) {
var rst = kernel.getProp('giiRST');
if (rst == 'a' || rst == 'p' && confirm(CONFIRM_GIIRST)) {
giikerutil.markSolved();
}
}
} else if (opcode == 0x3) { // state change
sendMessage(msg.slice(2, 7));
// check timestamps
var todoMoves = [[msg[34], ts]];
while (todoMoves.length < 10) {
var off = 91 - 5 * todoMoves.length;
var hisTs = (msg[off] << 24 | msg[off + 1] << 16 | msg[off + 2] << 8 | msg[off + 3]);
var hisMv = msg[off + 4];
if (hisTs <= lastTs) {
break;
}
todoMoves.push([hisMv, hisTs]);
}
if (todoMoves.length > 1) {
giikerutil.log('[qiyicube] miss history moves', JSON.stringify(todoMoves), lastTs);
}
var toCallback = [];
var curFacelet;
for (var i = todoMoves.length - 1; i >= 0; i--) {
var axis = [4, 1, 3, 0, 2, 5][(todoMoves[i][0] - 1) >> 1];
var power = [0, 2][todoMoves[i][0] & 1];
var m = axis * 3 + power;
mathlib.CubieCube.CubeMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie);
prevMoves.unshift("URFDLB".charAt(axis) + " 2'".charAt(power));
prevMoves = prevMoves.slice(0, 8);
curFacelet = curCubie.toFaceCube();
toCallback.push([curFacelet, prevMoves.slice(), [Math.trunc(todoMoves[i][1] / 1.6), locTime], _deviceName]);
var tmp = curCubie;
curCubie = prevCubie;
prevCubie = tmp;
}
var newFacelet = parseFacelet(msg.slice(7, 34));
if (newFacelet != curFacelet) {
giikerutil.log('[qiyicube] facelet', newFacelet);
curCubie.fromFacelet(newFacelet);
GiikerCube.callback(newFacelet, prevMoves, [Math.trunc(ts / 1.6), locTime], _deviceName);
var tmp = curCubie;
curCubie = prevCubie;
prevCubie = tmp;
} else {
for (var i = 0; i < toCallback.length; i++) {
GiikerCube.callback.apply(null, toCallback[i]);
}
}
var newBatteryLevel = msg[35];
if (newBatteryLevel != batteryLevel) {
batteryLevel = newBatteryLevel;
giikerutil.updateBattery([batteryLevel, _deviceName]);
}
}
lastTs = ts;
}
$.parseQYData = parseCubeData; // for debug
function parseFacelet(faceMsg) {
var ret = [];
for (var i = 0; i < 54; i++) {
ret.push("LRDUFB".charAt(faceMsg[i >> 1] >> (i % 2 << 2) & 0xf));
}
ret = ret.join("");
// giikerutil.log('[qiyicube]', 'parsedFacelet', ret);
return ret;
}
function clear() {
var result = Promise.resolve();
if (_chrct_cube) {
_chrct_cube.removeEventListener('characteristicvaluechanged', onCubeEvent);
result = _chrct_cube.stopNotifications().catch($.noop);
_chrct_cube = null;
}
_service = null;
_gatt = null;
_deviceName = null;
deviceMac = null;
curCubie = new mathlib.CubieCube();
prevCubie = new mathlib.CubieCube();
prevMoves = [];
lastTs = 0;
batteryLevel = 0;
return result;
}
GiikerCube.regCubeModel({
prefix: ['QY-QYSC', 'XMD-TornadoV4-i'],
init: init,
opservs: [SERVICE_UUID],
cics: QIYI_CIC_LIST,
getBatteryLevel: function() { return Promise.resolve([batteryLevel, _deviceName]); },
clear: clear
});
});
这个文件的流程是,用户点击后选择魔方连接,连接成功发送握手包获取蓝牙数据,绑定一个事件,魔方每次转动都会收到加密的蓝牙数据包,发一个然后再按一个数据包,基本流程如下:
蓝牙数据包(msg)
↓
校验0xFE头
↓
解析opcode(0x02=初始化 / 0x03=转动)
↓
┌──────────────┬────────────────────────┐
│ opcode 0x02 │ opcode 0x03 │
│ 初始化魔方 │ 解析旋转动作队列 │
│ 读取电量 │ 更新立方体模型状态 │
│ 解析面片颜色 │ 调用 GiikerCube.callback │
└──────────────┴────────────────────────┘
↓
更新全局状态(batteryLevel, lastTs, prevCubie)
三、魔方的数据解析
从最开始时,是一直获取不到数据的,先是请求ble设备、连接Gatt服务、获取Service、获取Characteristic、最后订阅通知都完成了,却没有任何输出,代码如下:
console.log("蓝牙ble测试");
var _gatt;
var _service;
var _deviceName;
var _chrct_cube;
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
var SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
var CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
var decoder = null;
var deviceMac = 'CC:A3:00:00:D2:D3';
var KEYS = ['NoDg7ANAjGkEwBYCc0xQnADAVgkzGAzHNAGyRTanQi5QIFyHrjQMQgsC6QA'];
window.addEventListener("dblclick", async () =>{ // 双击事件
console.log("开始连接");
// 1. 请求 BLE 设备
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [SERVICE_UUID] // 这里加上你要访问的所有 service UUID
});
console.log('设备:', device.name);
// 2. 连接 GATT 服务
const server = await device.gatt.connect();
console.log('已连接 GATT Server');
// 3. 获取 Service
const service = await server.getPrimaryService(SERVICE_UUID);
console.log('service:\n',service);
// 4. 获取 Characteristic
const characteristic = await service.getCharacteristic(CHRCT_UUID_CUBE);
console.log('Characteristic:\n', characteristic);
// 5. 订阅数据通知
_chrct_cube=await characteristic.startNotifications();
_chrct_cube.addEventListener('characteristicvaluechanged', onCubeEvent);
console.log('已订阅数据通知 ✅');
});
//数据处理函数
function onCubeEvent(event) {
console.log("aaaaaaaaaaaaaaaa");
}
订阅通知有了,但就是不输出,很长一段时间都不知应该如何解决,后面才知它要有握手包,也就是要有await sendHello(deviceMac)这方法才行,而发送与接收这蓝牙魔方的数据,又要用到AES-128 与 LZString压缩/解压缩库,而这种$.sha256 和 $.aes128写法是全局对象上附加功能,要引入jQuery 库,所以最后实现的握手包发送,才得到数据收发,这真的难,index代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>BLE 测试</title>
<style>
body {
background-color: #121212; /* 深色背景 */
color: #ffffff; /* 浅色文字 */
font-family: Arial, sans-serif;
}
a {
color: #80cbc4; /* 链接颜色 */
}
.card {
background-color: #1e1e1e;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<button id="connect">连接 BLE 设备</button>
<script type="text/javascript" src="js/lib/jquery-1.8.0.js"></script>
<script type="text/javascript" src="js/lib/sha256.js"></script>
<script type="text/javascript" src="js/lib/lzstring.js"></script>
<script type="text/javascript" src="js/qiyicube.js"></script>
</body>
</html>
这时的我是兴奋的,但这时我得到只是一串我不懂的数字字符,我一直尝试分析,弄懂它们但这似乎是困难的,而且我制作的版本中有个bug,刚开始接收到数据会一直跳个不停,要用cstimer连接后才能正常输出数据,又不知道怎么解决,后面查明了,在onCubeEvent方法只是获取到了数据,初始化的握手包没有发送,所以魔方发送那里一直询问,也就是这一段:
function parseCubeData(msg) {
if (msg[0] != 0xfe) {
console.log('[qiyicube] error cube data', msg);
}
var opcode = msg[2];
var ts = (msg[3] << 24 | msg[4] << 16 | msg[5] << 8 | msg[6]);
if (opcode == 0x2) { // cube hello
batteryLevel = msg[35];//电池电量
sendMessage(msg.slice(2, 7));//不加这个会一直报,初始化握手包
//初始化魔方
//var newFacelet = parseFacelet(msg.slice(7, 34));
//GiikerCube.callback(newFacelet, [], [Math.trunc(ts / 1.6), locTime], _deviceName);
}
else if (opcode == 0x3) { // state change,魔方状态改变
sendMessage(msg.slice(2, 7));
console.log("当前旋转动作:",msg[34]);//转一下,log一次
}
}
后面加了这个就可以正常输入与输出了,也不用打开cstimer了,功能也复刻的差不多了,但得到是什么也不知,看来看去发现cstiemr用的是src/js/lib/mathlib.js的魔方数字模型,它们用的是54个面片表示一个魔方的状态,也就是msg.slice(7, 34)它是27 字节,但是有parseFacelet方法可以解析成一个魔方的状态,代码如下:
function parseFacelet(faceMsg) {
var ret = [];
for (var i = 0; i < 54; i++) {
ret.push("LRDUFB".charAt(faceMsg[i >> 1] >> (i % 2 << 2) & 0xf));
}
ret = ret.join("");
// giikerutil.log('[qiyicube]', 'parsedFacelet', ret);
return ret;
}
同样的mathlib中有定义:

所以,可以得到这样的魔方状态图:

而当我正想着如将这个魔方状态,也就是54面片转换成(F/R/U/L/D/B)等旋转操作时,也就是最先我想不出去问原作者的问题,也就是连5次U的操作,也让人很难分析:
BUBUUU LUFURUR RR RR B UFR FFF DFFLDD DDDDDRUL FLLLLLFR BLBBBDBB
BUBUUU FLFLRUU RR FR B DFU FFF FFRRRU DDDDDRUL LLLDLLDR BLBBBDBB
BUBUUU DDLFRUL RR FR B FFD FFF RFUFUL DDDDDRUL RLLRLLUR BLBBBDBB
BUBUUU URRDRUD RR LR B RFF FFF UFDFLF DDDDDRUL FLLULLLR BLBBBDBB
BUBUUU LUFURUR RR RR B UFR FFF DFFLDD DDDDDRUL FLLLLLFR BLBBBDBB
难度太大了,不知是怎么定义的,结果搞了半天人家,魔方传过来的数据是直接有操作的,也就是F/R/U/L/D/B它是编好号的,直接读取就行了,压根不用算,这波真是无语了,而对应的是parseCubeData函数中的msg[34],也就是这段:
function parseCubeData(msg) {
var locTime = $.now();
if (msg[0] != 0xfe) {
giikerutil.log('[qiyicube] error cube data', msg);
return;
}
var opcode = msg[2];
var ts = (msg[3] << 24 | msg[4] << 16 | msg[5] << 8 | msg[6]);
if (opcode == 0x2) { // cube hello
batteryLevel = msg[35];
sendMessage(msg.slice(2, 7));
var newFacelet = parseFacelet(msg.slice(7, 34));
GiikerCube.callback(newFacelet, [], [Math.trunc(ts / 1.6), locTime], _deviceName);
prevCubie.fromFacelet(newFacelet);
if (newFacelet != kernel.getProp('giiSolved', mathlib.SOLVED_FACELET)) {
var rst = kernel.getProp('giiRST');
if (rst == 'a' || rst == 'p' && confirm(CONFIRM_GIIRST)) {
giikerutil.markSolved();
}
}
} else if (opcode == 0x3) { // state change
sendMessage(msg.slice(2, 7));
// check timestamps
var todoMoves = [[msg[34], ts]];
while (todoMoves.length < 10) {
var off = 91 - 5 * todoMoves.length;
var hisTs = (msg[off] << 24 | msg[off + 1] << 16 | msg[off + 2] << 8 | msg[off + 3]);
var hisMv = msg[off + 4];
if (hisTs <= lastTs) {
break;
}
todoMoves.push([hisMv, hisTs]);
}
if (todoMoves.length > 1) {
giikerutil.log('[qiyicube] miss history moves', JSON.stringify(todoMoves), lastTs);
}
var toCallback = [];
var curFacelet;
for (var i = todoMoves.length - 1; i >= 0; i--) {
var axis = [4, 1, 3, 0, 2, 5][(todoMoves[i][0] - 1) >> 1];
var power = [0, 2][todoMoves[i][0] & 1];
var m = axis * 3 + power;
mathlib.CubieCube.CubeMult(prevCubie, mathlib.CubieCube.moveCube[m], curCubie);
prevMoves.unshift("URFDLB".charAt(axis) + " 2'".charAt(power));
prevMoves = prevMoves.slice(0, 8);
curFacelet = curCubie.toFaceCube();
toCallback.push([curFacelet, prevMoves.slice(), [Math.trunc(todoMoves[i][1] / 1.6), locTime], _deviceName]);
var tmp = curCubie;
curCubie = prevCubie;
prevCubie = tmp;
}
var newFacelet = parseFacelet(msg.slice(7, 34));
if (newFacelet != curFacelet) {
giikerutil.log('[qiyicube] facelet', newFacelet);
curCubie.fromFacelet(newFacelet);
GiikerCube.callback(newFacelet, prevMoves, [Math.trunc(ts / 1.6), locTime], _deviceName);
var tmp = curCubie;
curCubie = prevCubie;
prevCubie = tmp;
} else {
for (var i = 0; i < toCallback.length; i++) {
GiikerCube.callback.apply(null, toCallback[i]);
}
}
var newBatteryLevel = msg[35];
if (newBatteryLevel != batteryLevel) {
batteryLevel = newBatteryLevel;
giikerutil.updateBattery([batteryLevel, _deviceName]);
}
}
lastTs = ts;
}
它可以回溯最多 9 个历史动作,当前动作是msg[34],所以可以得到:
F 绿 9、10
R 红 3、4
U 白 7、8
D 黄 5、6L 橙 1、2
B 蓝 11、12
其中偶数为顺时针、奇数为逆时针,若是E/M/S是上面两个组合
所以数据包是:
- 第一个字节(
msg[0])应当是固定的同步头0xFE,不合法,直接返回 - 第二个字节(msg[2])是操作码,
0x02:魔方上电/初始化(Hello 包)及0x03:魔方状态改变(转动事件) - msg.slice(2,7),向魔方握手回应消息
- msg.slice(7, 34),从第 7~33 字节是面片颜色编码(共 27 字节),可解析成54面片
- msg[34],读取出当前旋转动作
- msg[35],读取出电池电量
其中,parseCubeData函数中是接收与旋转魔方的地方,如何注释了,那只会接收数据而虚拟魔方不会转动,也就下面这三行代码:
prevMoves = prevMoves.slice(0, 8);//历史动作
curFacelet = curCubie.toFaceCube();
toCallback.push([curFacelet, prevMoves.slice(), [Math.trunc(todoMoves[i][1] / 1.6), locTime], _deviceName]);
for (var i = 0; i < toCallback.length; i++) {
GiikerCube.callback.apply(null, toCallback[i]);//重放所有动作
}
这样最后,得到了一个文件,就可以正确读取魔方状态了,Demo地址:test-ble
console.log("hello world~!");
var _gatt;
var _service;
var _deviceName;
var _chrct_cube;
var UUID_SUFFIX = '-0000-1000-8000-00805f9b34fb';
var SERVICE_UUID = '0000fff0' + UUID_SUFFIX;
var CHRCT_UUID_CUBE = '0000fff6' + UUID_SUFFIX;
let aesEcb;
var QIYI_CIC_LIST = [0x0504];
var decoder = null;
var deviceMac = 'CC:A3:00:00:D2:D3';
var KEYS = ['NoDg7ANAjGkEwBYCc0xQnADAVgkzGAzHNAGyRTanQi5QIFyHrjQMQgsC6QA'];
// js/test.js
document.addEventListener('DOMContentLoaded', () => {
clear();
const btn = document.getElementById('connect');
btn.addEventListener('click', async () => {
try {
// 1. 请求 BLE 设备
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [SERVICE_UUID] // 这里加上你要访问的所有 service UUID
});
console.log('设备:', device.name);
// 2. 连接 GATT 服务
const server = await device.gatt.connect();
console.log('已连接 GATT Server');
// 3. 获取 Battery Service
const service = await server.getPrimaryService(SERVICE_UUID);
console.log('service:\n',service);
// 4. 获取 Characteristic
const characteristic = await service.getCharacteristic(CHRCT_UUID_CUBE);
console.log('Characteristic:\n', characteristic);
// 5. 订阅数据通知
_chrct_cube=await characteristic.startNotifications();
_chrct_cube.addEventListener('characteristicvaluechanged', onCubeEvent);
console.log('已订阅数据通知 ✅');
deviceMac = 'CC:A3:00:00:D2:D3';
await sendHello(deviceMac);
} catch (error) {
console.error(error);
}
});
});
// 🔹 数据处理函数
function onCubeEvent(event) {
const value = event.target.value;
//得到加密数据
const encMsg = new Uint8Array(value.buffer);
// 初始化 AES-128 解密器
if (!decoder) {
const key = JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[0]));// 假设 KEYS[0] 是压缩的密钥字符串,需解压为 16 字节数组
decoder = $.aes128(key); // 创建 AES-128 实例
}
//解密msg
var msg = [];
for (var i = 0; i < encMsg.length; i += 16) {
var block = encMsg.slice(i, i + 16);
decoder.decrypt(block);
for (var j = 0; j < 16; j++) {
msg[i + j] = block[j];
}
}
console.log('[qiyicube] decrypted msg', msg);
//处理解密后魔方状态的数据,
parseCubeData(msg);
}
function sendHello(mac) {
if (!mac) {
return Promise.reject('empty mac');
}
var content = [0x00, 0x6b, 0x01, 0x00, 0x00, 0x22, 0x06, 0x00, 0x02, 0x08, 0x00];
for (var i = 5; i >= 0; i--) {
content.push(parseInt(mac.slice(i * 3, i * 3 + 2), 16));
}
return sendMessage(content);
}
//使用的 CRC16 校验算法,确保发送和接收的数据没有被损坏
function crc16modbus(data) {
var crc = 0xFFFF;
for (var i = 0; i < data.length; i++) {
crc ^= data[i];
for (var j = 0; j < 8; j++) {
crc = (crc & 0x1) > 0 ? (crc >> 1) ^ 0xa001 : crc >> 1;
}
}
return crc;
}
// content: [u8, u8, ..]
function sendMessage(content) {
// if (!_chrct_cube || DEBUGBL) {
// return DEBUGBL ? Promise.resolve() : Promise.reject();
// }
var msg = [0xfe];
msg.push(4 + content.length); // length = 1 (op) + cont.length + 2 (crc)
for (var i = 0; i < content.length; i++) {
msg.push(content[i]);
}
var crc = crc16modbus(msg);
msg.push(crc & 0xff, crc >> 8);
var npad = (16 - msg.length % 16) % 16;
for (var i = 0; i < npad; i++) {
msg.push(0);
}
var encMsg = [];
decoder = decoder || $.aes128(JSON.parse(LZString.decompressFromEncodedURIComponent(KEYS[0])));
for (var i = 0; i < msg.length; i += 16) {
var block = msg.slice(i, i + 16);
decoder.encrypt(block);
for (var j = 0; j < 16; j++) {
encMsg[i + j] = block[j];
}
}
console.log('[qiyicube] send message to cube', msg, encMsg);
return _chrct_cube.writeValue(new Uint8Array(encMsg).buffer);
}
function clear() {
var result = Promise.resolve();
if (_chrct_cube) {
_chrct_cube.removeEventListener('characteristicvaluechanged', onCubeEvent);
result = _chrct_cube.stopNotifications().catch($.noop);
_chrct_cube = null;
}
_service = null;
_gatt = null;
_deviceName = null;
deviceMac = null;
// curCubie = new mathlib.CubieCube();
// prevCubie = new mathlib.CubieCube();
prevMoves = [];
lastTs = 0;
batteryLevel = 0;
return result;
}
function parseCubeData(msg) {
var locTime = $.now();
if (msg[0] != 0xfe) {
console.log('[qiyicube] error cube data', msg);
}
var opcode = msg[2];
var ts = (msg[3] << 24 | msg[4] << 16 | msg[5] << 8 | msg[6]);
if (opcode == 0x2) { // cube hello,不加这个会直报
batteryLevel = msg[35];
sendMessage(msg.slice(2, 7));
//初始化魔方
//var newFacelet = parseFacelet(msg.slice(7, 34));
//GiikerCube.callback(newFacelet, [], [Math.trunc(ts / 1.6), locTime], _deviceName);
//prevCubie.fromFacelet(newFacelet);
// if (newFacelet != kernel.getProp('giiSolved', mathlib.SOLVED_FACELET)) {
// var rst = kernel.getProp('giiRST');
// if (rst == 'a' || rst == 'p' && confirm(CONFIRM_GIIRST)) {
// giikerutil.markSolved();
// }
// }
}
else if (opcode == 0x3) { // state change,魔方状态改变
sendMessage(msg.slice(2, 7));
console.log("当前旋转动作:",msg[34]);
}
}
最后给出一个可以连接奇异魔方的魔改案例,这个可以连接手上的魔方,跟cstimer魔方转动效果差不多,使用结果:

更多推荐

所有评论(0)