serialportnpm install serialport
编辑 app.js 中的配置区:
const PORT_NAME = 'COM5'; // 修改为实际的串口号
const BAUD_RATE = 2400; // Fresenius 4008S 标准波特率
const GET_DATA_INTERVAL = 30000; // 数据请求间隔(毫秒)
const ACTIVATE_INTERVAL = 30000; // 激活保活间隔(毫秒)
node app.js
Fresenius 4008S
↓
RS-232 9针连接器
↓
USB-to-Serial 转换器 (CH340/PL2303等)
↓
计算机 USB 端口
Fresenius 4008S (DB-9)
↓
串口线 (RS-232)
↓
计算机 COM 端口 (DB-9)
| 引脚 | 名称 | 功能 |
|---|---|---|
| 1 | DCD | 数据载体检测 |
| 2 | RXD | 接收数据 |
| 3 | TXD | 发送数据 |
| 4 | DTR | 数据终端就绪 |
| 5 | GND | 地线 |
| 6 | DSR | 数据集就绪 |
| 7 | RTS | 请求发送 |
| 8 | CTS | 允许发送 |
| 9 | RI | 振铃指示 |
最小连接 (3 线制):
- 引脚 2 (RXD) ← 设备 TXD
- 引脚 3 (TXD) → 设备 RXD
- 引脚 5 (GND) ↔ 设备 GND
Win + R 打开运行devmgmt.msc 打开设备管理器ls /dev/ttyUSB*
# 或
ls /dev/ttyACM*
ls /dev/tty.usb*
# 或
ls /dev/cu.usb*
┌─────────┬──────┬────────────┬─────────┬──────────────────────────┬─────────┐
│ 帧头 │ 帧号 │ 模式标识 │ 分隔符 │ 数据区 (临床参数) │ 帧尾 │
├─────────┼──────┼────────────┼─────────┼──────────────────────────┼─────────┤
│ 0x16 │ 0x31 │ 0x5F 0x43 │ 0x02 │ [A-Z][±数值]{3,6}... │ 0x06 │
│ `.` │ `1` │ `_C` │ `.` │ 字段1 字段2 ... │ 0x03 │
│ (1 byte)│ │ (3 bytes) │ (1 byte)│ (多字节) │ (2 byte)│
└─────────┴──────┴────────────┴─────────┴──────────────────────────┴─────────┘
[字段标记][符号可选][数值]
示例:
A006 → 动脉压 = 6 mmHg
T0325 → 温度 = 32.5°C (需 ÷10)
U+087 → TMP = 87 mmHg
N-140 → 负值参数 = -140
| 标记 | 名称 | 单位 | 范围 | 备注 |
|---|---|---|---|---|
| A | 动脉压 | mmHg | 0-300 | 血流入压力 |
| V | 静脉压 | mmHg | 0-300 | 血流出压力 |
| U | 跨膜压(TMP) | mmHg | 0-200 | 超滤驱动力 |
| T | 温度 | 0.1°C | 0-500 | 需 ÷10 |
| C | 电导率 | 0.1 mS/cm | 0-2000 | 需 ÷10 |
| I | 透析液流量 | mL/min | 0-1000 | 首个I为流量 |
| Q | 有效血流量 | mL/min | 0-500 | 实际通过的血流 |
| R | 超滤速率 | mL/h | 0-5000 | 当前超滤速度 |
| P | 已超滤量 | mL | 0-10000 | 累计超滤量 |
| G | 超滤目标量 | mL | 0-10000 | 计划超滤量 |
| H | 剩余治疗时间 | 分钟 | 0-300 | 分钟制 |
| N | 钠浓度 | 0.1 mmol/L | 0-2000 | 需 ÷10 |
| B | 血泵设定值 | mL/min | 0-500 | 可多次出现 |
| L | 肝素累计 | 单位 | 0-10000 | 用药记录 |
| X | 静脉壶状态 | 状态码 | 0-10 | 传感器状态 |
| M | 总治疗时间 | 分钟 | 0-600 | 计划时长 |
| Z | TMP补偿 | mmHg | -100-100 | 自动调节值 |
| S | 系统状态码 | 十进制 | 0-999 | 位字段状态 |
| Y | 累计血容量 | mL/L | 0-99999 | 可需 ÷1000 |
HEX: 16 2f 40 31 04 03
说明: 保持设备在线,防止超时断开
间隔: 每 30 秒
HEX: 16 31 3f 43 44 04 03
说明: 请求设备报告当前临床参数
间隔: 每 30 秒
┌─────────────────────────────────────────────────────┐
│ 程序启动 │
└────────────────────┬────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 打开串口 (COM5 @ 2400 bps) │
└────────────────────┬────────────────────────────────┘
↓
┌────────────────────────┐
│ 发送激活指令 (立即) │
└────────────────┬───────┘
↓
┌────────────────────────────────────────┐
│ 启动两个定时器: │
│ • 激活保活: 每 30 秒 │
│ • 数据请求: 每 30 秒 │
└────────────────┬───────────────────────┘
↓
┌────────────────────────────────────────┐
│ 等待接收数据 │
│ (持续监听串口) │
└────────┬─────────────────────┬─────────┘
↓ ↓
┌─────────────────┐ ┌──────────────────┐
│ 收到完整帧 │ │ 定时器触发 │
│ (0x06 0x03) │ │ 发送命令 │
└────────┬────────┘ └──────┬───────────┘
↓ ↓
┌──────────────────────────────────────┐
│ 解析数据: │
│ 1. 提取序列号 │
│ 2. 提取临床参数 │
│ 3. 格式化显示 │
└──────────────────────────────────────┘
const PORT_NAME = 'COM5'; // 串口号(必须配置)
const BAUD_RATE = 2400; // 波特率 (Fresenius 标准)
const GET_DATA_INTERVAL = 30000; // 30秒获取一次数据
const ACTIVATE_INTERVAL = 30000; // 30秒发送一次保活信号
参数说明:
- PORT_NAME: Windows 下通常为 COM1-COM9;Linux/Mac 为 /dev/ttyUSB0 等
- BAUD_RATE: 不要修改,透析机固定 2400 bps
- 时间间隔: 毫秒制,30000 = 30 秒
功能: 将 HEX 字符串转换为 Buffer
输入: "16 2f 40 31 04 03"
输出: <Buffer 16 2f 40 31 04 03>
功能: 向设备发送命令
参数:
- cmdHex: HEX 格式命令字符串
- label: 日志标签
示例:
sendCommand('16 2f 40 31 04 03', '激活指令');
功能: 从数据帧提取设备序列号
规则: 第二个 "I" 后跟的 8 位大小写字母/数字
输入: Buffer (完整数据帧)
输出: "7VCA0Y82" 或 null
功能: 从数据帧提取所有临床参数
正则: /([A-Z])([+-]?\d{3,6})/g
输出: {A: 6, V: 0, U: 87, T: 325, C: 105, ...}
功能: 格式化显示临床参数
处理:
1. 已知参数的单位转换和显示
2. 未知参数的列出
3. 异常情况提醒
功能: 处理接收到的完整数据帧
流程:
1. 显示 HEX 和 ASCII
2. 过滤短帧 (<15 bytes)
3. 提取序列号
4. 提取并显示临床参数
port.on('data', (chunk) => {
receiveBuffer = Buffer.concat([receiveBuffer, chunk]);
// 查找帧尾 (0x06 0x03)
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);
}
});
process.on('SIGINT', shutdown); // Ctrl+C
process.on('SIGTERM', shutdown); // 系统关闭信号
function shutdown() {
if (port && isPortOpen) {
port.close(() => {
console.log('串口已关闭');
process.exit(0);
});
}
}
确认硬件连接
✓ 串口线已连接 ✓ 设备已通电 ✓ 驱动程序已安装
查找串口号
ls /dev/ttyUSB*编辑配置
javascript const PORT_NAME = 'COM5'; // 改为实际串口号
运行程序
bash node app.js
观察输出
✅ 串口 COM5 已打开,波特率 2400 📤 [时间] 发送 激活指令(初始) 📥 [时间] 接收到完整数据帧 🏷️ 设备序列号: 7VCA0Y82 🩺 临床参数解析
| 符号 | 含义 | 说明 |
|---|---|---|
| 📤 | 发送 | 向设备发送命令 |
| 📥 | 接收 | 从设备接收数据 |
| 🏷️ | 标签 | 设备标识符 |
| 🩺 | 医疗 | 临床参数显示 |
| ✅ | 成功 | 操作成功 |
| ⚠️ | 警告 | 需要注意的事项 |
| ❌ | 错误 | 出现故障 |
原因:
- 串口号不正确
- 驱动程序未安装
- 串口被其他程序占用
解决:
1. 检查设备管理器中的实际串口号
2. 使用 USB 适配器时安装驱动 (CH340/PL2303)
3. 关闭其他串口监听程序
原因:
- 设备未通电或离线
- 硬件连接不良
- 波特率设置错误
解决:
1. 检查设备屏幕是否正常显示
2. 检查串口线两端是否牢固
3. 确认波特率为 2400 bps
原因:
- 波特率设置不匹配
- 数据传输受干扰
解决:
1. 确认波特率为 2400
2. 使用屏蔽串口线
3. 重启设备和程序
原因:
- 数据帧格式异常
- 设备返回不同格式的数据
解决:
1. 检查 ASCII 输出中是否有 "I...S" 的模式
2. 查看数据帧完整性
3. 修改正则表达式规则
const port = new SerialPort({
path: PORT_NAME,
baudRate: BAUD_RATE,
autoOpen: false,
highWaterMark: 256 // 增加缓冲区大小
});
// 如果数据太密集,增加间隔
const GET_DATA_INTERVAL = 60000; // 改为 60 秒
const ACTIVATE_INTERVAL = 120000; // 改为 120 秒
// 注释掉冗长的日志
// console.log(` └─ 尝试提取序列号,完整 ASCII: "${ascii}"`);
接收数据: A006V000U087T0325C0105
解析结果:
A = 006 → 动脉压 = 6 mmHg
V = 000 → 静脉压 = 0 mmHg
U = 087 → TMP = 87 mmHg
T = 0325 → 温度 = 32.5°C (÷10)
C = 0105 → 电导率 = 10.5 mS/cm (÷10)
接收数据: Q200I500R100P2000G5000
解析结果:
Q = 200 → 血流量 = 200 mL/min
I = 500 → 液流量 = 500 mL/min
R = 100 → 超滤速率 = 100 mL/h
P = 2000 → 已超滤 = 2000 mL
G = 5000 → 目标 = 5000 mL
接收数据: H180M240L4953
解析结果:
H = 180 → 剩余时间 = 180 分钟 (3 小时)
M = 240 → 总时间 = 240 分钟 (4 小时)
L = 4953 → 肝素 = 4953 单位
接收数据: A+150V-050U+087
解析结果:
A = +150 → 动脉压 = 150 mmHg
V = -050 → 静脉压 = -50 mmHg (异常)
U = +087 → TMP = 87 mmHg
// 添加云平台上报
function reportToCloud(params, serial) {
const payload = {
device_id: serial,
timestamp: new Date().toISOString(),
blood_pressure_a: params.A,
blood_pressure_v: params.V,
tmp: params.U,
temperature: params.T / 10,
conductivity: params.C / 10,
blood_flow: params.Q,
ultrafiltration_rate: params.R,
treatment_time_remaining: params.H
};
// POST 到服务器
// fetch('https://api.example.com/dialysis', {
// method: 'POST',
// body: JSON.stringify(payload)
// });
}
function checkAlarms(params) {
const alarms = [];
// 血压异常
if (params.A > 200) alarms.push('⚠️ 动脉压过高');
if (params.V > 200) alarms.push('⚠️ 静脉压过高');
// 温度异常
if (params.T < 350 || params.T > 380) alarms.push('⚠️ 温度异常');
// 电导率异常
if (params.C < 100 || params.C > 140) alarms.push('⚠️ 电导率异常');
// 血流中断
if (params.Q === 0 && params.H > 0) alarms.push('🚨 血流中断');
return alarms;
}
const fs = require('fs');
function logToFile(params, serial) {
const log = {
timestamp: new Date().toISOString(),
device: serial,
...params
};
fs.appendFileSync('dialysis_log.jsonl',
JSON.stringify(log) + '\n'
);
}
// 添加诊断命令
function diagnose() {
console.log('=== 系统诊断 ===');
console.log(`✓ 串口状态: ${isPortOpen ? '开启' : '关闭'}`);
console.log(`✓ 缓冲区大小: ${receiveBuffer.length} bytes`);
console.log(`✓ 设备序列号: ${detectedSerial || '未识别'}`);
console.log(`✓ 最后接收时间: ${new Date().toLocaleTimeString()}`);
}
function validateFrame(frame) {
const checks = {
frameStart: frame[0] === 0x16,
frameEnd: frame[frame.length-2] === 0x06 && frame[frame.length-1] === 0x03,
minLength: frame.length >= 15,
hasData: frame.length > 20
};
return checks;
}
let lastReceiveTime = Date.now();
const TIMEOUT = 60000; // 60秒超时
setInterval(() => {
if (Date.now() - lastReceiveTime > TIMEOUT) {
console.warn('⚠️ 长时间无数据接收,设备可能离线');
// 可选:自动重新连接
// reconnect();
}
}, 10000);
const devices = [
{ name: 'Device1', port: 'COM5', baud: 2400 },
{ name: 'Device2', port: 'COM6', baud: 2400 }
];
devices.forEach(dev => {
initSerialForDevice(dev);
});
const recordings = [];
function recordFrame(frame, params) {
recordings.push({
time: Date.now(),
frame: frame.toString('hex'),
params: params
});
}
function playback(index) {
const rec = recordings[index];
console.log(`回放: ${new Date(rec.time).toLocaleTimeString()}`);
console.log(rec.params);
}
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/api/status') {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify({
device: detectedSerial,
status: 'online'
}));
}
});
server.listen(3000);
⚠️ 重要: 本工具用于数据采集,不应用于临床决策
| 速率 | 说明 |
|---|---|
| 300 | 极低速,不适用 |
| 1200 | 低速通讯 |
| 2400 | Fresenius 标准 ✓ |
| 9600 | 常见速率 |
| 19200 | 高速通讯 |
| 115200 | 超高速 |
| HEX | ASCII | 含义 |
|---|---|---|
| 0x06 | ACK | 确认字符 |
| 0x03 | ETX | 文本结束 |
帧尾 06 03 表示数据帧结束和传输完成确认。
| 字段 | 类型 | 范围 | 转换 | 临床意义 |
|---|---|---|---|---|
| A | 压力 | 0-300 | ×1 | 血液流入压 |
| V | 压力 | 0-300 | ×1 | 血液流出压 |
| U | 压力 | 0-200 | ×1 | 超滤驱动压 |
| T | 温度 | 0-500 | ÷10 | 透析液温度 |
| C | 导率 | 0-2000 | ÷10 | 液体电解质 |
| I | 流量 | 0-1000 | ×1 | 液体流速 |
| Q | 流量 | 0-500 | ×1 | 血液流速 |
| R | 速率 | 0-5000 | ×1 | 超滤速度 |
| P | 量 | 0-10000 | ×1 | 已去除液体 |
| G | 量 | 0-10000 | ×1 | 目标去除量 |
| H | 时间 | 0-300 | ×1 | 剩余分钟数 |
| N | 浓度 | 0-2000 | ÷10 | 血清钠浓度 |
| B | 设定 | 0-500 | ×1 | 血泵速度 |
| L | 量 | 0-10000 | ×1 | 肝素用量 |
问题: [描述问题]
时间: [问题发生时间]
日志: [相关日志输出]
配置: [使用的配置参数]
硬件: [硬件连接方式]
# 查看已有 Node 进程
ps aux | grep node
# 杀死进程
kill -9 <PID>
# 实时查看日志
tail -f app.log
# 后台运行
nohup node app.js > app.log 2>&1 &
# 开启详细日志
NODE_DEBUG=* node app.js
文档版本: 1.0
更新时间: 2025-12-11
适用版本: app.js v1.0+
设备型号: Fresenius 4008S
通讯方式: RS-232 串口 (2400 bps)