ARTICLE DETAIL

资讯详情

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

C#货车称重上位机开发实战:串口通信、委托事件与数据落地

C#货车称重上位机开发实战:串口通信、委托事件与数据落地 简介这份源码面向从事物流称重系统开发的C#程序员与.NET学习者提供货车称重PC前端的完整实现方案可用于快速搭建称重数据采集、计算、显示与存储的桌面应用。压缩包共71个文件、约20.46MB以21个cs源代码文件为核心配合10个dll动态链接库、9个xml与6个resx资源文件另有sln解决方案、csproj项目文件、config配置、pfx数字签名及NuGet包等覆盖界面设计、业务逻辑与依赖管理各层面。项目采用Visual Studio工程结构包含登录、称重、数据同步等模块并引入Newtonsoft.Json与Excel互操作组件便于对接外部数据与报表导出。目前已有346人学习下载适合需要参考PC端称重系统架构、学习C#窗体开发与资源组织方式的开发者借鉴与二次开发。1. 货车称重 PC 前端到底在做什么从地磅仪表到 C# 上位机的一条数据链一辆满载的货车开上地磅30 秒内要完成车牌识别、毛重采集、皮重匹配、扣杂、结算单打印还要把数据同步到 ERP。这套流程里司机只关心一件事——数字准不准、单子出得快不快。而站在磅房里的操作员盯着的就是一块用 C# 写的 PC 前端界面。基于 C# 语言的货车称重 PC 前端设计源码本质上就是一套跑在 Windows 工控机上的上位机程序它通过串口或网络从称重仪表读重量通过相机或读卡器拿车辆身份再把两者绑定成一条不可篡改的过磅记录。热搜里高频出现的「C# 上位机」「C# 连接西门子 OPC」「C# 委托」这些词恰好覆盖了这套系统的三个技术底座——通信、解耦、界面刷新。它适合谁适合手里已经有地磅和仪表、但还在用 Excel 手工记账的现场工程师也适合想从 Web 转工控、拿一个真实项目练手的 C# 开发者。下面我按自己做过几套磅房系统的顺序把这条数据链拆开讲清楚。2. 称重数据怎么进到 C#串口、OPC 与仪表协议的三条路称重前端的第一道坎不是画界面而是把仪表里的重量稳定地读进来。仪表厂商五花八门托利多、耀华、志美、柯力的协议各不相同但落到 C# 这一侧常见做法无非三条串口直读、OPC 中转、厂商 DLL 调用。选哪条取决于你现场仪表的型号和是否允许改接线。2.1 串口直读最通用也最容易翻车的一条路绝大多数地磅仪表都带 RS232 或 RS485 口默认连续发送重量帧。C# 里用SerialPort类就能接。下面是我一般会先写的一个最小可跑通的读取循环用来验证协议对不对using System; using System.IO.Ports; public class ScaleReader { private readonly SerialPort _port; public ScaleReader(string portName) { // 波特率、数据位、校验位必须和仪表说明书完全一致错一位就是乱码 _port new SerialPort(portName, 9600, Parity.None, 8, StopBits.One); _port.ReadTimeout 500; _port.DataReceived OnDataReceived; // 事件回调避免主线程阻塞 } public void Start() _port.Open(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { // 仪表一帧通常 8~18 字节先读全部缓冲再解析 string raw _port.ReadExisting(); decimal weight ParseFrame(raw); if (weight 0) WeightArrived?.Invoke(this, weight); // 用事件把数据抛给界面层 } catch (TimeoutException) { // 超时不算故障连续超时 3 次才需要报警 } } private decimal ParseFrame(string frame) { // 以托利多连续输出为例STX 状态 6位重量 单位 ETX int stx frame.IndexOf(\x02); int etx frame.IndexOf(\x03); if (stx 0 || etx stx) return -1; string body frame.Substring(stx 1, etx - stx - 1); if (body.Length 8) return -1; string num body.Substring(2, 6).Trim(); return decimal.TryParse(num, out var w) ? w : -1; } public event EventHandlerdecimal WeightArrived; }这段代码的关键点有三个。第一DataReceived是后台线程触发的绝对不能在回调里直接更新 WinForm 控件否则就是经典的跨线程异常必须用事件或Invoke抛回 UI 线程。第二ReadExisting读到的可能是半帧真实项目里要维护一个缓冲区按 STX/ETX 切帧我这里为了讲清逻辑做了简化。第三ParseFrame里的偏移量Substring(2, 6)是托利多格式换成耀华 XK3190 就是另一套偏移必须拿仪表说明书逐字节对这是血泪经验猜格式十有八九读出来是负数或零。参数上要盯死四个波特率常见 9600 或 19200、校验位None/Even、停止位、以及仪表是否处于「连续发送」模式。很多现场读不到数不是代码问题是仪表菜单里被设成了「命令应答」模式得先发指令才回一帧。2.2 OPC 中转当仪表挂在 PLC 后面时如果现场是西门子 S7-1200/1500 把重量读进 PLC再想给 C# 用热搜里那个「C# 连接西门子 OPC」就派上用场了。常见做法是装 KEPServer 或西门子自带的 OPC 服务器C# 侧用 OPC UA 客户端订阅那个重量变量。这条路的好处是不用碰仪表接线坏处是多一层软件、多一个授权成本而且 OPC 服务器一崩整个称重就断了。我一般只在仪表已经被 PLC 接管、且客户已有 OPC 授权时才选它。2.3 厂商 DLL 与动态库调用少数高端仪表提供 .NET 或 C 的动态库。C# 调 C DLL 时热搜里那个「access violation c0000005」几乎人人都会遇到一次。根因通常是调用约定不匹配或字符串编码不对。声明DllImport时把CallingConvention和CharSet显式写清楚比事后调试内存崩溃省事得多using System.Runtime.InteropServices; public static class ScaleSdk { // Cdecl 还是 StdCall 必须和头文件一致写错就是栈失衡 c0000005 [DllImport(ScaleSdk.dll, CallingConvention CallingConvention.Cdecl, CharSet CharSet.Ansi)] public static extern int ReadWeight(out double weight); [DllImport(ScaleSdk.dll, CallingConvention CallingConvention.Cdecl, CharSet CharSet.Ansi)] public static extern int OpenPort(string portName, int baud); }CallingConvention决定参数压栈顺序和谁清栈CharSet决定字符串按 ANSI 还是 Unicode 传。这两个参数写错轻则返回乱码重则进程直接崩。选型上我的排序是能串口就串口串口被占用才考虑 OPC只有厂商强推 SDK 才走 DLL。3. 界面与业务解耦委托、事件和跨线程刷新的正确姿势重量读进来了接下来是把它变成操作员看得懂、点得动的界面。这一层最容易写成一锅粥——串口回调里直接改 Label业务逻辑和控件纠缠在一起后期加个「稳定判定」就得动全身。正确做法是用委托和事件把数据层和界面层切开。3.1 用委托和事件做数据总线热搜里「C# 委托」「C# 委托和事件」出现频率极高说明很多人卡在这一步。我的习惯是定义一个称重服务类对外只暴露事件界面订阅事件刷新自己// 数据层只负责产生重量不关心谁在听 public class WeighService { public event Actiondecimal, bool WeightUpdated; // 重量, 是否稳定 private decimal _last -1; private int _stableCount 0; public void OnRawWeight(decimal w) { // 稳定判定连续 5 帧波动小于 0.5kg 视为稳定 bool stable Math.Abs(w - _last) 0.5m; _stableCount stable ? _stableCount 1 : 0; _last w; WeightUpdated?.Invoke(w, _stableCount 5); } } // 界面层订阅事件用 Invoke 切回 UI 线程 public partial class MainForm : Form { private readonly WeighService _service new WeighService(); public MainForm() { InitializeComponent(); _service.WeightUpdated (w, stable) { if (IsDisposed) return; // 事件可能在串口线程触发必须 Invoke 回 UI 线程 BeginInvoke(new Action(() { lblWeight.Text w.ToString(F0) kg; lblStable.Text stable ? 已稳定 : 波动中; btnSave.Enabled stable; // 不稳定不允许保存防止录到跳变值 })); }; } }这段代码解决两个问题。一是稳定判定地磅上货车没停稳时重量会跳直接取瞬时值会录错连续多帧波动小于阈值才算稳定这是称重业务的核心逻辑不是可选项。二是跨线程刷新BeginInvoke把更新排进 UI 消息队列比Invoke少一次阻塞等待界面更跟手。参数上稳定帧数这里 5 帧和波动阈值0.5kg要按仪表输出频率调仪表 10Hz 输出时 5 帧约半秒体感刚好。3.2 主界面布局与关键控件一个能上生产的称重界面控件不多但每个都有讲究。下面是我常用的布局要素区域控件作用注意点顶部大号 Label实时重量字号至少 72磅房远距离可读中部DataGridView过磅记录列表绑定 BindingList自动刷新右侧车牌输入框车号录入配合读卡器可自动填充底部按钮组保存/打印/去皮未稳定时置灰状态栏StatusStrip串口状态断线要变红并声音提示DataGridView绑定数据源时用BindingListT而不是ListT因为前者在增删元素时会自动通知界面刷新后者不会。这个差别新手经常踩表现为「数据存进去了但表格不更新」。3.3 车牌与车辆身份的绑定重量必须和车绑定才有意义。常见做法有三种人工输入车牌、IC 卡读卡器、车牌识别相机。人工输入最便宜但最慢磅房高峰期会排队读卡器快但要给每辆车发卡相机最省事但受光照和污损车牌影响。我一般推荐「读卡器为主 人工兜底」因为相机在夜间和雨天的识别率会让操作员抓狂。绑定逻辑上一次完整过磅要记录车牌、毛重、皮重、净重、时间、操作员其中皮重是从历史记录里按车牌匹配的匹配不到就得先过一次空车。4. 数据落地与打印从内存记录到不可篡改的过磅单界面跑通了数据不能只停在内存里。磅房数据是要对账的一旦被质疑你得拿得出原始记录。这一层要解决存储、防篡改和打印三件事。4.1 本地库选型与表结构单机磅房我一般用 SQLite零配置、单文件、方便备份。多磅房联网才上 SQL Server 或 MySQL。核心表结构大致如下CREATE TABLE WeighRecord ( Id INTEGER PRIMARY KEY AUTOINCREMENT, PlateNo TEXT NOT NULL, -- 车牌 GrossWeight REAL NOT NULL, -- 毛重 TareWeight REAL NOT NULL DEFAULT 0,-- 皮重 NetWeight REAL NOT NULL, -- 净重 WeighTime TEXT NOT NULL, -- ISO8601 时间 Operator TEXT, -- 操作员 IsVoid INTEGER NOT NULL DEFAULT 0,-- 是否作废作废不删除 Remark TEXT ); CREATE INDEX idx_plate ON WeighRecord(PlateNo); CREATE INDEX idx_time ON WeighRecord(WeighTime);两个设计要点。第一作废用标记位而不是 DELETE过磅记录一旦生成就不能物理删除这是审计要求也是保护操作员自己的后悔药。第二车牌和时间都建索引因为查历史皮重和查当日汇总都靠这两个字段数据量上万后没索引会明显卡顿。4.2 用事务保证毛皮重配对一次过磅涉及「查历史皮重 插入新记录」这两步必须在一个事务里否则并发或异常时会出现半条记录public void SaveWeigh(string plate, decimal gross, string op) { using var conn new SQLiteConnection(Data Sourceweigh.db); conn.Open(); using var tx conn.BeginTransaction(); // 开启事务 try { // 取该车牌最近一次有效皮重 decimal tare 0; using (var cmd conn.CreateCommand()) { cmd.CommandText SELECT TareWeight FROM WeighRecord WHERE PlateNop AND IsVoid0 ORDER BY WeighTime DESC LIMIT 1; cmd.Parameters.AddWithValue(p, plate); var r cmd.ExecuteScalar(); if (r ! null) tare Convert.ToDecimal(r); } decimal net gross - tare; using (var cmd conn.CreateCommand()) { cmd.CommandText INSERT INTO WeighRecord(PlateNo,GrossWeight,TareWeight,NetWeight, WeighTime,Operator) VALUES(p,g,t,n,time,op); cmd.Parameters.AddWithValue(p, plate); cmd.Parameters.AddWithValue(g, gross); cmd.Parameters.AddWithValue(t, tare); cmd.Parameters.AddWithValue(n, net); cmd.Parameters.AddWithValue(time, DateTime.Now.ToString(o)); cmd.Parameters.AddWithValue(op, op); cmd.ExecuteNonQuery(); } tx.Commit(); // 两步都成功才提交 } catch { tx.Rollback(); // 任一步失败全部回滚 throw; } }BeginTransaction到Commit之间的操作要么全成要么全败这是防止「插入了毛重但没匹配上皮重」的关键。参数化查询p这种不只是防注入还能让 SQLite 复用执行计划批量插入时快不少。4.3 打印过磅单打印用System.Drawing.Printing里的PrintDocument把记录画到Graphics上。磅单一般一式两联字段固定我习惯把模板做成可配置的因为不同客户要的字段和抬头不一样。打印前务必先PrintPreviewDialog预览一次直接送打印机在针式打印机上很容易跑偏。5. 避坑与排查称重前端最常见的五类翻车现场这一章是我这些年攒下的排错清单每条都按「现象 → 原因 → 解决」写遇到问题照着对。5.1 重量一直显示 0 或负数现象串口打开了但界面重量始终是 0 或 -1。原因九成是协议解析偏移量不对或者仪表处于命令应答模式没主动发帧。解决先用串口调试助手看原始字节确认仪表到底在发什么再拿说明书逐字节核对 STX、数据位、单位位的位置。别猜猜就是浪费时间。5.2 界面卡死、点不动现象过磅时界面偶尔无响应几秒。原因把耗时操作数据库写入、打印放在了 UI 线程或者串口回调里直接Invoke同步等待。解决数据库和打印放到Task.Run里异步执行串口回调统一用BeginInvoke。记住一条UI 线程只干刷界面这一件事。5.3 跨线程操作控件异常现象程序跑一会儿弹「线程间操作无效」。原因DataReceived或Task里直接改了控件属性。解决所有控件更新都包在BeginInvoke里或者干脆用事件把数据抛回界面层统一处理。这个异常在调试时不一定复现上线后才冒出来属于典型玄学提前规范写法比事后抓强。5.4 皮重匹配错车现象两辆同型号车先后过磅净重算错。原因只按车型匹配皮重没按车牌精确匹配。解决皮重必须按车牌唯一匹配匹配不到就提示先过空车绝不允许多个车牌共用一个皮重。这是业务规则问题代码写得再对规则错了照样出错。5.5 数据重复插入现象同一次过磅在记录里出现两条。原因保存按钮没做防抖操作员连点两下或者网络重试导致重复提交。解决保存时先禁用按钮事务提交后再恢复数据库层给「车牌 时间戳」加唯一约束兜底。防抖和唯一约束双保险比事后删数据省心。6. 让称重前端更耐用的三个进阶技巧前面把主链路走通了最后说几个能让这套 C# 称重前端在現場活得更久的技巧都是我踩过坑之后固定下来的习惯。第一个是断线自动重连。串口和 OPC 都会因为电磁干扰、线缆松动掉线程序不能一掉就等人来重启。我的做法是起一个后台定时器每 3 秒检查一次连接状态断了就尝试重开连续失败 5 次才弹窗报警。重连期间界面重量显示「--」而不是保留旧值避免操作员误以为还是实时数据。这个逻辑不复杂但能省掉大量半夜被叫去现场的电话。第二个是重量数据的滑动平均滤波。地磅受风、车辆发动机震动影响原始值会有小幅抖动。除了前面说的稳定判定我还会对最近 3 帧做滑动平均再显示让数字看起来更稳。但要注意滤波只用于显示入库必须用稳定后的原始值否则对账时会有零点几公斤的偏差说不清。下面是一个简单的滑动窗口public class MovingAverage { private readonly Queuedecimal _window new Queuedecimal(); private readonly int _size; public MovingAverage(int size) _size size; public decimal Add(decimal value) { _window.Enqueue(value); if (_window.Count _size) _window.Dequeue(); // 超出窗口就丢掉最旧的 decimal sum 0; foreach (var v in _window) sum v; return sum / _window.Count; } }_size取 3 到 5 比较合适太大响应会迟钝操作员会觉得「数字跟不上车」。第三个是日志留痕。称重系统出问题事后复盘全靠日志。我一般用NLog或Serilog把每次串口收发、每次数据库写入、每次异常都记下来按天切文件。日志里带上时间戳和车牌出争议时能直接定位到某一次过磅的完整过程。这一步很多人嫌麻烦跳过等真出事就知道日志是唯一的后悔药。最后一个习惯上线前一定拿真实货车跑一遍全流程包括空车、重车、同车牌二次过磅、断网、断电重启。实验室里用砝码测出来的稳定和现场一辆真车开上来的震动完全是两回事。我吃过这个亏砝码测试全绿真车一上重量跳得没法看最后是调了稳定帧数和滤波窗口才压住。希望帮到你。本文还有配套的精品资源点击获取
返回列表