chenyc
2026-03-22 7885cede659f3255be56f77c1eef2ada7387d6f1
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
// 透析机协议解析模块
// 按《山外山血透机远程通讯协议(V1.11)》实现:
// - 帧头:0x55555555(4 字节)
// - 公共头:机器类型(1) + 机器编号(5) + 运行模式(1) + 数据帧类型(1) + 协议版本(1) + 保留(7)
// - 数据区长度:运行参数帧(0x1F)≈220B,报警帧(0x26)≈150B,血压帧(0x29)≈150B
 
const logger = require("./logger");
 
class ProtocolParser {
  constructor() {
    this.headerBytes = Buffer.from([0x55, 0x55, 0x55, 0x55]);
    // 公共头总长度:4(帧头) + 1 + 5 + 1 + 1 + 1 + 7 = 20
    this.baseHeaderLength = 20;
  }
 
  /**
   * 根据帧类型返回数据区长度
   * @param {number} frameType
   * @returns {number|null}
   */
  getDataLength(frameType) {
    switch (frameType) {
      case 0x1f: // 运行参数帧
        // 文档标注“数据长度 220 字节”,结合实际抓包:整帧长度就是 220,
        // 即 20 字节公共头 + 200 字节数据区,这里返回的数据区长度应为 200。
        return 200;
      case 0x26: // 报警信息帧
        // 文档标注“数据长度 150 字节”,实际抓包显示 **整帧长度就是 150**,
        // 即 20 字节公共头 + 130 字节数据区,这里返回的数据区长度应为 130。
        return 130;
      case 0x29: // 血压测量数据帧
        // 同报警帧,整帧 150 字节 => 数据区 130 字节。
        return 130;
      default:
        return null;
    }
  }
 
  /**
   * 从设备 buffer 中尽可能提取完整报文
   * @param {Buffer} buffer 当前累计 buffer
   * @returns {{ frames: Buffer[], remaining: Buffer }}
   */
  extractFrames(buffer) {
    const frames = [];
    let offset = 0;
 
    while (buffer.length - offset >= this.baseHeaderLength) {
      const start = buffer.indexOf(this.headerBytes, offset);
      if (start === -1) {
        // 没找到帧头:仅保留最后 3 字节,兼容下一次 data 事件和帧头跨包拼接
        const keepTail = this.headerBytes.length - 1;
        const remaining =
          buffer.length > keepTail ? buffer.slice(buffer.length - keepTail) : buffer;
        return { frames, remaining };
      }
 
      // 头部不完整,等待更多数据
      if (buffer.length - start < this.baseHeaderLength) {
        break;
      }
 
      const frameType = buffer[start + 11]; // 帧头后第 11 字节为数据帧类型
      const dataLen = this.getDataLength(frameType);
      if (!dataLen) {
        logger.warn("Unknown frame type, skip header", { frameType });
        // 跳过这 4 字节帧头,继续往后找,避免卡死
        offset = start + 4;
        continue;
      }
 
      const totalLen = this.baseHeaderLength + dataLen;
      if (buffer.length - start < totalLen) {
        // 不够一整帧,等待下次数据
        break;
      }
 
      const frame = buffer.slice(start, start + totalLen);
      frames.push(frame);
      offset = start + totalLen;
    }
 
    const remaining = buffer.slice(offset);
    return { frames, remaining };
  }
 
  /**
   * 工具:按字节长度读取无符号整数(运行参数帧使用小端)
   * @param {Buffer} buf
   * @param {number} offset
   * @param {number} length 1/2/4
   */
  readUInt(buf, offset, length) {
    if (offset + length > buf.length) return null;
    if (length === 1) return buf.readUInt8(offset);
    if (length === 2) return buf.readUInt16LE(offset);
    if (length === 4) return buf.readUInt32LE(offset);
    return null;
  }
 
  mapMachineType(type) {
    switch (type) {
      case 0x01:
        return "SWS-4000";
      case 0x02:
        return "SWS-4000A";
      case 0x31:
        return "SWS-6000";
      case 0x32:
        return "SWS-6000A";
      default:
        return "unknown";
    }
  }
 
  mapRunMode(mode) {
    switch (mode) {
      case 0x00:
        return "待机";
      case 0x01:
        return "透析";
      case 0x02:
        return "滤过";
      case 0x03:
        return "透析滤过";
      case 0x04:
        return "序贯-透析";
      case 0x05:
        return "单纯超滤";
      case 0x06:
        return "序贯-单超";
      case 0x07:
        return "预充";
      case 0x08:
        return "清洗";
      case 0x09:
        return "清洗消毒";
      case 0x14:
        return "透析结束";
      case 0x15:
        return "清洗结束";
      case 0x16:
        return "透析滤过结束";
      case 0x17:
        return "单纯超滤结束";
      default:
        return `MODE_${mode}`;
    }
  }
 
  buildTimeString(parts) {
    const [year, month, day, hour, minute, second] = parts;
    if (!year) return null;
    const y = year.toString().padStart(4, "0");
    const m = String(month || 0).padStart(2, "0");
    const d = String(day || 0).padStart(2, "0");
    const h = String(hour || 0).padStart(2, "0");
    const mi = String(minute || 0).padStart(2, "0");
    const s = String(second || 0).padStart(2, "0");
    return `${y}-${m}-${d} ${h}:${mi}:${s}`;
  }
 
