#!/usr/bin/env node const net = require('net'); const { crc8 } = require('./decoder'); const COMMAND_TYPE_DATA_SYNC = 0x01; const COMMAND_ID_REALTIME = 0x00; const COMMAND_ID_BLOOD_PRESSURE = 0x01; function parseArgs(argv) { const options = { host: '127.0.0.1', port: 9000, localAddress: '', repeat: 1, intervalMs: 500, mode: 'both', printOnly: false, }; for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; const value = argv[index + 1]; if (arg === '--host' && value) { options.host = value; index += 1; } else if (arg === '--port' && value) { options.port = Number(value); index += 1; } else if (arg === '--local-address' && value) { options.localAddress = value; index += 1; } else if (arg === '--repeat' && value) { options.repeat = Number(value); index += 1; } else if (arg === '--interval' && value) { options.intervalMs = Number(value); index += 1; } else if (arg === '--mode' && value) { options.mode = value.trim().toLowerCase(); index += 1; } else if (arg === '--print-only') { options.printOnly = true; } else if (arg === '--help' || arg === '-h') { options.help = true; } } return options; } function printHelp() { console.log(`JH2028 新协议 TCP 模拟器 说明: 本脚本只生成新版 2026-05-11 协议报文,帧头固定为 55 AA。 不发送旧版 EE 55 协议,也不发送旧版 AA 55 血压扩展协议。 用法: node tcp-simulator.js --host 127.0.0.1 --port 9000 node tcp-simulator.js --mode realtime --print-only node tcp-simulator.js --mode blood-pressure --print-only 参数: --host TCP 服务地址,默认 127.0.0.1 --port TCP 服务端口,默认 9000 --local-address 绑定本地源 IP,用于模拟设备来源 IP --repeat 发送轮数,默认 1,0 表示一直循环 --interval 发送间隔,默认 500 --mode realtime 只发送实时数据 blood-pressure 只发送血压数据 both 先发送实时数据,再发送血压数据 --print-only 只打印报文,不连接 TCP 服务 `); } function buildFrame(timestamp, commandType, commandId, payload) { const frameLength = 7 + payload.length; const frameWithoutCrc = Buffer.from([ 0x55, 0xAA, frameLength, timestamp & 0xFF, commandType & 0xFF, commandId & 0xFF, ...payload, ]); return Buffer.concat([frameWithoutCrc, Buffer.from([crc8(frameWithoutCrc)])]); } function writeInt16LE(value) { const buffer = Buffer.alloc(2); buffer.writeInt16LE(value, 0); return Array.from(buffer); } function writeUInt16LE(value) { const buffer = Buffer.alloc(2); buffer.writeUInt16LE(value, 0); return Array.from(buffer); } function writeUInt32LE(value) { const buffer = Buffer.alloc(4); buffer.writeUInt32LE(value, 0); return Array.from(buffer); } function buildRealtimePayload() { return Buffer.from([ ...writeInt16LE(365), ...writeInt16LE(368), ...writeUInt16LE(2000), ...writeUInt16LE(500), ...writeUInt16LE(250), ...writeUInt16LE(90), ...writeUInt16LE(500), ...writeUInt16LE(320), ...writeInt16LE(-100), ...writeInt16LE(120), ...writeInt16LE(-80), ...writeUInt32LE(2048), ...writeUInt16LE(138), ...writeUInt16LE(140), ...writeUInt16LE(25), ...writeUInt16LE(0), ...writeUInt16LE(0), ...writeUInt16LE(987), ...writeUInt16LE(365), ...writeUInt16LE(132), ...writeUInt16LE(367), ...writeUInt16LE(15), ]); } function buildBloodPressurePayload() { return Buffer.from([120, 80, 76, 0, 93]); } function bufferToHex(buffer) { return Array.from(buffer, (value) => value.toString(16).toUpperCase().padStart(2, '0')).join(' '); } function describeFrame(frame) { return { header: bufferToHex(frame.slice(0, 2)), length: frame[2], timestamp: frame[3], commandType: `0x${frame[4].toString(16).toUpperCase().padStart(2, '0')}`, commandId: `0x${frame[5].toString(16).toUpperCase().padStart(2, '0')}`, crc: `0x${frame[frame.length - 1].toString(16).toUpperCase().padStart(2, '0')}`, rawHex: bufferToHex(frame), }; } function prepareFrames(mode) { const frames = []; let timestamp = 1; if (mode === 'realtime' || mode === 'both') { frames.push({ name: '实时数据', frame: buildFrame(timestamp, COMMAND_TYPE_DATA_SYNC, COMMAND_ID_REALTIME, buildRealtimePayload()), }); timestamp += 1; } if (mode === 'blood-pressure' || mode === 'both') { frames.push({ name: '血压数据', frame: buildFrame(timestamp, COMMAND_TYPE_DATA_SYNC, COMMAND_ID_BLOOD_PRESSURE, buildBloodPressurePayload()), }); } return frames; } function printFrames(frames) { for (const item of frames) { const frameInfo = describeFrame(item.frame); console.log(`[SIM] ${item.name} 帧头=${frameInfo.header} 长度=${frameInfo.length} 命令类型=${frameInfo.commandType} 命令ID=${frameInfo.commandId} CRC=${frameInfo.crc}`); console.log(`[SIM] ${frameInfo.rawHex}`); } } function main() { const options = parseArgs(process.argv); if (options.help) { printHelp(); return; } const frames = prepareFrames(options.mode); if (frames.length === 0) { throw new Error(`不支持的模拟模式: ${options.mode}`); } if (options.printOnly) { printFrames(frames); return; } const socketOptions = { host: options.host, port: options.port, }; if (options.localAddress) { socketOptions.localAddress = options.localAddress; } const socket = net.createConnection(socketOptions); let round = 0; let index = 0; let timer = null; function stop(reason) { if (timer) { clearInterval(timer); timer = null; } if (!socket.destroyed) { socket.end(); socket.destroy(); } if (reason) { console.log(reason); } } function sendNext() { const item = frames[index]; const frameInfo = describeFrame(item.frame); socket.write(item.frame); console.log(`[SIM] 已发送 ${item.name} 轮次=${round + 1} 帧序号=${index + 1} 帧头=${frameInfo.header} 命令类型=${frameInfo.commandType} 命令ID=${frameInfo.commandId}`); console.log(`[SIM] ${frameInfo.rawHex}`); index += 1; if (index >= frames.length) { index = 0; round += 1; if (options.repeat > 0 && round >= options.repeat) { stop(`[SIM] 已完成 轮数=${options.repeat}`); } } } socket.on('connect', () => { console.log(`[SIM] 已连接 ${options.host}:${options.port}`); sendNext(); timer = setInterval(sendNext, options.intervalMs); }); socket.on('error', (error) => { stop(`[SIM] 连接异常: ${error.message}`); process.exitCode = 1; }); socket.on('close', () => { if (timer) { stop('[SIM] 连接已关闭'); } }); process.on('SIGINT', () => { stop('[SIM] 收到 SIGINT,停止发送'); process.exit(0); }); process.on('SIGTERM', () => { stop('[SIM] 收到 SIGTERM,停止发送'); process.exit(0); }); } if (require.main === module) { main(); } module.exports = { buildBloodPressurePayload, buildFrame, buildRealtimePayload, describeFrame, parseArgs, prepareFrames, };