标签 :数字孪生 WebSocket C# Three.js 工业自动化 联调实战
背景:从"能看"到"能动"
AutoFrame 上位机框架设计完成后,我面临一个问题:产线有 17 个运动轴、40 多个 IO 点位,传统的 WinForms 界面用数字和状态灯显示,信息密度低,不够直观。操作员需要盯着密密麻麻的表格,很难快速判断设备整体状态。
我决定做一个 3D 数字孪生前端。用 Three.js 构建产线的简化模型,通过 WebSocket 实时接收上位机推送的轴位置和 IO 状态,让模型跟着真实设备一起运动。
这个想法在文档里写得很清楚------DigitalTwinBridge.cs,WebSocket 服务端,JSON 协议,差异化推送频率。但文档归文档,真正把上位机和浏览器打通,中间隔着一整条"联调地狱"。
这篇文章记录的,就是我从"设计完成"到"真正跑通"的全过程。
架构设计:三层数据管道
整个数字孪生系统的数据流向如下:
text
scss
AutoFrameDll.dll (供应商运动控制库)
↓ 反射调用 (System.Reflection)
DigitalTwinBridge.cs (C# WebSocket 服务端)
↓ ws://上位机IP:8100
Next.js + Three.js 前端 (useWebSocketBridge.ts)
↓ 状态更新
3D 模型实时运动 + IO 状态变色
关键设计决策:
- 反射桥接,零编译依赖 :上位机不直接引用供应商的
AutoFrameDll.dll,而是运行时通过Assembly.LoadFrom动态加载。供应商升级 DLL,我只需要替换文件,不用重新编译主程序。这是工业场景下"不要动已经稳定的代码"的最佳实践。 - 差异化推送频率:轴位置 100ms 推送一次(每秒 10 帧,保证动画流畅),IO 状态和机器状态 500ms 推送一次(降低带宽和 CPU 占用)。这个数字不是拍脑袋的------轴位置变化快,需要高频;IO 变化慢,500ms 足够。
- 自动重连机制:WebSocket 断开后,前端每 3 秒重试一次,上位机保持监听。产线网络波动是常态,自动重连是刚需。
- 轴映射表硬编码 :17 个轴的逻辑名称(M1、M2...)与供应商的卡索引、轴号一一对应,在前端
machine-config.ts中维护。两边必须严格一致。
联调前的检查清单
联调最怕的不是复杂的问题,而是低级错误导致的"怎么都不通"。我给自己列了一份检查清单,逐项打勾。
| 检查项 | 验证方法 | 状态 |
|---|---|---|
| 上位机 WebSocket 服务已启动 | 浏览器访问 http://上位机IP:8100,看是否返回 WebSocket 握手响应 |
✅ |
| 防火墙放行 8100 端口 | 从另一台电脑 telnet 上位机IP 8100,看是否通 |
✅ |
| 前端 WebSocket URL 配置正确 | 检查 useWebSocketBridge.ts 中的 WS_URL,确保 IP 和端口正确 |
✅ |
| 轴映射表前后端一致 | 核对上位机轴号与前端 machine-config.ts 中的 M1、M2 等命名 |
✅ |
| IO 点位命名一致 | 核对 1-X01 等点位名称是否与前端模型绑定的名称一致 |
✅ |
上位机调用了 Start() |
在 DigitalTwinBridge.Instance.Start() 处打断点,确认定时器启动 |
✅ |
| 前端能收到第一条消息 | 打开浏览器开发者工具 → Network → WS 面板,看是否有 axis-update 消息 |
⏳ |
联调中踩过的坑
坑一:WebSocket 连接成功,但一条数据都没有
现象 :前端显示"WebSocket 已连接",Network 面板也能看到 101 Switching Protocols 成功。但等了 10 秒,控制台一条 axis-update 消息都没有,3D 模型一动不动。
排查过程:
- 先确认上位机定时器是否启动。在
DigitalTwinBridge.Start()方法里加日志,发现Start()根本没被调用。 - 原来我把
DigitalTwinBridge.Instance.Start()放在了Program.Main()里,但在窗体初始化之前就执行了。而Start()内部依赖MotionMgr.Instance,此时反射加载的 DLL 还没完全初始化。 - 更致命的是,我忘记在窗体加载完成后调用
Start()。
解决方案 :把 DigitalTwinBridge.Instance.Start() 移到主窗体的 Form_Load 事件中,确保所有管理器初始化完成后再启动数据推送。
教训 :反射加载 DLL 有时序依赖。在 Initialize() 之后、Start() 之前,要确保所有单例管理器已经就绪。加日志是排查时序问题的最快手段。
坑二:数据有推送,但模型位置乱跳
现象 :前端控制台能看到 axis-update 消息,数据看起来也正常。但 3D 模型偶尔会突然跳回原点,然后又跳回来。
排查过程:
- 打印收到的轴位置原始值,发现上位机推送的是实际物理位置(0-500mm)。
- 检查前端模型映射代码,发现我把轴位置直接赋值给了
mesh.position.y,没有做比例缩放。Three.js 场景中模型的运动范围是 0-5 单位,500mm 直接映射就飞出去了,触发了模型的边界限制,自动归零。 - 这是典型的"单位不一致"问题------上位机用毫米,前端用抽象单位,中间缺一层映射。
解决方案 :在前端 useWebSocketBridge.ts 的消息处理函数中增加映射逻辑:
typescript
ini
// 轴位置映射:物理范围 0-500mm → 模型范围 0-5 单位
const mappedPosition = (rawPosition / 500) * 5;
model.position.y = mappedPosition;
教训:数字孪生的"孪生"不是简单的数据镜像,而是带比例缩放和坐标变换的映射。前后端单位、坐标系必须提前约定好。
坑三:连接频繁断开重连
现象:前端每隔几秒就断开,然后自动重连,控制台一直刷"重连中..."。Network 面板的 WS 连接状态红绿交替。
排查过程:
- 先怀疑是上位机 WebSocket 服务端设置了超时断开。检查
DigitalTwinBridge代码,发现用的是System.Net.WebSockets的标准实现,没有主动超时逻辑。 - 用
ping -t持续 ping 上位机 IP,发现延迟波动很大,偶尔丢包。原来工控机连的是车间 WiFi,信号不稳定。 - 进一步发现,WebSocket 的心跳机制没配置。一旦网络抖动导致 TCP 连接半开,服务端和前端都感知不到,直到下次发送数据时才报错断开。
解决方案:
- 硬件层面:工控机改插网线,走有线网络。
- 软件层面:前端增加心跳检测,每 30 秒发一条
{"type":"ping"},服务端原样返回{"type":"pong"}。如果 10 秒内没收到 pong,主动断开重连。
教训:工业现场的网络环境比办公室复杂得多。数字孪生系统必须考虑弱网、断网、网络抖动的容错设计。心跳机制不是"可选",是"必选"。
坑四:反射加载 DLL 找不到类型
现象 :上位机启动时,DigitalTwinBridge.Initialize() 中反射加载 AutoFrameDll.dll 报错:Type.GetType("AutoFrameDll.MotionMgr") 返回 null。
排查过程:
- 确认 DLL 文件存在于运行目录下。
- 用 ILSpy 反编译
AutoFrameDll.dll,发现供应商内部的命名空间是AutoFrame.Motion,类名是MotionManager,不是我猜的AutoFrameDll.MotionMgr。 - 供应商给的接口文档里写的是"MotionMgr 类",但那是他们内部封装后的对外别名,实际类型名完全不同。
解决方案 :不用 Type.GetType(全名) 硬编码,改用遍历程序集类型的方式动态查找:
csharp
ini
Assembly asm = Assembly.LoadFrom("AutoFrameDll.dll");
Type motionType = asm.GetTypes()
.FirstOrDefault(t => t.Name.Contains("Motion") && t.GetProperty("Instance") != null);
这种写法更鲁棒,供应商改名也不怕。
教训:反射调用第三方 DLL 时,永远不要假设类名和命名空间。用特征匹配(类名包含关键词、有 Instance 属性)代替硬编码。
坑五:IO 点位名称对不上
现象 :轴位置同步正常,但 IO 状态更新完全不工作。前端收到的 io-update 消息中有数据,但模型没有任何变化。
排查过程:
- 打印前端收到的 IO 数据,发现点位名称是
1_X01(下划线)。 - 检查前端模型绑定的点位名称,写的是
1-X01(横杠)。 - 上位机的点位命名规范用了横杠,但供应商 DLL 内部返回的是下划线,我在
DigitalTwinBridge中直接透传了原始名称。
解决方案 :在 DigitalTwinBridge 中增加名称映射,统一转为横杠格式:
csharp
ini
string normalizedName = rawName.Replace('_', '-');
教训:命名规范是联调中最容易出问题的地方。最好在接口文档中明确定义命名规则,并在桥接层做一层标准化转换,不让上游的命名差异污染下游。
跑通后的效果验证
所有坑填平之后,联调成功的那一刻是这样的:
- 启动上位机,加载
AutoFrameDll.dll,WebSocket 服务监听 8100 端口。 - 浏览器打开
http://localhost:3000,3D 模型加载完成,右上角显示"WebSocket 已连接"。 - 在上位机界面上手动点动 M1 轴,从 0 走到 500mm。浏览器中的 3D 模型同步运动,Z 轴从底部升到顶部。
- 触发一个 IO 信号(比如上料气缸伸出),模型中对应的部件从灰色变绿色。
- 断开网线,前端显示"重连中",插回网线后自动恢复,数据继续推送。
性能数据:
- 轴推送频率:100ms(10fps),视觉上完全流畅。
- 端到端延迟:从点击上位机按钮到模型开始运动,约 150-200ms。
- CPU 占用:上位机新增约 3%(主要花在反射调用和 JSON 序列化),前端约 8-10%(Three.js 渲染)。
反思:数字孪生的工程价值
联调跑通之后,我录了一段 30 秒的 Demo 视频。发给同事看,他们的第一反应是:"这比看表格直观太多了。"
但这套系统的价值远不止"好看"。
1. 远程监控:工程师不需要跑到工位旁边,在办公室打开浏览器就能实时查看设备状态。疫情期间,供应商的售后工程师靠这个远程诊断了好几次问题。
2. 新人培训:3D 模型比平面图纸直观得多。新来的操作员看着模型运动,半天就理解了整条产线的物料流转逻辑。以前靠纸质流程图,至少要两天。
3. 故障回溯:配合 AgentClaw 的记忆系统,我把历史轴位置和 IO 状态存到了向量数据库。产线出故障时,可以把故障前 5 分钟的数据回放到 3D 模型上,像看"行车记录仪"一样重现设备动作。有一次 M2 轴报"跟随误差超限",回放发现是上料气缸提前退回了,导致夹爪空抓。没有数字孪生回溯,这个问题可能要查半天。
4. 与 Agent 联动:这是下一步要做的事。主动感知模块已经能通过 WebSocket 获取设备数据,Agent 发现异常后可以自动调取数字孪生的历史回放片段,附在告警报告里推送给工程师。"M2 轴异常,疑似上料气缸提前退回,请查看回放"------这是真正的智能运维。
写在最后
3D 数字孪生听起来很高大上,但真正做下来,核心就三件事:
- 把数据从设备里取出来(上位机 + 反射桥接)
- 把数据实时送到浏览器(WebSocket + JSON)
- 把数据映射到模型上(坐标变换 + 状态绑定)
每一件事都不难,难的是把三件事无缝串起来,并在产线的真实网络环境中稳定运行。
联调跑通的那一刻,我看着浏览器里的 3D 模型跟着真实设备一起运动,突然意识到:我做的不是"数字孪生",我是在给产线装一面"镜子"。这面镜子能照见过去(历史回放)、现在(实时监控)、未来(Agent 预测)。
这,就是一个 AME 在数字孪生领域的打开方式。