#!/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 <ip> TCP 服务地址,默认 127.0.0.1
|
--port <number> TCP 服务端口,默认 9000
|
--local-address <ip> 绑定本地源 IP,用于模拟设备来源 IP
|
--repeat <number> 发送轮数,默认 1,0 表示一直循环
|
--interval <ms> 发送间隔,默认 500
|
--mode <realtime|blood-pressure|both>
|
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,
|
};
|