跳到主要内容

多屏分发

"多屏分发" = 一台 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-505-15%工控机 i5
50-20015-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监控并限制同连接数

相关文档