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