09b510aa6c32315577af6f09aab787179265374c..7885cede659f3255be56f77c1eef2ada7387d6f1
2026-03-22 chenyc
初始化项目
7885ce 对比 | 目录
1个文件已修改
27个文件已添加
4490 ■■■■■ 已修改文件
.gitignore 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config.json 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/current-protocol-decoding.md 349 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/protocol.md 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/实施部署文档.md 506 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
schema.json 636 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/buildFrames.js 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/stress-50-devices.js 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/test-alarm.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/test-bp.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/test-runparams.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/aliyunClient.js 158 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/dataCache.js 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/deviceManager.js 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/httpServer.js 151 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/index.js 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/logger.js 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/mqttClient.js 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/propertyMapper.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/protocol.js 572 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/rateLimiter.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/tcpServer.js 156 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/结果.txt 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
透析机通讯文档.pdf 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -1,28 +1,42 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git
.DS_Store
node_modules
/dist
/package-lock.json
/.env.development
/cycgetlogo.sh
# Users Environment Variables
.lock-wscript
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# 忽略技能文档文件夹
skill/
# 忽略trae文件夹
.trae/
# 忽略github文件夹
.github/
# 忽略api自动生成文件夹
/src/api/generated/
/src/api/__swagger__
# 忽略自有工具文件夹
/tools/
# 忽略Skills文件
/codex-skills
logs/
build/
dist/
README.md
New file
@@ -0,0 +1,252 @@
# 透析机 Socket 服务器
## 概述
这是一个 Node.js Socket 服务器,用于接收和处理透析机数据。程序直接打开即可运行,无需额外配置环境。
## 一、系统目标
- 通过 TCP Socket 接入多台透析机设备,稳定接收设备实时数据报文。
- 将原始报文按协议解析为结构化数据,并在内存中维护“最新一帧”数据。
- 按配置将数据分发到三类上游通道:
  - 对内/对第三方系统的 HTTP 接口;
  - 消息中间件(MQTT);
  - 云平台(阿里云物联网平台)。
- 支持灵活配置:可独立启用/禁用 HTTP、MQTT、阿里云三通道,以及各自的端口、地址、主题等参数。
## 二、运行环境与外部依赖
- 运行环境:Node.js(可通过 `pkg` 打包成独立可执行文件)。
- 外部依赖:
  - TCP 网络(设备通过固定 IP+端口连接到网关)。
  - 可选:MQTT Broker(如 EMQX、Mosquitto 等)。
  - 可选:阿里云物联网平台(需产品和设备三元组管理服务)。
  - 可选:后端 HTTP API,用于根据设备序列号获取阿里云设备三元组。
## 三、模块划分与功能
### 1. TCP 通讯模块(Socket 服务)
- 职责:
  - 监听配置端口,接受透析机 TCP 连接。
  - 处理粘包、拆包:按照协议从流式数据中截取完整报文。
  - 维护设备连接状态、心跳、重试和超时清理。
- 功能点:
  - 监听端口由配置文件指定,支持 IPv4 / IPv6 映射地址。
  - 为每个连接维护:
    - `socket`
    - `lastAck`(最近一次收到有效数据的时间戳)
    - `status`:`pending` → `valid` → `registered`
    - 设备编号、最近一帧完整数据等。
### 2. 设备管理模块(DeviceManager)
- 职责:
  - 统一管理所有在线设备连接和状态。
  - 负责启动/停止设备级定时任务(重试、保活)。
  - 负责全局超时清理。
- 功能点:
  - 接收到新连接时,创建设备实例并发起初始握手信号。
  - **全局超时清理定时器**:
    - 固定周期(例如每 5 分钟)遍历所有设备;
    - 若当前时间减去 `lastAck` 超过配置的超时时间(如 2 分钟),判定为超时;
    - 自动断开连接、释放资源、移出设备列表并记录日志。
### 3. 协议解析模块
- 职责:
  - 对透析机发送的数据切片解析。
  - 将每个字段转换为具有业务含义的键值对,并附加元信息。
- 功能点:
  - 按字节/字符位置解析各字段(A~Z、a~z、C53~C55 等)。
  - 生成结构化对象,包括:
    - 协议字段(例如:机器运行模式、动脉压、IP地址 )。
    - 元字段:
      - `suedtime`:网关接收到数据的时间(格式:`YYYY-MM-DD HH:mm:ss`)。
      - `deviceType`:机器类型标识。
      - `IPAddress`:该设备 TCP 连接的远端 IP 地址。
### 4. 数据缓存模块(DataCache)
- 职责:
  - 以“设备序列号”为主键,缓存该设备“最新一帧”解析后的数据。
  - 提供查询、统计、空闲检测等能力给 HTTP 接口和监控使用。
- 功能点:
  - `setDeviceData(deviceNumber, data)`:写入/更新指定设备数据,并记录缓存时间。
  - `getDeviceData(deviceNumber)`:按设备号返回最新缓存。
  - `getAllDeviceData()`:返回所有设备的当前缓存快照。
  - `getDeviceList()`:返回设备列表(设备号 + 最后缓存时间)。
  - `getStats()`:统计设备数量、估算内存占用,返回详细设备列表。
  - `getIdleDevices(timeoutMs)`:返回在指定时间内未更新的“空闲设备”,可用于排查设备掉线或通讯异常。
### 5. 属性映射与物模型模块(PropertyMapper)
- 职责:
  - 将内部字段标识符(如 A/B/C、a/b/c、C53 等)映射为对业务友好的属性名称。
  - 为 HTTP/MQTT/阿里云三通道提供统一的数据结构。
- 功能点:
  - 从外部配置(如 `schema.json`)加载物模型定义,若失败则使用内置默认物模型。
  - 提供方法:
    - `mapData(rawData)`:将原始对象转换为属性数组 `[ {identifier, name, value}, ... ]`。
    - `transformForHTTP(rawData, deviceNumber)`:专用于 HTTP 返回结构。
    - `transformForMQTT(rawData, deviceNumber)`:专用于 MQTT 消息负载。
    - `transformForAliyun(rawData)`:用于阿里云属性上报(直接以 identifier 作为物模型属性 ID)。
### 6. HTTP 接口服务模块
- 职责:
  - 提供 REST 风格 HTTP 接口,供第三方系统查询透析机实时/近实时数据。
  - 提供缓存与限流的管理能力。
- 核心接口(示例):
  - `GET /api/device/data?deviceNumber={n}[&mapped=true]`
    - 返回指定设备最新数据,支持:
      - `raw` 格式:内部原始字段;
      - `mapped` 格式:基于物模型映射后的属性数组。
    - 按设备号做访问限流(例如 5 秒内重复查询返回 429)。
  - `GET /api/device/all[?mapped=true]`
    - 返回所有有数据设备的快照,可按配置和参数决定是否做属性映射。
    - 使用统一 Key(如 `__all_devices__`)做全局访问限流(例如 1 分钟一次)。
  - `GET /api/device/list`
    - 返回设备号 + 最后更新时间列表。
  - `GET /api/cache/stats`
    - 返回缓存统计信息。
  - `POST/GET /api/cache/clear`
    - 清空所有缓存数据。
  - `GET /api/device/idle?timeout={ms}`
    - 返回指定超时时间内未更新的设备。
  - `GET /api/ratelimit/stats` / `POST /api/ratelimit/clear`
    - 查询和清空限流统计。
  - `GET /api/health`
    - 健康检查接口。
- 配置能力:
  - 是否启用 HTTP 服务;
  - 监听地址和端口;
  - CORS 开关与允许域;
  - 单设备请求和全设备请求的限流间隔;
  - 属性映射是否默认启用、是否包含原始数据。
### 7. 限流模块(RateLimiter)
- 职责:
  - 防止外部系统高频访问导致网关负载过高或设备查询过于频繁。
- 功能点:
  - 为每个“限流 Key”(如设备号或 `__all_devices__`)记录最近访问时间。
  - 提供 `checkLimit(key, interval)` 方法:
    - 若距离上一次访问小于设定间隔,则拒绝请求并返回需要等待的剩余时间;
    - 否则更新访问时间并允许请求。
### 8. MQTT 通讯模块
- 职责:
  - 按配置与 MQTT Broker 建立连接;
  - 在接收到透析机数据后,将数据发布到配置的 Topic。
- 功能点:
  - 连接参数(地址、端口、用户名密码、重连间隔等)全部可配置。
  - 通过 `enabled` 标志控制是否启用 MQTT 功能。
  - 主题规则:`{前缀}/{设备序列号}`,前缀可配置。
  - 消息内容包含:
    - 解析后的完整数据对象;
    - 设备 ID(如 IP:Port);
    - 发送时间戳。
### 9. 阿里云物联网对接模块(可选)
- 职责:
  - 根据设备序列号从后端服务获取阿里云设备三元组;
  - 使用阿里云 SDK 建立到物联网平台的连接;
  - 在设备状态有效后,按阿里云物模型上报属性。
- 功能点:
  - 通过配置中的 `enabled` 控制是否启用阿里云对接。
  - 仅当设备握手完成并收到有效数据时才发起三元组获取与阿里云连接。
  - 支持错误处理与重试日志记录。
### 10. 日志与监控模块
- 职责:
  - 统一记录系统运行日志,便于问题排查与运行监控。
- 功能点:
  - 记录:
    - 设备连接/断开;
    - 报文接收情况(原始数据、完整报文、异常数据);
    - 缓存读写操作;
    - HTTP 请求、限流事件;
    - MQTT/阿里云连接状态与错误。
  - 支持按天滚动日志文件(可选)。
## 四、配置与可定制项
### 1. 通讯与服务配置
- TCP 监听端口、IP。
- HTTP 服务开关、端口、地址、CORS 设置。
- MQTT 连接参数、主题前缀、开关。
- 阿里云接入参数和开关。
### 2. 业务与安全策略
- 心跳间隔、重试间隔、设备超时时间。
- HTTP 单设备与全设备查询的限流间隔。
- 属性映射默认开关、是否包含原始数据。
### 3. 物模型与字段映射
- 通过物模型配置文件(如 `schema.json`)统一定义:
  - 每个内部字段(A/B/C 等)的含义、名称、类型等;
  - 可扩展字段(例如增加 IP、警告信息等)。
