简介:一套基于C#技术的无人值守地磅称重系统设计源码,面向仓储物流、矿业、化工、港口等行业的软件开发与系统集成人员,用于实现称重流程自动化、数据自动采集与记录,降低人工干预和操作误差。压缩包共243个文件,大小约148MB,包含100个C#源代码文件、47个动态链接库、19个XAML界面文件,以及PDF说明文档、配置文件、报表文件和可执行程序等。其中,C#源文件承载业务逻辑与处理算法,XAML负责前端交互界面,配置文件集中管理运行参数,报表文件用于生成称重统计与汇总信息,CHM帮助手册和Sqlite数据库支持组件则方便查阅接口规范与数据存储方式。已有259人学习下载。通过源码目录与配套文档,可快速理清整套系统的分层结构与工作流程,掌握无人值守称重场景下的设备对接、数据采集、存储分析和异常处理思路;项目命名规范、模块划分清晰,适合作为二次开发或同类自动化系统设计的参考模板。
1. 基于C#技术的无人值守地磅称重系统设计源码:先看清这是流程控制项目,不是写个串口工具
凌晨三点,运货司机把车开上地磅,驾驶室里没人递卡,窗口也没有司磅员。车辆停稳,摄像头抓拍车牌,大屏显示毛重,道闸自动抬杆走车。这套系统背后如果只用C#读个串口再插个数据库,大概率第一辆车就会翻车:重量还没稳就保存、车没完全上磅就称重、前一车没走道闸又抬起来。基于C#技术的无人值守地磅称重系统设计源码,难点不在“读重量”,而是把称重仪表、车牌识别、道闸控制、防作弊信号和数据库流水串成一套可靠的状态机。下面按一线落地的顺序拆开:设备链路怎么搭、数据表怎么设计、串口和IO怎么处理、最常见的坑在哪。适合做工厂物流、废品回收、搅拌站值守系统的上位机开发者,也适合刚接手类似源码但看不清结构的人。
2. 系统架构与设备选型:把“车辆—仪表—上位机—道闸”链路搭对再写代码
很多人拿到这类源码,第一反应是找 Form、找 SerialPort、找 SQL 语句。C#上位机开发里最容易被低估的其实是现场物理链路:称重仪表在哪、信号走RS232还是RS485、道闸继电器由谁控制、红外对射接到哪里。链路没搭对,代码写再漂亮也是空中楼阁。我一般会先在现场画一张信号流向图,然后再决定程序怎么分层。
2.1 先分清谁说话算数:称重仪表是数据源,上位机是状态机
地磅传感器把重量传给称重仪表,仪表显示数字,同时通过串口把重量字符串发出来。C#程序要做的第一件事是解析这个字符串,拿到重量。但仪表不会告诉你车停稳没有、车是不是完全上了磅、有没有倒车,更不会告诉你车牌是多少。这些判断全部要上位机做。换句话说,仪表只是数据源,无人值守的决策逻辑在C#这一端。
所以源码设计上不建议把流程写成“定时器轮询一下重量,到了就保存”的线性逻辑。常见做法是把它做成状态机:空闲、检测到上磅、等待重量稳定、抓拍识别、抬杆、等待离开、关闭道闸、回到空闲。每个状态有进入条件和超时处理,整个系统才算得上“无人值守”。如果只是读串口,那叫数据采集器,不叫无人值守称重系统。
2.2 串口、IO与道闸:三种信号分别怎么接
现场设备信号大致分三类,接入方式不同,C#侧处理方式也不同。称重数据走RS232或RS485,由上位机通过串口读取;车辆是否到位用红外对射或地感线圈,输出的是开关量信号;道闸、红绿灯、语音播报是控制输出,一般通过继电器模块或PLC的DO点驱动。
| 信号类型 | 典型设备 | 接入方式 | C#侧处理 |
|---|---|---|---|
| 称重数据 | 称重仪表 | RS232/RS485 | SerialPort 或 Modbus 读寄存器 |
| 车辆到位 | 红外对射、地感 | 开关量 → IO模块/PLC | 轮询IO或Modbus读线圈 |
| 控制输出 | 道闸、红绿灯、语音 | 继电器/PLC | Modbus写线圈或串口指令 |
如果仪表离磅房远,常见做法是加一个串口服务器,把RS485转成网络TCP,C#这边用一个TcpClient就能读,避免USB转串口线过长导致干扰。需要注意仪表波特率、数据位、校验位必须和程序配置完全一致,大多数仪表默认是9600、8、N、1,但也有不少老仪表用19200或者带偶校验,现场先接串口助手看一眼再写代码。
2.3 软件分层:把UI、业务和设备驱动拆开
源码目录结构决定了后面维护成本。我一般会把项目拆成三个核心程序集:主程序放WinForm或WPF界面和流程调度,Device层放所有设备驱动,Data层放数据库访问。这样换仪表、换IO模块时不需要动业务代码。设备层先定义一个最小接口:
public interface IWeightScale : IDisposable { bool Open(string portName); void Close(); event EventHandler<WeightReading> WeightReceived; bool IsStable { get; } }这个接口只暴露三样东西:打开、关闭、一个读数事件。业务层不关心重量到底来自串口还是TCP,也不关心仪表是什么品牌。WeightReading对象里至少要有Weight和Stable两个属性,有些仪表协议本身带稳定标志,有些需要上位机自己判断,接口后面实现里各做各的。portName在这里可能是“COM3”,也可能是“192.168.1.50:502”,具体在实现类里区分。有了这层抽象,后续换设备才真正有后悔药吃。
3. 数据层设计:称重流水、车辆档案与防作弊标记怎么落库
设备链路通了,接下来要解决的是数据怎么存。无人值守系统一天几百车很常见,数据表不仅要记重量,还要把每一车的抓拍图片路径、异常状态、防作弊标记一起存下来,否则出了问题说不清。
3.1 别用Access了:SQLite 和 MySQL 怎么选
网上很多老代码还在用 C# 与 Access 搭配。Access做单机教学没问题,但无人值守是7×24小时运行,多个线程同时在写,Access的文件锁和损坏恢复机制非常脆弱,我见过现场跑了一个月之后.mdb直接打不开的。现在常见做法是:单机磅房用SQLite,带ERP对接或者多磅房集中管理用MySQL。
SQLite在C#里用Microsoft.Data.Sqlite,部署时不需要额外装服务,一个.db文件拷走就是整个数据库,这对现场维护非常友好。MySQL适合需要多个客户端同时查询、或者后台要做报表统计的场景。我个人的判断是:不要因为“以后可能要上ERP”就直接上MySQL,运维环境不够专业的话,SQLite反而是最不容易出事的。
3.2 核心表结构与字段设计
称重记录表是整个系统的心脏。以一次完整的双向称重为例:车辆上磅先称毛重,卸货后回磅称皮重,净重等于毛重减皮重。记录表要能把这两次串联起来,还要存图片和状态。下面是一个简化但实用的建表语句:
CREATE TABLE WeightRecord ( RecordId INTEGER PRIMARY KEY AUTOINCREMENT, RecordNo TEXT NOT NULL, PlateNo TEXT, FirstWeight REAL, SecondWeight REAL, NetWeight REAL, Direction INTEGER, InTime DATETIME, OutTime DATETIME, Status INTEGER DEFAULT 0, Image1 TEXT, Image2 TEXT, IsForbidden INTEGER DEFAULT 0, Remark TEXT );字段设计上有几个细节值得说明。FirstWeight和SecondWeight分别存两次称重,NetWeight由程序算好写进去,SQLite的REAL足够满足地磅系统的精度要求。Direction用来区分是“上磅称毛重”还是“回磅称皮重”,无人值守双向过磅全靠它判断当前是哪一流程。Status是流程状态:0表示未完成,1表示完成,2表示异常。IsForbidden就是防作弊标记,后续如果红外被遮挡、重量异常波动,这里要记1。实际项目里还应该加复合索引,最常用的是PlateNo和InTime,不然车次一多查询会明显变慢。
3.3 写入用ADO.NET还是EF Core?
很多初学者喜欢用EF Core,因为实体模型写起来舒服。但无人值守高频写入和状态流转的场景下,我一般更愿意用Dapper或者原生ADO.NET。原因不是EF Core不行,而是这里的数据模型相对固定,不需要复杂的导航属性,轻量ORM反而执行逻辑更直观。更重要的是事务控制:道闸抬起前必须保证称重记录已经写入成功,如果写库失败,流程绝不能继续。
using var conn = new SqliteConnection(_connString); conn.Open(); using var tx = conn.BeginTransaction(); conn.Execute( "INSERT INTO WeightRecord(RecordNo, PlateNo, FirstWeight, Direction, InTime, Status) VALUES(@RecordNo, @PlateNo, @Weight, @Direction, @Time, 0)", new { RecordNo = recordNo, PlateNo = plateNo, Weight = weight, Direction = 1, Time = DateTime.Now }, tx); tx.Commit();这段代码的逻辑是:在事务里插入一条完整的上磅记录,插入成功后事务提交,再决定是否抬杆。使用Dapper时,Execute方法的第三个参数传事务对象,这时参数化的对象不能漏掉任何字段。参数里的RecordNo最好用“日期+流水号”生成,比如20250617-0001,避免数据库自增Id对外暴露车次数。如果这一步失败,直接进入异常流程并语音报警,而不是继续往下走。
4. 核心功能实现:串口称重、车牌识别与道闸联动的C#代码骨架
数据表准备好之后,最核心的联调阶段就是串口、摄像头和道闸三者配合。很多源码能单点跑通,但一联动就出问题,原因在于串口事件、网络请求、数据库写入各自在不同的线程里跑,没有统一协调。
4.1 SerialPort读仪表:稳定读数不是ReadLine,而是缓存拼接
称重仪表的通信协议各家并不统一,但大多数是ASCII字符串,以固定帧头开始、回车结束。SerialPort有一个现成的ReadLine方法,但我不建议直接依赖它,因为仪表输出不稳定时容易断成半包,ReadLine会一直等或者读错行。更可靠的做法是DataReceived事件里把数据全部收进缓冲区,再自己按帧头和帧尾拆包。
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { int count = _serial.BytesToRead; byte[] buffer = new byte[count]; _serial.Read(buffer, 0, count); _recvBuffer.AddRange(buffer); string text = Encoding.ASCII.GetString(_recvBuffer.ToArray()); int start = text.IndexOf('='); // 某个仪表的帧头 int end = text.IndexOf('\r'); if (start >= 0 && end > start) { string weightString = text.Substring(start + 1, end - start - 1); _recvBuffer.Clear(); RaiseWeightReceived(weightString); } }这段代码的逻辑是先把串口收到的字节追加到内存列表,再统一转字符串找帧边界。每个仪表协议的帧头不一样,有的用=,有的用>,还有的用逗号分隔多个字段,所以实际项目里建议用一个Config配置帧头位置。DataReceived事件在工作线程里触发,不能直接操作UI控件,需要像示例那样把数据封装成事件抛出去,再由主窗体决定是否Invoke到界面线程。串口参数方面,常见波特率是9600,数据位8,停止位1,无校验,但必须和仪表面板设置一致,否则读出来全是乱码。
4.2 红外对射与道闸控制:Modbus IO模块怎么读写
无人值守现场很少直接把开关量接到电脑串口上,常见做法是接一个远程IO模块,通过Modbus TCP和上位机通信。C#里一般用现成的Modbus库,读写输入和输出继电器。道闸抬杆就是写一个线圈,车辆到位就是读一个输入点。
using var tcpClient = new TcpClient(ioIp, 502); var master = ModbusIpMaster.CreateIp(tcpClient); bool frontSensor = master.ReadInputs(1, 0, 1)[0]; // 读第1路输入,前红外 bool rearSensor = master.ReadInputs(1, 1, 1)[0]; // 读第2路输入,后红外 if (frontSensor && rearSensor) { master.WriteCoil(1, 0, true); // 写第0路继电器,抬杆 }参数说明:CreateIp的第一个参数是IO模块的IP,第二个是端口,默认502。ReadInputs入参分别是从站地址、起始地址和读取数量,返回布尔数组。WriteCoil的参数是从站地址、线圈地址和值,大部分模块地址从0开始,也有从1开始的,具体以模块手册为准。强烈建议把IO地址写到配置文件里,不要散落在代码各处,否则现场接错线后改程序比改配置慢得多。抬杆之后不能立刻认为车辆会走,还要持续监控车辆离开状态,等道闸完全关闭后才能进入下一次流程。
4.3 车牌识别:优先HTTP API而不是SDK
车牌识别有两种接入方式:一种是厂商提供的SDK,另一种是HTTP API。很多看起来功能强大的SDK在x64环境或者Windows Server上会莫名崩溃,内存越界之后连数据库都跟着遭殃。我现在倾向于把车牌识别独立部署成一台识别服务,C#程序通过HTTP上传图片拿结果。这样主程序和识别服务互不影响,识别服务自己挂了也不至于拖垮整个磅房系统。
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; using var content = new MultipartFormDataContent(); content.Add(new ByteArrayContent(File.ReadAllBytes(imagePath)), "image", "plate.jpg"); var response = await http.PostAsync("http://127.0.0.1:8080/plate", content); string json = await response.Content.ReadAsStringAsync(); using var doc = JsonDocument.Parse(json); string plate = doc.RootElement.GetProperty("plate").GetString();这段代码的逻辑是读取抓拍文件,以multipart形式POST给本地识别服务,再解析返回的JSON拿车牌号。识别时机很重要:不要在车辆刚上磅、还在晃动的时候抓拍,那样识别率很低。我一般会等重量稳定后再触发摄像头抓拍,并同时抓拍车头和车尾两张图。Timeout设成5秒,如果识别服务没响应,走人工介入流程,而不是让道闸一直等。
4.4 主流程状态机:防重复、防作弊的核心
把串口读到的重量、IO读到的车辆位置、HTTP拿到的车牌串起来,靠的是状态机。用C#枚举定义状态,用一维状态转换表控制流程,代码会比层层if else清晰得多。
enum WeighState { Idle, VehicleArrived, WeighStable, PlateCaptured, GateOpened, VehicleLeft }Idle时只判断两个IO输入。两个红外都触发,并且重量大于设定的最小车辆重量,进入VehicleArrived。VehicleArrived状态下连续N次读到的重量差值小于阈值,才切换WeighStable,此时保存第一次称重记录并抓拍车牌,进入PlateCaptured。PlateCaptured成功后打开道闸,进入GateOpened。GateOpened状态下等待红外信号恢复无车,确认道闸关闭到位后回到Idle。这中间任何一个状态超时,都要自动回滚或进入异常报警,不能卡死。这个状态机是所有无人值守系统的主心骨,也是源码里最值得反复读的部分。
5. 无人值守最容易翻车的5个坑:称重漂移、重复过磅和抬杆误判
系统跑起来不难,难的是连续稳定运行不掉链子。这里整理5个我踩过或帮别人排过的真问题,按“现象→原因→解决”的方式列出来,碰到类似情况可以直接对照检查。
5.1 重量已经稳了,点保存却差几十公斤
现象是屏幕上重量数字跳动已经很小,但保存下来的重量和仪表最终显示值差几十公斤。原因往往不是仪表坏了,而是程序只在重量进入阈值后立刻保存,没等串口缓冲区里的“残留数据”全部处理完。另一部分原因是车辆停在秤台上时,大车发动机的振动传导会让重量在小范围内连续抖动。解决方法是连续读5次以上,每次间隔200毫秒,且任意两次差值不超过10公斤,才认为真正稳定。这里10公斤是经验值,钢材车和煤炭车的秤台振动特性不同,可以在系统设置里做成可调参数。
if (Math.Abs(current - last) <= stabilityTolerance) { stableCount++; if (stableCount >= 5) { RaiseWeightStable(current); stableCount = 0; } } else { stableCount = 0; }这段代码的逻辑很直白:每次读数后和上一次比,差值小于容差就累加稳定次数,否则清零。5次连续稳定才触发事件。这个思路在无人值守里比仪表自带的稳定标志更可靠,因为仪表稳定标志只代表传感器信号稳定,不代表车辆一定停稳了。
5.2 车没有完全上磅就开始称重
前面装了两个红外对射,结果前轮刚压上秤台红外触发,系统就判断“车辆到位”,最后存了一个不完整的重量。原因是对射安装位置太靠前,或者只装了一组红外,无法判断整个车是否都在磅上。常见做法是分前后两组地感或者对射,前红外和秤台前边缘平齐,后红外装在秤台后边缘外侧,两个信号同时触发才说明车辆基本到位。还需要辅助判断实际重量是否大于预设的最小阈值,否则空车轻压也可能触发误判。
5.3 抬杆后前车还没走,后车又跟进来
这是无人值守地磅最容易引发纠纷的问题。现象是道闸抬起后,前车还在秤台上慢慢移动,后车已经从入口跟进来,两个红外同时处于遮挡状态,系统分不清是几辆车。根本原因是缺少“车辆完全离开”的判断。解决方法是道闸下方再装一组地感或微波雷达,用来检测道闸区域内是否还有车。状态机里必须把“道闸关闭到位”作为下一次称重的必要条件,不能只看重量归零。
5.4 SQLite 偶尔报 database is locked
单机系统用SQLite遇到这个错,多半不是并发量大,而是没有开启WAL模式。无人值守程序里,一个线程写称重记录,另一个线程写日志,还有摄像头线程可能也在写图片路径,如果没有WAL,一旦写操作冲突就会锁库。解决方法是初始化数据库时执行两条PRAGMA:
using var conn = new SqliteConnection(_connString); conn.Open(); var cmd = conn.CreateCommand(); cmd.CommandText = "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=3000;"; cmd.ExecuteNonQuery();WAL模式让读写不阻塞,busy_timeout让写操作在遇到锁时等待而不是立刻报错。这两条命令要在程序启动时跑一次,后面所有连接都复用同一个连接池。很多人只写代码不初始化,导致现场过几周就开始随机报错。
5.5 程序跑几天后串口彻底读不到数
串口从正常到彻底无响应,最常见原因是USB转串口线的电源管理把端口休眠了,或者SerialPort对象被垃圾回收后没有释放干净。解决方法是到设备管理器里把USB根集线器的“允许计算机关闭此设备以节约电源”取消勾选,同时在程序里做一个心跳:如果超过30秒没有收到任何串口数据,主动Dispose掉当前SerialPort并重新Open。重新打开前要等1到2秒,否则可能提示端口已被占用。
6. 把无人值守从“能跑”改成“不守着”的3个进阶技巧
代码能正常过磅只是及格线,真正决定系统口碑的是这三点:假死能不能自愈、数据能不能防改、异常流程能不能快速定位。
6.1 用状态心跳和自动重启机制扛住上位机假死
无人值守现场最大的敌人是界面线程卡死。程序一旦卡住,道闸不动、语音不响、数据库不写,但进程还活着。我一般的做法是开一个独立看门狗进程,每5分钟请求主程序的HTTP健康检查端口,连续两次没响应就杀掉主程序并重新拉起。主程序侧只要开一个极简单的TcpListener,收到心跳请求就返回ok。
6.2 给称重记录加完整性校验字段
人工直接改数据库最隐蔽也最难查。应对办法不是加密,而是给每条记录加一个完整性校验码。保存记录时用记录Id、净重、时间拼接后计算SHA256,存到独立字段。定期扫描发现有校验对不上的记录,立刻标记异常。这样即使有人动过数据库,系统也能被察觉。
string checksum = Convert.ToHexString( SHA256.HashData(Encoding.UTF8.GetBytes($"{recordId}|{netWeight}|{inTime:O}")));这段代码我把记录主键、净重和入磅时间绑在一起,任何一项被改动,校验码就对不上。不用做高深的安全处理,目的就是让问题暴露出来,而不是黑匣子一样说不清。
6.3 两个“后悔药”级别的小习惯
所有串口参数、IO地址、识别服务地址、道闸动作延时,全部放到配置文件中,不要写死在代码里;交付前在秤台上模拟“车未停稳、压线、倒车、前车未走”四种异常流程,确认每一条都会超时报警而不是抬杆。我吃过没有看门狗的亏,凌晨三点客户打电话说系统卡住,等到现场已经是两小时以后。后来每个项目都强制做心跳自愈,从此再没被半夜叫醒过。希望这些经验能帮你少踩几个坑,也希望能帮你在接手这套源码时,一眼看到真正值得改的地方。
本文还有配套的精品资源,点击获取