// modbus.js - 优化版本 let _globalSlaveAddress = 0x01; //全局初始从机地址 let connected = false; // 连接状态 let sharedHeartbeatInterval = null; // 心跳定时器变量 let agreement = 'DEVICE_A'; //协议类型 默认为 DEVICE_A let taskInterval = 500; // 定时任务频率 let timeStatus = true; import store from '@/store'; // 先声明变量 let ecUI, ecBLE; // #ifdef APP import _ecUI from '@/utils/ecUI.js' import _ecBLE from '@/utils/ecBLE/ecBLE.js' ecUI = _ecUI; ecBLE = _ecBLE; // #endif // #ifdef MP const _ecUI = require('@/utils/ecUI.js') const _ecBLE = require('@/utils/ecBLE/ecBLE.js') ecUI = _ecUI; ecBLE = _ecBLE; // #endif // 将 ecBLE 导出,供其他页面使用 export { ecBLE, ecUI }; // 获取连接状态 export function getConnected() { return connected; } //设置协议类型 export function setAgreement(type) { agreement = type; } export function setTime(value) { if (value === taskInterval && isHeartbeatRunning()){ return; } taskInterval = value; stopHeartbeat(); startHeartbeat(); } export function setTimeStatus(value) { timeStatus = value; } export function initBLE() { // 监听连接状态变化 ecBLE.onBLEConnectionStateChange((res) => { console.log(res); if (res.ok && !connected) { connected = true; console.log("连接成功"); store.dispatch('ble/updateConnected', res.ok) ecBLE.stopBluetoothDevicesDiscovery(); } else { store.dispatch('ble/updateConnected', false) connected = false; ecUI.hideLoading(); this.$modal.showToast("请检查是否配置成功"); } }); // 接收数据 ecBLE.onBLECharacteristicValueChange((str, strHex) => { try { console.log("数据来了"); let data = strHex.replace(/[0-9a-fA-F]{2}/g, ' $&') ; const parsedData = readRegister(data) store.dispatch('ble/updateData', parsedData) } catch (error) { store.dispatch('ble/updateError', error) console.error('数据解析失败:', error); this.$modal.showToast("数据解析失败"); } }); } // Getter/Setter 管理全局从机地址 export function setGlobalSlaveAddress(addr) { if (addr < 0 || addr > 247) { throw new RangeError('slaveAddress 必须在 0~247 之间'); } _globalSlaveAddress = addr; } export function getGlobalSlaveAddress() { console.log(_globalSlaveAddress); return _globalSlaveAddress; } /** * 支持的 Modbus 类型 */ export const MODBUS_TYPES = { WRITE_ADDRESS: 'WRITE_ADDRESS', READ_REGISTER: 'READ_REGISTER', }; /** * Modbus 协议帧配置 */ export const MODBUS_FRAME_CONFIG = { DEVICE_A: { WRITE_ADDRESS: { //写地址 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: 0x00, functionCode: 0x06, startAddress: 0x0017, value: null, }, GET_ADDRESS: { //读取地址 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: 0xFF, // 从协议数据看是FF functionCode: 0x03, // 功能码03 startAddress: 0x0001, // 起始地址0001 value: "0x0046", // 读取0个寄存器(根据实际需求可调整) }, TIMED_TASKS:{ //定时任务 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x03, startAddress: 0x0001, value: "0x0046", }, RAIN:{ //雨 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0200", }, SNOW:{ //雪 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0100", }, WIND:{ //风 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0400", }, FLATTEN:{ //放平 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0040", }, STOP:{ //停止 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0080", }, READ_MANUAL:{ //手动 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0010", }, READ_AUTO:{ //自动 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0020", }, READ_DOWN:{ //向东 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0018", }, READ_UP:{ //向西 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0014", }, READ_CANCEL:{ //取消 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0029, value: "0x0000", }, READ_TIME:{ //校正时间 type: MODBUS_TYPES.READ_REGISTER, slaveAddress: _globalSlaveAddress, functionCode: 0x10, startAddress: 0x002C, value: null, }, READ_TEMPERATURE:{ //天文写入 type: MODBUS_TYPES.READ_REGISTER, slaveAddress: _globalSlaveAddress, functionCode: 0x10, startAddress: 0x0032, value: null, }, READ_LIMIT:{ //限位写入 type: MODBUS_TYPES.READ_REGISTER, slaveAddress: _globalSlaveAddress, functionCode: 0x10, startAddress: 0x0040, value: null, }, READ_INCLINATION:{ //坡度写入 type: MODBUS_TYPES.READ_REGISTER, slaveAddress: _globalSlaveAddress, functionCode: 0x10, startAddress: 0x003C, value: null, }, READ_FREQUENCY:{ //频点写入 type: MODBUS_TYPES.READ_REGISTER, slaveAddress: _globalSlaveAddress, functionCode: 0x10, startAddress: 0x0019, value: null, }, READ_DIRECTION:{ //电机方向 正转动 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0025, value: "0x0000", }, READ_REVERSE:{ //电机方向 反转 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0025, value: "0x0001", }, READ_RETURN:{ //夜返角度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0042, value: null, }, READ_FLAT:{ //放平角度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0043, value: null, }, READ_SPECIFY:{ //指定角度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0045, value: null, }, READ_SNOW:{ //雪天角度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0044, value: null, }, READ_WIND:{ //大风角度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0046, value: null, }, READ_OVERCURRENT:{ //过流写入 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0026, value: null, }, READ_TRACKING:{ //跟踪精度 type: MODBUS_TYPES.WRITE_ADDRESS, slaveAddress: _globalSlaveAddress, functionCode: 0x06, startAddress: 0x0027, value: null, }, }, DEVICE_B: { }, DEVICE_C: { }, }; /** * 写入操作 */ const TS = ['READ_TIME','READ_LIMIT','READ_INCLINATION','READ_FREQUENCY','READ_TEMPERATURE']; export function writeRegister(action, valueToWrite) { try{ if (action !='TIMED_TASKS'){ console.log("停止心跳--------"); stopHeartbeat(); } let value = valueToWrite; if (!TS.includes(action) && valueToWrite !== null && valueToWrite !== '' && valueToWrite !== undefined) { value = parseInt(valueToWrite, 10); } if (action =='WRITE_ADDRESS'){ value = parseInt(valueToWrite, 10); setGlobalSlaveAddress(value); } const buffer = generateModbusFrame(agreement, action, value); const arrayBuffer = arrayBufferToHex(buffer); const writeBLECharacteristicValue = ecBLE.writeBLECharacteristicValue(arrayBuffer, true); return writeBLECharacteristicValue; }catch(exception){ console.log("写入失败:", exception); }finally { if (action =='WRITE_ADDRESS'){ setGlobalSlaveAddress(value); }else if (action !='TIMED_TASKS' && action !='GET_ADDRESS' && timeStatus){ setTimeout(() => { if (getConnected() && timeStatus) { startHeartbeat(); } }, 50); } } } /** * 读取寄存器 */ export function readRegister(buffer) { return parseBluetoothData(buffer); } //心跳函数 export function heartbeat() { try { // 示例:发送一个读取寄存器的请求作为心跳 writeRegister('TIMED_TASKS',null); } catch (error) { console.error('心跳请求失败:', error); // 可以在这里添加重连逻辑 } } // 启动心跳定时器 export function startHeartbeat() { if (!getConnected()) { return; } if (!isHeartbeatRunning()) { sharedHeartbeatInterval = setInterval(heartbeat, taskInterval); } } // 停止心跳定时器 export function stopHeartbeat() { if (isHeartbeatRunning()) { clearInterval(sharedHeartbeatInterval); sharedHeartbeatInterval = null; } } // 检查心跳定时器是否正在运行 export function isHeartbeatRunning() { return sharedHeartbeatInterval !== null; } /** * 参数校验函数 */ function validateParams({ slaveAddress, functionCode, startAddress, valueToWrite }) { if (slaveAddress < 0 || slaveAddress > 247) { throw new RangeError('slaveAddress 必须在 0~247 之间'); } if (functionCode < 1 || functionCode > 255) { throw new RangeError('functionCode 必须在 1~255 之间'); } if (startAddress < 0 || startAddress > 0xFFFF) { throw new RangeError('startAddress 必须在 0~65535 之间'); } if (valueToWrite < 0 || valueToWrite > 0xFFFF) { throw new RangeError('valueToWrite 必须在 0~65535 之间'); } } /** * 创建 Modbus RTU 请求帧(根据配置) * @param {string} protocol 设备协议类型(DEVICE_A / DEVICE_B / DEVICE_C) * @param {string} action 操作 * @param {number} [valueToWrite=0] 写入值 (0~65535) * @returns {Buffer} Modbus RTU 请求帧 */ function generateModbusFrame(protocol, action, valueToWrite) { console.log(_globalSlaveAddress); const config = MODBUS_FRAME_CONFIG[protocol]?.[action]; if (!config) { throw new Error(`不支持的协议或写入类型: ${protocol} - ${action}`); } let { type, slaveAddress, functionCode, startAddress, value } = config; if ((valueToWrite == NaN || valueToWrite == null) && config.value !== undefined && config.value !== null) { valueToWrite = config.value; } if (action !== 'WRITE_ADDRESS' && action !== "GET_ADDRESS"){ slaveAddress = _globalSlaveAddress; } // validateParams({ slaveAddress, functionCode, startAddress, valueToWrite }); let buffer; if (type === MODBUS_TYPES.READ_REGISTER) { // valueToWrite 应该包含日期数据 const dateData = valueToWrite; // 应该是一个数组,包含年、月、日、时、分、秒 let buffer1 = new Uint8Array(7 + dateData.length * 2); // 基础7字节 + 数据字节 buffer1[0] = slaveAddress; buffer1[1] = functionCode; buffer1[2] = (startAddress >> 8) & 0xFF; buffer1[3] = startAddress & 0xFF; buffer1[4] = (dateData.length >> 8) & 0xFF; // 寄存器数量高字节 buffer1[5] = dateData.length & 0xFF; // 寄存器数量低字节 buffer1[6] = dateData.length * 2; // 字节数 // 填充数据 for (let i = 0; i < dateData.length; i++) { buffer1[7 + i*2] = (dateData[i] >> 8) & 0xFF; // 高字节 buffer1[8 + i*2] = dateData[i] & 0xFF; // 低字节 } const crc = calculateCRC(buffer1.subarray(0, 7 + dateData.length * 2)); const finalBuffer = new Uint8Array(buffer1.length + 2); finalBuffer.set(buffer1); finalBuffer[buffer1.length] = crc[0]; finalBuffer[buffer1.length + 1] = crc[1]; buffer = finalBuffer; } else { buffer = new Uint8Array(6); buffer[0] = slaveAddress; buffer[1] = functionCode; buffer[2] = (startAddress >> 8) & 0xFF; buffer[3] = startAddress & 0xFF; buffer[4] = (valueToWrite >> 8) & 0xFF; buffer[5] = valueToWrite & 0xFF; const crc = calculateCRC(buffer); const finalBuffer = new Uint8Array(buffer.length + crc.length); finalBuffer.set(buffer); finalBuffer.set(crc, buffer.length); buffer = finalBuffer; } return buffer; } function calculateCRC(buffer) { let crc = 0xFFFF; for (let i = 0; i < buffer.length; i++) { const tableIndex = (crc ^ buffer[i]) & 0xFF; crc = (crc >> 8) ^ crcTable[tableIndex]; } const crcBuffer = new Uint8Array(2); crcBuffer[0] = crc & 0xFF; crcBuffer[1] = (crc >> 8) & 0xFF; return crcBuffer; } // CRC 表只构建一次 const crcTable = buildCRCTable(); function buildCRCTable() { const table = new Uint16Array(256); for (let i = 0; i < 256; i++) { let crc = i; for (let j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } table[i] = crc; } return table; } /** * 将 ArrayBuffer 转为十六进制字符串 */ export function arrayBufferToHex(buffer, withSpaces = false) { const hexArray = [...new Uint8Array(buffer)] .map(b => b.toString(16).padStart(2, '0')); if (withSpaces) { return hexArray.join(' ').toUpperCase(); } else { return hexArray.join('').toUpperCase(); } } /** * 解析蓝牙数据 */ export function parseBluetoothData(hexString) { ecBLE.saveWriteDataToLocal("TX: " + hexString,"tx"); if (!hexString || hexString.length < 6) { const error = new Error('蓝牙数据不完整'); throw error; } // 移除所有空格并重新格式化为标准格式 const cleanHexString = hexString.replace(/\s/g, ''); if (cleanHexString.length < 6) { throw new Error('蓝牙数据不完整'); } // 将连续的十六进制字符串转换为带空格的格式 const formattedHexString = cleanHexString.match(/.{1,2}/g)?.join(' ') || cleanHexString; const byteStrings = formattedHexString.split(' ').filter(s => s.length > 0); console.log("字节字符串数组:", JSON.stringify(byteStrings)); console.log("字节数组长度:", byteStrings.length); if (byteStrings.length < 10 ){ //其他操作执行成功 // 其他操作执行成功 return; } if (!byteStrings || byteStrings.length < 3) { throw new Error('蓝牙数据不完整'); } const register = { device: '', function: '', registerNumber: '', Addres_23: '', Frequence_25: '', NetworkId_26: '', modAddre_27: '', MotorCurrent_30: '', MotorCurrent_35: '', Battery_32: '', Temperature_33: '', MotDrection_37: '', OverProtection_38: '', TrackingAccuracy_39: '', Message_40: '', WorkModle_41: '', TargetAngle_42: '', RealAngle_43: '', RealAngle_31: '', Year_44: '', Month_45: '', Day_46: '', Hour_47: '', Minute_48: '', Second_49: '', nowtime: '', Longitude_50: '', Latitude_51: '', TimeZone_52: '', EleAngle_53: '', Azimuth_54: '', width_60: '', inter_61: '', UpGrade_62: '', DownGrade_63: '', EasternLimit_64: '', WesternLimit_65: '', NightAngle_66: '', FlatAngle_67: '', SnowAngle_68: '', SpecifiedAngle_69: '', WindAngle_70: '', qAzimuth_54: '', qwidth_60: '', Interval_61: '', qEasternLimit_64: '', qWesternLimit_65: '', qNightAngle_66: '', qFlatAngle_67: '', qSnowAngle_68: '', qSpecifiedAngle_69: '', qWindAngle_70: '', qEleAngle_53: '', qTargetAngle_42: '', qRealAngle_43: '', qRealAngle_31: '' }; register.device = parseInt(byteStrings[0], 16); register.function = parseInt(byteStrings[1], 16); register.registerNumber = parseInt(byteStrings[2], 16); const formattedOutput = []; for (let i = 3; i < byteStrings.length; i += 2) { const byte1 = parseInt(byteStrings[i], 16); const byte2 = i + 1 < byteStrings.length ? parseInt(byteStrings[i + 1], 16) : 0; const combined = ((byte1 << 8) | byte2).toString(16).padStart(4, '0'); formattedOutput.push(combined.toUpperCase()); } // 参照mainwindow.cpp的解析逻辑补全 for (let i = 1; i < formattedOutput.length; i++) { const value = parseInt(formattedOutput[i], 16); const valueInt16 = (value > 0x7FFF) ? value - 0x10000 : value; // 转换为有符号16位整数 if (i === 22) { // 地址写入 register.Addres_23 = valueInt16.toString(); } else if (i === 24) { // 频点[0-83] register.Frequence_25 = valueInt16.toString(); } else if (i === 25) { // 网络ID[0-255] register.NetworkId_26 = valueInt16.toString(); } else if (i === 26) { // 模块地址 register.modAddre_27 = (parseInt(register.NetworkId_26, 10) * 256) + register.device; } else if (i === 29) { // 电机1电流1位小数(A) register.MotorCurrent_30 = valueInt16.toString(); register.MotorCurrent_30 = insertDecimal(register.MotorCurrent_30, 1); } else if (i === 34) { // 电机2电流1位小数(A) register.MotorCurrent_35 = valueInt16.toString(); register.MotorCurrent_35 = insertDecimal(register.MotorCurrent_35, 1); } else if (i === 31) { // 电池1位小数(V) register.Battery_32 = valueInt16.toString(); register.Battery_32 = insertDecimal(register.Battery_32, 1); } else if (i === 32) { // 温度(度) register.Temperature_33 = valueInt16.toString(); } else if (i === 35) { // 标定有效 register.Demarcate_36 = valueInt16.toString(); } else if (i === 36) { // 电机方向 register.MotDrection_37 = valueInt16.toString(); } else if (i === 37) { // 过流保护(A) register.OverProtection_38 = valueInt16.toString(); } else if (i === 38) { // 跟踪精度 register.TrackingAccuracy_39 = valueInt16.toString(); } else if (i === 39) { // 十进制转二进制0111倾角+过流+限位 register.Message_40 = valueInt16.toString(2); // 转二进制 } else if (i === 40) { // 工作模式 register.WorkModle_41 = valueInt16.toString(2); // 转二进制 } else if (i === 41) { // 目标角度_两位小数 register.TargetAngle_42 = valueInt16; register.qTargetAngle_42 = insertDecimal(register.TargetAngle_42.toString(), 2); } else if (i === 42) { // 实际角度1_两位小数 register.RealAngle_43 = valueInt16; register.qRealAngle_43 = insertDecimal(register.RealAngle_43.toString(), 2); } else if (i === 30) { // 实际角度2_两位小数 register.RealAngle_31 = valueInt16; register.qRealAngle_31 = insertDecimal(register.RealAngle_31.toString(), 2); } else if (i === 43) { // 年 register.Year_44 = valueInt16.toString(); } else if (i === 44) { // 月 register.Month_45 = valueInt16.toString(); } else if (i === 45) { // 日 register.Day_46 = valueInt16.toString(); } else if (i === 46) { // 时 register.Hour_47 = valueInt16.toString(); } else if (i === 47) { // 分 register.Minute_48 = valueInt16.toString(); } else if (i === 48) { // 秒 register.Second_49 = valueInt16.toString(); register.nowtime = `${register.Year_44}-${register.Month_45}-${register.Day_46} ${register.Hour_47.padStart(2, '0')}:${register.Minute_48.padStart(2, '0')}:${register.Second_49.padStart(2, '0')}`; } else if (i === 49) { // 太阳经度_两位小数 register.Longitude_50 = valueInt16.toString(); register.Longitude_50 = insertDecimal(register.Longitude_50, 2); } else if (i === 50) { // 太阳纬度_两位小数 register.Latitude_51 = valueInt16.toString(); register.Latitude_51 = insertDecimal(register.Latitude_51, 2); } else if (i === 51) { // 时区_两位小数800 register.TimeZone_52 = valueInt16.toString(); register.TimeZone_52 = insertDecimal(register.TimeZone_52, 2); } else if (i === 52) { // 太阳高度角_两位小数 register.EleAngle_53 = valueInt16; register.qEleAngle_53 = insertDecimal(register.EleAngle_53.toString(), 2); } else if (i === 53) { // 太阳方位角_两位小数180调零 register.Azimuth_54 = valueInt16; register.qAzimuth_54 = insertDecimal(register.Azimuth_54.toString(), 2); } else if (i === 59) { // 宽度_两位小数 register.width_60 = valueInt16; register.qwidth_60 = insertDecimal(register.width_60.toString(), 2); } else if (i === 60) { // 间距_两位小数 register.inter_61 = valueInt16; register.Interval_61 = insertDecimal(register.inter_61.toString(), 2); } else if (i === 61) { // 上坡度_两位小数 register.UpGrade_62 = valueInt16.toString(); register.UpGrade_62 = insertDecimal(register.UpGrade_62, 2); } else if (i === 62) { // 下坡度_两位小数 register.DownGrade_63 = valueInt16.toString(); register.DownGrade_63 = insertDecimal(register.DownGrade_63, 2); } else if (i === 63) { // 东限位 register.EasternLimit_64 = valueInt16; register.qEasternLimit_64 = register.EasternLimit_64.toString(); } else if (i === 64) { // 西限位 register.WesternLimit_65 = valueInt16; register.qWesternLimit_65 = register.WesternLimit_65.toString(); } else if (i === 65) { // 夜返角 register.NightAngle_66 = valueInt16; register.qNightAngle_66 = register.NightAngle_66.toString(); } else if (i === 66) { // 放平角度 register.FlatAngle_67 = valueInt16; register.qFlatAngle_67 = register.FlatAngle_67.toString(); } else if (i === 67) { // 雪天角度 register.SnowAngle_68 = valueInt16; register.qSnowAngle_68 = register.SnowAngle_68.toString(); } else if (i === 68) { // 指定角度 register.SpecifiedAngle_69 = valueInt16; register.qSpecifiedAngle_69 = register.SpecifiedAngle_69.toString(); } else if (i === 69) { // 大风角度 register.WindAngle_70 = valueInt16; register.qWindAngle_70 = register.WindAngle_70.toString(); } } console.log(JSON.stringify( register)); return register; } /** * 在数字中插入小数点 */ export function insertDecimal(value, digits = 2) { // 处理空值或无效值 if (value === null || value === undefined || value === '') { return (0).toFixed(digits); } // 确保是数字类型 const numValue = Number(value); // 如果不是有效数字,返回默认值 if (isNaN(numValue)) { return (0).toFixed(digits); } // 计算除数 const divisor = Math.pow(10, digits); // 执行除法并格式化为指定小数位数 return (numValue / divisor).toFixed(digits); } /** * 显示接收数据(格式化为 16 字节一行) */ export function displayReceiveData(buffer) { const hexStr = HexToAscii(buffer); const lines = hexStr.split(' '); let str = ''; for (let i = 0; i < lines.length; i++) { str += lines[i] + ' '; if ((i + 1) % 16 === 0) str += '\n'; } return str; } /** * 将字节数组转为十六进制字符串 */ export function HexToAscii(buffer) { return [...new Uint8Array(buffer)] .map(b => b.toString(16).padStart(2, '0')) .join(' ') .trim(); }