ARTICLE DETAIL

资讯详情

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

C#无人值守地磅称重系统设计:串口、状态机与防作弊实现

C#无人值守地磅称重系统设计:串口、状态机与防作弊实现 简介一套基于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/RS485SerialPort 或 Modbus 读寄存器车辆到位红外对射、地感开关量 → IO模块/PLC轮询IO或Modbus读线圈控制输出道闸、红绿灯、语音继电器/PLCModbus写线圈或串口指令如果仪表离磅房远常见做法是加一个串口服务器把RS485转成网络TCPC#这边用一个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 EventHandlerWeightReading 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一旦写操作冲突就会锁库。解决方法是初始化数据库时执行两条PRAGMAusing var conn new SqliteConnection(_connString); conn.Open(); var cmd conn.CreateCommand(); cmd.CommandText PRAGMA journal_modeWAL; PRAGMA busy_timeout3000;; 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地址、识别服务地址、道闸动作延时全部放到配置文件中不要写死在代码里交付前在秤台上模拟“车未停稳、压线、倒车、前车未走”四种异常流程确认每一条都会超时报警而不是抬杆。我吃过没有看门狗的亏凌晨三点客户打电话说系统卡住等到现场已经是两小时以后。后来每个项目都强制做心跳自愈从此再没被半夜叫醒过。希望这些经验能帮你少踩几个坑也希望能帮你在接手这套源码时一眼看到真正值得改的地方。本文还有配套的精品资源点击获取
返回列表