变量绑定基础
Darra HMI 的核心是 darra 全局 JS 对象 (定义在 /static/darra-plc.js), 封装了 WebSocket 通信、变量订阅、双向写入、元数据查询、连接生命周期事件。所有控件、脚本都必须通过 darra.* 访问 PLC 数据, 严禁直接操作 WebSocket。
PLC 变量地址语法 (IEC 61131-3)
| 语法 | 类型 | 示例 |
|---|---|---|
M<byte>.<bit> | 标志位 (BOOL) | M0.0 M0.7 M100.3 |
MB<offset> | 标志字节 (BYTE) | MB10 |
MW<offset> | 标志字 (WORD / INT16) | MW100 MW200 |
MD<offset> | 标志双字 (DWORD / INT32 / REAL) | MD100 MD200 |
I<byte>.<bit> | 输入 | I0.0 |
IW<offset> | 输入字 | IW0 |
Q<byte>.<bit> | 输出 | Q0.0 |
QW<offset> | 输出字 | QW4 |
DB<n>.<name> | 数据块字段 | DB1.Temperature |
DB<n>.<name>.<member> | 嵌套结构 | DB1.Motor.Speed |
MD100与MW100/MW102共享存储; 程序内注意避免别名冲突。
darra.init(config) — 初始化
darra.init({
updateRate: 100, // 变量推送节流 ms (服务端)
reconnect: true, // 自动重连
reconnectInterval: 3000, // 重连间隔 ms
locale: 'zh-CN' // 区域 ('zh-CN' | 'en-US')
})
默认已在 HmiLayoutRenderer 的 <body x-init> 中调用, 多数情况无需手动 init。只有需要修改默认配置时才调用。
darra.bind(varName, callback) — 绑定单变量
darra.bind('DB1.Temperature', (value, meta) => {
document.getElementById('temp').textContent = value.toFixed(2) + ' ℃'
})
| 参数 | 说明 |
|---|---|
varName | 变量地址 (DB1.Temperature, M0.0, MW100) |
callback(value, meta) | 值变化回调 |
meta.timestamp | 服务端采集时间 (ms since epoch) |
meta.quality | good / bad / cached |
meta.type | 'number' / 'boolean' / 'string' |
行为:
- 订阅会自动发送到 Service; 断线重连后会重新订阅
- 如果已有缓存值, 立即回调一次 (
quality='cached') - 可多次
bind同一变量, 每个 callback 都会触发
darra.bindDB(dbName, callback) — 绑定整个 DB
darra.bindDB('DB1', (values) => {
// values = { Temperature: 25.3, Pressure: 1.2, Level: 80.5, ... }
console.log('DB1 更新:', values)
})
整个 DB 块任一字段变化都触发一次 callback, 参数是增量字段的对象 (只含变化的字段)。
darra.bindGroup(vars[], callback) — 绑定多变量组
darra.bindGroup(['DB1.T1', 'DB1.T2', 'M0.0'], (values) => {
// values = { 'DB1.T1': 25, 'DB1.T2': 26, 'M0.0': true }
console.log('任一变化:', values)
})
任一变量变化时整组回调, values 包含所有组内变量的最新值 (不只是变化的)。
darra.write(varName, value) — 单变量写入
darra.write('M0.0', true)
darra.write('DB2.Setpoint', 75.5)
darra.write('MW100', 1234)
行为:
- 发送到 Service, Service 校验
WriteWhitelist, 未通过返回error事件 - 异步: 不等响应; 关心结果需要监听
error事件 - 写入后, 下一次推送会带新值, 绑定回调自动触发
darra.writeGroup(values) — 批量写入
darra.writeGroup({
'DB2.Setpoint': 75.5,
'M0.1': false,
'MW100': 1234
})
一次 WebSocket 帧完成所有写入, 性能优于多次 write。
darra.get(varName) — 取当前缓存值
const t = darra.get('DB1.Temperature')
if (t !== undefined && t > 80) { /* 告警 */ }
从本地缓存读 (不走 WebSocket), 未订阅的变量返回 undefined。
darra.describeDB(dbName) — 查询 DB 结构
const fields = await darra.describeDB('DB1')
// fields = [
// { name: 'Temperature', type: 'REAL', address: 'DB1.Temperature', comment: '反应釜温度' },
// { name: 'Pressure', type: 'REAL', address: 'DB1.Pressure', comment: '' },
// ...
// ]
发起 HTTP 请求 GET /api/hmi/db/DB1, 返回字段列表。常用于 <darra-dx-form> 自动生成表单。
darra.on(event, callback) — 事件监听
darra.on('ready', () => console.log('WebSocket 已连接'))
darra.on('offline', () => console.warn('连接断开'))
darra.on('error', err => console.error(err))
darra.on('alarm', alarm => { /* {level,source,message,timestamp} */ })
darra.on('themeChanged', name => { /* 'modern' | 'industrial' */ })
| 事件名 | 触发时机 | 参数 |
|---|---|---|
ready | WebSocket 首次连上 或 重连成功 | — |
offline | WebSocket 断开 | — |
error | 写入失败 / 订阅失败 | { type, var, error } |
alarm | Service 推送报警 | { level, source, message, timestamp } |
auth_ok | 登录成功 | { user, role, token } |
themeChanged | setTheme 调用后 | 'modern' / 'industrial' |
darra.setTheme / getTheme
darra.setTheme('industrial')
console.log(darra.getTheme()) // 'industrial'
切换 <body class="theme-industrial"> 并存入 localStorage['darra-hmi-theme']。
WebSocket 连接生命周期
┌───────────────────┐
│ 页面加载 │
└─────────┬─────────┘
↓
darra.init() ← 由 Alpine x-init 自动触发
↓
darra._autoConnect() → ws://host/ws
↓
┌──────────────────────┴──────────────────┐
↓ ↓
ready 事件 error → offline
│ │
↓ ↓
重新订阅 _subscribedVars scheduleReconnect
│ │
↓ ↓
update 消息 3s 后重新 connect
↓ │
_handleUpdate ↓
↓ (循环)
触发 bind/bindDB/bindGroup 回调
关键点:
- 断线后所有
bind/bindDB/bindGroup不会丢失, 重连后自动重新订阅 - 右下角
<div id="darra-conn-status">永远显示当前连接状态 (不可删除) - Service 默认 100ms 节流 (合并多个变量的更新为一条
update消息)
写入白名单 (WriteWhitelist)
HMIProjectDef.WriteWhitelist 是 Service 端的硬安全门:
{
"WriteWhitelist": [
"M0.*", // 允许写 M0 字节所有位
"DB2.Setpoint*", // 允许写 DB2 下以 Setpoint 开头的字段
"MW200" // 精确允许
]
}
不在白名单的写入会被 Service 拒绝并返回:
{ "type": "write_ack", "ok": false, "var": "M100.0", "error": "not_in_whitelist" }
darra.on('error', ...) 会收到, 建议 UI 提示 "无权限写入"。
最佳实践
- 能用控件就别手写 bind:
<darra-numeric var="DB1.T">比darra.bind('DB1.T', ...)更简洁 - 大批量用 bindGroup: 10+ 变量一起绑比 10 次
bind效率高 - 写入防抖: 滑块拖动时
debounce(300ms)后再write, 避免洪泛 - 始终监听 offline: 断开时禁用所有写入按钮, 避免用户误以为写成功
- 错误友好提示:
darra.on('error', ...)用 toast 展示而非静默 log - 描述 DB 不要频繁调:
describeDB是 HTTP, 放Alpine init里调一次, 结果缓存
排错表
| 现象 | 原因 | 排查 |
|---|---|---|
bind 回调不触发 | 变量未订阅成功 | F12 Network → WS → 看 subscribe 帧是否发出 |
| 写入无效 | 白名单未通过 | 控制台看 error 事件, 检查 WriteWhitelist |
| 右下角显示"断开" | Service 未启动 | 确认 18823 端口监听, 防火墙未拦 |
| 重连后值是老值 | 缓存未失效 | Service 重启时应清 HMI 侧缓存, 按 Ctrl+F5 |
describeDB 404 | DB 名拼写错 | /api/hmi/db/DB1 试, 区分大小写 |