Compare commits
No commits in common. "ecbdf483b6c4a5906cb160f32c2f5b1fbede2e88" and "854319a500ff07662510ba6251b49c3023f52bcc" have entirely different histories.
ecbdf483b6
...
854319a500
|
|
@ -40,9 +40,8 @@
|
|||
| IEC 点表解析 | `libiec` | ⬜ |
|
||||
| Modbus 主站管理 | `libmodbus_m`(原工程自研模块,导入+适配) | ✅ |
|
||||
| 通讯通道解码 | `libcom_decode` | ⬜ |
|
||||
| Web 管理服务 | `libweb_server`(3 文件拆分,单线程,EV_STOP 支持) | ✅ |
|
||||
| Mongoose 网络库 | `libmongoose`(开源库 v7.22,导入+适配) | ✅ |
|
||||
| 主程序入口 | `RTU`(X-Macro 框架 + app_config.json 启停控制 + 独立 config/ 目录) | ✅ |
|
||||
| Web 管理服务 | `libweb_server` | ⬜ |
|
||||
| 主程序入口 | `RTU` | ⬜ |
|
||||
|
||||
## 阶段 3:系统集成
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"apps": [
|
||||
{"name": "app_modbus_m", "enable": true}
|
||||
]
|
||||
}
|
||||
|
|
@ -24,21 +24,7 @@
|
|||
| 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/)
|
||||
|
||||
| 模块 | 文档 | 语言 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 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 实时推送) |
|
||||
### 通讯与协议库 (protocol/ — 阶段 2 计划)
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
|
|
|
|||
|
|
@ -1,369 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,531 +0,0 @@
|
|||
# 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`) |
|
||||
|
|
@ -1,630 +0,0 @@
|
|||
# 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行) |
|
||||
|
|
@ -101,13 +101,11 @@ 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 "libweb_server" "web_server.h"
|
||||
copy_sys_module "libmodbus_m" "myModbusM.h"
|
||||
|
||||
echo "=== done ==="
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
SUBDIRS := ./libmodbus ./lib60870 ./libmongoose
|
||||
SUBDIRS := ./libmodbus ./lib60870
|
||||
define make_subdir
|
||||
for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && make -f makefile $1) || true; done
|
||||
endef
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,14 +1,8 @@
|
|||
include ./../../linux.mk
|
||||
|
||||
# app_sys.cpp 已合并到 RTU makefile 中,此处仅管理子模块
|
||||
SUBDIRS := ./libmodbus_m ./libweb_server ./RTU
|
||||
|
||||
SUBDIRS := ./libmodbus_m
|
||||
define make_subdir
|
||||
for d in $(SUBDIRS); do [ -f "$$d/makefile" ] && (cd "$$d" && make -f makefile $1) || true; done
|
||||
endef
|
||||
.PHONY: all clean rebuild
|
||||
|
||||
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
|
||||
|
||||
all:; @$(call make_subdir,all)
|
||||
clean:; @$(call make_subdir,clean)
|
||||
rebuild: clean all
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* @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
|
|
@ -1,393 +0,0 @@
|
|||
/**
|
||||
* @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];
|
||||
}
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* @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)
|
||||
|
|
@ -1,11 +1,7 @@
|
|||
/**
|
||||
* @file mySystem.h
|
||||
* @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,无需改框架代码。
|
||||
* @brief 系统管理模块公共定义
|
||||
* @details 供 libmodbus_m/libiec/libcom_decode/RTU 等系统模块共用。
|
||||
*/
|
||||
#ifndef _MY_SYSTEM_H_
|
||||
#define _MY_SYSTEM_H_
|
||||
|
|
@ -18,69 +14,15 @@ extern "C"
|
|||
{
|
||||
#endif
|
||||
|
||||
/** 每个模块的定时器数量(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
|
||||
|
||||
/* ---- 应用数据收发结构体 ---- */
|
||||
/** @brief 应用任务上下文(RTU 多任务运行时使用) */
|
||||
typedef struct
|
||||
{
|
||||
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;
|
||||
const char *name; /**< 任务名称 */
|
||||
stru_task_event_t p_event; /**< 事件句柄 */
|
||||
stru_task_msg_queue_t p_queue; /**< 消息队列句柄 */
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* @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_ */
|
||||
|
|
@ -20,23 +20,13 @@ LOCAL std::vector<stru_modbus_m_write_req> g_write_queue;
|
|||
|
||||
LOCAL std::string get_base_path()
|
||||
{
|
||||
char buf[512] = {0};
|
||||
|
||||
/* 优先使用 WORK_PATH 环境变量(RK3568 装置运行时设置) */
|
||||
if (0 == func_get_work_path(buf, sizeof(buf)))
|
||||
char proc_dir[512] = {0};
|
||||
if(0 != func_proc_self_dir(proc_dir, sizeof(proc_dir)))
|
||||
{
|
||||
return std::string(buf) + "/";
|
||||
}
|
||||
|
||||
/* fallback: 从可执行文件路径推导(本地调试用) */
|
||||
if (0 != func_proc_self_dir(buf, sizeof(buf)))
|
||||
{
|
||||
LOG_E("func_proc_self_dir failed");
|
||||
|
||||
MY_LOG_E("func_get_process_self_dir failed");
|
||||
return "";
|
||||
}
|
||||
|
||||
return std::string(buf) + "/";
|
||||
return std::string(proc_dir);
|
||||
}
|
||||
|
||||
// ==================== 数据类型映射 ====================
|
||||
|
|
@ -93,14 +83,14 @@ LOCAL int parse_channels(XMLElement *p_root)
|
|||
ch.connected = false;
|
||||
|
||||
g_modbus_m_cfg.channels.push_back(ch);
|
||||
LOG_I("channel %d: mode=%s, host=%s:%d", ch.id, ch.mode.c_str(), ch.host.c_str(), ch.port);
|
||||
MY_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())
|
||||
{
|
||||
LOG_E("no channel configured");
|
||||
MY_LOG_E("no channel configured");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
|
|
@ -149,7 +139,7 @@ LOCAL int parse_point_item(XMLElement *p_item, std::vector<stru_modbus_m_item> &
|
|||
}
|
||||
if(!ch_found)
|
||||
{
|
||||
LOG_E("item no=%d: channel %d not found", item.no, item.channel);
|
||||
MY_LOG_E("item no=%d: channel %d not found", item.no, item.channel);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +148,7 @@ LOCAL int parse_point_item(XMLElement *p_item, std::vector<stru_modbus_m_item> &
|
|||
item.p_val = dc_create_data(local_type);
|
||||
if(NULL == item.p_val)
|
||||
{
|
||||
LOG_E("dc_create_data_ptr_by_type failed, type=%d", local_type);
|
||||
MY_LOG_E("dc_create_data_ptr_by_type failed, type=%d", local_type);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +161,7 @@ LOCAL int parse_points(XMLElement *p_root)
|
|||
XMLElement *p_point = p_root->FirstChildElement("Point");
|
||||
if(NULL == p_point)
|
||||
{
|
||||
LOG_E("Point not found");
|
||||
MY_LOG_E("Point not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +181,7 @@ LOCAL int parse_points(XMLElement *p_root)
|
|||
}
|
||||
p_item = p_item->NextSiblingElement("Item");
|
||||
}
|
||||
LOG_I("parsed %zu st items", g_modbus_m_cfg.point.st_vec.size());
|
||||
MY_LOG_I("parsed %zu st items", g_modbus_m_cfg.point.st_vec.size());
|
||||
}
|
||||
|
||||
// 解析 Mx(遥测)
|
||||
|
|
@ -207,7 +197,7 @@ LOCAL int parse_points(XMLElement *p_root)
|
|||
}
|
||||
p_item = p_item->NextSiblingElement("Item");
|
||||
}
|
||||
LOG_I("parsed %zu mx items", g_modbus_m_cfg.point.mx_vec.size());
|
||||
MY_LOG_I("parsed %zu mx items", g_modbus_m_cfg.point.mx_vec.size());
|
||||
}
|
||||
|
||||
// 解析 Co(遥控)
|
||||
|
|
@ -223,7 +213,7 @@ LOCAL int parse_points(XMLElement *p_root)
|
|||
}
|
||||
p_item = p_item->NextSiblingElement("Item");
|
||||
}
|
||||
LOG_I("parsed %zu co items", g_modbus_m_cfg.point.co_vec.size());
|
||||
MY_LOG_I("parsed %zu co items", g_modbus_m_cfg.point.co_vec.size());
|
||||
}
|
||||
|
||||
// 解析 Ao(参数)
|
||||
|
|
@ -239,7 +229,7 @@ LOCAL int parse_points(XMLElement *p_root)
|
|||
}
|
||||
p_item = p_item->NextSiblingElement("Item");
|
||||
}
|
||||
LOG_I("parsed %zu ao items", g_modbus_m_cfg.point.ao_vec.size());
|
||||
MY_LOG_I("parsed %zu ao items", g_modbus_m_cfg.point.ao_vec.size());
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
@ -250,30 +240,30 @@ int modbus_m_cfg_parse(const std::string &path)
|
|||
XMLDocument doc;
|
||||
if(XML_SUCCESS != doc.LoadFile(path.c_str()))
|
||||
{
|
||||
LOG_E("load xml failed: %s", path.c_str());
|
||||
MY_LOG_E("load xml failed: %s", path.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
XMLElement *p_root = doc.RootElement();
|
||||
if(NULL == p_root)
|
||||
{
|
||||
LOG_E("root element not found");
|
||||
MY_LOG_E("root element not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(0 != parse_channels(p_root))
|
||||
{
|
||||
LOG_E("parse channels failed");
|
||||
MY_LOG_E("parse channels failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(0 != parse_points(p_root))
|
||||
{
|
||||
LOG_E("parse points failed");
|
||||
MY_LOG_E("parse points failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_I("modbus_m cfg parsed: %zu channels, st=%zu, mx=%zu, co=%zu, ao=%zu",
|
||||
MY_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(),
|
||||
|
|
@ -307,7 +297,7 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
|
|||
p_ch->ctx = modbus_new_tcp(p_ch->host.c_str(), p_ch->port);
|
||||
if(NULL == p_ch->ctx)
|
||||
{
|
||||
LOG_E("modbus_new_tcp failed: %s:%d", p_ch->host.c_str(), p_ch->port);
|
||||
MY_LOG_E("modbus_new_tcp failed: %s:%d", p_ch->host.c_str(), p_ch->port);
|
||||
return -1;
|
||||
}
|
||||
// TCP 模式下设置默认从站地址
|
||||
|
|
@ -318,13 +308,13 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
|
|||
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)
|
||||
{
|
||||
LOG_E("modbus_new_rtu failed: %s", p_ch->device.c_str());
|
||||
MY_LOG_E("modbus_new_rtu failed: %s", p_ch->device.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_E("unknown mode: %s", p_ch->mode.c_str());
|
||||
MY_LOG_E("unknown mode: %s", p_ch->mode.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +327,7 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
|
|||
// 建立连接
|
||||
if(0 != modbus_connect(p_ch->ctx))
|
||||
{
|
||||
LOG_E("modbus_connect failed: ch=%d, mode=%s", p_ch->id, p_ch->mode.c_str());
|
||||
MY_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;
|
||||
|
|
@ -345,7 +335,7 @@ LOCAL int channel_connect(stru_modbus_m_channel *p_ch)
|
|||
}
|
||||
|
||||
p_ch->connected = true;
|
||||
LOG_I("channel %d connected: mode=%s", p_ch->id, p_ch->mode.c_str());
|
||||
MY_LOG_I("channel %d connected: mode=%s", p_ch->id, p_ch->mode.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -746,7 +736,7 @@ LOCAL void modbus_m_signal_co_change_callback(const char *saddr, dc_ctrl_step_t
|
|||
}
|
||||
|
||||
enqueue_write(req);
|
||||
LOG_I("co write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
|
||||
MY_LOG_I("co write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -785,7 +775,7 @@ LOCAL void modbus_m_signal_ao_change_callback(const char *saddr, dc_ctrl_step_t
|
|||
}
|
||||
|
||||
enqueue_write(req);
|
||||
LOG_I("ao write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
|
||||
MY_LOG_I("ao write: ch=%d, slave=%d, addr=%d, val=%d", req.channel, req.slave, req.addr, req.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -804,7 +794,7 @@ LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, co
|
|||
{
|
||||
if(0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
|
||||
{
|
||||
LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
|
||||
MY_LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -812,7 +802,7 @@ LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, co
|
|||
{
|
||||
if(0 != dc_signal_out(item.saddr.c_str(), item.desc.c_str(), local_type, item.p_val, MODULE_MODBUS_M))
|
||||
{
|
||||
LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
|
||||
MY_LOG_E("dc_signal_out failed: %s", item.saddr.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -820,7 +810,7 @@ LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, 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))
|
||||
{
|
||||
LOG_E("dc_signal_yk failed: %s", item.saddr.c_str());
|
||||
MY_LOG_E("dc_signal_yk failed: %s", item.saddr.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -828,7 +818,7 @@ LOCAL int signal_register_items(const std::vector<stru_modbus_m_item> &items, co
|
|||
{
|
||||
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))
|
||||
{
|
||||
LOG_E("dc_signal_ao failed: %s", item.saddr.c_str());
|
||||
MY_LOG_E("dc_signal_ao failed: %s", item.saddr.c_str());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -856,7 +846,7 @@ LOCAL int signals_init()
|
|||
return -1;
|
||||
}
|
||||
|
||||
LOG_I("signals registered: st=%zu, mx=%zu, co=%zu, ao=%zu",
|
||||
MY_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(),
|
||||
|
|
@ -872,13 +862,13 @@ int modbus_m_init()
|
|||
{
|
||||
if(0 != signals_init())
|
||||
{
|
||||
LOG_E("signals_init failed");
|
||||
MY_LOG_E("signals_init failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(0 != channels_init())
|
||||
{
|
||||
LOG_E("channels_init failed");
|
||||
MY_LOG_E("channels_init failed");
|
||||
// 部分通道失败也可继续
|
||||
}
|
||||
|
||||
|
|
@ -952,7 +942,7 @@ int app_modbus_m_init1(void *arg)
|
|||
std::string base_path = get_base_path();
|
||||
if(base_path.empty())
|
||||
{
|
||||
LOG_E("get_base_path failed");
|
||||
MY_LOG_E("get_base_path failed");
|
||||
return -1;
|
||||
}
|
||||
g_modbus_m_base_path = base_path + "config/MODBUS/";
|
||||
|
|
@ -960,7 +950,7 @@ int app_modbus_m_init1(void *arg)
|
|||
std::string cfg_path = g_modbus_m_base_path + "modbus_m.xml";
|
||||
if(0 != modbus_m_cfg_parse(cfg_path))
|
||||
{
|
||||
LOG_E("modbus_m_cfg_parse failed: %s", cfg_path.c_str());
|
||||
MY_LOG_E("modbus_m_cfg_parse failed: %s", cfg_path.c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -995,7 +985,7 @@ 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);
|
||||
LOG_I("poll groups: st=%zu, mx=%zu", st_groups.size(), mx_groups.size());
|
||||
MY_LOG_I("poll groups: st=%zu, mx=%zu", st_groups.size(), mx_groups.size());
|
||||
|
||||
while(1)
|
||||
{
|
||||
|
|
@ -1027,5 +1017,3 @@ void *app_modbus_m(void *arg)
|
|||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ==================== 模块注册(由 app_modules.h X-Macro 自动完成,无需手动 register) ==================== */
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* @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_ */
|
||||
|
|
@ -1,435 +0,0 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
|
|
@ -1,671 +0,0 @@
|
|||
/**
|
||||
* @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);
|
||||
}
|
||||
Loading…
Reference in New Issue