const { SerialPort } = require('serialport'); // ====== 配置区(请根据实际情况修改)====== const PORT_NAME = 'COM7'; // ← 修改为你的串口号 const BAUD_RATE = 2400; // Fresenius 4008S 标准波特率 const GET_DATA_INTERVAL = 30000; // 每 60 秒请求治疗数据 const ACTIVATE_INTERVAL = 30000; // 每 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" 后面紧跟 8 位大小写字母/数字 * @param {Buffer} frame - 完整数据帧 Buffer * @returns {string|null} 序列号 或 null */ function extractSerialNumber(frame) { const ascii = frame.toString('ascii'); console.log(` └─ 尝试提取序列号,完整 ASCII: "${ascii}"`); // 找到所有 "I" 后跟 8 位字母数字的匹配 const iMatches = [...ascii.matchAll(/I[A-Za-z0-9]{8}(?=[A-Z])/g)]; console.log(` • 找到 ${iMatches.length} 个匹配的 I 字段`); if (iMatches.length < 2) { console.log(` • 未找到第二个 I 字段`); return null; } // 取第二个匹配 const secondMatch = iMatches[1]; const serial = secondMatch[0].slice(1); // 去掉前面的 "I" console.log(` • 找到序列号: "${serial}"`); // 验证是否为有效格式(包含字母和数字) if (/[A-Za-z]/.test(serial) && /\d/.test(serial)) { return serial; } 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);