ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#无人值守地磅称重系统开发:串口、状态机与联动控制实战

C#无人值守地磅称重系统开发:串口、状态机与联动控制实战 简介基于C#技术的无人值守地磅称重系统源码面向工业与物流领域的软件开发者和系统集成人员用于实现地磅称重全流程的自动化管理解决传统人工称重效率低、易出错等问题。压缩包共243个文件、约148.16MB以100个C#源代码文件为核心另有47个动态链接库、19个XAML界面文件、13个PDF文档、11个配置文件及10个报表文件等覆盖业务逻辑、界面交互、运行配置与统计报表等完整开发链路。已有256人学习下载。源码内含完整的项目结构与可执行文件涵盖数据采集、自动化控制、数据库存储与报表生成等模块可帮助开发者快速理解地磅称重系统的整体架构附带的PDF文档及CHM使用手册提供接口规范与操作说明配合Sqlite数据库支持和报表生成器可借鉴自动化称重、数据采集与存储等关键实现适合作为二次开发或行业方案设计参考。1. 无人值守地磅称重系统C#落地时最容易轻敌的几条现场规则很多人接到“基于C#技术的无人值守地磅称重系统”这个需求时第一反应是写个WinForm界面把串口重量读出来存进数据库。真正跑到现场才发现仪表数据只是其中一条链路地磅旁边还有道闸、红外对射、车牌识别相机、红绿灯和语音播报任何一环配合不好车辆就会堵在磅台上甚至出现“车没停稳就抬杆”的安全隐患。这篇文章按我改造过多个地磅现场的经验从设备选型、状态机、串口稳定读取、道闸联动、批量入库到典型坑位逐个拆开给准备投入这方向的C#上位机开发者一套可以直接落地搭骨架的方案。2. 系统拓扑与设备选型先协议后界面C#里每个设备都是一条通讯链路我在现场做的第一件事不是打开Visual Studio而是把整个磅房设备列成一张通讯矩阵。无人值守地磅的C#上位机本质是一个同时管理多条通讯链路的控制程序界面只是最外层壳。设备链路清楚后开发效率会高很多后期排查问题也能直接定位到协议层。2.1 设备接口矩阵仪表、道闸、相机、红外对射分别怎么接地磅现场的硬件看着多归类以后只有三类接口串口、开关量或PLC寄存器、网络SDK。称重仪表一般输出RS232或RS485很多仪表支持连续发送和应答两种模式道闸、红绿灯、语音播报、红外对射全部是开关量常见做法是接到一个IO控制卡或PLC的输入输出端子上车牌识别相机走网口用厂商SDK拿识别结果和抓拍图片。下面这张接口矩阵表是项目启动时我会发给电气和机械同事逐项核对用的。它决定了C#工程要引用哪些库、起几个后台线程也直接暴露采购有没有漏设备。设备在称重流程里的作用常见接口C#对接方式称重仪表读取毛重/皮重/稳定标志RS232 / RS485ASCII协议System.IO.Ports.SerialPort 协议解析道闸阻止车辆上磅、放行继电器/开关量或Modbus寄存器IO卡DLL / Modbus TCP或RTU写线圈红外对射检测车辆位置防作弊开关量输入IO卡数字输入 / PLC输入寄存器车牌识别相机识别车牌留抓拍证据网口SDK或HTTP异步回调ConcurrentQueue缓存红绿灯/语音播报引导司机、提示异常继电器输出与道闸同一IO卡或ModbusC#工程结构上我习惯给每类设备建一个独立类库至少按“设备服务”分文件。例如WeightScaleService只负责串口读写GateService只管道闸和红绿灯PlateRecognizer只管相机。它们之间不直接互相调用而是通过事件或队列传递消息。设备参数统一放在JSON配置文件里程序启动时按设备名匹配串口号、波特率、Modbus站点号这样换一台设备不用重新编译维护成本低很多。2.2 流程状态机与线程模型不靠事件驱动靠轮询状态流转人工称重时有操作员判断“车到哪一步了”无人值守时程序必须自己知道车辆当前位置、道闸状态、重量是否稳定。我第一次用纯事件驱动写红外对射的IO抖动让程序一秒钟触发五六次事件状态全乱后来全部改成“轮询加状态机”问题一下子少了。最小状态集合可以这样定义enum WeighStep { Idle 0, VehicleOnScale 1, GateDown 2, PlateCaptured 3, WeightStable 4, RecordSaved 5, GateUp 6, VehicleLeft 7 }后台线程每隔100毫秒读一次所有IO和仪表数据再用一个switch判断当前状态该做什么。比如Idle状态下检测到上磅红外信号先切换为VehicleOnScale执行落杆命令确认落杆到位后再触发相机抓拍和车牌识别。每步都记录时间戳和关键数值方便事后回放。while (!ct.IsCancellationRequested) { bool frontBlocked _io.Read(0) 1; // 上磅红外 bool tailBlocked _io.Read(1) 1; // 下磅红外 switch (_currentStep) { case WeighStep.Idle: if (frontBlocked) { _logger.Write(车辆上磅); _gate.CloseGate(); _currentStep WeighStep.GateDown; } break; case WeighStep.GateDown: if (_gate.IsClosedFeedback()) { _currentStep WeighStep.PlateCaptured; _plateRecognizer.TriggerCapture(); } break; } Thread.Sleep(100); }轮询模式在工业现场有很实际的优势不会因为事件回调顺序混乱而丢失状态现场故障时能从日志按时间轴回放某个SDK回调丢了状态机也能靠超时恢复。至于处理状态转移的函数我一般用DictionaryWeighStep, Funcbool做映射这种C#泛型委托的写法比一堆if好维护也方便在运行时动态替换某个环节的处理器。3. 串口读取与稳定判断把仪表数据变成“可称重结果”的C#实现如果说无人值守地磅是一个人重量数据就是神经信号。串口这关过不了后面状态机再漂亮都是空转。现场仪表协议大体分两种连续发送和应答式。连续发送适合实时显示数据密度足应答式省流量但要等一个轮询周期。我偏向连续发送因为重量稳定判断必须拿到高频率样本。3.1 仪表协议解析缓冲、帧头、校验和串口数据不是按完整帧到达的。SerialPort.DataReceived事件触发时一帧可能拆成几次到也可能一次来了几帧。所以我不会直接用ReadLine()而是维护一个字节缓冲边收边找帧头帧尾。常见ASCII协议帧大概长这样ST,GS,00060.00kg\r\n“ST”是帧头“GS”表示毛重后面是重量文本和回车换行。解析代码可以这样写private readonly Listbyte _frameBuffer new Listbyte(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { byte[] bytes new byte[_serial.BytesToRead]; _serial.Read(bytes, 0, bytes.Length); _frameBuffer.AddRange(bytes); while (_frameBuffer.Count 14) { if (_frameBuffer[0] ! (byte)S || _frameBuffer[1] ! (byte)T) { _frameBuffer.RemoveAt(0); continue; } int end _frameBuffer.IndexOf((byte)\n); if (end 0) return; string line Encoding.ASCII.GetString(_frameBuffer.Take(end 1).ToArray()); _frameBuffer.RemoveRange(0, end 1); _rawWeightQueue.Enqueue(ParseWeight(line)); } }这段逻辑里一次性读取BytesToRead个字节避免逐字节读取在高速率下效率太低_frameBuffer是字节缓冲没找到帧头就删掉第一个字节继续找找到回车换行就切出一整帧。工业仪表数据量大这个简单缓冲能过滤大多数乱序和半包问题。如果要更严谨可以在解析后加BCC异或校验把非法帧丢弃。有个血泪经验不要在DataReceived里直接写数据库或驱动道闸。这个事件本身运行在串口接收线程一旦做耗时操作后续字节会堵在系统缓冲区最后看起来就像串口“死掉”。正确做法是这里只入队业务逻辑放到独立后台线程。3.2 稳定重量算法滑动窗口与阈值附C#实现“重量稳定”是无人值守系统最核心的判断。车辆停稳后重量还会缓慢变化旁边有压路机通过也会引起波动。最实用的判据是对一段时间样本做滑动窗口窗口内最大值和最小值差小于阈值就认为稳定了。我按每100毫秒采集一次样本连续60个样本也就是6秒窗口。阈值不能固定按仪表满量程的一定比例计算更科学。代码大致如下private Queuedouble _sampleWindow new Queuedouble(); public bool TryGetStableWeight(out double stableWeight) { if (_sampleWindow.Count 60) { stableWeight 0; return false; } double max _sampleWindow.Max(); double min _sampleWindow.Min(); double limit _capacity * 0.0002; // 满量程的0.02%50吨地磅约为10kg if (max - min limit) { stableWeight _sampleWindow.Average(); return true; } stableWeight 0; return false; }这里有三个参数需要现场调试窗口长度60是基础值仪表频率高可以加到120个样本频率低就减到30个阈值0.0002倍满量程对50吨地磅是10kg适合静态汽车衡如果现场震动明显可以把阈值放宽到0.0003但再宽就容易把“车还没停稳”误判成稳定。还要注意样本窗口的更新方式每来一个新值先Enqueue再判断Count 60时Dequeue。车辆上磅过程中重量变化很大如果窗口里既有“车没停稳”的高值又有“停稳后”的低值峰值差永远超阈值程序会一直等待。这时通常再加一个变化速率判断当相邻两次读数差超过大阈值时先清空窗口等变化速率降下来再重新积累样本稳定判定速度会明显提升。3.3 多线程界面刷新保证C#上位机不卡的两种做法串口事件天然运行在后台线程直接改Label.Text会抛跨线程异常。很多人用Control.Invoke硬发回调太频繁时界面一样卡。我现在的方案是用System.Threading.Channels做一个生产者消费者队列串口线程只放重量值UI线程专门消费。Channeldouble _weightChannel Channel.CreateUnboundeddouble(); private async void ConsumeWeight() { while (await _weightChannel.Reader.WaitToReadAsync()) { while (_weightChannel.Reader.TryRead(out double weight)) { lblWeight.Invoke((Action)(() { lblWeight.Text weight.ToString(F3); })); } } }这个模式比到处Invoke干净很多。UI线程每200毫秒刷新一次界面都够用串口那边每100毫秒入队两边互不拖累。对C#多线程调度不熟的话最容易翻车的是用Task.Run去跑一个死循环读串口然后又在里面同步操作界面。记住“串口线程只入队UI线程只显示业务线程只做状态机”整个系统就不会因为线程问题卡死。4. 联动控制与数据落库道闸、车牌识别、SQLBulkCopy状态机决定“什么时候做什么”联动控制决定“具体怎么做”。道闸抬落杆、车牌识别触发、称重记录保存这三件事是无人值守系统里最容易被现场挑毛病的部分。任何一步没有反馈确认都容易出现安全事故或数据缺失。4.1 抬杆/落杆控制Modbus RTU指令与IO反馈道闸控制不能写成“发一条指令就完事”。继电器可能没动作闸杆可能没到位程序必须用反馈信号确认。现场我最常用的做法是走Modbus RTU用串口或TCP写线圈控制继电器再读输入点判断道闸是否到位。下面是写Modbus RTU命令控制一路继电器的示例public void CloseGate() { byte[] cmd { 0x01, // 从站地址 0x05, // 写单线圈 0x00, 0x00, // 线圈地址0 0xFF, 0x00, // 置ON 0x8C, 0x3A // CRC16低位在前 }; _serial.Write(cmd, 0, cmd.Length); DateTime deadline DateTime.Now.AddSeconds(5); while (DateTime.Now deadline) { byte[] inputs _io.ReadInputs(0, 2); if ((inputs[0] 0x02) 0x02) // 关到位信号 { _logger.Write(道闸落杆到位); return; } Thread.Sleep(100); } _alarm.Raise(道闸落杆超时); }这里参数有几个讲究从站地址0x01对应IO模块的拨码开关线圈地址0x00是落杆继电器0x02是输入点1的位状态表示关到位。CRC16固定值只适合测试实际项目中要封装一个Crc16.Modbus()函数动态计算。道闸如果没有接到位反馈一定要给状态机加超时告警否则闸杆卡住时程序还会继续走流程车辆直接顶上去。道闸逻辑还要和状态机互锁车没完全上磅不能发抬杆命令车尾没离磅不能再次落杆。下磅红外恢复为零后再把状态切回Idle否则出口堵车时会反复抬杆造成误动作。4.2 车牌识别触发相机SDK回调与OpenCVSharp预处理车牌识别相机一般自带触发模式。常见流程是状态机进入PlateCaptured后调用相机SDK抓拍SDK通过网络协议返回车牌号和图片路径。终端SDK的回调可能在任意线程触发不能直接在回调里写界面或数据库先用队列缓存。ConcurrentQueuePlateResult _plateQueue new ConcurrentQueuePlateResult(); private void OnPlateCaptured(PlateResult result) { _plateQueue.Enqueue(result); if (string.IsNullOrEmpty(result.PlateNo)) { // 识别失败时用OpenCVSharp取抓拍图ROI做二次识别 Mat crop PreprocessCrop(result.ImagePath); result.PlateNo _ocr.Recognize(crop); } }PreprocessCrop里做的事是把抓拍图按车头区域裁切转灰度做中值滤波和二值化再交给OCR引擎。夜间车灯直射、车牌反光时相机SDK首识别率会掉ROI二次识别能把成功率拉回来。消费者线程不断从_plateQueue取结果拿到后组装称重记录。这个队列非常重要因为相机回调频率不稳定直接同步处理会让抓拍线程卡阻塞造成漏拍。4.3 称重记录落库SQLBulkCopy批量写入与本地缓冲称重记录包含车牌号、毛重、皮重、净重、毛重时间、皮重时间、图片路径、车道编号等。高峰期一个磅一分钟过七八辆车逐条INSERT去远程SQL Server不仅慢还会频繁锁表。我通常先在内存或本地SQLite里攒数据积累到一定量后通过SqlBulkCopy批量推上去。using (var bulk new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock)) { bulk.DestinationTableName WeighRecord; bulk.BulkCopyTimeout 60; bulk.BatchSize 1000; bulk.ColumnMappings.Add(PlateNo, PlateNo); bulk.ColumnMappings.Add(GrossWeight, GrossWeight); bulk.ColumnMappings.Add(TareWeight, TareWeight); bulk.ColumnMappings.Add(NetWeight, NetWeight); bulk.ColumnMappings.Add(WeighTime, WeighTime); bulk.WriteToServer(dataTable); }BatchSize不是越大越好。表字段多、磁盘I/O慢时一次性5000行很容易超时。现场常见参数是500到1000行BulkCopyTimeout保持在60秒。另外称重原始数据要保留业务流水号不能只用自增主键。如果网络断开程序把流水号写到本地SQLite网络恢复后按流水号去重补传避免重复记录或丢单。无人值守地磅是7×24小时运行的数据库写入失败不能把业务流程拖垮。“本地缓冲加批量同步”是我在几个项目里验证过最稳的落库结构。5. 无人值守地磅的4个常见坑串口丢包、重量漂移、道闸并发、抓拍不同步很多系统开发时一切正常一到现场就被各种随机问题折磨得焦头烂额。下面四个坑是我在不同项目里反复遇到的每次根因都不一样但解决思路基本能通用。5.1 串口数据乱码和丢帧现象重量显示突然为0或隔几秒才跳一次甚至出现明显乱码。查设备、查网络都没问题重启软件又恢复正常。原因一是波特率、校验位、停止位与仪表实际配置不一致二是USB转串口线质量太差地磅仪表附近有变频器和大功率电机干扰三是DataReceived事件里做了耗时操作导致系统缓冲区溢出丢包。解决先用串口调试助手确认仪表手册上的参数把波特率固定不要用程序自动探测。硬件上使用带光电隔离的RS232转RS485转换器USB转串口线选知名主控芯片。程序里严格落实“事件只入队不做事”解析线程独立跑丢帧率能明显下降。5.2 稳定判断失效导致记错磅现象系统提示重量已经稳定保存后发现毛重少了几百公斤或者重量不停变化几十秒都不稳定。原因稳定窗口阈值设置太严或太松车辆停稳后发动机产生的低频震动被当成波动车辆上磅过程中窗口混入大量中间过程样本导致极差始终超限。解决采用“变化速率预判加滑动窗口”双重判断。相邻两次采样重量差超过满量程的2%时先清空窗口重新积累窗口极差小于限值后才判定稳定。稳定后再连续读3次差值仍在范围内才最终写入记录这个二次确认能挡住大部分震动干扰。5.3 道闸抬落杆与车辆位置不同步现象车头刚压到磅台道闸就往下落险砸车顶或车尾还没离开磅台道闸又降下去把后面排队车辆堵住。原因状态机只看了上磅红外信号没等待道闸关到位反馈下磅红外恢复判断过早或者IO常开常闭逻辑接反。解决把“道闸关到位”作为独立状态条件。流程改成上磅红外触发后先落杆等到关到位反馈才允许抓拍下磅红外持续恢复正常后再切回Idle中间加一秒延时避免车辆在出口短暂停留造成误判。5.4 车牌识别漏拍导致数据不全现象称重记录里车牌号大量为空或过车时没有生成流水后台统计少了很多单。原因相机触发信号和道闸控制同时发生相机还没完成初始化就开始抓拍触发信号保持时间太短车辆通过时相机没跟上夜间车灯直射使识别率下降。解决触发信号保持时间至少300毫秒优先用工控机数字输出触发而不是软件延时触发。软件层面加入补拍机制调用TriggerCapture()后3秒内没有返回结果就从最近抓拍目录里挑一张做OpenCVSharp二次识别仍识别不出来就把流水标记为“待人工审核”让车牌空着也不堵磅台事后由监控截图人工补录。6. 让系统从“能用”到“敢用”毛重比对、离线缓冲与重放验证无人值守地磅最怕的不是设备坏而是“流程看着走了数据却没人敢信”。我上线前一定会做毛重皮重差值校验。同一辆车多次进出场时毛重和皮重都有历史记录净重异常或皮重突变超过±5%系统直接弹人工复核而不是默默保存。if (lastTare ! null Math.Abs(netWeight - lastTare) lastTare * 0.05) { _alarm.Raise(皮重异常波动等待人工复核); }这个校验阈值要按车辆类型设置罐车和普通货车不同同一辆车空车皮重受油量影响也会变化。做进配置后夜班值守就能少处理很多扯皮。第二个补强是离线重放验证。我会把每台仪表的串口原始数据按日存成日志文件里面带上时间戳、重量字节流和当时的状态机动作。周末没事时写一个小工具把日志文件重新喂给状态机跑一遍对比之前保存的结果。这个方法救过我一次某次改稳定阈值后上线当天频繁延迟重放日志才发现新窗口把前一辆车的样本也收进来了导致后续单子全部超时。从那以后我每次改稳定参数都会先重放三天旧日志通过后再上现场。源码交付时还要考虑保护问题。C#上位机经常要部署到客户工控机DLL容易被反编译有人问“C#怎样防止反编译”。实用经验是把核心业务算法放到独立程序集用混淆器处理连接串和PLC点位不要硬编码放加密配置文件。混淆不能防破解但能把改装门槛提高避免客户拿源码改一版就出去接项目。做无人值守地磅本质是让程序替人在现场做判断可靠比功能多更重要。我现在的习惯是每次改完代码先在测试环境跑一遍串口重放再去现场看一次车辆过磅确认道闸和识别逻辑没被改坏。希望这篇文章里这些偏实操的方法能帮你少走几个月的弯路真正把C#上位机系统稳稳落在磅房里。本文还有配套的精品资源点击获取
返回列表