ARTICLE DETAIL

资讯详情

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

C#无人值守地磅称重系统:串口采集、状态机与数据落库实战

C#无人值守地磅称重系统:串口采集、状态机与数据落库实战 简介一份基于C#实现的无人值守地磅称重系统设计源码适用于需要构建自动化称重管理流程的开发者与项目团队可降低人工操作成本、提升过磅效率。包内文件共137个以91个C#源代码文件与21个XAML界面文件为主体辅以8个config配置文件、4个csproj项目文件、4个vsidx索引文件、sln解决方案文件及settings、resx、gitignore等配套资源整体约2.44MB目录结构清晰便于理解系统分层与界面逻辑。目前已有414人学习下载。资源覆盖从业务逻辑到界面交互的完整实现包含IPC摄像头接入CHCNetSDK、Ipcsdk等关键模块适合用于学习C#与XAML协同开发、地磅称重无人值守方案设计也可作为二次开发或毕业设计参考。1. 无人值守地磅称重系统C# 在计量环节里到底管哪一段一辆货车从进厂到出厂过去要司机下车递单、门卫打电话、司磅员反复核对车号、手工录重量一车下来十来分钟高峰期排队排到厂外。无人值守地磅称重系统要解决的就是这段效率黑洞车牌识别相机负责认车红外对射确认上磅位置称重仪表把重量实时送到工控机道闸、红绿灯、LED 屏按流程自动动作而这一整条链路的“总控”通常就是一台 C# 写的上位机程序。C# 在这里不碰复杂的图像识别也不碰嵌入式仪表内部它专注做三件事读串口重量、编排业务流程、把每一笔记录可靠落库。正在做计量、物流、工厂进出厂系统的工程师照着这套设计思路可以直接搭出第一版能跑的系统这也是我接下来的落地方案。2. 把无人值守地磅拆成可落地的模块从车牌识别到道闸联动的数据流2.1 无人值守地磅的系统拓扑与 C# 程序的职责边界现场设备大概有这么几类称台和传感器埋在地磅坑里它们把重量信号汇总到称重仪表仪表对外提供串口输出入口和出口各装一套车牌识别相机相机通过以太网把识别结果交给工控机道闸和红绿灯由继电器控制板驱动控制板一般走串口或网口 IO磅房里的工控机是整个系统的核心节点C# 程序就部署在这台 Windows 机器上。有人误以为无人值守就是把司磅员的位置换成一台电脑实际上电脑要同时和五类设备打交道。串口这边常见的是仪表和道闸控制板各占一个 COM 口以太网这边车牌识别相机一般通过 HTTP 或 SDK 主动推送识别结果有些还带二次确认用的对讲和磅单打印功能。C# 程序在这些设备之间扮演的是协调者收到相机回调先判断当前状态允不允许抬杆收到仪表重量先判断重量是不是已经稳定两路信号都满足才去触发道闸动作。这个职责边界想清楚后面写代码才不会把业务逻辑散得到处都是。工控机选型也有讲究我一般建议用带 PCI 串口卡的机器而不是依赖 USB 转串口原因我会在避坑章具体讲。软件层面.NET Framework 4.x 和老项目兼容性最好新项目用 .NET 6 及以上也没什么障碍关键不是版本而是串口、HTTP、数据库这三个模块的封装方式。2.2 数据流设计一次完整称重涉及哪些环节、谁先谁后一次标准的进出厂称重数据流是分两步走的。第一步是进厂毛重车到达入口道闸入口相机抓拍车牌识别结果推送过来C# 程序校验车牌在白名单内给道闸控制板发抬杆指令车开上磅红外对射检测到车已到位程序开始按固定周期读仪表重量连续几次读数差值在阈值内就认为是稳定毛重把车号、毛重、时间、抓拍照片一并存库同时在 LED 屏上提示“毛重已记录请驶离”。第二步是出厂皮重也就是回空车过磅。车从卸货点返回出口相机再次识别车牌程序查到该车有未完成的毛重记录抬杆放行车上磅后重复读数逻辑得到皮重毛重减皮重得到净重。到这里才生成完整磅单打印小票、抓拍车顶和车尾照片、抬杆放行。注意毛重和皮重是两次独立过磅不是一次过磅把两个数都读了。有个很容易忽略的环节是“防作弊”数据流里必须留出红外对射和视频抓拍的位置。比如车没完全上磅就停住只有两个轮子在称台上重量读数会偏小再比如换车牌、套牌、压边都需要红外对射的到位信号加上磅前后的照片一起来约束。C# 程序要做的是把这些信号按时间顺序串起来任何一步缺失或超时就自动转入人工处理队列而不是直接放行。2.3 核心接口选型串口/Modbus/OPC 该走哪条路现有地磅项目里最常见的仪表输出是 RS232 串口少数用 RS485 转串口。串口协议分两类一种仪表持续往外发重量帧频率几赫兹到十几赫兹另一种是主机发指令、仪表应答。C# 这边无论是哪种本质都是读串口缓冲区的字节流区别只在解析方式。Modbus 协议也常见尤其是带 PLC 控制的厂区Modbus RTU 走串口、Modbus TCP 走网口C# 里有 NModbus 这类成熟库可以直接引用。OPC 一般出现在已经有大 PLC 系统的老厂C# 接 OPC 需要额外装 OPC 客户端库开发成本比前两者高。从落地角度看我的选择优先级是纯地磅无 PLC 的直接用串口仪表实现最快厂区已经有 PLC 控制道闸和红绿灯的走 Modbus 或 OPC避免两套控制逻辑打架改造老系统时优先在原有 PLC 上做程序联动C# 只负责计量和数据库。接口类型定了后面所有代码的输入输出边界也定了。接口选型对比表方案接口形式C# 实现成本适用场景仪表串口RS232/RS485低SerialPort 直接读多数地磅项目ModbusRTU/TCP中引用库或自己拼报文带 PLC 的厂区OPCDCOM/UA高需客户端配置老 PLC 系统相机 HTTP 回调以太网低HttpListener 即可车牌识别接入3. 用 C# 实现地磅数据采集上位机串口读数与指令解析的最小可跑代码3.1 串口参数与仪表协议连续发送和指令应答两种模式拿到一台不熟悉的仪表第一步不是写代码而是去仪表说明书里找串口参数。绝大多数地磅仪表出厂默认波特率 9600、8 位数据位、无校验、1 位停止位也就是常说的 9600-8-N-1。少数老仪表用 4800也有用偶校验的接不上时先用串口助手逐个波特率试探比对着代码猜快得多。协议格式上连续发送模式最常见的是每帧以 STX0x02开头重量正文用 ASCII 字符比如“STGS001520kg”以回车换行结束。命令应答模式则是主机先发一组指令比如“读毛重”的十六进制命令仪表再回一帧。前者代码简单但程序无法控制数据到达节奏后者多一次握手但便于和刷卡器、红外对射联动减少串口冲突。提示如果现场同时有仪表和道闸控制板务必给它们分配不同的 COM 口并确认没有其他软件占用串口。Windows 下两个程序同时开同一个串口是直接报错的这是无人值守现场最常见的启动失败原因。3.2 串口采集核心代码连接、读帧、断帧重组的 C# 实现串口读数据不能想当然地一次 ReadLine 就收工因为一帧数据可能被拆成两三个包到达直接按行读很容易拿到半个帧。正确做法是维护一个字节缓冲区收到什么先追加进去再按帧头帧尾把完整帧切出来。这部分代码在任何地磅上位机里基本通用。using System; using System.Collections.Generic; using System.IO.Ports; using System.Linq; using System.Text; public class ScaleSerialReader { private SerialPort _port; private Listbyte _buffer new Listbyte(); // 解析出一帧完整重量数据后触发 public event Actionstring OnFrameReceived; public ScaleSerialReader(string portName, int baudRate 9600) { _port new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _port.DataReceived DataReceived; _port.Open(); } private void DataReceived(object sender, SerialDataReceivedEventArgs e) { // 把当前串口缓冲区的字节全部搬到内部列表 while (_port.BytesToRead 0) { int len _port.BytesToRead; byte[] temp new byte[len]; _port.Read(temp, 0, len); _buffer.AddRange(temp); } // 找到帧头 STX丢掉之前的一切垃圾字节 int start _buffer.IndexOf(0x02); if (start 0) return; if (start 0) _buffer.RemoveRange(0, start); // 找帧尾 CRLF只有帧尾完整才切帧 int end -1; for (int i 0; i _buffer.Count - 1; i) { if (_buffer[i] 0x0D _buffer[i 1] 0x0A) { end i; break; } } if (end 0) return; // 帧尾还没到齐等下一次接收事件 byte[] frame _buffer.Take(end).ToArray(); _buffer.RemoveRange(0, end 2); OnFrameReceived?.Invoke(Encoding.ASCII.GetString(frame)); } }这段代码的核心逻辑是“攒够了再切”。DataReceived 事件触发时理论上有多少读多少但串口驱动不能保证从 Read 拿到的数据恰好是一整帧所以每次都先追加到 List然后从头找 STX。找到后从 STX 开始往后找回车换行找到了才算一帧帧尾之前的数据原样保留等着下一次事件再拼接。_frame 解析时还有个坑仪表的 ASCII 帧里有可能是 GBK 编码的中文单位比如“千克”这时候 Encoding.ASCII 会拿到乱码。我一般统一用 Encoding.GetEncoding(GBK) 读正文或者只按正则以提取数字部分不依赖单位字符。重量提取可以这么写正则匹配 [-]?\d(.\d)?第一个数字就是当前毛重或皮重。3.3 数据校验与防抖毛重/皮重何时有效重量读数到手不代表车已经稳住。车开上磅的瞬间秤台会经历一个明显的摆动过程数字从几百公斤冲到一万多又回落这个状态下直接读重量结果一定偏大或偏小。业界通用的做法是防抖窗口连续读 N 帧每帧间隔约 500 毫秒到 1 秒N 帧中最大值与最小值的差不超过设定阈值比如 20 公斤才认为重量稳定。public class WeightStabilizer { private readonly int _requiredCount 3; private readonly double _maxDiffKg 20; private readonly Listdouble _samples new Listdouble(); public bool TryFeed(double weight, out double stableWeight) { _samples.Add(weight); if (_samples.Count _requiredCount) { stableWeight 0; return false; } double maxVal _samples.Max(); double minVal _samples.Min(); if (maxVal - minVal _maxDiffKg) { stableWeight _samples.Average(); _samples.Clear(); return true; } _samples.RemoveAt(0); // 滑窗剔除最旧保留最新 stableWeight 0; return false; } }这个防抖类的关键参数是 _requiredCount 和 _maxDiffKg。地磅分度值一般是 20 公斤_maxDiffKg 设成 20 比较合适如果现场有大风或者秤台机械结构老化读数天然会有 30 公斤左右的波动阈值就得适当放大。调的太严车辆一直不能判定稳定后面流程全部卡住调的太松几十公斤的误差直接进了磅单。这个参数我在现场调过很多次没有一劳永逸的值只有结合称台状态来定。3.4 对接车牌识别与道闸HTTP 回调与 IO 控制的常见做法车牌识别相机的接口各式各样大多数厂家都提供 HTTP 回调也就是相机识别到车牌后主动往我们指定的端口推一串 JSON。C# 这边用 HttpListener 起一个轻量 HTTP 服务即可不需要上 WebAPI 框架。using System; using System.IO; using System.Net; using System.Text; using System.Text.Json; public class PlateCallbackServer { private HttpListener _listener; public void Start(string prefix http://:9000/plate/) { _listener new HttpListener(); _listener.Prefixes.Add(prefix); _listener.Start(); _listener.BeginGetContext(OnContext, null); } private void OnContext(IAsyncResult ar) { var ctx _listener.EndGetContext(ar); _listener.BeginGetContext(OnContext, null); using var reader new StreamReader(ctx.Request.InputStream); string body reader.ReadToEnd(); using var doc JsonDocument.Parse(body); string plate doc.RootElement.GetProperty(plate).GetString(); string timeStr doc.RootElement.GetProperty(time).GetString(); // 把识别结果交给主流程状态机处理 Program.Instance.HandlePlateArrived(plate, timeStr); byte[] resp Encoding.UTF8.GetBytes(ok); ctx.Response.StatusCode 200; ctx.Response.ContentLength64 resp.Length; ctx.Response.OutputStream.Write(resp, 0, resp.Length); ctx.Response.Close(); } }注意 Uri 前缀要注册成机器级才能让相机跨网络访问否则只能本机访问。注册方式是在 Windows 命令行执行 netsh http add urlacl urlhttp://:9000/ usereveryone这个细节漏掉相机回调永远到不了程序。道闸控制也一样串口发一个开闸命令字符继电器动作C# 里就是一个 SerialPort Write没有太多花活重点是把开闸条件卡在状态机里而不是裸奔放开。4. 无人值守业务流转的实现称重流程状态机与数据库落账4.1 称重流程为什么需要状态机而不是 if-else把整个称重流程写成 if-else最常见的结果是代码里到处是“当前步骤”变量每加一个新需求就要改七八个地方。比如现场新增“倒车二次上磅”这个操作本来只是给车一次重新对齐的机会if-else 写法可能改完这里漏了那里。状态机是一个更严谨的思路定义系统所有可能的状态定义每个状态在什么事件下能跳转到哪个状态非法跳转一律拒绝。无人值守地磅的状态序列不复杂但每一个状态都要能回答三个问题我现在是哪一步、什么事件能让我走到下一步、出现问题往哪兜底。状态机写清楚之后后续加设备、加流程只需要在状态转移表里补映射关系而不是动业务代码逻辑。4.2 状态机核心代码与数据库落账状态定义用枚举状态转移用字典加判断函数这样整个流转在代码里一眼能看到头。下面的代码是核心状态的流转骨架public enum WeighState { Idle, // 空闲等待车辆 VehicleIn, // 车牌已识别道闸放行 OnScale, // 红外对射确认车已上磅 GrossRead, // 毛重已成 Leaving, // 车已离场等待回程皮重 BackOnScale, // 回车再次上磅 TareRead, // 皮重已成净重计算完成 ManualNeeded // 异常转人工 } public class WeighFlow { private WeighState _state WeighState.Idle; private readonly DictionaryWeighState, List(string evt, Funcbool guard, WeighState next) _map new(); public WeighFlow() { _map[WeighState.Idle] new() { (PlatesArrived, () true, WeighState.VehicleIn) }; _map[WeighState.VehicleIn] new() { (AllClear, () _infraredOk, WeighState.OnScale) }; _map[WeighState.OnScale] new() { (WeightStable, () _stableWeight.HasValue, WeighState.GrossRead) }; _map[WeighState.GrossRead] new() { (VehicleLeft, () true, WeighState.Leaving) }; _map[WeighState.Leaving] new() { (PlatesArrived, () true, WeighState.BackOnScale) }; _map[WeighState.BackOnScale] new() { (WeightStable, () _stableWeight.HasValue, WeighState.TareRead) }; } public bool Fire(string evt) { if (!_map.ContainsKey(_state)) return false; foreach (var (ev, guard, next) in _map[_state]) { if (ev ! evt) continue; if (!guard()) return false; _state next; return true; } return false; } }状态机的触发口是 Fire 方法。车牌回调到达时触发 PlatesArrived重量稳定时触发 WeightStable红外对射复位触发 VehicleLeft。每条转移都可以挂一个守卫条件比如 OnScale 状态下如果红外对射被遮挡即使仪表读数稳定也不允许走到 GrossRead。这就是状态机比 if-else 扎实的地方非法路径根本走不进去。数据库落账我一般用 SQLite。之前也接过 Access 老库C# 里换成 OleDbConnection 改一下连接串就行但不推荐新项目再用 Access并发的读写锁太容易卡死。SQLite 单文件部署压在一台工控机上完全够用。using (var conn new SQLiteConnection(Data Sourceweigh.db)) { conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO weigh_records (plate, gross_weight, tare_weight, net_weight, gross_time, tare_time, image_path) VALUES (p, g, t, n, gt, tt, img); cmd.Parameters.AddWithValue(p, plate); cmd.Parameters.AddWithValue(g, grossWeight); cmd.Parameters.AddWithValue(t, tareWeight); cmd.Parameters.AddWithValue(n, netWeight); cmd.Parameters.AddWithValue(gt, grossTime); cmd.Parameters.AddWithValue(tt, tareTime); cmd.Parameters.AddWithValue(img, imagePath); cmd.ExecuteNonQuery(); }落库的关键不只是插一条记录而是毛重和皮重两次记录要关联起来。我一般在车牌上用主键式的关联方式来查进厂时插入一条只有毛重的记录出厂时按车牌找到未完成记录更新皮重并计算净重。查询时要注意车牌识别偶尔会有 O 和 0 的识别差异最好统一把字母转大写、数字和字母做一次归一化清洗再入库。4.3 关键参数配置车号白名单、误差允许值、超限处理现场调试时十个问题里有八个是参数没给对。我把经常要调的参数整理成了配置表C# 程序启动时从一个 config.json 读入改了参数不用重新编译这在交付阶段非常关键。参数默认值作用与调整建议稳定判定帧数3现场波动大就加到 5响应慢但更准稳定误差阈值20 kg按地磅分度值设定大风地区适当放宽红外到位超时15 s车识别后迟迟不上磅超时转人工车牌白名单开关false开启后只认名单内车牌最小称重量100 kg小于此值视为空秤异常报警超限处理方式禁止抬杆超载车辆启动声光报警等待人工参数化配置的收益在交付后体现得最明显。比如换了一台地磅分度值从 20 公斤变成 50 公斤业主只需要在 config.json 里改一个数字没有这个设计就要去代码里翻常量、重新发布。所以这套设计源码里我最看重 config 层和状态机层而不是界面长什么样。5. 无人值守地磅常见问题与排查这 5 个地方最容易让系统翻车5.1 串口读数偶尔丢帧、重量跳到十万八千里现象程序运行几小时或半天后重量突然出现一个离谱的大数比如明明车已经下磅仪表读数还是几万公斤或者读数正常但某几帧数据中间少了一段。原因先怀疑 USB 转串口。无人值守现场工控机旁边就是道闸和灯光电磁干扰大USB 转串口的稳定性天然不如 PCI 串口卡。其次是仪表串口线过长或者屏蔽层接地没做好。解决首选换插 PCI 串口卡或用带隔离的串口服务器串口转以太网。代码层面做两层防线帧头和帧尾校验不能丢重量解析后做范围校验单次重量超过仪表量程 120% 就直接丢弃并记录一条日志等稳定判断通过才采用。5.2 车没上磅到位道闸就抬杆了现象司机把车停在磅台前人没上磅系统直接抬杆放行后续毛重数据全是空的。原因抬杆条件只依赖车牌识别回调没加红外对射到位信号。车牌识别在入口道闸处就完成了车可能还在道闸外离磅台差了十几米。解决开闸放行分为两级。入口识别只负责第一级抬杆让车进入称重区域第二级必须等红外对射确认车头进入磅台两个信号都满足才允许后续读数流程启动。状态机里把 PlatesArrived 和 AllClear 分开就是这个原因。5.3 重量稳定判定太严车一直没法出磅现象车上了磅仪表读数明明不怎么动了系统却迟迟不判定稳定入口堵了一串车司机按喇叭。原因防抖参数设太极端。_requiredCount 设成 5、_maxDiffKg 设成 5大型地磅机械结构带来的微小抖动都能让读数窗口永远达不到要求。解决把稳定窗口改为“连续 3 帧差值不超过 20 公斤”同时引入超时兜底车到位后 30 秒内仍未稳定自动转人工由司磅员确认后强制记录当前重量。无人值守不是把人去掉而是把人从重复劳动里解放出来异常时人工接管是必要的安全阀。5.4 程序在夜间无人时卡死第二天现场乱成一团现象半夜程序突然崩溃或者界面假死早上才发现夜间一辆车都没过磅门口排队一公里。原因一个线程里同时处理串口数据、HTTP 回调、数据库写入数据库偶尔锁一下主线程 UI 就跟着卡。另外异常没有被捕获一只老鼠咬断红外线或者相机断电程序没有容错路径。解决串口数据、相机回调、数据库操作绝对不要塞进 UI 线程每个设备接入点都做 try-catch 并记录日志数据库写入失败时先缓存到内存队列等服务恢复再补写。再配一个外部看门狗Windows 计划任务每 5 分钟检查一次进程是否在跑不在就拉起。这个看门狗本身极简单一条批处理加一个任务计划但能救回一整夜的业务数据。5.5 红外对射被树叶或灰尘遮挡系统误判车辆到位现象红外对射安装在磅台两侧刮风时树叶飘过去遮挡光束程序误以为有车上磅空秤状态开始读数。原因红外对射本质就是一个遮挡传感器它分不清遮挡物是卡车还是树叶子。解决把红外对射的到位判定从电平触发改成持续确认。第一次检测到遮挡后延时 2 秒再确认一次仍然被遮挡才认为车辆到位。同时配合重量零点判断红外遮挡期间如果仪表读数始终小于最小称重量视为异物遮挡而不是车辆。两个逻辑合起来误判率会低很多。6. 上线后的三个进阶技巧日志回放、断网缓存与远程运维系统上线头一个月功能跑通只是及格线真正的考验是出了问题能不能快速定位。我最早做这个项目时现场半夜打电话说磅单数据不对只能大老远跑过去抓现场在工控机上翻遍历史记录也拼不出当时的操作顺序。后来我养成了一个习惯所有关键事件都写结构化日志一台车从进厂到出厂车牌回调时间、毛重读数到达时间、稳定判定通过时间、道闸抬杆时间全部记成一行 JSON重量的原始帧也完整保留。排查时把这些日志按时间轴拉出来问题出在哪个环节一眼就能看到。第二个技巧是断网缓存。无人值守现场经常不是核心机房网络偶尔闪断如果程序只依赖实时写数据库断网期间所有称重记录都会丢。我的做法是在本地 SQLite 里维护一张待传表所有过磅记录先写本地再异步同步到中心服务器中心库里加一个 sync_flag 字段来区分是否已上传。这样断网几小时业务不中断网络恢复后自动补传后台看到的只是延迟而不是缺口。第三个技巧是配置远程化。所有可调参数集中在 config.json包括仪表类型、串口波特率、稳定阈值、超时时间甚至 LED 屏显示文案。现场业主打电话要改提示语远程改一下 JSON 再热加载就生效不需要专门跑一趟现场。热加载可以做成一个很简单的文件监听定时检查文件更新时间变了就重新加载到内存配一个管理员密码保护避免误改。我现在做任何新项目第一件事是把日志和数据分层设计好界面反而放最后。没人喜欢半夜接到现场电话而齐全的日志和远程可调参数就是给自己留的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表