HMI 诊断
HMI 运行在浏览器/WebView2, 无法像 PLC 那样靠断点/单步; 调试手段全部是 Web 标准工具: 控制台、Network、Performance、darra 内部状态。本文汇总所有必备诊断手段。
PLC 程序没有断点/单步 (实时系统), HMI 作为前端展示层也不使用 WPF 调试器概念, 所有诊断均通过 DevTools 进行。
1. 打开 DevTools
| 环境 | 方法 |
|---|---|
| LocalHMI (Debug 构建) | Ctrl+Shift+I |
| LocalHMI (Release) | 默认禁用, 需改注册表 ms-edge-devtools-enabled=1 或 Darra.PLC.HMI.exe --dev |
| 浏览器 WebHMI | F12 |
2. Console 日志层级
console.log ('普通日志')
console.info ('提示')
console.warn ('警告')
console.error ('错误')
console.table([{id:1,name:'a'}, {id:2,name:'b'}]) // 表格
console.group('配方加载')
console.log('读取配方列表')
console.log('切换到目标配方')
console.groupEnd()
console.time('query')
await fetch('/api/hmi/db/DB_Recipe') // 查询 DB 块结构是真实 API
console.timeEnd('query') // query: 123ms
3. darra 内部状态
控制台直接查:
darra._connected // true / false
darra._ws.readyState // 0 CONNECTING 1 OPEN 2 CLOSING 3 CLOSED
darra._subscribedVars // Set { 'DB1.Temp', 'M0.0', ... }
darra._values // { 'DB1.Temp': 25.3, ... } 当前缓存
darra._bindings // Map { 'DB1.Temp' => Set(callbacks) }
darra._dbBindings // Map { 'DB1' => Set(callbacks) }
darra._groupBindings // [ { vars:[...], callback } ]
darra._config // { updateRate, reconnect, ... }
darra.getTheme() // 'modern' | 'industrial'
4. 业务日志 darra.log(...)
在 JsCode 顶部约定统一 logger:
darra.log = (level, module, ...args) => {
const line = `[${new Date().toLocaleTimeString()}] [${level}] [${module}]`
console[level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'](line, ...args)
// 同时上报到 Service 日志
if (level === 'error' || level === 'warn') {
darra._ws?.send(JSON.stringify({
type: 'log', level, module, message: args.map(String).join(' '),
ts: Date.now()
}))
}
}
// 使用
darra.log('info', 'RecipePanel', '加载配方', recipeCode)
darra.log('warn', 'RecipePanel', '配方参数超限:', value)
darra.log('error', 'RecipePanel', ex.message, ex.stack)
Service 端 HmiWebServer 收到 log 消息, 写入 logs/service_*.log。
5. Network 面板
HTTP 请求
Filter 输入 /api/hmi/ 看所有 Service 调用:
- Status Code 401: 未登录
- Status Code 500: Service 异常, 看 Response 错误堆栈
- Status Code 404: 路径错
WebSocket 帧
- Filter 切 WS 标签
- 点
ws连接 → Messages 标签 - 绿色 ↑ = 客户端发 (subscribe / write), 红色 ↓ = 服务端推 (update / alarm)
示例帧
// ↑ 订阅
{ "type": "subscribe", "vars": ["DB1.Temp", "M0.0"] }
// ↓ 更新推送
{ "type": "update", "values": { "DB1.Temp": 25.30, "M0.0": true }, "ts": 1713429123456 }
// ↑ 写入
{ "type": "write", "var": "DB2.Setpoint", "value": 75.5 }
// ↓ 写入应答
{ "type": "write_ack", "ok": true, "var": "DB2.Setpoint" }
// ↑ 写入失败
{ "type": "write_ack", "ok": false, "var": "M100.0", "error": "not_in_whitelist" }
6. 帧率 (FPS) 检测
let lastTs = performance.now(), frameCount = 0
function tick() {
frameCount++
const now = performance.now()
if (now - lastTs >= 1000) {
const fps = frameCount / ((now - lastTs) / 1000)
console.log('FPS:', fps.toFixed(1))
document.getElementById('fps-meter').textContent = fps.toFixed(0)
frameCount = 0
lastTs = now
}
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
健康值:
- 60 FPS: 空闲/简单页
- 30 FPS: 大量动画
<20 FPS: 卡顿, 需优化 (减少动画 / 降低 Chart update / 关闭 will-change)
7. Performance 面板
- F12 → Performance → Record
- 操作 10-20 秒 → Stop
- 查看:
- Frames 条目 (绿色健康, 红色掉帧)
- Scripting 蓝色 (JS 时间)
- Rendering 紫色 (布局/绘制)
- GPU 黄色 (合成)
Scripting 过长 → JS 代码优化; Rendering 过长 → 减少 DOM 操作。
8. 内存检测
// 快速快照
console.log((performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(1), 'MB')
// Chrome 专属 (Edge 也有)
// F12 → Memory → Take Heap Snapshot
观察:
- 每个页面加载后应在 20-50 MB 内
- 长期运行内存持续增长 → 有泄漏, 找
setInterval/ 未清理的bind
9. 绑定刷新率
// 统计每个变量每秒触发次数
const counter = {}
const origBind = darra.bind.bind(darra)
darra.bind = (v, cb) => origBind(v, (val, meta) => {
counter[v] = (counter[v] || 0) + 1
cb(val, meta)
})
setInterval(() => {
console.table(counter)
for (const k in counter) counter[k] = 0
}, 1000)
健康值: 每个变量 <=10 次/秒 (与 Service updateRate 100ms 吻合)。超出说明 Service 推送过于频繁。
10. WebSocket 健康检查
function healthCheck() {
return {
connected: darra._connected,
readyState: ['CONNECTING','OPEN','CLOSING','CLOSED'][darra._ws?.readyState ?? 3],
subscribed: darra._subscribedVars.size,
cachedValues: Object.keys(darra._values).length,
bindings: darra._bindings.size,
dbBindings: darra._dbBindings.size
}
}
console.table(healthCheck())
11. 报错收集
// 全局 JS 错误 (darra-plc.js 已内置)
window.addEventListener('error', e => {
console.error('[global error]', e.message, e.filename, e.lineno)
})
window.addEventListener('unhandledrejection', e => {
console.error('[unhandled promise]', e.reason)
})
darra-plc.js 已在右下角显示红色 toast, 并通过 WebSocket 上报到 Service。
12. CSS 调试
DevTools Elements 选中 → 右侧 Styles:
- 看每个 CSS 规则的来源 (darra-hmi.css / 皮肤 / 页面)
- 勾选/去掉单条规则试效果
- 计算样式 (Computed) 看最终生效值
- Box Model 看 margin/padding
13. 常见问题定位
| 症状 | 排查 |
|---|---|
| 页面白屏 | Console 有 darra is not defined → 检查 darra-plc.js 路径 |
| 变量不更新 | WS Messages 无 subscribe → bind 调用时机早于 ready |
| 写入无反应 | WS 有 write_ack ok:false → 白名单 |
| 右下角红色"断开" | Service 18823 未启动 或 防火墙 |
| 卡顿 | Performance Record → JS 重 or DOM 多 |
| 内存涨 | Memory 对比两次快照 → 找 Detached DOM 和未释放闭包 |
| 样式错乱 | Elements → Computed 看覆盖关系 |
| 字体模糊 | body { transform: scale(...) 过度缩放 → 改 DesignWidth |
14. Darra HMI 诊断卡片 (可内嵌)
提供一个页面右下角的诊断小面板:
<div id="darra-debug" x-data="darraDebug()" x-init="start()" x-show="visible"
style="position:fixed;bottom:40px;right:12px;background:#000a;color:#0f0;font:11px monospace;padding:8px 12px;border-radius:6px;z-index:99998">
<div>FPS: <span x-text="fps"></span></div>
<div>WS: <span x-text="wsState"></span></div>
<div>订阅: <span x-text="subs"></span></div>
<div>内存: <span x-text="mem"></span> MB</div>
</div>
<script>
function darraDebug() {
return {
visible: false, fps: 0, wsState: '-', subs: 0, mem: 0,
start() {
// Ctrl+Alt+D 切换
document.addEventListener('keydown', e => {
if (e.ctrlKey && e.altKey && e.key === 'd') this.visible = !this.visible
})
let last = performance.now(), count = 0
const tick = () => {
count++
const now = performance.now()
if (now - last >= 1000) {
this.fps = (count / ((now - last)/1000)).toFixed(0)
this.wsState = darra._connected ? 'OPEN' : 'CLOSED'
this.subs = darra._subscribedVars.size
this.mem = ((performance.memory?.usedJSHeapSize || 0) / 1048576).toFixed(1)
count = 0; last = now
}
requestAnimationFrame(tick)
}
requestAnimationFrame(tick)
}
}
}
</script>