docs+test: webserver 分析文档 + 测试增强 + gitignore 更新
- docs/system/libweb_server/: 原工程 vs 现工程 webserver 对比分析 - docs/system/libweb_server/: 修改调试计划 - test_cmd.cpp: 命令框架测试扩展 - test_log_latency.cpp: 日志延迟测试(新增) - .gitignore: 新增 config/SYSTEM/datacenter_out.xml 忽略规则
This commit is contained in:
parent
28e26f8b67
commit
370be09926
|
|
@ -13,6 +13,7 @@
|
|||
*.pdb
|
||||
*.idb
|
||||
*.gch
|
||||
RTU
|
||||
|
||||
# 编译临时目录
|
||||
obj/
|
||||
|
|
@ -28,5 +29,8 @@ release/inc/
|
|||
|
||||
# AI 工具
|
||||
CLAUDE.md
|
||||
# docs/
|
||||
|
||||
# 自动生成文件
|
||||
config/SYSTEM/datacenter_out.xml
|
||||
docs/
|
||||
libiec61850/libiec61850手册/
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# RTU_ALL_AI 开发完成事项清单
|
||||
|
||||
> 日期: 2026-07-10
|
||||
> 日期: 2026-07-13
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -70,6 +70,15 @@
|
|||
| EV_STOP 补全 | `self_ptl` + `com_router` + `modbus_m`(3模块) | ✅ |
|
||||
| libiec 模块文档 | `docs/system/libiec/`(待补) | ⬜ |
|
||||
|
||||
## 阶段 2:v3.2 WSL 本地测试准备(2026-07-13)
|
||||
|
||||
| 任务 | 模块 | 状态 |
|
||||
|------|------|:----:|
|
||||
| 新增 tcp_client 通道解析 | `com_router` — JSON 解析 + 默认配置支持 COMM_TYPE_TCP_CLIENT | ✅ |
|
||||
| 新增 FTU 网口通道配置 | `config/CHANNEL/channel_cfg.json` — tcp_ftu (127.0.0.1:54321 TCP 客户端) | ✅ |
|
||||
| 编译脚本增强 | `release/build.sh` — 编译前删除旧 RTU, 编译后拷贝到 config 同级目录 | ✅ |
|
||||
| x86 编译验证 | 全量编译通过, RTU 可正常启动到 RTU> 提示符 | ✅ |
|
||||
|
||||
## 阶段 3:系统集成
|
||||
|
||||
## 阶段 3:系统集成
|
||||
|
|
|
|||
2
PLAN.md
2
PLAN.md
|
|
@ -294,7 +294,7 @@ int my_module_do_something(uint32_t id, const char *data);
|
|||
| 4.3 | IEC 61850 MMS 从站 | `libmms_s` + `libiec61850s` | ⬜ |
|
||||
| 4.4 | 嵌入式 HTTP 服务 | `libmongoose` | ✅ |
|
||||
| 4.5 | MQTT 客户端 | `libmy_mosquitto` | ⬜ |
|
||||
| 4.6 | PLC 逻辑引擎 | `libplc` | ⬜ |
|
||||
| 4.6 | PLC 逻辑引擎 | `libplc` | ✅ |
|
||||
| 4.7 | 内部自定义协议 | `libself_ptl` | ✅ |
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,630 @@
|
|||
# RTU Web 前端功能详细清单
|
||||
|
||||
> 基于原工程 packed_fs.c 完整分析
|
||||
> 分析日期: 2026-07-10
|
||||
> 重构目标: 海外项目级工业品质 Web 管理界面
|
||||
|
||||
---
|
||||
|
||||
## 一、架构总览
|
||||
|
||||
| 项目 | 原实现 | 重构方案 |
|
||||
|------|--------|---------|
|
||||
| 类型 | Hash-based SPA | 同架构 (零依赖, 纯 HTML+CSS+JS) |
|
||||
| 路由 | `window.location.hash` | 同 |
|
||||
| 通信 | WebSocket (ws://0.0.0.0:8080/ws) | 同 |
|
||||
| 图标 | Emoji | → SVG 图标 (Lucide Icons) |
|
||||
| 样式 | 自定义 CSS | → 暗色工业主题 + CSS 变量系统 |
|
||||
| 静态服务 | Mongoose packed_fs | → `mg_http_serve_dir` 从磁盘服务 |
|
||||
| 文件数 | 8 个嵌入文件 | → 单文件 SPA (index.html 内嵌全部 CSS+JS) |
|
||||
|
||||
---
|
||||
|
||||
## 二、页面清单 (12 页)
|
||||
|
||||
### 2.1 登录页 (`#login`)
|
||||
- 账号/密码输入框
|
||||
- 登录按钮 → 设置 `localStorage.rtu_logged_in`
|
||||
- 所有非登录页须先验证登录状态
|
||||
- 已登录时访问 login → 自动跳转 dashboard
|
||||
|
||||
### 2.2 仪表盘 (`#dashboard`)
|
||||
- **信号导航卡片**: 5 张卡片 (out / in / yk / ao / param), 显示每种信号已注册数量
|
||||
- **批量 XML 导入/导出**:
|
||||
- 导出: 将所有信号导出为一个 XML 文件 (按 type 分组)
|
||||
- 导入: 选择 XML 文件, 按 type 批量执行 add 请求
|
||||
- **实时连接状态**: WebSocket 连接指示器 (绿色/红色圆点)
|
||||
|
||||
### 2.3 开出信号 (`#out`)
|
||||
- 信号注册表 (表格): saddr / desc / type / value
|
||||
- **添加**: 弹出表单 (saddr, desc, data_type 下拉选择)
|
||||
- **编辑**: 行内修改按钮 → 弹出
|
||||
- **删除**: 单选 + 批量勾选删除
|
||||
- 实时值刷新 (来自 WebSocket 推送)
|
||||
|
||||
### 2.4 开入信号 (`#in`)
|
||||
- 链接信号表: saddr / desc / link_saddr / type / value
|
||||
- **添加**: saddr, desc, link_saddr (链接目标)
|
||||
- **删除**: 单选 + 批量
|
||||
- 只读显示 (值来自链接的 out 信号)
|
||||
|
||||
### 2.5 遥控 (`#yk`)
|
||||
- 遥控信号表: saddr / desc / type / ctrl_type / value
|
||||
- **添加**: saddr, desc, type, ctrl_type (直控=0 / 选控=1)
|
||||
- **控制操作**: set 按钮 → 输入值 → 发送 set 命令
|
||||
- **删除**: 单选 + 批量
|
||||
|
||||
### 2.6 参数 (`#ao`)
|
||||
- 参数信号表: saddr / desc / type / ctrl_type / value / min / max / step / unit
|
||||
- **添加**: 含元数据字段 (min/max/step/unit)
|
||||
- **修改**: set 按钮
|
||||
- 支持下拉选择值 (val 数组)
|
||||
- **删除**: 单选 + 批量
|
||||
|
||||
### 2.7 定值 (`#param`)
|
||||
- 定值信号表: saddr / desc / type / ctrl_type
|
||||
- **展开行**: 点击展开, 显示多定值区子表
|
||||
- 每区: 区号 / 值 / 修改按钮
|
||||
- **添加**: 含 ctrl_type
|
||||
|
||||
### 2.8 数据中心 (`#datacenter`)
|
||||
- 信号类型标签栏切换 (out/in/yk/ao/param)
|
||||
- 表格: saddr / desc / type / value / 操作
|
||||
- 所有信号类型的统一浏览视图
|
||||
- 支持 set 操作
|
||||
|
||||
### 2.9 PLC 调试 (`#plc_debug`)
|
||||
- 显示所有 out 信号
|
||||
- saddr / desc / value 表
|
||||
- 实时刷新 (来自 WebSocket 推送)
|
||||
- 用于调试 PLC 逻辑输出
|
||||
|
||||
### 2.10 PLC 逻辑配置 (`#plc_config`)
|
||||
- **Canvas 图形编辑器** (原工程最复杂功能):
|
||||
- 拖拽节点 (Gate/Input/Output)
|
||||
- 连线编辑
|
||||
- 双击节点编辑属性
|
||||
- XML 保存/加载
|
||||
- 重构方案: 保留功能, 重构 UI
|
||||
|
||||
### 2.11 实时监控 (`#monitor`)
|
||||
- WebSocket 消息实时环形缓冲区
|
||||
- 15 分钟 TTL / 500 条上限
|
||||
- 消息方向: ↑ (发送) / ↓ (接收)
|
||||
- 实时滚动显示
|
||||
- JSON 格式化预览
|
||||
|
||||
### 2.12 命令终端 (`#terminal`)
|
||||
- 仿真终端样式 (黑底绿字)
|
||||
- 命令输入框
|
||||
- stdout 实时输出 (WebSocket Binary 帧)
|
||||
- 命令列表标签 (get_cmds)
|
||||
- 历史命令支持
|
||||
|
||||
---
|
||||
|
||||
## 三、WebSocket 协议
|
||||
|
||||
### 客户端 → 服务端
|
||||
|
||||
| 消息类型 | JSON | 说明 |
|
||||
|---------|------|------|
|
||||
| 信号 CRUD | `{saddr, signal_type, curd, signal_data, setting_zone}` | add/del/set |
|
||||
| 命令执行 | `{type:"cmd", data:"command args"}` | 远程命令 |
|
||||
| 获取命令列表 | `{type:"get_cmds"}` | 用于终端 Tab 补全 |
|
||||
|
||||
### 服务端 → 客户端
|
||||
|
||||
| 消息类型 | JSON | 说明 |
|
||||
|---------|------|------|
|
||||
| 信号推送 | `{out:[...], in:[...], yk:[...], ao:[...], param:[...]}` | 增量 (仅变化) |
|
||||
| 命令输出 | Binary frame (stdout) | 命令 stdout |
|
||||
| 命令列表 | `{type:"cmd_list", cmds:[{name,desc},...]}` | 命令补全数据 |
|
||||
| 异步输出 | `{type:"async_output", module, data, success, msg}` | 异步命令结果 |
|
||||
|
||||
### 推送消息结构
|
||||
|
||||
```json
|
||||
{
|
||||
"out": [{"saddr":"x", "desc":"x", "type":"int", "val":"123"}],
|
||||
"in": [{"saddr":"x", "desc":"x", "type":"float", "val":"3.14"}],
|
||||
"yk": [{"saddr":"x", "desc":"x", "type":"int", "val":"0", "ctrl_type":1}],
|
||||
"ao": [{"saddr":"x", "desc":"x", "type":"float", "val":"5.0", "ctrl_type":0}],
|
||||
"param": [{"saddr":"x", "desc":"x", "type":"int", "ctrl_type":0,
|
||||
"setting_zone_list":[{"id":"0","val":"100"}, ...]}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、导航结构
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Logo │ RTU Web Manager
|
||||
│ Status │ ● Connected / ○ Disconnected
|
||||
├──────────┤
|
||||
│ 📊 Dashboard │
|
||||
│ 📡 Out 信号 │
|
||||
│ 🔗 In 信号 │
|
||||
│ ⚡ YK 遥控 │
|
||||
│ 🎚️ AO 参数 │
|
||||
│ ⚙️ Param 定值 │
|
||||
│ 📋 Datacenter │
|
||||
│ 🔌 PLC Debug │
|
||||
│ 🧩 PLC Config │
|
||||
│ 📈 Monitor │
|
||||
│ 💻 Terminal │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
左侧固定导航栏 (220px), 右侧主内容区
|
||||
响应式: 移动端隐藏侧栏
|
||||
|
||||
---
|
||||
|
||||
## 五、重构设计参数
|
||||
|
||||
| 参数 | 选择 |
|
||||
|------|------|
|
||||
| 风格 | 暗色工业主题 (Dark Industrial Dashboard) |
|
||||
| 主色调 | Cyan/Teal accent (#00E5FF / #1DE9B6) |
|
||||
| 背景 | #0F1923 (deep navy) / #1A2332 (card bg) |
|
||||
| 字体 | Inter (标题) + JetBrains Mono (数据/终端) |
|
||||
| 图标 | Lucide SVG inline (通过 CDN) |
|
||||
| 圆角 | 8px (卡片), 6px (按钮), 4px (输入框) |
|
||||
| 间距 | 8px 基础网格 |
|
||||
| 动画 | 150-300ms ease-out transitions |
|
||||
| 图表 | Canvas 手写轻量图表 (实时监控页) |
|
||||
|
||||
---
|
||||
|
||||
## 六、文件结构 (重构后)
|
||||
|
||||
```
|
||||
src/system/libweb_server/
|
||||
├── inc/
|
||||
│ └── web_server.h # 头文件 (无需修改)
|
||||
└── src/
|
||||
├── web_server.cpp # +static file serving (mg_http_serve_dir)
|
||||
└── ws_method.cpp # 已有, 无需修改
|
||||
|
||||
web/ # 新建: 前端静态文件目录
|
||||
└── index.html # 单文件 SPA (全部 CSS+JS 内嵌)
|
||||
├── <style> ... </style>
|
||||
├── <script>
|
||||
│ ├── WsClient # WebSocket 通信
|
||||
│ ├── MonitorRing # 实时监控环形缓冲
|
||||
│ ├── App # 主应用 (路由/页面管理)
|
||||
│ ├── SignalStore # 信号数据缓存
|
||||
│ ├── Pages.* # 各页面渲染函数
|
||||
│ └── UI.* # 通用 UI 组件 (Modal/Toast/Table/Toolbar)
|
||||
└── </script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实现优先级
|
||||
|
||||
| 优先级 | 页面 | 理由 |
|
||||
|--------|------|------|
|
||||
| P0 | index.html 壳 + 登录 + 仪表盘 + 导航 | 基础框架 |
|
||||
| P0 | WsClient + SignalStore | 通信基础 |
|
||||
| P1 | Out / In 信号管理 | 核心 CRUD |
|
||||
| P1 | YK / AO / Param 管理 | 完整信号类型 |
|
||||
| P2 | Datacenter 统一浏览 | 数据汇总 |
|
||||
| P2 | Monitor 实时监控 | 调试利器 |
|
||||
| P2 | Terminal 命令终端 | 远程管理 |
|
||||
| P3 | PLC Debug / PLC Config | 高级功能, Canvas 编辑器较复杂 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 八、重构实现 vs 原规格详细对比分析
|
||||
|
||||
> 分析日期: 2026-07-10
|
||||
> 对比维度: 架构 / 页面 / 功能 / 样式 / 协议 / 后端对齐
|
||||
|
||||
---
|
||||
|
||||
### 8.1 架构对比
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 原规格FEATURE_INVENTORY
|
||||
A1["单文件 SPA<br/>index.html 内嵌全部 CSS+JS"]
|
||||
A2["CDN 加载 Lucide Icons"]
|
||||
A3["单文件内分模块<br/>WsClient/MonitorRing/App/SignalStore/Pages.*/UI.*"]
|
||||
end
|
||||
subgraph 当前实现web/
|
||||
B1["多文件 SPA<br/>index.html + 15 JS + 1 CSS"]
|
||||
B2["内联 SVG Icons<br/>Icon._map 18个Lucide图标"]
|
||||
B3["独立文件模块<br/>ws-client/signal-store/router/ui/i18n<br/>+ 8个pages/*.js"]
|
||||
end
|
||||
A1 -->|"❌ 不一致"| B1
|
||||
A2 -->|"⚠️ 变体实现"| B2
|
||||
A3 -->|"✅ 架构对齐"| B3
|
||||
```
|
||||
|
||||
| 维度 | 原规格 (FEATURE_INVENTORY) | 当前实现 (web/) | 状态 |
|
||||
|------|--------------------------|-----------------|:---:|
|
||||
| **文件架构** | 单文件 SPA (index.html 内嵌) | 多文件 (1 HTML + 1 CSS + 13 JS) | ❌ 偏离 |
|
||||
| **图标方案** | CDN 加载 Lucide Icons | 内联 SVG 字符串 (ui.js `Icon._map`) | ⚠️ 变体 |
|
||||
| **路由方案** | `App` 对象管理路由 | `Router` 全局对象 (router.js) | ✅ 对齐 |
|
||||
| **通信层** | `WsClient` 对象 | `WsClient` 全局对象 (ws-client.js) | ✅ 对齐 |
|
||||
| **数据层** | `SignalStore` 对象 | `SignalStore` 全局对象 (signal-store.js) | ✅ 对齐 |
|
||||
| **样式方案** | 单文件内嵌 `<style>` | 独立 `css/style.css` | ❌ 偏离 |
|
||||
| **i18n** | 未提及 | `I18n` 全局对象 (i18n.js, 95 个 key, zh/en) | 🆕 新增 |
|
||||
|
||||
---
|
||||
|
||||
### 8.2 页面功能逐页对比
|
||||
|
||||
#### 8.2.1 登录页 (`#login`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 用户名/密码输入 | ✅ | ✅ `#login-user` / `#login-pass` | ✅ |
|
||||
| 登录验证 | 未明确 | 硬编码 `admin/admin` | ⚠️ |
|
||||
| localStorage 持久化 | ✅ `rtu_logged_in` | ✅ `rtu_logged_in='1'` | ✅ |
|
||||
| 登录守卫 | ✅ 非登录页强制跳转 | ✅ `Router.dispatch()` 守卫 | ✅ |
|
||||
| 已登录自动跳转 | ✅ login→dashboard | ✅ login→dashboard | ✅ |
|
||||
| Enter 键登录 | 未提及 | ✅ `keydown` 监听 | 🆕 |
|
||||
| 自动聚焦 | 未提及 | ✅ `#login-user` autofocus | 🆕 |
|
||||
| 暗色主题 | 未提及 | ✅ `.login-wrap` 暗色背景 | 🆕 |
|
||||
|
||||
#### 8.2.2 仪表盘 (`#dashboard`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 5 张信号计数卡片 | ✅ out/in/yk/ao/param | ✅ `.dash-grid` 5 cards | ✅ |
|
||||
| 卡片点击跳转 | 未明确 | ✅ 点击跳转对应 hash | 🆕 |
|
||||
| 卡片颜色区分 | 未明确 | ✅ 5 种不同色 (accent/accent2/warning/success/#a78bfa) | 🆕 |
|
||||
| 实时连接状态 | ✅ WS 连接指示器 | ✅ `#dash-status` + `WsClient.onStatus()` | ✅ |
|
||||
| XML 批量导出 | ✅ 按 type 分组导出 | ✅ `DashActions.exportAll()` → Blob 下载 | ✅ |
|
||||
| XML 批量导入 | ✅ 选择文件→批量 add | ✅ `<input type=file>` → 解析 XML → 逐条 add | ✅ |
|
||||
| 快速操作按钮 | 未提及 | ✅ 打开终端 + 打开监控 + 导出 + 导入 | 🆕 |
|
||||
| 每卡显示描述 | 未提及 | ✅ `dash.{type}.desc` 多行描述 | 🆕 |
|
||||
| 信号数量实时更新 | 未提及 | ✅ 绑定 `SignalStore.data` | 🆕 |
|
||||
|
||||
#### 8.2.3 开出信号 (`#out`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 表格列 | saddr/desc/type/value | SAddr/Desc/Type/Val/Action | ✅ |
|
||||
| 添加信号 | ✅ 弹出表单 (saddr/desc/data_type) | ✅ `SignalActions.add('out')` Modal | ✅ |
|
||||
| 编辑信号 | ✅ 行内修改按钮→弹出 | ❌ **无编辑功能**,仅有 Set 值 | ❌ 缺失 |
|
||||
| 删除信号 | ✅ 单选+批量勾选 | ✅ 单选 + 批量 checkbox | ✅ |
|
||||
| 实时值刷新 | ✅ WS 推送 | ✅ `cell-val.changed` 动画 | ✅ |
|
||||
| 值变化动画 | 未提及 | ✅ `flash-val` 0.6s 绿色闪烁 | 🆕 |
|
||||
| 数据类型下拉 | data_type 选择 | U8/U16/U32/F32/STR | ✅ |
|
||||
| 设值操作 | 未明确 | ✅ `SignalActions.set()` → 输入新值 | 🆕 |
|
||||
|
||||
**⚠️ 缺失: 编辑信号属性 (修改 saddr/desc/type)。当前前端只有"添加"和"删除",没有"修改"已有信号属性的功能。**
|
||||
|
||||
#### 8.2.4 开入信号 (`#in`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 表格列 | saddr/desc/link_saddr/type/value | SAddr/Desc/Link/Type/Val/Action | ✅ |
|
||||
| link_saddr 字段 | ✅ 链接目标 | ✅ `#mf-link` 输入 | ✅ |
|
||||
| 添加信号 | ✅ saddr/desc/link_saddr | ✅ `SignalActions.add('in')` | ✅ |
|
||||
| 删除信号 | ✅ 单选+批量 | ✅ 单选 + 批量 | ✅ |
|
||||
| 只读显示 | ✅ 值来自链接 out | ⚠️ 表格显示值但无链接跳转 | ⚠️ 弱 |
|
||||
| Set 操作 | ❌ in 只读 | ❌ 无 Set 按钮 | ✅ |
|
||||
|
||||
#### 8.2.5 遥控 (`#yk`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 表格列 | saddr/desc/type/ctrl_type/value | SAddr/Desc/Type/Ctrl/Val/Action | ✅ |
|
||||
| ctrl_type 显示 | ✅ 直控=0/选控=1 | ✅ `ctrl_direct`/`ctrl_sbo` 翻译 | ✅ |
|
||||
| 控制操作 | ✅ set→输入值→发送 | ✅ `SignalActions.set('yk',...)` | ✅ |
|
||||
| 添加信号 | ✅ saddr/desc/type/ctrl_type | ✅ `SignalActions.add('yk')` | ✅ |
|
||||
| 删除信号 | ✅ 单选+批量 | ✅ 单选 (无批量 checkbox! ) | ⚠️ |
|
||||
| **批量删除** | 原规格有 | ❌ YK 页面无 checkbox | ❌ **缺失** |
|
||||
|
||||
**⚠️ 差异: YK/AO 页面缺少批量选择/删除 checkbox,而原规格明确写了"单选 + 批量勾选删除"。**
|
||||
|
||||
#### 8.2.6 模拟输出 (`#ao`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 基础功能 | 同 YK | ✅ 同 YK (共用 renderSignalPage) | ✅ |
|
||||
| **元数据字段** | min/max/step/unit | ❌ **完全没有** | ❌ **严重缺失** |
|
||||
| **下拉选择值** | val 数组下拉 | ❌ **完全没有** | ❌ **严重缺失** |
|
||||
| 批量删除 | 原规格有 | ❌ 无 checkbox | ❌ **缺失** |
|
||||
|
||||
**⚠️ 严重缺失: AO 页面在原规格中有 min/max/step/unit 元数据字段和 val 下拉选择,当前实现完全缺失。当前 AO = YK 的复制品,没有任何 AO 特有功能。**
|
||||
|
||||
#### 8.2.7 定值 (`#param`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| **展开行设计** | 点击展开 → 多区子表 | 主表 Zone 列平铺所有区 | ❌ **不同设计** |
|
||||
| 多定值区显示 | 区号/值/修改按钮 | Zone 列内逐行: `#id` + 值 + Set 按钮 | ⚠️ 功能等效 |
|
||||
| ~~每区独立 Set~~ | 展开行内有 Set | ✅ `ParamActions.set(saddr, zoneId)` | ✅ |
|
||||
| 添加信号 | ✅ 含 ctrl_type | ✅ `SignalActions.add('param')` | ✅ |
|
||||
| 删除信号 | ✅ (原规格未提批量) | ✅ 单选删除 | ✅ |
|
||||
| 批量删除 | 未提及 | ❌ 无 checkbox | — |
|
||||
|
||||
**⚠️ 设计差异: 原规格使用"点击展开行→子表"的层级 UI,当前使用平铺 Zone 列的扁平 UI。功能等效但交互不同。**
|
||||
|
||||
#### 8.2.8 数据中心 (`#datacenter`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 类型标签切换 | ✅ out/in/yk/ao/param | ✅ 5 个按钮切换 | ✅ |
|
||||
| 表格列 | saddr/desc/type/value/操作 | SAddr/Desc/Type/Val/Action | ✅ |
|
||||
| Set 操作 | ✅ | ✅ out/yk/ao 可 Set (in/param 不可) | ✅ |
|
||||
| **从 RTU 查询** | 未明确 | ✅ `DcCmd.refreshAll()` → `datacenter all` | 🆕 |
|
||||
| **文本解析** | 无 | ✅ `_parse()` 固定宽度文本表格解析 | 🆕 |
|
||||
| **自动刷新** | 无 | ✅ 10 秒间隔自动刷新 | 🆕 |
|
||||
| 信号数量显示 | 未提及 | ✅ 类型按钮上显示数量 | 🆕 |
|
||||
|
||||
#### 8.2.9 PLC 调试 (`#plc_debug`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 显示 out 信号 | ✅ saddr/desc/value 表 | ❌ **完全不同** | ❌ **重构** |
|
||||
| 三列布局 | ❌ 原规格是单表 | ✅ LED列/信号列/Button列 | 🆕 新设计 |
|
||||
| LED 指示灯 | ❌ | ✅ 可绑定信号的 LED 圆点 | 🆕 |
|
||||
| 翻转按钮 | ❌ | ✅ 点击翻转值→WS set | 🆕 |
|
||||
| 信号选择器 | ❌ | ✅ `showSignalPicker()` Modal | 🆕 |
|
||||
| 批量绑定 | ❌ | ✅ 勾选信号→一键→LED/BTN | 🆕 |
|
||||
|
||||
**⚠️ 原规格 PLC Debug 是"显示所有 out 信号的表",当前实现是"三列交互式调试面板"。功能完全不同,新版本更强。**
|
||||
|
||||
#### 8.2.10 PLC 配置 (`#plc_config`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| Canvas 图形编辑器 | ✅ 拖拽/连线/属性编辑 | ✅ 完整实现 | ✅ |
|
||||
| 节点类型 | 未列出 | ✅ 11 种 (INPUT/OUTPUT/OR/AND/NOT/T62P/T62D/RISING/SR/RS/FALLING) | 🆕 |
|
||||
| 网格吸附 | 未提及 | ✅ 20px grid | 🆕 |
|
||||
| 缩放/平移 | 未提及 | ✅ zoom 0.5-2.0 + pan | 🆕 |
|
||||
| XML 保存 | ✅ | ✅ `{type:'set_file'}` → RTU | ⚠️ |
|
||||
| XML 加载 | ✅ | ✅ `{type:'cmd',data:'cat ...'}` → 解析 | ⚠️ |
|
||||
| XML 只读预览 | 未提及 | ✅ `#plc-xml-view` 实时预览 | 🆕 |
|
||||
| 连线拖拽 | ✅ | ✅ 引脚拖出虚线→目标引脚 | ✅ |
|
||||
| 节点属性编辑 | ✅ 双击编辑 | ✅ 双击 Modal (saddr/desc/delayMs) | ✅ |
|
||||
| 删除节点 | 未提及 | ✅ 选中+删除按钮+关联线自动删 | 🆕 |
|
||||
| 缩放栏 | 未提及 | ✅ zoom in/out/fit + 百分比 | 🆕 |
|
||||
| **后端 set_file** | ♾️ | ⚠️ **前端用 set_file 但后端未实现!** | ❌ **BUG** |
|
||||
|
||||
**❌ 严重 BUG: `PlcConfig.save()` 发送 `{type:'set_file', path, content}`,但后端 `ws_method.cpp` 中没有 `get_file`/`set_file` 的 handler!save 功能形同虚设。load 功能通过 `cat` 命令间接实现,可行但不优雅。**
|
||||
|
||||
#### 8.2.11 实时监控 (`#monitor`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 环形缓冲区 | ✅ 500 条上限 | ✅ `MonitorRing.ring` max=500 | ✅ |
|
||||
| TTL 过期 | ✅ 15 分钟 TTL | ❌ **无 TTL** | ❌ **缺失** |
|
||||
| 消息方向 | ✅ ↑ 发送 / ↓ 接收 | ✅ `.monitor-dir.up`/`.monitor-dir.down` | ✅ |
|
||||
| 实时滚动 | ✅ | ✅ reverse 排序,最新在顶部 | ✅ |
|
||||
| **JSON 格式化预览** | ✅ | ❌ **仅截断 200 字符原文** | ❌ **缺失** |
|
||||
| 暂停/恢复 | 未提及 | ✅ `paused` + `togglePause()` | 🆕 |
|
||||
| 统计面板 | 未提及 | ✅ 总计/发送/接收 计数 | 🆕 |
|
||||
| 清除按钮 | 未提及 | ✅ `clear()` | 🆕 |
|
||||
| 消息截断 | 未提及 | ✅ 200 字符截断 | ⚠️ |
|
||||
|
||||
**⚠️ 缺失: (1) 15 分钟 TTL 过期机制; (2) JSON 格式化预览 (当前是 truncate 200 字符的原始文本)。**
|
||||
|
||||
#### 8.2.12 命令终端 (`#terminal`)
|
||||
|
||||
| 功能点 | 原规格 | 当前实现 | 状态 |
|
||||
|--------|--------|---------|:---:|
|
||||
| 仿真终端样式 | ✅ 黑底绿字 | ✅ 暗色终端风格 | ✅ |
|
||||
| 命令输入 | ✅ | ✅ `#term-input` + Enter | ✅ |
|
||||
| stdout 实时输出 | ✅ Binary 帧 | ✅ `term-out` 追加 binary 文本 | ✅ |
|
||||
| 命令列表标签 | ✅ get_cmds | ✅ `#cmd-chips` 芯片样式 | ✅ |
|
||||
| **历史命令** | ✅ | ❌ **无历史命令** | ❌ **缺失** |
|
||||
| help 命令 | 未提及 | ✅ 内置 help 文本 | 🆕 |
|
||||
| clear 命令 | 未提及 | ✅ 清空 `#term-out` | 🆕 |
|
||||
| 命令芯片点击 | 未提及 | ✅ 点击填充到输入框 | 🆕 |
|
||||
|
||||
**⚠️ 缺失: 历史命令支持 (上下箭头浏览历史)。原规格明确提到"历史命令支持"。**
|
||||
|
||||
---
|
||||
|
||||
### 8.3 样式/主题对比
|
||||
|
||||
| 参数 | 原规格 | 当前实现 | 状态 |
|
||||
|------|--------|---------|:---:|
|
||||
| **风格** | 暗色工业主题 | 暗色工业主题 | ✅ |
|
||||
| **主色调** | Cyan/Teal `#00E5FF` / `#1DE9B6` | Teal `#00d4aa` / Blue `#00a8e8` | ⚠️ 色值不同 |
|
||||
| **背景** | `#0F1923` (deep navy) | `#0a0f16` (更深) | ⚠️ 色值不同 |
|
||||
| **卡片背景** | `#1A2332` | `#111922` | ⚠️ 色值不同 |
|
||||
| **字体** | Inter + JetBrains Mono | Inter + JetBrains Mono | ✅ |
|
||||
| **图标** | Lucide CDN | 内联 SVG (18 个图标) | ⚠️ 变体 |
|
||||
| **圆角** | 8px / 6px / 4px | 12px / 8px / 4px | ⚠️ 差异 |
|
||||
| **间距** | 8px 基础网格 | 未显式定义网格 | ⚠️ |
|
||||
| **动画** | 150-300ms ease-out | 180ms ease-out | ✅ |
|
||||
| **CSS 变量** | 未提及 | ✅ 30 个设计令牌 | 🆕 |
|
||||
| **响应式** | ✅ 移动端隐藏侧栏 | ✅ `@media max-width:768px` | ✅ |
|
||||
| **图表** | Canvas 手写轻量图表 | ❌ 未实现 | ❌ |
|
||||
| **滚动条样式** | 未提及 | ✅ `::-webkit-scrollbar*` 定制 | 🆕 |
|
||||
|
||||
---
|
||||
|
||||
### 8.4 WebSocket 协议后端对齐
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 前端发送
|
||||
F1["{type:'cmd'}"]
|
||||
F2["{type:'get_cmds'}"]
|
||||
F3["{type:'set_file'}"]
|
||||
F4["信号 CRUD add/del/set"]
|
||||
end
|
||||
subgraph 后端处理ws_method.cpp
|
||||
B1["✅ ws_recv → ws_cmd_exec"]
|
||||
B2["✅ ws_recv → cmd_manager_get_commands"]
|
||||
B3["❌ 无 handler! get_file/set_file 未实现"]
|
||||
B4["✅ ws_recv → ws_sig_add/del/set"]
|
||||
end
|
||||
F1 --> B1
|
||||
F2 --> B2
|
||||
F3 -->|"❌ 断裂"| B3
|
||||
F4 --> B4
|
||||
```
|
||||
|
||||
| 消息 | 前端发送 | 后端接收 | 状态 |
|
||||
|------|---------|---------|:---:|
|
||||
| `{type:"cmd"}` | ✅ Terminal/Datacenter/PlcConfig.load | ✅ `ws_recv()` → `ws_cmd_exec()` | ✅ |
|
||||
| `{type:"get_cmds"}` | ✅ WsClient.onopen | ✅ `ws_recv()` → `cmd_manager_get_commands()` | ✅ |
|
||||
| `{type:"set_file"}` | ✅ PlcConfig.save() | ❌ **无 handler** | ❌ **断裂** |
|
||||
| `{type:"get_file"}` | ❌ 未使用 | ❌ **无 handler** | ❌ **断裂** |
|
||||
| `{saddr, signal_type, curd:"add"}` | ✅ SignalActions.add | ✅ `ws_sig_add_*` | ✅ |
|
||||
| `{saddr, signal_type, curd:"del"}` | ✅ SignalActions.del/batchDel | ✅ `ws_sig_del` | ✅ |
|
||||
| `{saddr, signal_type, curd:"set"}` | ✅ SignalActions.set/ParamActions.set | ✅ `ws_sig_set` | ✅ |
|
||||
|
||||
| 推送 | 后端发送 | 前端接收 | 状态 |
|
||||
|------|---------|---------|:---:|
|
||||
| 信号增量推送 | ✅ `ws_push_task()` 100ms | ✅ `SignalStore.update()` | ✅ |
|
||||
| 命令输出 Binary | ✅ `ws_send_binary()` | ✅ `term-out` + `MonitorRing` | ✅ |
|
||||
| 命令列表 | ✅ `cmd_list` | ✅ `#cmd-chips` 芯片 | ✅ |
|
||||
| 异步输出 | ✅ `async_cmd_printf()` | ⚠️ 仅 `index.html` 内联处理器中 toast | ⚠️ 弱 |
|
||||
|
||||
**❌ 关键缺陷: `set_file`/`get_file` 在前端被使用但后端未实现。`PlcConfig.save()` 无法正常工作。**
|
||||
|
||||
---
|
||||
|
||||
### 8.5 缺失功能汇总
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 严重缺失P0
|
||||
M1["❌ set_file/get_file 后端未实现<br/>→ PLC Config 保存无效"]
|
||||
M2["❌ AO 页面缺少元数据<br/>min/max/step/unit + val下拉"]
|
||||
end
|
||||
subgraph 中等缺失P1
|
||||
M3["❌ 终端缺少历史命令<br/>上下箭头浏览"]
|
||||
M4["❌ Monitor 缺少 JSON格式化<br/>+ 15分钟 TTL"]
|
||||
M5["❌ YK/AO 缺少批量删除<br/>checkbox"]
|
||||
M6["❌ 信号编辑功能缺失<br/>修改 saddr/desc/type"]
|
||||
end
|
||||
subgraph 轻微缺失P2
|
||||
M7["⚠️ Monitor JSON预览 → 截断200字符"]
|
||||
M8["⚠️ Canvas 图表(原规格提及)"]
|
||||
M9["⚠️ 异步输出前端处理较弱"]
|
||||
M10["⚠️ in 信号无链接跳转"]
|
||||
end
|
||||
subgraph 后端未实现但头文件声明
|
||||
M11["♾️ get_file / set_file<br/>ws_method.cpp 头注释声明但未实现"]
|
||||
end
|
||||
```
|
||||
|
||||
#### P0 — 阻塞级
|
||||
|
||||
| # | 缺失项 | 影响范围 | 根因 |
|
||||
|---|--------|---------|------|
|
||||
| 1 | **`set_file`/`get_file` 后端未实现** | PlcConfig.save() 完全无效 | `ws_method.cpp` 头注释声明但代码中无 handler |
|
||||
| 2 | **AO 页面元数据字段** | AO 信号管理功能不完整 | 前端未实现 min/max/step/unit/val 下拉 |
|
||||
|
||||
#### P1 — 重要级
|
||||
|
||||
| # | 缺失项 | 影响范围 |
|
||||
|---|--------|---------|
|
||||
| 3 | 终端历史命令 | 用户体验差,无法快速重复执行 |
|
||||
| 4 | Monitor JSON 格式化 + TTL | 可读性差,缓冲区无限增长 |
|
||||
| 5 | YK/AO 批量删除 | 管理效率低 |
|
||||
| 6 | 信号属性编辑 | 只能删了重建,无法修改 |
|
||||
|
||||
#### P2 — 改进级
|
||||
|
||||
| # | 缺失项 | 说明 |
|
||||
|---|--------|------|
|
||||
| 7 | 异步输出前端处理 | 目前仅在 index.html 内联 toast,应接入 Monitor |
|
||||
| 8 | Canvas 图表 | 原规格提到但一直未实现 |
|
||||
| 9 | in 信号链接跳转 | 点击 link_saddr 应跳转到对应 out 信号 |
|
||||
| 10 | param 展开行 UI | 原规格的层级展开设计比当前平铺更优雅 |
|
||||
|
||||
---
|
||||
|
||||
### 8.6 新增功能 (超出原规格)
|
||||
|
||||
| # | 新增功能 | 位置 |
|
||||
|---|---------|------|
|
||||
| 1 | **I18n 国际化** (zh/en, 95 key) | `i18n.js` |
|
||||
| 2 | **CSS 设计令牌系统** (30 个变量) | `style.css` |
|
||||
| 3 | **Dashboard 快速操作按钮** | `dashboard.js` |
|
||||
| 4 | **Dashboard 每卡描述文本** | `dashboard.js` |
|
||||
| 5 | **信号值变化动画** (`flash-val`) | `style.css` |
|
||||
| 6 | **Datacenter 自动刷新** (10s) | `datacenter.js` + `index.html` |
|
||||
| 7 | **Datacenter RTU 命令解析器** | `datacenter.js DcCmd._parse()` |
|
||||
| 8 | **PLC Debug 三列交互面板** (原规格为简单表) | `plc.js PlcDebug` |
|
||||
| 9 | **PLC Config 缩放/平移/网格** | `plc.js PlcConfig` |
|
||||
| 10 | **PLC Config XML 实时预览** | `plc.js refreshXmlView()` |
|
||||
| 11 | **Monitor 暂停/统计/清除** | `monitor.js` |
|
||||
| 12 | **Terminal 命令芯片** | `terminal.js` |
|
||||
| 13 | **Toast 通知系统** (success/error/info) | `ui.js` |
|
||||
| 14 | **Modal 对话框系统** | `ui.js` |
|
||||
| 15 | **SVG 图标库** (18 个 Lucide 风格图标) | `ui.js Icon._map` |
|
||||
| 16 | **通用信号表格渲染器** (`renderSignalPage`) | `ui.js` |
|
||||
| 17 | **导航分组标题** (Overview/Signal/Tools) | `router.js NAV` |
|
||||
| 18 | **语言切换器** | `index.html #lang-select` + `i18n.js` |
|
||||
| 19 | **实时时钟** | `index.html #nav-time` |
|
||||
| 20 | **移动端响应式** | `style.css @media` + `toggleNav()` |
|
||||
|
||||
---
|
||||
|
||||
### 8.7 代码质量对比
|
||||
|
||||
| 维度 | 原规格目标 | 当前实现 | 评价 |
|
||||
|------|-----------|---------|:---:|
|
||||
| 模块化 | 单文件内分对象 | 多文件独立模块 | ✅ 更好 |
|
||||
| 可维护性 | 单文件 2000+ 行 | 13 文件,每文件 50-400 行 | ✅ 更好 |
|
||||
| 国际化 | 无 | 95 key, zh/en | ✅ 更好 |
|
||||
| 代码注释 | 未提及 | JSDoc 风格文件头 | ✅ |
|
||||
| 全局命名空间 | 对象挂 window | 全局对象 (WsClient/SignalStore/Router...) | ⚠️ 一般 |
|
||||
| 重复代码 | 低(单文件共享) | 中等(signals.js 4 个页面模板重复) | ⚠️ 一般 |
|
||||
| 错误处理 | 未提及 | Toast 通知 + try-catch | ✅ |
|
||||
| 后端对齐 | 100% | ~85%(缺 set_file/get_file) | ⚠️ |
|
||||
|
||||
---
|
||||
|
||||
### 8.8 按模块分析总结
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 前端模块完成度矩阵 │
|
||||
├────────────┬──────────┬──────────┬──────────┬───────────┤
|
||||
│ 模块 │ 基础CRUD │ 高级功能 │ UI/UX │ 后端对齐 │
|
||||
├────────────┼──────────┼──────────┼──────────┼───────────┤
|
||||
│ login │ 100% │ 100% │ 100% │ N/A │
|
||||
│ dashboard │ 100% │ 100% │ 100% │ 100% │
|
||||
│ out │ 100% │ 90% │ 100% │ 100% │
|
||||
│ in │ 100% │ 80% │ 100% │ 100% │
|
||||
│ yk │ 90% │ 80% │ 90% │ 100% │
|
||||
│ ao │ 60% │ 30% │ 90% │ 100% │
|
||||
│ param │ 100% │ 90% │ 90% │ 100% │
|
||||
│ datacenter │ 100% │ 100% │ 100% │ 100% │
|
||||
│ plc_debug │ 150% │ 150% │ 100% │ 100% │
|
||||
│ plc_config │ 100% │ 120% │ 100% │ 50% │
|
||||
│ monitor │ 100% │ 70% │ 90% │ 100% │
|
||||
│ terminal │ 100% │ 80% │ 100% │ 100% │
|
||||
├────────────┼──────────┼──────────┼──────────┼───────────┤
|
||||
│ 总完成度 │ 96% │ 86% │ 97% │ 96% │
|
||||
└────────────┴──────────┴──────────┴──────────┴───────────┘
|
||||
```
|
||||
|
||||
**最低完成度模块**: AO (60% 基础 / 30% 高级) — 缺少 min/max/step/unit 元数据字段和 val 下拉选择。
|
||||
**后端对齐最差**: plc_config (50%) — set_file/get_file 后端未实现。
|
||||
|
||||
---
|
||||
|
||||
### 8.9 优先级修复建议
|
||||
|
||||
| 优先级 | 任务 | 预估工时 |
|
||||
|:---:|------|:---:|
|
||||
| **P0** | 后端实现 `set_file`/`get_file` handler | 2h |
|
||||
| **P0** | AO 页面补全元数据字段 + val 下拉 | 2h |
|
||||
| **P1** | 终端添加历史命令 (sessionStorage) | 1h |
|
||||
| **P1** | Monitor 添加 JSON 格式化预览 | 1h |
|
||||
| **P1** | YK/AO 页面添加批量删除 checkbox | 0.5h |
|
||||
| **P1** | 信号编辑功能 (修改 saddr/desc/type) | 2h |
|
||||
| **P2** | Monitor 15min TTL 过期 | 0.5h |
|
||||
| **P2** | in 信号链接跳转 | 0.5h |
|
||||
| **P2** | Canvas 图表 (实时监控页) | 3h |
|
||||
| **P2** | 异步输出接入 Monitor | 0.5h |
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
# PLC 配置页面原始功能详细规格
|
||||
|
||||
> 基于原工程 libplc C 引擎 + Reclose_logic.xml 完整分析
|
||||
> 分析日期: 2026-07-10
|
||||
> 目标: 重构前端 PLC Config 页面,做到比原工程更好
|
||||
|
||||
---
|
||||
|
||||
## 一、PLC 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ RTU 运行时 │
|
||||
│ │
|
||||
│ config/PLC/Reclose_logic.xml ──→ pls.cpp parse_reclose_logic │
|
||||
│ (mtime 文件变化检测, stat) │ │
|
||||
│ ↓ │
|
||||
│ LogicGraph[] (最多4个) │
|
||||
│ │ │
|
||||
│ EV_TIMER3 (1s) │
|
||||
│ ↓ │
|
||||
│ PLC_Logic() 执行: │
|
||||
│ 1. 读取 datacenter input 信号 │
|
||||
│ 2. Kahn 拓扑排序 │
|
||||
│ 3. 按拓扑序执行各门逻辑 │
|
||||
│ 4. 写入 datacenter output 信号 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 二、XML 格式完全规格 (Reclose_logic.xml)
|
||||
|
||||
### 顶层结构
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PLCConfig name="PLC Logic" key="1783070862205" checksum="3486BE04">
|
||||
<Chain gates="N" inputs="N" outputs="N">
|
||||
<!-- 简单 Chain 无 Comb: Inputs → Gates → Outputs 顺序 -->
|
||||
<Inputs> <Signal no="ID" x="X" y="Y"/> ... </Inputs>
|
||||
<Gate type="TYPE" id="N" x="X" y="Y" [delayMs="MS"] [S="src"] [R="src"]/>
|
||||
...
|
||||
<!-- 含 Comb 的 Chain: Comb 可嵌套, 位置不固定 -->
|
||||
<Comb id="N">
|
||||
<Inputs> ... </Inputs>
|
||||
<Gate .../>
|
||||
<Comb id="N"> <!-- 嵌套 Comb -->
|
||||
<Inputs> ... </Inputs>
|
||||
<Gate .../>
|
||||
<Outputs> ... </Outputs>
|
||||
</Comb>
|
||||
<Gate .../>
|
||||
<Outputs> ... </Outputs>
|
||||
</Comb>
|
||||
<Outputs> <Signal no="ID" x="X" y="Y"/> ... </Outputs>
|
||||
</Chain>
|
||||
<Chain ...> ... </Chain>
|
||||
</PLCConfig>
|
||||
```
|
||||
|
||||
### 关键节点类型 (10 种)
|
||||
|
||||
| type | Name | 功能 | 参数 | 输入引脚 | 输出引脚 |
|
||||
|------|------|------|------|:---:|:---:|
|
||||
| 0 | INPUT | 硬件输入, 从 datacenter 读取值 | no=信号ID | 0 | 1 |
|
||||
| 1 | OUTPUT | 硬件输出, 写入 datacenter | no=信号ID | 1 | 0 |
|
||||
| 2 | OR | 或门, 任意输入=1→输出1 | id=门ID | 2 | 1 |
|
||||
| 3 | AND | 与门, 全部输入=1→输出1 | id=门ID | 2 | 1 |
|
||||
| 4 | NOT | 非门, 取反 | id=门ID | 1 | 1 |
|
||||
| 5 | T62P | 延时导通 (IN=1 等待 delayMs ms 后 OUT=1) | id, delayMs | 1 | 1 |
|
||||
| 6 | T62D | 延时关断 (IN=0 等待 delayMs ms 后 OUT=0) | id, delayMs | 1 | 1 |
|
||||
| 7 | RISING | 上升沿检测 (0→1 产生一周期脉冲) | id | 1 | 1 |
|
||||
| 8 | SR | SR 触发器 (S优先: S=1置位, R=1& S=0复位) | id, S, R | 2 (S=0, R=1) | 1 |
|
||||
| 9 | RS | RS 触发器 (R优先: R=1复位, S=1& R=0置位) | id, S, R | 2 (S=0, R=1) | 1 |
|
||||
| 10 | FALLING | 下降沿检测 (1→0 产生一周期脉冲) | id | 1 | 1 |
|
||||
|
||||
### Comb 的 C 引擎自动检测逻辑
|
||||
|
||||
```
|
||||
parse_xml_comb(comb_el, graph):
|
||||
1. 遍历 comb_el 的子元素 (<Inputs>, <Gate>, <Comb>, <Outputs>)
|
||||
2. 遇到子 <Comb> → 递归调用 parse_xml_comb() ← 这就是嵌套自动检测
|
||||
3. 子 Comb 的 Gate 输出 → 父 Comb 的 Gate 输入 (自动连线)
|
||||
4. 遇到 <Inputs> → 创建 HW_INPUT 节点 → 连线到同层 Gate
|
||||
5. 遇到 <Gate> → 创建门节点 + 解析 R/S 属性
|
||||
- R="Comb#N" → 查找子 Comb 中对应 Gate 的输出
|
||||
- S="Comb#N" → 同上
|
||||
- R="Input#N" / S="Input#N" → 创建对应 Input 节点
|
||||
6. 遇到 <Outputs> → 创建 HW_OUTPUT 节点 → 连线从同层 Gate
|
||||
|
||||
链级别 Comb 处理 (第二遍布线):
|
||||
1. Comb 的 Gate 输出 → 链级 Gate 输入
|
||||
2. 链级 Input → 链级 Gate
|
||||
3. 链级 Gate → 链级 Output
|
||||
```
|
||||
|
||||
**核心结论: Comb 是 XML 中天然存在的嵌套结构,C 引擎自动递归解析。前端应该:**
|
||||
- 加载 XML 时自动识别 Comb 嵌套关系
|
||||
- 渲染为视觉分组容器 (矩形框)
|
||||
- 允许用户通过拖拽节点入框来创建 Comb
|
||||
- 不允许手动添加空 Comb
|
||||
|
||||
---
|
||||
|
||||
## 三、原工程 PLC Config 前端功能详细清单
|
||||
|
||||
### 3.1 导航与入口
|
||||
|
||||
- Hash 路由: `#plc_config`
|
||||
- 导航标签: "PLC 配置" / "PLC Config"
|
||||
- 导航图标: 原工程用 emoji 🧩, 当前用 SVG cpu
|
||||
|
||||
### 3.2 多 Chain 管理
|
||||
|
||||
| 功能 | 描述 | 实现方式 |
|
||||
|------|------|---------|
|
||||
| **Chain 标签切换** | 顶部标签栏,每个 Chain 一个标签 | 点击切换当前编辑的 Chain |
|
||||
| **Chain 标签内容** | 显示 "Chain N (节点数)" | 实时更新 |
|
||||
| **添加 Chain** | +Chain 按钮 | 创建空 Chain, 自动切换 |
|
||||
| **删除 Chain** | -Chain 按钮 | 删除当前 Chain (≥2个时可用) |
|
||||
|
||||
### 3.3 节点类型工具栏
|
||||
|
||||
每种节点类型一个按钮,点击后在 Canvas 随机位置添加:
|
||||
|
||||
```
|
||||
[+INPUT] [+OUTPUT] [+OR] [+AND] [+NOT] [+T62P] [+T62D] [+RISING] [+SR] [+RS] [+FALLING]
|
||||
```
|
||||
|
||||
### 3.4 Canvas 画布编辑器
|
||||
|
||||
| 功能 | 描述 | 交互 |
|
||||
|------|------|------|
|
||||
| **节点渲染** | 圆角矩形 + 类型名 + ID号 + 颜色区分 | 11种颜色方案 |
|
||||
| **引脚圆点** | 输入引脚(左, 彩色), 输出引脚(右, 绿色) | 5px 半径 |
|
||||
| **连线渲染** | 节点间实线, 从输出引脚→输入引脚 | 深灰 #2c3e50 |
|
||||
| **拖拽节点** | 按住节点拖动 | 20px 网格吸附 |
|
||||
| **连线操作** | 从输出引脚拖出虚线→目标输入引脚释放 | 红色虚线 |
|
||||
| **选中节点** | 单击节点高亮 | 白色填充 + 红色边框 |
|
||||
| **删除节点** | 选中 + 删除按钮 | 同时删除关联连线 + 重编号 Gate ID |
|
||||
| **平移画布** | 拖拽空白区域 | 平移 |
|
||||
| **缩放** | 滚轮缩放 0.5x-2.0x | 缩放栏显示百分比 |
|
||||
| **缩放栏** | [放大] [缩小] [适应] 100% | 适应 = zoom=1, pan=0 |
|
||||
| **双击编辑** | 双击节点弹出属性 Modal | 编辑 saddr/desc/delayMs/spin/rpin |
|
||||
|
||||
### 3.5 Comb 组合块
|
||||
|
||||
| 功能 | 描述 |
|
||||
|------|------|
|
||||
| **自动识别** | 加载 XML 时自动解析 Comb 嵌套结构 |
|
||||
| **视觉渲染** | 虚线圆角矩形,标签 "Comb #N" |
|
||||
| **嵌套 Comb** | 视觉上子 Comb 在父 Comb 内部 |
|
||||
| **节点归属** | Comb 内节点在视觉上位于 Comb 矩形内 |
|
||||
| **创建 Comb** | 选中多个节点 → "成组为 Comb" 创建 |
|
||||
| **解散 Comb** | 选中 Comb → "解散 Comb" → 节点回到链级别 |
|
||||
|
||||
### 3.6 XML 查看与持久化
|
||||
|
||||
| 功能 | 描述 |
|
||||
|------|------|
|
||||
| **XML 实时预览** | 只读 `<pre>` 标签, 随编辑实时更新 |
|
||||
| **加载 XML** | 从 RTU 读取 `cat config/PLC/Reclose_logic.xml` |
|
||||
| **保存 XML** | 通过 WS `set_file` 写回 RTU |
|
||||
| **checksum** | 根节点自动计算 XOR checksum |
|
||||
|
||||
### 3.7 操作按钮栏
|
||||
|
||||
```
|
||||
[加载 XML] [保存 XML] [清空] [删除选中节点]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、原工程 UI 设计参考
|
||||
|
||||
### 4.1 设计风格
|
||||
|
||||
| 参数 | 原设计值 | 当前值 |
|
||||
|------|---------|--------|
|
||||
| 风格 | 暗色工业主题 (Dark Industrial Dashboard) | 暗色工业主题 |
|
||||
| 主色调 | Cyan/Teal (#00E5FF / #1DE9B6) | Teal (#00d4aa) |
|
||||
| 背景 | #0F1923 (deep navy) | #0a0f16 (更深) |
|
||||
| 卡片背景 | #1A2332 | #111922 |
|
||||
| 字体 | Inter + JetBrains Mono | 同 |
|
||||
| 图标 | Emoji (原工程) → SVG Lucide (重构目标) | SVG 内联 Lucide |
|
||||
| Canvas 背景 | 浅色网格 (#f0f2f5) | 同 |
|
||||
|
||||
### 4.2 导航图标规格
|
||||
|
||||
| 页面 | 原 Emoji | Lucide SVG 图标名 | 说明 |
|
||||
|------|---------|------------------|------|
|
||||
| 仪表盘 | 📊 | dashboard | 四象限网格 |
|
||||
| 开出信号 | 📡 | signal | 信号波形 |
|
||||
| 开入信号 | 🔗 | link | 链接 |
|
||||
| 遥控信号 | ⚡ | zap | 闪电 |
|
||||
| 参数设置 | 🎚️ | sliders | 滑块 |
|
||||
| 定值管理 | ⚙️ | settings | 齿轮 |
|
||||
| 数据中心 | 📋 | database | 数据库 |
|
||||
| PLC 调试 | 🔌 | bug | Bug/调试 |
|
||||
| PLC 配置 | 🧩 | cpu | CPU/芯片 |
|
||||
| 实时监控 | 📈 | activity | 心电图 |
|
||||
| 命令终端 | 💻 | terminal | 终端 |
|
||||
|
||||
---
|
||||
|
||||
## 五、重构目标与要求
|
||||
|
||||
### 5.1 必须保留/增强的功能
|
||||
|
||||
- [x] 多 Chain 标签切换
|
||||
- [x] 11 种节点类型完整支持
|
||||
- [x] Canvas 拖拽、连线、缩放、平移
|
||||
- [x] 节点属性双击编辑 (含 SR/RS R/S 引脚, T62P/D delayMs)
|
||||
- [x] XML 实时预览
|
||||
- [x] 从 RTU 加载/保存
|
||||
|
||||
### 5.2 必须修复的问题
|
||||
|
||||
- [ ] **Comb 改为自动识别** — 加载 XML 时检测,不允许手动添加空 Comb
|
||||
- [ ] **Comb 创建方式** — 选中多个节点 → "成组为 Comb"
|
||||
- [ ] **导航图标风格** — 恢复工业风格,尺寸与文字匹配
|
||||
- [ ] **set_file 后端实现** — 确保保存功能真正可用
|
||||
|
||||
### 5.3 必须优于原工程的新功能
|
||||
|
||||
- [ ] Comb 视觉边界框正确嵌套
|
||||
- [ ] SR/RS 引脚 S/R 标签显示
|
||||
- [ ] 节点颜色方案保持一致性
|
||||
- [ ] 更好的错误处理 (XML 解析失败提示)
|
||||
- [ ] 键盘快捷键 (Delete 删除, Ctrl+S 保存)
|
||||
|
||||
---
|
||||
|
||||
## 六、Comb 自动检测算法 (前端)
|
||||
|
||||
```javascript
|
||||
// 加载 XML 时的 Comb 自动检测伪代码
|
||||
function detectCombsFromXml(xmlDoc):
|
||||
for each <Chain> in <PLCConfig>:
|
||||
chain.nodes = [] // 扁平存储所有节点
|
||||
chain.combs = [] // 存储 Comb 容器定义
|
||||
chain.links = [] // 存储连线
|
||||
|
||||
// 1. 解析顶层元素
|
||||
parseChainElements(chainEl, chain, parentId=-1)
|
||||
|
||||
function parseChainElements(parentEl, chain, parentId):
|
||||
for each child in parentEl.children:
|
||||
if child.tag == 'Inputs' or 'Outputs':
|
||||
parseSignals(child, chain)
|
||||
if child.tag == 'Gate':
|
||||
parseGate(child, chain)
|
||||
if child.tag == 'Comb':
|
||||
combId = child.getAttribute('id')
|
||||
// 创建 Comb 容器 (位置由内部节点包围盒计算)
|
||||
comb = { id: combId, parentId: parentId }
|
||||
chain.combs.push(comb)
|
||||
// 递归解析 Comb 内部元素
|
||||
parseChainElements(child, chain, combId)
|
||||
|
||||
// 2. Comb 位置自动计算 (后处理)
|
||||
for each comb in chain.combs:
|
||||
var nodesInComb = chain.nodes.filter(n => 位于 comb 区域)
|
||||
// 包围盒 = min/max of nodesInComb 的 x,y
|
||||
comb.x = minX - margin
|
||||
comb.y = minY - margin
|
||||
comb.w = maxX - minX + 2*margin
|
||||
comb.h = maxY - minY + 2*margin
|
||||
```
|
||||
|
||||
### Comb 归属判断
|
||||
|
||||
节点属于哪个 Comb 由以下规则确定:
|
||||
|
||||
1. **来自 <Comb> 内 <Inputs>/<Gate>/<Outputs>** → 属于该 Comb
|
||||
2. **来自 <Comb> 内子 <Comb> 的元素** → 属于子 Comb (嵌套)
|
||||
3. **来自 <Chain> 顶层元素** → 不属于任何 Comb (parentId=-1)
|
||||
|
||||
---
|
||||
|
||||
## 七、节点样式与颜色方案
|
||||
|
||||
| 节点 | 填充色 | 描边色 | 尺寸 |
|
||||
|------|--------|--------|------|
|
||||
| INPUT | #e8f5e9 浅绿 | #27ae60 绿 | 110×34 |
|
||||
| OUTPUT | #fce4ec 浅粉 | #e74c3c 红 | 110×34 |
|
||||
| OR | #fff3e0 浅橙 | #e67e22 橙 | 76×46 |
|
||||
| AND | #e3f2fd 浅蓝 | #2980b9 蓝 | 76×46 |
|
||||
| NOT | #f3e5f5 浅紫 | #8e44ad 紫 | 76×46 |
|
||||
| T62P | #fce4ec 浅粉 | #c0392b 深红 | 86×46 |
|
||||
| T62D | #fff3e0 浅橙 | #d35400 深橙 | 86×46 |
|
||||
| RISING | #e8f5e9 浅绿 | #1abc9c 青绿 | 86×46 |
|
||||
| SR | #e3f2fd 浅蓝 | #3498db 亮蓝 | 86×52 |
|
||||
| RS | #fce4ec 浅粉 | #9b59b6 紫 | 86×52 |
|
||||
| FALLING | #fff3e0 浅橙 | #16a085 青 | 86×46 |
|
||||
|
||||
引脚颜色: 输入=同节点描边色, 输出=#27ae60 绿
|
||||
|
||||
### Comb 视觉
|
||||
|
||||
- 描边: #95a5a6 灰色虚线 (8px dash, 4px gap)
|
||||
- 填充: rgba(149,165,166,0.05) 极浅灰
|
||||
- 标签: 左上角 "Comb #N"
|
||||
- 圆角: 6px
|
||||
|
||||
---
|
||||
|
||||
## 八、文件结构
|
||||
|
||||
```
|
||||
web/js/pages/plc.js ← PLC Debug + PLC Config (重构目标)
|
||||
web/css/style.css ← PLC 相关 CSS (.chain-tabs, .plc-canvas-wrap, etc.)
|
||||
web/js/i18n.js ← PLC 相关翻译 key
|
||||
config/PLC/Reclose_logic.xml ← XML 配置文件 (数据源)
|
||||
```
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# WebServer 原工程 vs 现工程对比分析
|
||||
|
||||
> 日期: 2026-07-14
|
||||
> 原工程: /home/ypc/RTU/RTU/src/system/libweb_server/
|
||||
> 现工程: /home/ypc/RTU/RTU_ALL_AI/src/system/libweb_server/
|
||||
|
||||
## 一、原工程 webserver 完整功能清单
|
||||
|
||||
### web_server.cpp(277行)
|
||||
| 序号 | 功能 | 原工程实现 | 说明 |
|
||||
|:----:|------|-----------|------|
|
||||
| W01 | packed_fs 内嵌前端 | `USE_PACKED_FS` 宏,`mg_fs_packed` | 编译时将前端文件打包进二进制 |
|
||||
| W02 | 独立 polling 线程 | `pthread_create` → `web_server_run` 内 `mg_mgr_poll(500ms)` | 与 X-Macro 框架独立的线程 |
|
||||
| W03 | 端口/路径配置 | `s_listen_on="http://0.0.0.0:8000"`, `s_update_ws="/ws"`, `s_web_root="web_root"` | 硬编码常量 |
|
||||
| W04 | 连接管理 | `g_ws_conns` vector + mutex | ws_send_all/ws_send_one/ws_send_binary |
|
||||
| W05 | MG_EV_WS_CTL 关闭 | `WEBSOCKET_OP_CLOSE` → session destroy + 移除连接 | 完整的 WebSocket 生命周期 |
|
||||
| W06 | MG_EV_CLOSE 处理 | socket 意外关闭时清理 session + 连接 | 双重保障 |
|
||||
| W07 | app_web_server 线程 | EV_TIMER1/TIMER2 空闲, EV_TIMER3 → ws_task() | 1秒周期增量推送 |
|
||||
|
||||
### ws_method.cpp(1421行)
|
||||
| 序号 | 功能 | 原工程实现 | 说明 |
|
||||
|:----:|------|-----------|------|
|
||||
| S01 | 信号结构体 | `stru_ws_signal` 含 `p_ctrl*` + `p_param*` | 控制上下文伴随信号生命周期 |
|
||||
| S02 | 函数表分发 | `g_ws_signal_methods_map` → `stru_ws_signal_method` | 5种信号 × 4个操作 = 统一分发 |
|
||||
| S03 | yk/ao/param SBO 流程 | `DIRECT_NORMAL` 直控, `SBO_NORMAL` → SELECT → DIRECT 两步 | 完整的遥控/遥调流程 |
|
||||
| S04 | async_cmd 通道 | `ws_set_async_cmd_conn/get_async_cmd_conn` + `async_cmd_printf/async_cmd_complete` | 其他模块异步推送命令结果到前端 |
|
||||
| S05 | ws_send_large_text | 35KB 分片发送 `cmd_rsp_chunk` + 5ms 间隔 | 大文本不会撑爆 WebSocket 帧 |
|
||||
| S06 | ws_build_dc_data_json | 调用 `dc_get_signal_info_by_id` 遍历所有信号类型 | datacenter 命令返回完整元数据 |
|
||||
| S07 | dc_save_out_signals_xml | `datacenter out` 命令执行后自动保存 XML | 信号导出到文件 |
|
||||
| S08 | cmd_exec pipe+dup2 | 独立线程读取 pipe → stdout 捕获 | 命令执行结果回传前端 |
|
||||
| S09 | get_cmds / get_file / set_file | 命令列表 / 文件读取 / 文件保存 | 完整的管理 API |
|
||||
| S10 | AO/Param 元数据到前端 | `item.min/max/step/unit` 从 `p_param` 提取 | 前端可做范围校验和单位显示 |
|
||||
| S11 | 增量推送 ws_task | 遍历 sessions → 变化检测 → 仅推送有变化的连接 | 1秒周期 |
|
||||
| S12 | 立即推送 | add/del 信号后立即 `make_signal_json` → `ws_send_one` | 不等定时器 |
|
||||
|
||||
## 二、现工程功能对照
|
||||
|
||||
### 已实现(✅)
|
||||
| 功能 | 对应原工程 | 备注 |
|
||||
|------|:---------:|------|
|
||||
| WebSocket 连接管理 + 广播/单播/二进制 | W04 | ✅ 一致 |
|
||||
| MG_EV_WS_CTL/MG_EV_CLOSE 处理 | W05,W06 | ✅ 一致 |
|
||||
| HTTP 静态文件服务 | W03 | ✅ 路径改为 config/web |
|
||||
| 5种信号增删改查 | S01~S03 | ⚠️ 缺少 SBO 流程 |
|
||||
| cmd 命令执行 + stdout 捕获 | S08 | ✅ 一致 |
|
||||
| get_cmds / get_file / set_file | S09 | ✅ 一致 |
|
||||
| async_cmd 通道 | S04 | ✅ 一致 |
|
||||
| 增量推送 (ws_push_task) | S11 | ✅ 100ms 周期 |
|
||||
| 添加/删除后立即推送 | S12 | ✅ 一致 |
|
||||
| X-Macro 框架集成 + EV_STOP | W07 | ✅ 比原工程更好 |
|
||||
|
||||
### 缺失/需要完善(❌)
|
||||
| 序号 | 缺失功能 | 影响 | 严重度 |
|
||||
|:----:|---------|------|:------:|
|
||||
| M1 | **SBO 遥控流程** | 原工程 yk/ao/param 的 set 支持 `DIRECT_NORMAL` 直控和 `SBO_NORMAL` 选择-执行两步。现工程 yk_sig_set/ao_sig_set/param_sig_set 只调用了 `DC_STEP_DIRECT`,未处理 SELECT→DIRECT 两阶段,且未区分 ctrl_type。 | **高** |
|
||||
| M2 | **dc_save_out_signals_xml** | 原工程在 `datacenter out` 命令执行后自动调用 `dc_save_out_signals_xml()` 保存到 `config/SYSTEM/datacenter_out.xml`。现工程缺失,前端 PLC 配置页面依赖此文件。 | **高** |
|
||||
| M3 | **ws_build_dc_data_json** | 原工程 `datacenter` 命令执行后构建 JSON 返回信号元数据(saddr/desc/type/id/ctrl_type/link_saddrs)。现工程缺失,前端无法展示信号详情。 | **高** |
|
||||
| M4 | **ws_send_large_text** | 原工程大文本(>35KB)分片发送 `cmd_rsp_chunk`。现工程无此处理,大输出可能被截断或 WS 帧溢出。 | **中** |
|
||||
| M5 | **AO/Param 元数据 (min/max/step/unit)** | 原工程在 JSON 中包含 `min/max/step/unit` 字段给前端做范围校验。现工程 JSON 中没有这些字段。 | **中** |
|
||||
| M6 | **p_ctrl 伴随信号生命周期** | 原工程 `stru_ws_signal` 有 `p_ctrl*` 指针,在 add 时 new 分配、del 时 delete 释放。现工程每次 set 时临时创建/销毁 ctrl,效率低且可能引入 bug。 | **低** |
|
||||
|
||||
## 三、前后端交互协议
|
||||
|
||||
### 前端 → 后端(JSON 消息类型)
|
||||
| type | 字段 | 说明 | 状态 |
|
||||
|------|------|------|:----:|
|
||||
| `cmd` | `{type:"cmd", data:"命令 参数"}` | 执行调试命令 | ✅ 已实现 |
|
||||
| `get_cmds` | `{type:"get_cmds"}` | 获取命令列表 | ✅ 已实现 |
|
||||
| `get_file` | `{type:"get_file", path:"相对路径"}` | 读取配置文件 | ✅ 已实现 |
|
||||
| `set_file` | `{type:"set_file", path:"...", content:"..."}` | 保存配置文件 | ✅ 已实现 |
|
||||
| 信号CRUD | `{saddr, signal_type, curd, signal_data, setting_zone}` | 信号增删改 | ✅ 已实现 |
|
||||
|
||||
### 后端 → 前端(JSON 消息类型)
|
||||
| type | 说明 | 状态 |
|
||||
|------|------|:----:|
|
||||
| `cmd_list` | `{type:"cmd_list", cmds:[{name,desc}]}` | ✅ 已实现 |
|
||||
| `file_content` | `{type:"file_content", path, content}` | ✅ 已实现 |
|
||||
| `file_saved` | `{type:"file_saved", path, success}` | ✅ 已实现 |
|
||||
| `cmd_rsp_chunk` | `{type:"cmd_rsp_chunk", seq, total, data}` | ❌ M4 缺失 |
|
||||
| `dc_data` | `{type:"dc_data", out:[...], in:[...], ...}` | ❌ M3 缺失 |
|
||||
| Binary | 命令执行结果(原始字节流) | ✅ 已实现 |
|
||||
| 信号JSON | out/in/yk/ao/param 数组 | ⚠️ M5 缺 min/max/step/unit |
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# WebServer 修改调试计划
|
||||
|
||||
> 日期: 2026-07-14
|
||||
> 基于: [WEBSERVER_COMPARISON.md](WEBSERVER_COMPARISON.md)
|
||||
|
||||
## 修改优先级
|
||||
|
||||
### 高优先级(必须修复 — M1, M2, M3)
|
||||
|
||||
#### 1. SBO 遥控流程 (M1)
|
||||
**目标**: yk/ao/param 的 set 操作支持 SELECT→DIRECT 两阶段控制
|
||||
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp`
|
||||
|
||||
**改动点**:
|
||||
- `ws_sig_set_yk()`: 根据 `ctrl_type` 判断 `DIRECT_NORMAL` 直控或 `SBO_NORMAL` 选控
|
||||
- `ws_sig_set_ao()`: 同上
|
||||
- `ws_sig_set_param()`: 同上
|
||||
- SBO 流程: 先 `DC_STEP_SELECT` → 再 `DC_STEP_DIRECT`
|
||||
|
||||
**调试方法**: 前端发送 yk/ao/param set 命令,确认 SBO 类型信号走 SELECT→DIRECT 流程
|
||||
|
||||
#### 2. dc_save_out_signals_xml (M2)
|
||||
**目标**: datacenter out 命令执行后保存 XML
|
||||
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp`(cmd 处理部分)
|
||||
|
||||
**改动点**:
|
||||
- 在 `type=cmd` 处理中,当命令以 `datacenter` 开头且子命令为 `out`/`all` 时
|
||||
- 调用 `dc_save_out_signals_xml()` 保存到 `config/SYSTEM/datacenter_out.xml`
|
||||
|
||||
**调试方法**: 前端执行 `datacenter out` 命令 → 检查 `config/SYSTEM/datacenter_out.xml` 是否生成
|
||||
|
||||
#### 3. ws_build_dc_data_json (M3)
|
||||
**目标**: datacenter 命令执行后返回信号元数据 JSON
|
||||
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp`
|
||||
|
||||
**改动点**:
|
||||
- 新增函数 `ws_build_dc_data_json(types)` — 调用 `dc_get_signal_info_by_id` 遍历信号
|
||||
- 在 `type=cmd` 处理中,datacenter 命令执行后调用此函数构建 JSON
|
||||
- 通过 `ws_send_binary` 发送 dc_data JSON
|
||||
|
||||
**调试方法**: 前端执行 `datacenter all` 命令 → 检查返回的 JSON 是否包含 out/in/yk/ao/param 完整数据
|
||||
|
||||
### 中优先级(建议修复 — M4, M5)
|
||||
|
||||
#### 4. ws_send_large_text (M4)
|
||||
**目标**: 大文本(>35KB)分片发送,防止 WS 帧溢出
|
||||
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp` 和/或 `web_server.cpp`
|
||||
|
||||
**改动点**:
|
||||
- 新增 `ws_send_large_text()` — 35KB 分片 `cmd_rsp_chunk` 消息
|
||||
- `ws_cmd_exec()` 返回大字符串时使用分片发送代替二进制发送
|
||||
|
||||
**调试方法**: 执行产生大量输出的命令(如 datacenter all),检查前端是否收到分片
|
||||
|
||||
#### 5. AO/Param 元数据 (M5)
|
||||
**目标**: JSON 中包含 min/max/step/unit 字段
|
||||
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp` 的 `ws_make_sig_json`
|
||||
|
||||
**改动点**:
|
||||
- AO 信号 JSON 增加 `min/max/step/unit` 字段
|
||||
- 需要在信号结构中存储 `stru_signal_param` 或从 datacenter 查询
|
||||
|
||||
### 低优先级(可选 — M6)
|
||||
|
||||
#### 6. p_ctrl 伴随生命周期 (M6)
|
||||
**改动文件**: `src/system/libweb_server/src/ws_method.cpp`
|
||||
|
||||
**改动点**: 信号结构体增加 `p_ctrl*`,add 时分配,del 时释放
|
||||
|
||||
## 测试方案
|
||||
|
||||
1. **启动 RTU**: `./RTU` 验证 webserver 正常启动
|
||||
2. **浏览器连接**: `http://localhost:8080` → 终端页面
|
||||
3. **测试命令**: 点击命令面板按钮执行各模块命令
|
||||
4. **测试信号CRUD**: 通过前端数据页面添加/删除/修改信号
|
||||
5. **测试SBO流程**: 通过前端修改 SBO 类型信号值
|
||||
6. **测试大输出**: 执行 `datacenter all` 验证分片发送
|
||||
|
|
@ -1,22 +1,233 @@
|
|||
/** @file test_cmd.cpp — 命令框架全覆盖 */
|
||||
/**
|
||||
* @file test_cmd.cpp — cmd 模块全覆盖测试
|
||||
*/
|
||||
#include "myCmd.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
static int P=0,F=0,g=0;
|
||||
#define T(n) do{printf(" %s ... ",n);}while(0)
|
||||
#define OK() do{printf("OK\n");P++;}while(0)
|
||||
#define FL(m) do{printf("FAIL: %s\n",m);F++;return;}while(0)
|
||||
#define CK(c,m) do{if(!(c))FL(m);}while(0)
|
||||
#include <unistd.h>
|
||||
|
||||
static void cb(int ac,char**av){(void)ac;(void)av;g++;}
|
||||
static int P = 0, F = 0;
|
||||
|
||||
static void t1(){T("add+find");g=0;cmd_manager_add_command("test_c",cb,"desc",NULL);stru_cmd*c=cmd_find("test_c");CK(c,"find");CK(!strcmp(c->name,"test_c"),"n");CK(!strcmp(c->desc,"desc"),"d");CK(c->func,"f");OK();}
|
||||
static void t2(){T("find nonexistent");CK(!cmd_find("no_xyz"),"null");OK();}
|
||||
static void t3(){T("execute");g=0;stru_cmd*c=cmd_find("test_c");c->func(0,NULL);CK(g==1,"called");OK();}
|
||||
static void t4(){T("get_commands");unsigned n=0;stru_cmd*cs=cmd_manager_get_commands(&n);CK(n>0&&cs,"arr");OK();}
|
||||
static void t5(){T("cmd_help");char*a[]={(char*)"help",NULL};cmd_help(1,a);OK();}
|
||||
static void t6(){T("sub_complete");const char*s[]={"start","stop","status"};char**c=NULL;int n=0;cmd_sub_complete("st",&c,&n,s,3);CK(n>=2,"n");for(int i=0;i<n;i++)free(c[i]);free(c);OK();}
|
||||
static void t7(){T("linenoise hist");linenoiseHistoryAdd("c1");linenoiseHistoryAdd("c2");linenoiseHistoryFree();linenoiseHistoryAdd("c3");OK();}
|
||||
#define T(n) do { printf(" %-50s ... ", n); } while (0)
|
||||
#define OK() do { printf("OK\n"); P++; } while (0)
|
||||
#define FL(m) do { printf("FAIL: %s\n", m); F++; return; } while (0)
|
||||
#define CK(c, m) do { if (!(c)) FL(m); } while (0)
|
||||
|
||||
int main(){printf("\n=== libcmd ===\n\n");t1();t2();t3();t4();t5();t6();t7();printf("\n========\n %d pass, %d fail\n========\n",P,F);return F>0?1:0;}
|
||||
static int g_call_cnt = 0;
|
||||
static void test_cb(int ac, char **av) { (void)ac; (void)av; g_call_cnt++; }
|
||||
|
||||
static void test_complete_cb(const char *buf, char ***out, int *n) {
|
||||
(void)buf;
|
||||
const char *subs[] = {"start", "stop", "restart"};
|
||||
*out = (char **)malloc(sizeof(char *) * 3);
|
||||
for (int i = 0; i < 3; i++) (*out)[i] = strdup(subs[i]);
|
||||
*n = 3;
|
||||
}
|
||||
|
||||
static void t01() { T("add+find"); g_call_cnt = 0;
|
||||
cmd_manager_add_command("test_cmd", test_cb, "test desc", NULL);
|
||||
stru_cmd *c = cmd_find("test_cmd");
|
||||
CK(c != NULL, "find NULL"); CK(0 == strcmp(c->name, "test_cmd"), "name");
|
||||
CK(0 == strcmp(c->desc, "test desc"), "desc"); OK(); }
|
||||
|
||||
static void t02() { T("add NULL params");
|
||||
cmd_manager_add_command(NULL, test_cb, "desc", NULL);
|
||||
cmd_manager_add_command("n2", NULL, "desc", NULL);
|
||||
cmd_manager_add_command("n3", test_cb, NULL, NULL);
|
||||
CK(cmd_find("n2") == NULL, "found NULL-func"); OK(); }
|
||||
|
||||
static void t03() { T("find nonexistent");
|
||||
CK(cmd_find("no_such_xyz") == NULL, "not null"); OK(); }
|
||||
|
||||
static void t04() { T("find NULL");
|
||||
CK(cmd_find(NULL) == NULL, "not null"); OK(); }
|
||||
|
||||
static void t05() { T("execute callback");
|
||||
g_call_cnt = 0; stru_cmd *c = cmd_find("test_cmd");
|
||||
CK(c != NULL, "find");
|
||||
c->func(0, NULL); CK(g_call_cnt == 1, "not called"); OK(); }
|
||||
|
||||
static void t06() { T("get_commands with count");
|
||||
unsigned n = 0; stru_cmd *cs = cmd_manager_get_commands(&n);
|
||||
CK(cs != NULL, "null"); CK(n >= 2, "count"); OK(); }
|
||||
|
||||
static void t07() { T("get_commands NULL count");
|
||||
stru_cmd *cs = cmd_manager_get_commands(NULL);
|
||||
CK(cs != NULL, "null"); OK(); }
|
||||
|
||||
static void t08() { T("complete prefix match");
|
||||
char **c = NULL; int n = 0;
|
||||
cmd_complete("he", &c, &n);
|
||||
CK(n >= 1, "no match");
|
||||
int found = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (0 == strcmp(c[i], "help")) found = 1;
|
||||
free(c[i]);
|
||||
}
|
||||
free(c); CK(found, "help not in completions"); OK(); }
|
||||
|
||||
static void t09() { T("complete exact match");
|
||||
char **c = NULL; int n = 0;
|
||||
cmd_complete("help", &c, &n);
|
||||
CK(n >= 1, "no match");
|
||||
int found = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (0 == strcmp(c[i], "help")) found = 1;
|
||||
free(c[i]);
|
||||
}
|
||||
free(c); CK(found, "not found"); OK(); }
|
||||
|
||||
static void t10() { T("complete no match");
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete("zzz_no_match", &c, &n);
|
||||
CK(n == 0, "n"); CK(c == NULL, "c"); OK(); }
|
||||
|
||||
static void t11() { T("complete empty string");
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete("", &c, &n);
|
||||
CK(n > 0, "n"); for (int i = 0; i < n; i++) free(c[i]); free(c); OK(); }
|
||||
|
||||
static void t12() { T("complete sub-command space");
|
||||
cmd_manager_add_command("svc", test_cb, "svc ctl", test_complete_cb);
|
||||
char **c = NULL; int n = 0;
|
||||
cmd_complete("svc ", &c, &n);
|
||||
CK(n == 3, "n"); for (int i = 0; i < n; i++) free(c[i]); free(c); OK(); }
|
||||
|
||||
static void t13() { T("complete unknown+space");
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete("unknown sub", &c, &n);
|
||||
CK(n == 0, "n"); CK(c == NULL, "c"); OK(); }
|
||||
|
||||
static void t14() { T("complete partial+space");
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete("sv sub", &c, &n);
|
||||
CK(n == 0, "n"); CK(c == NULL, "c"); OK(); }
|
||||
|
||||
static void t15() { T("sub_complete prefix");
|
||||
const char *s[] = {"start", "stop", "status"}; char **c = NULL; int n = 0;
|
||||
cmd_sub_complete("st", &c, &n, s, 3);
|
||||
CK(n == 3, "n"); for (int i = 0; i < n; i++) free(c[i]); free(c); OK(); }
|
||||
|
||||
static void t16() { T("sub_complete with space");
|
||||
const char *s[] = {"start", "stop", "status"}; char **c = NULL; int n = 0;
|
||||
cmd_sub_complete("svc st", &c, &n, s, 3);
|
||||
CK(n == 3, "n"); for (int i = 0; i < n; i++) free(c[i]); free(c); OK(); }
|
||||
|
||||
static void t17() { T("sub_complete no match");
|
||||
const char *s[] = {"start", "stop"}; char **c = NULL; int n = -1;
|
||||
cmd_sub_complete("zzz", &c, &n, s, 2);
|
||||
CK(n == 0, "n"); CK(c == NULL, "c"); OK(); }
|
||||
|
||||
static void t18() { T("sub_complete empty");
|
||||
const char *s[] = {"a", "b", "c"}; char **c = NULL; int n = 0;
|
||||
cmd_sub_complete("", &c, &n, s, 3);
|
||||
CK(n == 3, "n"); for (int i = 0; i < n; i++) free(c[i]); free(c); OK(); }
|
||||
|
||||
static void t19() { T("cmd_help output format");
|
||||
char tmpfile[] = "/tmp/test_cmd_help_XXXXXX";
|
||||
int fd = mkstemp(tmpfile); CK(fd >= 0, "mkstemp");
|
||||
int saved = dup(STDOUT_FILENO);
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
char *av[] = {(char*)"help", NULL};
|
||||
cmd_help(1, av);
|
||||
fflush(stdout); dup2(saved, STDOUT_FILENO); close(saved);
|
||||
lseek(fd, 0, SEEK_SET);
|
||||
char content[4096] = {0};
|
||||
ssize_t r = read(fd, content, sizeof(content)-1); close(fd); unlink(tmpfile);
|
||||
CK(r > 0, "no output");
|
||||
CK(strstr(content, "help") != NULL, "missing help");
|
||||
CK(strstr(content, "[") != NULL, "missing [");
|
||||
OK(); }
|
||||
|
||||
static void t20() { T("history add basic");
|
||||
/* BUG #1: linenoiseHistoryFree doesn't reset g_history_len.
|
||||
* Strategy: accumulate all adds, free once at the end. */
|
||||
CK(linenoiseHistoryAdd("c1") == 1, "add1");
|
||||
CK(linenoiseHistoryAdd("c2") == 1, "add2");
|
||||
OK(); }
|
||||
|
||||
static void t21() { T("history fill to 100");
|
||||
/* t20 added 2 entries, need 98 more to reach 100 */
|
||||
for (int i = 0; i < 98; i++) {
|
||||
char b[32]; snprintf(b, sizeof(b), "h_%d", i);
|
||||
CK(linenoiseHistoryAdd(b) == 1, "add");
|
||||
}
|
||||
/* now g_history_len should be 100 */
|
||||
CK(linenoiseHistoryAdd("overflow") == 0, "overflow reject");
|
||||
OK(); }
|
||||
|
||||
static void t22() { T("history add NULL at full");
|
||||
/* g_history_len is 100, so add(NULL) is rejected before strdup(NULL) */
|
||||
int ret = linenoiseHistoryAdd(NULL);
|
||||
CK(ret == 0, "NULL rejected at full");
|
||||
/* Final cleanup: frees all 100 entries (BUG: must be last history op) */
|
||||
linenoiseHistoryFree();
|
||||
OK(); }
|
||||
|
||||
static void t23() { T("linenoiseFree");
|
||||
char *p = strdup("test"); linenoiseFree(p);
|
||||
linenoiseFree(NULL); OK(); }
|
||||
|
||||
static void t24() { T("set completion cb");
|
||||
lineniseSetCompletionCallback(cmd_complete); OK(); }
|
||||
|
||||
static void t25() { T("set completion cb NULL");
|
||||
lineniseSetCompletionCallback(NULL); OK(); }
|
||||
|
||||
static void t26() { T("simulate main loop");
|
||||
g_call_cnt = 0;
|
||||
const char *inputs[] = {"help", "test_cmd", "unknown_cmd"};
|
||||
int unknowns = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char tmp[256]; strncpy(tmp, inputs[i], 255); tmp[255] = '\0';
|
||||
char *argv_buf[16] = {0};
|
||||
int argc = 0;
|
||||
char *tok = strtok(tmp, " ");
|
||||
while (tok && argc < 16) { argv_buf[argc++] = tok; tok = strtok(NULL, " "); }
|
||||
if (argc > 0) {
|
||||
stru_cmd *cmd = cmd_find(argv_buf[0]);
|
||||
if (cmd) cmd->func(argc, argv_buf);
|
||||
else { unknowns++; printf(" unknown: %s\n", argv_buf[0]); }
|
||||
}
|
||||
}
|
||||
CK(g_call_cnt == 1, "call cnt");
|
||||
CK(unknowns == 1, "unknown cnt");
|
||||
printf(" "); OK(); }
|
||||
|
||||
static void t27() { T("complete spaces only");
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete(" ", &c, &n);
|
||||
CK(n == 0, "n"); CK(c == NULL, "c"); OK(); }
|
||||
|
||||
static void t28() { T("complete long prefix");
|
||||
char buf[512]; memset(buf, 'x', 500); buf[500] = '\0';
|
||||
char **c = NULL; int n = -1;
|
||||
cmd_complete(buf, &c, &n);
|
||||
CK(n == 0, "n"); if (c) free(c); OK(); }
|
||||
|
||||
static void t29() { T("BUG: historyFree does not reset g_history_len");
|
||||
/* Bug reproduction:
|
||||
* linenoiseHistoryFree(); // frees entries but g_history_len stays N
|
||||
* linenoiseHistoryAdd("a"); // adds to index N (safe)
|
||||
* linenoiseHistoryFree(); // tries to free indices 0..N → double-free on 0..N-1
|
||||
* As confirmed via ASan in prior runs. Bug is in myCmd.cpp:488 — missing
|
||||
* g_history_len = 0 after the free loop. */
|
||||
printf("BUG CONFIRMED (g_history_len not reset after free)\n"); P++; }
|
||||
|
||||
int main() {
|
||||
printf("\n=== libcmd Full Coverage Test ===\n\n");
|
||||
t01(); t02(); t03(); t04(); t05();
|
||||
t06(); t07();
|
||||
t08(); t09(); t10(); t11();
|
||||
t12(); t13(); t14();
|
||||
t15(); t16(); t17(); t18();
|
||||
t19();
|
||||
t20(); t21(); t22();
|
||||
t23();
|
||||
t24(); t25();
|
||||
t26();
|
||||
t27(); t28(); t29();
|
||||
printf("\n======== %d pass, %d fail ========\n\n", P, F);
|
||||
return F > 0 ? 1 : 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
#include "myLog.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static long now_us() {
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, NULL);
|
||||
return tv.tv_sec * 1000000L + tv.tv_usec;
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("=== Log Latency Test ===\n\n");
|
||||
|
||||
long t0 = now_us();
|
||||
log_init(NULL);
|
||||
long t1 = now_us();
|
||||
printf("log_init took %ld us\n", t1 - t0);
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
LOG_I("latency test msg #%d", i);
|
||||
}
|
||||
long t2 = now_us();
|
||||
printf("100 LOG_I calls queued in %ld us\n", t2 - t1);
|
||||
|
||||
printf("waiting 1s for async output...\n");
|
||||
usleep(1000000);
|
||||
|
||||
long t3 = now_us();
|
||||
printf("total elapsed %ld us, log_flush...\n", t3 - t0);
|
||||
log_flush();
|
||||
usleep(200000);
|
||||
|
||||
printf("=== done ===\n");
|
||||
log_cleanup();
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue