费森4008s 网口通讯 ,原生串口透传网口
chenyc
7 天以前 11e1b9d3ed909b28ef8e3330d152730c4e6d67c6
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
// 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;