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

---
 scripts/buildFrames.js |  241 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 241 insertions(+), 0 deletions(-)

diff --git a/scripts/buildFrames.js b/scripts/buildFrames.js
new file mode 100644
index 0000000..762ba35
--- /dev/null
+++ b/scripts/buildFrames.js
@@ -0,0 +1,241 @@
+// 帮助函数:按 protocol.md 构造测试数据帧
+
+const net = require("net");
+const config = require("../src/config");
+
+/**
+ * 构造公共头
+ * @param {object} opts
+ * @param {number} opts.frameType 0x1F/0x26/0x29
+ * @param {number|string} [opts.deviceNumber] 机器号(5 字节小端整数,测试时可用数字或字符串)
+ */
+function buildHeader({ frameType, deviceNumber = 12345 }) {
+  const header = Buffer.alloc(20);
+
+  // 0-3: 帧头 0x55555555
+  header[0] = 0x55;
+  header[1] = 0x55;
+  header[2] = 0x55;
+  header[3] = 0x55;
+
+  // 4: 机器类型(随便选一个:0x01=SWS-4000)
+  header[4] = 0x01;
+
+  // 5-9: 机器编号,协议为 5 字节无符号整数,小端在前
+  let idNum = 0;
+  if (typeof deviceNumber === "string") {
+    // 字符串时按十进制解析
+    idNum = parseInt(deviceNumber, 10) || 0;
+  } else {
+    idNum = Number(deviceNumber) || 0;
+  }
+  for (let i = 0; i < 5; i++) {
+    header[5 + i] = idNum & 0xff;
+    idNum = Math.floor(idNum / 256);
+  }
+
+  // 10: 机器运行模式(0x01=透析,参见 protocol.md 2.3)
+  header[10] = 0x01;
+
+  // 11: 数据帧类型
+  header[11] = frameType;
+
+  // 12: 协议版本,当前 V1.10 => 0x6E(见 protocol.md 2.5)
+  header[12] = 0x6e;
+
+  // 13-19: 保留 7 字节,全部置 0
+  // Buffer.alloc 已经填 0,无需再写
+
+  return header;
+}
+
+/**
+ * 写入无符号整数(运行参数帧采用小端编码),与解析端 readUInt 对齐
+ */
+function writeUInt(buf, offset, value, length) {
+  if (length === 1) return buf.writeUInt8(value, offset);
+  if (length === 2) return buf.writeUInt16LE(value, offset);
+  if (length === 4) return buf.writeUInt32LE(value, offset);
+}
+
+/**
+ * 构造运行参数帧(0x1F),字段偏移参见 docs/protocol.md 3.2
+ */
+function buildRunParamsFrame(deviceNumber = 12345) {
+  const data = Buffer.alloc(220);
+
+  // 只是示例填一些看得见的值,便于验证解析
+  writeUInt(data, 0, 3600, 4); // SetTreatmentTime: 3600s
+  writeUInt(data, 4, 1200, 4); // K: 已治疗 1200s
+  writeUInt(data, 8, 300, 2); // D: 血泵流量 300 ml/min
+  writeUInt(data, 10, 1, 1); // xlyxbj: 血泵运行中
+  writeUInt(data, 11, 1, 1); // klfs: 肝素抗凝
+  writeUInt(data, 12, 1, 1); // z: 肝素泵运行
+  writeUInt(data, 13, 500, 2); // E: 肝素泵流量 50.0 ml/h (×10)
+  writeUInt(data, 17, 10, 2); // gstqjssj: 提前结束 10 min
+  writeUInt(data, 21, 2000, 4); // A: 超滤总量 2000 ml
+  writeUInt(data, 25, 500, 4); // B: 已超滤量 500 ml
+  writeUInt(data, 29, 1, 1); // cllyxbj: 超滤泵运行
+  writeUInt(data, 30, 0, 1); // plbj: 旁路关闭
+  writeUInt(data, 31, 500, 2); // L: 透析液流量 500 ml/min
+  writeUInt(data, 33, 370, 2); // F: 37.0℃ (×10)
+  writeUInt(data, 35, 1450, 2); // G: 14.50 mS/cm (×100)
+  writeUInt(data, 37, 100, 2); // C: 示例值
+  writeUInt(data, 41, 1000, 4); // ypyzhyl: 已补入置换液量
+  writeUInt(data, 45, 0, 1); // pyprfs: 前稀释
+  writeUInt(data, 46, 100, 2); // ldslq
+  writeUInt(data, 48, 50, 2); // ldslq2sysj
+  writeUInt(data, 50, 600, 4); // jqzyxsj: 总运行 600min
+  writeUInt(data, 54, 120, 2); // o: 动脉压 120 mmHg
+  writeUInt(data, 56, 200, 2); // H: 静脉压 200 mmHg
+  writeUInt(data, 58, 250, 2); // J: 跨膜压 250 mmHg
+  writeUInt(data, 60, 150, 2); // I: 15.0 kPa (×10)
+  writeUInt(data, 62, 20, 1); // lsxjl: 尿素下降率 20%
+  writeUInt(data, 64, 3450, 2); // ssqclz: 34.50 (×100)
+  writeUInt(data, 66, 365, 2); // jmyxh: 36.5℃ (×10)
+  writeUInt(data, 68, 368, 2); // dmyxw: 36.8℃ (×10)
+  writeUInt(data, 69, 80, 1); // xdxrl: 相对血容量 80%
+
+  const header = buildHeader({ frameType: 0x1f, deviceNumber });
+  return Buffer.concat([header, data]);
+}
+
+/**
+ * 构造报警信息帧(0x26)
+ *
+ * 数据区字段偏移对应 src/protocol.js::parseAlarm:
+ *  - 0:2 报警编号 alarmCode
+ *  - 2:1 报警类型 bjlx (0=解除,1=产生)
+ *  - 3:2 年, 5:1 月,6:1 日,7:1 时,8:1 分,9:1 秒
+ */
+function buildAlarmFrame(deviceNumber = 12345, { alarmType = 1, alarmCode = 283 } = {}) {
+  const data = Buffer.alloc(150);
+
+  // 报警编号在实际帧中为小端编码,这里按小端写入,
+  // 确保抓包看到的字节序与透析机一致,例如 0x011B = 283 → 1B 01。
+  data.writeUInt16LE(alarmCode, 0); // alarmCode: 示例编号
+
+  writeUInt(data, 2, alarmType, 1); // bjlx: 0/1=解除/产生
+
+  const now = new Date();
+  const year = now.getFullYear();
+  const month = now.getMonth() + 1;
+  const day = now.getDate();
+  const hour = now.getHours();
+  const minute = now.getMinutes();
+  const second = now.getSeconds();
+
+  // 时间中的年份在协议中也是小端编码
+  data.writeUInt16LE(year, 3);
+  writeUInt(data, 5, month, 1);
+  writeUInt(data, 6, day, 1);
+  writeUInt(data, 7, hour, 1);
+  writeUInt(data, 8, minute, 1);
+  writeUInt(data, 9, second, 1);
+
+  const header = buildHeader({ frameType: 0x26, deviceNumber });
+  return Buffer.concat([header, data]);
+}
+
+/**
+ * 构造血压测量数据帧(0x29)
+ *
+ * 注意:血压帧的数据区多字节字段使用小端序,
+ * 与运行参数帧/报警帧不同,因此这里直接用 Buffer 的 writeUInt16LE 写入。
+ *
+ * 数据区字段偏移对应 src/protocol.js::parseBloodPressure:
+ *  - 0:1  测量模式 bpMode (0/1=手动/自动)
+ *  - 1:1  测量结果 bpResult (0/1=出错/成功)
+ *  - 2 起 年/月/日/时/分/秒(年为 2 字节小端)
+ *  - 9:2  收缩压 N (mmHg,小端)
+ *  - 11:2 舒张压 O (mmHg,小端)
+ *  - 13:2 脉搏 P (bpm,小端)
+ *  - 15:2 平均动脉压 BPMPJDMY (mmHg,小端)
+ */
+function buildBloodPressureFrame(
+  deviceNumber = 12345,
+  { bpMode = 1, bpResult = 1, systolic = 129, diastolic = 93, heartRate = 86, map = 105 } = {}
+) {
+  const data = Buffer.alloc(150);
+
+  // 单字节字段
+  data.writeUInt8(bpMode, 0);
+  data.writeUInt8(bpResult, 1);
+
+  const now = new Date();
+  const year = now.getFullYear();
+  const month = now.getMonth() + 1;
+  const day = now.getDate();
+  const hour = now.getHours();
+  const minute = now.getMinutes();
+  const second = now.getSeconds();
+
+  // 时间:年为 2 字节小端,其它为 1 字节
+  data.writeUInt16LE(year, 2);
+  data.writeUInt8(month, 4);
+  data.writeUInt8(day, 5);
+  data.writeUInt8(hour, 6);
+  data.writeUInt8(minute, 7);
+  data.writeUInt8(second, 8);
+
+  // 血压相关字段,小端写入
+  data.writeUInt16LE(systolic, 9);
+  data.writeUInt16LE(diastolic, 11);
+  data.writeUInt16LE(heartRate, 13);
+  data.writeUInt16LE(map, 15);
+
+  const header = buildHeader({ frameType: 0x29, deviceNumber });
+  return Buffer.concat([header, data]);
+}
+
+/**
+ * 通用发送函数:连上 TCP -> 发送一帧 -> 关闭
+ */
+function sendFrameOnce(label, buildFn) {
+  const client = new net.Socket();
+
+  client.connect(config.tcp.port, "192.168.220.1", () => {
+    console.log(`[CLIENT] Connected to server ${config.tcp.port} for ${label}`);
+    const frame = buildFn(12345);
+    client.write(frame, () => {
+      console.log(`[CLIENT] ${label} frame sent, length=`, frame.length);
+      setTimeout(() => client.end(), 500);
+    });
+  });
+
+  client.on("error", (err) => {
+    console.error(`[CLIENT] ${label} socket error:`, err.message || err);
+  });
+
+  client.on("close", () => {
+    console.log(`[CLIENT] ${label} connection closed`);
+  });
+}
+
+function sendRunParamsOnce() {
+  sendFrameOnce("RunParams(0x1F)", buildRunParamsFrame);
+}
+
+function sendAlarmOnce() {
+  sendFrameOnce("Alarm(0x26)", buildAlarmFrame);
+}
+
+function sendBloodPressureOnce() {
+  sendFrameOnce("BloodPressure(0x29)", buildBloodPressureFrame);
+}
+
+if (require.main === module) {
+  // 直接运行本文件时,按顺序各发一帧三种类型,方便联调
+  sendRunParamsOnce();
+  setTimeout(() => sendAlarmOnce(), 800);
+  setTimeout(() => sendBloodPressureOnce(), 1600);
+}
+
+module.exports = {
+  buildRunParamsFrame,
+  buildAlarmFrame,
+  buildBloodPressureFrame,
+  sendRunParamsOnce,
+  sendAlarmOnce,
+  sendBloodPressureOnce
+};

--
Gitblit v1.8.0