  /**
   * 解析运行参数帧(0x1F),返回与 schema.json 对齐的字段对象
   *
   * 说明:
   * - 下表中的“偏移 / 长度”来自《运行参数帧》数据区定义;
   * - “协议含义”是 PDF 表格中的中文说明;
   * - "对应标识符" 是你 `schema.json` 里的 `identifier`,便于后续属性映射。
   *
   * 运行参数区字段对照(节选):
   *
   * | 偏移 | 长度 | 协议含义                       | 对应标识符        |
   * |------|------|--------------------------------|-------------------|
   * | 0    | 4    | 设置治疗时间 (s)              | SetTreatmentTime  |
   * | 4    | 4    | 已治疗时间 (s)                | K                 |
   * | 8    | 2    | 血泵流量 (ml/min)            | D                 |
   * | 10   | 1    | 血泵运行标志 0/1 停止/运行   | xlyxbj            |
   * | 11   | 1    | 抗凝方式 0/1 无抗凝/肝素抗凝 | klfs              |
   * | 12   | 1    | 肝素泵运行标志 0/1 停止/运行 | z                 |
  * | 13   | 2    | 肝素泵流量 (ml/h×10)         | E(还原后保存)   |
  * | 15   | 2    | 肝素提前结束时间 (min)       | gstqjssj          |
  * | 17   | 4    | 超滤总量 (ml)                | A                 |
  * | 21   | 4    | 已超滤量 (ml)                | B                 |
  * | 25   | 4    | 超滤率 (ml/h)                | C                 |
  * | 29   | 1    | 超滤泵运行标志               | cllyxbj           |
  * | 30   | 1    | 旁路标志 0/1 关/开           | plbj              |
  * | 31   | 2    | 透析液流量 (ml/min)          | L                 |
  * | 33   | 2    | 透析液实际温度 (℃×10)       | F(还原后保存)   |
  * | 35   | 2    | 透析液电导值 (mS/cm×100)     | G(还原后保存)   |
  * | 37   | 4    | 补液总量 (ml)                | pyzl              |
  * | 41   | 4    | 已补入置换液量 (ml)         | ypyzhyl           |
   * | 45   | 1    | 补液补入模式 0/1 前/后稀释   | pyprfs            |
   * | 46   | 2    | 内毒素滤器1使用时间 (h)     | ldslq             |
   * | 48   | 2    | 内毒素滤器2使用时间 (h)     | ldslq2sysj        |
   * | 50   | 4    | 机器总运行时间 (min)        | jqzyxsj           |
   * | 54   | 2    | 动脉压 (mmHg)                | o                 |
   * | 56   | 2    | 静脉压 (mmHg)                | H                 |
   * | 58   | 2    | 跨膜压 (mmHg)                | J                 |
   * | 60   | 2    | 透析液压 (kPa×10)            | I(还原后保存)   |
   * | 62   | 1    | 尿素下降率 (%)               | lsxjl             |
   * | 64   | 2    | 实时清除率值 (×100)          | ssqclz(还原)    |
   * | 66   | 2    | 静脉血温 (℃×10)             | jmyxh(还原)     |
   * | 68   | 2    | 动脉血温 (℃×10)             | dmyxw(还原)     |
   * | 69   | 1    | 相对血容量 (%)               | xdxrl             |
   */
  parseRunParams(data) {
    const r = (off, len) => this.readUInt(data, off, len);
    const rInt16 = (off) =>
      off + 2 <= data.length ? data.readInt16LE(off) : null; // 带符号 16 位,小端
    const result = {};
 
    // 以下偏移和长度依据协议截图整理,必要时可结合抓包微调
 
    // 偏移 0,长度 4 字节
    // 协议字段:设置治疗时间(s),这里转换为“分钟”保存
    // schema 标识符:SetTreatmentTime(按分钟上报)
    const SetTreatmentTimeSec = r(0, 4);
    if (SetTreatmentTimeSec != null) {
      // 结果四舍五入到整数分钟
      result.SetTreatmentTime = Math.round(SetTreatmentTimeSec / 60);
    }
 
    // 偏移 4,4 字节
    // 协议字段:已治疗时间(s),这里同样按“分钟”保存
    // schema 标识符:K(按分钟上报)
    const Ksec = r(4, 4); // 已透析时间(s)
    if (Ksec != null) {
      result.K = Math.round(Ksec / 60);
    }
 
    // 偏移 8,2 字节
    // 协议字段:血泵流量(ml/min)
    // schema 标识符:D
    const D = r(8, 2); // 血泵流量 ml/min
    if (D != null) result.D = D;
 
    // 偏移 10,1 字节
    // 协议字段:血泵运行标志 0/1-停止/运行
    // schema 标识符:xlyxbj
    const xlyxbj = r(10, 1);
    if (xlyxbj != null) result.xlyxbj = xlyxbj;
 
    // 偏移 11,1 字节
    // 协议字段:抗凝方式 0/1-无抗凝/肝素抗凝
    // schema 标识符:klfs
    const klfs = r(11, 1);
    if (klfs != null) result.klfs = klfs;
 
    // 偏移 12,1 字节
    // 协议字段:肝素泵运行标志 0/1-停止/运行
    // schema 标识符:z
    const z = r(12, 1);
    if (z != null) result.z = z;
 
    // 偏移 13,2 字节
    // 协议字段:肝素泵流量(ml/h),数值放大 10 倍
    // schema 标识符:E(在物模型中为 int,但这里解析时就做缩放)
    const Eraw = r(13, 2); // 肝素泵流量 ml/h,放大 10 倍
    if (Eraw != null) result.E = Eraw / 10;
 
    // 偏移 15,2 字节
    // 协议字段:肝素提前结束时间(min)
    // schema 标识符:gstqjssj
    const gstqjssj = r(15, 2);
    if (gstqjssj != null) result.gstqjssj = gstqjssj;
 
    // 偏移 17,4 字节
    // 协议字段:超滤总量(ml),这里按 L 保存,保留 3 位小数
    // schema 标识符:A
    const Araw = r(17, 4); // 超滤总量 ml
    if (Araw != null) {
      result.A = (Araw / 1000).toFixed(3); // L
    }
 
    // 偏移 21,4 字节
    // 协议字段:已超滤量(ml),这里按 L 保存,保留 3 位小数
    // schema 标识符:B
    const Braw = r(21, 4); // 已超滤量 ml
    if (Braw != null) {
      result.B = (Braw / 1000).toFixed(3); // L
    }
 
    // 偏移 29,1 字节
    // 协议字段:超滤泵运行标志 0/1-停止/运行
    // schema 标识符:cllyxbj
    const cllyxbj = r(29, 1);
    if (cllyxbj != null) result.cllyxbj = cllyxbj;
 
    // 偏移 30,1 字节
    // 协议字段:旁路标志 0/1-关闭/打开
    // schema 标识符:plbj
    const plbj = r(30, 1);
    if (plbj != null) result.plbj = plbj;
 
    // 偏移 31,2 字节
    // 协议字段:透析液流量(ml/min)
    // schema 标识符:L
    const L = r(31, 2); // 透析液流量 ml/min
    if (L != null) result.L = L;
 
    // 偏移 33,2 字节
    // 协议字段:透析液实际温度(℃),数值放大 10 倍
    // schema 标识符:F
    const Fraw = r(33, 2); // 透析液温度,放大 10 倍
    if (Fraw != null) result.F = Fraw / 10;
 
    // 偏移 35,2 字节
    // 协议字段:透析液电导值(mS/cm),数值放大 100 倍
    // schema 标识符:G
    const Graw = r(35, 2); // 透析液电导值,放大 100 倍
    if (Graw != null) result.G = Graw / 100;
 
    // 偏移 25,4 字节
    // 协议字段:超滤率(ml/h),这里按 L/h 保存,保留 3 位小数
    // schema 标识符:C
    const Craw = r(25, 4);
    if (Craw != null) {
      result.C = (Craw / 1000).toFixed(3); // L/h
    }
 
    // 偏移 37,4 字节
    // 协议字段:补液总量(ml)
    // schema 标识符:pyzl
    const pyzl = r(37, 4);
    if (pyzl != null) result.pyzl = pyzl;
 
    // 偏移 41,4 字节
    // 协议字段:已补入置换液量(ml)
    // schema 标识符:ypyzhyl
    const ypyzhyl = r(41, 4);
    if (ypyzhyl != null) result.ypyzhyl = ypyzhyl;
 
    // 偏移 45,1 字节
    // 协议字段:补液补入模式 0/1-前稀释/后稀释
    // schema 标识符:pyprfs
    const pyprfs = r(45, 1);
    if (pyprfs != null) result.pyprfs = pyprfs;
 
    // 偏移 46,2 字节
    // 协议字段:内毒素滤器1使用时间(h)
    // schema 标识符:ldslq
    const ldslq = r(46, 2);
    if (ldslq != null) result.ldslq = ldslq;
 
    // 偏移 48,2 字节
    // 协议字段:内毒素滤器2使用时间(h)
    // schema 标识符:ldslq2sysj
    const ldslq2sysj = r(48, 2);
    if (ldslq2sysj != null) result.ldslq2sysj = ldslq2sysj;
 
    // 偏移 50,4 字节
    // 协议字段:机器总运行时间(min)
    // schema 标识符:jqzyxsj
    const jqzyxsj = r(50, 4);
    if (jqzyxsj != null) result.jqzyxsj = jqzyxsj;
 
    // 偏移 54,2 字节
    // 协议字段:动脉压(mmHg),为带符号数,负值表示负压
    // schema 标识符:o
    const o = rInt16(54); // 动脉压
    if (o != null) result.o = o;
 
    // 偏移 56,2 字节
    // 协议字段:静脉压(mmHg),一般为正值,这里同样按带符号解析以兼容异常情况
    // schema 标识符:H
    const H = rInt16(56); // 静脉压
    if (H != null) result.H = H;
 
    // 偏移 58,2 字节
    // 协议字段:跨膜压(mmHg)
    // schema 标识符:J
    const J = rInt16(58); // 跨膜压
    if (J != null) result.J = J;
 
    // 偏移 60,2 字节
    // 协议字段:透析液压(kPa),数值放大 10 倍,可为负值
    // schema 标识符:I
    const Iraw = rInt16(60); // 透析液压 kPa,放大 10 倍
    if (Iraw != null) result.I = Iraw / 10;
 
    // 偏移 62,1 字节
    // 协议字段:尿素下降率(%)
    // schema 标识符:lsxjl
    const lsxjl = r(62, 1); // 尿素下降率 %
    if (lsxjl != null) result.lsxjl = lsxjl;
 
    // 偏移 64,2 字节
    // 协议字段:实时清除率值,数值放大 100 倍
    // schema 标识符:ssqclz
    const ssqclzRaw = r(64, 2); // 实时清除率,放大 100 倍
    if (ssqclzRaw != null) result.ssqclz = ssqclzRaw / 100;
 
    // 偏移 66,2 字节
    // 协议字段:静脉血温(℃),数值放大 10 倍
    // schema 标识符:jmyxh
    const jmyxhRaw = r(66, 2); // 静脉血温,放大 10 倍
    if (jmyxhRaw != null) result.jmyxh = jmyxhRaw / 10;
 
    // 偏移 68,2 字节
    // 协议字段:动脉血温(℃),数值放大 10 倍
    // schema 标识符:dmyxw
    const dmyxwRaw = r(68, 2); // 动脉血温,放大 10 倍
    if (dmyxwRaw != null) result.dmyxw = dmyxwRaw / 10;
 
    // 偏移 69,1 字节
    // 协议字段:相对血容量(%)
    // schema 标识符:xdxrl
    const xdxrl = r(69, 1); // 相对血容量 %
    if (xdxrl != null) result.xdxrl = xdxrl;
 
    return result;
  }
 
