Compare commits

..

10 Commits

Author SHA1 Message Date
ypc ecbdf483b6 docs(libweb_server): Web 管理服务模块详细 API 参考文档
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 使用示例
2026-07-08 18:02:21 +08:00
ypc 3dd52d2202 feat(libweb_server): Web 管理服务模块(3 文件拆分 + 单线程 + EV_STOP)
基于原工程 libweb_server 重构,核心改进:
- 双线程 → 单线程: mg_mgr_poll 合入 X-Macro 框架 EV_TIMER1
- 无停止机制 → EV_STOP 优雅退出
- 1500 行 → ~650 行(3 文件拆分: web_server.cpp/ws_method.cpp)
- 端口: http://0.0.0.0:8080, WebSocket: /ws
- 信号 CRUD: add/del/set × 5 种信号类型
- 增量推送: EV_TIMER2 每 100ms 对比 last_val
- 异步命令通道 + 文件上传/下载 + 远程命令执行

注册: app_modules.h + system/makefile + copy_headers.sh + RTU/makefile
构建: x86 + ARM 全通过,零警告
2026-07-08 17:57:17 +08:00
ypc db1ee1e979 docs: RTU 模块 + Mongoose API 参考文档
- docs/system/RTU/API-RTU.md: RTU 完整文档(531行)
  X-Macro 框架/启动流程/数据结构/新增模块指南
- docs/protocol/libmongoose/API-libmongoose.md: mongoose 文档(347行)
  HTTP/WebSocket/定时器 API + 集成约束
