// 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 this.CD_QUERY_CMD = Buffer.from([0x16, 0x31, 0x3F, 0x43, 0x44, 0x04, 0x03]); // . 1 ? C D 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;