gx
chenyc
5 天以前 747a86bb94006aaca721cfc0c0ce7061643a9ea6
jhm2028-service.js
@@ -1,19 +1,19 @@
#!/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"
 */
@@ -80,25 +80,25 @@
}
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
`);
}
@@ -132,7 +132,7 @@
        raw,
        value: raw / 10,
        unit: '°C',
        note: 'PDF example shows 0x00000172 => 370 => 37.0°C',
        note: '协议文档示例:0x00000172 => 370 => 37.0°C',
      };
    }
@@ -141,7 +141,7 @@
      return {
        name: 'value_0x03',
        raw,
        note: 'PDF indicates XX1<<24 + XX2<<16 + XX3<<8 + XX4',
        note: '协议文档说明:数值为 XX1<<24 + XX2<<16 + XX3<<8 + XX4',
      };
    }
@@ -152,7 +152,7 @@
        name: 'dual_uint16_0x04',
        value1,
        value2,
        note: 'PDF indicates first two bytes and last two bytes are separate values',
        note: '协议文档说明:前两字节和后两字节分别表示独立数值',
      };
    }
@@ -172,7 +172,7 @@
        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 组成数值',
      };
    }
@@ -193,7 +193,7 @@
        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',
      };
    }
@@ -202,7 +202,7 @@
        name: `command_${toHex(command)}`,
        raw: uint32BE(payload),
        payload: Array.from(payload),
        note: 'Meaning not fully legible in source PDF; raw data preserved',
        note: '原始协议文档语义不够清晰,先保留原始数据',
      };
  }
}
@@ -238,7 +238,7 @@
  if (valid) {
    parsed.decoded = decodePayload(command, payload);
  } else {
    parsed.error = 'Checksum mismatch';
    parsed.error = '校验和不匹配';
  }
  return parsed;
@@ -249,12 +249,12 @@
  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);
@@ -317,23 +317,23 @@
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 必须是正整数');
  }
}
@@ -342,17 +342,17 @@
}
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,
  });
@@ -360,7 +360,7 @@
  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} 的整数倍`);
  }
}
@@ -370,10 +370,10 @@
    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;
    }
@@ -395,7 +395,7 @@
  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,
  });
@@ -408,11 +408,11 @@
  });
  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', () => {
@@ -425,7 +425,7 @@
  port.open((error) => {
    if (error) {
      console.error('Failed to open serial port:', error.message);
      console.error('打开串口失败:', error.message);
      process.exit(1);
    }
  });
@@ -444,7 +444,7 @@
    runSerialService(options);
  } catch (error) {
    console.error(error.message);
    console.error('Use --help to see available options.');
    console.error('可使用 --help 查看可用参数。');
    process.exit(1);
  }
}