数据源
Darra HMI 的数据来源不局限于 PLC 变量, 页面可同时集成:
- PLC 变量 (最常用, WebSocket 订阅)
- Service REST API
/api/hmi/*(数据库 / 文件 / 配置) - 外部 REST (通过 Service 代理跨域)
- 外部 MQTT (通过 Service 桥接)
- IndexedDB (浏览器本地大数据缓存)
- LocalStorage (用户偏好 / 主题)
- Python 脚本桥接 (AI / 图像 / 大数据)
1. PLC 变量 (主干)
见 变量绑定基础. 所有 darra.bind / <darra-* var> 走 WebSocket /ws。
2. Service REST API /api/hmi/*
HmiWebServer 提供的内置端点:
HmiWebServer 是设备控制系统的 HMI 服务器, 不是业务后端。它只提供 HMI 自身需要的管理端点 (登录/DB 描述/页面列表/资源上传), 不提供业务 CRUD (订单/物料/客户等)。
| 路径 | 方法 | 说明 |
|---|---|---|
/api/hmi/status | GET | 服务器状态 (活跃会话数/启动时间/版本) |
/api/hmi/variables | GET | 所有可绑定 PLC 变量清单 |
/api/hmi/db/{dbName} | GET | 查询 DB 块结构 (字段名/类型/偏移) |
/api/hmi/pages | GET | 列出所有 HMI 页面 |
/api/hmi/templates | GET | 项目模板列表 |
/api/hmi/upload | POST | 上传图片/资源到项目 assets |
/api/hmi/login | POST | 用户登录 (body {user, pass}, 返回 JWT) |
/api/user/* | 多 | 用户管理 (角色/权限) |
/api/script/* | 多 | Python 脚本调度与结果查询 (HmiScriptScheduler) |
对实时值/写控制, 不要走 REST — 直接用 darra.bind / darra.write (走 WebSocket /ws, 见 变量绑定)。
通用调用模式
// 描述 DB 块 (拿到字段/类型, 做动态表单)
const db1 = await (await fetch('/api/hmi/db/DB1')).json()
// → { fields: [{name:'Temp', type:'REAL', offset:0}, ...] }
// 登录拿 JWT
const res = await fetch('/api/hmi/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user: 'op01', pass: '****' })
})
const { token } = await res.json()
// token 自动存 Cookie, 后续请求带上
Alpine 异步数据源 (PLC 变量首选 darra.bind)
读当前 DB 块结构 生成动态表单:
<div x-data="{ fields: [] }"
x-init="fields = (await (await fetch('/api/hmi/db/DB_Recipe')).json()).fields">
<template x-for="f in fields" :key="f.name">
<label>
<span x-text="f.name"></span>
<darra-input :var="'DB_Recipe.' + f.name"></darra-input>
</label>
</template>
</div>
3. 外部 REST (经 Service 代理)
浏览器不能直接跨域调外部 API (CORS 限制)。Service 提供代理路径 /api/hmi/proxy/*:
// 假设 Service 配置代理: upstream = 'https://api.factory.com'
const prod = await fetch('/api/hmi/proxy/mes/production?date=2026-04-18').then(r => r.json())
Service 端配置 (项目文件 .plc 中):
{
"HmiProxy": {
"mes": {
"upstream": "https://api.factory.com",
"auth": "Bearer ${env:MES_TOKEN}",
"allowedPaths": ["/mes/*"]
}
}
}
4. 外部 MQTT (Service 桥接)
HmiWebServer 内置 MQTT 桥接: 订阅的 MQTT 主题会通过 WebSocket 推送给 HMI。
订阅
darra.on('ready', () => {
// 发送桥接订阅
darra._ws.send(JSON.stringify({
type: 'mqtt_subscribe',
topic: 'factory/line1/temperature'
}))
})
// 收到推送
darra.on('mqtt', (msg) => {
// msg = { topic, payload }
console.log(msg.topic, msg.payload)
})
发布
darra._ws.send(JSON.stringify({
type: 'mqtt_publish',
topic: 'factory/line1/cmd',
payload: { action: 'start' }
}))
5. IndexedDB (本地大数据)
浏览器原生, 适合存储历史趋势 / 报警历史 / 工单缓存:
// 打开数据库
const dbReq = indexedDB.open('darra-hmi', 1)
dbReq.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains('trend')) {
db.createObjectStore('trend', { keyPath: 'ts' })
}
}
let dbHandle
dbReq.onsuccess = (e) => { dbHandle = e.target.result }
// 写入
function saveTrend(record) {
const tx = dbHandle.transaction('trend', 'readwrite')
tx.objectStore('trend').add(record)
}
darra.bindDB('DB1', (vals) => {
saveTrend({ ts: Date.now(), t: vals.Temperature, p: vals.Pressure })
})
// 读取近 1000 条
function queryRecent(n) {
return new Promise((resolve) => {
const tx = dbHandle.transaction('trend', 'readonly')
const req = tx.objectStore('trend').getAll(null, n)
req.onsuccess = () => resolve(req.result)
})
}
IndexedDB 只做纯本地缓存 — 设备历史趋势, 页面偏好, 离线暂存。不是业务数据库 (详见 通讯 / 数据库 说明 DarraRT 的数据库定位: 以读为主, 写罕见)。
6. LocalStorage (轻量配置)
// 保存用户偏好
localStorage.setItem('darra-layout', JSON.stringify({ showSidebar: true, columns: 3 }))
// 读取
const pref = JSON.parse(localStorage.getItem('darra-layout') || '{}')
// 监听变化 (多标签同步)
window.addEventListener('storage', (e) => {
if (e.key === 'darra-layout') {
console.log('其他标签改了偏好:', JSON.parse(e.newValue))
}
})
限制: 最大约 5-10 MB, 仅存字符串, 不适合大数据。
7. Python 脚本桥接
调用 Service 的 HmiPythonBridge:
// Service 通过 HmiScriptScheduler 调度 Python 脚本, REST 端点在 /api/script/*
const result = await fetch('/api/script/invoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
script: 'analyze_trend', // scripts/analyze_trend.py
args: { window: 3600 }
})
}).then(r => r.json())
console.log('Python 结果:', result) // { anomaly: true, score: 0.87 }
Service 端 scripts/analyze_trend.py:
import numpy as np
from bridge import get_trend
def run(window=3600):
data = get_trend('DB1.Temperature', window)
arr = np.array(data)
mean = arr.mean()
std = arr.std()
z = (arr[-1] - mean) / std if std > 0 else 0
return { 'anomaly': abs(z) > 3, 'score': float(z), 'mean': float(mean) }
多数据源聚合
一个 HMI 页面常同时消费多种数据源:
async function bootstrap() {
// 1. PLC 实时变量
darra.bindDB('DB1', updateKpi)
// 2. Historian 历史数据 (见 advanced/historian)
const history = await queryHistorian({ var: 'DB1.OEE', bucket: '5m', from: '-1d' })
renderHistoryChart(history)
// 3. IndexedDB 本地趋势缓存
const trend = await queryRecent(1000)
renderTrendChart(trend)
// 4. MQTT 外部事件桥接 (Service MQTT 客户端代理)
darra.on('mqtt', renderMqttAlert)
// 5. Python 分析 (每分钟一次, HmiScriptScheduler 执行)
setInterval(async () => {
const r = await callPython('analyze_trend', { window: 3600 })
updateAnomaly(r)
}, 60000)
}
darra.on('ready', bootstrap)
刷新策略
| 数据类型 | 推荐策略 |
|---|---|
| PLC 变量 | 订阅推送 (默认 100ms 节流) |
| REST 静态数据 | 启动拉 1 次 + 手动刷新按钮 |
| REST 动态数据 | 定时 setInterval (5s-60s) 或 MQTT 事件驱动 |
| IndexedDB | 本地读, 启动时全量 / 变化时增量 |
| Python 分析 | 按需 (用户点击) 或 定时 (1 分钟) |
断线容错
| 数据源 | 断线表现 | 处理 |
|---|---|---|
| PLC (WebSocket) | offline 事件 | darra-conn-status 红色, 3s 重连 |
| Service REST | HTTP 超时/500 | try/catch, 显示 toast, 不影响其他源 |
| 外部 REST | Service 代理返 502 | 降级到缓存 / 提示 |
| MQTT | 桥接断 | Service 自动重连, HMI 无需处理 |
| IndexedDB | 极少出错 | try/catch, 失败降级到 LocalStorage |
排错
| 现象 | 原因 | 排查 |
|---|---|---|
fetch 401 | 未登录 | 查 Cookie darra-token, 重新 login |
fetch 404 | 路径拼错 | Network 面板看完整 URL |
| WebSocket 收不到 MQTT | Service MQTT 未连 | Service 日志 [MqttBridge] connected? |
| IndexedDB 满 | 浏览器配额 | 定期清理旧数据 |