一直在产线自动化项目里打转的朋友应该都有印象真正把一个AGV调度系统跑起来难点往往不在AGV本体怎么走而在上位机怎么把 ERP、MES 这些上下游系统串起来再把任务可靠地发下去。最近我刚完成一个基于 C# WPF 开发的 AGV 上位机执行系统算是把 WPF 界面、数据库技术、多线程调度还有 AGV 路径规划这几个模块完整打通了。这篇就把项目从选型到落地过程中比较关键的部分拆开讲透特别是那些只在现场踩坑才能总结出来的细节。这套系统的定位很清晰它处在 MES 和 AGV 车体控制器之间的执行层。MES 下发工单上位机负责把工单拆解为 AGV 可执行的任务经过路径规划和交通调度后通过 TCP 或 HTTP 发给车体同时把任务状态、位置信息、电量数据持久化到数据库实时同步回 ERP 和 MES。对于正在做同类项目的同行或者打算用 WPF 切入工业上位机领域的新人这篇的经验应该能帮你避开不少弯路上的雷。1. 项目整体定位一套执行系统为什么敢连 ERP 和 MES1.1 从物料流转场景看系统职责很多第一次接触 AGV 上位机的人会误以为它只是个“遥控器”把车叫过来、叫过去就行。实际生产环境完全不是这个节奏。以我们那条线为例ERP 下了生产工单以后MES 按工站需求拆成配送任务MES 再调用上位机的接口把任务投递进来。上位机要负责判断哪台 AGV 当前空闲、路径是否拥堵、电量还够不够跑到目标点把这些全部算清楚以后才生成具体的运动指令。如果不够直观可以把上位机想象成一个物流调度室ERP 是销售下单的前台MES 是分拣中心AGV 是送货司机而上位机就是坐在调度室里盯着地图、对讲机指挥的人。它不只是把任务转交出去还要管司机走到哪了、有没有堵车、哪台车该去充电、哪条路正在修。正因为这个定位它的核心挑战不在单一环节而在“连接”这件事上。1.2 为什么选择自研而不是直接买成品调度系统市面上确实有成熟的 AGV 调度软件但你大概率会遇到三类问题一是各家 AGV 车体的控制协议不同对上位机的接口要求差异很大成品软件未必能覆盖二是 ERP/MES 的接口往是定制开发的和成品调度系统的衔接总会有一层厚厚的适配层三是实施周期和费用往往不低中小项目很难承受。自己开发就不一样了。你可以完全掌控通信协议想接私有 TCP 就接私有 TCP想走 HTTP/JSON 就走 HTTP/JSON数据库字段按业务需求设计不需要迁就软件厂商的固定表结构界面上要做一个 3D 动画看板或者把车的位置叠加到现场平面图里都可以自由实现。当然代价也很直观所有坑都得自己踩一遍。从技术栈上看WPF 来做这套系统非常合适。C# 在设备通信、多线程、数据库访问这些方面都有成熟类库WPF 最擅长做的实时看板、状态高亮、动画运动轨迹又正好命中工厂数字化的门面需求。2. 技术选型解析WPF 在工业上位机场景里的优势2.1 和 WinForms、Web 相比WPF 赢在数据驱动界面有人会问WinForms 不也能做上位机吗确实能但做出来的界面手感和信息密度完全是两回事。AGV 上位机界面通常需要同时展示几十台车的位置、任务状态、路径状态、电量阈值还要支持操作员快速筛选和操作。WinForms 在维护这种复杂交互时会逐渐变成噩梦每个控件都要手动同步状态一个任务状态变化可能要更新七八个控件。WPF 的数据绑定机制从根本上解决了这个问题。你在 ViewModel 里定义一个ObservableCollectionAgvViewModel界面上的列表和状态卡片就会自动跟随数据变化刷新。再配合 MVVM 模式UI 只负责展示业务逻辑全部写在 ViewModel 和 Service 层测试和修 bug 都会轻松很多。这也是我做这个项目最直观的体感大界面和小界面用 WPF 写的体验完全不一样数据越复杂、状态越多WPF 的收益越明显。2.2 数据绑定、命令和 Prism 框架的取舍如果只是做一个几百行代码的演示程序用原生 MVVM 就够了。真实项目里模块越多就越推荐引入轻量的 Prism 框架或者至少自己实现一个简单的 ViewModel 基类和 DelegateCommand。我们的做法是用 Prism 做模块化容器把设备通信模块、数据库模块、任务调度模块、UI 模块拆成独立区域。WPF 里一个非常容易翻车的地方是INotifyPropertyChanged的通知粒度太粗。比如一辆 AGV 的状态从“空闲”变成“搬运中”你可能会重新赋值整个车辆对象导致界面所有相关绑定全部刷新甚至引起布局抖动。正确做法是尽量细化属性通知只对状态属性单独触发这样既节省性能也避免操作员视觉疲劳。2.3 HttpClient 和异步任务不能让通信阻塞 UI 线程上位机每天最大的消息量集中在 AGV 状态回报和 MES 任务拉取上。这类 IO 操作绝对不能走同步代码否则通信线程阻塞、UI 全部卡住操作员第一反应就是系统死机了。C# 的async/await配合HttpClient是标准答案关键是要注意上下文捕获问题。在 WPF 里如果你在 UI 线程的async方法里用await等网络请求默认会尝试回到 UI 线程继续执行。这本身没问题但容易引发死锁如果上游代码同步调用了这个异步方法而 UI 线程又在等它返回就典型地卡死了。我的习惯是公共 Service 层的异步方法不要直接操作 WPF 控件只返回TaskT由 ViewModel 层去决定是否切换上下文数据库访问和网络通信统一走后台Task.Run或真正的异步 APIUI 线程永远只做数据绑定和轻量计算。3. 数据库与多线程执行系统的两个底座3.1 数据库选型本地 SQLite 加远程 MySQL 的组合思路AGV 上位机的数据库需求比普通业务系统更特殊它既要高可用又要低延迟还要对实时状态和历史轨迹做并发写入。我们最终采用了本地 SQLite 加远程 MySQL 的双层组合行驶数据先落到本地再通过数据库同步任务发到中心库。为什么不用 SQL Server不是不行而是工厂现场的部署环境差异太大。有的客户机房里有 Windows Server有的客户根本不想多维护一台数据库机器。MySQL 轻量、部署快、社区资料多而且和 WPF 的对接非常成熟。SQLite 则负责本地缓存AGV 每一条心跳记录、任务流水都会先写入本地文件避免因为网络抖动丢失消息。数据库连接池这块需要特别注意。很多人误以为连接池是 ORM 自动管理的其实默认参数在工业高频写入场景下经常不够用。我们用的是 MySQL 官方 Connector 的自带连接池建议把Poolingtrue、MinimumPoolSize5、MaximumPoolSize50显式写进连接字符串并在程序启动时预热连接避免高峰时刻临时建连导致的延迟。3.2 核心表结构设计任务表、状态表、地图表直接上我们项目的精简表结构方便参考表名关键字段用途agv_tasktask_id, task_type, target_node, priority, status, create_time, finish_time任务队列与执行记录agv_vehiclevehicle_id, current_node, battery, task_id, status, error_code车辆实时状态快照agv_map_nodenode_id, x_coord, y_coord, node_type, is_charge, is_rest_area地图节点定义agv_route_edgeedge_id, start_node, end_node, direction, length路径连接关系agv_loglog_id, vehicle_id, log_type, content, create_time操作日志与异常记录任务表是系统的核心我把status设计成一套有限状态机待执行、已分配、已到达取货点、搬运中、已到达卸货点、已完成、异常。MES 每次查询任务状态就是查这个字段所以它的更新频率非常高必须走索引。priority字段也很关键紧急任务可以直接插队但插队逻辑不能只改优先级数字还要在调度模块里做队列重排。3.3 多线程模型通信线程、调度线程和 UI 线程怎么协同AGV 上位机里最让人头疼的就是线程问题。我们设计了三个独立线程组通信线程组负责和每台 AGV 保持 TCP 长连接接收心跳、位置、状态上报同时轮询 MES 任务接口。调度计算线程组做路径规划和任务分配。规划计算是 CPU 密集型的必须放到独立线程避免阻塞通信。数据库写入线程组用BlockingCollection接收日志、位置轨迹等消息批量写入 SQLite。线程之间靠什么传数据我的经验是能用ConcurrentQueue就别用共享变量能发事件就别跨线程直接访问界面元素。调度线程算好一条路径后往“待发送队列”里塞指令通信线程从队列里取出并下发两者之间完全不直接接触就不会出现 A 线程改了一半、B 线程读了一个脏值的问题。这里要特别强调一下WPF 的元素只能由 UI 线程修改。后台线程想更新界面上某台车的状态必须通过Dispatcher.Invoke或异步的Dispatcher.BeginInvoke回到 UI 线程。如果后台线程密集更新建议把事件聚合成批次不要每条心跳都Invoke一次否则 UI 刷新频率跟不上界面照样卡。4. AGV 路径规划与调度逻辑4.1 单机 A* 算法的落地细节AGV 最常见的路径规划算法就是 A*。它本质上是在地图网格上做启发式搜索用f(n)g(n)h(n)来选择优先扩展的格子。g(n)是从起点到当前格子已经产生的成本h(n)是当前格子到终点的估算剩余成本通常用曼哈顿距离或欧氏距离。我做项目时最大的感受是算法原理花半小时就能看懂但工程化的时候要考虑很多边界情况。比如A* 找到的最短路径是中心线路径但 AGV 车体有宽度弯道要留安全距离。我们会在路径生成后做一次“膨胀处理”把节点坐标按车体半径向外偏移或者在地图数据里预先标注哪些节点和边是宽通道、哪些是窄通道路径规划时直接过滤掉超出车体能力的路段。还有一点工业现场的路径往往不是标准网格而是预设的行驶通道和折返点。对这种情况我更推荐用图搜索而不是网格搜索。把地图抽象成agv_map_node和agv_route_edge两张表A* 就在这个有向图上运行搜索效率和可维护性都高很多。4.2 多 AGV 避碰与交通管制方案单机路径规划做完多车场景才是真正的大坑。两辆车同时驶向同一个交叉口怎么办路径重叠时该让哪辆车先走我们的方案分两层静态锁段法把每条路径边按长度分成若干锁段车要进入一段路前必须申请锁拿到锁才能进入驶出后立即释放。锁段由调度线程统一管理可以防止多家车同时占用同一段路。动态优先级如果一个锁段被占用后到的车会等待如果等待超时调度模块会重新规划一条避开当前拥堵区的替代路径。这里的重点是“替代路径”必须在线计算机器不能只靠简单等待。用代码表示关键的部分就是每台车维护一个状态机。状态机里我会监听“等待锁段”事件超过 5 秒就触发重规划。实际应用中发现很多死锁其实是任务分配导致的。A 车要取货点 X路径必须经过 B 车当前停靠的卸货点而 B 车又占着锁不肯走两头等待。解决办法是队列全局扫描发现等待关系成环时强制让较低优先级的车让位到临时停车点。4.3 要不要上强化学习做多车规划最近“多 AGV 路径规划强化学习”这个话题很热我们的结论是在大部分生产基地传统调度足够用强化学习适合极大规模、动态性极强的场景比如几十台车同时运行、订单秒级变化。为什么传统方案依然能打因为工厂路径约束明确任务类型相对固定规则先验可以覆盖一大部分情况。强化学习泛化能力强但不好解释“为什么这辆车选择了这条路径”在事故分析和责任界定时很头疼。当然如果以后项目节点数超过几百个、车内数量翻倍我会认真考虑把强化学习用在离线预规划层在线实时调度仍然保留规则锁段以保证确定性。5. ERP 与 MES 对接数据流怎么串起整条链5.1 接口协议选择HTTP/JSON 和 WebService 的取舍很多老 ERP 系统还在用 WebService新一点的 ERP 或 MES 一般提供 HTTP/JSON 接口。项目里我们两种都遇到过。接 MES 用的是 HTTP/JSON简单直接HttpClient发 POST 请求序列化请求体反序列化响应体就完事了。接 ERP 时就比较痛苦连的是老牌 ERP 的 API动不动就抛连接异常后面会专门讲这个问题。接口设计上我建议做一个独立的IMesService和IErpService接口内部封装协议细节。上层调度模块只调FetchWorkOrderAsync()、ReportTaskStatusAsync()完全不关心底层是一 HTTP 还是 WebService。这样换 ERP、换 MES 版本只需要替换实现类不会牵连到核心调度逻辑。5.2 数据同步策略轮询、消息队列还是数据库同步工具ERP/MES 和上位机之间的数据同步方式很多项目里要根据实时性要求来选轮询接口最简单定时去 MES 拉取新任务。我们的频率是 2 秒一次对大多数场景足够。消息队列适合实时性要求高的场景通过 MQ 中间件推送任务上位机订阅后立即处理。加重了中间件运维负担但效果最好。数据库同步工具当 MES 使用若依框架这类开源项目、数据存在 MySQL 里时可以直接同步数据库表。例如用 Canal 监听 binlog 日志把新增任务实时同步到上位机的本地库上位机一查本地库就发现新任务响应非常快而且不增加 MES 系统接口压力。数据库同步软件在工业场景里要谨慎使用。它做的是“最终一致性”同步延迟在最坏情况下可能超过 10 秒如果 MES 对任务下达有严格的秒级要求就要改成接口或者 MQ 方案。我们的实践是“MES 下发的任务用接口拉取状态回报用数据库同步工具”两者互补。5.3 对接 ERP 连接异常一个实际排查案例项目中遇到的最典型问题就是热词里有提到的“易飞 ERP 系统连接异常”。现象是上位机经常在拉取工单时偶发超时有时一小时一次有时半小时就报错。最初以为是网络问题但 ping 服务没有任何丢包。排查后发现问题出在连接池上。这个 ERP 系统的 API 要求每次请求都带上一个会话令牌而我们的会话令牌没有做无缝续期偶尔会在服务端过期和本地刷新之间产生几秒窗口请求落在这个窗口里就会失败。另外ERP 的 API 连接数有限上位机高频轮询时前端服务器来不及释放连接池句柄导致连接堆积。解决措施有两个一是对令牌做预刷新提前 5 分钟判断令牌是否快过期主动换新令牌让发出去的请求永远带着有效令牌二是降低调用频率把原来 1 秒一次的全量查询改成 5 秒一次的增量查询只在modify_time有变化的记录才拉全量。改造之后异常彻底消失。6. 常见问题与排查技巧实录6.1 WPF 界面卡顿从绑定性能查起界面卡顿最简单粗暴的判断方法就是用Stopwatch量一下 UI 线程空余时间。如果发现 UI 线程占有率很高优先怀疑是绑定的集合没有开启虚拟化或者某个属性的 getter 做了重计算。我们用过一个实时位置列表里面装了 200 台车的坐标和 5 个状态属性列表项没有开启虚拟化时每秒钟刷新 10 次CPU 直接跑满。优化方案是给ItemsControl外层套上VirtualizingStackPanel并把容器重用法打开滚动和刷新立刻顺滑。另外建议把地图上那些频繁移动的小车图标用一层独立Canvas绘制不要每次替换整个ItemsControl。图标是 UIElement布局测量开销很大一多就容易卡。6.2 数据库写入慢导致任务积压上线初期我们遇到过数据库写入任务堆积严重的情况。事故剧照是这样的任务日志以每秒几十条的量级写入而调度线程还在不断往队列塞新任务后台写入线程追不上积压到几万条以后内存占用飙升。后来看到问题背后的两个直接原因一是用了逐条 INSERT没有批处理磁盘 IO 效率低二是 SQLite 没有开启 WAL 模式写操作互相锁文件导致堵塞。修复后把日志写入改成“攒够 200 条或者满 500 毫秒批量提交一次”SQLite 的journal_mode设为 WAL写入性能直接提升了一个数量级。另外也把调度线程的队列长度做了熔断超过设置的阈值就暂停下发新任务等队列消化到安全线以下再恢复。6.3 TCP 长连接断线重连与任务丢失AGV 车体通信用的是 TCP 长连接最怕的就是半开连接。车端 WiFi 信号弱或短暂断电时服务端 TCP 连接表面上还活着实际上已经收不到数据了。如果不做心跳检测你会发现所有车都显示“空闲”但车其实已经离线。我们的方案是服务端每 3 秒 ping 一次车端车端如果 10 秒没有任何响应就判定为离线把该车执行中的任务状态改成“异常待恢复”。与此同时车端重连恢复后第一件事就是向上位机上报本地任务序号上位机基于这个序号决定是继续执行还是重发任务保证任务不丢。这里有一个细节我特别注意重连不代表恢复。如果任务阶段是“卸载完成”重连后可以直接回空闲状态如果任务阶段是“搬运中”车体可能停在半路必须等待操作员手动确认位置后才能恢复执行。状态机里把这类任务标记为人工介入能避免车自己乱跑造成安全隐患。6.4 长期运行时内存缓慢上涨工业上位机基本是 7×24 小时不停机的跑两三天内存涨一点可能没人注意跑一个月内存翻倍就有人骂系统是“带病工作”。我们用dotnet-counters和PerfView抓了几次内存快照发现元凶基本是事件泄漏数据处理回调里订阅了控件事件对象不再使用但没退订就导致 GC 永远回收不了它们。修这类问题没有捷径只能规规矩矩遵循“谁订阅谁退订”的原则。比如车辆状态变化的事件只在页面激活时订阅关闭页面时统一Unsubscribe。另外还启动了定时托管线程去清理缓存字典确保长期运行的内存曲线是水平的而不是往上走的。7. 一些值得后续扩展的方向这套系统目前已经稳定跑在产线上但有意向的功能迭代也一直没停。比如 WPF 界面里加 3D 动画看板的呼声很高比起传统 2D 平面图3D 能直观显示出 AGV 在楼层间的行驶路径对现场讲解和远程监控都有帮助。WPF 里的 3D 可以通过Viewport3D实现数据绑定和动画机制依然适用但要注意模型数量过多时帧率下降的问题。还有一个方向是把调度算法独立成服务和 UI 分开部署。现在 WPF 和调度计算在同一个进程里界面偶尔卡顿会影响任务下发长期看还是拆开更稳妥。拆开以后WPF 客户端只做监控和手工干预调度引擎作为 Windows 服务跑在后台再配合现有的 MySQL 中心库整条链路会更抗风险。另一个有意思的方向是用 WPF 配合数据库同步做“虚拟产线仿真”。在本地库导入完整的 ERP 工单数据加上 AGV 调度日志可以离线回放整个生产流程排查历史问题非常方便。这是把现有系统积累的数据盘活的好思路也算给后续迭代留了个不错的种子。