| | |
| | | #!/usr/bin/env node |
| | | |
| | | /** |
| | | * JHM-2028 serial communication service. |
| | | * JHM-2028 串口调试服务。 |
| | | * |
| | | * 当前项目正式运行入口是 `app.js`,通过 `tcp-service.js` 接收透传盒子的 TCP 数据。 |
| | | * 本文件保留为“串口直连调试参考脚本”,用于协议排查、串口联调和模拟数据验证。 |
| | | * 它不参与当前 TCP + MQTT 的生产主流程。 |
| | | * |
| | | * Install: |
| | | * 安装依赖: |
| | | * npm install serialport |
| | | * |
| | | * Serial mode: |
| | | * 串口模式: |
| | | * node jhm2028-service.js --port COM3 --baudRate 4800 |
| | | * |
| | | * Simulation mode: |
| | | * 模拟模式: |
| | | * node jhm2028-service.js --simulate "EE 55 01 00 00 01 72 74" |
| | | */ |
| | | |
| | |
| | | } |
| | | |
| | | function printHelp() { |
| | | console.log(`JHM-2028 Node.js serial communication service |
| | | console.log(`JHM-2028 Node.js 串口调试服务 |
| | | |
| | | Usage: |
| | | 用法: |
| | | node jhm2028-service.js --port COM3 [options] |
| | | node jhm2028-service.js --simulate "EE 55 01 00 00 01 72 74" |
| | | |
| | | Options: |
| | | --port <name> Serial port name, e.g. COM3 |
| | | --baudRate <number> Baud rate, default 4800 |
| | | --dataBits <number> Data bits, default 8 |
| | | --stopBits <number> Stop bits, default 1 |
| | | --parity <value> none | even | odd, default none |
| | | --raw Print raw incoming bytes in hex |
| | | --no-timestamp Omit timestamp in JSON output |
| | | --simulate <hex> Parse one or more hex bytes without opening a serial port |
| | | --help, -h Show this help |
| | | 参数: |
| | | --port <name> 串口名称,例如 COM3 |
| | | --baudRate <number> 波特率,默认 4800 |
| | | --dataBits <number> 数据位,默认 8 |
| | | --stopBits <number> 停止位,默认 1 |
| | | --parity <value> 校验位:none | even | odd,默认 none |
| | | --raw 以十六进制打印原始输入 |
| | | --no-timestamp JSON 输出中不带时间戳 |
| | | --simulate <hex> 不打开串口,直接解析一段十六进制字节 |
| | | --help, -h 显示帮助 |
| | | |
| | | Protocol: |
| | | Frame = EE 55 NN XX1 XX2 XX3 XX4 CY |
| | | 协议格式: |
| | | 帧格式 = EE 55 NN XX1 XX2 XX3 XX4 CY |
| | | CY = (NN + XX1 + XX2 + XX3 + XX4) & 0xFF |
| | | `); |
| | | } |
| | |
| | | raw, |
| | | value: raw / 10, |
| | | unit: '°C', |
| | | note: 'PDF example shows 0x00000172 => 370 => 37.0°C', |
| | | note: '协议文档示例:0x00000172 => 370 => 37.0°C', |
| | | }; |
| | | } |
| | | |
| | |
| | | return { |
| | | name: 'value_0x03', |
| | | raw, |
| | | note: 'PDF indicates XX1<<24 + XX2<<16 + XX3<<8 + XX4', |
| | | note: '协议文档说明:数值为 XX1<<24 + XX2<<16 + XX3<<8 + XX4', |
| | | }; |
| | | } |
| | | |
| | |
| | | name: 'dual_uint16_0x04', |
| | | value1, |
| | | value2, |
| | | note: 'PDF indicates first two bytes and last two bytes are separate values', |
| | | note: '协议文档说明:前两字节和后两字节分别表示独立数值', |
| | | }; |
| | | } |
| | | |
| | |
| | | name: 'stateful_value_0x09', |
| | | mode, |
| | | raw, |
| | | note: 'PDF indicates XX1 is a flag (0x00/0x01) and XX2-XX4 compose the value', |
| | | note: '协议文档说明:XX1 为符号标志位(0x00/0x01),XX2-XX4 组成数值', |
| | | }; |
| | | } |
| | | |
| | |
| | | flag, |
| | | raw, |
| | | value: raw / 10, |
| | | note: 'PDF indicates XX1 is 0x00 and XX2-XX4 value should be divided by 10', |
| | | note: '协议文档说明:XX1 固定为 0x00,XX2-XX4 组成的数值需除以 10', |
| | | }; |
| | | } |
| | | |
| | |
| | | name: `command_${toHex(command)}`, |
| | | raw: uint32BE(payload), |
| | | payload: Array.from(payload), |
| | | note: 'Meaning not fully legible in source PDF; raw data preserved', |
| | | note: '原始协议文档语义不够清晰,先保留原始数据', |
| | | }; |
| | | } |
| | | } |
| | |
| | | if (valid) { |
| | | parsed.decoded = decodePayload(command, payload); |
| | | } else { |
| | | parsed.error = 'Checksum mismatch'; |
| | | parsed.error = '校验和不匹配'; |
| | | } |
| | | |
| | | return parsed; |
| | |
| | | const hexPairs = normalized.split(/\s+/).filter(Boolean); |
| | | |
| | | if (hexPairs.length === 0) { |
| | | throw new Error('No hex bytes found in simulate input'); |
| | | throw new Error('模拟输入中未找到十六进制字节'); |
| | | } |
| | | |
| | | const values = hexPairs.map((pair) => { |
| | | if (pair.length > 2) { |
| | | throw new Error(`Invalid byte: ${pair}`); |
| | | throw new Error(`非法字节: ${pair}`); |
| | | } |
| | | |
| | | return Number.parseInt(pair, 16); |
| | |
| | | |
| | | function validateOptions(options) { |
| | | if (!options.simulate && !options.port) { |
| | | throw new Error('Missing serial port. Example: node jhm2028-service.js --port COM3'); |
| | | throw new Error('缺少串口参数,例如:node jhm2028-service.js --port COM3'); |
| | | } |
| | | |
| | | if (![5, 6, 7, 8].includes(options.dataBits)) { |
| | | throw new Error('dataBits must be one of 5, 6, 7, 8'); |
| | | throw new Error('dataBits 只能是 5、6、7、8 之一'); |
| | | } |
| | | |
| | | if (![1, 1.5, 2].includes(options.stopBits)) { |
| | | throw new Error('stopBits must be one of 1, 1.5, 2'); |
| | | throw new Error('stopBits 只能是 1、1.5、2 之一'); |
| | | } |
| | | |
| | | if (!['none', 'even', 'odd', 'mark', 'space'].includes(options.parity)) { |
| | | throw new Error('parity must be one of none, even, odd, mark, space'); |
| | | throw new Error('parity 只能是 none、even、odd、mark、space 之一'); |
| | | } |
| | | |
| | | if (!Number.isInteger(options.baudRate) || options.baudRate <= 0) { |
| | | throw new Error('baudRate must be a positive integer'); |
| | | throw new Error('baudRate 必须是正整数'); |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | function printStartup(options) { |
| | | console.log('JHM-2028 serial service started'); |
| | | console.log(`Port: ${options.port}`); |
| | | console.log(`Config: ${options.baudRate} baud, ${options.dataBits} data bits, ${options.stopBits} stop bit(s), parity=${options.parity}`); |
| | | console.log(`Frame format: ${toHex(HEADER_1)} ${toHex(HEADER_2)} NN XX1 XX2 XX3 XX4 CY`); |
| | | console.log('Waiting for data...'); |
| | | console.log('JHM-2028 串口调试服务已启动'); |
| | | console.log(`串口: ${options.port}`); |
| | | console.log(`配置: ${options.baudRate} 波特率,${options.dataBits} 数据位,${options.stopBits} 停止位,parity=${options.parity}`); |
| | | console.log(`帧格式: ${toHex(HEADER_1)} ${toHex(HEADER_2)} NN XX1 XX2 XX3 XX4 CY`); |
| | | console.log('正在等待数据...'); |
| | | } |
| | | |
| | | function runSimulation(options) { |
| | | const parser = new Jhm2028Parser({ |
| | | includeTimestamp: options.timestamp, |
| | | onRawChunk: options.raw ? (chunk) => console.log(`[raw] ${bytesToHex(chunk)}`) : null, |
| | | onRawChunk: options.raw ? (chunk) => console.log(`[原始数据] ${bytesToHex(chunk)}`) : null, |
| | | onFrame: printJson, |
| | | }); |
| | | |
| | |
| | | parser.push(buffer); |
| | | |
| | | if (buffer.length % FRAME_LENGTH !== 0) { |
| | | console.error(`Warning: simulate input length is ${buffer.length} bytes, not a multiple of ${FRAME_LENGTH}`); |
| | | console.error(`警告:模拟输入长度为 ${buffer.length} 字节,不是 ${FRAME_LENGTH} 的整数倍`); |
| | | } |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | |
| | | console.log('\nClosing serial port...'); |
| | | console.log('\n正在关闭串口...'); |
| | | port.close((error) => { |
| | | if (error) { |
| | | console.error('Failed to close serial port:', error.message); |
| | | console.error('关闭串口失败:', error.message); |
| | | process.exit(1); |
| | | return; |
| | | } |
| | |
| | | |
| | | const parser = new Jhm2028Parser({ |
| | | includeTimestamp: options.timestamp, |
| | | onRawChunk: options.raw ? (chunk) => console.log(`[raw] ${bytesToHex(chunk)}`) : null, |
| | | onRawChunk: options.raw ? (chunk) => console.log(`[原始数据] ${bytesToHex(chunk)}`) : null, |
| | | onFrame: printJson, |
| | | }); |
| | | |
| | |
| | | }); |
| | | |
| | | port.on('error', (error) => { |
| | | console.error('Serial port error:', error.message); |
| | | console.error('串口异常:', error.message); |
| | | }); |
| | | |
| | | port.on('close', () => { |
| | | console.log('Serial port closed'); |
| | | console.log('串口已关闭'); |
| | | }); |
| | | |
| | | process.on('SIGINT', () => { |
| | |
| | | |
| | | port.open((error) => { |
| | | if (error) { |
| | | console.error('Failed to open serial port:', error.message); |
| | | console.error('打开串口失败:', error.message); |
| | | process.exit(1); |
| | | } |
| | | }); |
| | |
| | | runSerialService(options); |
| | | } catch (error) { |
| | | console.error(error.message); |
| | | console.error('Use --help to see available options.'); |
| | | console.error('可使用 --help 查看可用参数。'); |
| | | process.exit(1); |
| | | } |
| | | } |