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:
ypc 2026-07-14 11:02:44 +08:00
parent 28e26f8b67
commit 370be09926
9 changed files with 1388 additions and 18 deletions

6
.gitignore vendored
View File

@ -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手册/

View File

@ -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/`(待补) | ⬜ |
## 阶段 2v3.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系统集成

View File

@ -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` | ✅ |
---

View File

@ -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` 的 handlersave 功能形同虚设。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 |

View File

@ -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 配置文件 (数据源)
```

View File

@ -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.cpp277行
| 序号 | 功能 | 原工程实现 | 说明 |
|:----:|------|-----------|------|
| 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.cpp1421行
| 序号 | 功能 | 原工程实现 | 说明 |
|:----:|------|-----------|------|
| 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 |

View File

@ -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` 验证分片发送

View File

@ -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;
}

View File

@ -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;
}