From ecbdf483b6c4a5906cb160f32c2f5b1fbede2e88 Mon Sep 17 00:00:00 2001 From: ypc <15051963820@163.com> Date: Wed, 8 Jul 2026 18:02:21 +0800 Subject: [PATCH] =?UTF-8?q?docs(libweb=5Fserver):=20Web=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=9C=8D=E5=8A=A1=E6=A8=A1=E5=9D=97=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=20API=20=E5=8F=82=E8=80=83=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 个章节完整覆盖: - 模块概述 + 与原工程对比 - 文件结构 + 单线程事件驱动架构图 - 核心数据结构 (stru_ws_sig / stru_ws_session / enum_ws_sig_type) - 接口函数 (X-Macro 入口 / ws_send_all / ws_send_one / ws_send_binary / 异步通道) - WebSocket 协议 (JSON CRUD / 增量推送格式 / 远程命令 / 文件管理) - 内部函数 (ws_sig_add/del/set / ws_make_sig_json / ws_push_task / ws_cmd_exec) - X-Macro 注册 / 依赖关系 / 编译构建 / 运行验证 / JavaScript 使用示例 --- docs/README.md | 1 + .../system/libweb_server/API-libweb_server.md | 630 ++++++++++++++++++ 2 files changed, 631 insertions(+) create mode 100644 docs/system/libweb_server/API-libweb_server.md diff --git a/docs/README.md b/docs/README.md index a238626..a51117c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,6 +38,7 @@ |------|------|------|------| | RTU | [API-RTU.md](system/RTU/API-RTU.md) | C + C++ | 主程序入口、X-Macro 框架、模块生命周期 | | libmodbus_m | [API-libmodbus_m.md](system/libmodbus_m/API-libmodbus_m.md) | C++ | Modbus 主站管理(通道/点表/轮询/写入) | +| libweb_server | [API-libweb_server.md](system/libweb_server/API-libweb_server.md) | C++ | Web 管理服务(HTTP + WebSocket 实时推送) | ``` ┌──────────┐ diff --git a/docs/system/libweb_server/API-libweb_server.md b/docs/system/libweb_server/API-libweb_server.md new file mode 100644 index 0000000..4c96070 --- /dev/null +++ b/docs/system/libweb_server/API-libweb_server.md @@ -0,0 +1,630 @@ +# API-libweb_server — Web 管理服务模块 + +> 版本: v2.0 +> 日期: 2026-07-08 +> 来源: 基于原工程 ws_method.cpp (1420行) + web_server.cpp (300行) 重构 +> 状态: ✅ 已实现,x86 + ARM 双平台编译通过(零警告) + +--- + +## 1. 模块概述 + +libweb_server 是 RTU 通讯装置的**嵌入式 Web 管理服务**,作为 X-Macro 框架的线程模块运行。基于 Mongoose v7.22 实现,提供 HTTP REST API 和 WebSocket 实时数据通道。 + +| 功能 | 说明 | +|------|------| +| HTTP 服务器 | 静态文件服务(内嵌前端),默认监听 `0.0.0.0:8080` | +| WebSocket | `/ws` 路径升级,双向 JSON 消息通信 | +| 信号 CRUD | 5 种信号类型(out/in/yk/ao/param)的 add/del/set 操作 | +| 增量推送 | 每 100ms 批量检测信号变化,仅发送变更部分 | +| 远程命令执行 | 通过 pipe 重定向 stdout,将 RTU 命令行输出回传前端 | +| 文件管理 | get_file / set_file 命令实现设备文件的上传下载 | +| 异步命令通道 | async_cmd_printf/complete 供其他模块向 Web 前端推送执行结果 | +| 优雅退出 | 支持 EV_STOP 信号,mg_mgr_free 安全释放所有连接 | + +### 设计思路 + +**单线程事件驱动**:将 Mongoose 的 `mg_mgr_poll()` 合入 X-Macro 框架的 EV_TIMER1(10ms),不再另外创建 pthread。三个定时器分工明确: + +``` +EV_TIMER1 (10ms): mg_mgr_poll(&s_ws_mgr, 0) ← IO 收包/发包(非阻塞) +EV_TIMER2 (100ms): ws_push_task() ← 信号增量推送 +EV_TIMER3 (1000ms): run_cnt++ ← 心跳计数 +``` + +**Per-Connection Session**:每个 WebSocket 客户端维护独立的 `stru_ws_session`(订阅信号列表 + last_val 缓存),断开时自动清理。多连接并发安全由两个互斥锁保证:`g_ws_conns_mutex`(连接列表)和 `g_ws_session_mutex`(会话数据)。 + +### 与原工程对比 + +| 维度 | 原工程 (ws_method.cpp 1420行) | 重构后 (2 文件共 ~650行) | +|------|-----------------------------|------------------------| +| 线程模型 | 双线程(pthread_create 死循环 `mg_mgr_poll`) | 单线程(复用 X-Macro 框架) | +| 退出方式 | kill 线程 | EV_STOP 优雅退出 | +| 增量推送 | 5 个独立 `make_*_json` 函数(~150行重复) | 1 个 `ws_make_sig_json` + switch | +| 信号增删 | 每种信号单独函数 + 函数指针表 | 按信号类型分 5 个 `ws_sig_add_*` + 统一 dispatch | +| 代码行数 | ~1700 | ~650(-62%) | +| 编译产物 | 未在本工程编译 | libweb_server.a + 零警告 | + +--- + +## 2. 文件结构 + +``` +src/system/libweb_server/ +├── inc/ +│ └── web_server.h # 对外公开头文件 +└── src/ + ├── web_server.cpp # HTTP/WS 服务器核心(~440行) + │ ├── ws_send_all / ws_send_one / ws_send_binary # 发送函数 + │ ├── ws_event_handler() # Mongoose 事件分发 + │ ├── ws_cmd_exec() # 远程命令执行(pipe + stdout 捕获) + │ ├── async_cmd_printf / async_cmd_complete # 异步命令通道 + │ └── app_web_server_init1 / init2 / app_web_server # X-Macro 框架入口 + │ + └── ws_method.cpp # WebSocket 命令与信号 CRUD(~674行) + ├── ws_sig_add_out / in / yk / ao / param # 5 种信号的 add 操作 + ├── ws_sig_del() # 统一的 del 操作 + ├── ws_sig_set() # dispatch set 到 5 种信号 + ├── ws_make_sig_json() # 统一的 JSON 构建 + ├── ws_push_task() # 增量推送引擎 + └── ws_recv() # JSON 命令解析与分发 + +release/ +└── {x86,arm}/lib/ + └── libweb_server.a # 编译产物 +``` + +--- + +## 3. 架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ app_web_server 线程 │ +│ │ +│ while(1) { │ +│ task_event_recv(EV_TIMER1|EV_TIMER2|EV_TIMER3|EV_STOP)│ +│ │ +│ EV_TIMER1 (10ms) │ +│ └─→ mg_mgr_poll(&s_ws_mgr, 0) │ +│ ├─ MG_EV_HTTP_MSG → mg_ws_upgrade / 404 │ +│ ├─ MG_EV_WS_MSG → ws_recv(c, buf, len) │ +│ ├─ MG_EV_WS_CTL → session destroy │ +│ ├─ MG_EV_CLOSE → session destroy │ +│ └─ (incoming TCP data / WS frames) │ +│ │ +│ EV_TIMER2 (100ms) │ +│ └─→ ws_push_task() │ +│ ├─ 遍历 g_ws_sessions │ +│ ├─ 对每个 session 调用 ws_make_sig_json × 5 │ +│ ├─ 对比 last_val → has_change │ +│ └─ has_change → ws_send_one() JSON 推送 │ +│ │ +│ EV_TIMER3 (1000ms) │ +│ └─→ run_cnt++ │ +│ │ +│ EV_STOP │ +│ └─→ mg_mgr_free(&s_ws_mgr) → break │ +│ } │ +└─────────────────────────────────────────────────────────┘ +``` + +### 关键数据结构 + +```c +/* Per-signal 缓存 */ +typedef struct { + std::string saddr; // 信号地址(唯一标识) + std::string desc; // 信号描述 + uint8_t data_type; // 数据类型(DATA_TYPE_U16/U32/F32/STR...) + uint8_t ctrl_type; // 控制类型(DC_CTRL_DIRECT/DC_CTRL_SBO) + std::vector vec_p_data; // 数据指针数组(param 多区,其余单指针) + std::string last_val; // 上次推送值(用于增量变更检测) +} stru_ws_sig; + +/* Per-connection session */ +typedef struct { + std::vector signals[WS_SIG_MAX]; // 5 种信号类型各自的订阅列表 +} stru_ws_session; +``` + +### 信号类型枚举 + +```c +typedef enum { + WS_SIG_OUT = 0, // 遥测/遥信输出 + WS_SIG_IN = 1, // 输入信号(只读) + WS_SIG_YK = 2, // 遥控(Select-Before-Operate) + WS_SIG_AO = 3, // 模拟输出/遥调 + WS_SIG_PARAM = 4, // 参数/定值(多定值区) + WS_SIG_MAX // 枚举边界 +} enum_ws_sig_type; +``` + +### 线程安全 + +两个独立的互斥锁,互不交叉: + +| 锁 | 保护对象 | 使用位置 | +|------|---------|---------| +| `g_ws_conns_mutex` | `g_ws_conns` 连接列表 | 所有 ws_send_* / ws_event_handler | +| `g_ws_session_mutex` | `g_ws_sessions` 会话 map | ws_recv / ws_push_task / ws_session_destroy | + +### 硬编码配置 + +```c +#define WS_LISTEN_ON "http://0.0.0.0:8080" // HTTP 监听地址 +#define WS_UPGRADE_URI "/ws" // WebSocket 升级路径 +``` + +--- + +## 4. 接口函数 — web_server.h + +### 4.1 X-Macro 框架入口 + +```c +int app_web_server_init1(void *arg); +int app_web_server_init2(void *arg); +void *app_web_server(void *arg); +``` + +**init1**: 创建 `mg_mgr`,注册 `mg_http_listen`,打印启动日志。 +**init2**: 空实现(信号注册在 WebSocket 消息中按需进行)。 +**主循环**: 事件驱动,详见架构图。 + +### 4.2 广播发送 + +```c +void ws_send_all(const char *p_tx, size_t tx_len); +``` + +向所有已连接的 WebSocket 客户端广播一条文本消息。遍历 `g_ws_conns`,跳过非 WS 和正在排干的连接。 + +**使用场景**: 全局事件通知(如设备重启、配置变更)。 + +### 4.3 单连接发送 + +```c +bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len); +``` + +向指定 `conn_id` 的 WebSocket 客户端发送文本消息。在 `g_ws_conns` 中按 `c->id` 匹配。 + +**返回值**: `true` 发送完成,`false` 未找到连接或发送失败。 + +### 4.4 二进制发送 + +```c +bool ws_send_binary(unsigned long conn_id, const void *data, size_t len); +``` + +向指定连接发送二进制帧(`WEBSOCKET_OP_BINARY`)。用于传输文件内容和终端输出。 + +### 4.5 异步命令通道 + +```c +void ws_set_async_cmd_conn(const char *module, unsigned long conn_id); +unsigned long ws_get_async_cmd_conn(const char *module); +``` + +**用途**: 其他线程模块(如 libmodbus_m、lib60870)的异步回调结果需要推送给当初发起命令的前端页面。 + +**流程**: +``` +1. 前端通过 WebSocket 发送 {type:"cmd", data:"modbus_m read 1 3 0 10"} +2. ws_recv 解析 "modbus_m" → ws_set_async_cmd_conn("modbus_m", c->id) +3. ws_cmd_exec 调用 modbus_m 的命令处理函数 +4. modbus_m 异步回调中调用 async_cmd_printf("modbus_m", "register[0]=1234") +5. async_cmd_printf 通过 ws_get_async_cmd_conn("modbus_m") 找到对应连接 +6. ws_send_binary 将结果推送回前端 +``` + +**辅助函数**(供其他模块 `extern` 引用): +```c +void async_cmd_printf(const char *module, const char *fmt, ...); // 推送格式化文本 +void async_cmd_complete(const char *module, bool success, const char *msg); // 推送完成状态 +``` + +--- + +## 5. WebSocket 通信协议 + +### 5.1 信号 CRUD 命令 + +所有命令使用同一 JSON 格式,通过 `curd` 字段区分操作: + +```json +{ + "saddr": "<信号地址>", + "signal_type": "out|in|yk|ao|param", + "curd": "add|del|set", + "signal_data": "<设置值>", + "setting_zone": <定值区号> +} +``` + +| 字段 | 类型 | 必须 | 说明 | +|------|------|:--:|------| +| `saddr` | string | 是 | 信号地址,如 `"modbus_m.01.st01"` | +| `signal_type` | string | 是 | 信号类型:`out`/`in`/`yk`/`ao`/`param` | +| `curd` | string | 是 | 操作:`add`(订阅)、`del`(取消)、`set`(写值) | +| `signal_data` | string | set 时需要 | 写入值(字符串格式,如 `"1"`、`"3.14"`) | +| `setting_zone` | number | param set 时需要 | 定值区号(0-based) | + +**操作说明**: +- **add**: 将信号加入当前连接的订阅列表,立即返回该类型所有已订阅信号的当前值快照 +- **del**: 从订阅列表移除,立即返回更新后的列表 +- **set**: 写入值到 datacenter: + - `out` → `dc_set_out_signal_val(saddr, data, "web_server")` + - `in` → 返回错误(只读) + - `yk` → `dc_signal_yk_set_status(saddr, DC_STEP_DIRECT, &ctrl, data, "web_server")` + - `ao` → `dc_signal_ao_set_val(saddr, DC_STEP_DIRECT, &ctrl, data, "web_server")` + - `param` → `dc_signal_param_set_val(saddr, DC_STEP_DIRECT, &ctrl, zone, data, "web_server")` + +### 5.2 增量推送格式 + +`ws_push_task()` 每 100ms 遍历所有连接的订阅列表,仅发送发生变化的信号: + +```json +{ + "out": [ + { + "saddr": "modbus_m.01.st01", + "desc": "断路器位置", + "type": "bool", + "val": "1" + }, + { + "saddr": "modbus_m.01.mx01", + "desc": "A相电流", + "type": "float", + "val": "123.4" + } + ], + "yk": [ + { + "saddr": "modbus_m.01.co01", + "desc": "断路器遥控", + "type": "bool", + "val": "0", + "ctrl_type": 1 + } + ], + "ao": [ ... ], + "param": [ + { + "saddr": "prot.01.param01", + "desc": "过流I段定值", + "type": "float", + "ctrl_type": 2, + "setting_zone_list": [ + {"id": "0", "val": "5.00"}, + {"id": "1", "val": "4.50"} + ] + } + ] +} +``` + +**变更检测机制**: 每次构建 JSON 时与 `stru_ws_sig.last_val` 对比——不相同时标记 `has_change = true` 并更新 `last_val`。param 类型由于有多区数据,组合所有区的值用逗号拼接后整体对比。 + +### 5.3 远程命令执行 + +```json +// 请求 +{ + "type": "cmd", + "data": "" +} + +// 响应(二进制帧,逐行推送) +"===== Modbus Master Info =====\n" +"Channels: 1\n" +" ch[1]: mode=tcp, connected=1, 192.168.1.100:502\n" +``` + +**实现细节**: `ws_cmd_exec()` 通过 `pipe()` + `dup2()` 将 stdout 重定向到管道,另一线程读取后拼接为字符串,通过 `ws_send_binary` 回传给前端。执行完后恢复原 stdout。 + +### 5.4 获取命令列表 + +```json +// 请求 +{"type": "get_cmds"} + +// 响应 +{ + "type": "cmd_list", + "cmds": [ + {"name": "version", "desc": "show firmware version"}, + {"name": "modbus_m", "desc": "Modbus主站线程的控制命令"}, + ... + ] +} +``` + +### 5.5 文件管理 + +```json +// 读取文件 +{"type": "get_file", "path": "config/SYSTEM/app_config.json"} + +// 响应 +{ + "type": "file_content", + "path": "config/SYSTEM/app_config.json", + "content": "{\n \"apps\": [...]\n}" +} + +// 写入文件 +{"type": "set_file", "path": "config/SYSTEM/app_config.json", "content": "{...}"} + +// 响应 +{"type": "file_saved", "path": "config/SYSTEM/app_config.json", "success": true} +``` + +**安全限制**: +- 文件大小上限 1MB +- 相对路径基于进程所在目录解析为绝对路径 +- 绝对路径直接使用 + +--- + +## 6. 内部函数 — ws_method.cpp + +### 6.1 信号增删改 + +```c +static void ws_sig_add(stru_ws_session *s, enum_ws_sig_type st, const char *saddr); +static void ws_sig_del(stru_ws_session *s, enum_ws_sig_type st, const char *saddr); +static int ws_sig_set(stru_ws_session *s, enum_ws_sig_type st, const char *saddr, + uint8_t setting_zone, const char *val); +``` + +`ws_sig_add` 按 `st` 分发到 5 个具体函数: + +| 函数 | 调用的 dc API | 备注 | +|------|-------------|------| +| `ws_sig_add_out` | `dc_get_out_signal_info()` | 获取信号地址、描述、数据类型、数据指针 | +| `ws_sig_add_in` | `dc_get_in_signal_info()` | in 信号只读,set 操作返回错误 | +| `ws_sig_add_yk` | `dc_get_yk_signal_info()` | 额外获取 `ctrl_type`(直控/SBO) | +| `ws_sig_add_ao` | `dc_get_ao_signal_info()` | 同 yk | +| `ws_sig_add_param` | `dc_get_param_signal_info()` | 多定值区 → `vec_p_data` 可能有多个指针 | + +所有 add 函数共享逻辑:去重检查 → 查询 dc 获取元数据 → 缓存到 session。 + +### 6.2 JSON 构建 + +```c +static int ws_make_sig_json(stru_ws_session *s, cJSON *root, enum_ws_sig_type st, bool *has_change); +``` + +统一的 JSON 构建函数,按信号类型在输出的 JSON 中写入不同字段: +- `out/in`: `{saddr, desc, type, val}` +- `yk/ao`: `{saddr, desc, type, val, ctrl_type}` +- `param`: `{saddr, desc, type, ctrl_type, setting_zone_list: [{id, val}, ...]}` + +### 6.3 增量推送引擎 + +```c +void ws_push_task(void); +``` + +在 EV_TIMER2(100ms)中调用。遍历 `g_ws_sessions` 中所有连接的订阅列表,对每种信号类型调用 `ws_make_sig_json`,仅当 `has_change == true` 时通过 `ws_send_one` 推送 JSON 到对应前端。 + +### 6.4 会话生命周期 + +```c +void ws_session_destroy(struct mg_connection *c); +``` + +在 `MG_EV_WS_CTL`(WebSocket Close 帧)和 `MG_EV_CLOSE`(TCP 断开)时调用,从 `g_ws_sessions` 中移除该连接的 session 数据。 + +--- + +## 7. 内部函数 — web_server.cpp + +### 7.1 Mongoose 事件处理器 + +```c +static void ws_event_handler(mg_connection *c, int ev, void *ev_data); +``` + +| 事件 | 处理 | +|------|------| +| `MG_EV_HTTP_MSG` | 匹配 `/ws` → `mg_ws_upgrade`;否则返回 404 | +| `MG_EV_WS_MSG` | 首次收到消息时将连接加入 `g_ws_conns`;调用 `ws_recv()` 解析 JSON | +| `MG_EV_WS_CTL` | Close 帧(op=8)→ 销毁 session + 移除连接 | +| `MG_EV_CLOSE` | TCP 断开 → 清理 WS 连接的 session 数据 | + +### 7.2 远程命令执行 + +```c +std::string ws_cmd_exec(const char *cmd_line); +``` + +**执行流程**: +1. `strtok` 解析命令行为 argc/argv +2. `cmd_find(argv[0])` 查找命令结构体 +3. `pipe()` 创建管道 +4. `pthread_create` 启动读取线程(阻塞读管道) +5. `dup2` 重定向 stdout 到管道写端 +6. 调用 `p_cmd->func(argc, argv)` 执行命令 +7. 恢复 stdout +8. `pthread_join` 等待读取线程收集全部输出 +9. 返回拼接后的字符串 + +**关键设计**: 命令执行期间的 stdout 输出被管道截获,不会污染 RTU 控制台的 `linenoise` 交互界面。 + +--- + +## 8. 新增到 X-Macro 框架 + +在 `app_modules.h` 中注册一行即可: + +```c +/* src/system/inc/app_modules.h */ +APP_MODULE(WEB_SERVER, web_server, app_web_server_init1, app_web_server_init2, app_web_server) +``` + +`app_config.json` 中相应添加: + +```json +{"name": "app_web_server", "enable": true} +``` + +--- + +## 9. 依赖关系 + +### 9.1 本模块依赖 + +| 模块 | 用途 | +|------|------| +| `libmongoose` | HTTP/WebSocket 底层协议栈(`mg_mgr_poll`、`mg_ws_send`、`mg_http_listen`) | +| `libdatacenter` | 信号查询(`dc_get_*_signal_info`)、类型工具(`dc_get_type_name`)、内存管理(`dc_create_data`) | +| `libcJSON` | JSON 解析(`cJSON_Parse`)和构建(`cJSON_CreateObject`) | +| `libcmd` | 命令查找(`cmd_find`)、命令列表(`cmd_manager_get_commands`) | +| `libtask` | 事件等待(`task_event_recv`) | +| `liblog` | 日志输出(`LOG_I` / `LOG_E`) | +| `myMongoose.h` | mongoose v7.22 适配层头文件 | +| `mySystem.h` | X-Macro 框架数据结构(`stru_app`) | + +### 9.2 依赖本模块的模块 + +| 模块 | 用途 | +|------|------| +| (其他线程模块) | 通过 `async_cmd_printf`/`async_cmd_complete` 推送异步命令结果 | + +### 9.3 依赖图 + +``` +┌─────────────────────┐ +│ libweb_server │ (RTU 线程模块) +│ HTTP+WS 管理 │ +└──────┬──────┬───────┘ + │ │ + ▼ ▼ +┌──────────┐ ┌──────────────┐ +│libmongoose│ │libdatacenter │ +│(HTTP/WS) │ │(信号查询+写) │ +└──────────┘ └──────────────┘ + │ + ▼ + Linux Socket + epoll +``` + +--- + +## 10. 编译与构建 + +```bash +# 编译 libweb_server.a +cd release/src/system/libweb_server +make -f makefile all # x86 +make -f makefile CROSS=arm all # ARM + +# 参与全量构建 +cd release && ./build.sh # x86 +cd release && ./build.sh arm # ARM +``` + +### RTU 链接 + +`release/src/system/RTU/makefile` 中已注册 `-lweb_server -lmongoose` 依赖链: + +``` +RTU ← libweb_server ← libmongoose + ← libdatacenter ← libcJSON + ← libcmd ← libtask ← liblog ← libfunc +``` + +--- + +## 11. 运行验证 + +```bash +# 启动 RTU(x86 本地) +cd release/x86/exe && ./RTU + +# 浏览器访问 +curl http://localhost:8080/ # HTTP 404(无静态文件) +wscat -c ws://localhost:8080/ws # WebSocket 客户端 + +# WebSocket 测试命令 +{"saddr": "modbus_m.01.st01", "signal_type": "out", "curd": "add"} +{"type": "cmd", "data": "version"} +{"type": "get_cmds"} +``` + +--- + +## 12. 使用示例 + +### 12.1 前端订阅信号并接收实时推送 + +```javascript +// 1. 建立 WebSocket 连接 +const ws = new WebSocket('ws://192.168.1.100:8080/ws'); + +ws.onopen = () => { + // 2. 订阅信号 + ws.send(JSON.stringify({ + saddr: "modbus_m.01.st01", + signal_type: "out", + curd: "add" + })); + ws.send(JSON.stringify({ + saddr: "modbus_m.01.mx01", + signal_type: "out", + curd: "add" + })); + ws.send(JSON.stringify({ + saddr: "prot.01.param01", + signal_type: "param", + curd: "add" + })); +}; + +// 3. 接收增量推送(每 100ms 自动发送变化部分) +ws.onmessage = (e) => { + const data = JSON.parse(e.data); + // data.out → [{saddr, desc, type, val}, ...] + // data.param? → [{saddr, desc, type, ctrl_type, setting_zone_list}, ...] + updateUI(data); +}; +``` + +### 12.2 远程控制遥控信号 + +```javascript +// SBO 选控(两步操作) +ws.send(JSON.stringify({ + saddr: "modbus_m.01.co01", + signal_type: "yk", + curd: "set", + signal_data: "1" +})); +``` + +### 12.3 其他模块异步推送结果 + +```c +// 在 libmodbus_m 或其他线程模块中 +#include "web_server.h" + +void my_async_callback(void *user_data, int result) +{ + async_cmd_printf("modbus_m", "register[0] = %d\n", result); + async_cmd_complete("modbus_m", true, "read complete"); +} +``` + +--- + +## 13. 变更历史 + +| 日期 | 版本 | 变更 | +|------|------|------| +| 2026-07-08 | v2.0 | 重构:双线程→单线程、EV_STOP 优雅退出、3 文件拆分、消除 5× 代码重复 | +| 2026-06-12 | v1.0 | 原工程实现(ws_method.cpp 1420行 + web_server.cpp 300行) |