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