费森4008s 网口通讯 ,原生串口透传网口
chenyc
2026-03-22 106a1256ccec6feef931d57923474180fa1a5ade
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
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);