<修改> 1、在前端页面中增加命令终端页面,在此页面中可以调用自定义命令,并将自定义命令的响应信息展示到前端页面中

This commit is contained in:
ypc 2026-06-18 15:42:09 +08:00
parent 742d9b36a0
commit 3c19148b97
10 changed files with 2645 additions and 2323 deletions

129
AGENTS.md Normal file
View File

@ -0,0 +1,129 @@
# Mimo RTU Project — AI Agent Instructions
## 📋 项目概览
**RTU (Remote Terminal Unit)** — 基于 IEC 61850 的智能通信网关,用于电力系统自动化。核心协议为 IEC 61850 MMS同时支持 IEC 101/104 (ICP67)、Modbus、MQTT、WebSocket。
- **技术栈**: C/C++ (GCC/Linux ARM 交叉编译)
- **编译**: `./release/build.sh` (x86) 或 `./release/build.sh arm` (ARM 交叉编译)
- **运行**: `./test/RTU`
## 🏗 模块架构
```
src/
├── system/RTU/ # 主程序入口、CLI、自点表配置
├── system/libdatacenter/ # 数据中心(信号注册/变更检测/SBO控制/事件队列)
├── system/libcom_channel/ # 通信通道管理TCP服务端/客户端、串口)
├── system/libweb_server/ # HTTP/WebSocket 服务器
├── system/libiec61850s/ # IEC 61850 MMS 服务端
├── system/libiec61850m/ # IEC 61850 MMS 客户端
├── system/libself_ptl/ # 自点表/自协议模块
├── system/libiec/ # IEC 基础协议库
├── system/libcom_decode/ # 通信解码库
├── system/libmodbus_m/ # Modbus 主站
├── system/libmodbus_s/ # Modbus 从站
├── protocol/libmms_s/ # MMS 服务端协议层
├── protocol/libmms_m/ # MMS 客户端协议层
├── protocol/libicp67/ # ICP67 协议IEC 101/104 帧编解码)
├── protocol/libmongoose/ # Mongoose 嵌入式 Web 服务器
├── protocol/lib60870/ # IEC 60870 协议
├── protocol/libmodbus/ # Modbus 协议
├── public/libcomm/ # 统一通信抽象层TCP/UART/UDP
├── public/libtask/ # 任务/定时器调度库
├── public/libcmd/ # CLI 命令解析
├── public/libmy_mosquitto/ # MQTT 客户端
├── public/libcJSON/ # JSON 解析
├── public/libfunc/ # 工具函数库
└── public/libxml/ # XML 解析 (tinyxml2)
```
## 🔨 构建命令
| 命令 | 说明 |
|------|------|
| `./release/build.sh` | 本地 x86 编译 |
| `./release/build.sh arm` | ARM 交叉编译 |
| `make -C release` | 在 release 目录执行 make |
| `./test/RTU` | 运行编译产物 |
## 📐 代码规范
### 风格
- **缩进**: Tab 字符(显示宽度 4
- **括号**: Allman 风格 — 大括号独占一行
- **关键字括号间无空格**: `if(`, `for(`, `while(`, `switch(`
- **函数名后无空格**: `func(args)`
- **指针声明**: `type *name``*` 前有空格)
- **Yoda 条件**: `if(NULL == ptr)` 而非 `if(ptr == NULL)`
- **函数间空行**: 两行
### 命名
- 全局变量: `g_` 前缀
- 结构体: `stru_` 前缀
- 枚举: `enum_` 前缀 / `ENUM_` 值前缀
- 局部静态: `LOCAL` 宏(等价于 `static`
### 错误处理
- 整数返回码: `0` 成功,`-1` 或非 0 表示失败
- 错误时记录 `LOG_E` 并返回错误码
### 日志
- `LOG_I()` / `LOG_E()` 宏(定义见 `release/inc/myLog.h`
- `MY_LOG_*` 宏(定义见 `release/inc/myBase.h`
## 📚 参考文档
| 文档 | 位置 |
|------|------|
| 项目记忆(架构/规则/决策) | [mimo/MEMORY.md](mimo/MEMORY.md) |
| 全局记忆(用户偏好) | [mimo/global-MEMORY.md](mimo/global-MEMORY.md) |
| 问题处理记录 | [mimo/问题处理文档.md](mimo/问题处理文档.md) |
| 模块分析文档 | [mimo/工程/](mimo/工程/) |
| 设计方案文档 | [mimo/plan/](mimo/plan/) |
| 中间修复文档 | [mimo/中间文档/](mimo/中间文档/) |
| IEC 61850 服务端手册 | [libiec61850/libiec61850手册/libiec61850_服务端开发手册.md](libiec61850/libiec61850手册/libiec61850_服务端开发手册.md) |
| IEC 61850 客户端手册 | [libiec61850/libiec61850手册/libiec61850_客户端开发手册.md](libiec61850/libiec61850手册/libiec61850_客户端开发手册.md) |
| 构建配置 | [release/linux.mk](release/linux.mk) |
| 公共头文件目录 | [release/inc/](release/inc/) |
| 模块分析文档生成技能 | [mimo/skills/模块分析文档生成/SKILL.md](mimo/skills/模块分析文档生成/SKILL.md) |
## ⚠️ 注意事项
1. **交叉编译依赖**: `release/linux.mk` 引用了本地交叉工具链路径(`GCCLIB_PATH`),不同主机可能需要调整
2. **Web 资源打包**: 构建流程中的 `pack_web.sh` 会打包 web 资源到可执行文件中,不可跳过
3. **代码风格严格**: 修改代码时必须遵循上述规范Tab/Allman/Yoda否则可能导致审查问题
4. **多线程安全**: 大量使用多线程9 个应用线程),修改时需注意竞态条件
5. **C 风格内存管理**: 使用 `malloc`/`free`,需注意空指针检查和释放时机
6. **第三方依赖**: mongoose、cJSON、mosquitto、tinyxml2、libiec61850 是关键依赖
## 🔄 工作流程规则
以下规则是每次交互必须遵守的工作流程,优先级高于其他指令。
### 1. 语言
全部对话、思考过程(`thinking`)、输出内容均使用中文显示。代码本身(变量名、函数名、关键字)保持英文。
### 2. 问题处理 → 记录文档
每次解决 Bug 或问题后,将**问题现象、原因分析、解决方案**追加到 `mimo/问题处理文档.md`,保留原有内容不覆盖。
### 3. Plan 计划 → 形成文档
制定并执行 Plan 解决问题后,将 Plan 内容整理为文档存入 `mimo/plan/``mimo/中间文档/` 目录。文件命名需体现内容主题。
### 4. 模块分析 → 形成文档
每次深入读取/分析某个模块后,将分析结果整理为文档存入 `mimo/工程/` 目录。文档应包含模块职责、核心接口、关键逻辑、数据流等。
### 5. C/C++ 代码调整 → 编译确认
修改 C/C++ 代码后,必须执行 `./release/build.sh`x86进行编译验证确认无编译错误。如果编译失败应立即修复。
### 6. 前端代码调整 → 浏览器验证
修改 Web 前端代码(`web_root/` 目录下的 HTML/CSS/JS应使用浏览器打开查看效果确认界面显示正确。
### 7. 文档目录继承
严格遵守现有的文档目录结构:
- `mimo/问题处理文档.md` — 问题处理记录
- `mimo/工程/` — 模块分析文档
- `mimo/plan/` — 设计方案文档
- `mimo/中间文档/` — 中间修复文档
- `mimo/skills/` — 技能文档
后续新增文档按此规则归类存放,不随意创建新的根级目录。

View File

@ -0,0 +1,62 @@
# WebSocket 异步命令输出修复方案
## 问题描述
Web 前端的命令终端页面中,`datacenter in` 等同步命令能正确显示输出,但 `self_ptl 6` 等异步命令的输出不会传输到前端。
## 原因分析
`ws_cmd_exec()` 通过重定向 STDOUT → pipe → reader 线程的方式捕获命令输出:
```
ws_cmd_exec() → dup2(pipe, stdout) → p_cmd->func() → dup2(saved_stdout, stdout) → join reader
```
- **datacenter in**: `cmd_dc()` 直接调用 `dc_show_in_signals()`,使用 `std::cout`/`printf` 同步输出到 stdout → **被 pipe 捕获**
- **self_ptl 6**: `cmd_self_ptl()` 仅设置 `g_flag[6]=1`,立即返回。实际数据经 `test_self_check_get()``icp67_self_check_get_cb()` → ICP67 协议发送 → 远端响应 → `self_check_pop_out()` 异步回调处理。该回调仅输出 `LOG_I` 日志,不走 stdout → **不被 pipe 捕获**
## 解决方案
### 核心思路
在 WebSocket 服务端维护一个 `<命令名 → WebSocket 连接 ID>` 的 Map 映射,让异步回调可以根据命令名查找到对应的连接 ID将结果推送回前端。
### 架构设计要点
1. **跨模块解耦**:在 `mySystem.h` 中声明共享 API`ws_set_async_cmd_conn`、`ws_get_async_cmd_conn`、`ws_send_binary`),各模块只需包含 `mySystem.h` 即可使用,无需跨模块引用头文件
2. **读写接口封装**:用 `ws_set_async_cmd_conn()` / `ws_get_async_cmd_conn()` 取代直接暴露全局变量,内部用 mutex 保护
3. **多连接支持**:使用 `std::map<std::string, unsigned long>` 以命令名为 key 存储,不同命令互不干扰。同一命令的后续请求会覆盖前一个
### 修改文件
| 文件 | 修改内容 |
|------|----------|
| `src/system/libweb_server/src/ws_method.cpp` | 用 `std::map` + mutex 替代单一全局变量,添加 `ws_set/ws_get_async_cmd_conn()` 接口 |
| `src/system/libweb_server/inc/ws_method.h` | 移除 `extern unsigned long g_async_cmd_conn_id` |
| `release/inc/mySystem.h` | 添加 `ws_set_async_cmd_conn()`、`ws_get_async_cmd_conn()`、`ws_send_binary()` 跨模块声明 |
| `src/system/libself_ptl/src/method.cpp` | 移除 `#include "ws_method.h"`,改为 `#include "mySystem.h"`,使用 `ws_get_async_cmd_conn("self_ptl")` |
| `release/src/system/libself_ptl/makefile` | 移除跨模块包含路径 `-I $(SRC_ROOT_DIR)/system/libweb_server/inc` |
### 数据流
```
前端 WebSocket 键入 self_ptl 6 (conn_id=100)
→ ws_recv() 提取命令名 "self_ptl"
→ ws_set_async_cmd_conn("self_ptl", 100)
→ map["self_ptl"] = 100
→ ws_cmd_exec() 返回初始输出 → ws_send_binary(初始输出)
同一时刻另一用户键入 datacenter in (conn_id=200)
→ ws_set_async_cmd_conn("datacenter", 200)
→ map["datacenter"] = 200 (不会覆盖 self_ptl 的条目 ✅)
(异步) ICP67 响应到达 → self_check_pop_out()
→ conn_id = ws_get_async_cmd_conn("self_ptl") → 100
→ ws_send_binary(100, 格式化结果) → 第一个用户前端显示 ✅
```
### 多连接场景
- 同一命令(如 `self_ptl 6`)被多个标签页先后执行 → 后续的会覆盖之前的 conn_id → 异步结果只推送给最后一个执行者
- 不同命令互不干扰,各自独立存储 conn_id
- 如需进一步优化(如广播到所有标签页),可在 map 中存储 `std::vector<unsigned long>` 实现多连接推送

View File

@ -230,6 +230,14 @@ uint32_t com_channel_interface_get(uint32_t comm_id);
// ======== 异步命令输出分发器(命令模块通用接口,不感知 WebSocket ========
// 格式化输出到前端命令终端
void async_cmd_printf(const char *module, const char *fmt, ...);
// 命令完成通知(成功/失败 + 消息)
void async_cmd_complete(const char *module, bool success, const char *msg);

View File

@ -773,6 +773,25 @@ LOCAL int dc_check_val_valid(const stru_signal *p_signal, uint8_t setting_zone,
return 0; return 0;
} }
LOCAL int dc_check_yk_val_valid(stru_signal *p_signal, void *p_data)
{
if(p_signal->data_type == DATA_TYPE_B || p_signal->data_type == DATA_TYPE_U8)
{
uint8_t u8 = *(uint8_t*)p_data;
if(u8 != 0 && u8 != 1)
{
LOG_E("invalid uint8_t value %d, range [0, 1]", u8);
return -1;
}
}
else
{
LOG_E("data_type is not valid: %d", p_signal->data_type);
return -1;
}
return 0;
}
LOCAL int dc_signal_add_to_map(stru_signal &signal, stru_signal_map &dc_signal_map) LOCAL int dc_signal_add_to_map(stru_signal &signal, stru_signal_map &dc_signal_map)
@ -1488,7 +1507,7 @@ int dc_signal_yk_set_status(const std::string &saddr, SIGNAL_CTRL_STEP step, str
{ {
if(0 == dc_check_ctrl_valid(p_signal, step, ctrl, p_data)) if(0 == dc_check_ctrl_valid(p_signal, step, ctrl, p_data))
{ {
if(0 != dc_check_val_valid(p_signal, 0, p_data)) if(0 != dc_check_yk_val_valid(p_signal, p_data))
{ {
LOG_E("YK val check failed, saddr=%s", saddr.c_str()); LOG_E("YK val check failed, saddr=%s", saddr.c_str());
return -1; return -1;

View File

@ -500,6 +500,15 @@ LOCAL void cmd_iec(int argc, char *argv[])
return; return;
} }
if(argc < 2)
{
printf("\n");
printf("iec 命令 - IEC 101/104 线程控制\n");
printf(" iec run_cnt get ----------> 查看运行计数\n");
printf(" iec run_cnt reset ----------> 重置运行计数\n");
return;
}
if(argc > 2 && 0 == strcmp(argv[1], "run_cnt")) if(argc > 2 && 0 == strcmp(argv[1], "run_cnt"))
{ {
if(0 == strcmp(argv[2], "get")) if(0 == strcmp(argv[2], "get"))

View File

@ -49,7 +49,7 @@ LOCAL std::vector<stru_iec61850m_info> g_vec_iec61850m_info =
}; };
LOCAL std::map<uint8_t, uint8_t> g_mms_m_type_to_local_type = { LOCAL std::map<uint8_t, uint8_t> g_mms_m_type_to_local_type = {
{MMS_BOOLEAN, DATA_TYPE_U8}, {MMS_BOOLEAN, DATA_TYPE_B},
{MMS_BIT_STRING, DATA_TYPE_U32}, {MMS_BIT_STRING, DATA_TYPE_U32},
{MMS_INTEGER, DATA_TYPE_S32}, {MMS_INTEGER, DATA_TYPE_S32},
{MMS_UNSIGNED, DATA_TYPE_U32}, {MMS_UNSIGNED, DATA_TYPE_U32},
@ -832,9 +832,9 @@ LOCAL void iec61850m_show_info()
LOCAL void cmd_iec61850m(int argc, char *argv[]) LOCAL void cmd_iec61850m(int argc, char *argv[])
{ {
const char *str = "\ const char *str = "\
iec61850m info ----------> iec61850m信息\n\ iec61850m info ----------> fd/prj_name\n\
iec61850m yk <fd> <saddr> <val> <ctrl_type> ------fd对应61850m的fd, \n\ iec61850m yk <fd> <saddr> <val> <ctrl_type> ------ fd对应61850m的fd\n\
iec61850m set <fd> <saddr> <val> <set_type> ------fd对应61850m的fd, \n"; iec61850m set <fd> <saddr> <val> <set_type> ------ fd对应61850m的fd\n";
stru_app *p_app = app_get_ptr(ENUM_APP_IEC61850M); stru_app *p_app = app_get_ptr(ENUM_APP_IEC61850M);
if(NULL == p_app) if(NULL == p_app)
@ -863,10 +863,10 @@ LOCAL void cmd_iec61850m(int argc, char *argv[])
int fd = atoi(argv[2]); int fd = atoi(argv[2]);
std::string saddr = argv[3]; std::string saddr = argv[3];
int val = atoi(argv[4]); std::string val = argv[4];
int ctrl_type = atoi(argv[5]); int ctrl_type = atoi(argv[5]);
// mms_send_control(fd, saddr.c_str(), val, ctrl_type); mms_send_control(fd, saddr.c_str(), val.c_str(), MMS_BOOLEAN, ctrl_type, 0);
} }
} }

View File

@ -18,6 +18,7 @@
#include <string> #include <string>
#include "myDatacenter.h" #include "myDatacenter.h"
#include "mySystem.h"
@ -206,6 +207,10 @@ void self_check_pop_out(stru_icp67 *p_icp67, stru_data_info *p_data, uint16_t ad
std::string data = dc_get_signal_val(p_data->data, p_data->data_type); std::string data = dc_get_signal_val(p_data->data, p_data->data_type);
LOG_I("self_check_pop_out: addr %d, type %d, len %d, data %s", addr, p_data->data_type, p_data->data_len, data.c_str()); LOG_I("self_check_pop_out: addr %d, type %d, len %d, data %s", addr, p_data->data_type, p_data->data_len, data.c_str());
// 将异步结果推送到前端命令终端
async_cmd_printf("self_ptl", "[self_ptl 6] self_check: addr=%d type=%d len=%d data=%s\n",
addr, p_data->data_type, p_data->data_len, data.c_str());
} }
void dir_pop_out(stru_icp67 *p_icp67, uint8_t *p_name, uint8_t len, uint32_t attr, uint32_t size, uint8_t follow) void dir_pop_out(stru_icp67 *p_icp67, uint8_t *p_name, uint8_t len, uint32_t attr, uint32_t size, uint8_t follow)
@ -231,6 +236,14 @@ void dir_pop_out(stru_icp67 *p_icp67, uint8_t *p_name, uint8_t len, uint32_t att
{ {
LOG_I("name %s, len %d, attr %d, size %d", g_dir_info_vec[i].name, g_dir_info_vec[i].len, g_dir_info_vec[i].attr, g_dir_info_vec[i].size); LOG_I("name %s, len %d, attr %d, size %d", g_dir_info_vec[i].name, g_dir_info_vec[i].len, g_dir_info_vec[i].attr, g_dir_info_vec[i].size);
} }
async_cmd_printf("self_ptl", "[self_ptl 11] dir_read: %d files in %s\n",
g_dir_info_vec.size(), g_local_file.dir_name.c_str());
for(uint16_t i = 0; i < g_dir_info_vec.size(); i++)
{
async_cmd_printf("self_ptl", " %s (size=%d)\n",
g_dir_info_vec[i].name, g_dir_info_vec[i].size);
}
} }
} }
@ -245,11 +258,15 @@ void file_read_act_confirm(stru_icp67 *p_icp67, uint8_t *p_name, uint8_t len, ui
g_file_name = (char *)p_name; g_file_name = (char *)p_name;
LOG_I("file_read_act_confirm: name %s, len %d, id %d, size %d, crc %04x", p_name, len, id, size, crc); LOG_I("file_read_act_confirm: name %s, len %d, id %d, size %d, crc %04x", p_name, len, id, size, crc);
async_cmd_printf("self_ptl", "[self_ptl 11] file_read: %s, size=%d\n", p_name, size);
std::string file_name = g_file_path + g_local_file.dir_name + "/" + g_file_name; std::string file_name = g_file_path + g_local_file.dir_name + "/" + g_file_name;
if(0 == unlink(file_name.c_str())) if(0 == unlink(file_name.c_str()))
{ {
LOG_I("file_read_act_confirm: unlink file %s success", file_name.c_str()); LOG_I("file_read_act_confirm: unlink file %s success", file_name.c_str());
async_cmd_printf("self_ptl", "[self_ptl 11] file_read: %s success\n", p_name);
} }
} }
@ -264,6 +281,8 @@ void file_read(stru_icp67 *p_icp67, uint32_t id, uint32_t offset, uint32_t follo
if(offset == 0) if(offset == 0)
{ {
LOG_I("file_read: read file %s start", g_file_name.c_str()); LOG_I("file_read: read file %s start", g_file_name.c_str());
async_cmd_printf("self_ptl", "[self_ptl 11] file_read: %s reading...\n", g_file_name.c_str());
} }
std::string file_name = g_file_path + g_local_file.dir_name + "/" + g_file_name; std::string file_name = g_file_path + g_local_file.dir_name + "/" + g_file_name;
@ -272,6 +291,7 @@ void file_read(stru_icp67 *p_icp67, uint32_t id, uint32_t offset, uint32_t follo
if(0 == follow) if(0 == follow)
{ {
LOG_I("file_read: read file %s success", file_name.c_str()); LOG_I("file_read: read file %s success", file_name.c_str());
async_cmd_printf("self_ptl", "[self_ptl 11] file_read: %s success\n", g_file_name.c_str());
} }
if(p_icp67->method.file_read_confirm_cb) if(p_icp67->method.file_read_confirm_cb)
@ -292,6 +312,7 @@ void file_write_act_confirm(stru_icp67 *p_icp67, uint8_t result, uint8_t *p_name
if(result != FILE_WRITE_ACT_0) if(result != FILE_WRITE_ACT_0)
{ {
LOG_E("file_write_act_confirm: write file %s failed", p_name); LOG_E("file_write_act_confirm: write file %s failed", p_name);
async_cmd_printf("self_ptl", "[self_ptl 11] file_write: %s failed (result=%d)\n", p_name, result);
return; return;
} }
@ -381,6 +402,11 @@ void file_write_end(stru_icp67 *p_icp67, uint8_t result, uint32_t id, uint32_t o
if(0 == result) if(0 == result)
{ {
LOG_I("file_write_end: write file %s success", g_file_name.c_str()); LOG_I("file_write_end: write file %s success", g_file_name.c_str());
async_cmd_printf("self_ptl", "[self_ptl 11] file_write: %s completed successfully\n", g_file_name.c_str());
}
else
{
async_cmd_printf("self_ptl", "[self_ptl 11] file_write: %s failed (md5 mismatch)\n", g_file_name.c_str());
} }
stru_icp67 *p_icp67_task = self_ptl_icp67_ptr_get(); stru_icp67 *p_icp67_task = self_ptl_icp67_ptr_get();
@ -941,8 +967,14 @@ LOCAL void test_ao_get(stru_icp67 *p_icp67)
stru_self_ptl_cfg_base *p_base = &p_data->p_param->base; stru_self_ptl_cfg_base *p_base = &p_data->p_param->base;
printf("saddr %s, desc %s, inf %04x, type %04d, len %04d, val %s\n", p_base->saddr.c_str(), p_base->desc.c_str(), p_base->inf, p_data->p_param->type, dc_get_data_type_len(p_data->p_param->type), val.c_str()); printf("saddr %s, desc %s, inf %04x, type %04d, len %04d, val %s\n", p_base->saddr.c_str(), p_base->desc.c_str(), p_base->inf, p_data->p_param->type, dc_get_data_type_len(p_data->p_param->type), val.c_str());
async_cmd_printf("self_ptl", "[self_ptl 1] param_get: no=%06d saddr=%s desc=%s inf=%04x type=%04d len=%04d val=%s\n",
i, p_base->saddr.c_str(), p_base->desc.c_str(), p_base->inf, p_data->p_param->type, dc_get_data_type_len(p_data->p_param->type), val.c_str());
} }
async_cmd_printf("self_ptl", "[self_ptl 1] param_get completed, total %d items\n",
p_app_cfg_data->ao_vec.size());
g_flag[1] = 0; g_flag[1] = 0;
return; return;
} }
@ -1000,6 +1032,9 @@ LOCAL void test_time_set(stru_icp67 *p_icp67)
p_icp67->method.time_set_cb(p_icp67, &tm); p_icp67->method.time_set_cb(p_icp67, &tm);
} }
async_cmd_printf("self_ptl", "[self_ptl 5] time_set: %04d-%02d-%02d %02d:%02d:%03d\n",
tm.year + 2000, tm.month, tm.day, tm.hour, tm.min, tm.ms);
g_flag[5] = 0; g_flag[5] = 0;
} }
@ -1021,6 +1056,8 @@ LOCAL void test_upgrade_start(stru_icp67 *p_icp67)
p_icp67->method.upgrade_start_cb(p_icp67, 2); p_icp67->method.upgrade_start_cb(p_icp67, 2);
} }
async_cmd_printf("self_ptl", "[self_ptl 9] upgrade_start command sent\n");
g_flag[9] = 0; g_flag[9] = 0;
} }
@ -1133,12 +1170,12 @@ LOCAL void cmd_show_iec_tbl(uint16_t info_addr)
LOCAL void cmd_self_ptl(int argc, char *argv[]) LOCAL void cmd_self_ptl(int argc, char *argv[])
{ {
printf("self_ptl 1 -----param_get\n"); printf("self_ptl 1 --------> param_get - 读取本地参数AO点表值\n");
printf("self_ptl 3 100 -----iec_point_tbl_get\n"); printf("self_ptl 3 100 -----> iec_point_tbl_get - 读取IEC点表MX/ST/CO/AO/DD\n");
printf("self_ptl 5 -----time_set\n"); printf("self_ptl 5 --------> time_set - 设置ICP67对时发送当前系统时间\n");
printf("self_ptl 6 -----self_check_get\n"); printf("self_ptl 6 --------> self_check_get - 读取自检信息(版本/IP/状态/GPS等\n");
printf("self_ptl 9 -----upgrade_start\n"); printf("self_ptl 9 --------> upgrade_start - 启动ICP67升级流程\n");
printf("self_ptl 11 1 -----file\n"); printf("self_ptl 11 1 <dir> -> file - 读取目录文件列表\n");
if(argc >= 2) if(argc >= 2)
{ {

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,9 @@
#include "myCmd.h" #include "myCmd.h"
#include <pthread.h> #include <pthread.h>
#include <vector> #include <vector>
#include <map>
#include <unistd.h> #include <unistd.h>
#include <cstdarg>
typedef struct typedef struct
{ {
@ -78,6 +80,74 @@ LOCAL std::map<std::string, stru_ws_signal_method> g_ws_signal_methods_map =
{"param", {add_param_signal, del_param_signal, set_param_signal_data, make_param_signal_json}}, {"param", {add_param_signal, del_param_signal, set_param_signal_data, make_param_signal_json}},
}; };
// 异步命令输出通道:按命令名记录最后发起的 WebSocket 连接 ID
// 供 self_ptl 等模块的异步回调将结果推送回对应的前端
LOCAL std::map<std::string, unsigned long> g_async_cmd_conn_map;
LOCAL 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(NULL == 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(NULL == module) return 0;
pthread_mutex_lock(&g_async_cmd_conn_mutex);
auto it = g_async_cmd_conn_map.find(module);
unsigned long conn_id = (it != g_async_cmd_conn_map.end()) ? it->second : 0;
pthread_mutex_unlock(&g_async_cmd_conn_mutex);
return conn_id;
}
// ======== 异步命令输出分发器实现 ========
void async_cmd_printf(const char *module, const char *fmt, ...)
{
if(NULL == module || NULL == fmt)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if(0 == conn_id)
{
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(NULL == module || NULL == msg)
{
return;
}
unsigned long conn_id = ws_get_async_cmd_conn(module);
if(0 == conn_id)
{
return;
}
char buf[1024];
int n = snprintf(buf, sizeof(buf), "[%s] %s %s\n",
module, success ? "" : "", msg);
ws_send_binary(conn_id, buf, (size_t)n);
}
@ -727,6 +797,16 @@ void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
cJSON *data = cJSON_GetObjectItem(root, "data"); cJSON *data = cJSON_GetObjectItem(root, "data");
if(data && cJSON_IsString(data) && data->valuestring) if(data && cJSON_IsString(data) && data->valuestring)
{ {
// 提取命令名(第一个单词),存入异步命令连接映射
char module_name[64] = {0};
const char *p = data->valuestring;
int i = 0;
while(*p && *p != ' ' && *p != '\t' && i < 63)
{
module_name[i++] = *p++;
}
ws_set_async_cmd_conn(module_name, c->id);
bool is_dc = (0 == strncmp(data->valuestring, "datacenter", 10)); bool is_dc = (0 == strncmp(data->valuestring, "datacenter", 10));
std::string result = ws_cmd_exec(data->valuestring); std::string result = ws_cmd_exec(data->valuestring);
ws_send_binary(c->id, result.data(), result.size()); ws_send_binary(c->id, result.data(), result.size());

View File

@ -13,6 +13,30 @@
var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' }; var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' };
/* ========================================================================
* 共用工具
* ======================================================================== */
function renderTable(containerId, headers, rowsHtml, emptyText) {
var html = '<div class="table-card"><div class="table-wrapper"><table class="signal-table">';
html += '<thead><tr>';
for (var i = 0; i < headers.length; i++) {
html += '<th>' + headers[i] + '</th>';
}
html += '</tr></thead><tbody id="' + containerId + '">';
if (rowsHtml) {
html += rowsHtml;
} else {
html += '<tr class="empty-row"><td colspan="' + headers.length + '">' + (emptyText || '暂无数据,请先添加信号') + '</td></tr>';
}
html += '</tbody></table></div></div>';
return html;
}
function fmtVal(v) {
if (v === null || v === undefined || v === '') return '-';
return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ======================================================================== /* ========================================================================
* 共用工具栏 HTML * 共用工具栏 HTML
* ======================================================================== */ * ======================================================================== */
@ -61,29 +85,6 @@ function bindToolbar(signalType) {
}; };
} }
/* ========================================================================
* 表格渲染工具
* ======================================================================== */
function renderTable(containerId, headers, rowsHtml, emptyText) {
var html = '<div class="table-card"><div class="table-wrapper"><table class="signal-table">';
html += '<thead><tr>';
for (var i = 0; i < headers.length; i++) {
html += '<th>' + headers[i] + '</th>';
}
html += '</tr></thead><tbody id="' + containerId + '">';
if (rowsHtml) {
html += rowsHtml;
} else {
html += '<tr class="empty-row"><td colspan="' + headers.length + '">' + (emptyText || '暂无数据,请先添加信号') + '</td></tr>';
}
html += '</tbody></table></div></div>';
return html;
}
function fmtVal(v) {
if (v === null || v === undefined || v === '') return '-';
return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ======================================================================== /* ========================================================================
* 1. LoginPage 登录页 * 1. LoginPage 登录页
@ -105,18 +106,13 @@ var LoginPage = {
+ '<button class="login-btn" id="login-btn">登 录</button>' + '<button class="login-btn" id="login-btn">登 录</button>'
+ '<p class="login-hint">当前版本无需认证,点击登录即可进入系统</p>' + '<p class="login-hint">当前版本无需认证,点击登录即可进入系统</p>'
+ '</div>'; + '</div>';
document.getElementById('login-btn').onclick = function() { document.getElementById('login-btn').onclick = function() {
localStorage.setItem('rtu_logged_in', '1'); localStorage.setItem('rtu_logged_in', '1');
window.location.hash = '#dashboard'; window.location.hash = '#dashboard';
}; };
}, },
onEnter: function() { onEnter: function() { document.body.classList.add('login-page'); },
document.body.classList.add('login-page'); onLeave: function() { document.body.classList.remove('login-page'); }
},
onLeave: function() {
document.body.classList.remove('login-page');
}
}; };
/* ======================================================================== /* ========================================================================
@ -125,15 +121,14 @@ var LoginPage = {
var DashboardPage = { var DashboardPage = {
render: function(container) { render: function(container) {
var cards = [ var cards = [
{ cls: 'out', icon: '📡', title: '注册', desc: 'out — 状态量信号注册与修改', hash: '#out' }, { cls: 'out', icon: '📡', title: '注册', desc: 'out — 状态量信号注册与修改', hash: '#out' },
{ cls: 'in', icon: '📊', title: '链接注册', desc: 'in — 测量量信号链接注册与监视', hash: '#in' }, { cls: 'in', icon: '📊', title: '链接注册', desc: 'in — 测量量信号链接注册与监视', hash: '#in' },
{ cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' }, { cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' },
{ cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' }, { cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' },
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' }, { cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' }, { cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' } { cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' }
]; ];
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>'; var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
html += '<div class="dashboard-grid">'; html += '<div class="dashboard-grid">';
for (var i = 0; i < cards.length; i++) { for (var i = 0; i < cards.length; i++) {
@ -156,7 +151,6 @@ var DashboardPage = {
* ======================================================================== */ * ======================================================================== */
var OutPage = (function() { var OutPage = (function() {
var data = []; var data = [];
function renderTableHtml() { function renderTableHtml() {
if (data.length === 0) return ''; if (data.length === 0) return '';
var rows = ''; var rows = '';
@ -174,7 +168,6 @@ var OutPage = (function() {
} }
return rows; return rows;
} }
function bindModifyButtons() { function bindModifyButtons() {
var buttons = document.querySelectorAll('#out-tbody .btn-info'); var buttons = document.querySelectorAll('#out-tbody .btn-info');
for (var k = 0; k < buttons.length; k++) { for (var k = 0; k < buttons.length; k++) {
@ -184,14 +177,10 @@ var OutPage = (function() {
var val = input.value.trim(); var val = input.value.trim();
if (val === '') { alert('请输入修改值'); return; } if (val === '') { alert('请输入修改值'); return; }
var item = data[idx]; var item = data[idx];
WsClient.send({ WsClient.send({ curd:'set', signal_type:'out', saddr:item.saddr, signal_data:val, setting_zone:'0' });
curd: 'set', signal_type: 'out', saddr: item.saddr,
signal_data: val, setting_zone: '0'
});
}; };
} }
} }
return { return {
render: function(container) { render: function(container) {
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改']; var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
@ -208,10 +197,7 @@ var OutPage = (function() {
data = arr || []; data = arr || [];
var tbody = document.getElementById('out-tbody'); var tbody = document.getElementById('out-tbody');
if (!tbody) return; if (!tbody) return;
if (data.length === 0) { if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>'; return; }
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml(); tbody.innerHTML = renderTableHtml();
bindModifyButtons(); bindModifyButtons();
} }