  /**
    * 解析报警帧(0x26)
    *
    * 数据区字段对照:
    * - 偏移 0,长度 2:报警编号(小端)→ 这里记为 alarmCode
    * - 偏移 2,长度 1:报警类型 → 0/1=消除报警/产生报警,对应 schema 中的 `bjlx`
    * - 偏移 3~?: 年(2 字节小端)、月、日、时、分、秒 → 组合为 `bjsj`(报警时间,字符串格式 YYYY-MM-DD HH:MM:SS)
   */
  parseAlarm(data) {
    // 报警帧里的 16 位数值(报警编号、年份)使用小端序,
    // 与运行参数帧的大端序不同,这里单独处理。
    const r8 = (off) => (off < data.length ? data.readUInt8(off) : null);
    const r16le = (off) =>
      off + 2 <= data.length ? data.readUInt16LE(off) : null;
 
    const alarmCode = r16le(0);
    const alarmType = r8(2); // 0=解除,1=产生
 
    const year = r16le(3);
    const month = r8(5);
    const day = r8(6);
    const hour = r8(7);
    const minute = r8(8);
    const second = r8(9);
 
    const alarmTime = this.buildTimeString([year, month, day, hour, minute, second]);
 
    return {
      alarmCode,
      bjlx: alarmType,
      bjsj: alarmTime
    };
  }
 
