0.0.0.082345060000,且不低于 1000600000,且不低于 60000说明:打包后的可执行程序会优先读取与可执行文件同目录下的 tcp-config.json(exedir),开发模式下读取源码目录(srcdir)。如果找不到配置文件,则使用默认值。
启动时会在控制台打印:
- TCP 配置来源与路径(exedir/srcdir/default)
- 指令编码(激活与取数)
- 周期间隔(毫秒)
false0.0.0.08080启用后服务会打印 HTTP 配置来源与监听地址。
开发模式运行:
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);