From 7885cede659f3255be56f77c1eef2ada7387d6f1 Mon Sep 17 00:00:00 2001
From: chenyc <501753378@qq.com>
Date: 星期日, 22 三月 2026 16:23:21 +0800
Subject: [PATCH] 初始化项目

---
 src/protocol.js |  572 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 572 insertions(+), 0 deletions(-)

diff --git a/src/protocol.js b/src/protocol.js
new file mode 100644
index 0000000..5b260b2
--- /dev/null
+++ b/src/protocol.js
@@ -0,0 +1,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;

--
Gitblit v1.8.0