/** * 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} 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} 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