const net = require('net');
|
const fs = require('fs');
|
const path = require('path');
|
const iconv = require('iconv-lite');
|
|
const {
|
extractFramesFromUtf16leBuffer,
|
guessTextEncodingFromBuffer,
|
} = require('./framing');
|
|
function parseArgs(argv) {
|
const args = {
|
host: '127.0.0.1',
|
port: 3021,
|
interval: 1000,
|
once: false,
|
mode: 'frames', // frames | bytes
|
chunk: 0, // when mode=bytes: bytes per write; 0 = one write
|
jitter: 0, // optional random jitter ms added to interval
|
fromHexFile: null,
|
fromBinFile: null,
|
encoding: null, // utf16le | utf8 | null(auto)
|
debug: false,
|
};
|
|
// Collect positional args too (in case caller passes: host port hexFile interval)
|
const positional = [];
|
|
for (let i = 2; i < argv.length; i++) {
|
const a = argv[i];
|
const next = argv[i + 1];
|
|
if (a && !a.startsWith('-')) {
|
positional.push(a);
|
continue;
|
}
|
|
if (a === '--host' && next) { args.host = next; i++; continue; }
|
if (a === '--port' && next) { args.port = Number(next); i++; continue; }
|
if (a === '--interval' && next) { args.interval = Number(next); i++; continue; }
|
if (a === '--jitter' && next) { args.jitter = Number(next); i++; continue; }
|
if (a === '--once') { args.once = true; continue; }
|
|
if (a === '--mode' && next) { args.mode = next; i++; continue; }
|
if (a === '--chunk' && next) { args.chunk = Number(next); i++; continue; }
|
|
if (a === '--hex-file' && next) { args.fromHexFile = next; i++; continue; }
|
if (a === '--bin-file' && next) { args.fromBinFile = next; i++; continue; }
|
if (a === '--utf16le') { args.encoding = 'utf16le'; continue; }
|
if (a === '--utf8') { args.encoding = 'utf8'; continue; }
|
|
if (a === '--debug') { args.debug = true; continue; }
|
if (a === '--help' || a === '-h') { args.help = true; continue; }
|
}
|
|
// Fallback positional mapping (only when flags are not provided)
|
// node sim_server.js 127.0.0.1 39023 .\结果全部.txt 200
|
if (positional.length) {
|
const [pHost, pPort, pFile, pInterval] = positional;
|
|
if (pHost && args.host === '127.0.0.1') args.host = pHost;
|
if (pPort && args.port === 3021) {
|
const n = Number(pPort);
|
if (!Number.isNaN(n) && n > 0) args.port = n;
|
}
|
|
if (pFile && !args.fromHexFile && !args.fromBinFile) {
|
// Assume it's a hex dump text file like 结果全部.txt
|
args.fromHexFile = pFile;
|
}
|
|
if (pInterval && args.interval === 1000) {
|
const n = Number(pInterval);
|
if (!Number.isNaN(n) && n >= 0) args.interval = n;
|
}
|
}
|
|
return args;
|
}
|
|
function usage() {
|
// eslint-disable-next-line no-console
|
console.log(`\n用法: node .\\sim_server.js [options]\n\n常用:(从你抓到的透析机 hex 数据里取帧并发送)\n node .\\sim_server.js --port 3021 --hex-file .\\结果全部.txt --interval 1000\n\n选项:\n --host <ip> 监听地址 (默认 127.0.0.1)\n --port <n> 监听端口 (默认 3021)\n --interval <ms> 每帧发送间隔 (默认 1000)\n --jitter <ms> 每帧额外随机抖动 (0~jitter)\n --once 只发送一轮后停止\n\n --hex-file <path> 输入:hex 文本(可含空格/换行,或 RTF 包裹)\n --bin-file <path> 输入:原始二进制(抓包 .bin / payload.bin)\n --utf16le/--utf8 强制输入编码(默认自动猜测)\n\n --mode frames 按“帧”发送(默认):每次 write 一整帧(可能粘包取决于系统)\n --mode bytes 按“字节流”发送:把整段 bytes 拆成小块写出,模拟半包/粘包\n --chunk <n> mode=bytes 时每次写出 n 字节;0=一次性写出\n\n --debug 打印发送日志\n`);
|
}
|
|
function stripNonHex(text) {
|
return text
|
.replace(/0x/gi, '')
|
.replace(/\\x/gi, '')
|
.replace(/[^0-9a-fA-F]/g, '');
|
}
|
|
function extractLikelyHexBytesFromText(text) {
|
const candidates = text.match(/[0-9A-Fa-f]{2}(?:\s+[0-9A-Fa-f]{2}){40,}/g);
|
if (candidates && candidates.length) {
|
let best = candidates[0];
|
for (const c of candidates) if (c.length > best.length) best = c;
|
return best.split(/\s+/g).filter(Boolean).join('');
|
}
|
return stripNonHex(text);
|
}
|
|
function decodeHexTextToBytes(text) {
|
let cleaned = extractLikelyHexBytesFromText(text);
|
if (!cleaned || cleaned.length < 40) {
|
throw new Error('未在 hex-file 中提取到足够的 hex 字节');
|
}
|
if (cleaned.length % 2 !== 0) {
|
// eslint-disable-next-line no-console
|
console.warn('⚠️ hex 长度不是偶数,已丢弃最后 1 个半字节继续');
|
cleaned = cleaned.slice(0, -1);
|
}
|
return Buffer.from(cleaned, 'hex');
|
}
|
|
function loadBytes(args) {
|
if (args.fromBinFile) {
|
const p = path.resolve(process.cwd(), args.fromBinFile);
|
return fs.readFileSync(p);
|
}
|
|
if (args.fromHexFile) {
|
const p = path.resolve(process.cwd(), args.fromHexFile);
|
const text = fs.readFileSync(p, 'utf8');
|
return decodeHexTextToBytes(text);
|
}
|
|
// default: a tiny demo ORU_R31 frame in UTF-16LE
|
const demoXml = `<?xml version="1.0" encoding="UTF-8"?>\n<ORU_R31>\n <MSH><MSH.18>UNICODE</MSH.18></MSH>\n <ORU_R31.OBSERVATION>\n <OBX>\n <OBX.3><CE.1>13</CE.1><CE.2>Venous Pressure</CE.2></OBX.3>\n <OBX.5><FN.1>150</FN.1></OBX.5>\n <OBX.6><CE.1>mmHg</CE.1></OBX.6>\n </OBX>\n </ORU_R31.OBSERVATION>\n</ORU_R31>`;
|
return iconv.encode(demoXml, 'utf16le');
|
}
|
|
function splitBytes(bytes, chunkSize) {
|
if (!chunkSize || chunkSize <= 0) return [bytes];
|
const chunks = [];
|
for (let i = 0; i < bytes.length; i += chunkSize) {
|
chunks.push(bytes.slice(i, i + chunkSize));
|
}
|
return chunks;
|
}
|
|
function buildFrameBuffers(bytes, forcedEncoding) {
|
const encoding = forcedEncoding || guessTextEncodingFromBuffer(bytes);
|
if (encoding !== 'utf16le') {
|
throw new Error(`当前模拟器只支持 utf16le 帧拆分(检测到 encoding=${encoding})。如需发送 utf8,请先提供二进制或改造 framing。`);
|
}
|
|
const { frames, remainderBuffer } = extractFramesFromUtf16leBuffer(bytes);
|
if (frames.length === 0) {
|
throw new Error('未从输入中提取到任何 ORU_R31/ORF_R04 完整帧');
|
}
|
|
const frameBuffers = frames.map(f => iconv.encode(f, 'utf16le'));
|
return { frameBuffers, remainderBuffer };
|
}
|
|
async function sleep(ms) {
|
return new Promise(r => setTimeout(r, ms));
|
}
|
|
async function main() {
|
const args = parseArgs(process.argv);
|
if (args.help) { usage(); return; }
|
|
let bytes;
|
try {
|
bytes = loadBytes(args);
|
} catch (e) {
|
// eslint-disable-next-line no-console
|
console.error(`加载输入失败: ${e && e.message ? e.message : String(e)}`);
|
usage();
|
process.exitCode = 1;
|
return;
|
}
|
|
let frameBuffers = null;
|
let remainderBuffer = null;
|
if (args.mode === 'frames') {
|
try {
|
const built = buildFrameBuffers(bytes, args.encoding);
|
frameBuffers = built.frameBuffers;
|
remainderBuffer = built.remainderBuffer;
|
} catch (e) {
|
// eslint-disable-next-line no-console
|
console.error(`拆帧失败: ${e && e.message ? e.message : String(e)}`);
|
process.exitCode = 1;
|
return;
|
}
|
}
|
|
const server = net.createServer();
|
server.on('connection', (socket) => {
|
// eslint-disable-next-line no-console
|
console.log(`client connected: ${socket.remoteAddress}:${socket.remotePort}`);
|
socket.setNoDelay(true);
|
|
(async () => {
|
try {
|
if (args.mode === 'bytes') {
|
const chunks = splitBytes(bytes, args.chunk || 0);
|
// eslint-disable-next-line no-console
|
console.log(`send mode=bytes: total=${bytes.length} bytes, writes=${chunks.length} (chunk=${args.chunk || 0})`);
|
|
for (let i = 0; i < chunks.length; i++) {
|
socket.write(chunks[i]);
|
if (args.debug) {
|
// eslint-disable-next-line no-console
|
console.log(`write #${i + 1}: ${chunks[i].length} bytes`);
|
}
|
const wait = args.interval + (args.jitter ? Math.floor(Math.random() * (args.jitter + 1)) : 0);
|
if (wait > 0) await sleep(wait);
|
}
|
|
if (args.once) {
|
socket.end();
|
server.close();
|
}
|
return;
|
}
|
|
// frames
|
// eslint-disable-next-line no-console
|
console.log(`send mode=frames: frames=${frameBuffers.length}, interval=${args.interval}ms`);
|
if (remainderBuffer && remainderBuffer.length) {
|
// eslint-disable-next-line no-console
|
console.warn(`WARN: input has trailing ${remainderBuffer.length} bytes without a complete frame (capture may be truncated)`);
|
}
|
|
let round = 0;
|
while (!socket.destroyed) {
|
round++;
|
for (let i = 0; i < frameBuffers.length; i++) {
|
socket.write(frameBuffers[i]);
|
if (args.debug) {
|
// eslint-disable-next-line no-console
|
console.log(`round=${round} frame #${i + 1}: ${frameBuffers[i].length} bytes`);
|
}
|
const wait = args.interval + (args.jitter ? Math.floor(Math.random() * (args.jitter + 1)) : 0);
|
if (wait > 0) await sleep(wait);
|
}
|
|
if (args.once) {
|
socket.end();
|
server.close();
|
return;
|
}
|
}
|
} catch (e) {
|
// eslint-disable-next-line no-console
|
console.error(`发送失败: ${e && e.message ? e.message : String(e)}`);
|
try { socket.destroy(); } catch (_) {}
|
}
|
})();
|
|
socket.on('close', () => {
|
// eslint-disable-next-line no-console
|
console.log('client closed');
|
});
|
|
socket.on('error', (err) => {
|
// eslint-disable-next-line no-console
|
console.log(`socket error: ${err.message}`);
|
});
|
});
|
|
server.on('error', (err) => {
|
// eslint-disable-next-line no-console
|
console.error(`server error: ${err.message}`);
|
process.exitCode = 1;
|
});
|
|
server.listen(args.port, args.host, () => {
|
// eslint-disable-next-line no-console
|
console.log(`sim server listening: ${args.host}:${args.port}`);
|
// eslint-disable-next-line no-console
|
console.log(`client connect example: node .\\index.js --host ${args.host} --port ${args.port}`);
|
|
const src = args.fromBinFile ? `bin:${args.fromBinFile}` : (args.fromHexFile ? `hex:${args.fromHexFile}` : 'demo');
|
// eslint-disable-next-line no-console
|
console.log(`source=${src}; mode=${args.mode}; interval=${args.interval}ms; chunk=${args.chunk || 0}`);
|
});
|
}
|
|
main();
|