多屏分发
"多屏分发" = 一台 PLC Service (18823 端口), 同时给多个终端提供不同页面 + 不同皮肤 + 相同实时数据。典型场景: 车间墙面大屏、工位平板、班组长手机、经理办公室电视、指挥中心拼接屏, 每个终端看到不同的 HMI 页面, 但都连同一个 PLC。
1. 架构
┌──────────────────────────────┐
│ Darra.PLC.Service (18823) │
│ ┌────────────────────────┐ │
│ │ HmiWebServer │ │
│ │ /web/dashboard (4K) │ │
│ │ /web/line1 (平板) │ │
│ │ /web/scan (手机) │ │
│ │ /web/executive (客厅) │ │
│ │ /web/scada (大屏) │ │
│ └─────────────┬──────────┘ │
│ 共享 WebSocket / 变量 │
└─────────────────┼────────────┘
│
┌──────────┬──────────┬───────┼──────────┬──────────┐
↓ ↓ ↓ ↓ ↓ ↓
[4K 电视] [平板] [手机] [大屏墙] [办公电视] [拼接屏]
车间总览 工位终端 扫码 SCADA 高管总览 指挥中心
同一个 Service:
- 所有终端共享 WebSocket 变量状态 (一次 PLC 订阅, 多终端收到推送)
- 不同终端用不同路由 (
/web/dashboard,/web/line1...) 访问不同页面 - 每个页面可用不同皮肤 (深色大屏 / 浅色工位)
2. 页面规划
HMIProjectDef.Pages 设计多个路由:
{
"Type": "Web",
"Pages": [
{ "Name": "总览", "Route": "/", "IsHome": true,
"Tags": ["dashboard","4k"],
"Settings": { "DesignWidth": 3840, "DesignHeight": 2160 } },
{ "Name": "线 1", "Route": "/line1",
"Tags": ["tablet"],
"Settings": { "DesignWidth": 1280, "DesignHeight": 800 } },
{ "Name": "扫码", "Route": "/scan",
"Tags": ["mobile"],
"Settings": { "DesignWidth": 375, "DesignHeight": 667 } },
{ "Name": "高管", "Route": "/executive",
"Tags": ["tv"],
"Settings": { "DesignWidth": 1920, "DesignHeight": 1080 } },
{ "Name": "SCADA", "Route": "/scada",
"Tags": ["wallscreen"],
"Settings": { "DesignWidth": 5120, "DesignHeight": 1440 } }
],
"HomeRoute": "/"
}
每个页面可独立设置
DesignWidth/DesignHeight/ActiveSkin(通过 JsCode 动态覆盖)。
3. 不同终端部署
4K 电视 (车间总览)
# 树莓派 + Chromium Kiosk
chromium-browser --kiosk http://192.168.1.100:18823/web/?rotate
工位平板 (Android)
Chrome "添加到主屏" → 全屏启动 → URL: http://192.168.1.100:18823/web/line1
手机 (扫码 PDA)
工作区扫码扫到 URL http://192.168.1.100:18823/web/scan?worker=W001
办公室电视 (Windows)
Darra.PLC.HMI.exe --url=http://192.168.1.100:18823/web/executive --fullscreen
拼接屏 (Linux 工业 PC)
firefox --kiosk http://192.168.1.100:18823/web/scada
4. 按 URL 参数动态切皮肤
const params = new URLSearchParams(location.search)
const theme = params.get('theme') || 'modern'
darra.setTheme(theme)
用法:
- 大屏:
http://ip/web/?theme=modern - 工位:
http://ip/web/line1?theme=industrial
5. 按 User-Agent 自动选路由
Service 端 HmiWebServer.cs 拦截 /web/ 请求, 根据 UA 重定向:
// 伪代码
if (request.Headers["User-Agent"] contains "Mobile")
response.Redirect("/web/scan")
else if (viewport_width >= 3000)
response.Redirect("/web/dashboard")
前端 JS 也可做:
if (location.pathname === '/web/') {
if (/Mobile|Android/.test(navigator.userAgent)) location.replace('/web/scan')
else if (window.innerWidth >= 2560) location.replace('/web/dashboard')
else location.replace('/web/line1')
}
6. 跨终端联动
场景: 手机扫码 → 大屏高亮对应工位
// 手机扫码页
async function onScan(code) {
await fetch('/api/hmi/dispatch/highlight', {
method: 'POST',
body: JSON.stringify({ target: 'dashboard', station: code })
})
}
Service 端广播:
// HmiBroadcaster: 广播一个自定义事件给所有订阅该 tag 的客户端
broadcaster.Broadcast("highlight", new { station = code }, tag: "dashboard")
大屏页面接收:
darra.on('highlight', (data) => {
document.querySelectorAll('.station').forEach(el => el.classList.remove('highlighted'))
document.querySelector(`.station[data-id="${data.station}"]`)?.classList.add('highlighted')
})
场景: 班组长手机切页 → 同步班组平板
// 手机: 班组长按"切到 Line2"
darra.bus.emit('team-page-change', { route: '/web/line2' })
// Service 中转 WebSocket
fetch('/api/hmi/broadcast', {
method: 'POST',
body: JSON.stringify({ tag: 'team-shift-a', event: 'page-change', data: { route: '/web/line2' } })
})
班组平板订阅:
darra.on('page-change', (d) => {
if (confirm(`班长切到 ${d.route}, 是否跟随?`)) location.href = d.route
})
7. 共享变量状态
同一个变量 (如 DB1.Temp) 在多个终端同时显示:
- Service 只向 PLC 订阅一次 (节流
100ms) - 收到 update 后, 广播给所有已订阅该变量的 WebSocket 客户端
- 客户端 100+ 台时 Service CPU 依然可控
验证:
// 每台终端独立调 darra.bind('DB1.Temp', ...)
// Service 内部会去重合并为一次 PLC 请求
8. 按 Tag 过滤广播
// WebSocket 握手时带 tag
new WebSocket('ws://ip:18823/ws?tags=dashboard,4k')
Service 根据 tags 过滤:
// 只广播给 tag 包含 "dashboard" 的客户端
broadcaster.Broadcast("alarm", alarm, tag: "dashboard")
HMI 客户端初始化时用 URL 参数或 meta 指定:
<meta name="darra-tags" content="dashboard,4k" />
const tags = document.querySelector('meta[name="darra-tags"]')?.content || ''
darra.init({ tags })
9. 多语言多终端
每终端可用不同语言:
const lang = new URLSearchParams(location.search).get('lang') || 'zh-CN'
darra.init({ locale: lang })
document.documentElement.lang = lang
10. 负载 & 性能
| 终端数 | Service CPU | 推荐配置 |
|---|---|---|
<10 | <5% | 任意 PC |
| 10-50 | 5-15% | 工控机 i5 |
| 50-200 | 15-40% | i7 + 16G |
>200 | 考虑水平扩展 | 多 Service + nginx 反代 |
11. 水平扩展 (大工厂)
单 Service 撑不住时:
负载均衡 (nginx)
├── Service-1 (负责 PLC A)
├── Service-2 (负责 PLC B)
└── Service-3 (负责 PLC C)
每个 Service 负责一部分 PLC, 前端通过路由区分:
/web/shopA/*→ Service-1/web/shopB/*→ Service-2/web/shopC/*→ Service-3
12. 固定终端布局 (Kiosk)
锁住终端防止误操作:
Darra.PLC.HMI.exe --kiosk --url=http://ip:18823/web/line1 --no-esc-exit --disable-devtools
Kiosk 模式:
- 全屏无边框
- 屏蔽 Alt+F4 / Ctrl+W / F11 / F12
- ESC 退出禁用
- 开发者工具禁用
13. 终端状态监控
Service 记录所有连接的终端:
GET /api/hmi/clients
→ [
{ id: 'c001', ip: '192.168.1.10', ua: 'Chrome/Android', route: '/web/scan', connectedAt: 1713... },
{ id: 'c002', ip: '192.168.1.20', ua: 'Chrome/Windows', route: '/web/dashboard', ... },
...
]
管理页面展示:
<darra-dx-grid var="HmiClients"
columns="ip:IP,ua:终端,route:页面,connectedAt:连接时间"></darra-dx-grid>
14. 路由权限
不同路由需要不同角色:
{
"HmiRouteAccess": {
"/web/dashboard": { "roles": ["*"], "public": true },
"/web/line1": { "roles": ["operator","engineer"] },
"/web/executive": { "roles": ["admin"] },
"/web/scada": { "roles": ["engineer"] }
}
}
未授权终端访问 → 跳 /login。
15. 陷阱
| 陷阱 | 解法 |
|---|---|
| 手机访问大屏页卡死 | 路由区分 + UA 自动重定向 |
| 跨终端联动消息丢失 | 用持久订阅, WebSocket 断后自动 resync |
| Chrome Kiosk 屏保自动唤醒 | systemctl mask sleep.target suspend.target |
| 拼接屏 5120 宽度计算误差 | DesignWidth 精确 + ScaleMode=Fixed |
| 200+ 终端 Service OOM | 监控并限制同连接数 |