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);
|