1. 先从业务链条说起ERP、MES和AGV之间谁指挥谁接到这个项目的时候客户现场的ERP和MES已经稳定跑了好几年生产工单、物料清单、工序报工都走得好好的。真正的问题出在物流环节车间两万多平米物料转运全靠人工叉车高峰期几个工位同时要料调度全靠对讲机喊。所以客户想上AGV但又不满足于按钮呼叫一辆车这种单机玩法他们要的是ERP里来了生产计划MES拆成工序任务AGV上位机自动把物料送到对应工位整个过程不用人盯。这就是标题里WPF开发AGV上位机执行系统要做的事也是整个项目里最有含金量的一层。1.1 三个系统在车间里的真实分工先聊清楚角色不然后面所有设计都会跑偏。ERP计划层。管订单、物料需求计划、库存水位。它只关心明天要生产什么不关心哪台车在几点把货送到几号工位。MES执行层。把ERP的生产订单拆成工序级任务跟踪每个工单在哪个工位、做了多少件、用了什么物料。它掌握车间的实时节奏。AGV上位机设备调度层。接收MES下发的搬运需求结合当前车辆位置、路况、站点忙闲把任务拆成一条条可执行的移动指令发给具体的AGV再回报执行结果。我用生活里的场景类比一下ERP是公司老板只拍板这个月要出货3000件MES是车间主任排工序、盯进度、管物料AGV上位机就是最底层的调度员接到车间主任的电话后告诉哪辆叉车司机去哪儿、搬什么、放哪儿、什么时候回来。三者的信息粒度完全不同不能互相替代。1.2 为什么必须单独做一个AGV上位机很多人会问AGV厂家不是自带调度系统吗为什么还要自己开发上位机这个问题我每年都会被问到好几次。答案分两层。第一厂家的调度系统普遍面向单品牌、单型号的AGV而客户现场往往存在多品牌混用甚至还有老旧的潜入式AGV和新的二维码导航AGV并存。厂家自己的软件指挥不了别家的车但上位机通过标准协议可以统一接管。第二也是更关键的一点厂家的调度系统只解决车怎么走不解决任务从哪来和做完之后怎么反馈。它通常没有和MES深度绑定的概念工单、批次、物料追溯这些业务数据都接不进来。你想要的不是一台能动的车而是一套能自动接单、自动派车、自动回执的物流中枢。这个中枢只能根据自己车间的业务流程来定制所以上位机必须自己写。1.3 从工单到搬运任务的完整业务流把整条链路画一遍你就知道上位机的接口设计都要围着谁转。ERP下发生产工单给MESMES生成工序计划。MES根据工序计划算出物料需求产生搬运需求比如3号工位需要A料200件。上位机通过接口轮询或者消息订阅拿到搬运需求转换成标准任务。上位机根据任务类型、目标站点、车辆状态选一台合适的AGV。AGV执行移动、取货、放货期间实时上报位置和状态。任务完成后上位机构造回报报文给MESMES更新工序状态和物料消耗。遇到异常缺料、堵车、车辆故障上位机要能自动挂起任务并通知MES重新调度。这套流程里上位机就像整个车间的物流翻译官一边用业务语言跟MES对话一边用设备语言跟AGV说话。两边都不需要改自己的底层逻辑所有业务匹配都在上位机里完成。这也是我建议所有做类似项目的团队第一优先想清楚的事情先理业务链路再谈技术选型。2. 为什么选中WPF上位机界面框架选型的底层逻辑技术选型这件事最怕跟风。我在决定用WPF之前把市面上能接手这个场景的方案都过了一遍最终选择WPF不是因为它最新最潮而是因为它在桌面级实时监控这个领域确实有不可替代的优势。2.1 备选方案横向对比方案开发效率界面表现力实时通信能力团队上手难度典型场景WinForms高弱控件老旧中低传统简单设备监控WPF中高强支持动画/模板/3D强中复杂上位机、可视化调度QtC中强强高跨平台、性能苛刻的工控Web前端HTML/JS高强中需WebSocket等中轻量看板、远程监控从上表能看出来WinForms开发最快但真要做车辆实时位置、图层缩放、设备状态闪烁这类效果写起来非常痛苦基本要靠自定义控件和GDI代码量大还难维护。Qt性能没得说可团队都是C#背景临时转C成本太高。Web前端做看板很漂亮但上位机要常驻工控机要直连TCP、串口浏览器里做这些总隔着一层还要考虑断网、开机自启等问题。WPF恰好卡在中间开发语言是C#团队无缝衔接XAML声明式界面让我能把地图、车辆、站点都做成可视化元素而不是一堆坐标数字界面线程模型清晰配合异步编程做实时通信是成熟套路。所以选WPF不是拍脑袋是拿需求一项项比出来的。2.2 数据绑定和MVVM带来的开发效率WPF里最值钱的东西我认为是数据绑定。做上位机免不了大量数据变了界面要跟着动的需求车辆速度、电池电量、任务进度、报警状态全屏都是实时数据。如果用WinForms的写法每个值变了都要手动给控件赋值比如label1.Text car.Speed.ToString()一百辆车就有几百个控件代码全是赋值语句。WPF里我只需要把界面元素的属性绑定到ViewModel的属性上DataTemplate DataType{x:Type vm:CarViewModel} Grid Ellipse Fill{Binding StatusColor} / TextBlock Text{Binding CarNo} / TextBlock Text{Binding Speed, StringFormat{}{0} m/s} / /Grid /DataTemplate车辆的速度值一变只要ViewModel里正确触发PropertyChanged界面上这个文本自动刷新。整个界面层几乎没有手工赋值的代码全部通过绑定驱动。MVVM模式下地图拖拽、车辆选中、任务下发这些逻辑也全写在ViewModel里UI设计师改XAML不影响我的业务代码。对AGV上位机这种界面复杂、数据刷新频繁的项目这套模式省下的工作量是巨大的。2.3 界面呈现能力从二维地图到数据看板AGV上位机的核心界面就是一张车间地图上面分布着站点、路径、车辆实时位置。WPF的Canvas布局天然适合做二维地图可视化我可以用Path画轨道线用Ellipse表示车辆用动画实时移动车辆图标。更关键的是当客户提出要一个大屏看板能看到每辆车的状态曲线和任务完成量这种需求时WPF的Chart控件、复杂的渐变背景、数据模板都应付得来。网上热词里提到WPF实现3D动画看板我也实际做过一个简化版把仓库货架用Viewport3D画出来AGV的取放货动作做了个简单的3D动画视觉效果相当能打。客户演示时满意验收时顺畅这套表现力在纯WinForms里很难做出来。2.4 实时通信与异步编程的配合上位机最大的技术特点就是实时。TCP要一直挂着接收数据数据库要异步读写界面同时还要响应用户点击。WPF的Dispatcher模型保证了UI线程单线程访问配合async/await可以让通信代码用同步的写法、异步的执行不会阻塞界面。举个例子车辆回报位置数据到达我需要在界面上刷新十几台车的坐标如果在通信线程里直接改UI控件会抛出调用线程无法访问此对象的异常。WPF的Dispatcher.Invoke把更新操作封送到UI线程这事就解决了。异步通信加线程封送这套组合是WPF做上位机的标准套路稳定可靠。当然WPF有它的短板比如跨平台不行Linux下跑不了。但上位机基本都部署在Windows工控机上这个短板在AGV调度场景里根本不算问题。选型就像找对象关键是合适不是最好。3. 系统整体架构一条数据从工单到车轮的完整链路确定技术栈之后第二步就是搭架构。我见过不少AGV项目死在架构混乱上界面代码里直接写TCP收发数据库访问散落在各处MES接口调一半忘了一半。这个项目的架构我一开始就按四层来切后面所有开发都沿着这条线走。3.1 四层架构划分UI层WPF所有界面包括地图、车辆列表、任务看板、告警窗口。里面只有视图和ViewModel不写任何业务逻辑。业务层任务调度引擎、车辆分配算法、路径规划、状态机管理、和MES对接的服务。这一层是系统的头脑。通信层TCP客户端管理、报文编解码、心跳处理、断线重连。负责和AGV、MES的所有网络交互。数据层数据库访问、本地缓存、配置管理。统一封装的仓储接口业务层完全不关心数据存哪、怎么存。关键约束是依赖方向UI层只调业务层业务层只调通信层和数据层的接口绝不允许反向依赖。比如任务调度引擎要发一条指令给AGV它只需要调用通信层暴露的SendCommand(carId, frame)方法完全不用管报文是怎么编的、TCP断了要怎么办。这样做的好处是后期换通信协议或者换数据库只改动对应层其他层毫发无损。3.2 与MES/ERP集成的三种主流方式上位机想从MES拿任务主要有三条路我列个表对比集成方式原理优点缺点适用场景REST APIMES提供接口上位机定时轮询或回调松耦合接口清晰易调试需要MES配合开发轮询有延迟MES团队能排期支持中间数据库表双方共用一个数据库通过表交换数据实现简单双方改动少要统一数据库权限和管理规范易出脏数据老系统改造MES不方便动消息队列通过RabbitMQ/Kafka传递任务消息实时性强解耦彻底可削峰多一套中间件要运维新建系统、任务量大、对实时性要求高我这次用了REST API 中间状态表的组合。实时任务下发走REST因为MES对上线一台AGV本来就要开发接口任务完成后的回执也走REST保证状态能立刻反馈到MES业务闭环里。中间状态表用来做双向对账每天晚上查一次当天任务数两边的记录对得上就说明系统是健康的。消息队列在这个场景里没有强需求为了让现场少一个中间件就暂时没上。3.3 接口调用链的代码形态拿最常用的获取搬运任务来说上位机的处理逻辑大概是public class MesTaskService { private readonly HttpClient _httpClient; public async TaskListMesTaskDto FetchPendingTasksAsync() { var response await _httpClient.GetAsync(/api/mes/tasks?statuspending); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsAsyncListMesTaskDto(); } public async Taskbool ReportTaskResultAsync(string taskId, int result) { var payload new { taskId, result, finishTime DateTime.Now }; var response await _httpClient.PostAsJsonAsync(/api/mes/tasks/report, payload); return response.IsSuccessStatusCode; } }这段代码放在业务层UI不直接接触通信层也不关心内容是JSON还是XML。拿到的任务先落地到本地数据库再交给调度引擎统一决策。不管将来换MES厂商还是改接口版本只动这个MesTaskService类就行。3.4 部署形态与本地缓存上位机系统通常部署在一台专用的Windows工控机上和AGV在同一局域网通过交换机连接MES服务器在另一个网段走标准网络接口。为了让系统在MES短暂宕机时还能继续干活我在本地用SQLite做了一层缓存任务、车辆状态、操作日志都先落本地保证不丢网络恢复后自动补报。这里踩过一个教训一开始把SQLite读写也放在业务线程里同步执行任务一多UI明显掉帧。后来改成独立的数据访问层加异步写入界面响应立刻恢复正常。上位机这种系统架构上偷懒的每一笔技术债都会在上线后连本带利还回来。4. 数据库设计任务表如何完成从计划到执行的状态流转AGV上位机要存的数据其实不复杂但实时性多线程分布式事务这三个词会让数据库设计瞬间变得重要。我见过有人拿一张大表从头存到尾字段堆了三四十个状态靠字符串硬编码最后任务一多到处是脏数据。下面是我整理过的一套实用设计。4.1 核心表结构设计我按功能域拆成几组表每组职责单一基础资料类AGV_Info车辆表存车辆编号、类型、所在站点、状态、Station_Info站点表存地标编号、名称、坐标、是否忙闲。任务类Task_Info任务主表存任务编号、来源、类型、优先级、状态、创建时间、Task_Detail任务明细存起点站点、终点站点、物料代码、数量、超时时间。日志类Exec_Log操作日志、Alarm_Log告警日志、Heartbeat_Log心跳记录可定期清理。Task_Info是我认为整个库的灵魂建表大概长这样CREATE TABLE Task_Info ( TaskId NVARCHAR(50) PRIMARY KEY, Source INT NOT NULL, -- 来源1MES下发2人工创建3异常补发 TaskType INT NOT NULL, -- 类型1送料2收空箱3充电 Priority INT NOT NULL DEFAULT 5, -- 优先级1最高10最低 Status INT NOT NULL DEFAULT 0, -- 状态机见下方说明 AssignedCar NVARCHAR(20) NULL, -- 分配的唯一车辆 CreateTime DATETIME NOT NULL, StartTime DATETIME NULL, FinishTime DATETIME NULL, Remark NVARCHAR(200) NULL );站点、车辆这些基础表反而不用太早优化因为数据量小、访问频率高正常建主键索引就够了。真正的设计重点在任务表的状态流转和并发控制上。4.2 任务状态机系统可恢复性的根基任务的每个状态必须清晰可查这是上线后排查问题的救命稻草。我定义的状态如下0-待下发任务刚写入本地库还没决定派哪辆车。1-已下发已通过TCP发指令给指定AGV等待车辆应答。2-执行中AGV已应答并开始移动/取货/放货。3-已完成AGV回报任务成功上位机已回执MES。4-失败AGV报告异常或者响应超时。5-已取消人工干预取消或上游MES撤回。状态的迁移必须满足约束比如0→1→2→3是正常链路2不能跳到0。我在代码里用一个状态机类统一管理所有修改状态的操作都先校验当前状态是否允许跳转。上线后我查问题基本靠这个状态字段就能定位某辆车卡在哪一步哪个环节没应答一目了然。没有状态机的任务表就是一坨随时间腐烂的字符串集合。4.3 多线程并发访问与数据一致性任务表会有多个入口同时改调度线程给任务派车、通信线程回报任务状态、UI线程可能人工取消任务。如果大家都直接怼同一行轻则锁等待重则死锁或覆盖更新。我采用的办法是所有状态修改统一走一个仓储方法内部用事务加条件更新而不是select后改。核心SQL思路是UPDATE Task_Info SET Status NewStatus, AssignedCar CarNo, StartTime GETDATE() WHERE TaskId TaskId AND Status ExpectedStatus这条SQL只有一行作用说明状态符合预期更新成功返回0表示状态已变调用方就知道是并发冲突需要重新拉数据。这套乐观锁思路在任务表这种高频更新场景里比全局锁高效得多。车辆分配的时候我还会给AssignedCar建唯一约束相当于从数据库层面保证一台车同一时间只能执行一个主任务避免调度逻辑抽风导致一辆车同时接两个单。4.4 容量规划与历史数据归档AGV上位机一天会产生大量任务记录和心跳日志。现场18台车心跳每3秒一条一天下来就有50多万条任务记录每天几百到上千条例行运行半年就是几十万行。心跳日志这类时序数据我设定了保留30天用后台定时任务把超过天数的物理删除或者迁移到单独的Heartbeat_Log_History表。任务记录和告警日志保留一年满了就做分区归档。数据库文件也要做定期备份策略最好每天凌晨备份一次防止工控机硬盘故障导致整个系统瘫痪。这一块虽然不起眼但哪天磁盘满了、系统写不进去数据你就会明白它有多重要。5. 多线程应用让上位机同时管住几十台车的关键手段AGV上位机是一个天然的多线程系统甚至可以说写不好多线程代码整个调度系统就是定时炸弹。我见过太多项目开始时单机演示都正常一旦真连上十几台车UI卡死、数据错乱、任务超时全都冒出来。根本原因就是没把线程模型想清楚。5.1 为什么必须有独立的线程池和任务队列试想一下现场的并发场景通信层同时维护十几条TCP连接随时有数据进来每台车每1~3秒上报一次位置和状态MES可能随时下发新任务或撤回旧任务调度引擎要根据所有车的状态实时决策UI需要每秒刷新好几次地图和看板数据库要异步写入日志和任务状态。如果把这一切全放在同一个线程里排队处理任何一项稍慢一点全系统的实时性都会被拖垮。所以第一原则是耗时操作一律不进UI线程每个职责都有独立的工作线程或异步任务。5.2 线程划分方案我实际用的线程模型大概这样线程/任务职责实现要点UI线程界面渲染、用户交互只做绑定和轻量计算不碰通信通信接收线程每台AGV一个Task监听TCP数据、解析报文收到完整报文后放入统一队列报文处理任务从队列取报文分发到对应业务模块用Channel或者ConcurrentQueue做缓冲调度引擎独立Task循环扫描待下发任务、匹配车辆、发送指令每隔500ms~1000ms跑一轮心跳监控任务检查各车最后心跳时间超时告警后台循环扫内存字典数据库写库任务异步写操作日志、心跳记录、任务变化批量写入避免频繁连接5.3 UI线程安全从踩坑到熟练使用Dispatcher刚接手WPF那会儿我犯过一个经典错误在通信线程里直接改界面上的车辆控件结果抛了InvalidOperationException。后来才知道WPF控件只能在创建它那个线程通常是UI线程上访问。我的解决方案是底层线程永远不碰UI它们只把数据写到ViewModel的属性里由WPF的绑定机制自动刷新界面。当需要主动从后台线程更新界面时用Application.Current.Dispatcher.BeginInvoke把操作丢到UI线程消息队列里而不是Invoke。为什么用BeginInvoke因为它是异步的不会让后台线程死等UI执行完避免拖慢通信处理。一个具体的坑如果每台车每秒上报一次位置你在UI线程里每次刷新地图坐标18辆车就是每秒18次界面更新加上地图动画很容易卡。我的做法是位置刷新做了节流收到位置数据先更新内存模型UI绑定触发频率由程序控制比如每200毫秒批量刷新一次车辆图标的坐标。实测下来流畅度和实时性都能兼顾。5.4 用async/await优雅处理大量并发连接C#的async/await非常适合上位机的通信场景。每台车一个接收循环写法是同步的执行却不会阻塞线程private async Task ReceiveLoopAsync(TcpClient client, CancellationToken token) { using var stream client.GetStream(); var buffer new byte[4096]; while (!token.IsCancellationRequested) { int readCount await stream.ReadAsync(buffer, 0, buffer.Length, token); if (readCount 0) { // 对端关闭连接 break; } OnDataReceived(client, buffer.AsSpan(0, readCount).ToArray()); } }这里有个关键点是CancellationToken。系统要退出、或者某台车被手动断开时用令牌通知接收循环结束而不是直接硬杀线程。硬杀线程可能导致TCP连接资源没释放、数据写到一半后面重连就出各种怪问题。5.5 多台车并行调度的一个机制路段锁上位机调度引擎最核心的算法是多车路径规划。热词里老提到A和多AGV路径规划强化学习实际上现场项目里能跑得最稳的还是把地图抽象成有向图每台车按A算最短路径同时加一个路段锁和时间窗机制一张地图切成若干路段同一路段同一时间段只允许一台车进入。如果有两台车争抢同一路段调度引擎根据优先级和任务紧迫度让其中一辆等待或绕行。这套模型不算先进但胜在稳定可控、可解释。强化学习在仿真里跑得再好真车一上传感器误差、故障、人为占道都会把它打回原形。我的经验是调度系统先把规则逻辑做扎实再谈花哨算法对客户来说稳定不出错比偶尔超水平发挥重要得多。6. 通信协议与报文设计上位机和AGV之间的行话上位机与AGV之间的通信协议是整个系统的神经网络。协议设计得不好后面每接一种新车都要改一遍代码维护成本成倍上升。我强烈建议在动手写代码之前先把协议帧结构定下来。6.1 协议选型从Modbus TCP到自定义TCP报文市面上的AGV通信方式五花八门有些支持Modbus TCP有些厂家只给一个动态链接库有些开放简单的TCP自定义协议新一代的甚至支持MQTT。我的建议是优先选择TCP自定义报文因为最灵活能完整表达上下文的业务语义Modbus TCP作为备选用于不支持自定义协议的设备。在架构上通信层做成协议无关的无论底层是Modbus还是自定义TCP对外暴露的接口都是SendCommand(carId, commandType, payload)上层业务不关心帧的细节。这样接新车型时只需要在通信层加一个协议适配器。6.2 自定义报文帧结构我用的帧结构如下| 帧头(2B) | 长度(2B) | 版本(1B) | 指令(2B) | 流水号(4B) | 数据区(NB) | 校验(2B) |帧头固定0xAA 0x55用于同步识别。长度从版本到数据区末尾的长度用于防粘包。指令比如0x0001为握手0x0002为心跳0x0010为任务下发0x0011为任务应答0x0020为位置上报0x00FF为报警。流水号每次请求自增用于匹配应答。校验CRC16防止数据在传输中被干扰。6.3 关键报文类型的设计细节握手报文上位机与AGV建立TCP连接后双方先交换设备编号、版本、支持的协议版本。握手机制不能省否则对端是谁都不知道就贸然发任务后果不堪设想。心跳报文AGV主动定时上报一般3秒一次。心跳里带上当前状态、是否空闲、电量、当前所在站点。上位机据此维护车辆在线状态。如果超过10秒没收到某车心跳上位机就标记该车离线并触发告警。任务下发报文包含任务编号、起点站点、终点站点、动作类型、超时时间。下发后上位机必须等AGV回应答ACK而不是发完就认为任务已派出去。我加了一个超时重发机制发出去10秒没收到应答自动重发一遍最多三次。位置上报报文AGV定时上报当前坐标或地标编号。上位机根据这个更新时间地图上的车辆图标。public class TaskDispatchFrame { public int Command 0x0010; public string TaskId { get; set; } public int StartStation { get; set; } public int EndStation { get; set; } public int ActionType { get; set; } public int TimeoutSeconds { get; set; } }6.4 粘包半包问题通信层必过的一道关TCP是流式协议没有消息边界。现场最常遇到的问题就是粘包车辆A的位置上报和车辆B的报警报文几乎同时到达读出来是一坨数据或者读了一半剩下的在下一个包。解决思路是按帧结构解析维护一个接收缓冲区。每次收到数据先追加到缓冲区。循环检查缓冲区开头是不是0xAA 0x55如果不是丢弃前一个字节继续找。找到帧头后根据帧头后的长度字段判断是否收齐了一整帧。收齐了取出一帧交给编解码器解析没收齐等下一批数据。这段逻辑还有很多边界情况比如帧头正好跨两次接收才出现。我建议把解析过程封装成FrameDecoder类单独写单元测试覆盖这些边界别直接堆在接收循环里。6.5 断线重连与双方状态对账网络不可能永远稳定。断线后最怕的不是画面卡而是任务状态乱掉——上位机以为车在干活其实车早就因为收不到指令停在原地了。我在设计里加入了一个重连后的状态对账流程AGV重新连上后上位机不急着下发新任务而是先主动查询每台车的当前状态、当前任务编号、是否处于异常完成状态。双方把各自记录的任务状态做一次比对不一致的以设备端的实际行动为准然后重启待执行任务或者标记失败。这一套对账逻辑上线后至少帮我排除了八十多个潜在的生产事故。7. 开发避坑实录我从这个项目里记下的教训写到最后我整理几个真实踩过的坑。这些细节网上很难搜到但碰到一次就够你折腾好几天。希望看我文章的朋友能少走点弯路。7.1 车辆位置刷新的性能坑最开始我为了实时让每台车的位置上报进来后立刻刷新地图上的车辆图标位置。18台车一跑起来CPU直接飙到80%界面卡成幻灯片。问题出在每台车1秒报一次位置18台车每秒18次触发Dispatcher更新每次还要重新计算Canvas坐标和触发绑定。后来改成两层优化一是位置先去更新内存模型不直接触发UI二是用一个定时器每200毫秒来一次统一刷新把200毫秒内收到的所有位置变化一次性更新到地图上。这样界面每秒刷新5次肉眼看起来毫无差别CPU占用降到15%以下。7.2 数据库连接池耗尽的问题有一阵子系统跑了几个小时就报连接池已满重启才好。查了半天最后发现是日志写入代码里new SqlConnection()之后没有用完Dispose。多线程下每个任务都来借一个连接池子很快就爆了。教训是所有数据库访问统一走一个仓储类并且用using或者await using确保连接及时释放。另外批量日志写入用后台队列合并不要每一条日志都开一次连接。后来改成100条或者5秒刷一次批量写数据库压力小得多性能也稳定了。7.3 断线重连后任务状态错乱一次联调时我故意把一台AGV的WiFi断掉再恢复结果发现上位机给这台车重新下发了一个已经在执行中的任务AGV当场停在路口不知往哪走。原因就是重连后没有做状态对账就直接派新活了。从那以后我把重连立刻发起状态对账写成一个铁律而且是对账完成之前任何任务都不许派给这台车。这个规则看着简单但在真实项目中能救你一命。7.4 仿真与真车联调的差异仿真环境里地图坐标是精确的路径是干净的车是准时准点的。真车一跑问题全来了二维码偏差几厘米、拐弯需要减速、前方有人挡住会停车、载重不同刹车距离也不同。所以我的建议是上位机的调度逻辑一开始就预留速度等级和站点容差这两个参数仿真时用最快配置真车联调时降级使用。另外真车联调一定要留出足够时间别指望仿真通过就压缩现场联调的周期。7.5 上线前的联调清单项目上线前我列过一张清单每项都必须过少一项我都不敢让客户签验收每台AGV的握手、心跳、位置上报、任务执行全流程逐条验证。断点测试模拟TCP断开重连验证状态对账逻辑。故障测试人为拔掉一台车的电源确认上位机能告警并重新分配任务。并发测试同时下发20个任务确认调度不混乱、数据库不锁死。数据完整性核对跑一整天的任务用任务表记录和MES的任务记录逐条对账。这套清单执行完实际上线后的意外基本都被拦在前面了。项目交付后客户有一次在交流会上问我这套系统最值钱的究竟是WPF界面还是调度算法。我说都不是是系统把状态这件小事管明白了任务有状态、车辆有状态、连接有状态每个环节都透明可查出问题能定位恢复起来有章法。做到这一点上位机系统的骨架就算立住了剩下那些界面效果、通信速度都是锦上添花的事。