// fresenius-serial-service.js
|
const { SerialPort } = require('serialport');
|
|
class FreseniusSerialService {
|
constructor(options = {}) {
|
this.portPath = options.portPath || 'COM5'; // 默认 Windows 串口
|
this.baudRate = options.baudRate || 2400;
|
this.pollInterval = options.pollInterval || 1000;
|
|
this.port = null;
|
this.isActivated = false; // 标记是否已成功激活
|
this.isPolling = false;
|
this.reconnectTimer = null;
|
|
// === 严格按照你指定的命令 ===
|
this.ACTIVATE_CMD = Buffer.from([0x16, 0x2F, 0x40, 0x31, 0x04, 0x03]); // . / @ 1 <EOT><ETX>
|
this.CD_QUERY_CMD = Buffer.from([0x16, 0x31, 0x3F, 0x43, 0x44, 0x04, 0x03]); // . 1 ? C D <EOT><ETX>
|
|
this.onData = options.onData || ((buf) => {});
|
this.onError = options.onError || console.error;
|
this.onLog = options.onLog || ((msg) => console.log(`[${new Date().toISOString()}] ${msg}`));
|
}
|
|
async open() {
|
try {
|
this.onLog(`🔌 正在打开串口: ${this.portPath}`);
|
this.port = new SerialPort({
|
path: this.portPath,
|
baudRate: this.baudRate,
|
dataBits: 8,
|
stopBits: 1,
|
parity: 'none',
|
autoOpen: false,
|
highWaterMark: 1024,
|
});
|
|
await new Promise((resolve, reject) => {
|
this.port.open((err) => (err ? reject(err) : resolve()));
|
});
|
|
// 👇 打印所有原始输入(关键调试信息)
|
this.port.on('data', (buffer) => {
|
const hex = buffer.toString('hex').toUpperCase();
|
this.onLog(`📥 原始输入 (${buffer.length} 字节): ${hex}`);
|
});
|
|
// 仅在底层错误或设备拔出时重连
|
this.port.on('error', (err) => {
|
this.onError(`🚨 串口错误: ${err.message}`);
|
this.scheduleReconnect();
|
});
|
this.port.on('close', () => {
|
this.onLog('🔌 串口连接断开,准备重连...');
|
this.scheduleReconnect();
|
});
|
|
this.onLog(`✅ 串口 ${this.portPath} 已就绪`);
|
this.startCommunication();
|
} catch (err) {
|
this.onError(`❌ 无法打开串口: ${err.message}`);
|
this.scheduleReconnect(3000);
|
}
|
}
|
|
scheduleReconnect(delay = 3000) {
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
this.reconnectTimer = setTimeout(() => {
|
this.close();
|
this.open();
|
}, delay);
|
}
|
|
close() {
|
this.isPolling = false;
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
if (this.port?.isOpen) this.port.close();
|
}
|
|
sleep(ms) {
|
return new Promise(resolve => setTimeout(resolve, ms));
|
}
|
|
// 发送命令并等待有效响应(自动合并分片)
|
async sendCommand(cmd, validator, timeout = 2000) {
|
return new Promise((resolve, reject) => {
|
const timer = setTimeout(() => {
|
this.port.off('data', onData);
|
reject(new Error('响应超时'));
|
}, timeout);
|
|
const chunks = [];
|
const onData = (data) => {
|
chunks.push(data);
|
const full = Buffer.concat(chunks);
|
if (validator(full)) {
|
clearTimeout(timer);
|
this.port.off('data', onData);
|
resolve(full);
|
}
|
};
|
|
this.port.on('data', onData);
|
this.port.write(cmd);
|
this.onLog(`📤 发送命令: ${cmd.toString('hex').toUpperCase()}`);
|
});
|
}
|
|
// 判断是否为有效的 CD 响应帧
|
isValidCDResponse(data) {
|
// 费森尤斯 CD 响应通常以 STX (0x02) 开头,长度 > 80 字节
|
return data.length > 80 && data[0] === 0x02;
|
}
|
|
async startCommunication() {
|
this.isPolling = true;
|
|
while (this.isPolling && this.port?.isOpen) {
|
try {
|
// 第一步:如果未激活,先发送一次激活命令
|
if (!this.isActivated) {
|
this.onLog('🔑 发送激活命令 (仅一次)...');
|
this.port.write(this.ACTIVATE_CMD);
|
this.onLog(`📤 激活命令: ${this.ACTIVATE_CMD.toString('hex').toUpperCase()}`);
|
|
// 等待设备内部服务启动(实测需 2~5 秒)
|
this.onLog('⏳ 等待设备激活(3秒)...');
|
await this.sleep(3000);
|
|
// 尝试获取 CD 数据确认激活成功
|
let activated = false;
|
for (let i = 0; i < 3; i++) {
|
try {
|
const response = await this.sendCommand(
|
this.CD_QUERY_CMD,
|
this.isValidCDResponse.bind(this),
|
2000
|
);
|
this.onData(response);
|
this.onLog('✅ 激活成功!收到有效 CD 数据');
|
this.isActivated = true;
|
activated = true;
|
break;
|
} catch (e) {
|
this.onLog(`⚠️ 激活验证第 ${i + 1}/3 次失败: ${e.message}`);
|
await this.sleep(800);
|
}
|
}
|
|
if (!activated) {
|
this.onLog('❗ 激活未确认,将在下一轮重试');
|
}
|
}
|
|
// 第二步:正常轮询 CD 数据
|
if (this.isActivated) {
|
try {
|
const response = await this.sendCommand(
|
this.CD_QUERY_CMD,
|
this.isValidCDResponse.bind(this),
|
2000
|
);
|
this.onData(response);
|
this.onLog('📊 成功读取一帧治疗数据');
|
} catch (err) {
|
this.onLog(`⚠️ CD 查询失败: ${err.message}`);
|
this.isActivated = false; // 下次重新激活
|
}
|
}
|
|
await this.sleep(this.pollInterval);
|
} catch (err) {
|
this.onError(`⚠️ 通信循环异常: ${err.message}`);
|
await this.sleep(2000);
|
}
|
}
|
}
|
}
|
|
|
module.exports = FreseniusSerialService;
|