## 五、并发压测(50 台透析机)
可使用内置脚本模拟 50 台设备并发连接并持续发送 `0x1F/0x26/0x29` 三类报文,用于提前验证网关稳定性。
### 1)启动网关
```powershell
npm run start
```
### 2)执行 50 台并发压测(默认 120 秒)
```powershell
npm run stress:50
```
### 3)可选参数(环境变量)
```powershell
$env:DEVICE_COUNT="50"
$env:DURATION_SEC="180"
$env:INTERVAL_MS="1000"
$env:TCP_HOST="127.0.0.1"
$env:TCP_PORT="19000"
npm run stress:50
```
参数说明:
- `DEVICE_COUNT`:并发设备数(默认 `50`)
- `DURATION_SEC`:压测持续时间秒数(默认 `120`)
- `INTERVAL_MS`:每台设备发送运行参数帧间隔(默认 `1000ms`)
- `ALARM_EVERY`:每 N 个发送周期插入报警帧(默认 `30`)
- `BP_EVERY`:每 N 个发送周期插入血压帧(默认 `15`)
## 六、本地日志审计(Winston)
项目已接入 `winston` + 按天滚动日志,默认日志目录由 `config.log.dir` 控制(默认 `../logs`)。
日志文件:
- `gateway-YYYY-MM-DD.log`:全量业务日志(连接、原始报文、解析、上报)
- `gateway-error-YYYY-MM-DD.log`:错误日志
关键追踪事件:
- `Device connected`:记录连接时间与 `ip:port`
- `Device identified`:将连接绑定到机器编号(`deviceNumber`)
- `Raw TCP data received`:记录原始十六进制报文
- `Dialysis frame parsed` / `Dialysis frame payload`:记录解析结果
- `MQTT publish success` / `Aliyun postProps success`:记录上报成功
- `Device disconnected`:记录断开时间、在线时长、总收包字节、总帧数
Windows 下按机器号查询示例:
```powershell
Get-Content .\logs\gateway-$(Get-Date -Format yyyy-MM-dd).log | Select-String "6220903393"
```
config.json
New file
@@ -0,0 +1,29 @@
{
  "tcp": {
    "host": "192.168.220.1",
    "port": 10000,
    "idleTimeoutMs": 1200000
  },
  "http": {
    "enabled": false,
    "host": "0.0.0.0",
    "port": 19001,
    "rateLimit": {
      "singleDeviceMs": 5000,
      "allDevicesMs": 60000
    }
  },
  "mqtt": {
    "enabled": false,
    "url": "mqtt.ihemodialysis.com",
    "port": 62283,
    "username": "data",
    "password": "data#2018",
    "defaultTopicPrefix": "touxiji",
    "retain": true
  },
  "aliyun": {
    "enabled": false,
    "baseURL": "https://things.icoldchain.cn/"
  }
}
docs/current-protocol-decoding.md
New file
@@ -0,0 +1,349 @@
# 山外山透析机当前通讯解码说明
> 本文档基于当前项目代码实现、现场抓包校验结果和 `schema.json` 物模型整理。
>
> 适用项目:`SWS-Communication`
>
> 主要对应文件:
> - `src/protocol.js`
> - `src/tcpServer.js`
> - `schema.json`
## 1. 总体说明
当前网关通过 TCP 接收山外山透析机上报的数据帧,按帧类型拆分并解码为结构化对象,然后写入缓存、输出本地日志,并按配置转发到 MQTT / 阿里云。
当前已实现并验证的帧类型:
- `0x1F`:运行参数帧
- `0x26`:报警帧
- `0x29`:血压测量帧
## 2. 帧结构
### 2.1 公共头结构
每一帧的公共头长度固定为 `20` 字节:
| 偏移 | 长度 | 含义 |
|---|---:|---|
| 0 | 4 | 帧头,固定为 `55 55 55 55` |
| 4 | 1 | 机器类型 |
| 5 | 5 | 机器编号 |
| 10 | 1 | 机器运行模式 |
| 11 | 1 | 数据帧类型 |
| 12 | 1 | 协议版本 |
| 13 | 7 | 保留 |
### 2.2 机器类型
当前代码映射如下:
| 值 | 机型 |
|---|---|
| `0x01` | `SWS-4000` |
| `0x02` | `SWS-4000A` |
| `0x31` | `SWS-6000` |
| `0x32` | `SWS-6000A` |
### 2.3 机器编号
当前实现已根据现场数据确认:
- 机器编号为 **5 字节无符号整数**
- 编码方式为 **小端**
例如:
- 字节:`E1 73 CB 72 01`
- 解码后:`6220903393`
项目中对应字段:
- `n`:机器编号字符串
### 2.4 运行模式
当前已实现的运行模式映射:
| 值 | 含义 |
|---|---|
| `0x00` | 待机 |
| `0x01` | 透析 |
| `0x02` | 滤过 |
| `0x03` | 透析滤过 |
| `0x04` | 序贯-透析 |
| `0x05` | 单纯超滤 |
| `0x06` | 序贯-单超 |
| `0x07` | 预充 |
| `0x08` | 清洗 |
| `0x09` | 清洗消毒 |
| `0x14` | 透析结束 |
| `0x15` | 清洗结束 |
| `0x16` | 透析滤过结束 |
| `0x17` | 单纯超滤结束 |
## 3. 帧长度与拆包规则
当前代码和现场抓包已对齐的长度如下:
| 帧类型 | 含义 | 整帧长度 | 数据区长度 |
|---|---|---:|---:|
| `0x1F` | 运行参数帧 | `220` 字节 | `200` 字节 |
| `0x26` | 报警帧 | `150` 字节 | `130` 字节 |
| `0x29` | 血压帧 | `150` 字节 | `130` 字节 |
说明:
- 当前拆包逻辑按 `20 字节公共头 + 数据区长度` 计算整帧。
- 若当前 buffer 中没有找到帧头,则只保留最后 `3` 字节,防止垃圾数据导致缓冲区无限增长。
- 每个连接当前有 buffer 上限保护,避免异常设备把内存打满。
## 4. 字节序说明
当前项目中经过现场验证后的字节序规则如下:
### 4.1 运行参数帧 `0x1F`
- 多字节字段按 **小端** 解码
- 压力类字段按 **有符号小端 16 位** 解码
对应:
- `readUInt16LE`
- `readUInt32LE`
- `readInt16LE`
### 4.2 报警帧 `0x26`
- 报警编号:**小端**
- 年份:**小端**
- 月/日/时/分/秒:单字节
### 4.3 血压帧 `0x29`
- 年份、收缩压、舒张压、脉搏、平均动脉压:均按 **小端** 解码
## 5. 运行参数帧 `0x1F`
## 5.1 当前解码字段
当前代码 `parseRunParams()` 中已经实现并使用的字段如下:
| 偏移 | 长度 | 协议含义 | 当前字段 | 当前保存方式 |
|---|---:|---|---|---|
| 0 | 4 | 设置治疗时间(秒) | `SetTreatmentTime` | **转换为分钟后保存** |
| 4 | 4 | 已治疗时间(秒) | `K` | **转换为分钟后保存** |
| 8 | 2 | 血泵流量(ml/min) | `D` | 原值 |
| 10 | 1 | 血泵运行标志 | `xlyxbj` | 原值 |
| 11 | 1 | 抗凝方式 | `klfs` | 原值 |
| 12 | 1 | 肝素泵运行标志 | `z` | 原值 |
| 13 | 2 | 肝素泵流量(放大 10 倍) | `E` | 除以 `10` |
| 15 | 2 | 肝素提前结束时间(min) | `gstqjssj` | 原值 |
| 17 | 4 | 超滤总量(ml) | `A` | **转换为 L,保留 3 位小数** |
| 21 | 4 | 已超滤量(ml) | `B` | **转换为 L,保留 3 位小数** |
| 25 | 4 | 超滤率(ml/h) | `C` | 原值 |
| 29 | 1 | 超滤泵运行标志 | `cllyxbj` | 原值 |
| 30 | 1 | 旁路标志 | `plbj` | 原值 |
| 31 | 2 | 透析液流量(ml/min) | `L` | 原值 |
| 33 | 2 | 透析液实际温度(放大 10 倍) | `F` | 除以 `10` |
| 35 | 2 | 透析液电导值(放大 100 倍) | `G` | 除以 `100` |
| 37 | 4 | 补液总量(ml) | `pyzl` | 原值 |
| 41 | 4 | 已补入置换液量(ml) | `ypyzhyl` | 原值 |
| 45 | 1 | 补液补入模式 | `pyprfs` | 原值 |
| 46 | 2 | 内毒素滤器1使用时间(h) | `ldslq` | 原值 |
| 48 | 2 | 内毒素滤器2使用时间(h) | `ldslq2sysj` | 原值 |
| 50 | 4 | 机器总运行时间(min) | `jqzyxsj` | 原值 |
| 54 | 2 | 动脉压(mmHg) | `o` | `readInt16LE` |
| 56 | 2 | 静脉压(mmHg) | `H` | `readInt16LE` |
| 58 | 2 | 跨膜压(mmHg) | `J` | `readInt16LE` |
| 60 | 2 | 透析液压(kPa × 10) | `I` | `readInt16LE` 后除以 `10` |
| 62 | 1 | 尿素下降率(%) | `lsxjl` | 原值 |
| 64 | 2 | 实时清除率值(放大 100 倍) | `ssqclz` | 除以 `100` |
| 66 | 2 | 静脉血温(放大 10 倍) | `jmyxh` | 除以 `10` |
| 68 | 2 | 动脉血温(放大 10 倍) | `dmyxw` | 除以 `10` |
| 69 | 1 | 相对血容量(%) | `xdxrl` | 原值 |
## 5.2 当前业务换算说明
### 时间字段
协议原始值单位为“秒”,但当前项目保存为“分钟”:
- `SetTreatmentTime = Math.round(秒 / 60)`
- `K = Math.round(秒 / 60)`
说明:
- 日志和上报中看到的 `SetTreatmentTime`、`K` 现在是“分钟值”。
- `schema.json` 中字段名仍保留旧命名(如 `已透析时间s`),但当前代码语义已改为“分钟”。
### 超滤量字段
协议原始值单位为 `ml`,当前保存为 `L`:
- `A = (ml / 1000).toFixed(3)`
- `B = (ml / 1000).toFixed(3)`
示例:
- `1800 ml` -> `1.800`
- `500 ml` -> `0.500`
## 5.3 压力字段说明
现场抓包已确认以下字段必须按 **有符号数** 解析:
- `o`:动脉压
- `H`:静脉压
- `J`:跨膜压
- `I`:透析液压
原因:
- 动脉压、透析液压等字段可能出现负值
- 若按无符号解析,会出现类似 `65411` 这样的异常大正数
## 6. 报警帧 `0x26`
当前代码 `parseAlarm()` 解码规则:
| 偏移 | 长度 | 含义 | 当前字段 |
|---|---:|---|---|
| 0 | 2 | 报警编号(小端) | `alarmCode` |
| 2 | 1 | 报警类型(0=解除,1=产生) | `bjlx` |
| 3 | 2 | 年(小端) | `bjsj` 组成部分 |
| 5 | 1 | 月 | `bjsj` 组成部分 |
| 6 | 1 | 日 | `bjsj` 组成部分 |
| 7 | 1 | 时 | `bjsj` 组成部分 |
| 8 | 1 | 分 | `bjsj` 组成部分 |
| 9 | 1 | 秒 | `bjsj` 组成部分 |
输出字段:
```json
{
  "alarmCode": 283,
  "bjlx": 1,
  "bjsj": "2026-03-16 10:30:45"
}
```
当前已通过现场帧验证:
- 报警编号需按小端读取
- 年份需按小端读取
- 报警产生/解除状态可以正确区分
## 7. 血压帧 `0x29`
当前代码 `parseBloodPressure()` 解码规则:
| 偏移 | 长度 | 含义 | 当前字段 |
|---|---:|---|---|
| 0 | 1 | 测量模式(0/1=手动/自动) | `bpMode` |
| 1 | 1 | 测量结果(0/1=失败/成功) | `bpResult` |
| 2 | 2 | 年(小端) | `M` 组成部分 |
| 4 | 1 | 月 | `M` 组成部分 |
| 5 | 1 | 日 | `M` 组成部分 |
| 6 | 1 | 时 | `M` 组成部分 |
| 7 | 1 | 分 | `M` 组成部分 |
| 8 | 1 | 秒 | `M` 组成部分 |
| 9 | 2 | 收缩压(小端) | `N` |
| 11 | 2 | 舒张压(小端) | `O` |
| 13 | 2 | 脉搏(小端) | `P` |
| 15 | 2 | 平均动脉压(小端) | `BPMPJDMY` |
输出字段:
```json
{
  "bpMode": 1,
  "bpResult": 1,
  "M": "2026-03-16 10:32:15",
  "N": 143,
  "O": 98,
  "P": 80,
  "BPMPJDMY": 113
}
```
当前已通过现场帧验证:
- 收缩压、舒张压、脉搏、平均压均为小端
- 偏移为 `9 / 11 / 13 / 15`
## 8. 网关输出字段
每次成功解析后,当前统一输出的基础字段包括:
| 字段 | 含义 |
|---|---|
| `suedtime` | 网关接收时间 |
| `deviceType` | 机器类型 |
| `IPAddress` | 设备来源 IP |
| `n` | 机器编号 |
| `jqyxms` | 机器运行模式(中文) |
| `jqyxmsRaw` | 机器运行模式原始值 |
| `frameType` | 帧类型 |
| `protocolVersion` | 协议版本 |
然后按帧类型追加:
- 运行参数字段
- 报警字段
- 血压字段
## 9. 日志追踪
当前项目已经接入本地日志,并可追踪以下关键链路:
- `Device connected`:设备什么时间连上
- `Raw TCP data received`:设备发来了什么原始十六进制数据
- `Parsed frame` / `Dialysis frame parsed`:报文解析成功
- `Dialysis frame payload`:完整解析对象
- `MQTT publish success`:MQTT 上报成功
- `Aliyun postProps success`:阿里云上报成功
- `Device disconnected`:设备何时断开、在线多久、收了多少字节和帧
## 10. 当前注意事项
### 10.1 `schema.json` 与当前业务语义存在部分差异
当前代码已经做了业务换算,但 `schema.json` 的部分名称还是旧含义:
- `K` 的名称仍写成“已透析时间s”,但当前保存值是“分钟”
- `SetTreatmentTime` 当前也已经改为“分钟”
- `A/B` 在 `schema.json` 中是文本类型,当前保存为升值字符串,例如 `1.800`
如果后续要进一步规范,建议:
- 把时间字段名称改成“分钟”
- 把 `A/B` 改成 `float/double` 类型
### 10.2 当前文档以“现有实现”为准
本文档的目标是说明:**当前程序实际是怎么解码和上报的**,便于联调、排障、交付。
如果后续协议理解继续修正,建议同步维护:
- `src/protocol.js`
- `schema.json`
- `docs/current-protocol-decoding.md`
## 11. 建议的后续维护方式
每次现场确认新字段时,建议按下面顺序更新:
1. 在 `src/protocol.js` 中补偏移和解码逻辑
2. 在 `schema.json` 中补 identifier 与字段说明
3. 在本文档中追加“偏移 / 长度 / 含义 / 当前字段 / 单位换算”
4. 保留一条真实抓包样例,方便后续回归验证
---
如果需要,我下一步还可以继续给你输出一份:
1. **更适合交付甲方的版本**(偏业务说明,少代码痕迹)
2. **更适合开发维护的版本**(带完整偏移表和字段映射)
3. **带真实示例帧的版本**(把你已经验证过的十六进制样例附进去)
docs/protocol.md
New file
@@ -0,0 +1,230 @@
# 山外山血透机远程通讯协议(V1.11)整理摘要
> 说明:本文件是根据你提供的协议截图做的技术要点整理,方便在网关程序中实现解析逻辑,非原文逐字抄录。
## 1. 通信总览
- **传输方式**:TCP
- **服务器端口**:默认 `10000`
- **终端角色**:血液透析机作为 TCP 客户端,主动连接透析中心服务器并发送数据
- **信息粒度**:协议以“数据帧”为单位,不同业务含义对应不同“数据帧类型”
## 2. 帧总体结构
每一帧的基本结构如下(按顺序排列):
| 字段           | 长度    | 说明                       |
|----------------|---------|----------------------------|
| 帧头           | 4 字节  | 固定标识,文中示例为 `0x55555555`(十六进制) |
| 机器类型       | 1 字节  | 标识具体机型               |
| 机器编号       | 5 字节  | 机器的出厂编号             |
| 机器运行模式   | 1 字节  | 当前运行模式(待机 / 透析 / 滤过等) |
| 数据帧类型     | 1 字节  | 区分运行参数、报警信息、血压测量等帧 |
| 协议版本       | 1 字节  | 版本号编码(见后文)       |
| 保留           | 7 字节  | 预留字段                   |
| 数据信息       | N 字节  | 实际业务数据区             |
### 2.1 机器类型
机器类型字段为 1 字节,主要取值示意:
- `0x01`:SWS-4000
- `0x02`:SWS-4000A
- `0x31`:SWS-6000
- `0x32`:SWS-6000A
### 2.2 机器编号
- 机器编号为 **5 字节**,表示机器出厂编号,例如文档示例:`6210100001`。
- 协议未在截图中明确说明编码方式,一般实现中可按 **ASCII** 或 **BCD** 存储,需要结合现场抓包确认。
### 2.3 机器运行模式
机器运行模式为 1 字节,常见取值(节选):
| 值    | 模式示意           |
|-------|---------------------|
| 0x00  | 待机                |
| 0x01  | 透析                |
| 0x02  | 滤过 / 净化         |
| 0x03  | 透析滤过            |
| 0x04  | 序贯-透析           |
| 0x05  | 单纯超滤            |
| 0x06  | 序贯-单超           |
| 0x07  | 预充                |
| 0x08  | 清洗                |
| 0x09  | 清洗消毒            |
| 0x0C  | 自检                |
| 0x14  | 透析结束            |
| 0x15  | 清洗结束            |
| 0x16  | 透析滤过结束        |
| 0x17  | 单纯超滤结束        |
| 0x18  | 序贯治疗结束        |
| ...   | 其它模式参见原表    |
> 上表为对截图模式表的概括,具体值-文案映射可按业务需要在网关侧定义常量或枚举,不影响解析。
### 2.4 数据帧类型
数据帧类型字段为 1 字节,主要取值:
- `0x1F`:**运行参数帧**(定期上报透析运行过程中的各种参数)
- `0x26`:**报警信息帧**(产生报警 / 解除报警即时上报)
- `0x29`:**血压测量数据帧**(每次测量血压后即时上报)
### 2.5 协议版本编码
- 协议版本字段为 1 字节。
- 编码规则:取协议版本号中的**数字部分**乘以 100,再转为 16 进制传输。
  - 例如当前版本 `V1.10` → 数值 `1.10 × 100 = 110` → 十六进制 `0x6E`。
## 3. 运行参数帧(0x1F)
### 3.1 报文特性
- **数据帧类型**:`0x1F`
- **数据区长度**:约 220 字节
- **发送时机**:
  - 客户端(透析机)与服务器建立 TCP 连接成功后;
  - 当透析机处于“治疗界面”时,每隔约 **4 秒**向服务器发送一次运行参数帧。
