# 使用说明 ## TCP 服务配置(tcp-config.json) - host: 监听地址,默认 `0.0.0.0` - port: 监听端口,默认 `8234` - maxClients: 最大并发连接数,默认 `50` - getDataIntervalMs: 获取治疗数据指令的下发周期(毫秒),默认 `60000`,且不低于 `1000` - activateIntervalMs: 保活激活指令的下发周期(毫秒),默认 `600000`,且不低于 `60000` 说明:打包后的可执行程序会优先读取与可执行文件同目录下的 `tcp-config.json`(exedir),开发模式下读取源码目录(srcdir)。如果找不到配置文件,则使用默认值。 启动时会在控制台打印: - TCP 配置来源与路径(exedir/srcdir/default) - 指令编码(激活与取数) - 周期间隔(毫秒) ## HTTP API(http-config.json) - enabled: 是否启用 HTTP 服务,默认 `false` - host: 监听地址,默认 `0.0.0.0` - port: 端口,默认 `8080` 启用后服务会打印 HTTP 配置来源与监听地址。 ## 运行与停止 开发模式运行: ```powershell node .\fresenius-tcp-server.js ``` 停止:在运行该进程的终端使用 `Ctrl+C` 中断。 打包运行请参考 `package.json` 中的打包命令,确保将 `tcp-config.json`、`http-config.json` 与可执行文件放在同一目录以便配置生效。 --- const { SerialPort } = require('serialport'); // ====== 配置区(请根据实际情况修改)====== const PORT_NAME = 'COM5'; // ← 修改为你的串口号 const BAUD_RATE = 2400; // Fresenius 4008S 标准波特率 const GET_DATA_INTERVAL = 60000; // 每 60 秒请求治疗数据 const ACTIVATE_INTERVAL = 600000; // 每 10 分钟发送激活保活指令 // ========================================= const ACTIVATE_CMD = '16 2f 40 31 04 03'; const GET_DATA_CMD = '16 31 3f 43 44 04 03'; let port; let isPortOpen = false; let receiveBuffer = Buffer.alloc(0); let detectedSerial = null; // 缓存已识别的序列号 const FRAME_END = Buffer.from([0x06, 0x03]); // 工具:HEX 字符串转 Buffer function hexToBuffer(hexStr) { const hex = hexStr.replace(/\s+/g, ''); const bytes = []; for (let i = 0; i < hex.length; i += 2) { bytes.push(parseInt(hex.substr(i, 2), 16)); } return Buffer.from(bytes); } // 发送命令 function sendCommand(cmdHex, label) { if (!isPortOpen) return; const buf = hexToBuffer(cmdHex); console.log(`📤 [${new Date().toLocaleTimeString()}] 发送 ${label}: ${cmdHex}`); port.write(buf); } // 提取临床参数(字母+数值) function extractAllFields(frame) { const ascii = frame.toString('ascii'); const fields = {}; const regex = /([A-Z])([+-]?\d{3,6})/g; let match; while ((match = regex.exec(ascii)) !== null) { const tag = match[1]; const valueStr = match[2]; const num = parseInt(valueStr, 10); if (!isNaN(num) && !(tag in fields)) { fields[tag] = num; } } return fields; } // 尝试提取设备序列号(8位大写字母/数字,出现在帧前部) /** * 精准提取 Fresenius 4008S 设备序列号 * 规则:位于最后一个 I 字段 和 第一个后续 S 字段 之间的 8 位字符串 * @param {Buffer} frame - 完整数据帧 Buffer * @returns {string|null} 序列号 或 null */ function extractSerialNumber(frame) { const ascii = frame.toString('ascii'); // 1. 找到最后一个 "I" 字段的位置(I 后跟数字) const iMatches = [...ascii.matchAll(/I[+-]?\d+/g)]; if (iMatches.length === 0) return null; const lastIMatch = iMatches[iMatches.length - 1]; const iEndIndex = lastIMatch.index + lastIMatch[0].length; // 2. 从 I 结束处向后查找第一个 "S" 字段 const sMatch = ascii.slice(iEndIndex).match(/S[+-]?\d+/); if (!sMatch) return null; const sStartIndex = iEndIndex + sMatch.index; // 3. 提取 I 与 S 之间的子串 const between = ascii.substring(iEndIndex, sStartIndex).trim(); // 4. 验证是否为 8 位大写字母/数字,且含字母+数字 if (/^[A-Z0-9]{8}$/.test(between) && /[A-Z]/.test(between) && /\d/.test(between)) { return between; } return null; } // 打印临床参数 function printClinicalParams(params) { console.log('\n🩺 临床参数解析(基于实机对照 - 最新版):'); const known = {}; if ('V' in params) { console.log(` • 静脉压: ${params.V} mmHg`); known.V = true; } if ('A' in params) { console.log(` • 动脉压: ${params.A} mmHg`); known.A = true; } if ('U' in params) { console.log(` • 跨膜压 (TMP): ${params.U} mmHg`); known.U = true; } if ('T' in params) { console.log(` • 透析液温度: ${(params.T / 10).toFixed(1)} °C`); known.T = true; } if ('C' in params) { console.log(` • 电导率: ${(params.C / 10).toFixed(1)} mS/cm`); known.C = true; } if ('I' in params) { console.log(` • 实际透析液流量: ${params.I} mL/min`); known.I = true; } if ('Q' in params) { console.log(` • 有效血流量: ${params.Q} mL/min`); known.Q = true; } if ('R' in params) { console.log(` • 超滤速率: ${params.R} mL/h`); known.R = true; } if ('P' in params) { console.log(` • 已完成超滤量: ${params.P} mL`); known.P = true; } if ('G' in params) { console.log(` • 超滤目标量: ${params.G} mL`); known.G = true; } if ('H' in params) { const mins = params.H; console.log(` • 剩余治疗时间: ${mins} 分钟 (${Math.floor(mins / 60)}h ${mins % 60}m)`); known.H = true; } if ('N' in params) { console.log(` • 钠浓度: ${(params.N / 10).toFixed(1)} mmol/L`); known.N = true; } if ('Y' in params) { const vol = params.Y; const display = vol >= 10000 ? `${(vol / 1000).toFixed(1)} L` : `${vol} mL`; console.log(` • 累计血容量: ${display}`); known.Y = true; } const maybe = { B: '血泵设定值?', S: '备用流量?', X: '静脉壶状态?', M: '总治疗时间?', L: '肝素累计?', Z: 'TMP补偿?' }; for (const [tag, desc] of Object.entries(maybe)) { if (tag in params && !(tag in known)) { console.log(` • ${desc} (${tag}): ${params[tag]}`); known[tag] = true; } } const unknownTags = Object.keys(params).filter(t => !(t in known)); if (unknownTags.length > 0) { console.log(` • 未识别字段: ${unknownTags.map(t => `${t}=${params[t]}`).join(', ')}`); } if (Object.keys(known).length === 0 && unknownTags.length === 0) { console.log(' ❌ 未解析到任何有效参数'); } } // 处理完整数据帧 function handleFrame(frame) { const hexStr = frame.toString('hex').replace(/(.{2})/g, '$1 ').trim().toUpperCase(); const asciiFull = frame.toString('ascii').replace(/[^\x20-\x7E]/g, '.'); console.log(`\n📥 [${new Date().toLocaleTimeString()}] 接收到完整数据帧`); console.log(` HEX (${frame.length} bytes): ${hexStr}`); console.log(` ASCII: "${asciiFull}"`); if (frame.length < 15) { console.log(' └─ ⚠️ 忽略短帧(ACK)'); return; } // === 提取并显示设备序列号(仅首次)=== if (!detectedSerial) { const serial = extractSerialNumber(frame); if (serial) { detectedSerial = serial; console.log(` 🏷️ 设备序列号: ${serial}`); } } const params = extractAllFields(frame); printClinicalParams(params); } // 初始化串口 async function initSerial() { try { port = new SerialPort({ path: PORT_NAME, baudRate: BAUD_RATE, autoOpen: false }); await new Promise((resolve, reject) => { port.open(err => { if (err) return reject(err); isPortOpen = true; console.log(`✅ 串口 ${PORT_NAME} 已打开,波特率 ${BAUD_RATE}`); resolve(); }); }); port.on('data', (chunk) => { receiveBuffer = Buffer.concat([receiveBuffer, chunk]); let endIndex; while ((endIndex = receiveBuffer.indexOf(FRAME_END)) !== -1) { const frame = receiveBuffer.subarray(0, endIndex + FRAME_END.length); handleFrame(frame); receiveBuffer = receiveBuffer.subarray(endIndex + FRAME_END.length); } if (receiveBuffer.length > 2048) { console.warn('⚠️ 接收缓冲区异常增长,已清空'); receiveBuffer = Buffer.alloc(0); } }); // === 启动通信流程 === sendCommand(ACTIVATE_CMD, '激活指令(初始)'); setInterval(() => { sendCommand(ACTIVATE_CMD, '激活指令(保活)'); }, ACTIVATE_INTERVAL); setInterval(() => { sendCommand(GET_DATA_CMD, '获取治疗数据'); }, GET_DATA_INTERVAL); } catch (err) { console.error('❌ 串口初始化失败:', err.message); process.exit(1); } } // 优雅关闭 function shutdown() { console.log('\n🛑 正在关闭...'); if (port && isPortOpen) { port.close(() => { console.log('🔌 串口已关闭'); process.exit(0); }); } else { process.exit(0); } } // 启动程序 console.log('🩺 Fresenius 4008S 全功能监控工具(含序列号识别 + 10分钟保活)'); console.log(`⚙️ 配置: ${PORT_NAME} @ ${BAUD_RATE} bps`); console.log(`🕒 数据请求: 每 ${GET_DATA_INTERVAL / 1000} 秒`); console.log(`🔁 激活保活: 每 ${ACTIVATE_INTERVAL / 60000} 分钟\n`); initSerial(); process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown);