/**
|
* dxVgBle Module.
|
* - UART based communication for VgBle (Bluetooth) device.
|
* - Supports reading device config (getConfig) and pushing config (setConfig).
|
* - Parses common protocol frames (55AA...) and inner BLE-specific payloads (P1=7E/7A).
|
* - Exposes a loop method to be polled by upper layer.
|
*
|
* Design notes:
|
* - All binary data is represented as hex uppercase string when crossing the JS boundary.
|
* - This module is protocol-focused and keeps state in `dxMap("dxVgBleMap")` so it can be shared across workers.
|
* - Public APIs are exposed via `dxVgBle.slave` and `dxVgBle.loop`.
|
*/
|
import dxCommonUtils from './dxCommonUtils.js'
|
import dxChannel from './dxChannel.js'
|
import dxLogger from './dxLogger.js'
|
import dxDriver from './dxDriver.js'
|
import dxMap from './dxMap.js'
|
import dxStd from './dxStd.js'
|
import dxOs from './dxOs.js'
|
|
const dxVgBle = {}
|
|
let handle_id
|
|
// Global debug switch for all dxVgBle logs (controlled by init debug parameter)
|
let g_debug = false
|
|
/**
|
* Internal log wrapper. All dxVgBle logs should go through this function
|
* so that they can be globally enabled/disabled by `init(debug)`.
|
* @param {...any} args - arguments forwarded to dxLogger.info
|
*/
|
function logInfo() {
|
if (!g_debug) {
|
return;
|
}
|
dxLogger.info.apply(dxLogger, arguments);
|
}
|
|
// Global callbacks for BLE events
|
let g_callbacks = {
|
onMessage: null,
|
onConnectSuccess: null
|
}
|
|
/**
|
* Initialize UART channel and store handle into dxMap (idempotent across threads).
|
* Upper layer should call this once before using any `dxVgBle.slave.*` APIs.
|
* @param {string} [rate] UART config string, for example '115200-8-N-1'.
|
* @param {boolean} [debug=false] When true, enable all dxVgBle logs; when false, suppress all logs.
|
*/
|
dxVgBle.init = function (rate = '115200-8-N-1', debug = false) {
|
// Always update debug flag even if already inited, so upper layer can toggle logging.
|
g_debug = !!debug
|
|
const vgMap = dxMap.get("dxVgBleMap")
|
const inited = vgMap.get("inited")
|
if (inited === true) {
|
return;
|
}
|
handle_id = dxChannel.open(dxChannel.TYPE.UART, dxDriver.CHANNEL.BLE_PATH)
|
dxChannel.ioctl(handle_id, dxChannel.IOC_SET_CMD.CHANNEL_IOC_SET_UART_PARAM, rate)
|
vgMap.put("handle_id", handle_id)
|
vgMap.put("inited", true)
|
}
|
|
// Public BLE slave operations (high-level API for upper layers)
|
dxVgBle.slave = {
|
/**
|
* Send BLE config to device.
|
* Params accept hex strings; empty values are skipped.
|
* Returns a Promise resolved by async setConfig response (P1=7A).
|
*
|
* Protocol:
|
* 55 AA 60 00 LL HH [P1=7A] [TLVs...] FE BCC
|
*
|
* @param {string} key - auth key in hex string, used for later ciphertext verification.
|
* @param {object} [params] - optional, { nameHex, macHex, snHex, deviceStatusHex } hex strings.
|
* @returns {Promise<any>} resolved when setConfig response arrives or timeout.
|
*/
|
setConfig: function (key, params = {}) {
|
checkHandleId()
|
|
const vgMap = dxMap.get("dxVgBleMap");
|
|
// Store auth key into vgMap for later use in cmd08 verification.
|
// Expect key to be a hex string.
|
if (key && typeof key === 'string') {
|
vgMap.put("auth_key_hex", key.toUpperCase())
|
}
|
|
const { nameHex, macHex, snHex, deviceStatusHex } = params
|
const hasConfig = !!(nameHex || macHex || snHex || deviceStatusHex);
|
|
// If params is empty (no config fields), only store key and return.
|
if (!hasConfig) {
|
return Promise.resolve();
|
}
|
|
function makeTlv(tHex, vHex) {
|
if (!vHex) {
|
return '';
|
}
|
const safeV = vHex.toUpperCase();
|
const lenByte = Math.floor(safeV.length / 2);
|
const lenHex = lenByte.toString(16).padStart(2, '0');
|
return tHex + lenHex + safeV;
|
}
|
|
const nameTlv = makeTlv("10", nameHex);
|
const macTlv = makeTlv("11", macHex);
|
const snTlv = makeTlv("14", snHex);
|
const statusTlv = makeTlv("15", deviceStatusHex);
|
|
const payloadHex = "7a00" + nameTlv + macTlv + snTlv + statusTlv + "fe";
|
const payloadInfo = buildPayloadLength(payloadHex);
|
const finalContent = buildAndSendFrame(0x60, 0x00, payloadHex, 'dxVgBle TX: slave.setConfig', payloadInfo);
|
const token = Date.now().toString();
|
|
vgMap.put("slave_setConfig_token", token);
|
vgMap.del("slave_setConfig_result");
|
|
return new Promise((res, rej) => {
|
const start = Date.now();
|
const intervalId = dxStd.setInterval(() => {
|
const result = vgMap.get("slave_setConfig_result");
|
if (result && result.token === token) {
|
dxStd.clearInterval(intervalId);
|
vgMap.del("slave_setConfig_result");
|
res(result.semantic || result);
|
return;
|
}
|
if (Date.now() - start > 5000) {
|
dxStd.clearInterval(intervalId);
|
rej(new Error("dxVgBle.slave: setConfig timeout."));
|
}
|
}, 50);
|
})
|
},
|
/**
|
* Request BLE config from device.
|
* Returns a Promise resolved by async getConfig response (P1=7E cmd=0x02).
|
*
|
* Protocol:
|
* 55 AA 60 00 06 00 7E 01 00 02 00 FE BCC
|
*
|
* @returns {Promise<any>} resolved with semantic config or timeout error.
|
*/
|
getConfig: async function () {
|
checkHandleId()
|
const getCfgFrame = new Uint8Array([
|
0x55, 0xaa, 0x60, 0x00, 0x06, 0x00,
|
0x7e, 0x01, 0x00, 0x02, 0x00, 0xfe, 0x9c
|
])
|
logInfo('dxVgBle TX: slave.getConfig', bytesToHex(getCfgFrame))
|
dxChannel.send(handle_id, getCfgFrame.buffer)
|
const token = Date.now().toString()
|
const vgMap = dxMap.get("dxVgBleMap")
|
vgMap.put("slave_getConfig_token", token)
|
vgMap.del("slave_getConfig_result")
|
|
return new Promise((res, rej) => {
|
const start = Date.now()
|
const intervalId = dxStd.setInterval(() => {
|
const result = vgMap.get("slave_getConfig_result")
|
if (result && result.token === token) {
|
dxStd.clearInterval(intervalId)
|
vgMap.del("slave_getConfig_result")
|
res(result.semantic || result)
|
return
|
}
|
if (Date.now() - start > 5000) {
|
dxStd.clearInterval(intervalId)
|
rej(new Error("dxVgBle.slave: getConfig timeout."))
|
}
|
}, 50)
|
})
|
},
|
/**
|
* Register callbacks from upper layer.
|
* - `onMessage(dataHex)` is triggered when transparent data (cmd=0x0F) is received.
|
* - `onConnectSuccess(connId)` is triggered when AES verification in cmd08 succeeds.
|
* @param {object} callbacks
|
*/
|
setCallbacks: function (callbacks) {
|
if (!callbacks || typeof callbacks.onMessage !== 'function' || typeof callbacks.onConnectSuccess !== 'function') {
|
throw new Error('Callbacks must be an object with onMessage and onConnectSuccess functions');
|
}
|
|
g_callbacks.onMessage = callbacks.onMessage;
|
g_callbacks.onConnectSuccess = callbacks.onConnectSuccess;
|
},
|
/**
|
* Send transparent data to remote side (cmd = 0x0F).
|
* Message is application data in hex string; `connId` is the BLE connection identifier.
|
* @param {string} message - application data in hex string (without connId).
|
* @param {number} connId - connection identifier (0-255).
|
*/
|
sendMessage: function (message, connId) {
|
const payloadHex = message + connId.toString(16).padStart(2, '0');
|
const payloadInfo = buildPayloadLength(payloadHex);
|
buildAndSendFrame(0x0f, 0x00, payloadHex, 'dxVgBle TX: cmd0f', payloadInfo);
|
}
|
}
|
|
/**
|
* Polling entry to process incoming UART frames.
|
* Should be called periodically by upper layer main loop / timer.
|
*/
|
dxVgBle.loop = function () {
|
vgCommonProtocol();
|
}
|
|
/**
|
* Uint8Array -> hex uppercase string.
|
* @param {Uint8Array} uint8Arr
|
* @returns {string}
|
*/
|
function bytesToHex(uint8Arr) {
|
if (!uint8Arr) {
|
return '';
|
}
|
let hex = '';
|
for (let i = 0; i < uint8Arr.length; i++) {
|
const h = uint8Arr[i].toString(16).padStart(2, '0');
|
hex += h;
|
}
|
return hex.toUpperCase();
|
}
|
|
/**
|
* Hex string -> Uint8Array.
|
* @param {string} hexStr
|
* @returns {Uint8Array}
|
*/
|
function hexToBytes(hexStr) {
|
if (!hexStr || typeof hexStr !== 'string') {
|
return new Uint8Array(0);
|
}
|
const cleanHex = hexStr.trim();
|
if (cleanHex.length % 2 !== 0) {
|
return new Uint8Array(0);
|
}
|
const len = cleanHex.length / 2;
|
const arr = new Uint8Array(len);
|
for (let i = 0; i < len; i++) {
|
const byteHex = cleanHex.substr(i * 2, 2);
|
arr[i] = parseInt(byteHex, 16);
|
}
|
return arr;
|
}
|
|
/**
|
* Ensure handle_id is available (fetch from dxMap if needed).
|
* This makes sure APIs can work after cross-thread calls where the local variable was not initialized.
|
*/
|
function checkHandleId() {
|
if (handle_id == null || handle_id == undefined) {
|
handle_id = dxMap.get("dxVgBleMap").get("handle_id")
|
}
|
}
|
|
/**
|
* Pre-calc payload length and its low/high bytes from a payload hex string.
|
* @param {string} payloadHex
|
* @returns {{payloadLen:number,lenLowHex:string,lenHighHex:string}}
|
*/
|
function buildPayloadLength(payloadHex) {
|
const payloadLen = Math.floor((payloadHex ? payloadHex.length : 0) / 2);
|
const lenLowHex = (payloadLen & 0xff).toString(16).padStart(2, '0');
|
const lenHighHex = ((payloadLen >> 8) & 0xff).toString(16).padStart(2, '0');
|
return {
|
payloadLen: payloadLen,
|
lenLowHex: lenLowHex,
|
lenHighHex: lenHighHex
|
};
|
}
|
|
/**
|
* Build full 55AA frame, calculate checksum and send through UART.
|
* Caller is responsible for making sure handle_id is ready.
|
* @param {number} cmd - command byte.
|
* @param {number} result - result byte (0x00 normally).
|
* @param {string} payloadHex - payload hex string.
|
* @param {string} logPrefix - logger prefix string.
|
* @param {{payloadLen:number,lenLowHex:string,lenHighHex:string}} [payloadInfo] - optional pre-calculated length info.
|
* @returns {string} final hex content that has been sent.
|
*/
|
function buildAndSendFrame(cmd, result, payloadHex, logPrefix, payloadInfo) {
|
checkHandleId();
|
if (handle_id == null || handle_id == undefined) {
|
return '';
|
}
|
|
const cmdHex = cmd.toString(16).padStart(2, '0');
|
const resultHex = result.toString(16).padStart(2, '0');
|
const info = payloadInfo || buildPayloadLength(payloadHex || '');
|
|
// full content without checksum
|
const content = "55aa" + cmdHex + resultHex + info.lenLowHex + info.lenHighHex + (payloadHex || '');
|
|
// calc bcc over payload
|
const payloadBytes = payloadHex ? hexToBytes(payloadHex) : new Uint8Array(0);
|
const checksum = blebcc(info.payloadLen, cmd, result, payloadBytes);
|
const checksumHex = checksum.toString(16).padStart(2, '0');
|
|
const finalContent = (content + checksumHex).toUpperCase();
|
|
logInfo(logPrefix, finalContent);
|
const sendBuf = hexToBytes(finalContent);
|
dxChannel.send(handle_id, sendBuf.buffer);
|
|
return finalContent;
|
}
|
|
/**
|
* Common protocol handler: reads one frame from UART, validates BCC, dispatches to vgBleProtocol.
|
*/
|
function vgCommonProtocol() {
|
checkHandleId();
|
if (handle_id == null || handle_id == undefined) {
|
return;
|
}
|
|
// Read header (2 bytes)
|
const header = dxChannel.receive(handle_id, 2, 100);
|
if (header === null || header.length !== 2) {
|
return;
|
}
|
|
if (header[0] !== 0x55 || header[1] !== 0xAA) {
|
// Not a valid header, ignore this byte sequence
|
return;
|
}
|
|
// Read cmd (1 byte)
|
const cmdBuf = dxChannel.receive(handle_id, 1, 100);
|
if (cmdBuf === null || cmdBuf.length !== 1) {
|
return;
|
}
|
const cmd = cmdBuf[0];
|
|
// Read length (2 bytes, little-endian)
|
const lenBuf = dxChannel.receive(handle_id, 2, 100);
|
if (lenBuf === null || lenBuf.length !== 2) {
|
return;
|
}
|
const length = lenBuf[0] | (lenBuf[1] << 8);
|
|
// Read payload (length bytes)
|
let payload = null;
|
if (length > 0) {
|
payload = dxChannel.receive(handle_id, length, 100);
|
if (payload === null || payload.length !== length) {
|
return;
|
}
|
} else {
|
payload = new Uint8Array(0);
|
}
|
|
// Read checksum (1 byte)
|
const checksumBuf = dxChannel.receive(handle_id, 1, 100);
|
if (checksumBuf === null || checksumBuf.length !== 1) {
|
return;
|
}
|
const checksum = checksumBuf[0];
|
|
// Build whole packet buffer for logging
|
const totalLen = 2 + 1 + 2 + payload.length + 1;
|
const packetBuf = new Uint8Array(totalLen);
|
packetBuf[0] = header[0];
|
packetBuf[1] = header[1];
|
packetBuf[2] = cmd;
|
packetBuf[3] = lenBuf[0];
|
packetBuf[4] = lenBuf[1];
|
if (payload.length > 0) {
|
packetBuf.set(payload, 5);
|
}
|
packetBuf[totalLen - 1] = checksum;
|
|
const packet = {
|
header: '55AA',
|
cmd: cmd,
|
length: length,
|
payloadHex: bytesToHex(payload),
|
checksum: checksum,
|
rawHex: bytesToHex(packetBuf)
|
};
|
|
if (blebcc(length, cmd, 0, payload) == checksum) {
|
logInfo('vgCommonProtocol packet:', JSON.stringify(packet));
|
vgBleProtocol(packet);
|
} else {
|
throw new Error('vgCommonProtocol: bcc check failed');
|
}
|
}
|
|
/**
|
* Calculate BCC for payload.
|
* @param {number} dlen - payload length
|
* @param {number} cmd
|
* @param {number} result
|
* @param {Uint8Array} data
|
* @returns {number}
|
*/
|
function blebcc(dlen, cmd, result, data) {
|
let bcc = 0;
|
bcc ^= 0x55;
|
bcc ^= 0xaa;
|
bcc ^= cmd;
|
bcc ^= result;
|
bcc ^= (dlen - 1 & 0xff);
|
bcc ^= (dlen - 1 & 0xff00) >> 8;
|
for (let i = 0; i < dlen; i++) {
|
bcc ^= data[i];
|
}
|
return bcc;
|
}
|
|
/**
|
* Parse TLV buffer. Each item format: T(1 byte) L(1 byte) V(L bytes).
|
* @param {Uint8Array} dataBytes
|
* @returns {Array<{t:number,length:number,valueHex:string,value:number[]}>}
|
*/
|
function parseTlvs(dataBytes) {
|
const tlvs = [];
|
if (!dataBytes || !dataBytes.length) {
|
return tlvs;
|
}
|
|
let offset = 0;
|
while (offset + 2 <= dataBytes.length) {
|
const t = dataBytes[offset];
|
const l = dataBytes[offset + 1];
|
if (offset + 2 + l > dataBytes.length) {
|
break;
|
}
|
const vBytes = dataBytes.slice(offset + 2, offset + 2 + l);
|
tlvs.push({
|
t: t,
|
length: l,
|
valueHex: bytesToHex(vBytes),
|
value: Array.from(vBytes)
|
});
|
offset += 2 + l;
|
}
|
return tlvs;
|
}
|
|
// VgBle protocol parser (handles BLE-specific payload inside common 0x60/0x07/0x08/0x0F frames)
|
/**
|
* Parse inner BLE protocol (payload of common frame).
|
* Dispatch to specific command handlers by cmd/P1.
|
* @param {object} pack - { header, cmd, length, payloadHex, checksum, rawHex }
|
*/
|
function vgBleProtocol(pack) {
|
if (!pack || typeof pack.payloadHex !== 'string') {
|
return;
|
}
|
|
switch (pack.cmd.toString(16).padStart(2, '0')) {
|
case '60':
|
cmd60(pack)
|
break;
|
case '07':
|
cmd07(pack)
|
break;
|
case '08':
|
cmd08(pack)
|
break;
|
case '0f':
|
cmd0f(pack)
|
break;
|
default:
|
break;
|
}
|
}
|
|
function getUrandom(len) {
|
return dxOs.systemWithRes(`dd if=/dev/urandom bs=1 count="${len}" 2>/dev/null | xxd -p`, 100).split(/\s/g)[0]
|
}
|
|
function cmd07(pack) {
|
if (!pack || !pack.payloadHex) {
|
return;
|
}
|
|
const payloadBytes = hexToBytes(pack.payloadHex);
|
let connId;
|
let randomBits = 16; // default 16 bits
|
|
if (pack.length === 1) {
|
// 1 byte: connection identifier only
|
connId = payloadBytes[0];
|
} else if (pack.length === 2) {
|
// 2 bytes: first byte is P1 (random bits), second byte is connection identifier
|
const P1 = payloadBytes[0];
|
if (P1 === 16 || P1 === 32) {
|
randomBits = P1;
|
} else {
|
// Invalid P1, use default 16 bits
|
randomBits = 16;
|
}
|
connId = payloadBytes[1];
|
} else {
|
// Invalid length, ignore
|
logInfo('cmd07: invalid payload length, expected 1 or 2 bytes, got', pack.length);
|
return;
|
}
|
|
// Generate random number
|
const randomHex = getUrandom(randomBits);
|
|
// Get existing connections array from map, or initialize as empty array
|
const vgMap = dxMap.get("dxVgBleMap")
|
let connections = vgMap.get("cmd07_connections");
|
if (!Array.isArray(connections)) {
|
connections = [];
|
}
|
|
// Find existing connection with same connId
|
let existingIndex = -1;
|
for (let i = 0; i < connections.length; i++) {
|
if (connections[i].connId === connId) {
|
existingIndex = i;
|
break;
|
}
|
}
|
|
// Create connection object
|
const connObj = {
|
connId: connId,
|
random: randomHex,
|
randomBits: randomBits
|
};
|
|
// Update or add connection
|
if (existingIndex >= 0) {
|
// Update existing connection
|
connections[existingIndex] = connObj;
|
logInfo('cmd07: updated connection connId=', connId, 'random=', randomHex, 'randomBits=', randomBits);
|
} else {
|
// Add new connection
|
connections.push(connObj);
|
logInfo('cmd07: added new connection connId=', connId, 'random=', randomHex, 'randomBits=', randomBits);
|
}
|
|
// Store updated connections array to map
|
vgMap.put("cmd07_connections", connections);
|
|
const payloadHex = randomHex + connId.toString(16).padStart(2, '0');
|
const payloadInfo = buildPayloadLength(payloadHex);
|
buildAndSendFrame(0x07, 0x00, payloadHex, 'dxVgBle TX: cmd07', payloadInfo);
|
}
|
|
function cmd08(pack) {
|
if (!pack || !pack.payloadHex) {
|
return;
|
}
|
|
const payloadBytes = hexToBytes(pack.payloadHex);
|
|
if (pack.length < 2) {
|
logInfo('cmd08: invalid payload length, expected at least 2 bytes, got', pack.length);
|
return;
|
}
|
|
// First byte is P1 (MTU), skip for now
|
const P1 = payloadBytes[0];
|
|
// Last byte is connection identifier
|
const connId = payloadBytes[payloadBytes.length - 1];
|
|
// Middle bytes are encrypted ciphertext
|
const ciphertextBytes = payloadBytes.slice(1, payloadBytes.length - 1);
|
const ciphertextHex = bytesToHex(ciphertextBytes);
|
|
logInfo('cmd08: P1(MTU)=', P1, 'connId=', connId, 'ciphertextHex=', ciphertextHex);
|
|
// Get connection info from map to find corresponding random number
|
const vgMap = dxMap.get("dxVgBleMap");
|
let connections = vgMap.get("cmd07_connections");
|
if (!Array.isArray(connections)) {
|
connections = [];
|
}
|
|
// Find connection by connId
|
let connection = null;
|
for (let i = 0; i < connections.length; i++) {
|
if (connections[i].connId === connId) {
|
connection = connections[i];
|
break;
|
}
|
}
|
|
if (!connection) {
|
logInfo('cmd08: connection not found for connId=', connId);
|
return;
|
}
|
|
logInfo('cmd08: found connection for connId=', connId, 'random=', connection.random);
|
|
// Get auth key from map (value previously stored by slave.setConfig key parameter).
|
const keyHex = vgMap.get("auth_key_hex");
|
if (!keyHex || typeof keyHex !== 'string') {
|
logInfo('cmd08: auth key not found in vgMap, cannot verify ciphertext. connId=', connId);
|
return;
|
}
|
|
// Verify ciphertext: encrypt the random with the same AES-128-ECB(NoPadding) and compare with device ciphertext
|
const randomHex = connection.random;
|
const randomByteLen = randomHex ? Math.floor(randomHex.length / 2) : 0;
|
if (!randomHex || randomByteLen === 0 || randomByteLen % 16 !== 0) {
|
logInfo('cmd08: invalid random length for AES-ECB, randomHex=', randomHex);
|
return;
|
}
|
|
const encryptedRandomBase64 = dxCommonUtils.crypto.aes.encrypt(
|
hexToBytes(randomHex).buffer,
|
hexToBytes(keyHex).buffer,
|
{
|
mode: dxCommonUtils.AES_MODE.ECB,
|
keySize: dxCommonUtils.AES_KEY_SIZE.BITS_128,
|
padding: dxCommonUtils.AES_PADDING.NONE
|
}
|
);
|
|
if (encryptedRandomBase64 === null) {
|
logInfo('cmd08: encrypt random failed for connId=', connId);
|
return;
|
}
|
|
const encryptedRandomBuf = dxCommonUtils.codec.base64ToArrayBuffer(encryptedRandomBase64);
|
const encryptedRandomHex = bytesToHex(new Uint8Array(encryptedRandomBuf));
|
|
// Take first N hex chars (N = random length) and compare with ciphertext
|
const compareLen = randomHex.length;
|
const truncatedHex = encryptedRandomHex.slice(0, compareLen);
|
const truncatedCipherHex = ciphertextHex.slice(0, compareLen);
|
|
const verifyOk = truncatedHex.toUpperCase() === truncatedCipherHex.toUpperCase();
|
connection.verifyOk = verifyOk;
|
vgMap.put("cmd07_connections", connections);
|
|
let result = 0x00;
|
if (verifyOk) {
|
result = 0x00;
|
logInfo('cmd08: ciphertext verify success, connId=', connId);
|
if (typeof g_callbacks.onConnectSuccess === 'function') {
|
try {
|
g_callbacks.onConnectSuccess(connId);
|
} catch (e) {
|
logInfo('cmd08: onConnectSuccess callback error:', e && e.message ? e.message : e);
|
}
|
}
|
} else {
|
result = 0x90;
|
logInfo('cmd08: ciphertext verify failed, connId=', connId, 'randomHex=', randomHex, 'encryptedRandomHex=', encryptedRandomHex, 'truncatedHex=', truncatedHex);
|
}
|
|
// Only 1 byte payload: connId
|
const connIdHex = connId.toString(16).padStart(2, '0');
|
const payloadHex = connIdHex;
|
|
// For cmd08, length is fixed to 1; we can pre-build length info.
|
const payloadInfo = {
|
payloadLen: 1,
|
lenLowHex: '01',
|
lenHighHex: '00'
|
};
|
|
buildAndSendFrame(0x08, result, payloadHex, 'dxVgBle TX: cmd08', payloadInfo);
|
}
|
|
function cmd0f(pack) {
|
if (!pack || !pack.payloadHex) {
|
return;
|
}
|
|
const hex = pack.payloadHex.toUpperCase();
|
|
// Need at least P1 P2 P3 + connId = 4 bytes => 8 hex chars
|
if (hex.length < 8) {
|
logInfo('cmd0f: payload too short, hex =', hex);
|
return;
|
}
|
|
const P1 = hex.slice(0, 2);
|
const P2 = hex.slice(2, 4);
|
const P3 = hex.slice(4, 6);
|
const connIdHex = hex.slice(hex.length - 2);
|
const dataHex = hex.slice(6, hex.length - 2); // transparent data
|
const dataBytes = hexToBytes(dataHex);
|
|
const transparentPacket = {
|
P1: P1,
|
P2: P2,
|
P3: P3,
|
connId: connIdHex,
|
dataHex: dataHex,
|
data: Array.from(dataBytes)
|
};
|
|
// Attach parsed 0F packet to pack for upper layer usage
|
pack.transparent = transparentPacket;
|
|
const vgMap = dxMap.get("dxVgBleMap");
|
vgMap.put("last_cmd0f_packet", transparentPacket);
|
|
if (typeof g_callbacks.onMessage === 'function') {
|
try {
|
g_callbacks.onMessage(dataHex);
|
} catch (e) {
|
logInfo('cmd0f: onMessage callback error:', e && e.message ? e.message : e);
|
}
|
}
|
|
logInfo('cmd0f: parsed transparent packet:', JSON.stringify(transparentPacket));
|
}
|
|
function cmd60(pack) {
|
const hex = pack.payloadHex.toUpperCase();
|
// Need at least 3(P1,P2,P3)+1(cmd)+1(len)+1(conn_id) bytes = 6 bytes => 12 hex chars
|
if (hex.length < 12) {
|
return;
|
}
|
|
const P1 = hex.slice(0, 2);
|
const P2 = hex.slice(2, 4);
|
const P3 = hex.slice(4, 6);
|
|
switch (P1) {
|
case '7E':
|
parseP1_7E(pack, hex, P2, P3);
|
break;
|
case '7A':
|
parseP1_7A(pack, hex, P2);
|
break;
|
default:
|
logInfo('vgBleProtocol: unsupported P1, ignore packet. P1 =', P1);
|
break;
|
}
|
}
|
|
/**
|
* Parser for P1 = 0x7E (3-byte header P1/P2/P3, then cmd/len/data/connId).
|
* @param {object} pack
|
* @param {string} hex - payload hex
|
* @param {string} P2
|
* @param {string} P3
|
*/
|
function parseP1_7E(pack, hex, P2, P3) {
|
const cmdHex = hex.slice(6, 8);
|
const lenHex = hex.slice(8, 10);
|
const cmd = parseInt(cmdHex, 16);
|
const dataLen = parseInt(lenHex, 16);
|
|
// 3(P1,P2,P3) + 1(cmd) + 1(len) + dataLen + 1(conn_id)
|
const expectedTotalBytes = 3 + 1 + 1 + dataLen + 1;
|
const expectedHexLen = expectedTotalBytes * 2;
|
if (hex.length < expectedHexLen) {
|
return;
|
}
|
|
const dataStart = 10;
|
const dataEnd = dataStart + dataLen * 2;
|
const dataHex = hex.slice(dataStart, dataEnd);
|
const connIdHex = hex.slice(dataEnd, dataEnd + 2);
|
|
const dataBytes = hexToBytes(dataHex);
|
const tlvs = parseTlvs(dataBytes);
|
|
// Semantic TLV mapping
|
const semantic = {};
|
for (let i = 0; i < tlvs.length; i++) {
|
const item = tlvs[i];
|
switch (item.t) {
|
case 0x01:
|
// Bluetooth name
|
semantic.nameHex = item.valueHex;
|
break;
|
case 0x02:
|
// Bluetooth MAC
|
semantic.macHex = item.valueHex;
|
break;
|
case 0x03:
|
// Serial number
|
semantic.snHex = item.valueHex;
|
break;
|
case 0x04:
|
// Device status in advertising
|
semantic.deviceStatusHex = item.valueHex;
|
break;
|
default:
|
break;
|
}
|
}
|
|
const blePacket = {
|
P1: '7E',
|
P2: P2,
|
P3: P3,
|
cmd: cmd,
|
length: dataLen,
|
dataHex: dataHex,
|
data: Array.from(dataBytes),
|
connId: connIdHex,
|
tlv: tlvs,
|
semantic: semantic
|
};
|
|
// attach parsed ble packet into original vg packet for upper layer usage
|
pack.ble = blePacket;
|
logInfo('vgBleProtocol ble packet (P1=7E):', JSON.stringify(blePacket));
|
|
// When P1 == 0x7E and inner cmd == 0x02, store semantic data for getConfig via dxMap
|
if (cmd === 0x02) {
|
const vgMap = dxMap.get("dxVgBleMap");
|
const token = vgMap.get("slave_getConfig_token");
|
vgMap.put("slave_getConfig_result", {
|
token: token,
|
semantic: semantic
|
});
|
}
|
}
|
|
/**
|
* Parser for P1 = 0x7A (2-byte header P1/P2, then TLVs, then connId).
|
* @param {object} pack
|
* @param {string} hex - payload hex
|
* @param {string} P2
|
*/
|
function parseP1_7A(pack, hex, P2) {
|
// Format: P1 P2 [TLVs ...] connId
|
if (hex.length < 6) { // at least P1 P2 + connId
|
return;
|
}
|
|
const connIdHex = hex.slice(hex.length - 2, hex.length);
|
const dataHex = hex.slice(4, hex.length - 2); // after P1 P2 until before connId
|
const dataBytes = hexToBytes(dataHex);
|
const tlvs = parseTlvs(dataBytes);
|
|
// Semantic TLV mapping
|
const semantic = {};
|
for (let i = 0; i < tlvs.length; i++) {
|
const item = tlvs[i];
|
switch (item.t) {
|
case 0x10:
|
semantic.nameHex = item.valueHex;
|
break;
|
case 0x11:
|
semantic.macHex = item.valueHex;
|
break;
|
case 0x14:
|
semantic.snHex = item.valueHex;
|
break;
|
case 0x15:
|
semantic.deviceStatusHex = item.valueHex;
|
break;
|
default:
|
break;
|
}
|
}
|
|
const blePacket = {
|
P1: '7A',
|
P2: P2,
|
P3: '',
|
cmd: null,
|
length: dataBytes.length,
|
dataHex: dataHex,
|
data: Array.from(dataBytes),
|
connId: connIdHex,
|
tlv: tlvs,
|
semantic: semantic
|
};
|
|
pack.ble = blePacket;
|
logInfo('vgBleProtocol ble packet (P1=7A):', JSON.stringify(blePacket));
|
|
// When P1 == 0x7A, treat as setConfig response; store semantic via dxMap for async return
|
const vgMap = dxMap.get("dxVgBleMap");
|
const token = vgMap.get("slave_setConfig_token");
|
if (token) {
|
vgMap.put("slave_setConfig_result", {
|
token: token,
|
semantic: semantic
|
});
|
}
|
}
|
|
export default dxVgBle
|