### 3.2 运行参数数据区结构(节选)
数据区由多个字段按固定偏移排布。截图中的表按“偏移值(从 0 开始)+ 字节数 + 含义”的形式列出。下面摘录部分关键字段,辅助和 `schema.json` 中的物模型做对应:
> 注意:偏移和长度根据截图识别,实际以正式文档为准,解析实现时建议结合真实抓包核对。
| 偏移 | 长度 (Byte) | 含义示意                          | 对应物模型(示例) |
|------|-------------|------------------------------------|--------------------|
| 0    | 4           | 设置治疗时间 (s)                  | `SetTreatmentTime` |
| 4    | 4           | 已治疗时间 (s)                    | `K` / 自定义       |
| 8    | 2~4         | 血泵流量 (ml/min)                 | `D`                |
| 10   | 1           | 血泵运行标志 0/1 停止/运行        | `xlyxbj`           |
| 11   | 1           | 抗凝方式 0/1 无抗凝/肝素抗凝      | `klfs`             |
| 12   | 1           | 肝素泵运行标志 0/1 停止/运行      | `z`                |
| 13   | 2~4         | 肝素泵流量 (ml/h,放大 10 倍)     | `E`                |
| 17   | 2~4         | 肝素提前结束时间 (min)            | `gstqjssj`         |
| 21   | 4           | 超滤总量 (ml)                     | `A`                |
| 25   | 4           | 已超滤量 (ml)                     | `B`                |
| 29   | 1           | 超滤泵运行标志 0/1 停止/运行      | `cllyxbj`          |
| 30   | 1           | 旁路标志 0/1 旁路关/旁路开        | `plbj`             |
| 31   | 2~4         | 透析液流量 (ml/min)               | `L`                |
| 33   | 2~4         | 透析液实际温度 (℃,放大 10 倍)   | `F`                |
| 35   | 2~4         | 透析液电导值 (mS/cm,放大 100 倍) | `G`                |
| 37   | 2~4         | 血液总量/超滤率等                 | `C`/自定义        |
| 41   | 4           | 已补入置换液量 (ml)               | `ypyzhyl`          |
| 45   | 1~2         | 补液补入模式 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~2         | 尿素下降率 (%)                    | `lsxjl`            |
| 64   | 1~2         | 实时清除率值 (放大 100 倍)        | `ssqclz`           |
| 66   | 1~2         | 静脉血温 (℃,放大 10 倍)         | `jmyxh`            |
| 68   | 1~2         | 动脉血温 (℃,放大 10 倍)         | `dmyxw`            |
| 69   | 1~2         | 相对血容量 (%)                    | `xdxrl`            |
| 其余 | 若干        | 预留/扩展字段                     | 见原文             |
通过上述偏移-字段对照,可以在程序中将运行参数帧直接拆解成结构化对象,然后再映射到 `schema.json` 中定义的属性标识符。
## 4. 报警信息帧(0x26)
### 4.1 报文特性
- **数据帧类型**:`0x26`
- **数据区长度**:约 150 字节
- **发送时机**:
  - 透析机每当产生报警时,立即发送一个报警信息帧;
  - 报警解除后,同样发送一个报警信息帧,用于表示“报警消除”。
### 4.2 报警信息数据区结构(概要)
截图中给出的字段包括(偏移从 0 开始):
- 报警编号(2 字节)
- 报警类型(1 字节):0/1 → 消除报警 / 产生报警
- 产生报警时间:按 年-月-日-时-分-秒 分别占若干字节存储(类似 `YYYY-MM-DD HH:MM:SS` 拆分编码)
- 预留字段若干
网关侧实现时可将这些字段合并为:
```json
{
  "alarmCode": <number>,
  "alarmType": 0 | 1,  // 0=解除, 1=产生
  "alarmTime": "YYYY-MM-DD HH:MM:SS"
}
```
并按需要记录报警历史或转发至上位系统。
## 5. 血压测量数据帧(0x29)
### 5.1 报文特性
- **数据帧类型**:`0x29`
- **数据区长度**:约 150 字节
- **发送时机**:
  - 客户端与服务器建立 TCP 连接成功后;
  - 血液透析机每测量一次血压,立即发送一帧血压测量数据。
### 5.2 血压测量数据区结构(概要)
根据截图表格,主要包含:
- 测量模式(1 字节):0/1 → 手动测量 / 自动测量
- 测量结果(1 字节):0/1 → 测量出错 / 测量成功
- 测量时间:年、月、日、时、分、秒(各占若干字节,类似报警时间编码方式)
- 血压与心率:
  - 收缩压 (mmHg)
  - 舒张压 (mmHg)
  - 脉搏/心率
  - 平均动脉压 (mmHg)
- 预留字段
在网关中可归纳为:
```json
{
  "bpMode": 0 | 1,        // 手动/自动
  "bpResult": 0 | 1,      // 出错/成功
  "bpTime": "YYYY-MM-DD HH:MM:SS",
  "systolic": <number>,   // 收缩压
  "diastolic": <number>,  // 舒张压
  "heartRate": <number>,  // 心率
  "map": <number>         // 平均动脉压
}
```
## 6. 与现有网关代码的对应关系建议
结合你仓库中的 `schema.json` 与 README 里的物模型介绍,推荐的映射思路:
1. **帧头识别与拆包**
   - 在 `protocol.js` 中:
     - 按 4 字节帧头 = `0x55555555` 查找帧起始;
     - 之后根据固定总长度(例如 4+1+5+1+1+1+7+数据区长度)从流中截取完整帧;
     - 未完整的一部分留在 buffer 中等待下一次数据。
2. **公共头部解析**
   - 把机器类型、机器编号、运行模式、数据帧类型、协议版本先解析出来,挂在统一的 `meta` 上。
3. **按数据帧类型分支解析**
   - 当 `frameType == 0x1F` → 调用 `parseRunParams()`,按上文偏移表解析运行参数;
   - 当 `frameType == 0x26` → 调用 `parseAlarm()`;
   - 当 `frameType == 0x29` → 调用 `parseBloodPressure()`。
4. **与物模型字段对齐**
   - 运行参数里的:超滤总量、已超滤量、血泵流量、动/静/跨膜压、透析液温度/电导、尿素下降率、相对血容量等,对应 `schema.json` 中的 `A/B/C/D/F/G/H/I/J/...` 和诸如 `lsxjl`、`xdxrl` 等自定义标识符;
   - 在解析函数中直接生成以这些 **identifier** 为 key 的对象,方便后续 `PropertyMapper` 直接透传或做二次包装。
5. **时间与缩放处理**
   - 所有“放大 N 倍”的数值(如温度放大 10 倍、电导放大 100 倍、清除率放大 100 倍)建议在解析时还原为物理量(除以 N),再存入缓存;
   - 年/月/日/时/分/秒推荐组合成标准字符串 `YYYY-MM-DD HH:mm:ss`,便于上位系统直接使用。
---
上面这份 `docs/protocol.md` 已经在仓库里生成,你可以打开对照原 PDF 看是否有需要补充或修正的地方。如果你愿意,下一步我可以基于这份整理直接把 `src/protocol.js` 的拆包和字段解析逻辑改成真实实现。需要的话告诉我你希望**优先解析哪些字段**(比如只要运行参数中的 A/B/C/D/F/G/H/I/J 和动静脉压等)。
docs/实施部署文档.md
New file
@@ -0,0 +1,506 @@
# 透析机数据接收网关部署实施文档
> 适用对象:实施工程师、运维工程师
>
> 适用程序:`SWS-Communication` 打包后的 Windows / Linux 服务程序
>
> 当前打包产物:
> - Windows:`dist/index-win.exe`
> - Linux:`dist/index-linux`
## 1. 目标说明
本程序用于接收山外山透析机通过 TCP 主动上报的数据,完成以下工作:
- 接收透析机 TCP 连接
- 解析运行参数帧 / 报警帧 / 血压帧
- 记录本地运行日志
- 按配置转发到 MQTT 或阿里云
- 支持 50 台及以上设备并发接入
## 2. 部署前准备
### 2.1 网络准备
实施前请确认以下条件:
- 服务器与透析机网络互通
- 透析机可以访问网关服务器的 TCP 监听地址和端口
- 防火墙已放行网关 TCP 监听端口
- 如启用 MQTT / 阿里云,上行网络可访问对应服务器
### 2.2 服务器建议配置
#### Windows
- Windows Server 2016 / 2019 / 2022
- 至少 2 核 CPU
- 至少 4 GB 内存
- 至少 10 GB 可用磁盘空间
#### Linux
- CentOS 7+/Rocky 8+/Ubuntu 20.04+
- 至少 2 核 CPU
- 至少 4 GB 内存
- 至少 10 GB 可用磁盘空间
## 3. 部署文件清单
建议部署目录结构如下:
```text
SWS-Gateway/
├─ dist/
│  ├─ index-win.exe        # Windows 可执行文件
│  ├─ index-linux          # Linux 可执行文件
│  ├─ config.json          # 外部配置文件
│  └─ schema.json          # 物模型映射文件(必须放置)
├─ logs/                   # 日志目录(程序自动创建/写入)
└─ doc/                    # 可选文档目录
```
### 3.1 必须文件
实施时至少确保以下文件到位:
- Windows 部署:
  - `dist/index-win.exe`
  - `dist/config.json`
  - `dist/schema.json`
- Linux 部署:
  - `dist/index-linux`
  - `dist/config.json`
  - `dist/schema.json`
### 3.2 重要说明
当前程序启动后会从**可执行文件同目录**读取:
- `config.json`
- `schema.json`
因此:
- `config.json` 必须和可执行文件放在一起
- `schema.json` 也必须和可执行文件放在一起
否则:
- 程序虽然可能能启动
- 但属性映射会退化,影响 MQTT / 阿里云上报字段名称
## 4. 配置文件说明
当前程序默认使用外部 `config.json` 覆盖内置默认配置。
### 4.1 当前示例配置
```json
{
  "tcp": {
    "host": "3.0.0.27",
    "port": 10000,
    "idleTimeoutMs": 1200000
  },
  "http": {
    "enabled": false,
    "host": "0.0.0.0",
    "port": 19001,
    "rateLimit": {
      "singleDeviceMs": 5000,
      "allDevicesMs": 60000
    }
  },
  "mqtt": {
    "enabled": true,
    "url": "mqtt.ihemodialysis.com",
    "port": 62283,
    "username": "data",
    "password": "data#2018",
    "defaultTopicPrefix": "touxiji",
    "retain": true
  },
  "aliyun": {
    "enabled": false,
    "baseURL": "https://things.icoldchain.cn/"
  }
}
```
### 4.2 关键配置项说明
#### TCP 接入
| 配置项 | 说明 | 建议 |
|---|---|---|
| `tcp.host` | TCP 监听地址 | 建议设为 `0.0.0.0` 或服务器实际监听 IP |
| `tcp.port` | TCP 监听端口 | 根据现场约定,常用 `10000` |
| `tcp.idleTimeoutMs` | 设备空闲超时 | 建议 `120000` ~ `1200000` |
> 说明:如果服务器有多个网卡,建议优先使用 `0.0.0.0` 监听所有地址,避免只绑定某一个 IP 后导致透析机无法接入。
#### HTTP(可选)
| 配置项 | 说明 |
|---|---|
| `http.enabled` | 是否启用 HTTP 查询接口 |
| `http.port` | HTTP 端口 |
当前默认可关闭,仅在联调或运维场景需要时启用。
#### MQTT
| 配置项 | 说明 |
|---|---|
| `mqtt.enabled` | 是否启用 MQTT 上报 |
| `mqtt.url` | MQTT 服务地址 |
| `mqtt.port` | MQTT 端口 |
| `mqtt.username` | 用户名 |
| `mqtt.password` | 密码 |
| `mqtt.defaultTopicPrefix` | Topic 前缀 |
| `mqtt.retain` | 是否保留最后一条消息 |
#### 阿里云
| 配置项 | 说明 |
|---|---|
| `aliyun.enabled` | 是否启用阿里云物联网 |
| `aliyun.baseURL` | 获取设备三元组的后端接口地址 |
## 5. 本地日志说明
程序已接入 `winston` 本地日志,默认写入日志文件。
### 5.1 日志目录
当前默认日志目录配置为:
- `../logs`
这意味着:
- 如果可执行文件在 `dist/` 目录下
- 日志会写到 `dist` 上一级目录的 `logs/`
例如:
```text
SWS-Gateway/
├─ dist/
│  ├─ index-win.exe
│  ├─ config.json
│  └─ schema.json
└─ logs/
   ├─ gateway-2026-03-16.log
   └─ gateway-error-2026-03-16.log
```
### 5.2 日志文件类型
- `gateway-YYYY-MM-DD.log`:业务全量日志
- `gateway-error-YYYY-MM-DD.log`:错误日志
### 5.3 可查询内容
日志可追踪以下内容:
- 哪台机器什么时间连上
- 哪台机器发来了什么原始十六进制数据
- 哪台机器解析出了什么数据
- 什么时间 MQTT 上报成功
- 什么时间阿里云上报成功
- 哪台机器什么时间断开、在线多久、总共发了多少帧
## 6. Windows 部署步骤
## 6.1 拷贝文件
在 Windows 服务器上创建目录,例如:
```text
D:\SWS-Gateway\
```
拷贝以下文件:
- `dist/index-win.exe`
- `dist/config.json`
- `schema.json`(从项目根目录拷贝到 `dist/`)
建议最终结构:
```text
D:\SWS-Gateway\
├─ dist\
│  ├─ index-win.exe
│  ├─ config.json
│  └─ schema.json
└─ logs\
```
## 6.2 首次手工启动验证
在 PowerShell 中进入 `dist` 目录执行:
```powershell
cd D:\SWS-Gateway\dist
.\index-win.exe
```
正常情况下应看到:
- 程序启动日志
- `TCP server listening`
- 如启用 MQTT,则看到 `MQTT connected`
## 6.3 防火墙放行端口
如监听 `10000` 端口,可执行:
```powershell
New-NetFirewallRule -DisplayName "SWS Gateway TCP 10000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 10000
```
## 6.4 注册为 Windows 服务(推荐)
推荐使用 `nssm` 注册服务。
### 方式一:使用 NSSM(推荐)
下载 `nssm` 后执行:
```powershell
nssm install SWSGateway
```
在弹出界面中填写:
- `Path`:`D:\SWS-Gateway\dist\index-win.exe`
- `Startup directory`:`D:\SWS-Gateway\dist`
安装完成后执行:
```powershell
nssm start SWSGateway
```
查看状态:
```powershell
nssm status SWSGateway
```
### 方式二:任务计划或开机自启动
如果现场不方便安装 `nssm`,可通过任务计划程序设置“开机自动启动”。
## 7. Linux 部署步骤
## 7.1 拷贝文件
在 Linux 服务器上建议创建目录:
```bash
/opt/sws-gateway/
```
拷贝以下文件:
- `dist/index-linux`
- `dist/config.json`
- `schema.json`(从项目根目录拷贝到 `dist/`)
建议结构:
```text
/opt/sws-gateway/
├─ dist/
│  ├─ index-linux
│  ├─ config.json
│  └─ schema.json
└─ logs/
```
## 7.2 赋予执行权限
```bash
chmod +x /opt/sws-gateway/dist/index-linux
```
## 7.3 首次手工启动验证
```bash
cd /opt/sws-gateway/dist
./index-linux
```
若启动正常,可看到监听日志。
## 7.4 配置防火墙
如使用 `firewalld`:
```bash
firewall-cmd --permanent --add-port=10000/tcp
firewall-cmd --reload
```
如使用 `ufw`:
```bash
ufw allow 10000/tcp
```
## 7.5 注册为 systemd 服务(推荐)
新建文件:
```bash
/etc/systemd/system/sws-gateway.service
```
内容如下:
```ini
[Unit]
Description=SWS Dialysis Gateway
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/sws-gateway/dist
ExecStart=/opt/sws-gateway/dist/index-linux
Restart=always
RestartSec=5
User=root
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
```
启用并启动:
```bash
systemctl daemon-reload
systemctl enable sws-gateway
systemctl start sws-gateway
```
查看状态:
```bash
systemctl status sws-gateway
```
查看日志:
```bash
tail -f /opt/sws-gateway/logs/gateway-$(date +%F).log
```
## 8. 实施联调步骤
建议实施工程师按以下顺序联调:
1. 启动网关程序
2. 确认日志中出现 `TCP server listening`
3. 让一台透析机指向服务器 IP 和端口
4. 观察日志是否出现:
   - `Device connected`
   - `Raw TCP data received`
   - `Dialysis frame parsed`
   - `MQTT publish success` 或 `Aliyun postProps success`