  /**
    * 解析血压测量帧(0x29)
    *
    * 数据区字段对照(结合血压数据表与 schema):
    * - 偏移 0,1 字节:测量模式 0/1=手动/自动 → 这里记为 `bpMode`
    * - 偏移 1,1 字节:测量结果 0/1=测量出错/测量成功 → `bpResult`
    * - 偏移 2 起:年/月/日/时/分/秒 → 组装为测量时间,挂到运行参数表字段 `M`(BPM监测时间)
    * - 偏移 13,2 字节:收缩压(mmHg) → schema 标识符 `N`
    * - 偏移 15,2 字节:舒张压(mmHg) → schema 标识符 `O`
    * - 偏移 17,2 字节:脉搏/心率 → schema 标识符 `P`
    * - 偏移 19,2 字节:平均动脉压 → schema 标识符 `BPMPJDMY`
   */
  parseBloodPressure(data) {
    // 血压帧的数据区里,多字节数值使用小端序(低字节在前),
    // 与运行参数帧/报警帧的大端序不同,这里单独按小端解析。
 
    const r8 = (off) => (off < data.length ? data.readUInt8(off) : null);
    const r16le = (off) =>
      off + 2 <= data.length ? data.readUInt16LE(off) : null;
 
    const bpMode = r8(0); // 测量模式:0/1 手动/自动
    const bpResult = r8(1); // 结果:0/1 出错/成功
 
    // 年:2 字节小端;月/日/时/分/秒:1 字节
    const year = r16le(2);
    const month = r8(4);
    const day = r8(5);
    const hour = r8(6);
    const minute = r8(7);
    const second = r8(8);
 
    const bpTime = this.buildTimeString([year, month, day, hour, minute, second]);
 
    // 根据抓包数据校正:
    // - 偏移  9,2 字节小端:收缩压 N(例如 0x81 0x00 => 129)
    // - 偏移 11,2 字节小端:舒张压 O(例如 0x5D 0x00 => 93)
    // - 偏移 13,2 字节小端:脉搏 P   (例如 0x56 0x00 => 86)
    // - 偏移 15,2 字节小端:平均动脉压 BPMPJDMY(例如 0x69 0x00 => 105)
    const systolic = r16le(9); // 收缩压 N
    const diastolic = r16le(11); // 舒张压 O
    const heartRate = r16le(13); // 脉搏 P
    const map = r16le(15); // 平均动脉压 BPMPJDMY
 
    return {
      bpMode,
      bpResult,
      M: bpTime,
      N: systolic,
      O: diastolic,
      P: heartRate,
      BPMPJDMY: map
    };
  }
 
