智能仓储AGV运维实战:C#上位机对接科聪/极智嘉MQTT协议,远程调度+故障诊断一步到位

张开发
2026/4/12 16:37:12 15 分钟阅读

分享文章

智能仓储AGV运维实战:C#上位机对接科聪/极智嘉MQTT协议,远程调度+故障诊断一步到位
一、项目背景去年底接了个天津东丽区电商仓储的活他们之前用的是AGV厂商自带的调度系统有三个硬伤调度系统只能在本地中控室用运维人员在仓库巡检时AGV出故障得跑回中控室查日志效率极低厂商的故障诊断功能太弱只会显示“AGV离线”“路径阻塞”不会给出具体原因比如是激光雷达脏了、轮子卡了、还是WiFi信号弱想对接自己的WMS系统厂商要收10万的接口费还要排队等3个月客户预算只有3万要求用C#写一个轻量级的上位机对接现有20台科聪AGV的MQTT协议支持简单的远程调度比如指定AGV去某个货位取货支持详细的故障诊断结合AGV上报的传感器数据给出具体原因和处理建议支持手机/平板Web端远程查看AGV状态和故障信息预留WMS对接接口以后自己就能对接我当时想这不就是C#上位机MQTT的又一个完美场景吗科聪AGV的MQTT协议文档是公开的极智嘉、快仓这些主流厂商的协议也大同小异写好一个其他厂商的改改主题和数据结构就行。二、整体架构设计先给你看架构图这个架构我在2个电商仓储、1个汽车零部件仓储里用过非常稳定。远程监控层本地服务器层仓储设备层科聪AGV-001科聪AGV-002...科聪AGV-020客户现有WMSEMQX BrokerC# ASP.NET Core Web API 调度引擎SQL Server 2022 ExpressC# WPF本地运维看板Vue3 Element Plus Web运维看板企业微信应用消息2.1 为什么选这个架构MQTT协议轻量级、低延迟、支持QoS服务质量非常适合AGV这种移动设备。科聪AGV的MQTT协议默认QoS选1保证消息至少送达一次不会丢调度指令和故障数据。EMQX Broker开源免费的MQTT Broker支持百万级连接性能比Mosquitto好很多而且自带Web管理界面方便调试AGV的连接状态和消息收发。C# ASP.NET Core Web API跨平台以后可以直接部署到Linux服务器上支持异步处理20台AGV的并发消息毫无压力预留RESTful接口以后对接WMS非常简单。C# WPF本地运维看板界面美观、响应快适合中控室的大屏显示支持实时更新AGV状态延迟不超过100ms。Vue3 Element Plus Web运维看板跨平台手机/平板/电脑都能访问用内网穿透工具比如花生壳企业版一年才几百块暴露到公网不需要VPN运维人员在仓库巡检时就能远程查看。三、核心功能实现3.1 科聪AGV MQTT协议对接科聪AGV的MQTT协议文档是公开的我给你简化一下核心的主题和数据结构主题方向主题名称说明AGV→Brokeragv/{device_id}/statusAGV实时状态上报位置、速度、电量、传感器数据等AGV→Brokeragv/{device_id}/faultAGV故障上报故障代码、故障时间、故障描述等Broker→AGVagv/{device_id}/command调度指令下发取货、放货、充电、暂停等3.1.1 MQTT订阅状态存储C#服务端用MQTTnet库订阅EMQX的主题然后把AGV的实时状态存到SQL Server 2022 Express里。MQTTnet是.NET平台最好用的MQTT库开源免费支持异步。给你看个简化的订阅存储逻辑usingMQTTnet;usingMQTTnet.Client;usingSystem.Data.SqlClient;usingSystem.Text.Json;// 连接SQL ServervarconnectionStringServer192.168.1.100;DatabaseAGVDB;User Idsa;Password123456;;varconnectionnewSqlConnection(connectionString);connection.Open();// 创建MQTT客户端varfactorynewMqttFactory();varclientfactory.CreateMqttClient();// 连接EMQXvaroptionsnewMqttClientOptionsBuilder().WithTcpServer(192.168.1.100,1883).WithClientId(CSharp_AGV_Server).Build();awaitclient.ConnectAsync(options);// 订阅AGV状态和故障主题awaitclient.SubscribeAsync(agv//status,MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);awaitclient.SubscribeAsync(agv//fault,MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);// 处理收到的AGV状态消息client.ApplicationMessageReceivedAsyncasynce{vartopice.ApplicationMessage.Topic;varpayloadEncoding.UTF8.GetString(e.ApplicationMessage.Payload);if(topic.StartsWith(agv/)topic.EndsWith(/status)){vardeviceIdtopic.Split(/)[1];varstatusJsonSerializer.DeserializeAGVStatus(payload);// 存到SQL ServervarsqlMERGE INTO AGVStatus AS target USING (VALUES (DeviceId, X, Y, Angle, Speed, Battery, LaserStatus, WheelStatus, WiFiSignal, Timestamp)) AS source (DeviceId, X, Y, Angle, Speed, Battery, LaserStatus, WheelStatus, WiFiSignal, Timestamp) ON target.DeviceId source.DeviceId WHEN MATCHED THEN UPDATE SET X source.X, Y source.Y, Angle source.Angle, Speed source.Speed, Battery source.Battery, LaserStatus source.LaserStatus, WheelStatus source.WheelStatus, WiFiSignal source.WiFiSignal, Timestamp source.Timestamp WHEN NOT MATCHED THEN INSERT (DeviceId, X, Y, Angle, Speed, Battery, LaserStatus, WheelStatus, WiFiSignal, Timestamp) VALUES (source.DeviceId, source.X, source.Y, source.Angle, source.Speed, source.Battery, source.LaserStatus, source.WheelStatus, source.WiFiSignal, source.Timestamp);;varcommandnewSqlCommand(sql,connection);command.Parameters.AddWithValue(DeviceId,deviceId);command.Parameters.AddWithValue(X,status.X);command.Parameters.AddWithValue(Y,status.Y);command.Parameters.AddWithValue(Angle,status.Angle);command.Parameters.AddWithValue(Speed,status.Speed);command.Parameters.AddWithValue(Battery,status.Battery);command.Parameters.AddWithValue(LaserStatus,status.LaserStatus);command.Parameters.AddWithValue(WheelStatus,status.WheelStatus);command.Parameters.AddWithValue(WiFiSignal,status.WiFiSignal);command.Parameters.AddWithValue(Timestamp,DateTimeOffset.FromUnixTimeMilliseconds(status.Timestamp).DateTime);awaitcommand.ExecuteNonQueryAsync();}elseif(topic.StartsWith(agv/)topic.EndsWith(/fault)){vardeviceIdtopic.Split(/)[1];varfaultJsonSerializer.DeserializeAGVFault(payload);// 存到SQL ServervarsqlINSERT INTO AGVFault (DeviceId, FaultCode, FaultDescription, FaultLevel, Timestamp) VALUES (DeviceId, FaultCode, FaultDescription, FaultLevel, Timestamp);varcommandnewSqlCommand(sql,connection);command.Parameters.AddWithValue(DeviceId,deviceId);command.Parameters.AddWithValue(FaultCode,fault.FaultCode);command.Parameters.AddWithValue(FaultDescription,fault.FaultDescription);command.Parameters.AddWithValue(FaultLevel,fault.FaultLevel);command.Parameters.AddWithValue(Timestamp,DateTimeOffset.FromUnixTimeMilliseconds(fault.Timestamp).DateTime);awaitcommand.ExecuteNonQueryAsync();// 发送企业微信报警awaitSendWeChatAlert(deviceId,fault);}};3.1.2 调度指令下发给你看个简化的调度指令下发逻辑// 伪代码不是完整源码publicasyncTaskboolSendAGVCommand(stringdeviceId,stringcommandType,objectcommandParams){varpayloadnew{command_idGuid.NewGuid().ToString(),command_typecommandType,command_paramscommandParams,timestampDateTimeOffset.UtcNow.ToUnixTimeMilliseconds()};varjsonPayloadJsonSerializer.Serialize(payload);varmessagenewMqttApplicationMessageBuilder().WithTopic($agv/{deviceId}/command).WithPayload(jsonPayload).WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce).Build();varresultawaitclient.PublishAsync(message);returnresult.ReasonCodeMQTTnet.Client.Publishing.MqttClientPublishReasonCode.Success;}// 调用示例指定AGV-001去货位A01取货awaitSendAGVCommand(AGV-001,pickup,new{locationA01});3.2 故障诊断功能这是客户最关心的功能之一。厂商的故障诊断功能只会显示“AGV离线”“路径阻塞”我结合AGV上报的传感器数据写了一个简单的故障诊断引擎能给出具体原因和处理建议。给你看个简化的故障诊断引擎逻辑// 伪代码不是完整源码publicclassFaultDiagnosisEngine{publicstaticFaultDiagnosisResultDiagnose(AGVStatusstatus,AGVFaultfault){varresultnewFaultDiagnosisResult();// 先处理故障代码switch(fault.FaultCode){caseE001:// AGV离线if(status.WiFiSignal-70){result.CauseWiFi信号弱;result.Suggestion检查AGV附近的WiFi路由器或者调整AGV的路径避开信号盲区;}else{result.CauseAGV电源断开或硬件故障;result.Suggestion检查AGV的电源开关和电池或者联系AGV厂商维修;}break;caseE002:// 路径阻塞result.CauseAGV前方有障碍物;result.Suggestion检查AGV前方的障碍物或者手动将AGV移开障碍物然后重新下发调度指令;break;caseE003:// 激光雷达故障if(status.LaserStatus0){result.Cause激光雷达脏了;result.Suggestion用干净的软布擦拭激光雷达的镜头;}else{result.Cause激光雷达硬件故障;result.Suggestion联系AGV厂商维修;}break;caseE004:// 轮子卡了result.CauseAGV轮子卡了异物;result.Suggestion检查AGV的轮子清除异物;break;caseE005:// 电量低result.CauseAGV电量低于20%;result.Suggestion下发充电指令让AGV去充电站充电;break;default:result.Cause未知故障;result.Suggestion联系AGV厂商维修;break;}returnresult;}}3.3 WPF本地运维看板WPF本地运维看板用LiveCharts.Wpf库做AGV位置的实时地图用MaterialDesignInXamlToolkit库做界面美观、响应快。给你看个AGV位置地图的XAML代码逻辑!-- 伪代码不是完整源码 -- Grid !-- 仓储地图背景 -- Image Source/Images/WarehouseMap.png StretchUniform/ !-- AGV位置标记 -- ItemsControl ItemsSource{Binding AGVList} ItemsControl.ItemsPanel ItemsPanelTemplate Canvas / /ItemsPanelTemplate /ItemsControl.ItemsPanel ItemsControl.ItemContainerStyle Style TargetTypeContentPresenter Setter PropertyCanvas.Left Value{Binding X, Converter{StaticResource CoordinateConverter}}/ Setter PropertyCanvas.Top Value{Binding Y, Converter{StaticResource CoordinateConverter}}/ /Style /ItemsControl.ItemContainerStyle ItemsControl.ItemTemplate DataTemplate Border Width30 Height30 CornerRadius15 Background{Binding Status, Converter{StaticResource StatusToColorConverter}} RenderTransformOrigin0.5,0.5 Border.RenderTransform RotateTransform Angle{Binding Angle}/ /Border.RenderTransform TextBlock Text{Binding DeviceId.Substring(3)} FontSize12 ForegroundWhite VerticalAlignmentCenter HorizontalAlignmentCenter/ /Border /DataTemplate /ItemsControl.ItemTemplate /ItemsControl /Grid四、项目成果这个项目花了2周时间完成总成本不到2.5万花生壳企业版1年600SQL Server 2022 Express免费其他组件开源免费开发费用24000上线后运行了3个月非常稳定AGV状态更新延迟平均50ms最高不超过100ms调度指令下发成功率100%故障诊断准确率95%以上运维人员处理故障的时间从平均30分钟降到了平均5分钟预留的WMS对接接口已经测试通过客户准备下个月自己对接客户非常满意说以后再上其他AGV还用这个方案。五、实战踩坑总结AGV的MQTT协议文档一定要仔细看科聪AGV的MQTT协议文档里有很多细节比如坐标的单位是毫米角度的单位是度顺时针为正这些细节如果搞错了AGV的位置显示和调度都会出问题。MQTT的QoS选1就够了QoS2虽然保证消息只送达一次但性能开销大延迟高。AGV场景下QoS1足够了即使偶尔重复收到一条调度指令AGV会自动忽略重复的指令。SQL Server的索引一定要建对一开始没建索引查询AGV的历史状态和故障信息的时候非常慢后来在DeviceId和Timestamp字段上建了联合索引查询速度提升了100倍以上。内网穿透工具一定要用企业版免费版的花生壳速度慢、不稳定而且有流量限制。企业版的速度快、稳定没有流量限制一年才几百块非常划算。

更多文章