5. 再逐步增加设备数量
6. 最后进行整病区并发接入验证
## 9. 日常运维检查项
每日建议检查:
- 程序进程是否存活
- 日志是否持续有新内容
- `gateway-error-*.log` 是否有连续错误
- MQTT / 阿里云是否上报成功
- 是否有设备频繁断开重连
## 10. 常见问题排查
### 10.1 透析机连不上
排查顺序:
1. 服务器 IP 是否正确
2. 透析机端口配置是否正确
3. 防火墙是否放行
4. 程序是否已启动
5. `tcp.host` 是否绑定错误(建议 `0.0.0.0`)
### 10.2 程序启动了但没有数据
排查顺序:
1. 查看是否有 `Device connected`
2. 查看是否有 `Raw TCP data received`
3. 如果只有连接没有数据,通常是透析机未实际开始发送
4. 如果有原始数据但无解析结果,需核对协议或抓包
### 10.3 MQTT 未收到数据
排查顺序:
1. `config.json` 中 `mqtt.enabled` 是否为 `true`
2. 查看日志中是否有 `MQTT connected`
3. 查看日志中是否有 `MQTT publish success`
4. 检查 Broker 地址、端口、用户名密码是否正确
### 10.4 阿里云未上报成功
排查顺序:
1. `aliyun.enabled` 是否为 `true`
2. 后端获取三元组接口是否可访问
3. 日志中是否有 `Request Aliyun device secret`
4. 日志中是否有 `Aliyun postProps success`
### 10.5 没有日志文件
排查顺序:
1. 检查 `logs` 目录权限
2. 检查 `config.log.toFile` 是否为 `true`
3. 检查程序工作目录和日志目录相对路径是否正确
## 11. 建议实施标准
正式实施时建议遵循:
- Windows / Linux 均采用“服务化启动”
- `config.json` 和 `schema.json` 固定随程序一起部署
- 日志目录单独保留,至少保留 30 天
- 初期上线先从 1 台、5 台、10 台逐步扩容到全病区
- 全病区接入前先使用压测脚本验证并发能力
## 12. 上线前检查清单
上线前请逐项确认:
- [ ] 可执行文件已部署
- [ ] `config.json` 已按现场修改
- [ ] `schema.json` 已放到可执行文件同目录
- [ ] TCP 监听端口已放行
- [ ] MQTT / 阿里云配置已验证
- [ ] 日志目录可写
- [ ] 服务已设置开机自启
- [ ] 已完成单机联调
- [ ] 已完成多机联调
- [ ] 已完成异常断网/重连测试
---
如需进一步交付实施工程师,建议同时附带:
- `docs/current-protocol-decoding.md`
- 一份现场专用 `config.json`
- 一份《实施验收记录表》
package.json
New file
@@ -0,0 +1,44 @@
{
  "name": "sws-communication-gateway",
  "version": "1.0.0",
  "description": "透析机 Socket 通讯网关服务端",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node src/index.js",
    "build": "ncc build src/index.js -o dist",
    "stress:50": "node scripts/stress-50-devices.js",
    "pkg": "npm run build && pkg ./dist/index.js --targets node18-win-x64,node18-linux-x64 --out-path dist",
    "build:exe": "pkg . --targets node18-win-x64 --out-path dist",
    "build:linux": "pkg . --targets node18-linux-x64 --out-path dist",
    "build:all": "pkg . --targets node18-win-x64,node18-linux-x64 --out-path dist"
  },
  "bin": "src/index.js",
  "dependencies": {
    "aliyun-iot-device-sdk": "^1.0.1",
    "axios": "^1.13.6",
    "express": "^4.19.2",
    "mqtt": "^5.7.0",
    "qs": "^6.15.0",
    "winston": "^3.17.0",
    "winston-daily-rotate-file": "^5.0.0"
  },
  "devDependencies": {
    "pkg": "^5.8.1",
    "@vercel/ncc": "^0.38.1"
  },
  "pkg": {
    "assets": [
      "schema.json",
      "config.json"
    ],
    "scripts": [
      "src/**/*.js"
    ],
    "targets": [
      "node18-win-x64",
      "node18-linux-x64"
    ],
    "outputPath": "dist"
  }
}
schema.json
New file
@@ -0,0 +1,636 @@
{
  "schema": "https://iotx-tsl.oss-ap-southeast-1.aliyuncs.com/schema.json",
  "profile": {
    "version": "1.0",
    "productKey": "k08fzbwFdUT"
  },
  "properties": [
    {
      "identifier": "A",
      "name": "超滤总量",
      "accessMode": "rw",
      "desc": "脱水设置总量 单位全部要改成L",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "B",
      "name": "已超滤量",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "C",
      "name": "超滤率",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "F",
      "name": "透析液实际温度",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "G",
      "name": "透析液电导值",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "H",
      "name": "静脉压",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "I",
      "name": "透析液压",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "J",
      "name": "跨膜压",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "K",
      "name": "已透析时间s",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "h",
      "name": "其他报警",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "L",
      "name": "透析液流量",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "M",
      "name": "BPM监测时间",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "X",
      "name": "肝素总量",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "n",
      "name": "机器编号",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "o",
      "name": "动脉压",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "suedtime",
      "name": "传输时间",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "deviceType",
      "name": "机器类型",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "IPAddress",
      "name": "IP地址",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "255"
        }
      }
    },
    {
      "identifier": "deviceName",
      "name": "机器名称",
      "accessMode": "r",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "SetTreatmentTime",
      "name": "设置治疗时间s",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "D",
      "name": "血泵流量",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "unit": "ml/min",
          "unitName": "滴速",
          "step": "1"
        }
      }
    },
    {
      "identifier": "xlyxbj",
      "name": "血泵运行标志",
      "accessMode": "rw",
      "desc": "0/1-停止/运行",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "klfs",
      "name": "抗凝方式",
      "accessMode": "rw",
      "desc": "抗凝方式:0/1—无抗凝/肝素抗凝",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "z",
      "name": "肝素泵运行标志",
      "accessMode": "rw",
      "desc": "肝素泵运行标志:0/1-停止/运行",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "E",
      "name": "肝素泵流量",
      "accessMode": "rw",
      "desc": "肝素泵流量(ml/h,放大 10 倍)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "gstqjssj",
      "name": "肝素提前结束时间",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "unit": "min",
          "unitName": "分钟",
          "step": "1"
        }
      }
    },
    {
      "identifier": "cllyxbj",
      "name": "超滤泵运行标志",
      "accessMode": "rw",
      "desc": "超滤泵运行标志:0/1-停止/运行",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "plbj",
      "name": "旁路标志",
      "accessMode": "rw",
      "desc": "旁路标志:0/1-旁路关闭/旁路打开",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "pyzl",
      "name": "补液总量",
      "accessMode": "rw",
      "desc": "补液总量(ml)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "ypyzhyl",
      "name": "已补入置换液量",
      "accessMode": "rw",
      "desc": "已补入置换液量(ml)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "pyprfs",
      "name": "补液补入模式",
      "accessMode": "rw",
      "desc": "补液补入模式:0/1-前稀释/后稀释",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "ldslq",
      "name": "内毒素滤器1使用时间",
      "accessMode": "rw",
      "desc": "内毒素滤器 1 使用时间(h)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "ldslq2sysj",
      "name": "内毒素滤器2使用时间",
      "accessMode": "rw",
      "desc": "内毒素滤器 2 使用时间(h)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "jqzyxsj",
      "name": "机器总运行时间",
      "accessMode": "rw",
      "desc": "机器总运行时间(min)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "lsxjl",
      "name": "尿素下降率",
      "accessMode": "rw",
      "desc": "尿素下降率(%)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "ssqclz",
      "name": "实时清除率值",
      "accessMode": "rw",
      "desc": "实时清除率值(放大 100 倍)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "jmyxh",
      "name": "静脉血温",
      "accessMode": "rw",
      "desc": "静脉血温(℃,放大 10 倍)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "dmyxw",
      "name": "动脉血温",
      "accessMode": "rw",
      "desc": "动脉血温",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "xdxrl",
      "name": "相对血容量",
      "accessMode": "rw",
      "desc": "相对血容量(%)",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "N",
      "name": "BPM监测-收缩压",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "O",
      "name": "BPM监测-舒张压",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "P",
      "name": "BPM监测-脉搏",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "BPMPJDMY",
      "name": "BPM监测-平均动脉压",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "jqyxms",
      "name": "机器运行模式",
      "accessMode": "rw",
      "desc": "待机,滤过,序贯-透析",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    },
    {
      "identifier": "bjlx",
      "name": "报警类型",
      "accessMode": "rw",
      "desc": "报警类型:0/1-消除报警/产生报警",
      "required": false,
      "dataType": {
        "type": "int",
        "specs": {
          "min": "-2147483648",
          "max": "2147483647",
          "step": "1"
        }
      }
    },
    {
      "identifier": "bjsj",
      "name": "报警时间",
      "accessMode": "rw",
      "required": false,
      "dataType": {
        "type": "text",
        "specs": {
          "length": "10240"
        }
      }
    }
  ]
}
scripts/buildFrames.js
New file
@@ -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
};
scripts/stress-50-devices.js
New file
@@ -0,0 +1,223 @@
const net = require("net");
const config = require("../src/config");
const DEVICE_COUNT = Number(process.env.DEVICE_COUNT || 50);
const DURATION_SEC = Number(process.env.DURATION_SEC || 120);
const INTERVAL_MS = Number(process.env.INTERVAL_MS || 1000);
const ALARM_EVERY = Number(process.env.ALARM_EVERY || 30);
const BP_EVERY = Number(process.env.BP_EVERY || 15);
const HOST = process.env.TCP_HOST || config.tcp.host || "127.0.0.1";
const PORT = Number(process.env.TCP_PORT || config.tcp.port || 19000);
function writeUIntLE(buf, offset, value, bytes) {
  if (bytes === 1) buf.writeUInt8(value, offset);
  if (bytes === 2) buf.writeUInt16LE(value, offset);
  if (bytes === 4) buf.writeUInt32LE(value, offset);
}
function writeInt16LE(buf, offset, value) {
  buf.writeInt16LE(value, offset);
}
function buildHeader(frameType, machineId) {
  const header = Buffer.alloc(20);
  header[0] = 0x55;
  header[1] = 0x55;
  header[2] = 0x55;
  header[3] = 0x55;
  header[4] = 0x31;
  let id = Number(machineId) || 0;
  for (let i = 0; i < 5; i++) {
    header[5 + i] = id & 0xff;
    id = Math.floor(id / 256);
  }
  header[10] = 0x01;
  header[11] = frameType;
  header[12] = 0x6e;
  return header;
}
function buildRunParamsFrame(machineId, elapsedSec) {
  const data = Buffer.alloc(200);
  const setTimeSec = 4 * 3600;
  const doneSec = Math.max(0, elapsedSec);
  writeUIntLE(data, 0, setTimeSec, 4);
  writeUIntLE(data, 4, doneSec, 4);
  writeUIntLE(data, 8, 280, 2);
  writeUIntLE(data, 10, 1, 1);
  writeUIntLE(data, 11, 1, 1);
  writeUIntLE(data, 12, 1, 1);
  writeUIntLE(data, 13, 500, 2);
  writeUIntLE(data, 15, 20, 2);
  const ufTotalMl = 1800;
  const ufDoneMl = Math.min(ufTotalMl, Math.floor(doneSec / 10));
  writeUIntLE(data, 17, ufTotalMl, 4);
  writeUIntLE(data, 21, ufDoneMl, 4);
  writeUIntLE(data, 25, 600, 4);
  writeUIntLE(data, 29, 1, 1);
  writeUIntLE(data, 30, 0, 1);
  writeUIntLE(data, 31, 500, 2);
  writeUIntLE(data, 33, 368, 2);
  writeUIntLE(data, 35, 1450, 2);
  writeUIntLE(data, 37, 1500, 4);
  writeUIntLE(data, 41, Math.min(1500, Math.floor(doneSec / 12)), 4);
  writeUIntLE(data, 45, 0, 1);
  writeUIntLE(data, 46, 120, 2);
  writeUIntLE(data, 48, 100, 2);
  writeUIntLE(data, 50, Math.floor(doneSec / 60), 4);
  writeInt16LE(data, 54, -120);
  writeInt16LE(data, 56, 130);
  writeInt16LE(data, 58, 90);
  writeInt16LE(data, 60, -8);
  writeUIntLE(data, 62, 15, 1);
  writeUIntLE(data, 64, 3500, 2);
  writeUIntLE(data, 66, 365, 2);
  writeUIntLE(data, 68, 366, 2);
  writeUIntLE(data, 69, 82, 1);
  return Buffer.concat([buildHeader(0x1f, machineId), data]);
}
function buildAlarmFrame(machineId) {
  const data = Buffer.alloc(130);
  writeUIntLE(data, 0, 283, 2);
  writeUIntLE(data, 2, 1, 1);
  const now = new Date();
  writeUIntLE(data, 3, now.getFullYear(), 2);
  writeUIntLE(data, 5, now.getMonth() + 1, 1);
  writeUIntLE(data, 6, now.getDate(), 1);
  writeUIntLE(data, 7, now.getHours(), 1);
  writeUIntLE(data, 8, now.getMinutes(), 1);
  writeUIntLE(data, 9, now.getSeconds(), 1);
  return Buffer.concat([buildHeader(0x26, machineId), data]);
}
function buildBpFrame(machineId) {
  const data = Buffer.alloc(130);
  writeUIntLE(data, 0, 1, 1);
  writeUIntLE(data, 1, 1, 1);
  const now = new Date();
  writeUIntLE(data, 2, now.getFullYear(), 2);
  writeUIntLE(data, 4, now.getMonth() + 1, 1);
  writeUIntLE(data, 5, now.getDate(), 1);
  writeUIntLE(data, 6, now.getHours(), 1);
  writeUIntLE(data, 7, now.getMinutes(), 1);
  writeUIntLE(data, 8, now.getSeconds(), 1);
  writeUIntLE(data, 9, 140, 2);
  writeUIntLE(data, 11, 90, 2);
  writeUIntLE(data, 13, 80, 2);
  writeUIntLE(data, 15, 105, 2);
  return Buffer.concat([buildHeader(0x29, machineId), data]);
}
const sockets = [];
let sentFrames = 0;
let sentBytes = 0;
let connectOk = 0;
let connectErr = 0;
let runtimeSocketErr = 0;
let shutdownIgnoredErr = 0;
let stopping = false;
const startTs = Date.now();
for (let i = 0; i < DEVICE_COUNT; i++) {
  const machineId = 6220903000 + i;
  const socket = new net.Socket();
  sockets.push(socket);
  let tick = 0;
  let timer = null;
  socket.connect(PORT, HOST, () => {
    connectOk += 1;
    timer = setInterval(() => {
      tick += 1;
      const elapsedSec = Math.floor((Date.now() - startTs) / 1000);
      const runFrame = buildRunParamsFrame(machineId, elapsedSec);
      socket.write(runFrame);
      sentFrames += 1;
      sentBytes += runFrame.length;
      if (ALARM_EVERY > 0 && tick % ALARM_EVERY === 0) {
        const alarm = buildAlarmFrame(machineId);
        socket.write(alarm);
        sentFrames += 1;
        sentBytes += alarm.length;
      }
      if (BP_EVERY > 0 && tick % BP_EVERY === 0) {
        const bp = buildBpFrame(machineId);
        socket.write(bp);
        sentFrames += 1;
        sentBytes += bp.length;
      }
    }, INTERVAL_MS);
  });
  socket.on("error", (err) => {
    const msg = (err && (err.message || String(err))) || "";
    const code = err && err.code;
    const isExpectedDuringStop =
      stopping && (code === "ECONNRESET" || code === "EPIPE" || code === "ECONNABORTED");
    if (isExpectedDuringStop) {
      shutdownIgnoredErr += 1;
      return;
    }
    runtimeSocketErr += 1;
    connectErr += 1;
    console.error(`[device ${machineId}] socket error:`, code || msg);
  });
  socket.on("close", () => {
    if (timer) clearInterval(timer);
  });
}
setTimeout(() => {
  stopping = true;
  for (const socket of sockets) {
    try {
      socket.end();
      socket.destroy();
    } catch (_) {}
  }
  const duration = Math.max(1, Math.floor((Date.now() - startTs) / 1000));
  const fps = (sentFrames / duration).toFixed(2);
  const kbps = ((sentBytes / 1024) / duration).toFixed(2);
  console.log("\n=== Stress Test Summary ===");
  console.log("host:", HOST, "port:", PORT);
  console.log("devices:", DEVICE_COUNT, "duration(s):", duration);
  console.log("connected:", connectOk, "errors:", connectErr);
  console.log("runtimeSocketErrors:", runtimeSocketErr);
  console.log("ignoredDuringShutdown:", shutdownIgnoredErr);
  console.log("sentFrames:", sentFrames, "fps:", fps);
  console.log("throughput(KB/s):", kbps);
  process.exit(0);
}, DURATION_SEC * 1000);
console.log(
  `Starting stress test: devices=${DEVICE_COUNT}, duration=${DURATION_SEC}s, interval=${INTERVAL_MS}ms, target=${HOST}:${PORT}`
);
scripts/test-alarm.js
New file
@@ -0,0 +1,5 @@
// 测试客户端:发送一帧报警信息帧 (0x26)
const { sendAlarmOnce } = require("./buildFrames");
sendAlarmOnce();
scripts/test-bp.js
New file
@@ -0,0 +1,5 @@
// 测试客户端:发送一帧血压测量数据帧 (0x29)
const { sendBloodPressureOnce } = require("./buildFrames");
sendBloodPressureOnce();
scripts/test-runparams.js
New file
@@ -0,0 +1,5 @@
// 测试客户端:连接到本地网关 TCP 端口,发送一帧运行参数数据
const { sendRunParamsOnce } = require("./buildFrames");
sendRunParamsOnce();
src/aliyunClient.js
New file
@@ -0,0 +1,158 @@
const aliyunIot = require("aliyun-iot-device-sdk");
const logger = require("./logger");
const config = require("./config");
const { getAliyunDeviceSecret } = require("./api");
// 阿里云物联网对接管理器:按设备号获取三元组、建立 iotDevice,并在每次收到数据时上报属性
function createAliyunManager({ propertyMapper }) {
  if (!config.aliyun || !config.aliyun.enabled) {
    logger.info("Aliyun IoT is disabled by config");
    return {
      reportDeviceData: () => {},
      closeAll: () => {}
    };
  }
  // 设备号 -> { iotDevice, productKey, deviceName }
  const deviceMap = new Map();
  async function ensureIotDevice(deviceNumber) {
    let entry = deviceMap.get(deviceNumber);
    if (entry && entry.iotDevice) {
      return entry;
    }
    try {
      logger.info("Request Aliyun device secret", { deviceNumber });
      // 复用旧项目的 api.js 约定:第一个参数是相对路径
      const resp = await getAliyunDeviceSecret(
        "device/info/getAliyunDeviceSecret",
        deviceNumber
      );
      const body = resp && resp.data ? resp.data : null;
      if (!body || !body.data) {
        logger.warn("Aliyun device secret response invalid", {
          deviceNumber,
          body
        });
        return null;
      }
      const data = body.data;
      if (!data.productKey || !data.deviceName || !data.deviceSecret) {
        logger.warn("Aliyun device secret missing fields", {
          deviceNumber,
          data
        });
        return null;
      }
      const productKey = data.productKey;
      const deviceName = data.deviceName;
      const deviceSecret = data.deviceSecret;
      logger.info("Creating Aliyun IoT device", {
        deviceNumber,
        productKey,
        deviceName
      });
      const iotDevice = aliyunIot.device({
        ProductKey: productKey,
        DeviceName: deviceName,
        DeviceSecret: deviceSecret
      });
      iotDevice.on("connect", () => {
        logger.info("Aliyun IoT connected", { deviceNumber, productKey, deviceName });
      });
      iotDevice.on("error", (err) => {
        logger.error("Aliyun IoT error", {
          deviceNumber,
          error: err.message || err
        });
      });
      iotDevice.on("close", () => {
        logger.warn("Aliyun IoT connection closed", { deviceNumber });
      });
      entry = { iotDevice, productKey, deviceName };
      deviceMap.set(deviceNumber, entry);
      return entry;
    } catch (err) {
      logger.error("ensureIotDevice error", {
        deviceNumber,
        error: err.message || err
      });
      return null;
    }
  }
  async function reportDeviceData(deviceNumber, rawData) {
    try {
      const entry = await ensureIotDevice(deviceNumber);
      if (!entry || !entry.iotDevice) {
        return;
      }
      // 使用 PropertyMapper 将内部字段转换为阿里云物模型属性对象
      let props = rawData;
      if (propertyMapper && typeof propertyMapper.transformForAliyun === "function") {
        try {
          props = propertyMapper.transformForAliyun(rawData);
        } catch (e) {
          logger.error("transformForAliyun error", {
            deviceNumber,
            error: e.message || e
          });
          props = rawData;
        }
      }
      // 记录准备上报到阿里云的物模型数据,便于和设备原始数据对照
      logger.info("Aliyun postProps payload", {
        deviceNumber,
        productKey: entry.productKey,
        deviceName: entry.deviceName,
        props
      });
      entry.iotDevice.postProps(props, (res) => {
        if (res && res.message === "success") {
          logger.info("Aliyun postProps success", { deviceNumber });
        } else {
          logger.error("Aliyun postProps failed", { deviceNumber, res });
        }
      });
    } catch (err) {
      logger.error("reportDeviceData error", {
        deviceNumber,
        error: err.message || err
      });
    }
  }
  function closeAll() {
    for (const [deviceNumber, entry] of deviceMap.entries()) {
      try {
        if (entry.iotDevice && typeof entry.iotDevice.end === "function") {
          entry.iotDevice.end();
        }
      } catch (err) {
        logger.error("Close Aliyun device error", {
          deviceNumber,
          error: err.message || err
        });
      }
      deviceMap.delete(deviceNumber);
    }
  }
  return {
    reportDeviceData,
    closeAll
  };
}
module.exports = createAliyunManager;
src/api.js
New file
@@ -0,0 +1,50 @@
const config = require("./config");
async function postForm(path, formData) {
    const baseURL = (config.aliyun && config.aliyun.baseURL) || "https://things.icoldchain.cn/";
    const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
    const url = new URL(path, base).toString();
    const body = new URLSearchParams();
    for (const [key, value] of Object.entries(formData || {})) {
        body.append(key, value == null ? "" : String(value));
    }
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        body: body.toString()
    });
    const text = await response.text();
    let data = text;
    try {
        data = text ? JSON.parse(text) : null;
    } catch (_) {}
    // 兼容旧调用方:返回结构保持 { data, status, headers }
    return {
        data,
        status: response.status,
        headers: Object.fromEntries(response.headers.entries())
    };
}
// 兼容旧项目:按相对路径 + 设备号去请求三元组
function getAliyunDeviceSecret(path, deviceName) {
    return postForm(path, {
        isAutoRegister: 1,
        deviceName
    });
}
function getDevices(path, userClient, productCode) {
    return postForm(path, {
        client_code: userClient,
        product_code: productCode
    });
}
module.exports = { getAliyunDeviceSecret, getDevices };
src/config.js
New file
@@ -0,0 +1,105 @@
// 配置文件:默认配置 + 同级目录 ../config.json 覆盖
// 这样打包成可执行文件 / APK 时,只需修改外部 JSON 即可调整参数。
const fs = require("fs");
const path = require("path");
const defaultConfig = {
  log: {
    // 是否写入本地日志文件
    toFile: true,
    // 日志目录(相对本项目根目录)
    dir: "../logs",
    // 预留日志级别控制(当前未严格使用)
    level: "debug"
  },
  tcp: {
    host: "0.0.0.0",
    // 为避免本机已有程序占用,测试阶段改用 19000 端口
    port: 19000,
    idleTimeoutMs: 2 * 60 * 1000, // 设备超时时间 2 分钟
    // 单连接缓冲区上限,防止异常数据导致内存持续增长
    maxBufferBytes: 1024 * 1024,
    // 每次 data 事件最多解析帧数,避免单连接长时间占用事件循环
    maxFramesPerChunk: 50,
    // TCP 最大并发连接数(覆盖 50 台透析机场景并保留余量)
    maxConnections: 200
  },
  http: {
    enabled: false,
    host: "0.0.0.0",
    // HTTP 服务测试端口改为 19001
    port: 19001,
    cors: {
      enabled: true,
      origins: ["*"]
    },
    rateLimit: {
      singleDeviceMs: 5000,
      allDevicesMs: 60 * 1000
    }
  },
  mqtt: {
    enabled: false,
    url: "mqtt.ihemodialysis.com",
    port: 62283,
    username: "data",
    password: "data#2018",
    reconnectPeriod: 5000,
    defaultTopicPrefix: "touxiji",
    // 是否对每台机的最后一条消息做 retain,方便后续订阅方随时获取最新快照
    retain: true
  },
  aliyun: {
    enabled: true,
    // 旧项目中三元组接口的基础地址,如需更换请修改此处
    baseURL: "https://things.icoldchain.cn/",
    // 其他阿里云物联网接入参数可在此扩展
  }
};
// 递归浅合并:以 external 为准覆盖 default
function mergeConfig(defaultCfg, externalCfg) {
  if (!externalCfg || typeof externalCfg !== "object") return defaultCfg;
  const result = { ...defaultCfg };
  for (const key of Object.keys(externalCfg)) {
    const val = externalCfg[key];
    if (
      val &&
      typeof val === "object" &&
      !Array.isArray(val) &&
      typeof defaultCfg[key] === "object" &&
      defaultCfg[key] !== null
    ) {
      result[key] = mergeConfig(defaultCfg[key], val);
    } else {
      result[key] = val;
    }
  }
  return result;
}
// 运行根目录:
// - 普通 Node 环境下:项目根目录;
// - 使用 pkg 打包后:可执行文件所在目录。
const appRoot = (function () {
  if (process.pkg) {
    // pkg 打包后的场景
    return path.dirname(process.execPath);
  }
  // 开发环境:src 目录的上一级
  return path.resolve(__dirname, "..");
})();
let externalConfig = {};
try {
  const jsonPath = path.join(appRoot, "config.json");
  if (fs.existsSync(jsonPath)) {
    const raw = fs.readFileSync(jsonPath, "utf8");
    externalConfig = JSON.parse(raw);
  }
} catch (e) {
  // 如果 JSON 解析失败,直接忽略,继续使用默认配置
}
module.exports = mergeConfig(defaultConfig, externalConfig);
src/dataCache.js
New file
@@ -0,0 +1,56 @@
class DataCache {
  constructor() {
    this.devices = new Map(); // deviceNumber -> { data, updatedAt }
  }
  setDeviceData(deviceNumber, data) {
    const now = Date.now();
    this.devices.set(deviceNumber, { data, updatedAt: now });
  }
  getDeviceData(deviceNumber) {
    const entry = this.devices.get(deviceNumber);
    return entry || null;
  }
  getAllDeviceData() {
    const result = {};
    for (const [deviceNumber, value] of this.devices.entries()) {
      result[deviceNumber] = value;
    }
    return result;
  }
  getDeviceList() {
    const list = [];
    for (const [deviceNumber, value] of this.devices.entries()) {
      list.push({ deviceNumber, updatedAt: value.updatedAt });
    }
    return list;
  }
  getStats() {
    const list = this.getDeviceList();
    return {
      count: list.length,
      devices: list
    };
  }
  getIdleDevices(timeoutMs) {
    const now = Date.now();
    const result = [];
    for (const [deviceNumber, value] of this.devices.entries()) {
      if (now - value.updatedAt > timeoutMs) {
        result.push({ deviceNumber, updatedAt: value.updatedAt });
      }
    }
    return result;
  }
  clear() {
    this.devices.clear();
  }
}
module.exports = DataCache;
src/deviceManager.js
New file
@@ -0,0 +1,160 @@
const logger = require("./logger");
class DeviceManager {
  constructor({ idleTimeoutMs, onFrameParsed, maxBufferBytes, maxFramesPerChunk }) {
    this.devices = new Map(); // key(ip:port) -> device
    this.idleTimeoutMs = idleTimeoutMs || 2 * 60 * 1000;
    this.onFrameParsed = onFrameParsed;
    this.maxBufferBytes = maxBufferBytes || 1024 * 1024;
    this.maxFramesPerChunk = maxFramesPerChunk || 50;
    // 启动全局超时清理
    const intervalMs = 5 * 60 * 1000;
    setInterval(() => this.cleanupIdleDevices(), intervalMs).unref();
  }
  _keyFromSocket(socket) {
    return `${socket.remoteAddress}:${socket.remotePort}`;
  }
  addConnection(socket) {
    const key = this._keyFromSocket(socket);
    const connectedAt = Date.now();
    const device = {
      key,
      socket,
      status: "pending",
      connectedAt,
      lastAck: connectedAt,
      buffer: Buffer.alloc(0),
      totalBytes: 0,
      totalFrames: 0
    };
    this.devices.set(key, device);
    logger.info("Device connected", {
      key,
      connectedAt: new Date(connectedAt).toISOString()
    });
    return device;
  }
  removeConnection(socket) {
    const key = this._keyFromSocket(socket);
    const device = this.devices.get(key);
    if (device) {
      this.devices.delete(key);
      logger.info("Device disconnected", {
        key,
        deviceNumber: device.deviceNumber,
        connectedAt: device.connectedAt
          ? new Date(device.connectedAt).toISOString()
          : undefined,
        disconnectedAt: new Date().toISOString(),
        onlineMs: device.connectedAt ? Date.now() - device.connectedAt : undefined,
        totalBytes: device.totalBytes || 0,
        totalFrames: device.totalFrames || 0
      });
    }
  }
  handleData(socket, data, protocolParser) {
    const key = this._keyFromSocket(socket);
    let device = this.devices.get(key);
    if (!device) {
      device = this.addConnection(socket);
    }
    device.lastAck = Date.now();
    device.status = "valid";
    device.totalBytes = (device.totalBytes || 0) + data.length;
    // 累加 buffer
    device.buffer = Buffer.concat([device.buffer, data]);
    // 防御:异常数据流导致 buffer 无上限增长时,保留尾部并告警
    if (device.buffer.length > this.maxBufferBytes) {
      const keepBytes = Math.floor(this.maxBufferBytes / 2);
      device.buffer = device.buffer.slice(device.buffer.length - keepBytes);
      logger.warn("Device buffer exceeded limit, truncated", {
        key,
        maxBufferBytes: this.maxBufferBytes,
        keepBytes
      });
    }
    // 按协议拆包
    let frames = [];
    try {
      const extracted = protocolParser.extractFrames(device.buffer);
      frames = extracted.frames || [];
      device.buffer = extracted.remaining || Buffer.alloc(0);
    } catch (err) {
      logger.error("extractFrames failed", { key, error: err.message || err });
      // 拆包失败时清空本连接 buffer,避免脏数据反复触发异常
      device.buffer = Buffer.alloc(0);
      return;
    }
    if (frames.length > this.maxFramesPerChunk) {
      logger.warn("Too many frames in one chunk, capping parse count", {
        key,
        frames: frames.length,
        cap: this.maxFramesPerChunk
      });
      frames = frames.slice(0, this.maxFramesPerChunk);
    }
    for (const frame of frames) {
      try {
        const { deviceNumber, data: parsed } = protocolParser.parseFrame(
          frame,
          socket.remoteAddress
        );
        const previousDeviceNumber = device.deviceNumber;
        device.status = "registered";
        device.deviceNumber = deviceNumber;
        device.lastFrame = parsed;
        device.totalFrames = (device.totalFrames || 0) + 1;
        if (!previousDeviceNumber || previousDeviceNumber !== deviceNumber) {
          logger.info("Device identified", { key, deviceNumber });
        }
        if (typeof this.onFrameParsed === "function") {
          this.onFrameParsed({ device, deviceNumber, data: parsed });
        }
      } catch (err) {
        logger.error("Failed to parse frame", { key, error: err.message || err });
      }
    }
  }
  cleanupIdleDevices() {
    const now = Date.now();
    for (const [key, device] of this.devices.entries()) {
      if (now - device.lastAck > this.idleTimeoutMs) {
        logger.warn("Device idle timeout, closing", { key });
        try {
          device.socket.destroy();
        } catch (e) {
          // ignore
        }
        this.devices.delete(key);
      }
    }
  }
  getStats() {
    const list = [];
    for (const [key, device] of this.devices.entries()) {
      list.push({
        key,
        status: device.status,
        lastAck: device.lastAck,
        deviceNumber: device.deviceNumber
      });
    }
    return list;
  }
}
module.exports = DeviceManager;
src/httpServer.js
New file
@@ -0,0 +1,151 @@
const express = require("express");
const config = require("./config");
const logger = require("./logger");
function createCorsMiddleware() {
  return (req, res, next) => {
    if (!config.http.cors || !config.http.cors.enabled) {
      return next();
    }
    const origin = req.headers.origin || "*";
    const allowed =
      config.http.cors.origins.includes("*") ||
      config.http.cors.origins.includes(origin);
    if (allowed) {
      res.header("Access-Control-Allow-Origin", origin);
      res.header("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
      res.header("Access-Control-Allow-Headers", "Content-Type");
    }
    if (req.method === "OPTIONS") {
      return res.sendStatus(204);
    }
    next();
  };
}
function createHttpServer({ dataCache, rateLimiter, propertyMapper, deviceManager }) {
  if (!config.http || !config.http.enabled) {
    logger.info("HTTP server is disabled by config");
    return null;
  }
  const app = express();
  app.use(express.json({ limit: "1mb" }));
  app.use(createCorsMiddleware());
  app.get("/api/device/data", (req, res) => {
    const deviceNumber = req.query.deviceNumber;
    const mapped = String(req.query.mapped || "false").toLowerCase() === "true";
    if (!deviceNumber) {
      return res.status(400).json({ error: "deviceNumber is required" });
    }
    const limit = rateLimiter.checkLimit(
      `device:${deviceNumber}`,
      config.http.rateLimit.singleDeviceMs
    );
    if (!limit.allowed) {
      return res.status(429).json({
        error: "too many requests",
        waitMs: limit.waitMs
      });
    }
    const entry = dataCache.getDeviceData(deviceNumber);
    if (!entry) {
      return res.status(404).json({ error: "device not found" });
    }
    const base = mapped
      ? propertyMapper.transformForHTTP(entry.data, deviceNumber)
      : { deviceNumber, data: entry.data };
    return res.json({ ...base, updatedAt: entry.updatedAt });
  });
  app.get("/api/device/all", (req, res) => {
    const mapped = String(req.query.mapped || "false").toLowerCase() === "true";
    const limit = rateLimiter.checkLimit(
      "__all_devices__",
      config.http.rateLimit.allDevicesMs
    );
    if (!limit.allowed) {
      return res.status(429).json({
        error: "too many requests",
        waitMs: limit.waitMs
      });
    }
    const all = dataCache.getAllDeviceData();
    const result = {};
    for (const [deviceNumber, entry] of Object.entries(all)) {
      if (mapped) {
        result[deviceNumber] = {
          ...propertyMapper.transformForHTTP(entry.data, deviceNumber),
          updatedAt: entry.updatedAt
        };
      } else {
        result[deviceNumber] = {
          deviceNumber,
          data: entry.data,
          updatedAt: entry.updatedAt
        };
      }
    }
    return res.json(result);
  });
  app.get("/api/device/list", (req, res) => {
    return res.json(dataCache.getDeviceList());
  });
  app.get("/api/cache/stats", (req, res) => {
    return res.json(dataCache.getStats());
  });
  app.post("/api/cache/clear", (req, res) => {
    dataCache.clear();
    return res.json({ ok: true });
  });
  app.get("/api/device/idle", (req, res) => {
    const timeout = parseInt(req.query.timeout, 10);
    const timeoutMs = Number.isFinite(timeout) && timeout > 0 ? timeout : 5 * 60 * 1000;
    return res.json(dataCache.getIdleDevices(timeoutMs));
  });
  app.get("/api/ratelimit/stats", (req, res) => {
    return res.json(rateLimiter.getStats());
  });
  app.post("/api/ratelimit/clear", (req, res) => {
    rateLimiter.clear();
    return res.json({ ok: true });
  });
  app.get("/api/health", (req, res) => {
    res.json({
      status: "ok",
      tcp: "listening",
      devices: deviceManager ? deviceManager.getStats() : []
    });
  });
  const server = app.listen(config.http.port, config.http.host, () => {
    logger.info("HTTP server listening", {
      host: config.http.host,
      port: config.http.port
    });
  });
  return server;
}
module.exports = createHttpServer;
src/index.js
New file
@@ -0,0 +1,105 @@
// 整个网关程序的入口文件
// 负责:
// 1)初始化各个核心模块(缓存、限流、物模型映射、MQTT、阿里云等);
// 2)创建 TCP Socket 服务,用于接收透析机数据;
// 3)创建 HTTP 服务,对外提供查询与运维接口;
// 4)统一处理优雅关闭逻辑(Ctrl+C / 进程退出信号)。
const logger = require("./logger");
const config = require("./config");
const DataCache = require("./dataCache");
const RateLimiter = require("./rateLimiter");
const PropertyMapper = require("./propertyMapper");
const createMqttClient = require("./mqttClient");
const createAliyunManager = require("./aliyunClient");
const createTcpServer = require("./tcpServer");
const createHttpServer = require("./httpServer");
function main() {
  // 进程级兜底:记录未捕获异常,避免因为单点问题导致静默退出
  process.on("uncaughtException", (err) => {
    logger.error("uncaughtException", err && (err.stack || err.message || err));
  });
  process.on("unhandledRejection", (reason) => {
    logger.error("unhandledRejection", reason && (reason.stack || reason.message || reason));
  });
  // 启动日志,方便从日志文件中定位一次完整的运行周期
  logger.info("Starting SWS Communication Gateway...");
  // 初始化核心业务模块
  // dataCache:按设备号缓存“最新一帧”透析机数据
  // rateLimiter:HTTP 接口访问限流
  // propertyMapper:协议字段到物模型属性的映射
  const dataCache = new DataCache();
  const rateLimiter = new RateLimiter();
  const propertyMapper = new PropertyMapper();
  // 创建 MQTT 客户端(可通过配置开启/关闭)
  // publishDeviceData:对外暴露的简单发布函数
  const { client: mqttClient, publishDeviceData } = createMqttClient();
  // 启动时输出阿里云传输开关状态,便于现场快速确认配置是否生效
  if (config.aliyun && config.aliyun.enabled) {
    logger.info("Aliyun IoT transfer enabled", {
      baseURL: config.aliyun.baseURL
    });
  } else {
    logger.info("Aliyun IoT transfer disabled by config");
  }
  // 阿里云物联网对接管理器
  // 封装:根据设备号自动获取三元组、建立连接,并在收到数据时上报属性
  const aliyunManager = createAliyunManager({ propertyMapper });
  // 创建 TCP Socket 服务器
  // - dataCache:在 onFrameParsed 时写入最新一帧数据
  // - mqttPublisher:可选,解析成功后往 MQTT 推送
  // - aliyunReporter:可选,解析成功后上报阿里云物联网
  const { server: tcpServer, deviceManager } = createTcpServer({
    dataCache,
    mqttPublisher: config.mqtt && config.mqtt.enabled ? publishDeviceData : null,
    aliyunReporter:
      config.aliyun && config.aliyun.enabled
        ? (deviceNumber, data) => aliyunManager.reportDeviceData(deviceNumber, data)
        : null
  });
  // 创建 HTTP 服务,对外提供数据查询与运维接口
  const httpServer = createHttpServer({
    dataCache,
    rateLimiter,
    propertyMapper,
    deviceManager
  });
  // 统一的优雅退出逻辑:
  // - 关闭 TCP / HTTP 监听
  // - 断开 MQTT / 阿里云连接
  // - 最后退出进程
  function shutdown() {
    logger.warn("Shutting down gateway...");
    try {
      if (tcpServer) tcpServer.close();
    } catch (e) {}
    try {
      if (httpServer) httpServer.close();
    } catch (e) {}
    try {
      if (mqttClient) mqttClient.end(true);
    } catch (e) {}
    try {
      if (aliyunManager && typeof aliyunManager.closeAll === "function") {
        aliyunManager.closeAll();
      }
    } catch (e) {}
    process.exit(0);
  }
  // 进程信号监听:在 Ctrl+C 或系统退出时触发优雅关闭
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
}
// 直接启动主函数
main();
src/logger.js
New file
@@ -0,0 +1,100 @@
const fs = require("fs");
const path = require("path");
const winston = require("winston");
const DailyRotateFile = require("winston-daily-rotate-file");
const config = require("./config");
function resolveLogDir() {
  const dirFromConfig = (config.log && config.log.dir) || "logs";
  const appRoot = process.pkg
    ? path.dirname(process.execPath)
    : path.resolve(__dirname, "..");
  return path.isAbsolute(dirFromConfig)
    ? dirFromConfig
    : path.resolve(appRoot, dirFromConfig);
}
const logDir = resolveLogDir();
try {
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }
} catch (_) {}
const logLevel = (config.log && config.log.level) || "info";
const enableFileLog = !config.log || config.log.toFile !== false;
const logFormat = winston.format.combine(
  winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSS" }),
  winston.format.errors({ stack: true }),
  winston.format.printf((info) => {
    const base = `[${info.timestamp}] [${info.level.toUpperCase()}] ${info.message}`;
    const extra = { ...info };
    delete extra.level;
    delete extra.message;
    delete extra.timestamp;
    const hasExtra = Object.keys(extra).length > 0;
    if (!hasExtra) return base;
    try {
      return `${base} ${JSON.stringify(extra)}`;
    } catch (_) {
      return base;
    }
  })
);
const transports = [
  new winston.transports.Console({ level: logLevel })
];
if (enableFileLog) {
  transports.push(
    new DailyRotateFile({
      level: logLevel,
      dirname: logDir,
      filename: "gateway-%DATE%.log",
      datePattern: "YYYY-MM-DD",
      maxSize: "50m",
      maxFiles: "30d"
    })
  );
  transports.push(
    new DailyRotateFile({
      level: "error",
      dirname: logDir,
      filename: "gateway-error-%DATE%.log",
      datePattern: "YYYY-MM-DD",
      maxSize: "50m",
      maxFiles: "30d"
    })
  );
}
const baseLogger = winston.createLogger({
  level: logLevel,
  format: logFormat,
  transports,
  exitOnError: false
});
const logger = {
  debug(message, extra) {
    if (extra === undefined) baseLogger.debug(message);
    else baseLogger.debug(message, extra);
  },
  info(message, extra) {
    if (extra === undefined) baseLogger.info(message);
    else baseLogger.info(message, extra);
  },
  warn(message, extra) {
    if (extra === undefined) baseLogger.warn(message);
    else baseLogger.warn(message, extra);
  },
  error(message, extra) {
    if (extra === undefined) baseLogger.error(message);
    else baseLogger.error(message, extra);
  }
};
module.exports = logger;
src/mqttClient.js
New file
@@ -0,0 +1,73 @@
const mqtt = require("mqtt");
const config = require("./config");
const logger = require("./logger");
function createMqttClient() {
  if (!config.mqtt || !config.mqtt.enabled) {
    logger.info("MQTT is disabled by config");
    return {
      client: null,
      publishDeviceData: () => {}
    };
  }
  // 兼容当前配置结构:url 为主机名,port 为端口
  const host = config.mqtt.url || "127.0.0.1";
  const port = config.mqtt.port || 1883;
  const brokerUrl = `mqtt://${host}:${port}`;
  const client = mqtt.connect(brokerUrl, {
    username: config.mqtt.username || undefined,
    password: config.mqtt.password || undefined,
    clientId: config.mqtt.clientId,
    reconnectPeriod: config.mqtt.reconnectPeriod
  });
  client.on("connect", () => {
    logger.info("MQTT connected", { url: brokerUrl });
  });
  client.on("reconnect", () => {
    logger.info("MQTT reconnecting");
  });
  client.on("error", (err) => {
    logger.error("MQTT error", err.message || err);
  });
  client.on("close", () => {
    logger.warn("MQTT connection closed");
  });
  function publishDeviceData(deviceNumber, data) {
    if (!client.connected) {
      logger.warn("MQTT not connected, skip publish");
      return;
    }
    // Topic 前缀优先取配置的 defaultTopicPrefix,否则退回 "dialysis"
    const prefix = config.mqtt.defaultTopicPrefix || config.mqtt.topicPrefix || "dialysis";
    const topic = `${prefix}/${deviceNumber}`;
    const payload = JSON.stringify({
      deviceId: data.deviceNumber,
      ts: Date.now(),
      ...data
    });
    // 记录发送到 MQTT 的物模型/数据内容,便于对比阿里云或下游收到的报文
    logger.info("MQTT publish", {
      topic,
      deviceNumber,
      ...data
    });
    client.publish(topic, payload, { qos: 0, retain: !!config.mqtt.retain }, (err) => {
      if (err) {
        logger.error("MQTT publish error", err.message || err);
      } else {
        logger.info("MQTT publish success", { topic, deviceNumber });
      }
    });
  }
  return { client, publishDeviceData };
}
module.exports = createMqttClient;
src/propertyMapper.js
New file
@@ -0,0 +1,74 @@
const fs = require("fs");
const path = require("path");
const logger = require("./logger");
class PropertyMapper {
  constructor() {
    this.map = new Map(); // identifier -> { identifier, name, ... }
    this.loadSchema();
  }
  loadSchema() {
    try {
      // 在普通 Node 环境下,schema.json 位于项目根目录;
      // 使用 pkg 打包后,优先从可执行文件同级目录读取 schema.json,便于现场替换。
      const appRoot = process.pkg
        ? path.dirname(process.execPath)
        : path.join(__dirname, "..");
      const schemaPath = path.join(appRoot, "schema.json");
      const raw = fs.readFileSync(schemaPath, "utf8");
      const json = JSON.parse(raw);
      if (Array.isArray(json.properties)) {
        for (const prop of json.properties) {
          if (prop.identifier) {
            this.map.set(prop.identifier, prop);
          }
        }
      }
      logger.info("Property schema loaded", { count: this.map.size });
    } catch (err) {
      logger.warn("Failed to load schema.json, using empty mapping", err.message || err);
    }
  }
  mapData(rawData) {
    const result = [];
    if (!rawData || typeof rawData !== "object") return result;
    for (const [key, value] of Object.entries(rawData)) {
      const def = this.map.get(key) || { identifier: key, name: key };
      result.push({
        identifier: def.identifier,
        name: def.name,
        value
      });
    }
    return result;
  }
  transformForHTTP(rawData, deviceNumber) {
    return {
      deviceNumber,
      properties: this.mapData(rawData)
    };
  }
  transformForMQTT(rawData, deviceNumber) {
    return {
      deviceNumber,
      timestamp: Date.now(),
      properties: this.mapData(rawData)
    };
  }
  transformForAliyun(rawData) {
    // 阿里云物模型上报通常直接使用 identifier -> value
    const result = {};
    if (!rawData || typeof rawData !== "object") return result;
    for (const [key, value] of Object.entries(rawData)) {
      result[key] = value;
    }
    return result;
  }
}
module.exports = PropertyMapper;
src/protocol.js
New file
@@ -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;
src/rateLimiter.js
New file
@@ -0,0 +1,33 @@
class RateLimiter {
  constructor() {
    this.records = new Map(); // key -> lastTs
  }
  checkLimit(key, intervalMs) {
    const now = Date.now();
    const last = this.records.get(key) || 0;
    const diff = now - last;
    if (diff < intervalMs) {
      return {
        allowed: false,
        waitMs: intervalMs - diff
      };
    }
    this.records.set(key, now);
    return { allowed: true, waitMs: 0 };
  }
  getStats() {
    const result = [];
    for (const [key, ts] of this.records.entries()) {
      result.push({ key, lastAt: ts });
    }
    return result;
  }
  clear() {
    this.records.clear();
  }
}
module.exports = RateLimiter;
src/tcpServer.js
New file
@@ -0,0 +1,156 @@
const net = require("net");
const config = require("./config");
const logger = require("./logger");
const DeviceManager = require("./deviceManager");
const ProtocolParser = require("./protocol");
// 将解析后的数据整理成人类可读的中文列表,便于在日志中快速查看关键参数
function formatFrameSummary(data) {
  const lines = [];
  lines.push(`机器号: ${data.n || ""}`);
  lines.push(`机型: ${data.deviceType || ""}`);
  lines.push(`运行模式: ${data.jqyxms || ""}`);
  // SetTreatmentTime 和 K 现在已在解析阶段转换为“分钟”
  lines.push(
    `治疗时间(设/已)(分): ${data.SetTreatmentTime ?? ""} / ${data.K ?? ""}`
  );
  // A/B/C 已在解析阶段转换为“升(L)”或“升每小时(L/h)”并保留三位小数
  lines.push(`超滤总量A/已超滤量B(L): ${data.A ?? ""} / ${data.B ?? ""}`);
  lines.push(`超滤率C(L/h): ${data.C ?? ""}`);
  lines.push(`血泵流量D(ml/min): ${data.D ?? ""}`);
  lines.push(`透析液流量L(ml/min): ${data.L ?? ""}`);
  lines.push(
    `动/静脉压/跨膜压(mmHg): ${data.o ?? ""} / ${data.H ?? ""} / ${
      data.J ?? ""
    }`
  );
  if (data.N != null || data.O != null || data.P != null || data.BPMPJDMY != null) {
    lines.push(
      `血压N/O/P/平均压(mmHg,bpm): ${data.N ?? ""} / ${data.O ?? ""} / ${
        data.P ?? ""
      } / ${data.BPMPJDMY ?? ""}`
    );
  }
  if (data.alarmCode != null || data.bjlx != null || data.bjsj) {
    lines.push(
      `报警编号/类型/时间: ${data.alarmCode ?? ""} / ${
        data.bjlx ?? ""
      } / ${data.bjsj || ""}`
    );
  }
    if (data.pyzl != null || data.ypyzhyl != null) {
      lines.push(
        `补液总量pyzl/已补入置换液量(ml): ${data.pyzl ?? ""} / ${
          data.ypyzhyl ?? ""
        }`
      );
    }
  return lines.join("; ");
}
function createTcpServer({ dataCache, mqttPublisher, aliyunReporter }) {
  const protocolParser = new ProtocolParser();
  const deviceManager = new DeviceManager({
    idleTimeoutMs: config.tcp.idleTimeoutMs,
    maxBufferBytes: config.tcp.maxBufferBytes,
    maxFramesPerChunk: config.tcp.maxFramesPerChunk,
    onFrameParsed: ({ deviceNumber, data }) => {
      try {
        // 每次成功解析一帧透析机数据时,记录一条带中文说明的概要日志,方便排查问题
        logger.info("Dialysis frame parsed", {
          deviceNumber,
          frameType: data.frameType,
          runMode: data.jqyxms,
          ip: data.IPAddress,
          suedtime: data.suedtime,
          summary: formatFrameSummary(data)
        });
        // 同时输出完整解析结果,便于查看所有字段
        logger.debug("Dialysis frame payload", { deviceNumber, payload: data });
        if (dataCache) {
          dataCache.setDeviceData(deviceNumber, data);
        }
        if (mqttPublisher) {
          try {
            mqttPublisher(deviceNumber, data);
          } catch (err) {
            logger.error("mqttPublisher error", {
              deviceNumber,
              error: err.message || err
            });
          }
        }
        if (aliyunReporter) {
          // 阿里云上报是异步的,这里不等待其完成
          Promise.resolve(aliyunReporter(deviceNumber, data)).catch((err) => {
            logger.error("aliyunReporter error", {
              deviceNumber,
              error: err.message || err
            });
          });
        }
      } catch (err) {
        logger.error("onFrameParsed handler error", err.message || err);
      }
    }
  });
  const server = net.createServer((socket) => {
    const key = `${socket.remoteAddress}:${socket.remotePort}`;
    logger.info("Incoming TCP connection", { key });
    deviceManager.addConnection(socket);
    socket.setNoDelay(true);
    socket.setKeepAlive(true, 30 * 1000);
    socket.on("data", (chunk) => {
      // 记录收到的原始 TCP 数据(十六进制),便于现场抓包对比
      try {
        const hex = chunk.toString("hex").match(/.{1,2}/g)?.join(" ") || "";
        logger.debug("Raw TCP data received", { key, length: chunk.length, hex });
      } catch (e) {
        logger.error("Failed to log raw TCP data", { key, error: e.message || e });
      }
      try {
        deviceManager.handleData(socket, chunk, protocolParser);
      } catch (err) {
        logger.error("handleData crashed", { key, error: err.message || err });
      }
    });
    socket.on("close", () => {
      deviceManager.removeConnection(socket);
    });
    socket.on("error", (err) => {
      logger.error("Socket error", { key, error: err.message || err });
      deviceManager.removeConnection(socket);
    });
  });
  server.on("error", (err) => {
    logger.error("TCP server error", err.message || err);
  });
  if (config.tcp.maxConnections) {
    server.maxConnections = config.tcp.maxConnections;
  }
  server.listen(config.tcp.port, config.tcp.host, () => {
    logger.info("TCP server listening", {
      host: config.tcp.host,
      port: config.tcp.port
    });
  });
  return { server, deviceManager };
}
module.exports = createTcpServer;
src/结果.txt
New file
@@ -0,0 +1,106 @@
{\rtf1\ansi\ansicpg936\deff0\deflang1033\deflangfe2052{\fonttbl{\f0\fnil\fcharset134 \'cb\'ce\'cc\'e5;}}
\viewkind4\uc1\pard\lang2052\f0\fs18\'a1\'beReceive from 192.168.50.74 : 53168\'a1\'bf:
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 FE 00 01 EA 07 03 0D 16 27 22 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 19 0A 18 00 00 00 9F BF 6D A7 00 24 7E A7 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F BF 6D A7 5B 00 00 00 00 D7 19 0A 18 00 00 00 9F BF 6D A7 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 19 0A 18 00 00 00 30 2455 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 FD 00 01 EA 07 03 0D 16 27 2A 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 19 0A 18 00 00 00 9F BF 6D A7 00 24 7E A7 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F BF 6D A7 5B 00 00 00 00 D7 19 0A 18 00 00 00 9F BF 6D A7 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 19 0A 18 00 00 00 30 2455 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 1B 01 00 EA 07 03 0D 16 27 2F 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 19 0A 18 00 00 00 9F BF 6D A7 00 24 7E A7 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F BF 6D A7 5B 00 00 00 00 D7 19 0A 18 00 00 00 9F BF 6D A7 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 19 0A 18 00 00 00 30 24 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 FE 00 00 EA 07 03 0D 16 27 2F 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 19 0A 18 00 00 00 9F BF 6D A7 00 24 7E A7 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F BF 6D A7 5B 00 00 00 00 D7 19 0A 18 00 00 00 9F BF 6D A7 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 19 0A 18 00 00 00 30 24 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 FD 00 00 EA 07 03 0D 16 27 2F 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 19 0A 18 00 00 00 9F BF 6D A7 00 24 7E A7 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F BF 6D A7 5B 00 00 00 00 D7 19 0A 18 00 00 00 9F BF 6D A7 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 19 0A 18 00 00 00 30 24
\par \'a1\'beReceive from 192.168.50.74 : 32818\'a1\'bf:
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 01 EA 07 03 0E 06 38 34 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 01 EA 07 03 0E 06 38 34 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 9C 01 01 EA 07 03 0E 06 3B 27 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 00 EA 07 03 0E 07 07 08 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 00 EA 07 03 0E 08 1E 19 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 64 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 9C 01 00 EA 07 03 0E 08 1E 19 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 01 EA 07 03 0E 08 1E 19 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0B 26 6E 00 00 00 00 00 00 00 FE 00 00 EA 07 03 0E 08 1E 23 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 6455 55 55 55 31 E1 73 CB 72 01 0B 26 6E 00 00 00 00 00 00 00 62 01 01 EA 07 03 0E 08 1E 27 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 64
\par 55 55 55 55 31 E1 73 CB 72 01 0B 26 6E 00 00 00 00 00 00 00 62 01 00 EA 07 03 0E 08 1E 2D 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 11 B0 08 18 00 00 00 9F FF F6 AC 00 64 07 AD 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F FF F6 AC 5B 00 00 00 00 17 B0 08 18 00 00 00 9F FF F6 AC 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 17 B0 08 18 00 00 00 30 64
\par \'a1\'beReceive from 192.168.50.74 : 43154\'a1\'bf:
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 01 EA 07 03 0E 09 1A 1B 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 01 EA 07 03 0E 09 1A 1B 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 00 EA 07 03 0E 09 1A 25 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 00 EA 07 03 0E 09 1B 08 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 1B 01 01 EA 07 03 0E 09 26 19 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 FD 00 01 EA 07 03 0E 09 28 24 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 21 E1 09 18 00 00 00 9F 5F 64 AE 00 C4 74 AE 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 5F 64 AE 5B 00 00 00 00 27 E1 09 18 00 00 00 9F 5F 64 AE 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 27 E1 09 18 00 00 00 30 C4
\par
\par \'a1\'beReceive from 192.168.50.74 : 47778\'a1\'bf:
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 01 EA 07 03 0E 0C 1C 31 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 01 EA 07 03 0E 0C 1C 31 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FD 00 00 EA 07 03 0E 0C 1E 38 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 0C 26 6E 00 00 00 00 00 00 00 FE 00 00 EA 07 03 0E 0C 20 18 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 1B 01 01 EA 07 03 0E 0C 29 32 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 1B 01 00 EA 07 03 0E 0C 2B 33 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 14 01 01 EA 07 03 0E 0C 30 38 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 00 26 6E 00 00 00 00 00 00 00 14 01 00 EA 07 03 0E 0C 30 39 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 07 26 6E 00 00 00 00 00 00 00 38 01 01 EA 07 03 0E 0D 0A 27 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 07 26 6E 00 00 00 00 00 00 00 38 01 00 EA 07 03 0E 0D 0A 28 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 40 D1 0F 0A 18 00 00 00 9F 1F B2 B6 00 84 C2 B6 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 9F 1F B2 B6 5B 00 00 00 00 D7 0F 0A 18 00 00 00 9F 1F B2 B6 00 00 00 00 10 27 00 00 14 00 00 00 03 00 00 00 03 00 00 00 00 00 00 00 38 00 00 00 00 00 00 00 5B 00 00 00 18 D7 0F 0A 18 00 00 00 30 84
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 33 00 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 07 00 00 00 1B 02 00 00 01 00 F4 01 73 01 76 05 98 3A 00 00 3D 00 00 00 00 F0 10 F0 10 86 B8 09 00 83 FF 3D 00 77 00 FD FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 6F 00 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 10 00 00 00 1B 02 00 00 01 00 F4 01 72 01 77 05 98 3A 00 00 88 00 00 00 00 F0 10 F0 10 86 B8 09 00 83 FF 5C 00 96 00 FD FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 AB 00 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 19 00 00 00 1B 02 00 00 01 00 F4 01 71 01 78 05 98 3A 00 00 D2 00 00 00 00 F0 10 F0 10 86 B8 09 00 83 FF 5D 00 97 00 FD FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 E7 00 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 22 00 00 00 1B 02 00 00 01 00 F4 01 70 01 78 05 98 3A 00 00 1D 01 00 00 00 F0 10 F0 10 86 B8 09 00 82 FF 5F 00 AB 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 23 01 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 2B 00 00 00 1B 02 00 00 01 00 F4 01 70 01 77 05 98 3A 00 00 67 01 00 00 00 F0 10 F0 10 86 B8 09 00 83 FF 5F 00 AB 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 5F 01 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 34 00 00 00 1B 02 00 00 01 00 F4 01 71 01 76 05 98 3A 00 00 B2 01 00 00 00 F1 10 F1 10 8C B8 09 00 82 FF 5E 00 AA 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 9B 01 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 3D 00 00 00 1B 02 00 00 01 00 F4 01 71 01 77 05 98 3A 00 00 FD 01 00 00 00 F1 10 F1 10 8C B8 09 00 81 FF 61 00 AD 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 D7 01 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 46 00 00 00 1B 02 00 00 01 00 F4 01 72 01 77 05 98 3A 00 00 47 02 00 00 00 F1 10 F1 10 8C B8 09 00 7F FF 5F 00 AB 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 13 02 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 4F 00 00 00 1B 02 00 00 01 00 F4 01 72 01 78 05 98 3A 00 00 92 02 00 00 00 F1 10 F1 10 8C B8 09 00 81 FF 61 00 AD 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 4F 02 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 58 00 00 00 1B 02 00 00 01 00 F4 01 73 01 78 05 98 3A 00 00 DD 02 00 00 00 F1 10 F1 10 8C B8 09 00 81 FF 63 00 AF 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 8B 02 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 61 00 00 00 1B 02 00 00 01 00 F4 01 73 01 7A 05 98 3A 00 00 27 03 00 00 00 F1 10 F1 10 8C B8 09 00 7D FF 60 00 AC 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 C7 02 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 6A 00 00 00 1B 02 00 00 01 00 F4 01 73 01 7C 05 98 3A 00 00 72 03 00 00 00 F1 10 F1 10 92 B8 09 00 7F FF 5F 00 AB 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 03 03 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 73 00 00 00 1B 02 00 00 01 00 F4 01 73 01 7C 05 98 3A 00 00 BD 03 00 00 00 F1 10 F1 10 92 B8 09 00 80 FF 60 00 AC 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 3F 03 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 7C 00 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 08 04 00 00 00 F1 10 F1 10 92 B8 09 00 80 FF 60 00 AC 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 29 6E 02 E0 FB D7 B6 00 80 00 01 EA 07 03 0E 0D 23 09 9C 00 59 00 4D 00 6F 00 B6 28 00 00 00 48 00 00 00 18 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00 00 84 C2 B6 00 00 00 00 30 84 C2 B6 30 84 C2 B6 2C 84 C2 B6 CD BB BE B6 01 00 00 00 69 49 B2 B6 F8 6F 30 B7 0C 00 00 00 50 82 30 B7 00 00 00 00 BE 0E E5 B6 38 70 0C 0A 40 00 00 00 9B 0F B2 B6 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 00 84 C2 B6 D8 20 B2 B6
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 7B 03 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 85 00 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 52 04 00 00 00 F1 10 F1 10 92 B8 09 00 82 FF 61 00 AD 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 B7 03 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 8E 00 00 00 1B 02 00 00 01 00 F4 01 72 01 77 05 98 3A 00 00 9D 04 00 00 00 F2 10 F2 10 92 B8 09 00 7F FF 60 00 AC 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 F3 03 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 97 00 00 00 1B 02 00 00 01 00 F4 01 71 01 77 05 98 3A 00 00 E8 04 00 00 00 F2 10 F2 10 92 B8 09 00 80 FF 63 00 AF 00 E5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 2F 04 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 A0 00 00 00 1B 02 00 00 01 00 F4 01 71 01 79 05 98 3A 00 00 32 05 00 00 00 F2 10 F2 10 98 B8 09 00 7F FF 62 00 BF 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 6B 04 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 A9 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 7D 05 00 00 00 F2 10 F2 10 98 B8 09 00 7C FF 62 00 BF 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 A7 04 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 B2 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 C8 05 00 00 00 F2 10 F2 10 98 B8 09 00 7F FF 63 00 C0 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 E3 04 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 BB 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 12 06 00 00 00 F3 10 F3 10 98 B8 09 00 7C FF 63 00 C0 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 1F 05 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 C4 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7C 05 98 3A 00 00 5D 06 00 00 00 F3 10 F3 10 98 B8 09 00 80 FF 5F 00 BC 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 5B 05 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 CD 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7C 05 98 3A 00 00 A7 06 00 00 00 F3 10 F3 10 98 B8 09 00 7F FF 60 00 BD 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 97 05 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 D5 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 F2 06 00 00 00 F3 10 F3 10 9E B8 09 00 81 FF 60 00 BD 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 D3 05 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 DF 00 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 3D 07 00 00 00 F3 10 F3 10 9E B8 09 00 82 FF 62 00 BF 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 0F 06 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 E7 00 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 88 07 00 00 00 F4 10 F4 10 9E B8 09 00 7A FF 62 00 BF 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 4B 06 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 F0 00 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 D2 07 00 00 00 F4 10 F4 10 9E B8 09 00 86 FF 64 00 C1 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 87 06 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 F9 00 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 1D 08 00 00 00 F4 10 F4 10 9E B8 09 00 83 FF 61 00 BE 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 C3 06 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 02 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 68 08 00 00 00 F4 10 F4 10 9E B8 09 00 7F FF 5F 00 BC 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 FF 06 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 0B 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 B2 08 00 00 00 F4 10 F4 10 A4 B8 09 00 81 FF 62 00 BF 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 3B 07 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 14 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 FD 08 00 00 00 F5 10 F5 10 A4 B8 09 00 83 FF 60 00 BD 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 77 07 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 1D 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 48 09 00 00 00 F5 10 F5 10 A4 B8 09 00 84 FF 61 00 BE 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 B3 07 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 26 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 92 09 00 00 00 F5 10 F5 10 A4 B8 09 00 80 FF 5F 00 BC 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 EF 07 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 2F 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 DD 09 00 00 00 F5 10 F5 10 A4 B8 09 00 86 FF 60 00 BD 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 2B 08 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 38 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7A 05 98 3A 00 00 27 0A 00 00 00 F5 10 F5 10 A4 B8 09 00 8B FF 73 00 D0 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 67 08 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 41 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 72 0A 00 00 00 F6 10 F6 10 AA B8 09 00 81 FF 61 00 BE 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 A3 08 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 4A 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7B 05 98 3A 00 00 BD 0A 00 00 00 F6 10 F6 10 AA B8 09 00 7F FF 5F 00 BC 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 E0 08 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 53 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7C 05 98 3A 00 00 09 0B 00 00 00 F6 10 F6 10 AA B8 09 00 80 FF 64 00 C1 00 CE FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 1B 09 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 5C 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 53 0B 00 00 00 F6 10 F6 10 AA B8 09 00 82 FF 62 00 C9 00 C1 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 57 09 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 65 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 9D 0B 00 00 00 F6 10 F6 10 AA B8 09 00 89 FF 72 00 E3 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 93 09 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 6E 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 E8 0B 00 00 00 F6 10 F6 10 AA B8 09 00 80 FF 62 00 D3 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 CF 09 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 77 01 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 32 0C 00 00 00 F6 10 F6 10 B0 B8 09 00 7F FF 61 00 D2 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 0B 0A 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 80 01 00 00 1B 02 00 00 01 00 F4 01 72 01 78 05 98 3A 00 00 7D 0C 00 00 00 F6 10 F6 10 B0 B8 09 00 8B FF 76 00 E7 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 47 0A 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 89 01 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 C8 0C 00 00 00 F6 10 F6 10 B0 B8 09 00 85 FF 70 00 E1 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 83 0A 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 92 01 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 12 0D 00 00 00 F6 10 F6 10 B0 B8 09 00 7F FF 60 00 D1 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 BF 0A 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 9B 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 5D 0D 00 00 00 F7 10 F7 10 B0 B8 09 00 80 FF 65 00 D6 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 FB 0A 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 A4 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 A8 0D 00 00 00 F7 10 F7 10 B0 B8 09 00 83 FF 69 00 DA 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 37 0B 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 AD 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 F2 0D 00 00 00 F7 10 F7 10 B6 B8 09 00 88 FF 75 00 E6 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 73 0B 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 B6 01 00 00 1B 02 00 00 01 00 F4 01 71 01 79 05 98 3A 00 00 3D 0E 00 00 00 F7 10 F7 10 B6 B8 09 00 7C FF 65 00 D6 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 AF 0B 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 BF 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7A 05 98 3A 00 00 88 0E 00 00 00 F7 10 F7 10 B6 B8 09 00 7C FF 61 00 D2 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 EB 0B 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 C8 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 D2 0E 00 00 00 F8 10 F8 10 B6 B8 09 00 85 FF 72 00 E3 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 27 0C 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 D1 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 1D 0F 00 00 00 F8 10 F8 10 B6 B8 09 00 7C FF 68 00 D9 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 63 0C 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 DA 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7A 05 98 3A 00 00 68 0F 00 00 00 F8 10 F8 10 B6 B8 09 00 7A FF 61 00 D2 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 9F 0C 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 E3 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7A 05 98 3A 00 00 B2 0F 00 00 00 F8 10 F8 10 BC B8 09 00 7B FF 64 00 D5 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 DB 0C 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 EC 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7C 05 98 3A 00 00 FD 0F 00 00 00 F8 10 F8 10 BC B8 09 00 89 FF 72 00 E3 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 17 0D 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 F5 01 00 00 1B 02 00 00 01 00 F4 01 71 01 7A 05 98 3A 00 00 48 10 00 00 00 F9 10 F9 10 BC B8 09 00 7D FF 66 00 D7 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 53 0D 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 FE 01 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 93 10 00 00 00 F9 10 F9 10 BC B8 09 00 87 FF 6B 00 DC 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 8F 0D 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 07 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 DD 10 00 00 00 F9 10 F9 10 BC B8 09 00 84 FF 6D 00 DE 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 CB 0D 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 10 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 28 11 00 00 00 F9 10 F9 10 BC B8 09 00 81 FF 6D 00 DE 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 07 0E 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 19 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 73 11 00 00 00 F9 10 F9 10 C2 B8 09 00 7D FF 65 00 D6 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 43 0E 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 22 02 00 00 1B 02 00 00 01 00 F4 01 72 01 78 05 98 3A 00 00 BE 11 00 00 00 FA 10 FA 10 C2 B8 09 00 7B FF 63 00 D4 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 7F 0E 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 2B 02 00 00 1B 02 00 00 01 00 F4 01 72 01 78 05 98 3A 00 00 08 12 00 00 00 FA 10 FA 10 C2 B8 09 00 83 FF 6C 00 DD 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 BB 0E 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 34 02 00 00 1B 02 00 00 01 00 F4 01 72 01 78 05 98 3A 00 00 53 12 00 00 00 FA 10 FA 10 C2 B8 09 00 7B FF 5F 00 D0 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 F7 0E 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 3D 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7A 05 98 3A 00 00 9E 12 00 00 00 FA 10 FA 10 C2 B8 09 00 84 FF 6D 00 DE 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 33 0F 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 46 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 E8 12 00 00 00 FA 10 FA 10 C2 B8 09 00 83 FF 6D 00 DE 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 6F 0F 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 4F 02 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 33 13 00 00 00 FB 10 FB 10 C8 B8 09 00 7B FF 61 00 D2 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 AB 0F 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 57 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 7E 13 00 00 00 FB 10 FB 10 C8 B8 09 00 85 FF 6E 00 DF 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 E7 0F 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 60 02 00 00 1B 02 00 00 01 00 F4 01 71 01 78 05 98 3A 00 00 C8 13 00 00 00 FB 10 FB 10 C8 B8 09 00 80 FF 6F 00 E0 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 23 10 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 69 02 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 13 14 00 00 00 FB 10 FB 10 C8 B8 09 00 7F FF 6C 00 DD 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 5F 10 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 72 02 00 00 1B 02 00 00 01 00 F4 01 72 01 79 05 98 3A 00 00 5D 14 00 00 00 FB 10 FB 10 C8 B8 09 00 80 FF 71 00 E2 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 9B 10 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 7B 02 00 00 1B 02 00 00 01 00 F4 01 71 01 79 05 98 3A 00 00 A8 14 00 00 00 FB 10 FB 10 C8 B8 09 00 87 FF 70 00 E1 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 D7 10 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 84 02 00 00 1B 02 00 00 01 00 F4 01 71 01 7C 05 98 3A 00 00 F3 14 00 00 00 FB 10 FB 10 CE B8 09 00 81 FF 73 00 E4 00 B3 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 13 11 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 8D 02 00 00 1B 02 00 00 01 00 F4 01 71 01 79 05 98 3A 00 00 3E 15 00 00 00 FB 10 FB 10 CE B8 09 00 79 FF 67 00 E3 00 A5 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 4F 11 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 96 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7B 05 98 3A 00 00 88 15 00 00 00 FB 10 FB 10 CE B8 09 00 82 FF 6F 00 F5 00 97 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 8B 11 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 9F 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 D3 15 00 00 00 FB 10 FB 10 CE B8 09 00 82 FF 72 00 F8 00 97 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par 55 55 55 55 31 E1 73 CB 72 01 03 1F 6E 0F 0A 10 02 00 00 30 E0 2E 00 00 C7 11 00 00 E6 00 01 00 00 00 00 1E 00 08 07 00 00 A8 02 00 00 1B 02 00 00 01 00 F4 01 72 01 7C 05 98 3A 00 00 1E 16 00 00 00 FC 10 FC 10 CE B8 09 00 78 FF 67 00 ED 00 97 FF 00 00 00 00 00 00 00 30 B7 08 00 00 00 04 00 00 00 F8 44 B2 B6 08 70 0C 0A 22 1D 3F 08 F8 6F 30 B7 00 80 C2 B6 00 84 C2 B6 48 C7 00 00 48 C7 00 00 93 4F B2 B6 C0 E8 0F 0A 00 00 00 00 0C 02 00 00 C0 E8 0F 0A 00 75 48 08 88 CC DF B6 F8 6F 30 B7 00 80 C2 B6 00 00 00 00 30 77 0C 0A 00 00 00 00 F8 EF AD B6 01 00 00 00 08 02 00 00 01 00 00 00 55 82 30 B7 60 E8 0F 0A 00 B0 82 08 09 00 00 00 00 80 C2 B6 09 00 00 00 08
\par
\par
\par }
透析机通讯文档.pdf
Binary files differ