  /**
   * 将一帧报文解析为结构化对象。
   * @param {Buffer} frame 完整帧(包含公共头 + 数据区)
   * @param {string} ip 设备 IP
   * @returns {{ deviceNumber: string, data: object }}
   */
  parseFrame(frame, ip) {
    const now = new Date();
    const suedtime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(
      now.getDate()
    ).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(
      now.getMinutes()
    ).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
 
    const machineTypeByte = frame[4];
    const machineNumberBuf = frame.slice(5, 10); // 5 字节机器号
 
    // 按协议:机器编号为 5 字节无符号整数,小端在前。
    // 例如字节 E1 73 CB 72 01 => 十进制 6220903393。
    let deviceNumber = "";
    if (machineNumberBuf.length === 5) {
      let id = 0;
      for (let i = 0; i < 5; i++) {
        id += machineNumberBuf[i] * Math.pow(256, i); // 小端累加
      }
      deviceNumber = String(id);
    }
    if (!deviceNumber) {
      // 回退为十六进制字符串,避免完全丢失标识
      deviceNumber = Array.from(machineNumberBuf)
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");
    }
 
    const runModeByte = frame[10];
    const frameType = frame[11];
    const protocolVersion = frame[12];
 
    const dataBuf = frame.slice(this.baseHeaderLength);
 
    const baseData = {
      suedtime,
      deviceType: this.mapMachineType(machineTypeByte),
      IPAddress: ip,
      n: deviceNumber,
      jqyxms: this.mapRunMode(runModeByte),
      jqyxmsRaw: runModeByte,
      frameType,
      protocolVersion
    };
 
    if (frameType === 0x1f) {
      Object.assign(baseData, this.parseRunParams(dataBuf));
    } else if (frameType === 0x26) {
      Object.assign(baseData, this.parseAlarm(dataBuf));
    } else if (frameType === 0x29) {
      Object.assign(baseData, this.parseBloodPressure(dataBuf));
    } else {
      logger.warn("Unhandled frame type", { frameType });
    }
 
    const finalDeviceNumber = deviceNumber || ip || "unknown";
 
    logger.debug("Parsed frame", { ip, deviceNumber: finalDeviceNumber, frameType });
 
    return { deviceNumber: finalDeviceNumber, data: baseData };
  }
}
 
module.exports = ProtocolParser;