十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

C#上位机称重系统实战:从串口通信到业务落地的完整指南

C#上位机称重系统实战:从串口通信到业务落地的完整指南 干了很多年上位机开发称重系统算是我做过的项目里最“不起眼但事最多”的一类。表面上看过磅软件不就是读个数、存个记录、打个票嘛可真把一套地磅程序从需求聊到上线你会发现里面藏着串口通信、数据稳定性、业务流程控制、权限管理、打印排版、甚至防作弊一堆事。这篇博文我就拿C#的完整源码思路来讲讲称重系统到底怎么落地从硬件接线到代码实现再到那些坑一次性说透。这套内容适合正在做C#上位机开发、刚接触工控项目、或者接了称重项目不知道怎么下手的同学。我尽量不写那种“教科书式”的空话直接给你一套能抄的作业同时把每一步为什么要这么做的逻辑也讲清楚。项目本身不复杂但想把细节做好确实需要花不少心思。1. 先搞清楚称重系统到底在解决什么问题很多新手拿到这种项目第一反应是打开Visual Studio开始拖控件。我劝你冷静先花半天时间把现场流程摸清楚否则后面改起来会非常痛苦。称重软件的核心不是界面而是它背后对应的一条完整的业务流程。1.1 一套地磅系统由哪些硬件组成先说硬件因为软件写得再好硬件不对接都是白搭。标准的地磅称重系统一般包含这几样东西地磅秤台上面有多个称重传感器这是重量信号的源头。称重仪表传感器把形变转成电信号仪表再把这个信号转成数字并显示在表头上。计算机也就是工控机或者普通电脑运行我们的过磅软件。打印机用于打印称重小票通常是用热敏打印机或者针式打印机。辅助设备比如道闸、红绿灯、摄像头、LED显示屏这些不是必备的但很多现场会加。电脑和称重仪表之间最常见的通信方式就是串口RS232有些老仪表是RS485需要转接头。仪表把实时的重量值通过串口发出来软件只需要去读串口、解析字符串就能拿到当前重量。理解这一点非常重要因为这意味着所有称重系统软件的起点就是“串口通信 字符串解析”。界面做得再炫数据源搞不定就全是空的。1.2 一次过磅的完整业务逻辑很多需求方自己都说不清流程但作为开发者你一定要帮他理清楚。最常见的汽车衡地磅业务流程是这样的车辆上磅称毛重。这个时候车上装着货比如一车沙子。软件记录毛重、时间、车号可能还要抓拍照片。车辆去卸货。空车再上磅称皮重。软件用毛重减去皮重得到净重生成一条完整的过磅记录并打印小票。这是最经典的“毛重-皮重-净重”模式全中国的散装物料过磅几乎都是这个套路。难点在于软件怎么知道这一次是称毛重还是皮重这就需要业务状态控制了。我用得最多的方式是给“磅单”设计一个状态字段比如0表示未完成、1表示完成。然后通过车号来判断如果这辆车没有进行中的毛重记录那这次上磅就当作毛重写进去如果已经有毛重记录了那这次就是皮重直接算净重生成记录状态置为完成。有朋友可能会问如果车辆不按套路来先称皮重、后称毛重怎么办所以很多系统允许设置“允许先皮后毛”的选项。开发的时候不要把这些写死做成配置项比什么都强。1.3 从MVP做起别一上来就想搞大而全我之前带人做项目最怕的需求是“我要像高速公路收费站一样全自动”。自动识别车牌、自动抬杆、自动抓拍、自动语音播报听起来很酷但你要知道这些功能每一个背后都是单独的系统集成工作。车牌识别要加摄像头和算法模块道闸要接IO控制LED要接通信协议任何一个环节出问题整套流程就卡住了。所以我一直建议第一版先做MVP最小可行产品核心就是串口读重量、人工录车号、自动算净重、存数据库、打小票。这套流程跑顺了稳定了再往上面加自动化的东西。我们在后续章节讲到的架构也是按照这个思路组织的保证你后面加功能不需要推翻重来。2. 技术选型为什么C# WinForm依然是称重系统的首选选型这个话题每次聊都能吵起来。有说用WPF的有说用Web的甚至还有说用LabVIEW的。但到目前为止工业现场用得最多的依然是C# WinForm这不是没有道理的。2.1 Windows工控环境决定了技术栈你去看称重仪表厂商提供的SDK和例程绝大多数都是基于Windows的提供的动态库也基本是C或者C#写的有的甚至直接给一个DLL让你调用。工控机装的系统也是Windows 7、Windows 10这种很少有人说现场给我搞个Linux服务器跑称重软件。既然终端环境是Windows那就用Windows平台最成熟的桌面开发框架WinForm在这件事上有天然优势部署简单双击装个.NET Framework就行串口控件直接用报表打印方面第三方库支持也好。WPF确实界面更漂亮动画更炫但在工业现场这些都不重要。现场看重的是稳定、操作效率、字体清晰、按钮够大。WinForm的DataGridView在显示称重记录时比WPF的DataGrid写起来简单粗暴很多。我做过的项目里WinForm依然是最稳妥的选择。2.2 数据库到底选SQLite、Access还是SQL Server关于数据库要看项目的使用规模我分三种场景说单机版只有一台电脑过磅数据量也不大首选SQLite。它是文件型数据库不用装服务一个.db文件搞定一切备份直接复制文件对不懂数据库的客户来说非常友好。小局域网版两三台电脑同时使用比如一个磅房一台、办公室一台。这种情况推荐SQL Server Express或MySQL。Express版免费性能完全够用。大型集团版多个地磅、多个分公司数据要汇总那就得正经上SQL Server标准版或者走HTTP接口把数据推送到云端。我现在做项目默认用SQLite做单机SQL Server做网络版。代码层面用三层架构数据访问层单独封装换数据库的时候只需要改连接字符串和少量SQL方言不用动UI。2.3 读懂仪表协议是选型和开发的前提读重量之前必须先拿到仪表厂家提供的通信协议文档。这个东西因厂家而异但主流仪表基本就两种模式第一种是连续发送模式。仪表不断往外发数据比如每秒10次格式可能是STGS000100.0kg这种ASCII码字符串。软件只需要监听串口的数据接收事件解析每一帧就行。第二种是指令应答模式。软件发一条指令仪表才回一条数据。这种方式的好处是软件可以控制读取节奏但需要处理好指令和应答的配对。做这个的时候我建议第一步先拿串口调试助手我用VSPD虚拟串口真实仪表都测过把仪表发出来的原始报文看懂。你连原始数据都没见过就开始写解析代码那纯属瞎忙活。数据格式看明白了后面的代码就是几分钟的事。3. 核心功能实现从串口读数到打印小票的完整链路下面进入正题我把这套称重系统最核心的几个模块拆开讲每一段都给出关键代码和设计思路。代码是实际项目里整理出来的去掉了一些和业务强绑定的部分你可以直接参考。3.1 串口通信模块别直接用DataReceived事件做解析C#的SerialPort类提供了一个DataReceived事件新手最喜欢在里面直接做字符串解析。这样写有个问题串口数据是按字节流到达的不是按帧到达的。一次接收事件拿到的数据可能只有半帧也可能攒了好几帧。如果你每次都在事件里解析很容易出现乱码、缺数据、重量不对的情况。我的做法是事件里只管把数据追加到缓冲区然后按帧头帧尾从缓冲区里完整地“抠”出一帧再交给解析函数。这样不管数据怎么粘包、拆包都能稳定处理。// 串口数据接收只做缓冲不直接解析 private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { int bytesToRead serialPort1.BytesToRead; byte[] buffer new byte[bytesToRead]; int bytesRead serialPort1.Read(buffer, 0, bytesToRead); string chunk Encoding.ASCII.GetString(buffer, 0, bytesRead); lock (recvLock) { recvBuffer.Append(chunk); ProcessRecvBuffer(); // 从缓冲中提取完整帧 } } catch (Exception ex) { // 串口读异常别让程序崩了记录日志即可 Logger.Error(串口接收异常, ex); } }ProcessRecvBuffer的逻辑就是找帧头比如或者ST一直找到帧尾比如换行符\n把中间的字符串截取出来。针对不同的仪表这个函数需要定制化但思路不变先找完整的帧再解析。对了SerialPort的ReceivedBytesThreshold属性可以调默认是1也就是来一个字节就触发一次事件。在数据量大的时候这是灾难建议把它设为帧长度的一半可以有效减少事件触发次数。3.2 重量解析稳定判断和数值提取解析出来的字符串要转成“重量值”注意几个细节单位要从字符串里去识别有的仪表不带单位这时候需要你自己配置“分度值”。比如仪表显示1000kg但发出来的字符串可能是0010000小数点位置是配置出来的。我建议在参数设置界面加一个“小数点位数”的配置项默认0位地磅一般是0到3位不等。稳定标志很重要。仪表输出的字符串里一般有一个状态位比如ST代表稳定StableUS代表不稳定。业务上要求“重量稳定后才能保存数据”这个逻辑必须做否则车辆在磅上来回动的时候软件就会把跳动的重量当成真实值。负数和小数要兼容。有的仪表会输出-000005.0kg你的解析代码得能处理符号和小数。下面是我常用的重量解析逻辑public bool TryParseWeight(string frame, out decimal weight, out bool isStable) { weight 0m; isStable false; // 假设帧格式类似STGS000100.0kg if (string.IsNullOrEmpty(frame)) return false; // 判断稳定位 isStable frame.Contains(ST) || frame.StartsWith(ST); // 提取数字部分 Match match Regex.Match(frame, [-]?\d(\.\d)?); if (match.Success decimal.TryParse(match.Value, out weight)) { return true; } return false; }这种方式用正则提取数字应对大多数ASCII协议的仪表都够了。但如果仪表是Modbus RTU协议那就不能这么写了得按Modbus的寄存器地址去读。Modbus的实现也不难网上有很多成熟库比如NModbus我只提一个注意点Modbus设备地址要和你仪表里设置的从站地址一致否则指令发过去没响应。3.3 重量数据稳定判断与业务防抖这里的“防抖”不只是仪表本身的稳定标志还包括汽车在磅上停稳的过程。我做过一个现场司机上磅后轮胎动一下、踩一脚刹车重量都会跳。如果软件不加延时很容易在重量还没稳定的时候就把数据存了对客户来说就是亏钱的事。所以在保存毛重/皮重之前我通常要求连续读到N次比如3次稳定且差值不超过10kg的重量值才认为数据有效。这个判断是在业务层做的不是串口层。下面这段代码是核心逻辑private Queuedecimal stableQueue new Queuedecimal(); private const int StableSampleCount 3; private const decimal StableMaxDeviation 10m; // 单位kg private bool IsWeightStable(decimal currentWeight) { stableQueue.Enqueue(currentWeight); if (stableQueue.Count StableSampleCount) stableQueue.Dequeue(); if (stableQueue.Count StableSampleCount) return false; decimal max stableQueue.Max(); decimal min stableQueue.Min(); return (max - min) StableMaxDeviation; }样本次数、允许的偏差值我都做到配置文件里。不同的地磅吨位不一样100吨的地磅和10吨的小秤波动范围完全不同得让现场人员能调。另外还要注意“零位跟踪”。有的仪表在空秤时会输出一个很小的值比如0.5kg这不是故障。业务上一般会把绝对值小于某个阈值比如20kg的重量当作0避免空车数据里出现奇怪的瑕疵。3.4 毛重皮重业务控制状态机设计这块我认为是整个称重软件最需要用心的地方。用一个状态机去管理每一辆车的过磅状态比用一堆if else要清晰得多。我定义的核心数据结构是WeightTicket对应数据库里的一条过磅记录。字段包括public class WeightTicket { public long Id { get; set; } public string PlateNumber { get; set; } // 车号 public string MaterialName { get; set; } // 货名 public string Supplier { get; set; } // 供货方 public decimal GrossWeight { get; set; } // 毛重单位kg public decimal TareWeight { get; set; } // 皮重单位kg public decimal NetWeight { get; set; } // 净重 public DateTime GrossTime { get; set; } // 毛重时间 public DateTime TareTime { get; set; } // 皮重时间 public string Operator { get; set; } // 操作员 public string Status { get; set; } // 进行中/已完成 }保存毛重时查一下有没有这辆车的“进行中”记录没有就新建一条写入毛重和毛重时间状态设为“进行中”。有那这次就是皮重更新皮重计算净重状态改成“已完成”并且可以触发打印。这套逻辑用SQL来表达也很简单SELECT TOP 1 * FROM WeightTicket WHERE PlateNumber plate AND Status 进行中 ORDER BY GrossTime DESC;数据库那边要给PlateNumber Status建索引否则数据量大了以后每次过磅都全表扫描现场会明显卡顿。我遇到过一次几万条记录后每次查询要两秒多加了索引立刻到毫秒级。3.5 数据落库与唯一性控制防止重复过磅防重复过磅在现场是很现实的问题。有的司机为了多过几次磅会故意绕圈再上一次。软件层面怎么做比较粗糙的办法是限制同一车号在一段时间内不能重复生成毛重记录。但“一段时间”不好拍脑袋不同厂区节奏完全不同。我用的策略是可配置的“最小过磅间隔分钟数”默认比如5分钟。如果同一车号距离上一次毛重记录时间小于这个间隔就弹窗提示操作员“该车X分钟前刚过磅确定继续吗”让操作员来决策。这样既防了恶意重复也不会因为个别特殊情况把流程卡死。另外数据库层面要加唯一约束的话不能简单对PlateNumber做唯一因为同一辆车一天要过好几次磅。我建议加一个业务流水号比如“过磅日期车号物料”用这个做唯一性控制。但说实话现实生活中车牌识别不完全可靠太严格的唯一约束反而会误伤正常业务所以最终我选择了软校验人工确认的方案。3.6 打印小票热敏打印机最简单打印这块有几种路线。如果现场用的是Windows驱动的普通打印机你只要用PrintDocument类绘制内容就行和画界面差不多。但地磅房现在用得最多的是58mm或者80mm热敏打印机这种打印机的优势是速度快、成本低、换纸方便。热敏打印机的驱动方式有两种第一种是装Windows驱动然后当成普通打印机用。这种方式最简单但有时候现场装驱动会出各种幺蛾子。第二种是直接走串口或者网口用ESC/POS指令控制打印机。这种方式不依赖驱动稳定性高很适合工控环境。核心指令就几个初始化、打印文字、切纸、走纸。我用C#封装过一个小类核心打印方法大致长这样private void PrintTicket(WeightTicket ticket) { // 58mm热敏纸一行最多大约32个中文字符 var lines new Liststring(); lines.Add( XX公司过磅单); lines.Add(--------------------------------); lines.Add(车号 ticket.PlateNumber); lines.Add(货名 ticket.MaterialName); lines.Add(string.Format(毛重{0,10:N0} kg, ticket.GrossWeight)); lines.Add(string.Format(皮重{0,10:N0} kg, ticket.TareWeight)); lines.Add(string.Format(净重{0,10:N0} kg, ticket.NetWeight)); lines.Add(日期 ticket.GrossTime.ToString(yyyy-MM-dd HH:mm:ss)); lines.Add(--------------------------------); lines.Add(); lines.Add(); foreach (var line in lines) { byte[] buffer Encoding.Default.GetBytes(line \n); serialPortPrinter.Write(buffer, 0, buffer.Length); } }注意几个坑热敏打印机的编码中文一般用GBKEncoding.Default在某些系统上就是GBK别用UTF-8否则中文打出来全是乱码。打印内容别太宽58mm纸超过32个汉字就会换行排版容易错乱。打印完成以后要判断打印机是否缺纸。串口打印机可以通过状态指令查询但很多国产打印机并不标准这块做不了太深入至少要做到“打印异常不让程序崩溃”。3.7 操作界面设计大按钮、大字、防误操作最后提一嘴界面。地磅房的环境往往比较嘈杂操作员可能戴着手套屏幕可能有点反光。所以按钮要大、颜色要分明、关键操作要有二次确认。我常用的界面布局是左侧当前重量显示区用超大字体实时显示仪表重量稳定状态用绿/红颜色区分。中间车号、货名、客户等业务信息的录入区。车号如果能接车牌识别就直接读否则做一个常用车号下拉自动补全。右侧最近的过磅记录列表用DataGridView展示。底部毛重按钮、皮重按钮、打印补打按钮。“毛重”和“皮重”这两个按钮要做的特别醒目而且要加防误触逻辑。我见过操作员聊天分心本来要存毛重结果点了旁边的删除按钮一条记录没了。所以删除记录一律要有管理员权限普通操作员只能做称重操作这样能少很多麻烦。4. 开发与调试中遇到的坑还有排查方法这段算是我个人经验里最有价值的部分。这些坑文档里一般不会写但每个都是真实耗过我时间的。4.1 串口数据乱码、丢数据现象串口调试助手看着数据正常但程序里收出来是乱码或者断断续续。排查思路先检查波特率、数据位、停止位、校验位是否和仪表一致。这是最基本但也是最容易错的仪表说明书写了96008N1你的代码串口参数必须一字不差。再检查USB转串口线的质量。现场很多工控机用USB转RS232劣质转换芯片比如CH340的兼容版本在高波特率下就是会丢数据。我建议现场一律用原装的FTDI芯片或者至少是正品CH340贵一点但值得。最后才是代码层面的粘包拆包问题用我们前面说的“缓冲完整帧提取”方案解决。4.2 重量跳数、归零不准现象仪表屏幕显示稳定但软件里数字偶尔跳一下或者空秤时总是有几十公斤的偏差。原因分析信号干扰。地磅房的电源往往不干净大功率设备启动会导致电压波动串口信号不稳。解决办法是给电脑和仪表都用带滤波的电源信号线走线避开动力电缆。接地问题。这是医疗设备、工控设备老生常谈的问题。地磅传感器信号和仪表外壳如果不共地会产生电位差表现出来就是重量漂移。现场排查时拿万用表量一下仪表外壳和电脑机箱之间的电压超过1V基本就是接地有问题。软件层面的处理除了前面说的“多次采样取稳定”还可以对重量做一阶低通滤波。代码如下// 一阶低通滤波alpha取值0.1~0.3 public decimal FilterWeight(decimal newValue, decimal lastValue, decimal alpha 0.2m) { return lastValue alpha * (newValue - lastValue); }不过要提醒一点滤波会带来滞后。车辆快速上磅时显示值会“追”不上真实重量所以滤波只用在显示和业务判断上不要影响最终保存的数值。4.3 数据库并发读写冲突现象多台电脑同时过磅时偶尔提示数据库正忙或者记录丢失。原因分析SQLite在高并发写入场景下会有锁冲突。SQLite本质上是单写多读多个进程同时写就容易报“database is locked”。解决办法一是把账套数据库放到本地SSD减少锁等待时间二是写入操作加上重试机制三是如果并发确实多直接换SQL Server。在代码层面所有数据库操作统一走一个数据访问入口不要每个窗体各写各的SQL。这样出了问题只需要在一个地方调整连接策略。4.4 程序崩溃导致正在过磅的数据丢失现象过磅到一半停电或者软件被强制关闭刚录的毛重记录没了。解决办法毛重记录在写入数据库成功后界面才提示成功。不要先更新界面再写库。写库操作做成事务性的插入毛重记录时如果有未完成的皮重记录同时更新。要么都成功要么都失败。有条件的话仪表数据本身就是纸质的“备份”所以不用担心没有冗余。但是软件这边建议加一个本地操作日志表记录每一步关键操作方便事后追溯。4.5 常见问题速查表现象可能原因解决办法软件收不到串口数据串口号选错、USB转串口驱动没装好设备管理器里查看COM口号换USB口重插数据乱码波特率/校验位不匹配核对仪表协议参数重新配置中文打印乱码编码用了UTF-8改为Encoding.DefaultGBK或GB2312重量数值偶尔跳变接地不良、电源干扰检查接地加滤波电源软件加滤波数据库锁死SQLite多进程写入换SQL Server或加写入重试机制车牌识别偶尔误读车牌识别算法局限性人工确认车号识别结果作为默认值打印小票位置偏移打印机纸宽设置不对调整打印内容宽度检查打印机参数5. 进阶扩展从“能用”到“好用”前面这套做下来一套标准地磅软件已经能上线了。但如果你想让项目更有竞争力或者在投标的时候更有话说下面这几个扩展方向很值得做。我按投入产出比排序讲。5.1 防作弊数据加密与操作审计地磅行业的痛点是作弊。作弊的手段五花八门从最简单的遥控器干扰传感器信号到内部人员改数据库。软件层面能做的数据加密数据库里的关键数值不存明文用一定规则加密存储。比如重量值乘以一个密钥再加一个偏移量就算有人拿到数据库文件也看不懂数值。操作审计所有的修改、删除、补录操作都记录下来。包括操作员账号、时间、修改前数值、修改后数值、操作原因。这块我用过最简单的方式就是建一张AuditLog表所有写操作走统一的日志方法。前端界面显示的是正常数据后台审计表里藏着每一次操作的痕迹。仪表数字信号高于模拟信号向客户强调数字式地磅比模拟式地磅更难作弊。这不是软件功能但作为系统集成方案的一部分你能帮客户选型本身就是专业度体现。5.2 车牌识别与自动过磅说真的车牌识别现在是成熟技术成本也不高几百块的USB摄像头加一个识别库就能跑。但自动过磅的难点不在识别在于“怎么确认车停稳了并且过了磅”。这里涉及一套完整的逻辑地感线圈检测到车到位。摄像头抓拍识别车牌。等待重量稳定。自动保存重量抬杆放行。这套流程要实现软件侧需要对接地感信号一般通过IO卡或者PLC、摄像头SDK、道闸控制。说实话工作量比核心称重逻辑还大而且现场设备的稳定性是最大挑战。所以我的建议是把车牌识别做成“辅助录入工具”识别结果自动填到车号输入框里操作员确认一下就行。这样既提升了效率又不用承担全自动过磅的稳定性风险。5.3 与ERP/财务系统对接很多企业不止要过磅数据还要把数据同步到ERP系统里。最常见的是金蝶、用友、SAP还有各种定制化系统。对接方式主要有两种直接操作对方数据库。前提是对方愿意开放表结构风险是对方升级数据库可能导致程序报错。通过Web API/中间表。这是目前的主流做法我们开发一个数据同步服务定时把本地过磅记录同步到中间库或者调用对方API写入。我用得最多的是中间表定时同步。原因很简单对方系统的变动不会直接影响到我的称重程序。称重程序只需要关心“有没有新增记录”同步服务负责把记录推到各个下游系统。5.4 单机版到网络版再到云端如果客户有多个地磅或者老板要在办公室看数据网络版是必然趋势。网络版的架构升级其实没那么恐怖只要一开始数据库访问层封装得好换一个数据库连接字符串就搞定。更难的是业务权限控制不同角色操作员、管理员、老板看到的数据范围不一样。我建议至少分三级操作员只能做称重操作和查看自己的记录管理员可以改配置、补录数据老板只读报表。云端版是这两年越来越流行的需求一套系统管全国多个分厂的数据。这种项目技术栈就不再是单纯的WinForm了通常需要“WinForm做磅房客户端 Web后台做管理 App做移动端审批”的组合。但磅房端的核心逻辑依然离不开我们前面讲的这些玩意儿。所以把基础打牢云端扩展只是换了一层皮里面还是那个称重系统。6. 个人经验总结做了这么多年称重项目我最大的体会是这类系统难点不在代码而在对业务的敬畏心。你去现场看一次真实的过磅看操作员在磅房里忙着协调车辆、记录数据、处理司机纠纷你就知道自己写的每一行代码都关系到这家企业每天几十万甚至上百万的物资流转。串口多帧数据可靠性、重量稳定逻辑、防重复过磅这些细节看起来不起眼却实实在在影响系统的可用性。如果你正准备开发自己的称重系统我给三个落地建议第一代码里所有的阈值、端口、协议参数都做成可配置的不要写死因为你永远不知道下一个现场是什么仪表第二日志一定要打全尤其是串口原始数据和数据库写操作这是线上排查问题的救命稻草第三一定要在真机上测试虚拟串口模拟器只能帮你验证逻辑流程验证不了真实硬件的种种脾气。这套系统我前后做了很多个版本从最早的Access数据库单机版到现在SQL Server网络版、甚至对接云端大数据平台核心架构一直没怎么变过。你按这篇文章的思路搭出来的系统后续扩展空间会很充裕不会因为当初图省事而推倒重来。
返回列表