RTU_ALL_AI/docs/system/libweb_server/API-libweb_server.md

21 KiB
Raw Permalink Blame History

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_TIMER110ms不再另外创建 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                 │
│  }                                                      │
└─────────────────────────────────────────────────────────┘

关键数据结构

/* 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<void *>   vec_p_data;   // 数据指针数组param 多区,其余单指针)
    std::string           last_val;     // 上次推送值(用于增量变更检测)
} stru_ws_sig;

/* Per-connection session */
typedef struct {
    std::vector<stru_ws_sig> signals[WS_SIG_MAX];  // 5 种信号类型各自的订阅列表
} stru_ws_session;

信号类型枚举

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

硬编码配置

#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 框架入口

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 广播发送

void ws_send_all(const char *p_tx, size_t tx_len);

向所有已连接的 WebSocket 客户端广播一条文本消息。遍历 g_ws_conns,跳过非 WS 和正在排干的连接。

使用场景: 全局事件通知(如设备重启、配置变更)。

4.3 单连接发送

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 二进制发送

bool ws_send_binary(unsigned long conn_id, const void *data, size_t len);

向指定连接发送二进制帧(WEBSOCKET_OP_BINARY)。用于传输文件内容和终端输出。

4.5 异步命令通道

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 引用):

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 字段区分操作:

{
    "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
    • outdc_set_out_signal_val(saddr, data, "web_server")
    • in → 返回错误(只读)
    • ykdc_signal_yk_set_status(saddr, DC_STEP_DIRECT, &ctrl, data, "web_server")
    • aodc_signal_ao_set_val(saddr, DC_STEP_DIRECT, &ctrl, data, "web_server")
    • paramdc_signal_param_set_val(saddr, DC_STEP_DIRECT, &ctrl, zone, data, "web_server")

5.2 增量推送格式

ws_push_task() 每 100ms 遍历所有连接的订阅列表,仅发送发生变化的信号:

{
    "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 远程命令执行

// 请求
{
    "type": "cmd",
    "data": "<RTU 命令行,如 modbus_m info>"
}

// 响应(二进制帧,逐行推送)
"===== 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 获取命令列表

// 请求
{"type": "get_cmds"}

// 响应
{
    "type": "cmd_list",
    "cmds": [
        {"name": "version", "desc": "show firmware version"},
        {"name": "modbus_m", "desc": "Modbus主站线程的控制命令"},
        ...
    ]
}

5.5 文件管理

// 读取文件
{"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 信号增删改

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_addst 分发到 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 构建

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 增量推送引擎

void ws_push_task(void);

在 EV_TIMER2100ms中调用。遍历 g_ws_sessions 中所有连接的订阅列表,对每种信号类型调用 ws_make_sig_json,仅当 has_change == true 时通过 ws_send_one 推送 JSON 到对应前端。

6.4 会话生命周期

void ws_session_destroy(struct mg_connection *c);

MG_EV_WS_CTLWebSocket Close 帧)和 MG_EV_CLOSETCP 断开)时调用,从 g_ws_sessions 中移除该连接的 session 数据。


7. 内部函数 — web_server.cpp

7.1 Mongoose 事件处理器

static void ws_event_handler(mg_connection *c, int ev, void *ev_data);
事件 处理
MG_EV_HTTP_MSG 匹配 /wsmg_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 远程命令执行

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 中注册一行即可:

/* 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 中相应添加:

{"name": "app_web_server", "enable": true}

9. 依赖关系

9.1 本模块依赖

模块 用途
libmongoose HTTP/WebSocket 底层协议栈(mg_mgr_pollmg_ws_sendmg_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. 编译与构建

# 编译 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. 运行验证

# 启动 RTUx86 本地)
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 前端订阅信号并接收实时推送

// 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 远程控制遥控信号

// SBO 选控(两步操作)
ws.send(JSON.stringify({
    saddr: "modbus_m.01.co01",
    signal_type: "yk",
    curd: "set",
    signal_data: "1"
}));

12.3 其他模块异步推送结果

// 在 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行