const net = require('net');
|
|
// ====== 配置区(请根据实际情况修改)======
|
const HOST = '0.0.0.0'; // ← 修改为透析机的IP地址
|
const PORT = 8234; // 固定TCP端口
|
const GET_DATA_INTERVAL = 30000; // 每 30 秒请求治疗数据
|
const ACTIVATE_INTERVAL = 30000; // 每 30 秒发送激活保活指令
|
// =========================================
|
|
const ACTIVATE_CMD = '16 2f 40 31 04 03';
|
const GET_DATA_CMD = '16 31 3f 43 44 04 03';
|
|
let socket;
|
let isConnected = 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);
|
}
|
|
// 发送命令(TCP)
|
function sendCommand(cmdHex, label) {
|
if (!isConnected) return;
|
const buf = hexToBuffer(cmdHex);
|
console.log(`📤 [${new Date().toLocaleTimeString()}] 发送 ${label}: ${cmdHex}`);
|
socket.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;
|
}
|
|
// 设备序列号提取:第二个 I 后跟8位字母数字
|
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🩺 临床参数解析(TCP 通讯):');
|
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);
|
}
|
|
// 初始化 TCP 连接
|
function initTCP() {
|
socket = new net.Socket();
|
|
socket.on('connect', () => {
|
isConnected = true;
|
console.log(`✅ TCP 已连接: ${HOST}:${PORT}`);
|
|
// 初始激活
|
sendCommand(ACTIVATE_CMD, '激活指令(初始)');
|
});
|
|
socket.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 > 1024 * 1024) { // 防护:1MB
|
console.warn('⚠️ 接收缓冲区异常增长,已清空');
|
receiveBuffer = Buffer.alloc(0);
|
}
|
});
|
|
socket.on('close', () => {
|
if (isConnected) console.log('🔌 TCP 连接已关闭');
|
isConnected = false;
|
// 尝试重连
|
setTimeout(connectTCP, 3000);
|
});
|
|
socket.on('error', (err) => {
|
console.error('❌ TCP 错误:', err.message);
|
});
|
|
connectTCP();
|
}
|
|
function connectTCP() {
|
try {
|
console.log(`🔗 正在连接 ${HOST}:${PORT} ...`);
|
socket.connect(PORT, HOST);
|
} catch (err) {
|
console.error('❌ 连接创建失败:', err.message);
|
setTimeout(connectTCP, 3000);
|
}
|
}
|
|
// 定时器:保活与数据请求
|
function startTimers() {
|
setInterval(() => {
|
sendCommand(ACTIVATE_CMD, '激活指令(保活)');
|
}, ACTIVATE_INTERVAL);
|
|
setInterval(() => {
|
sendCommand(GET_DATA_CMD, '获取治疗数据');
|
}, GET_DATA_INTERVAL);
|
}
|
|
// 优雅关闭
|
function shutdown() {
|
console.log('\n🛑 正在关闭...');
|
if (socket && isConnected) {
|
socket.end(() => {
|
console.log('🔌 TCP 已断开');
|
process.exit(0);
|
});
|
} else {
|
process.exit(0);
|
}
|
}
|
|
// 启动程序
|
console.log('🩺 Fresenius 4008S TCP 通讯监控工具(含序列号识别 + 保活)');
|
console.log(`🌐 目标: ${HOST}:${PORT}`);
|
console.log(`🕒 数据请求: 每 ${GET_DATA_INTERVAL / 1000} 秒`);
|
console.log(`🔁 激活保活: 每 ${ACTIVATE_INTERVAL / 1000} 秒\n`);
|
|
initTCP();
|
startTimers();
|
|
process.on('SIGINT', shutdown);
|
process.on('SIGTERM', shutdown);
|