<修改> 1、在前端页面中增加命令终端页面,在此页面中可以调用自定义命令,并将自定义命令的响应信息展示到前端页面中
This commit is contained in:
parent
742d9b36a0
commit
3c19148b97
|
|
@ -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/` — 技能文档
|
||||
后续新增文档按此规则归类存放,不随意创建新的根级目录。
|
||||
|
|
@ -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>` 实现多连接推送
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -773,6 +773,25 @@ LOCAL int dc_check_val_valid(const stru_signal *p_signal, uint8_t setting_zone,
|
|||
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)
|
||||
|
|
@ -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_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());
|
||||
return -1;
|
||||
|
|
|
|||
|
|
@ -500,6 +500,15 @@ LOCAL void cmd_iec(int argc, char *argv[])
|
|||
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(0 == strcmp(argv[2], "get"))
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
{MMS_BOOLEAN, DATA_TYPE_U8},
|
||||
{MMS_BOOLEAN, DATA_TYPE_B},
|
||||
{MMS_BIT_STRING, DATA_TYPE_U32},
|
||||
{MMS_INTEGER, DATA_TYPE_S32},
|
||||
{MMS_UNSIGNED, DATA_TYPE_U32},
|
||||
|
|
@ -832,9 +832,9 @@ LOCAL void iec61850m_show_info()
|
|||
LOCAL void cmd_iec61850m(int argc, char *argv[])
|
||||
{
|
||||
const char *str = "\
|
||||
iec61850m info ----------> 查看iec61850m信息\n\
|
||||
iec61850m yk <fd> <saddr> <val> <ctrl_type> ------遥控,fd对应61850m的fd, 短地址,值,控制类型\n\
|
||||
iec61850m set <fd> <saddr> <val> <set_type> ------修改定值,fd对应61850m的fd, 短地址,值,设置类型\n";
|
||||
iec61850m info ----------> 查看所有客户端连接信息(fd/prj_name)\n\
|
||||
iec61850m yk <fd> <saddr> <val> <ctrl_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);
|
||||
if(NULL == p_app)
|
||||
|
|
@ -863,10 +863,10 @@ LOCAL void cmd_iec61850m(int argc, char *argv[])
|
|||
|
||||
int fd = atoi(argv[2]);
|
||||
std::string saddr = argv[3];
|
||||
int val = atoi(argv[4]);
|
||||
std::string val = argv[4];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <string>
|
||||
|
||||
#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);
|
||||
|
||||
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)
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
if(0 == unlink(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)
|
||||
{
|
||||
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;
|
||||
|
|
@ -272,6 +291,7 @@ void file_read(stru_icp67 *p_icp67, uint32_t id, uint32_t offset, uint32_t follo
|
|||
if(0 == follow)
|
||||
{
|
||||
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)
|
||||
|
|
@ -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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -381,6 +402,11 @@ void file_write_end(stru_icp67 *p_icp67, uint8_t result, uint32_t id, uint32_t o
|
|||
if(0 == result)
|
||||
{
|
||||
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();
|
||||
|
|
@ -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;
|
||||
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
|
@ -1000,6 +1032,9 @@ LOCAL void test_time_set(stru_icp67 *p_icp67)
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1021,6 +1056,8 @@ LOCAL void test_upgrade_start(stru_icp67 *p_icp67)
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -1133,12 +1170,12 @@ LOCAL void cmd_show_iec_tbl(uint16_t info_addr)
|
|||
|
||||
LOCAL void cmd_self_ptl(int argc, char *argv[])
|
||||
{
|
||||
printf("self_ptl 1 -----param_get\n");
|
||||
printf("self_ptl 3 100 -----iec_point_tbl_get\n");
|
||||
printf("self_ptl 5 -----time_set\n");
|
||||
printf("self_ptl 6 -----self_check_get\n");
|
||||
printf("self_ptl 9 -----upgrade_start\n");
|
||||
printf("self_ptl 11 1 -----file\n");
|
||||
printf("self_ptl 1 --------> param_get - 读取本地参数(AO点表值)\n");
|
||||
printf("self_ptl 3 100 -----> iec_point_tbl_get - 读取IEC点表(MX/ST/CO/AO/DD)\n");
|
||||
printf("self_ptl 5 --------> time_set - 设置ICP67对时(发送当前系统时间)\n");
|
||||
printf("self_ptl 6 --------> self_check_get - 读取自检信息(版本/IP/状态/GPS等)\n");
|
||||
printf("self_ptl 9 --------> upgrade_start - 启动ICP67升级流程\n");
|
||||
printf("self_ptl 11 1 <dir> -> file - 读取目录文件列表\n");
|
||||
|
||||
if(argc >= 2)
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,9 @@
|
|||
#include "myCmd.h"
|
||||
#include <pthread.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <unistd.h>
|
||||
#include <cstdarg>
|
||||
|
||||
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}},
|
||||
};
|
||||
|
||||
// 异步命令输出通道:按命令名记录最后发起的 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");
|
||||
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));
|
||||
std::string result = ws_cmd_exec(data->valuestring);
|
||||
ws_send_binary(c->id, result.data(), result.size());
|
||||
|
|
|
|||
|
|
@ -13,6 +13,30 @@
|
|||
|
||||
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* 共用工具栏 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/* ========================================================================
|
||||
* 1. LoginPage — 登录页
|
||||
|
|
@ -105,18 +106,13 @@ var LoginPage = {
|
|||
+ '<button class="login-btn" id="login-btn">登 录</button>'
|
||||
+ '<p class="login-hint">当前版本无需认证,点击登录即可进入系统</p>'
|
||||
+ '</div>';
|
||||
|
||||
document.getElementById('login-btn').onclick = function() {
|
||||
localStorage.setItem('rtu_logged_in', '1');
|
||||
window.location.hash = '#dashboard';
|
||||
};
|
||||
},
|
||||
onEnter: function() {
|
||||
document.body.classList.add('login-page');
|
||||
},
|
||||
onLeave: function() {
|
||||
document.body.classList.remove('login-page');
|
||||
}
|
||||
onEnter: function() { document.body.classList.add('login-page'); },
|
||||
onLeave: function() { document.body.classList.remove('login-page'); }
|
||||
};
|
||||
|
||||
/* ========================================================================
|
||||
|
|
@ -133,7 +129,6 @@ var DashboardPage = {
|
|||
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' },
|
||||
{ cls: 'terminal', icon: '💻', title: '命令终端', desc: '实时命令行操作 RTU 系统', hash: '#terminal' }
|
||||
];
|
||||
|
||||
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
|
||||
html += '<div class="dashboard-grid">';
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
|
|
@ -156,7 +151,6 @@ var DashboardPage = {
|
|||
* ======================================================================== */
|
||||
var OutPage = (function() {
|
||||
var data = [];
|
||||
|
||||
function renderTableHtml() {
|
||||
if (data.length === 0) return '';
|
||||
var rows = '';
|
||||
|
|
@ -174,7 +168,6 @@ var OutPage = (function() {
|
|||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function bindModifyButtons() {
|
||||
var buttons = document.querySelectorAll('#out-tbody .btn-info');
|
||||
for (var k = 0; k < buttons.length; k++) {
|
||||
|
|
@ -184,14 +177,10 @@ var OutPage = (function() {
|
|||
var val = input.value.trim();
|
||||
if (val === '') { alert('请输入修改值'); return; }
|
||||
var item = data[idx];
|
||||
WsClient.send({
|
||||
curd: 'set', signal_type: 'out', saddr: item.saddr,
|
||||
signal_data: val, setting_zone: '0'
|
||||
});
|
||||
WsClient.send({ curd:'set', signal_type:'out', saddr:item.saddr, signal_data:val, setting_zone:'0' });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
render: function(container) {
|
||||
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
|
||||
|
|
@ -208,10 +197,7 @@ var OutPage = (function() {
|
|||
data = arr || [];
|
||||
var tbody = document.getElementById('out-tbody');
|
||||
if (!tbody) return;
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>';
|
||||
return;
|
||||
}
|
||||
if (data.length === 0) { tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>'; return; }
|
||||
tbody.innerHTML = renderTableHtml();
|
||||
bindModifyButtons();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue