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 监听地址 (默认 127.0.0.1)\n --port 监听端口 (默认 3021)\n --interval 每帧发送间隔 (默认 1000)\n --jitter 每帧额外随机抖动 (0~jitter)\n --once 只发送一轮后停止\n\n --hex-file 输入:hex 文本(可含空格/换行,或 RTF 包裹)\n --bin-file 输入:原始二进制(抓包 .bin / payload.bin)\n --utf16le/--utf8 强制输入编码(默认自动猜测)\n\n --mode frames 按“帧”发送(默认):每次 write 一整帧(可能粘包取决于系统)\n --mode bytes 按“字节流”发送:把整段 bytes 拆成小块写出,模拟半包/粘包\n --chunk 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 = `\n\n UNICODE\n \n \n 13Venous Pressure\n 150\n mmHg\n \n \n`; 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();