- docs/README.md: 新增 system/ + protocol/libmongoose 索引
- COMPLETION.md: 更新 RTU + mongoose 完成状态
2026-07-08 17:36:15 +08:00
ypc d1569c92bb feat(libmongoose): 导入 Mongoose v7.22 MIT 开源网络库
- 导入 mongoose v7.22 源码 (mongoose.h 5748行 + mongoose.c 31786行)
- 创建 myMongoose.h 适配头(epoll/poll/socket 启用,filesystem 禁用)
- makefile 纯 C 编译为 libmongoose.a
- 注册到 copy_headers.sh + protocol SUBDIRS
- 编译产物: x86 462KB / ARM 471KB,零警告
- 用途: libweb_server 模块的 HTTP REST API + WebSocket 底层协议栈
2026-07-08 17:35:59 +08:00
ypc a0158a6cbf refactor(libmodbus_m): MY_LOG_→LOG_ 统一日志宏 + 代码风格规范
- 全局替换 MY_LOG_I/MY_LOG_E/MY_LOG_W 为统一的 LOG_I/LOG_E/LOG_W
- get_base_path(): 对齐配置路径约定(WORK_PATH 优先 → 可执行文件目录
- 修复关键字空格(if(/for(/while(/switch( → if (/for (/while (/switch ()
- 花括号/空行按 Allman 风格规范
2026-07-08 17:35:42 +08:00
ypc 16b697f9c4 refactor(RTU): 配置文件独立化 + app_sys_stop() + 代码风格统一
- config/SYSTEM/app_config.json: 配置文件从源码树迁出到独立 config/ 目录
- app_sys.cpp: 配置路径优先级 WORK_PATH 环境变量 > 可执行文件目录
- app_sys.cpp: 新增 app_sys_stop() 完整生命周期(EV_STOP + pthread_join)
- app_sys.cpp: 代码风格统一(空格/花括号/allman)
- main.c: 添加 SIGPIPE 处理 + app_sys_stop() 调用
- mySystem.h: 添加 EV_STOP 宏 + app_sys_stop() 声明
- RTU/makefile: 修复 .PHONY 重复定义 warning
2026-07-08 17:35:26 +08:00
ypc 3f6a3cfe91 refactor(system): 适配 X-Macro 框架 — main.c + libmodbus_m + makefiles
- main.c: 移除显式 app_modbus_m_register() 调用,改为 dc_init → app_sys_init
- modbus_m.cpp: 删除 app_modbus_m_register(),app_modules.h X-Macro 自动注册
- release/src/system/makefile: 移除 libsystem.a 构建(app_sys.cpp 并入 RTU)
- release/src/system/RTU/makefile: 增加 app_sys.cpp 编译,链接 -lcJSON
- 编译验证: x86 + ARM RTU 可执行文件全通过

新增模块流程: 只需在 app_modules.h 添加一行 X-Macro
2026-07-08 16:38:20 +08:00
ypc 447443a6e5 refactor(system): 迁移至 X-Macro 框架 + app_config.json 启停控制
- 新增 app_modules.h: X-Macro 模块中心化列表(当前仅 modbus_m)
- 重写 mySystem.h: 移除旧 app_module_register API,新增:
  * enum_app: 通过 app_modules.h X-Macro 自动生成
  * extern 声明: 通过 app_modules.h 自动生成全部 init1/init2/run
  * stru_app: 增加 p_event/timer[3]/rtx/run_cnt(对齐原工程)
- 新增 app_sys.cpp (替换 app_modules.c):
  * 读取 app_config.json → 按 enable:true/false 启停模块
  * 自动为每个模块创建 3 级定时器 (10ms/100ms/1000ms) + 事件
  * X-Macro 展开 g_vec_app 初始化列表
- 新增 app_config.json: {"apps":[{"name":"app_modbus_m","enable":true}]}
- 删除 app_modules.c: 旧的动态注册框架
2026-07-08 16:38:10 +08:00
ypc aed907a977 refactor(libmodbus_m): 解耦线程模块,移除对外头文件
- 删除 myModbusM.h: 线程模块不再对外暴露头文件
- copy_headers.sh: 移除 libmodbus_m 拷贝
- modbus_m.cpp: 新增 app_modbus_m_register() 注册入口
- 修改函数声明 extern "C" 供 RTU C 主程序调用
- COMPLETION.md: RTU 标记完成

设计原则: system/ 下除 RTU 外所有线程模块不对外提供头文件,
通过 app_module_register() 动态注册,实现完全解耦
2026-07-08 16:23:49 +08:00
ypc e1736315e4 feat(RTU): 主程序入口 + 模块注册框架
- src/system/inc/mySystem.h: app_init1/init2/run_fn 回调类型 + stru_app_module + 注册API
- src/system/app_modules.c: 模块注册框架实现(register/init1/init2/start/stop/join)
- src/system/RTU/src/main.c: 主程序入口(信号处理/模块生命周期/命令交互)
- release/src/system/makefile: 构建 libsystem.a(app_modules.c)
- release/src/system/RTU/makefile: 编译 RTU 可执行文件(链接全部 lib)
- 构建验证: x86 + ARM RTU 可执行文件全通过
2026-07-08 16:23:39 +08:00
24 changed files with 41210 additions and 185 deletions

View File

@ -40,8 +40,9 @@
| IEC 点表解析 | `libiec` | ⬜ |
| Modbus 主站管理 | `libmodbus_m`(原工程自研模块,导入+适配) | ✅ |
| 通讯通道解码 | `libcom_decode` | ⬜ |
| Web 管理服务 | `libweb_server` | ⬜ |
| 主程序入口 | `RTU` | ⬜ |
| Web 管理服务 | `libweb_server`(3 文件拆分,单线程,EV_STOP 支持) | ✅ |
| Mongoose 网络库 | `libmongoose`(开源库 v7.22,导入+适配) | ✅ |
| 主程序入口 | `RTU`(X-Macro 框架 + app_config.json 启停控制 + 独立 config/ 目录) | ✅ |
## 阶段 3:系统集成

View File

@ -0,0 +1,5 @@
{
"apps": [
{"name": "app_modbus_m", "enable": true}
]
}

View File

@ -24,7 +24,21 @@
| libcmd | [API-libcmd.md](public/libcmd/API-libcmd.md) | C 接口+C++ 内部 | 命令注册/补全 + 内置 linenoise |
| libcomm | [API-libcomm.md](public/libcomm/API-libcomm.md) | C 接口+C++ 内部 | TCP/UDP/UART 通讯抽象层 |
### 通讯与协议库 (protocol/ — 阶段 2 计划)
### 通讯与协议库 (protocol/)
| 模块 | 文档 | 语言 | 说明 |
|------|------|------|------|
| libmodbus | [API-libmodbus.md](protocol/libmodbus/API-libmodbus.md) | C (开源库) | Modbus 协议栈 v3.1.12 |
| lib60870 | [API-lib60870.md](protocol/lib60870/API-lib60870.md) | C (自研) | IEC 60870-5-101/103/104 协议栈 |
| libmongoose | [API-libmongoose.md](protocol/libmongoose/API-libmongoose.md) | C (MIT 开源) | HTTP/WebSocket 嵌入式网络库 v7.22 |
### 系统模块 (system/)
| 模块 | 文档 | 语言 | 说明 |
|------|------|------|------|
| 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 实时推送) |
```
┌──────────┐

View File

@ -0,0 +1,369 @@
# API-libmongoose — 嵌入式 HTTP/WebSocket 网络库
> 版本: v1.0
> 日期: 2026-07-08
> 来源: Mongoose v7.22 (MIT 许可,https://github.com/cesanta/mongoose)
> 状态: ✅ 导入完成,x86 + ARM 双平台编译通过(零警告)
---
## 1. 模块概述
libmongoose 是 Mongoose v7.22 开源库的封装模块。Mongoose 是一个**单文件、纯 C 实现的嵌入式网络库**,广泛用于 IoT 和嵌入式设备。本模块主要用于 RTU 的 Web 管理服务(`libweb_server`),提供 HTTP REST API 和 WebSocket 双向通信能力。
| 功能 | 说明 |
|------|------|
| HTTP 服务器 | 解析请求、路由匹配、JSON 响应,用于 REST API |
| WebSocket | 双向消息推送(实时遥测数据、告警通知) |
| 事件驱动 | 基于 epoll 的异步 IO,单线程管理所有连接 |
| 定时器 | 毫秒级软定时器,无需额外线程 |
| 跨平台 | Linux / FreeRTOS / Zephyr / ESP32 等,本工程只用 Linux |
### 架构位置
```
libweb_server (system/) — REST API + WebSocket 管理
│
▼
libmongoose (protocol/) — HTTP/WS 协议栈
│
▼
Linux kernel socket + epoll
```
---
## 2. 文件结构
```
src/protocol/libmongoose/
├── inc/
│ ├── myMongoose.h # RTU 适配头(编译配置宏 + #include "mongoose.h")
│ └── mongoose.h # Mongoose v7.22 头文件(5748 行,上游文件)
└── src/
└── mongoose.c # Mongoose v7.22 实现(31786 行,上游文件)
release/
├── inc/
│ ├── myMongoose.h # 拷贝自 src,供其他模块包含
│ └── mongoose.h # 拷贝自 src
└── x86/lib/
└── libmongoose.a # x86 编译产物(462KB)
└── arm/lib/
└── libmongoose.a # ARM 编译产物(471KB)
```
---
## 3. 编译配置
在 `myMongoose.h` 中定义以下宏(必须在 `#include "mongoose.h"` 之前):
| 宏 | 值 | 说明 |
|------|-----|------|
| `MG_ENABLE_LOG` | 1 | 启用内置日志(printf 到 stderr) |
| `MG_ENABLE_FILE` | 0 | 禁用文件系统(RTU 不通过 mongoose 提供静态文件) |
| `MG_ENABLE_POSIX_FS` | 0 | 禁用 POSIX 文件 API |
| `MG_ENABLE_EPOLL` | 1 | 启用 epoll(Linux 高效 IO 多路复用) |
| `MG_ENABLE_POLL` | 1 | 启用 poll(epoll 回退方案) |
| `MG_ENABLE_SOCKET` | 1 | 使用 Linux 内核 socket |
---
## 4. 核心数据结构
### 4.1 mg_mgr — 事件管理器
```c
struct mg_mgr
{
struct mg_connection *conns; /* 活动连接链表 */
struct mg_timer *timers; /* 定时器链表 */
// ... 内部字段
};
```
每个进程只需一个 `mg_mgr` 实例,管理所有连接和定时器。
### 4.2 mg_connection — 连接
```c
struct mg_connection
{
struct mg_connection *next; /* 链表指针 */
struct mg_mgr *mgr; /* 所属管理器 */
struct mg_addr peer; /* 对端地址 */
void *fd; /* socket 文件描述符 */
unsigned long id; /* 连接 ID */
unsigned is_websocket : 1; /* 是否为 WebSocket 连接 */
unsigned is_client : 1; /* 是否为客户端连接 */
// ...
};
```
### 4.3 事件类型
```c
enum
{
MG_EV_ERROR, /* 错误 */
MG_EV_OPEN, /* 连接建立(仅客户端出站连接) */
MG_EV_POLL, /* mg_mgr_poll() 每次迭代触发 */
MG_EV_RESOLVE, /* DNS 解析完成 */
MG_EV_CONNECT, /* TCP 连接完成 */
MG_EV_ACCEPT, /* TCP 新连接接受 */
MG_EV_TLS_HS, /* TLS 握手完成 */
MG_EV_READ, /* 数据可读 */
MG_EV_WRITE, /* 数据可写 */
MG_EV_CLOSE, /* 连接关闭 */
MG_EV_HTTP_MSG, /* HTTP 请求/响应解析完成 */
MG_EV_HTTP_CHUNK, /* HTTP 分块数据 */
MG_EV_WS_OPEN, /* WebSocket 握手完成 */
MG_EV_WS_MSG, /* WebSocket 消息到达 */
MG_EV_WS_CTL, /* WebSocket 控制帧 */
MG_EV_MQTT_CMD, /* MQTT 命令 */
MG_EV_MQTT_MSG, /* MQTT 消息 */
MG_EV_MQTT_OPEN, /* MQTT 连接建立 */
MG_EV_SNTP_TIME, /* SNTP 时间 */
MG_EV_USER, /* 用户自定义事件起点 */
};
```
---
## 5. 接口函数
### 5.1 生命周期
```c
/** 初始化事件管理器 */
void mg_mgr_init(struct mg_mgr *mgr);
/** 事件循环(每次调用处理一批事件,阻塞不超过 ms 毫秒) */
void mg_mgr_poll(struct mg_mgr *mgr, int ms);
/** 销毁事件管理器(关闭所有连接,释放所有内存) */
void mg_mgr_free(struct mg_mgr *mgr);
```
**典型用法**:
```c
struct mg_mgr mgr;
mg_mgr_init(&mgr);
/* 注册 HTTP/WS 监听 + 定时器 */
// ...
while (running)
{
mg_mgr_poll(&mgr, 50); /* 50ms 超时 */
}
mg_mgr_free(&mgr);
```
### 5.2 HTTP 服务器
```c
/** 创建 HTTP 监听连接 */
struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data);
/** 获取 HTTP 请求/响应消息 */
int mg_http_get_request_len(const unsigned char *buf, size_t buf_len);
struct mg_http_message *mg_http_parse(const char *buf, size_t len);
struct mg_http_message *mg_http_parse2(const char *buf, size_t len);
/** 响应辅助 */
void mg_http_reply(struct mg_connection *c, int status_code,
const char *headers, const char *body_fmt, ...);
void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm,
const char *root);
/** 路由匹配 */
bool mg_match(struct mg_str str, struct mg_str pattern, struct mg_str *caps);
bool mg_http_match_uri(const struct mg_http_message *hm, const char *pattern);
```
**REST API 示例**:
```c
static void fn(struct mg_connection *c, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG)
{
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
if (mg_http_match_uri(hm, "/api/status"))
{
mg_http_reply(c, 200, "Content-Type: application/json\r\n",
"{\"status\":\"ok\"}");
}
}
}
mg_http_listen(&mgr, "http://0.0.0.0:8080", fn, NULL);
```
### 5.3 WebSocket
```c
/** 升级 HTTP 连接到 WebSocket */
void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm,
const char *fmt, ...);
/** 发送 WebSocket 消息 */
size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op);
/** 格式化发送 WebSocket 消息 */
size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...);
/** 封装非 WS socket 为 WS 帧(用于代理) */
size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op,
const void *buf, size_t buf_len);
/** 主动连接 WebSocket 服务器(客户端模式) */
struct mg_connection *mg_ws_connect(struct mg_mgr *mgr, const char *url,
mg_event_handler_t fn, void *fn_data,
const char *fmt, ...);
```
**WebSocket 帧操作码 (op)**:
```c
#define WEBSOCKET_OP_CONTINUE 0 /* 继续帧 */
#define WEBSOCKET_OP_TEXT 1 /* 文本消息 */
#define WEBSOCKET_OP_BINARY 2 /* 二进制消息 */
#define WEBSOCKET_OP_CLOSE 8 /* 关闭握手 */
#define WEBSOCKET_OP_PING 9 /* Ping */
#define WEBSOCKET_OP_PONG 10 /* Pong */
```
**WebSocket 服务端示例**:
```c
static void fn(struct mg_connection *c, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG)
{
struct mg_http_message *hm = (struct mg_http_message *)ev_data;
if (mg_match(hm->uri, mg_str("/ws"), NULL))
{
mg_ws_upgrade(c, hm, NULL);
}
}
else if (ev == MG_EV_WS_MSG)
{
struct mg_ws_message *wm = (struct mg_ws_message *)ev_data;
/* 回显消息 */
mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT);
}
}
```
### 5.4 定时器
```c
/** 添加一次性定时器 */
struct mg_timer *mg_timer_add(struct mg_mgr *mgr, uint64_t milliseconds,
unsigned flags, void (*fn)(void *), void *arg);
/** 定时器标志 */
#define MG_TIMER_REPEAT 1 /* 周期性定时器 */
#define MG_TIMER_RUN_NOW 2 /* 立即运行一次 */
```
### 5.5 通用收发
```c
/** 发送原始数据 */
bool mg_send(struct mg_connection *c, const void *buf, size_t len);
/** 格式化发送 */
size_t mg_printf(struct mg_connection *c, const char *fmt, ...);
size_t mg_vprintf(struct mg_connection *c, const char *fmt, va_list *ap);
/** 关闭连接 */
void mg_close_conn(struct mg_connection *c);
```
---
## 6. 使用约束与注意事项
### 6.1 线程安全
- `mg_mgr_poll()` 必须在**单线程**中调用
- 如需要多线程,通过 `mg_wakeup()` 向事件循环投递消息
### 6.2 内存管理
- 所有通过 `mg_http_listen()` 创建的连接在 `mg_mgr_free()` 时自动关闭释放
- `mg_http_message` 中的 `hm->body`、`hm->query` 等字段为协议栈内部缓冲区,**不可手动 free**
### 6.3 与 RTU 的集成方式
- `libweb_server` 作为 RTU 的独立线程模块,在线程内创建自己的 `mg_mgr`
- 通过 X-Macro 框架的定时器触发 `mg_mgr_poll()`
- HTTP 路由中使用 `dc_get_signal_val()` 获取实时数据,`dc_set_out_signal_val()` 发布前端写入的数据
### 6.4 日志说明
mongoose 内部使用 `printf` 向 stderr 输出日志(`MG_ENABLE_LOG=1`)。在 RTU 装置上 stderr 通常重定向到 `/dev/null` 或 syslog。如需捕获 mongoose 日志,可定义 `MG_ENABLE_CUSTOM_LOG=1` 并提供自定义 `MG_LOG()` 宏。
---
## 7. 依赖关系
### 7.1 本模块依赖
- **无**(mongoose 是纯 C、零依赖的单文件库,直接使用 Linux socket API)
### 7.2 依赖本模块的模块
| 模块 | 用途 |
|------|------|
| `libweb_server` (system/) | HTTP REST API + WebSocket 通信 |
### 7.3 模块依赖图
```
┌──────────────────┐
│ libweb_server │ (RTU 线程模块)
│ REST API + WS │
└────────┬─────────┘
│ myMongoose.h
▼
┌──────────────────┐
│ libmongoose │ (纯 C 协议栈)
│ HTTP + WebSocket │
└────────┬─────────┘
│
▼
Linux Socket API + epoll
```
---
## 8. 编译与构建
```bash
# 编译 libmongoose.a
cd release/src/protocol/libmongoose
make -f makefile all # x86
make -f makefile CROSS=arm all # ARM
# 参与全量构建
cd release && ./build.sh # x86
cd release && ./build.sh arm # ARM
```
---
## 9. 许可证
Mongoose 使用 **MIT 许可证**(商业友好)。完整许可文本见 `mongoose.h` 头部注释。
---
## 10. 参考链接
- [Mongoose 官方文档](https://mongoose.ws/documentation/)
- [Mongoose GitHub](https://github.com/cesanta/mongoose)
- [WebSocket RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455)
- [HTTP/1.1 RFC 7230](https://datatracker.ietf.org/doc/html/rfc7230)

531
docs/system/RTU/API-RTU.md Normal file
View File

@ -0,0 +1,531 @@
# API-RTU — RTU 主程序模块
> 版本: v3.0
> 日期: 2026-07-08
> 来源: 基于原工程 e173631 重构,升级为 X-Macro 框架
> 状态: 已实现,x86 + ARM 双平台编译通过
---
## 1. 模块概述
RTU 模块是 RTU_ALL_AI 项目的**主程序入口和系统管理框架**,负责:
| 功能 | 说明 |
|------|------|
| 模块生命周期管理 | X-Macro 驱动的线程模块注册、初始化(init1→init2→start→stop) |
| 配置文件启停控制 | 通过 `config/SYSTEM/app_config.json` 按需启用/禁用模块 |
| 三级定时器 | 为每个模块自动创建 10ms / 100ms / 1000ms 三级周期性定时器 |
| 事件驱动循环 | 每个模块通过 `stru_task_event_t` 等待定时器事件和自己业务事件 |
| 优雅关闭 | `EV_STOP` 信号 + `pthread_join` 确保所有线程安全退出 |
| 命令行交互 | 内置 `version` / `uptime` 命令,各模块可注册自定义命令 |
### 核心设计理念
**新增模块只需修改 [app_modules.h](../../../src/system/inc/app_modules.h) 中一行宏**,无需改动框架代码。模块的枚举值、`extern` 声明、`g_vec_app` 初始化列表全部由 X-Macro 自动展开生成,消除了传统运行时注册的样板代码。
### 与原始框架(e173631)对比
| 方面 | 原始(运行时注册) | 升级后(X-Macro) |
|------|---------------|--------------|
| 模块注册 | `app_module_register()` 运行时调用 | `APP_MODULE()` 编译时宏,零运行时开销 |
| 启停控制 | 无 | `app_config.json` 按需启用 |
| 定时器 | 无(模块自行实现) | 自动三级定时器 |
| 停止 | `app_modules_stop/join` 独立函数 | `app_sys_stop()` 一体化 |
| 新增模块 | 修改 main.c + app_modules.c | 仅修改 app_modules.h 一行 |
---
## 2. 文件结构
```
src/system/
├── inc/
│ ├── app_modules.h # 模块注册表(X-Macro 中心列表)
│ └── mySystem.h # 公共定义(stru_app / enum_app / API 声明)
└── RTU/
└── src/
├── main.c # 主程序入口(信号处理 → 初始化 → 主循环 → 关闭)
└── app_sys.cpp # 系统管理实现(配置解析 / 模块初始化 / 停止)
config/
└── SYSTEM/
└── app_config.json # 运行时模块启停配置(部署到 RK 装置)
```
---
## 3. 架构
```
app_config.json → cJSON 解析
│
▼
g_enabled_apps (std::set)
│
▼
app_modules.h ──X-Macro──▶ g_vec_app (std::vector<stru_app>)
│ │
│ ┌─────────┼─────────┐
│ ▼ ▼ ▼
│ app_init1 app_init2 创建定时器
│ │ │ (10/100/1000ms)
│ │ │ │
│ ▼ ▼ ▼
│ ┌────────────────────────────┐
└── is_app_enabled( ) ──▶ pthread_create() │
│ 独立线程运行 fun_cb │
└──────────┬─────────┘
│
┌──────────▼──────────┐
│ task_event_wait() │
│ ← EV_TIMER1/2/3 │
│ ← EV_STOP │
└──────────────────────┘
```
### 启动流程
```
main()
│
├── signal(SIGINT/SIGTERM) → sig_handler()
│
├── log_init() / dc_init()
│
├── app_sys_init()
│ ├── [1] app_config_load()
│ │ ├── get_system_config_path()
│ │ │ ├── WORK_PATH 环境变量(RK 装置)
│ │ │ └── func_proc_self_dir()(本地调试)
│ │ └── cJSON_Parse(app_config.json)
│ │ → g_enabled_apps
│ │
│ ├── [2] app_init()
│ │ ├── init1: 遍历 g_vec_app → 调用各模块 init1()
│ │ ├── init2: 遍历 g_vec_app → 调用各模块 init2()
│ │ └── start: 遍历 g_vec_app
│ │ ├── task_event_create()
│ │ ├── app_timer_create() × 3
│ │ └── pthread_create() → fun_cb
│ │
│ └── return 0
│
├── while(g_running) → 命令行输入循环
│
└── app_sys_stop() ← Ctrl+C
├── EV_STOP → 各模块
└── pthread_join → 各线程
```
---
## 4. 核心数据结构
### 4.1 模块枚举(enum_app)
由 `app_modules.h` X-Macro 生成,无需手动定义:
```c
/* app_modules.h */
APP_MODULE(MODBUS_M, modbus_m, app_modbus_m_init1, app_modbus_m_init2, app_modbus_m)
// 新增模块只需追加一行
/* mySystem.h 自动展开为 */
typedef enum
{
ENUM_APP_MODBUS_M, // ← 自动生成
// ENUM_APP_XXX, // ← 新增模块的枚举值
ENUM_APP_MAX
} enum_app;
```
### 4.2 模块上下文(stru_app)
每个线程模块一个实例,由 `app_sys.cpp` 自动创建和管理。
```c
typedef struct
{
const char *name; /**< 模块名称,如 "app_modbus_m" */
uint32_t app_id; /**< 模块 ID(枚举值) */
app_init_fn app_init1; /**< 阶段 1 初始化(信号注册前) */
app_init_fn app_init2; /**< 阶段 2 初始化(信号注册后) */
app_func_fn fun_cb; /**< 主循环入口(独立线程) */
void *arg; /**< 线程参数(指向自身) */
stru_task_event_t p_event; /**< 事件句柄(定时器 + 停止信号) */
stru_rtx_data rtx; /**< 收发数据缓冲区 */
uint32_t timer_arg[TIMER_NUM]; /**< 定时器参数([app_id:8, idx:8]) */
stru_task_timer_t p_timer[TIMER_NUM]; /**< 三级定时器句柄 */
uint32_t run_cnt; /**< 运行计数 */
} stru_app;
```
### 4.3 事件定义
```c
#define TIMER_NUM 3 /* 定时器级数 */
#define EV_TIMER1 (0x01 << 0) /* 10ms 定时器事件 */
#define EV_TIMER2 (0x01 << 1) /* 100ms 定时器事件 */
#define EV_TIMER3 (0x01 << 2) /* 1000ms 定时器事件 */
#define EV_STOP (0x80) /* 模块停止信号 */
#define MSG_SIZE 2048 /* 收发缓冲区大小 */
```
---
## 5. 接口函数
### 5.1 app_sys_init()
```c
int app_sys_init(void);
```
**功能**: 初始化所有 X-Macro 注册的模块。包括加载 `app_config.json`、执行各模块 init1/init2、创建事件 + 定时器 + 启动线程。
**参数**: 无
**返回值**:
| 值 | 说明 |
|----|------|
| 0 | 初始化成功 |
| -1 | 初始化失败(配置文件解析/模块 init/线程创建) |
**调用位置**: `main.c` 中 `dc_init()` 之后、主循环之前。
### 5.2 app_sys_stop()
```c
void app_sys_stop(void);
```
**功能**: 停止所有模块。先向每个模块发送 `EV_STOP` 信号通知其退出主循环,然后 `pthread_join` 等待所有线程安全退出。
**调用位置**: `main.c` 中收到 SIGINT/SIGTERM 或 `Ctrl+D` 后、`dc_cleanup()` 之前。
### 5.3 app_get_ptr()
```c
stru_app *app_get_ptr(uint32_t app_id);
```
**功能**: 根据模块 ID 获取 `stru_app` 指针。供模块内部或外部代码访问其他模块数据。
**参数**:
| 参数 | 说明 |
|------|------|
| app_id | 模块 ID(`enum_app` 枚举值) |
**返回值**: 有效模块指针,超出范围返回 `NULL`。
---
## 6. 配置文件
### 6.1 app_config.json
位置:`config/SYSTEM/app_config.json`
```json
{
"apps": [
{"name": "app_modbus_m", "enable": true}
]
}
```
**字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `apps` | Array | 模块配置列表 |
| `name` | String | 模块名称,格式为 `app_<配置名>`,与 `app_modules.h` 中 cfg_name 对应 |
| `enable` | Boolean | `true`=启用,`false`=禁用 |
**配置路径优先级**:
1. `WORK_PATH` 环境变量 → `$WORK_PATH/config/SYSTEM/`(RK3568 装置运行时由启动脚本设定)
2. 可执行文件所在目录 → `<exe_dir>/config/SYSTEM/`(x86 本地调试用 fallback)
**部署约定**: `config/` 整个目录直接同步到 RK 装置可执行文件的同级目录下。
### 6.2 配置文件缺失
若 `app_config.json` 未找到,框架**默认启用所有模块**,不视为错误。日志输出:
```
app_config.json not found, enabling all modules by default
```
---
## 7. 新增模块步骤
只需三步:
### Step 1: 在 `app_modules.h` 追加一行
```c
/* src/system/inc/app_modules.h */
APP_MODULE(NEW_MODULE, new_module, app_new_module_init1, app_new_module_init2, app_new_module)
```
**宏参数说明**:
| 参数 | 示例 | 说明 |
|------|------|------|
| `name` | `NEW_MODULE` | 枚举后缀,生成 `ENUM_APP_NEW_MODULE` |
| `cfg_name` | `new_module` | 配置名,生成名称 `"app_new_module"` |
| `init1` | `app_new_module_init1` | 阶段 1 初始化函数(信号注册前) |
| `init2` | `app_new_module_init2` | 阶段 2 初始化函数(信号注册后),可为 `NULL` |
| `func` | `app_new_module` | 主循环入口函数(独立线程) |
### Step 2: 实现模块代码
在 `src/system/<模块名>/src/` 下实现四个函数:
```c
/* init1: 信号注册前(可解析配置文件) */
int app_new_module_init1(void *arg)
{
stru_app *p_app = (stru_app *)arg;
// 1. 注册输出信号到 datacenter
// 2. 解析模块配置文件(XML/JSON)
return 0;
}
/* init2: 信号注册后(注册遥控/遥调信号及回调) */
int app_new_module_init2(void *arg)
{
// 注册 yk/ao 信号及其回调函数
return 0;
}
/* 主循环: 事件驱动 */
void *app_new_module(void *arg)
{
stru_app *p_app = (stru_app *)arg;
uint32_t event;
while (1)
{
task_event_wait(p_app->p_event, &event, 0);
if (event & EV_STOP)
{
break; /* 退出循环 */
}
if (event & EV_TIMER1)
{
/* 10ms 周期任务 */
p_app->run_cnt++;
}
/* ... 其他事件处理 ... */
}
return NULL;
}
```
### Step 3: 更新构建系统
在 `release/src/system/` 下创建模块子目录和 makefile,并在 `release/src/system/makefile` 中注册:
```makefile
SUBDIRS := ./new_module ./libmodbus_m ./RTU
```
### Step 4: 更新配置文件
在 `config/SYSTEM/app_config.json` 中添加模块条目:
```json
{"name": "app_new_module", "enable": true}
```
---
## 8. 模块编写规范
### 8.1 主循环模板
```c
void *app_xxx(void *arg)
{
stru_app *p_app = (stru_app *)arg;
if (!p_app || !p_app->p_event)
{
LOG_E("app_xxx arg null");
return NULL;
}
uint32_t event;
uint32_t cnt_10ms = 0;
uint32_t cnt_100ms = 0;
uint32_t cnt_1s = 0;
while (1)
{
task_event_wait(p_app->p_event, &event, 0);
if (event & EV_STOP)
{
LOG_I("app_xxx stopping, total runs: 10ms=%u 100ms=%u 1s=%u",
cnt_10ms, cnt_100ms, cnt_1s);
break;
}
if (event & EV_TIMER1)
{
cnt_10ms++;
/* 10ms 周期处理 */
}
if (event & EV_TIMER2)
{
cnt_100ms++;
/* 100ms 周期处理 */
}
if (event & EV_TIMER3)
{
cnt_1s++;
/* 1000ms 周期处理 */
p_app->run_cnt++;
}
}
return NULL;
}
```
### 8.2 事件处理顺序
事件处理遵循 **先停止后业务** 原则:`EV_STOP` 检查放在最前面,确保收到停止信号后立刻退出。
### 8.3 配置路径约定
模块内部读取配置文件时,遵循与框架一致的路径约定(参考 `modbus_m.cpp` 的 `get_base_path()`):
```c
LOCAL std::string get_base_path()
{
char buf[512] = {0};
if (0 == func_get_work_path(buf, sizeof(buf))) /* 优先 WORK_PATH */
{
return std::string(buf) + "/";
}
if (0 != func_proc_self_dir(buf, sizeof(buf))) /* fallback 可执行文件目录 */
{
LOG_E("get_base_path failed");
return "";
}
return std::string(buf) + "/";
}
```
---
## 9. 编译与构建
### 9.1 编译 RTU 可执行文件
```bash
# x86 本地编译
cd release && ./build.sh
# ARM 交叉编译(RK3568)
cd release && ./build.sh arm
```
### 9.2 产物
| 平台 | 路径 | 说明 |
|------|------|------|
| x86 | `release/x86/exe/RTU` | ELF 64-bit x86-64, with debug_info |
| ARM | `release/arm/exe/RTU` | ELF 64-bit ARM aarch64, with debug_info |
### 9.3 链接依赖
RTU 可执行文件链接以下库:
```
libmodbus_m libmodbus lib60870
libdatacenter libxml libcmd libtask
liblog libfunc libmy_xxhash libcJSON libmd5 libcomm
libpthread libstdc++
```
---
## 10. 依赖关系
### 10.1 依赖的库模块
| 模块 | 用途 |
|------|------|
| `liblog` | 日志输出 |
| `libfunc` | 文件操作、环境变量路径、字符串处理 |
| `libdatacenter` | 信号数据中心(dc_init/dc_cleanup) |
| `libtask` | 事件机制、定时器、延迟 |
| `libcmd` | 命令行注册与交互 |
| `libcJSON` | app_config.json 解析 |
| `libmodbus` | 版本号显示 |
| `libmodbus_m` | Modbus 主站线程模块 |
### 10.2 被依赖
RTU 是最顶层模块,不被其他模块依赖。
### 10.3 模块间依赖图
```
┌─────────────────────────────────────────────────┐
│ RTU │
│ (main.c + app_sys.cpp) │
│ │
│ 信号 → libtask (事件/定时器) │
│ 日志 → liblog │
│ 配置 → libcJSON + libfunc (文件/路径) │
│ 命令 → libcmd (linenoise) │
└──────┬──────────────────────────────────────────┘
│ app_modules.h X-Macro
│
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ libmodbus_m │ │ libcom_decode│ │ libweb_server│
│ (已实现) │ │ (预留) │ │ (预留) │
└──────┬───────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ libdatacenter│
│ libmodbus │
│ lib60870 │
└──────────────┘
```
---
## 11. 命令行参考
| 命令 | 说明 |
|------|------|
| `help` | 显示所有已注册命令(libcmd 内置) |
| `version` | 显示固件版本 + libmodbus 版本 |
| `uptime` | 显示运行状态 |
| `modbus_m info` | 显示 Modbus 主站通道和点表信息 |
| `datacenter stat` | 显示数据中心统计(libdatacenter 内置) |
---
## 12. 变更历史
| 日期 | 版本 | 变更 |
|------|------|------|
| 2026-07-08 | v3.0 | 配置文件独立化(`config/` 目录)、添加 `app_sys_stop()`、代码风格统一 |
| 2026-07-08 | v2.0 | X-Macro 框架重构 + `app_config.json` 启停控制 + 三级定时器 |
| 2026-07-08 | v1.0 | 原始运行时注册框架(`app_modules.c` + `main.c`) |

View File

@ -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<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;
```
### 信号类型枚举
```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": "<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 获取命令列表
```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行) |

View File

@ -101,11 +101,13 @@ copy_module "libcomm" "myComm.h"
# ================================================================
copy_proto_module "libmodbus" "myModbus.h"
copy_proto_module "lib60870" "myIec60870.h"
copy_proto_module "libmongoose" "myMongoose.h"
copy_proto_module "libmongoose" "mongoose.h"
# ================================================================
# 系统模块(system/)
# ================================================================
copy_sys_header "mySystem.h"
copy_sys_module "libmodbus_m" "myModbusM.h"
copy_sys_module "libweb_server" "web_server.h"
echo "=== done ==="

View File

@ -0,0 +1,31 @@
include ./../../../linux.mk
L := $(notdir $(realpath $(CURDIR)/..))
M := libmongoose
O := $(LIB_REL)/$(M).a
S := $(SRC_ROOT_DIR)/$(L)/libmongoose/src
I := -I$(SRC_ROOT_DIR)/$(L)/libmongoose/inc
B := $(CURDIR)/$(M)/obj
SRCS := $(wildcard $(S)/*.c)
OBJS := $(patsubst $(S)/%.c, $(B)/%.o, $(SRCS))
F := $(C_FLAGS) $(I)
.PHONY: all
all:
@mkdir -p $(ROOT_DIR)/release/inc
@$(MAKE) $(O)
$(O): $(OBJS)
@mkdir -p $(dir $@)
$(AR) rcs $@ $^
@echo "[$(M)] built"
$(B)/%.o: $(S)/%.c
@mkdir -p $(dir $@)
$(CC) $(F) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(B) $(O)
.PHONY: rebuild
rebuild: clean all

View File

@ -1,4 +1,4 @@
SUBDIRS := ./libmodbus ./lib60870
SUBDIRS := ./libmodbus ./lib60870 ./libmongoose
define make_subdir
for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && make -f makefile $1) || true; done
endef

View File

@ -0,0 +1,49 @@
include ./../../../linux.mk
ROOT := $(realpath $(CURDIR)/../../../..)
INC_PATH := -I$(ROOT)/release/inc \
-I$(ROOT)/src/system/inc \
-I$(ROOT)/src/system/libweb_server/inc \
-I$(ROOT)/src/public/libdatacenter/inc \
-I$(ROOT)/src/public/liblog/inc \
-I$(ROOT)/src/public/libfunc/inc \
-I$(ROOT)/src/public/libcmd/inc \
-I$(ROOT)/src/public/libtask/inc \
-I$(ROOT)/src/public/libmy_xxhash/inc \
-I$(ROOT)/src/public/libcJSON/inc \
-I$(ROOT)/src/protocol/libmodbus/inc \
-I$(ROOT)/src/protocol/libmongoose/inc
LIBS := -L$(ROOT)/release/$(BUILD_TYPE)/lib \
-lweb_server -lmodbus_m -lmodbus -l60870 \
-lmongoose -ldatacenter -lxml -lcmd -ltask \
-llog -lfunc -lmy_xxhash -lcJSON -lmd5 -lcomm \
-lpthread -lstdc++
O := $(ROOT)/release/$(BUILD_TYPE)/exe/RTU
S := $(ROOT)/src/system/RTU/src
C_OBJ := $(CURDIR)/RTU/obj/main.o
CXX_OBJ := $(CURDIR)/RTU/obj/app_sys.o
.PHONY: all clean rebuild
all: $(O)
$(O): $(C_OBJ) $(CXX_OBJ)
@mkdir -p $(dir $@)
$(CXX) $^ $(LIBS) -o $@
@echo "[RTU] built -> $(O)"
$(C_OBJ): $(S)/main.c
@mkdir -p $(dir $@)
$(CC) $(C_FLAGS) $(INC_PATH) -c $< -o $@
$(CXX_OBJ): $(S)/app_sys.cpp
@mkdir -p $(dir $@)
$(CXX) $(CXX_FLAGS) $(INC_PATH) -c $< -o $@
clean:
rm -rf $(CURDIR)/RTU/obj $(O)
rebuild: clean all

View File

@ -0,0 +1,39 @@
include ./../../../linux.mk
L := $(notdir $(realpath $(CURDIR)/..))
M := libweb_server
O := $(LIB_REL)/$(M).a
S := $(SRC_ROOT_DIR)/$(L)/libweb_server/src
I := -I$(SRC_ROOT_DIR)/$(L)/libweb_server/inc \
-I$(SRC_ROOT_DIR)/$(L)/inc \
-I$(SRC_ROOT_DIR)/protocol/libmongoose/inc \
-I$(SRC_ROOT_DIR)/public/libdatacenter/inc \
-I$(SRC_ROOT_DIR)/public/liblog/inc \
-I$(SRC_ROOT_DIR)/public/libfunc/inc \
-I$(SRC_ROOT_DIR)/public/libcmd/inc \
-I$(SRC_ROOT_DIR)/public/libtask/inc \
-I$(SRC_ROOT_DIR)/public/libcJSON/inc
B := $(CURDIR)/$(M)/obj
SRCS := $(wildcard $(S)/*.cpp)
OBJS := $(patsubst $(S)/%.cpp, $(B)/%.o, $(SRCS))
F := $(CXX_FLAGS) $(I)
.PHONY: all
all:
@mkdir -p $(ROOT_DIR)/release/inc
@$(MAKE) $(O)
$(O): $(OBJS)
@mkdir -p $(dir $@)
$(AR) rcs $@ $^
@echo "[$(M)] built"
$(B)/%.o: $(S)/%.cpp
@mkdir -p $(dir $@)
$(CXX) $(F) -c $< -o $@
.PHONY: clean
clean:
rm -rf $(B) $(O)
.PHONY: rebuild
rebuild: clean all

View File

@ -1,8 +1,14 @@
SUBDIRS := ./libmodbus_m
define make_subdir
for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && make -f makefile $1) || true; done
endef
include ./../../linux.mk
# app_sys.cpp 已合并到 RTU makefile 中,此处仅管理子模块
SUBDIRS := ./libmodbus_m ./libweb_server ./RTU
.PHONY: all clean rebuild
all:; @$(call make_subdir,all)
clean:; @$(call make_subdir,clean)
all:
@for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && $(MAKE) -f makefile all) || true; done
clean:
@for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && $(MAKE) -f makefile clean) || true; done
rebuild: clean all

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
/**
* @file myMongoose.h
* @brief Mongoose 适配层 — 嵌入式 HTTP/WebSocket 网络库
* @details 封装 mongoose v7.22 开源库(MIT 许可),提供:
* - HTTP 服务器(REST API)
* - WebSocket 双向通信
* - 事件驱动异步 IO(epoll)
* 本头文件是对外公开接口,模块内部直接使用 mongoose.h。
*
* 许可证: MIT (https://github.com/cesanta/mongoose)
* 官方文档: https://mongoose.ws/documentation/
*/
#ifndef _MY_MONGOOSE_H_
#define _MY_MONGOOSE_H_
#ifdef __cplusplus
extern "C"
{
#endif
/* ---- 编译配置(在包含 mongoose.h 之前定义) ---- */
/** 启用日志(printf 到 stderr,与 RTU liblog 并行工作) */
#define MG_ENABLE_LOG 1
/** 禁用文件系统支持(RTU 不通过 mongoose 提供静态文件服务) */
#define MG_ENABLE_FILE 0
/** 禁用 POSIX 文件系统相关 API */
#define MG_ENABLE_POSIX_FS 0
/** 启用 epoll 支持(Linux 下高效的 IO 多路复用) */
#define MG_ENABLE_EPOLL 1
/** 启用 poll 支持(epoll 不可用时的回退方案) */
#define MG_ENABLE_POLL 1
/** 启用 TCP/IP 协议栈(使用 Linux 内核 socket) */
#define MG_ENABLE_SOCKET 1
#include "mongoose.h"
#ifdef __cplusplus
}
#endif
#endif /* _MY_MONGOOSE_H_ */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,393 @@
/**
* @file app_sys.cpp
* @brief 应用系统初始化 — X-Macro 驱动 + app_config.json 启停控制
* @details 运行时从 <exe目录>/config/SYSTEM/app_config.json 读取模块启停配置,
* 通过 app_modules.h X-Macro 自动生成模块列表和 g_vec_app,
* 为每个模块创建事件 + 3 级定时器 + 独立线程。
*
* 配置文件目录结构:
* WORK_PATH/ (可执行文件所在目录)
* ├── RTU
* └── config/
* └── SYSTEM/
* └── app_config.json
*/
#include "mySystem.h"
#include "myLog.h"
#include "myFunc.h"
#include "myDatacenter.h"
#include "cJSON.h"
#include <string>
#include <vector>
#include <set>
#include <pthread.h>
/* ---- 启用的应用名集合(从 app_config.json 读取) ---- */
static std::set<std::string> g_enabled_apps;
/* ---- 线程句柄(用于 stop 时 join) ---- */
static std::vector<pthread_t> g_threads;
/* ---- g_vec_app — 由 app_modules.h X-Macro 自动生成 ---- */
static std::vector<stru_app> g_vec_app =
{
#define APP_MODULE(name, cfg_name, init1, init2, func) \
{"app_" #cfg_name, 0, init1, init2, (app_func_fn)func, NULL, 0, {}, {}, {}, 0},
#include "app_modules.h"
#undef APP_MODULE
};
/* ---- 三级定时器事件 ---- */
static uint32_t g_timer_event[TIMER_NUM] = {EV_TIMER1, EV_TIMER2, EV_TIMER3};
/* ---- 获取系统配置目录路径(优先级: WORK_PATH 环境变量 > 可执行文件所在目录) ---- */
static std::string get_system_config_path(void)
{
char buf[512] = {0};
/* 优先使用 WORK_PATH 环境变量(RK3568 装置运行时设置) */
if (0 == func_get_work_path(buf, sizeof(buf)))
{
return std::string(buf) + "/config/SYSTEM/";
}
/* fallback: 从可执行文件路径推导(本地调试用) */
if (0 != func_proc_self_dir(buf, sizeof(buf)))
{
LOG_E("func_proc_self_dir failed");
return "";
}
return std::string(buf) + "/config/SYSTEM/";
}
/* ---- 解析 app_config.json ---- */
static int app_config_load(void)
{
std::string path = get_system_config_path();
if (path.empty())
{
return -1;
}
std::string cfg_path = path + "app_config.json";
uint32_t fsize = 0;
uint8_t *buf = func_read_file_alloc(cfg_path.c_str(), &fsize);
if (!buf)
{
/* 配置文件不存在时不视为错误——默认启用所有模块 */
LOG_I("app_config.json not found, enabling all modules by default");
for (size_t i = 0; i < g_vec_app.size(); i++)
{
g_enabled_apps.insert(g_vec_app[i].name);
}
return 0;
}
cJSON *root = cJSON_Parse((const char *)buf);
free(buf);
if (!root)
{
LOG_E("cJSON_Parse failed for app_config.json");
return -1;
}
cJSON *apps = cJSON_GetObjectItem(root, "apps");
if (!apps || !cJSON_IsArray(apps))
{
LOG_E("app_config.json missing 'apps' array");
cJSON_Delete(root);
return -1;
}
int arr_size = cJSON_GetArraySize(apps);
for (int i = 0; i < arr_size; i++)
{
cJSON *item = cJSON_GetArrayItem(apps, i);
if (!item)
{
continue;
}
cJSON *name = cJSON_GetObjectItem(item, "name");
cJSON *enable = cJSON_GetObjectItem(item, "enable");
if (!name || !cJSON_IsString(name))
{
continue;
}
if (enable && cJSON_IsTrue(enable))
{
g_enabled_apps.insert(std::string(name->valuestring));
LOG_I("app enabled: %s", name->valuestring);
}
else
{
LOG_I("app disabled: %s", name->valuestring);
}
}
cJSON_Delete(root);
if (g_enabled_apps.empty())
{
LOG_E("no apps enabled in config");
return -1;
}
return 0;
}
/* ---- 判断模块是否启用 ---- */
static bool is_app_enabled(const char *name)
{
return g_enabled_apps.find(name) != g_enabled_apps.end();
}
/* ---- 定时器回调:发送对应的定时器事件给模块 ---- */
static void app_timer_expire(void *arg)
{
if (!arg)
{
return;
}
uint32_t signal = *(uint32_t *)arg;
uint8_t thid = signal >> 8;
uint8_t tm_id = signal & 0xFF;
if (thid >= g_vec_app.size() || tm_id >= TIMER_NUM)
{
return;
}
task_event_send(g_vec_app[thid].p_event, g_timer_event[tm_id]);
}
/* ---- 为一个模块创建 3 级定时器 ---- */
static int app_timer_create(stru_app *p_app)
{
if (!p_app)
{
return -1;
}
uint32_t delay_ms = 1;
for (int i = 0; i < TIMER_NUM; i++)
{
std::string tm_name = std::string(p_app->name) + "_tm" + std::to_string(i + 1);
p_app->timer_arg[i] = ((p_app->app_id << 8) | i) & 0xFFFF;
delay_ms *= 10;
p_app->p_timer[i] = task_timer_create(
tm_name.c_str(),
app_timer_expire,
(void *)&p_app->timer_arg[i],
delay_ms,
TASK_TIMER_FLAG_PERIODIC);
if (!p_app->p_timer[i])
{
LOG_E("timer%d create failed for %s", i + 1, p_app->name);
return -1;
}
task_timer_start(p_app->p_timer[i]);
}
return 0;
}
/* ---- 主初始化流程 ---- */
static int app_init(void)
{
size_t i;
/* init1: 信号注册前 */
for (i = 0; i < g_vec_app.size(); i++)
{
stru_app *p = &g_vec_app[i];
if (!p->app_init1)
{
continue;
}
if (!is_app_enabled(p->name))
{
LOG_I("app skipped (disabled): %s", p->name);
continue;
}
if (0 != p->app_init1((void *)p))
{
LOG_E("app_init1 failed: %s", p->name);
return -1;
}
}
/* init2: 信号注册后 */
for (i = 0; i < g_vec_app.size(); i++)
{
stru_app *p = &g_vec_app[i];
if (!p->app_init2)
{
continue;
}
if (!is_app_enabled(p->name))
{
LOG_I("app skipped (disabled): %s", p->name);
continue;
}
if (0 != p->app_init2((void *)p))
{
LOG_E("app_init2 failed: %s", p->name);
return -1;
}
}
/* 创建事件 + 定时器 + 启动线程 */
for (i = 0; i < g_vec_app.size(); i++)
{
stru_app *p = &g_vec_app[i];
p->app_id = i;
std::string ev_name = "EV_" + std::string(p->name);
p->p_event = task_event_create(ev_name.c_str());
if (!p->p_event)
{
LOG_E("task_event_create failed: %s", ev_name.c_str());
return -1;
}
p->arg = (void *)p;
if (0 != app_timer_create(p))
{
LOG_E("app_timer_create failed: %s", p->name);
return -1;
}
if (!is_app_enabled(p->name))
{
LOG_I("app skipped (disabled): %s", p->name);
continue;
}
pthread_t thread;
if (0 != pthread_create(&thread, NULL, p->fun_cb, p->arg))
{
LOG_E("pthread_create failed: %s", p->name);
return -1;
}
g_threads.push_back(thread);
LOG_I("thread started: %s", p->name);
task_sleep_ms(10);
}
return 0;
}
/* ---- 公共 API ---- */
int app_sys_init(void)
{
if (0 != app_config_load())
{
LOG_E("app_config_load failed");
return -1;
}
if (g_vec_app.size() != (size_t)ENUM_APP_MAX)
{
LOG_E("g_vec_app size mismatch: %zu vs ENUM %d",
g_vec_app.size(), ENUM_APP_MAX);
return -1;
}
if (0 != app_init())
{
LOG_E("app_init failed");
return -1;
}
return 0;
}
/**
* @brief 停止所有应用模块
* @details 向每个模块发送 EV_STOP 信号,通知其退出主循环,然后 join 所有线程
*/
void app_sys_stop(void)
{
LOG_I("stopping all app modules...");
for (size_t i = 0; i < g_vec_app.size(); i++)
{
stru_app *p = &g_vec_app[i];
if (p->p_event)
{
task_event_send(p->p_event, EV_STOP);
}
}
/* 等待所有线程退出 */
for (size_t i = 0; i < g_threads.size(); i++)
{
pthread_join(g_threads[i], NULL);
LOG_I("module thread [%zu] exited", i);
}
LOG_I("all app modules stopped");
}
stru_app *app_get_ptr(uint32_t app_id)
{
if (app_id >= ENUM_APP_MAX)
{
LOG_E("app_id out of range: %u max %d", app_id, ENUM_APP_MAX);
return NULL;
}
return &g_vec_app[app_id];
}

125
src/system/RTU/src/main.c Normal file
View File

@ -0,0 +1,125 @@
/**
* @file main.c
* @brief RTU 主程序入口
* @details app_sys_init() 自动启动所有 X-Macro 注册的模块。
* 新增模块只需修改 src/system/inc/app_modules.h,无需改动本文件。
*
* 配置文件约定:
* WORK_PATH/ (可执行文件所在目录,或 WORK_PATH 环境变量指定)
* ├── RTU
* └── config/
* └── SYSTEM/
* └── app_config.json
*/
#include "mySystem.h"
#include "myLog.h"
#include "myDatacenter.h"
#include "myModbus.h"
#include "myFunc.h"
#include "myCmd.h"
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
static volatile int g_running = 1;
static void sig_handler(int sig)
{
(void)sig;
g_running = 0;
LOG_I("signal %d received, shutting down...", sig);
}
static void cmd_version(int argc, char *argv[])
{
(void)argc;
(void)argv;
printf("RTU_ALL_AI v1.0\n");
printf(" libmodbus v%d.%d.%d\n",
libmodbus_version_major, libmodbus_version_minor,
libmodbus_version_micro);
}
static void cmd_uptime(int argc, char *argv[])
{
(void)argc;
(void)argv;
printf("RTU uptime: (running)\n");
}
CMD_REGISTER("version", cmd_version, "show firmware version");
CMD_REGISTER("uptime", cmd_uptime, "show uptime");
int main(int argc, char *argv[])
{
(void)argc;
(void)argv;
printf("\n=== RTU_ALL_AI Starting ===\n\n");
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
signal(SIGPIPE, SIG_IGN);
log_init(NULL);
LOG_I("RTU booting...");
dc_init(NULL);
if (app_sys_init() != 0)
{
LOG_E("app_sys_init failed, aborting");
return 1;
}
LOG_I("RTU running");
while (g_running)
{
char *line = linenoise("RTU> ");
if (!line)
{
break;
}
if (line[0] != '\0')
{
linenoiseHistoryAdd(line);
char *argv_buf[16];
uint8_t argc = 0;
if (func_str2argv(line, &argc, argv_buf, 16) == 0 && argc > 0)
{
stru_cmd *cmd = cmd_find(argv_buf[0]);
if (cmd)
{
cmd->func((int)argc, argv_buf);
}
else
{
printf("unknown command: %s (type 'help')\n", argv_buf[0]);
}
}
}
linenoiseFree(line);
}
LOG_I("shutting down...");
app_sys_stop();
dc_cleanup();
log_cleanup();
printf("=== RTU_ALL_AI Stopped ===\n");
return 0;
}

View File

@ -0,0 +1,27 @@
/**
* @file app_modules.h
* @brief 应用模块注册表 — X-Macro 中心化列表
* @details 此文件故意不设 include guard,因为需要被多次包含
* (每次在不同的 APP_MODULE 宏定义下展开)。
* 新增模块只需在此文件中追加一行,无需修改其他代码。
*
* 格式: APP_MODULE(枚举后缀, 配置名, init1, init2, 线程入口)
* - 枚举后缀: 用于生成 ENUM_APP_<后缀> 枚举值(大写)
* - 配置名: 用于生成 "app_<配置名>" 名称字符串(小写)
* - init1: 阶段1 初始化函数(信号注册前)
* - init2: 阶段2 初始化函数(信号注册后,可为 NULL)
* - 线程入口: 主循环入口函数(独立线程中运行)
*/
#ifndef APP_MODULE
#error "APP_MODULE macro must be defined before including app_modules.h"
#endif
/* ============== 线程模块(system/ 子目录) ============== */
APP_MODULE(MODBUS_M, modbus_m, app_modbus_m_init1, app_modbus_m_init2, app_modbus_m)
APP_MODULE(WEB_SERVER, web_server, app_web_server_init1, app_web_server_init2, app_web_server)
/* ============== 预留模块(阶段 2-3 实现后取消注释) ============== */
// APP_MODULE(COM_DECODE, com_decode, app_com_decode_init1, app_com_decode_init2, app_com_decode)
// APP_MODULE(IEC, iec, app_iec_init1, app_iec_init2, app_iec)
// APP_MODULE(WEB_SERVER, web_server, app_web_server_init1, app_web_server_init2, app_web_server)

View File

@ -1,7 +1,11 @@
/**
* @file mySystem.h
* @brief 系统管理模块公共定义
* @details 供 libmodbus_m/libiec/libcom_decode/RTU 等系统模块共用。
* @brief 系统管理模块公共定义 — X-Macro 框架
* @details 通过 app_modules.h X-Macro 自动生成:
* 1) enum_app 枚举(所有模块 ID)
* 2) extern 函数声明(init1/init2/thread)
* 3) g_vec_app 初始化列表(app_sys.cpp 中使用)
* 新增模块只需修改 app_modules.h,无需改框架代码。
*/
#ifndef _MY_SYSTEM_H_
#define _MY_SYSTEM_H_
@ -14,15 +18,69 @@ extern "C"
{
#endif
/** @brief 应用任务上下文(RTU 多任务运行时使用) */
/** 每个模块的定时器数量(10ms / 100ms / 1000ms 三级) */
#define TIMER_NUM 3
#define EV_TIMER1 (0x01 << 0)
#define EV_TIMER2 (0x01 << 1)
#define EV_TIMER3 (0x01 << 2)
#define EV_STOP (0x80) /**< 模块停止信号 */
#define MSG_SIZE 2048
/* ---- 应用枚举,由 app_modules.h 生成 ---- */
#define APP_MODULE(name, cfg_name, init1, init2, func) ENUM_APP_##name,
typedef enum
{
#include "app_modules.h"
ENUM_APP_MAX
} enum_app;
#undef APP_MODULE
/* ---- 应用数据收发结构体 ---- */
typedef struct
{
const char *name; /**< 任务名称 */
stru_task_event_t p_event; /**< 事件句柄 */
stru_task_msg_queue_t p_queue; /**< 消息队列句柄 */
uint32_t run_cnt; /**< 运行计数 */
uint16_t rx_len;
uint16_t tx_len;
uint8_t rx[MSG_SIZE];
uint8_t tx[MSG_SIZE];
} stru_rtx_data;
/* ---- 应用上下文(每个线程模块一个实例) ---- */
typedef int (*app_init_fn)(void *arg);
typedef void *(*app_func_fn)(void *arg);
typedef struct
{
const char *name;
uint32_t app_id;
app_init_fn app_init1;
app_init_fn app_init2;
app_func_fn fun_cb;
void *arg;
stru_task_event_t p_event;
stru_rtx_data rtx;
uint32_t timer_arg[TIMER_NUM];
stru_task_timer_t p_timer[TIMER_NUM];
uint32_t run_cnt;
} stru_app;
/* ---- 获取模块指针 ---- */
stru_app *app_get_ptr(uint32_t app_id);
/* ---- extern 函数声明,由 app_modules.h 生成 ---- */
#define APP_MODULE(name, cfg_name, init1, init2, func) \
extern int init1(void *arg); \
extern int init2(void *arg); \
extern void *func(void *arg);
#include "app_modules.h"
#undef APP_MODULE
/* ---- 初始化与停止 ---- */
int app_sys_init(void);
void app_sys_stop(void);
#ifdef __cplusplus
}
#endif

View File

@ -1,13 +0,0 @@
/**
* @file myModbusM.h
* @brief Modbus 主站管理模块统一入口
* @details 原工程自研模块,通过 XML 配置通道和点表,轮询读取 Modbus 从站数据,
* 写入 libdatacenter,支持遥控/遥调异步写入。
* 调用者只需 #include "myModbusM.h"。
*/
#ifndef _MY_MODBUS_M_H_
#define _MY_MODBUS_M_H_
#include "modbus_m.h"
#endif /* _MY_MODBUS_M_H_ */

View File

@ -20,20 +20,30 @@ LOCAL std::vector<stru_modbus_m_write_req> g_write_queue;
LOCAL std::string get_base_path()
{
char proc_dir[512] = {0};
if(0 != func_proc_self_dir(proc_dir, sizeof(proc_dir)))
char buf[512] = {0};
/* 优先使用 WORK_PATH 环境变量(RK3568 装置运行时设置) */
if (0 == func_get_work_path(buf, sizeof(buf)))
{
MY_LOG_E("func_get_process_self_dir failed");
return std::string(buf) + "/";
}
/* fallback: 从可执行文件路径推导(本地调试用) */
if (0 != func_proc_self_dir(buf, sizeof(buf)))
{
LOG_E("func_proc_self_dir failed");
return "";
}
return std::string(proc_dir);
return std::string(buf) + "/";
}
// ==================== 数据类型映射 ====================
LOCAL uint8_t modbus_type_to_local_type(uint8_t type)
{
switch(type)
switch (type)
{
case 2: return DATA_TYPE_B; // bit/线圈
case 4: return DATA_TYPE_U16; // uint16
@ -46,7 +56,7 @@ LOCAL uint8_t modbus_type_to_local_type(uint8_t type)
LOCAL uint8_t dc_data_type_size(uint8_t data_type)
{
switch(data_type)
switch (data_type)
{
case DATA_TYPE_B: return 1;
case DATA_TYPE_U8: return 1;
@ -65,7 +75,7 @@ LOCAL uint8_t dc_data_type_size(uint8_t data_type)
LOCAL int parse_channels(XMLElement *p_root)
{
XMLElement *p_ch = p_root->FirstChildElement("Channel");
while(NULL != p_ch)
while (NULL != p_ch)
{
stru_modbus_m_channel ch;
ch.id = (uint32_t)p_ch->IntAttribute("id", 0);
@ -83,14 +93,14 @@ LOCAL int parse_channels(XMLElement *p_root)
ch.connected = false;
g_modbus_m_cfg.channels.push_back(ch);
MY_LOG_I("channel %d: mode=%s, host=%s:%d", ch.id, ch.mode.c_str(), ch.host.c_str(), ch.port);
LOG_I("channel %d: mode=%s, host=%s:%d", ch.id, ch.mode.c_str(), ch.host.c_str(), ch.port);
p_ch = p_ch->NextSiblingElement("Channel");
}
if(g_modbus_m_cfg.channels.empty())
if (g_modbus_m_cfg.channels.empty())
{
MY_LOG_E("no channel configured");
LOG_E("no channel configured");
return -1;
}
return 0;
@ -122,33 +132,33 @@ LOCAL int parse_point_item(XMLElement *p_item, std::vector<stru_modbus_m_item> &
item.factor = (float)p_item->FloatAttribute("factor", 1.0f);
item.offset = (float)p_item->FloatAttribute("offset", 0.0f);
if(NULL != p_item->Attribute("saddr"))
if (NULL != p_item->Attribute("saddr"))
item.saddr = p_item->Attribute("saddr");
if(NULL != p_item->Attribute("desc"))
if (NULL != p_item->Attribute("desc"))
item.desc = p_item->Attribute("desc");
// 验证通道ID
bool ch_found = false;
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
if(ch.id == item.channel)
if (ch.id == item.channel)
{
ch_found = true;
break;
}
}
if(!ch_found)
if (!ch_found)
{
MY_LOG_E("item no=%d: channel %d not found", item.no, item.channel);
LOG_E("item no=%d: channel %d not found", item.no, item.channel);
return -1;
}
// 创建数据指针
uint8_t local_type = modbus_type_to_local_type(item.type);
item.p_val = dc_create_data(local_type);
if(NULL == item.p_val)
if (NULL == item.p_val)
{
MY_LOG_E("dc_create_data_ptr_by_type failed, type=%d", local_type);
LOG_E("dc_create_data_ptr_by_type failed, type=%d", local_type);
return -1;
}
@ -159,77 +169,77 @@ LOCAL int parse_point_item(XMLElement *p_item, std::vector<stru_modbus_m_item> &
LOCAL int parse_points(XMLElement *p_root)
{
XMLElement *p_point = p_root->FirstChildElement("Point");
if(NULL == p_point)
if (NULL == p_point)
{
MY_LOG_E("Point not found");
LOG_E("Point not found");
return -1;
}
if(NULL != p_point->Attribute("desc"))
if (NULL != p_point->Attribute("desc"))
g_modbus_m_cfg.point.desc = p_point->Attribute("desc");
// 解析 St(遥信)
XMLElement *p_st = p_point->FirstChildElement("St");
if(NULL != p_st)
if (NULL != p_st)
{
XMLElement *p_item = p_st->FirstChildElement("Item");
while(NULL != p_item)
while (NULL != p_item)
{
if(0 != parse_point_item(p_item, g_modbus_m_cfg.point.st_vec))
if (0 != parse_point_item(p_item, g_modbus_m_cfg.point.st_vec))
{
return -1;
}
p_item = p_item->NextSiblingElement("Item");
}
MY_LOG_I("parsed %zu st items", g_modbus_m_cfg.point.st_vec.size());
LOG_I("parsed %zu st items", g_modbus_m_cfg.point.st_vec.size());
}
// 解析 Mx(遥测)
XMLElement *p_mx = p_point->FirstChildElement("Mx");
if(NULL != p_mx)
if (NULL != p_mx)
{
XMLElement *p_item = p_mx->FirstChildElement("Item");
while(NULL != p_item)
while (NULL != p_item)
{
if(0 != parse_point_item(p_item, g_modbus_m_cfg.point.mx_vec))
if (0 != parse_point_item(p_item, g_modbus_m_cfg.point.mx_vec))
{
return -1;
}
p_item = p_item->NextSiblingElement("Item");
}
MY_LOG_I("parsed %zu mx items", g_modbus_m_cfg.point.mx_vec.size());
LOG_I("parsed %zu mx items", g_modbus_m_cfg.point.mx_vec.size());
}
// 解析 Co(遥控)
XMLElement *p_co = p_point->FirstChildElement("Co");
if(NULL != p_co)
if (NULL != p_co)
{
XMLElement *p_item = p_co->FirstChildElement("Item");
while(NULL != p_item)
while (NULL != p_item)
{
if(0 != parse_point_item(p_item, g_modbus_m_cfg.point.co_vec))
if (0 != parse_point_item(p_item, g_modbus_m_cfg.point.co_vec))
{
return -1;
}
p_item = p_item->NextSiblingElement("Item");
}
MY_LOG_I("parsed %zu co items", g_modbus_m_cfg.point.co_vec.size());
LOG_I("parsed %zu co items", g_modbus_m_cfg.point.co_vec.size());
}
// 解析 Ao(参数)
XMLElement *p_ao = p_point->FirstChildElement("Ao");
if(NULL != p_ao)
if (NULL != p_ao)
{
XMLElement *p_item = p_ao->FirstChildElement("Item");
while(NULL != p_item)
while (NULL != p_item)
{
if(0 != parse_point_item(p_item, g_modbus_m_cfg.point.ao_vec))
if (0 != parse_point_item(p_item, g_modbus_m_cfg.point.ao_vec))
{
return -1;
}
p_item = p_item->NextSiblingElement("Item");
}
MY_LOG_I("parsed %zu ao items", g_modbus_m_cfg.point.ao_vec.size());
LOG_I("parsed %zu ao items", g_modbus_m_cfg.point.ao_vec.size());
}
return 0;
@ -238,32 +248,32 @@ LOCAL int parse_points(XMLElement *p_root)
int modbus_m_cfg_parse(const std::string &path)
{
XMLDocument doc;
if(XML_SUCCESS != doc.LoadFile(path.c_str()))
if (XML_SUCCESS != doc.LoadFile(path.c_str()))
{
MY_LOG_E("load xml failed: %s", path.c_str());
LOG_E("load xml failed: %s", path.c_str());
return -1;
}
XMLElement *p_root = doc.RootElement();
if(NULL == p_root)
if (NULL == p_root)
{
MY_LOG_E("root element not found");
LOG_E("root element not found");
return -1;
}
if(0 != parse_channels(p_root))
if (0 != parse_channels(p_root))
{
MY_LOG_E("parse channels failed");
LOG_E("parse channels failed");
return -1;
}
if(0 != parse_points(p_root))
if (0 != parse_points(p_root))
{
MY_LOG_E("parse points failed");
LOG_E("parse points failed");
return -1;
}
MY_LOG_I("modbus_m cfg parsed: %zu channels, st=%zu, mx=%zu, co=%zu, ao=%zu",
LOG_I("modbus_m cfg parsed: %zu channels, st=%zu, mx=%zu, co=%zu, ao=%zu",
g_modbus_m_cfg.channels.size(),
g_modbus_m_cfg.point.st_vec.size(),
g_modbus_m_cfg.point.mx_vec.size(),
@ -278,13 +288,13 @@ int modbus_m_cfg_parse(const std::string &path)
LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
{
if(NULL == p_ch)
if (NULL == p_ch)
{
return -1;
}
// 释放旧连接
if(NULL != p_ch->ctx)
if (NULL != p_ch->ctx)
{
modbus_close(p_ch->ctx);
modbus_free(p_ch->ctx);
@ -292,29 +302,29 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
}
// 创建上下文
if(p_ch->mode == "tcp")
if (p_ch->mode == "tcp")
{
p_ch->ctx = modbus_new_tcp(p_ch->host.c_str(), p_ch->port);
if(NULL == p_ch->ctx)
if (NULL == p_ch->ctx)
{
MY_LOG_E("modbus_new_tcp failed: %s:%d", p_ch->host.c_str(), p_ch->port);
LOG_E("modbus_new_tcp failed: %s:%d", p_ch->host.c_str(), p_ch->port);
return -1;
}
// TCP 模式下设置默认从站地址
modbus_set_slave(p_ch->ctx, MODBUS_TCP_SLAVE);
}
else if(p_ch->mode == "rtu")
else if (p_ch->mode == "rtu")
{
p_ch->ctx = modbus_new_rtu(p_ch->device.c_str(), p_ch->baud, p_ch->parity, p_ch->data_bit, p_ch->stop_bit);
if(NULL == p_ch->ctx)
if (NULL == p_ch->ctx)
{
MY_LOG_E("modbus_new_rtu failed: %s", p_ch->device.c_str());
LOG_E("modbus_new_rtu failed: %s", p_ch->device.c_str());
return -1;
}
}
else
{
MY_LOG_E("unknown mode: %s", p_ch->mode.c_str());
LOG_E("unknown mode: %s", p_ch->mode.c_str());
return -1;
}
@ -325,9 +335,9 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
modbus_set_error_recovery(p_ch->ctx, (modbus_error_recovery_mode)(MODBUS_ERROR_RECOVERY_LINK | MODBUS_ERROR_RECOVERY_PROTOCOL));
// 建立连接
if(0 != modbus_connect(p_ch->ctx))
if (0 != modbus_connect(p_ch->ctx))
{
MY_LOG_E("modbus_connect failed: ch=%d, mode=%s", p_ch->id, p_ch->mode.c_str());
LOG_E("modbus_connect failed: ch=%d, mode=%s", p_ch->id, p_ch->mode.c_str());
modbus_free(p_ch->ctx);
p_ch->ctx = NULL;
p_ch->connected = false;
@ -335,13 +345,13 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
}
p_ch->connected = true;
MY_LOG_I("channel %d connected: mode=%s", p_ch->id, p_ch->mode.c_str());
LOG_I("channel %d connected: mode=%s", p_ch->id, p_ch->mode.c_str());
return 0;
}
LOCAL int channels_init()
{
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
channel_connect(&ch);
}
@ -350,9 +360,9 @@ LOCAL int channels_init()
LOCAL void channels_close()
{
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
if(NULL != ch.ctx)
if (NULL != ch.ctx)
{
modbus_close(ch.ctx);
modbus_free(ch.ctx);
@ -368,7 +378,7 @@ LOCAL void channels_close()
// 读取线圈 (FC 0x01) 或离散输入 (FC 0x02)
LOCAL int read_bits(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc, uint16_t addr, uint16_t count, uint8_t *p_dest)
{
if(NULL == p_ch || NULL == p_ch->ctx || !p_ch->connected)
if (NULL == p_ch || NULL == p_ch->ctx || !p_ch->connected)
{
return -1;
}
@ -376,11 +386,11 @@ LOCAL int read_bits(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc, uint
modbus_set_slave(p_ch->ctx, slave);
int ret = -1;
if(fc == 0x01)
if (fc == 0x01)
{
ret = modbus_read_bits(p_ch->ctx, addr, count, p_dest);
}
else if(fc == 0x02)
else if (fc == 0x02)
{
ret = modbus_read_input_bits(p_ch->ctx, addr, count, p_dest);
}
@ -391,7 +401,7 @@ LOCAL int read_bits(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc, uint
// 读取寄存器 (FC 0x03 或 0x04)
LOCAL int read_registers(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc, uint16_t addr, uint16_t count, uint16_t *p_dest)
{
if(NULL == p_ch || NULL == p_ch->ctx || !p_ch->connected)
if (NULL == p_ch || NULL == p_ch->ctx || !p_ch->connected)
{
return -1;
}
@ -399,11 +409,11 @@ LOCAL int read_registers(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc,
modbus_set_slave(p_ch->ctx, slave);
int ret = -1;
if(fc == 0x03)
if (fc == 0x03)
{
ret = modbus_read_registers(p_ch->ctx, addr, count, p_dest);
}
else if(fc == 0x04)
else if (fc == 0x04)
{
ret = modbus_read_input_registers(p_ch->ctx, addr, count, p_dest);
}
@ -415,7 +425,7 @@ LOCAL int read_registers(stru_modbus_m_channel *p_ch, uint8_t slave, uint8_t fc,
// 读取单个 Item 值(按 fc 分发)
LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_item)
{
if(NULL == p_ch || NULL == p_item)
if (NULL == p_ch || NULL == p_item)
{
return -1;
}
@ -423,11 +433,11 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
uint8_t local_type = modbus_type_to_local_type(p_item->type);
// FC 0x01/0x02: 读线圈/离散输入(位)
if(p_item->fc == 0x01 || p_item->fc == 0x02)
if (p_item->fc == 0x01 || p_item->fc == 0x02)
{
uint8_t bit_val = 0;
int ret = read_bits(p_ch, p_item->slave, p_item->fc, p_item->addr, p_item->count, &bit_val);
if(ret > 0)
if (ret > 0)
{
uint8_t val = bit_val ? 1 : 0;
memcpy(p_item->p_val, &val, 1);
@ -436,20 +446,20 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
}
// FC 0x03/0x04: 读保持/输入寄存器
if(p_item->fc == 0x03 || p_item->fc == 0x04)
if (p_item->fc == 0x03 || p_item->fc == 0x04)
{
// 临时缓冲区(最大 125 个寄存器)
uint16_t reg_buf[256];
uint16_t read_cnt = p_item->count > 125 ? 125 : p_item->count;
int ret = read_registers(p_ch, p_item->slave, p_item->fc, p_item->addr, read_cnt, reg_buf);
if(ret <= 0)
if (ret <= 0)
{
return ret;
}
// type=2: 从寄存器提取位(FC03/04 读线圈模式)
if(p_item->type == 2 && p_item->bit < 16)
if (p_item->type == 2 && p_item->bit < 16)
{
uint8_t val = (reg_buf[0] >> p_item->bit) & 0x01;
memcpy(p_item->p_val, &val, 1);
@ -457,10 +467,10 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
}
// type=4 (uint16)
if(p_item->type == 4)
if (p_item->type == 4)
{
uint16_t val = reg_buf[0];
if(p_item->factor != 1.0f || p_item->offset != 0.0f)
if (p_item->factor != 1.0f || p_item->offset != 0.0f)
{
float fval = val * p_item->factor + p_item->offset;
val = (uint16_t)fval;
@ -470,10 +480,10 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
}
// type=6 (float32): 2 个连续寄存器
if(p_item->type == 6 && read_cnt >= 2)
if (p_item->type == 6 && read_cnt >= 2)
{
float val = modbus_get_float(&reg_buf[0]);
if(p_item->factor != 1.0f || p_item->offset != 0.0f)
if (p_item->factor != 1.0f || p_item->offset != 0.0f)
{
val = val * p_item->factor + p_item->offset;
}
@ -482,7 +492,7 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
}
// type=7 (uint32): 2 个连续寄存器
if(p_item->type == 7 && read_cnt >= 2)
if (p_item->type == 7 && read_cnt >= 2)
{
uint32_t val = ((uint32_t)reg_buf[0] << 16) | reg_buf[1];
memcpy(p_item->p_val, &val, 4);
@ -490,10 +500,10 @@ LOCAL int read_item_value(stru_modbus_m_channel *p_ch, stru_modbus_m_item *p_ite
}
// type=13 (string)
if(p_item->type == 13)
if (p_item->type == 13)
{
char *p_str = (char *)p_item->p_val;
for(uint16_t i = 0; i < read_cnt && i < 64; i++)
for (uint16_t i = 0; i < read_cnt && i < 64; i++)
{
p_str[i * 2] = (reg_buf[i] >> 8) & 0xFF;
p_str[i * 2 + 1] = reg_buf[i] & 0xFF;
@ -526,29 +536,29 @@ typedef struct
LOCAL void build_poll_groups(std::vector<stru_modbus_m_item> &items, std::vector<stru_poll_group> &groups)
{
for(auto &item : items)
for (auto &item : items)
{
// 只处理读类型的 fc
if(item.fc != 0x01 && item.fc != 0x02 && item.fc != 0x03 && item.fc != 0x04)
if (item.fc != 0x01 && item.fc != 0x02 && item.fc != 0x03 && item.fc != 0x04)
{
continue;
}
// 查找已有组
bool found = false;
for(auto &g : groups)
for (auto &g : groups)
{
if(g.channel == item.channel && g.slave == item.slave && g.fc == item.fc)
if (g.channel == item.channel && g.slave == item.slave && g.fc == item.fc)
{
g.items.push_back(&item);
if(item.addr < g.min_addr) g.min_addr = item.addr;
if(item.addr + item.count > g.max_addr) g.max_addr = item.addr + item.count;
if (item.addr < g.min_addr) g.min_addr = item.addr;
if (item.addr + item.count > g.max_addr) g.max_addr = item.addr + item.count;
found = true;
break;
}
}
if(!found)
if (!found)
{
stru_poll_group g;
g.channel = item.channel;
@ -564,19 +574,19 @@ LOCAL void build_poll_groups(std::vector<stru_modbus_m_item> &items, std::vector
LOCAL void poll_groups(std::vector<stru_poll_group> &groups)
{
for(auto &g : groups)
for (auto &g : groups)
{
// 查找通道
stru_modbus_m_channel *p_ch = NULL;
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
if(ch.id == g.channel && ch.connected)
if (ch.id == g.channel && ch.connected)
{
p_ch = &ch;
break;
}
}
if(NULL == p_ch)
if (NULL == p_ch)
{
continue;
}
@ -584,13 +594,13 @@ LOCAL void poll_groups(std::vector<stru_poll_group> &groups)
uint16_t total_count = g.max_addr - g.min_addr;
// FC 01/02: 读位
if(g.fc == 0x01 || g.fc == 0x02)
if (g.fc == 0x01 || g.fc == 0x02)
{
uint8_t bits[256] = {0};
int ret = read_bits(p_ch, g.slave, g.fc, g.min_addr, total_count > 2000 ? 2000 : total_count, bits);
if(ret > 0)
if (ret > 0)
{
for(auto *p_item : g.items)
for (auto *p_item : g.items)
{
uint16_t offset = p_item->addr - g.min_addr;
uint8_t val = bits[offset] ? 1 : 0;
@ -601,44 +611,44 @@ LOCAL void poll_groups(std::vector<stru_poll_group> &groups)
}
// FC 03/04: 读寄存器
if(g.fc == 0x03 || g.fc == 0x04)
if (g.fc == 0x03 || g.fc == 0x04)
{
uint16_t regs[256] = {0};
int ret = read_registers(p_ch, g.slave, g.fc, g.min_addr, total_count > 125 ? 125 : total_count, regs);
if(ret > 0)
if (ret > 0)
{
for(auto *p_item : g.items)
for (auto *p_item : g.items)
{
uint16_t offset = (p_item->addr - g.min_addr);
if(p_item->type == 2)
if (p_item->type == 2)
{
// FC03/04 线圈模式:从寄存器提取位
uint16_t reg_idx = offset;
uint8_t val = (regs[reg_idx] >> p_item->bit) & 0x01;
memcpy(p_item->p_val, &val, 1);
}
else if(p_item->type == 4)
else if (p_item->type == 4)
{
uint16_t val = regs[offset];
if(p_item->factor != 1.0f || p_item->offset != 0.0f)
if (p_item->factor != 1.0f || p_item->offset != 0.0f)
{
float fval = val * p_item->factor + p_item->offset;
val = (uint16_t)fval;
}
memcpy(p_item->p_val, &val, 2);
}
else if(p_item->type == 6 && offset + 1 < 256)
else if (p_item->type == 6 && offset + 1 < 256)
{
// float val = modbus_get_float(&regs[offset]);
float val = modbus_get_float_abcd(&regs[offset]);
if(p_item->factor != 1.0f || p_item->offset != 0.0f)
if (p_item->factor != 1.0f || p_item->offset != 0.0f)
{
val = val * p_item->factor + p_item->offset;
}
memcpy(p_item->p_val, &val, 4);
}
else if(p_item->type == 7 && offset + 1 < 256)
else if (p_item->type == 7 && offset + 1 < 256)
{
uint32_t val = ((uint32_t)regs[offset] << 16) | regs[offset + 1];
memcpy(p_item->p_val, &val, 4);
@ -662,19 +672,19 @@ LOCAL void process_write_queue()
queue.swap(g_write_queue);
}
for(auto &req : queue)
for (auto &req : queue)
{
// 查找通道
stru_modbus_m_channel *p_ch = NULL;
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
if(ch.id == req.channel && ch.ctx != NULL)
if (ch.id == req.channel && ch.ctx != NULL)
{
p_ch = &ch;
break;
}
}
if(NULL == p_ch)
if (NULL == p_ch)
{
LOG_E("write: channel %d not found", req.channel);
continue;
@ -683,7 +693,7 @@ LOCAL void process_write_queue()
modbus_set_slave(p_ch->ctx, req.slave);
int ret = -1;
if(req.is_coil)
if (req.is_coil)
{
ret = modbus_write_bit(p_ch->ctx, req.addr, req.value ? 1 : 0);
}
@ -692,7 +702,7 @@ LOCAL void process_write_queue()
ret = modbus_write_register(p_ch->ctx, req.addr, req.value);
}
if(ret < 0)
if (ret < 0)
{
LOG_E("write failed: ch=%d, slave=%d, fc=0x%02x, addr=%d", req.channel, req.slave, req.fc, req.addr);
}
@ -710,14 +720,14 @@ LOCAL void enqueue_write(const stru_modbus_m_write_req &req)
LOCAL void modbus_m_signal_co_change_callback(const char *saddr, dc_ctrl_step_t step, uint8_t data_type, uint8_t setting_zone, const void *p_data)
{
if(step != DC_CTRL_DIRECT)
if (step != DC_CTRL_DIRECT)
{
return;
}
for(auto &item : g_modbus_m_cfg.point.co_vec)
for (auto &item : g_modbus_m_cfg.point.co_vec)
{
if(item.saddr == saddr)
if (item.saddr == saddr)
{
stru_modbus_m_write_req req;
req.channel = item.channel;
@ -726,7 +736,7 @@ LOCAL void modbus_m_signal_co_change_callback(const char *saddr, dc_ctrl_step_t
req.addr = item.addr;
req.is_coil = true;
if(data_type == DATA_TYPE_B)
if (data_type == DATA_TYPE_B)
{
req.value = p_data ? (*(uint8_t *)p_data ? 1 : 0) : 0;
}
@ -736,7 +746,7 @@ LOCAL void modbus_m_signal_co_change_callback(const char *saddr, dc_ctrl_step_t
}
enqueue_write(req);
MY_LOG_I("co write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
LOG_I("co write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
break;
}
}
@ -744,14 +754,14 @@ LOCAL void modbus_m_signal_co_change_callback(const char *saddr, dc_ctrl_step_t
LOCAL void modbus_m_signal_ao_change_callback(const char *saddr, dc_ctrl_step_t step, uint8_t data_type, uint8_t setting_zone, const void *p_data)
{
if(step != DC_CTRL_DIRECT)
if (step != DC_CTRL_DIRECT)
{
return;
}
for(auto &item : g_modbus_m_cfg.point.ao_vec)
for (auto &item : g_modbus_m_cfg.point.ao_vec)
{
if(item.saddr == saddr)
if (item.saddr == saddr)
{
stru_modbus_m_write_req req;
req.channel = item.channel;
@ -760,11 +770,11 @@ LOCAL void modbus_m_signal_ao_change_callback(const char *saddr, dc_ctrl_step_t
req.addr = item.addr;
req.is_coil = false;
if(data_type == DATA_TYPE_U16)
if (data_type == DATA_TYPE_U16)
{
req.value = p_data ? *(uint16_t *)p_data : 0;
}
else if(data_type == DATA_TYPE_F32)
else if (data_type == DATA_TYPE_F32)
{
float f = p_data ? *(float *)p_data : 0;
req.value = (uint16_t)f;
@ -775,7 +785,7 @@ LOCAL void modbus_m_signal_ao_change_callback(const char *saddr, dc_ctrl_step_t
}
enqueue_write(req);
MY_LOG_I("ao write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
LOG_I("ao write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
break;
}
}
@ -786,39 +796,39 @@ LOCAL void modbus_m_signal_ao_change_callback(const char *saddr, dc_ctrl_step_t
LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, const std::string &signal_type)
{
for(auto &item : items)
for (auto &item : items)
{
uint8_t local_type = modbus_type_to_local_type(item.type);
if(signal_type == "st")
if (signal_type == "st")
{
if(0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
if (0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
{
MY_LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
return -1;
}
}
else if(signal_type == "mx")
else if (signal_type == "mx")
{
if(0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
if (0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
{
MY_LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
return -1;
}
}
else if(signal_type == "co")
else if (signal_type == "co")
{
if(0 != dc_signal_yk(item.saddr.c_str(), item.desc.c_str(), local_type, DC_CTRL_DIRECT, item.p_val, modbus_m_signal_co_change_callback, MODULE_MODBUS_M))
if (0 != dc_signal_yk(item.saddr.c_str(), item.desc.c_str(), local_type, DC_CTRL_DIRECT, item.p_val, modbus_m_signal_co_change_callback, MODULE_MODBUS_M))
{
MY_LOG_E("dc_signal_yk failed: %s", item.saddr.c_str());
LOG_E("dc_signal_yk failed: %s", item.saddr.c_str());
return -1;
}
}
else if(signal_type == "ao")
else if (signal_type == "ao")
{
if(0 != dc_signal_ao(item.saddr.c_str(), item.desc.c_str(), local_type, DC_CTRL_DIRECT, item.p_val, modbus_m_signal_ao_change_callback, MODULE_MODBUS_M))
if (0 != dc_signal_ao(item.saddr.c_str(), item.desc.c_str(), local_type, DC_CTRL_DIRECT, item.p_val, modbus_m_signal_ao_change_callback, MODULE_MODBUS_M))
{
MY_LOG_E("dc_signal_ao failed: %s", item.saddr.c_str());
LOG_E("dc_signal_ao failed: %s", item.saddr.c_str());
return -1;
}
}
@ -829,24 +839,24 @@ LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, co
LOCAL int signals_init()
{
if(0 != signal_register_items(g_modbus_m_cfg.point.st_vec, "st"))
if (0 != signal_register_items(g_modbus_m_cfg.point.st_vec, "st"))
{
return -1;
}
if(0 != signal_register_items(g_modbus_m_cfg.point.mx_vec, "mx"))
if (0 != signal_register_items(g_modbus_m_cfg.point.mx_vec, "mx"))
{
return -1;
}
if(0 != signal_register_items(g_modbus_m_cfg.point.co_vec, "co"))
if (0 != signal_register_items(g_modbus_m_cfg.point.co_vec, "co"))
{
return -1;
}
if(0 != signal_register_items(g_modbus_m_cfg.point.ao_vec, "ao"))
if (0 != signal_register_items(g_modbus_m_cfg.point.ao_vec, "ao"))
{
return -1;
}
MY_LOG_I("signals registered: st=%zu, mx=%zu, co=%zu, ao=%zu",
LOG_I("signals registered: st=%zu, mx=%zu, co=%zu, ao=%zu",
g_modbus_m_cfg.point.st_vec.size(),
g_modbus_m_cfg.point.mx_vec.size(),
g_modbus_m_cfg.point.co_vec.size(),
@ -860,15 +870,15 @@ LOCAL int signals_init()
int modbus_m_init()
{
if(0 != signals_init())
if (0 != signals_init())
{
MY_LOG_E("signals_init failed");
LOG_E("signals_init failed");
return -1;
}
if(0 != channels_init())
if (0 != channels_init())
{
MY_LOG_E("channels_init failed");
LOG_E("channels_init failed");
// 部分通道失败也可继续
}
@ -882,10 +892,10 @@ void modbus_m_show_info()
{
printf("===== Modbus Master Info =====\n");
printf("Channels: %zu\n", g_modbus_m_cfg.channels.size());
for(auto &ch : g_modbus_m_cfg.channels)
for (auto &ch : g_modbus_m_cfg.channels)
{
printf(" ch[%d]: mode=%s, connected=%d", ch.id, ch.mode.c_str(), ch.connected);
if(ch.mode == "tcp")
if (ch.mode == "tcp")
{
printf(", %s:%d", ch.host.c_str(), ch.port);
}
@ -910,13 +920,13 @@ LOCAL void cmd_modbus_m(int argc, char *argv[])
const char *str = "\
modbus_m info ----------> 查看所有通道和点表信息\n";
if(argc < 2)
if (argc < 2)
{
printf("%s", str);
return;
}
if(0 == strcmp(argv[1], "info"))
if (0 == strcmp(argv[1], "info"))
{
modbus_m_show_info();
}
@ -940,21 +950,21 @@ int app_modbus_m_init1(void *arg)
dc_signal_out("modbus_m.run_cnt", "modbus_m.run_cnt", DATA_TYPE_U32, &p_app->run_cnt, MODULE_MODBUS_M);
std::string base_path = get_base_path();
if(base_path.empty())
if (base_path.empty())
{
MY_LOG_E("get_base_path failed");
LOG_E("get_base_path failed");
return -1;
}
g_modbus_m_base_path = base_path + "config/MODBUS/";
std::string cfg_path = g_modbus_m_base_path + "modbus_m.xml";
if(0 != modbus_m_cfg_parse(cfg_path))
if (0 != modbus_m_cfg_parse(cfg_path))
{
MY_LOG_E("modbus_m_cfg_parse failed: %s", cfg_path.c_str());
LOG_E("modbus_m_cfg_parse failed: %s", cfg_path.c_str());
return -1;
}
if(0 != modbus_m_init())
if (0 != modbus_m_init())
{
LOG_E("modbus_m_init failed");
return -1;
@ -970,7 +980,7 @@ int app_modbus_m_init2(void *arg)
void *app_modbus_m(void *arg)
{
if(NULL == arg)
if (NULL == arg)
{
LOG_E("app_modbus_m arg null");
return NULL;
@ -985,9 +995,9 @@ void *app_modbus_m(void *arg)
// 预构建轮询分组
build_poll_groups(g_modbus_m_cfg.point.st_vec, st_groups);
build_poll_groups(g_modbus_m_cfg.point.mx_vec, mx_groups);
MY_LOG_I("poll groups: st=%zu, mx=%zu", st_groups.size(), mx_groups.size());
LOG_I("poll groups: st=%zu, mx=%zu", st_groups.size(), mx_groups.size());
while(1)
while (1)
{
task_event_recv(p_app->p_event,
0x01 | 0x02 | 0x04,
@ -1017,3 +1027,5 @@ void *app_modbus_m(void *arg)
return NULL;
}
/* ==================== 模块注册(由 app_modules.h X-Macro 自动完成,无需手动 register) ==================== */

View File

@ -0,0 +1,59 @@
/**
* @file web_server.h
* @brief Web 管理服务模块 — HTTP REST API + WebSocket 实时数据通道
* @details 基于 mongoose v7.22 实现嵌入式 Web 服务器。
* 作为 RTU X-Macro 框架的线程模块运行。
*
* 功能:
* - HTTP 静态文件服务(packed_fs 内嵌前端)
* - WebSocket 信号实时推送(增量更新)
* - 5 种信号类型增删改查(out/in/yk/ao/param)
* - 远程命令执行(捕获 stdout 回传)
* - 文件上传/下载
* - 异步命令输出通道
*/
#ifndef _WEB_SERVER_H_
#define _WEB_SERVER_H_
#include "myBase.h"
#include "mySystem.h"
#ifdef __cplusplus
extern "C"
{
#endif
#define MODULE_WEB_SERVER "web_server"
/** WebSocket 信号类型 */
typedef enum
{
WS_SIG_OUT = 0,
WS_SIG_IN,
WS_SIG_YK,
WS_SIG_AO,
WS_SIG_PARAM,
WS_SIG_MAX
} enum_ws_sig_type;
struct mg_connection;
/* ---- X-Macro 框架入口 ---- */
int app_web_server_init1(void *arg);
int app_web_server_init2(void *arg);
void *app_web_server(void *arg);
/* ---- 广播/单播发送 ---- */
void ws_send_all(const char *p_tx, size_t tx_len);
bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len);
bool ws_send_binary(unsigned long conn_id, const void *data, size_t len);
/* ---- 异步命令通道 ---- */
void ws_set_async_cmd_conn(const char *module, unsigned long conn_id);
unsigned long ws_get_async_cmd_conn(const char *module);
#ifdef __cplusplus
}
#endif
#endif /* _WEB_SERVER_H_ */

View File

@ -0,0 +1,435 @@
/**
* @file web_server.cpp
* @brief HTTP/WebSocket 服务器核心 — Mongoose 集成 + 连接管理
* @details 在 app_web_server 线程的 EV_TIMER1 中调用 mg_mgr_poll,
* 单线程复用 X-Macro 框架,消除原工程的双线程设计。
* EV_TIMER2: ws_push_task() 增量推送
* EV_TIMER3: run_cnt++
* EV_STOP: 优雅退出
*/
#include "web_server.h"
#include "myLog.h"
#include "myMongoose.h"
#include "cJSON.h"
#include "myFunc.h"
#include <string>
#include <vector>
#include <map>
#include <pthread.h>
#include <csignal>
/* ---- 硬编码配置 ---- */
#define WS_LISTEN_ON "http://0.0.0.0:8080"
#define WS_UPGRADE_URI "/ws"
/* ---- 全局状态 ---- */
static struct mg_mgr s_ws_mgr;
static std::vector<mg_connection *> g_ws_conns;
static pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
/* ---- 前向声明 ---- */
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len);
void ws_session_destroy(struct mg_connection *c);
void ws_push_task(void);
/* ---- 广播发送 ---- */
void ws_send_all(const char *p_tx, size_t tx_len)
{
if (!p_tx || tx_len == 0)
{
return;
}
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->is_websocket && !c->is_draining)
{
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
/* ---- 单连接发送 ---- */
bool ws_send_one(unsigned long conn_id, const char *p_tx, size_t tx_len)
{
if (!p_tx || tx_len == 0)
{
return false;
}
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->id == conn_id && c->is_websocket && !c->is_draining)
{
size_t sent = mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
ok = (sent >= tx_len);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
return ok;
}
/* ---- 二进制发送 ---- */
bool ws_send_binary(unsigned long conn_id, const void *data, size_t len)
{
if (!data || len == 0)
{
return false;
}
bool ok = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
mg_connection *c = g_ws_conns[i];
if (c->id == conn_id && c->is_websocket && !c->is_draining)
{
size_t sent = mg_ws_send(c, data, len, WEBSOCKET_OP_BINARY);
ok = (sent >= len);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
return ok;
}
/* ---- 连接列表管理 ---- */
static void ws_conn_remove(mg_connection *c)
{
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
if (g_ws_conns[i] == c)
{
g_ws_conns.erase(g_ws_conns.begin() + i);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
/* ---- Mongoose 事件处理器 ---- */
static void ws_event_handler(mg_connection *c, int ev, void *ev_data)
{
if (ev == MG_EV_HTTP_MSG)
{
mg_http_message *hm = (mg_http_message *)ev_data;
if (mg_match(hm->uri, mg_str(WS_UPGRADE_URI), NULL))
{
mg_ws_upgrade(c, hm, NULL);
c->data[0] = 'W';
}
else
{
/* HTTP 静态文件: 默认返回 404 */
mg_http_reply(c, 404, "Content-Type: text/plain\r\n", "Not Found\n");
}
}
else if (ev == MG_EV_WS_MSG)
{
mg_ws_message *wm = (mg_ws_message *)ev_data;
bool found = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for (size_t i = 0; i < g_ws_conns.size(); i++)
{
if (g_ws_conns[i] == c)
{
found = true;
break;
}
}
if (!found)
{
g_ws_conns.push_back(c);
}
pthread_mutex_unlock(&g_ws_conns_mutex);
ws_recv(c, wm->data.buf, (uint16_t)wm->data.len);
}
else if (ev == MG_EV_WS_CTL)
{
mg_ws_message *wm = (mg_ws_message *)ev_data;
uint8_t op = wm->flags & 15;
if (op == WEBSOCKET_OP_CLOSE)
{
ws_session_destroy(c);
ws_conn_remove(c);
}
}
else if (ev == MG_EV_CLOSE)
{
if (c->is_websocket)
{
ws_session_destroy(c);
ws_conn_remove(c);
}
}
}
/* ---- 命令行执行(捕获 stdout → 字符串) ---- */
#include "myCmd.h"
#include <unistd.h>
#include <cstring>
std::string ws_cmd_exec(const char *cmd_line)
{
if (!cmd_line)
{
return "";
}
char line[512];
strncpy(line, cmd_line, sizeof(line) - 1);
line[sizeof(line) - 1] = '\0';
/* strtok 解析参数 */
char *argv[16];
int argc = 0;
char *p = strtok(line, " \t");
while (p && argc < 16)
{
argv[argc++] = p;
p = strtok(NULL, " \t");
}
if (argc == 0)
{
return "";
}
stru_cmd *p_cmd = cmd_find(argv[0]);
if (!p_cmd)
{
return std::string("[unknown] ") + argv[0];
}
/* pipe 重定向 stdout */
fflush(stdout);
int pipefd[2];
if (pipe(pipefd) != 0)
{
return "pipe failed";
}
std::string result;
auto *ctx = new std::pair<int, std::string *>(pipefd[0], &result);
pthread_t reader;
pthread_create(&reader, NULL,
[](void *arg) -> void *
{
auto *cx = (std::pair<int, std::string *> *)arg;
char buf[4096];
ssize_t n;
while ((n = read(cx->first, buf, sizeof(buf))) > 0)
{
cx->second->append(buf, n);
}
close(cx->first);
delete cx;
return NULL;
},
ctx);
int saved = dup(STDOUT_FILENO);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
p_cmd->func(argc, argv);
fflush(stdout);
dup2(saved, STDOUT_FILENO);
close(saved);
pthread_join(reader, NULL);
if (!result.empty())
{
return result;
}
return "(no output)";
}
/* ---- 异步命令通道实现 ---- */
static std::map<std::string, unsigned long> g_async_cmd_conn_map;
static pthread_mutex_t g_async_cmd_conn_mutex = PTHREAD_MUTEX_INITIALIZER;
void ws_set_async_cmd_conn(const char *module, unsigned long conn_id)
{
if (!module)
{
return;
}
pthread_mutex_lock(&g_async_cmd_conn_mutex);
g_async_cmd_conn_map[module] = conn_id;
pthread_mutex_unlock(&g_async_cmd_conn_mutex);
}
unsigned long ws_get_async_cmd_conn(const char *module)
{
if (!module)
{
return 0;
}
pthread_mutex_lock(&g_async_cmd_conn_mutex);
auto it = g_async_cmd_conn_map.find(module);
unsigned long id = (it != g_async_cmd_conn_map.end()) ? it->second : 0;
pthread_mutex_unlock(&g_async_cmd_conn_mutex);
return id;
}
/* ---- 异步命令输出(给其他模块调用) ---- */
void async_cmd_printf(const char *module, const char *fmt, ...)
{
if (!module || !fmt)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if (conn_id == 0)
{
return;
}
va_list ap;
char buf[2048];
va_start(ap, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n > 0)
{
ws_send_binary(conn_id, buf, (size_t)n);
}
}
void async_cmd_complete(const char *module, bool success, const char *msg)
{
if (!module || !msg)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if (conn_id == 0)
{
return;
}
char buf[1024];
int n = snprintf(buf, sizeof(buf), "[%s] %s %s\n",
module, success ? "OK" : "FAIL", msg);
ws_send_binary(conn_id, buf, (size_t)n);
}
/* ================================================================
* X-Macro 框架入口
* ================================================================ */
int app_web_server_init1(void *arg)
{
(void)arg;
mg_mgr_init(&s_ws_mgr);
mg_http_listen(&s_ws_mgr, WS_LISTEN_ON, ws_event_handler, NULL);
LOG_I("web_server listening on %s, ws=%s", WS_LISTEN_ON, WS_UPGRADE_URI);
return 0;
}
int app_web_server_init2(void *arg)
{
(void)arg;
return 0;
}
void *app_web_server(void *arg)
{
if (!arg)
{
LOG_E("app_web_server arg null");
return NULL;
}
stru_app *p_app = (stru_app *)arg;
uint32_t event;
uint32_t cnt_1s = 0;
while (1)
{
task_event_recv(p_app->p_event,
EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
TASK_EVENT_FLAG_OR | TASK_EVENT_FLAG_CLEAR,
TASK_EVENT_WAIT_FOREVER,
&event);
if (event & EV_STOP)
{
LOG_I("web_server stopping");
break;
}
if (event & EV_TIMER1)
{
/* IO 轮询(非阻塞) */
mg_mgr_poll(&s_ws_mgr, 0);
}
if (event & EV_TIMER2)
{
/* 增量推送 */
ws_push_task();
}
if (event & EV_TIMER3)
{
cnt_1s++;
p_app->run_cnt++;
}
}
mg_mgr_free(&s_ws_mgr);
LOG_I("web_server stopped, total 1s cycles=%u", cnt_1s);
return NULL;
}

View File

@ -0,0 +1,671 @@
/**
* @file ws_method.cpp
* @brief WebSocket 命令解析与信号 CRUD 操作
* @details 接收 JSON 命令({type, saddr, signal_type, curd, signal_data}),
* 通过 g_ws_sig_methods 函数表分发到对应的信号增删改查函数。
* 支持 cmd/get_cmds/get_file/set_file 等管理命令。
*/
#include "web_server.h"
#include "myLog.h"
#include "myDatacenter.h"
#include "myCmd.h"
#include "myFunc.h"
#include "myMongoose.h"
#include "cJSON.h"
#include <string>
#include <vector>
#include <map>
#include <cstring>
#include <unistd.h>
/* ---- 前向声明(来自 web_server.cpp) ---- */
extern std::string ws_cmd_exec(const char *cmd_line);
/* ---- Per-Connection Session ---- */
typedef struct
{
std::string saddr;
std::string desc;
uint8_t data_type;
uint8_t ctrl_type;
std::vector<void *> vec_p_data;
std::string last_val;
} stru_ws_sig;
typedef struct
{
std::vector<stru_ws_sig> signals[WS_SIG_MAX];
} stru_ws_session;
static std::map<unsigned long, stru_ws_session> g_ws_sessions;
static pthread_mutex_t g_ws_session_mutex = PTHREAD_MUTEX_INITIALIZER;
/* ---- 前向声明 ---- */
static int ws_make_sig_json(stru_ws_session *s, cJSON *root, enum_ws_sig_type st, bool *has_change);
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);
/* ---- 函数表: 信号类型 → 名称 ---- */
static const char *g_ws_sig_names[WS_SIG_MAX] =
{
"out", "in", "yk", "ao", "param"
};
/* ================================================================
* 内部工具
* ================================================================ */
static int ws_sig_find_idx(const std::vector<stru_ws_sig> &vec, const char *saddr)
{
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr == saddr)
{
return (int)i;
}
}
return -1;
}
static std::string ws_get_val_str(const void *p_data, uint8_t data_type)
{
char buf[128] = {0};
dc_get_signal_val(p_data, data_type, buf, sizeof(buf));
return std::string(buf);
}
/* ================================================================
* 信号增删改 — out/in/yk/ao/param 分 5 种路径
* ================================================================ */
static void ws_sig_add_out(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_OUT];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
sig.data_type = 0;
char desc[128] = {0};
if (0 != dc_get_out_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &p_data))
{
LOG_E("dc_get_out_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_in(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_IN];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_in_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &p_data))
{
LOG_E("dc_get_in_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_yk(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_YK];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_yk_signal_info(saddr, desc, sizeof(desc), &sig.data_type, &sig.ctrl_type, &p_data))
{
LOG_E("dc_get_yk_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_ao(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_AO];
void *p_data = NULL;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_ao_signal_info(saddr, desc, sizeof(desc), &sig.data_type, NULL, &sig.ctrl_type, &p_data))
{
LOG_E("dc_get_ao_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
sig.vec_p_data.push_back(p_data);
vec.push_back(sig);
}
static void ws_sig_add_param(stru_ws_session *s, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[WS_SIG_PARAM];
void *p_data_arr[16] = {NULL};
int num_zones = 0;
if (ws_sig_find_idx(vec, saddr) >= 0)
{
return;
}
stru_ws_sig sig;
sig.saddr = saddr;
char desc[128] = {0};
if (0 != dc_get_param_signal_info(saddr, desc, sizeof(desc), &sig.data_type, NULL, &sig.ctrl_type, p_data_arr, &num_zones))
{
LOG_E("dc_get_param_signal_info failed: %s", saddr);
return;
}
sig.desc = desc;
for (int i = 0; i < num_zones; i++)
{
sig.vec_p_data.push_back(p_data_arr[i]);
}
vec.push_back(sig);
}
static void ws_sig_add(stru_ws_session *s, enum_ws_sig_type st, const char *saddr)
{
switch (st)
{
case WS_SIG_OUT: ws_sig_add_out(s, saddr); break;
case WS_SIG_IN: ws_sig_add_in(s, saddr); break;
case WS_SIG_YK: ws_sig_add_yk(s, saddr); break;
case WS_SIG_AO: ws_sig_add_ao(s, saddr); break;
case WS_SIG_PARAM: ws_sig_add_param(s, saddr); break;
default: break;
}
}
static void ws_sig_del(stru_ws_session *s, enum_ws_sig_type st, const char *saddr)
{
std::vector<stru_ws_sig> &vec = s->signals[st];
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr == saddr)
{
vec.erase(vec.begin() + i);
return;
}
}
}
static int ws_sig_set(stru_ws_session *s, enum_ws_sig_type st, const char *saddr,
uint8_t setting_zone, const char *val)
{
std::vector<stru_ws_sig> &vec = s->signals[st];
uint8_t data[128] = {0};
for (size_t i = 0; i < vec.size(); i++)
{
if (vec[i].saddr != saddr)
{
continue;
}
dc_set_val_from_str(data, vec[i].data_type, val);
switch (st)
{
case WS_SIG_OUT:
return dc_set_out_signal_val(saddr, data, MODULE_WEB_SERVER);
case WS_SIG_IN:
/* in 信号只读 */
LOG_E("set in signal not supported: %s", saddr);
return -1;
case WS_SIG_YK:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_yk_set_status(saddr, DC_STEP_DIRECT, &ctrl, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
case WS_SIG_AO:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_ao_set_val(saddr, DC_STEP_DIRECT, &ctrl, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
case WS_SIG_PARAM:
{
dc_ctrl_t ctrl;
ctrl.type = vec[i].ctrl_type;
ctrl.step = DC_STEP_READY;
ctrl.data_type = vec[i].data_type;
ctrl.p_data = dc_create_data(vec[i].data_type);
int rc = dc_signal_param_set_val(saddr, DC_STEP_DIRECT, &ctrl, setting_zone, data, MODULE_WEB_SERVER);
dc_destroy_data(ctrl.p_data, vec[i].data_type);
return rc;
}
default:
break;
}
}
LOG_E("signal not found in session: %s", saddr);
return -1;
}
/* ================================================================
* JSON 构建 — 按信号类型有不同字段
* ================================================================ */
static int ws_make_sig_json(stru_ws_session *s, cJSON *root, enum_ws_sig_type st, bool *has_change)
{
const std::vector<stru_ws_sig> &vec = s->signals[st];
if (vec.empty())
{
return 0;
}
cJSON *arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, g_ws_sig_names[st], arr);
for (size_t i = 0; i < vec.size(); i++)
{
const stru_ws_sig *sig = &vec[i];
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "saddr", sig->saddr.c_str());
cJSON_AddStringToObject(item, "desc", sig->desc.c_str());
cJSON_AddStringToObject(item, "type", dc_get_type_name(sig->data_type));
/* yk/ao/param: 控制类型 */
if (st == WS_SIG_YK || st == WS_SIG_AO || st == WS_SIG_PARAM)
{
cJSON_AddNumberToObject(item, "ctrl_type", sig->ctrl_type);
}
/* param: 多定值区 */
if (st == WS_SIG_PARAM)
{
cJSON *zone_arr = cJSON_CreateArray();
cJSON_AddItemToObject(item, "setting_zone_list", zone_arr);
for (size_t j = 0; j < sig->vec_p_data.size(); j++)
{
cJSON *zone_item = cJSON_CreateObject();
std::string id = std::to_string(j);
std::string val = ws_get_val_str(sig->vec_p_data[j], sig->data_type);
cJSON_AddStringToObject(zone_item, "id", id.c_str());
cJSON_AddStringToObject(zone_item, "val", val.c_str());
cJSON_AddItemToArray(zone_arr, zone_item);
}
/* 组合值用于变化检测 */
std::string combined;
for (size_t j = 0; j < sig->vec_p_data.size(); j++)
{
combined += ws_get_val_str(sig->vec_p_data[j], sig->data_type);
combined += ",";
}
if (combined != sig->last_val)
{
/* 注意: last_val 是 const 方法访问,这里需要 const_cast */
const_cast<stru_ws_sig *>(sig)->last_val = combined;
*has_change = true;
}
}
else
{
/* out/in/yk/ao: 单值 */
std::string val = ws_get_val_str(sig->vec_p_data[0], sig->data_type);
cJSON_AddStringToObject(item, "val", val.c_str());
if (val != sig->last_val)
{
const_cast<stru_ws_sig *>(sig)->last_val = val;
*has_change = true;
}
}
cJSON_AddItemToArray(arr, item);
}
return 0;
}
/* ================================================================
* 增量推送引擎(在 EV_TIMER2 中调用)
* ================================================================ */
void ws_push_task(void)
{
pthread_mutex_lock(&g_ws_session_mutex);
for (auto &pair : g_ws_sessions)
{
unsigned long conn_id = pair.first;
stru_ws_session &sess = pair.second;
bool has_any = false;
for (int st = 0; st < WS_SIG_MAX; st++)
{
if (!sess.signals[st].empty())
{
has_any = true;
break;
}
}
if (!has_any)
{
continue;
}
cJSON *root = cJSON_CreateObject();
bool has_change = false;
for (int st = 0; st < WS_SIG_MAX; st++)
{
ws_make_sig_json(&sess, root, (enum_ws_sig_type)st, &has_change);
}
if (!has_change)
{
cJSON_Delete(root);
continue;
}
char *p_tx = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (p_tx)
{
ws_send_one(conn_id, p_tx, strlen(p_tx));
free(p_tx);
}
}
pthread_mutex_unlock(&g_ws_session_mutex);
}
/* ================================================================
* WebSocket JSON 命令分发
* ================================================================ */
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len)
{
if (!p_rx || rx_len == 0)
{
return;
}
cJSON *root = cJSON_Parse(p_rx);
if (!root)
{
LOG_E("ws_recv parse failed: %.200s", p_rx);
return;
}
/* ---- type=cmd: 远程命令执行 ---- */
cJSON *jt = cJSON_GetObjectItem(root, "type");
if (jt && cJSON_IsString(jt) && jt->valuestring)
{
if (0 == strcmp(jt->valuestring, "cmd"))
{
cJSON *jd = cJSON_GetObjectItem(root, "data");
if (jd && cJSON_IsString(jd) && jd->valuestring)
{
/* 提取模块名存异步通道 */
char mod[64] = {0};
const char *pp = jd->valuestring;
int mi = 0;
while (*pp && *pp != ' ' && *pp != '\t' && mi < 63)
{
mod[mi++] = *pp++;
}
ws_set_async_cmd_conn(mod, c->id);
std::string out = ws_cmd_exec(jd->valuestring);
ws_send_binary(c->id, out.data(), out.size());
}
cJSON_Delete(root);
return;
}
/* ---- type=get_cmds: 获取命令列表 ---- */
if (0 == strcmp(jt->valuestring, "get_cmds"))
{
unsigned int count = 0;
stru_cmd *cmds = cmd_manager_get_commands(&count);
cJSON *resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "type", "cmd_list");
cJSON *arr = cJSON_CreateArray();
for (unsigned int i = 0; i < count; i++)
{
cJSON *item = cJSON_CreateObject();
cJSON_AddStringToObject(item, "name", cmds[i].name);
cJSON_AddStringToObject(item, "desc", cmds[i].desc);
cJSON_AddItemToArray(arr, item);
}
cJSON_AddItemToObject(resp, "cmds", arr);
char *p_tx = cJSON_PrintUnformatted(resp);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(resp);
cJSON_Delete(root);
return;
}
}
/* ---- 信号 CRUD 命令 ---- */
cJSON *jsaddr = cJSON_GetObjectItem(root, "saddr");
cJSON *jsigtype = cJSON_GetObjectItem(root, "signal_type");
cJSON *jcurd = cJSON_GetObjectItem(root, "curd");
cJSON *jsigdata = cJSON_GetObjectItem(root, "signal_data");
cJSON *jzone = cJSON_GetObjectItem(root, "setting_zone");
if (!jsaddr || !cJSON_IsString(jsaddr) || !jsaddr->valuestring)
{
cJSON_Delete(root);
return;
}
if (!jsigtype || !cJSON_IsString(jsigtype) || !jsigtype->valuestring)
{
cJSON_Delete(root);
return;
}
if (!jcurd || !cJSON_IsString(jcurd) || !jcurd->valuestring)
{
cJSON_Delete(root);
return;
}
/* 查找信号类型 */
enum_ws_sig_type st = WS_SIG_OUT;
for (int i = 0; i < WS_SIG_MAX; i++)
{
if (0 == strcmp(jsigtype->valuestring, g_ws_sig_names[i]))
{
st = (enum_ws_sig_type)i;
break;
}
}
const char *saddr = jsaddr->valuestring;
const char *curd = jcurd->valuestring;
const char *sig_data = jsigdata && cJSON_IsString(jsigdata) ? jsigdata->valuestring : "";
uint8_t zone = jzone && cJSON_IsNumber(jzone) ? (uint8_t)jzone->valueint : 0;
pthread_mutex_lock(&g_ws_session_mutex);
stru_ws_session &sess = g_ws_sessions[c->id];
if (0 == strcmp(curd, "add"))
{
ws_sig_add(&sess, st, saddr);
/* 立即推送当前状态 */
cJSON *push = cJSON_CreateObject();
bool dummy = false;
ws_make_sig_json(&sess, push, st, &dummy);
char *p_tx = cJSON_PrintUnformatted(push);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(push);
}
else if (0 == strcmp(curd, "del"))
{
ws_sig_del(&sess, st, saddr);
cJSON *push = cJSON_CreateObject();
bool dummy = false;
ws_make_sig_json(&sess, push, st, &dummy);
char *p_tx = cJSON_PrintUnformatted(push);
if (p_tx)
{
ws_send_one(c->id, p_tx, strlen(p_tx));
free(p_tx);
}
cJSON_Delete(push);
}
else if (0 == strcmp(curd, "set"))
{
ws_sig_set(&sess, st, saddr, zone, sig_data);
}
pthread_mutex_unlock(&g_ws_session_mutex);
cJSON_Delete(root);
}
/* ---- 会话销毁 ---- */
void ws_session_destroy(struct mg_connection *c)
{
if (!c)
{
return;
}
pthread_mutex_lock(&g_ws_session_mutex);
g_ws_sessions.erase(c->id);
pthread_mutex_unlock(&g_ws_session_mutex);
}