C#无人值守地磅称重系统实战:串口通信与防作弊

发布时间:2026/10/8 16:23:54
C#无人值守地磅称重系统实战:串口通信与防作弊
简介基于C#技术的无人值守地磅称重系统设计源码是一套完整的自动化称重项目面向工业、物流领域从事称重系统开发或设备集成的工程师解决车辆称重环节人工干预多、数据记录繁琐等问题。压缩包共243个文件大小148.16MB核心为100个C#源码文件覆盖业务逻辑与处理算法47个DLL提供运行时支持19个XAML构建前端交互界面另有PDF说明文档、配置文件、报表文件、项目文件、可执行文件等便于理解结构与二次开发。系统可自动采集、记录、存储称重数据支持对接条码扫描器、打印机等外设并附带设备网络SDK手册、CHM帮助和PDF操作维护文档能帮助使用者快速掌握部署与排错。目前已有259人学习下载对落地无人值守称重项目或研究C#桌面应用架构的开发者具有直接参考价值。1. 无人值守地磅称重系统是什么为什么 C# 是工控现场最务实的选择基于C#技术的无人值守地磅称重系统看起来只是一套过磅软件真正交付的是一个把人从磅房里解放出来的控制工程。常见落点在粮食收储、钢厂原料通道、矿区出场计量、垃圾中转站。这类现场的诉求很一致车辆上磅后能自动稳定、自动抓拍、自动锁磅数据直接落库事后能追溯作弊损耗。选C#做主语言是因为它能把串口、摄像头、数据库、WinForms 和打印塞进一个进程里一个人也能交付整套系统。适合做上位机的开发也适合想从“会写界面”往“能控现场”再走一步的人。2. 系统方案与通信选型串口、Modbus 冗余和数据库兜底2.1 把系统拆成三层没人会一上来就写代码无人值守地磅的核心不是“读重量”而是“确认这辆车可以放心地放行”。一个可维护的地磅上位机我会先拆成三层现场层秤台传感器、称重仪表仪表负责 AD 转换和标定、道闸、红绿灯、光幕、地感线圈、车牌识别摄像头。控制层C# 上位机里的串口通信服务、状态机、规则引擎稳定判断、防作弊判断、超时判断。数据层记录毛重、皮重、净重、车牌、时间、抓拍照片路径、操作员信息。这三层对应到项目里就是三个工程或者三个命名空间不要混合。我见过不少翻车源码是把串口事件里直接写数据库结果串口一抖数据库就跟着抖这是最典型的架构错误。控制层的职责是“判断”数据层的职责是“记账”两者之间用内存队列隔开尽量不在串口中断回调里做耗时操作。现场层的硬件顺序也有讲究。以最常见的双向过磅为例来车方向有红绿灯接近时触发地感线圈道闸自动抬起车缓慢上磅前后光幕对射确认车头车尾都没越界然后系统开始判断重量稳定。稳定后锁存当前重量抓拍照片语音提示司机下车或直接放行前方道闸抬起。整个过程如果全部依赖人工盯监控架子就白搭了。2.2 通信口选型串口直读、RS485 远传和 Modbus 冗余仪表输出的通行方式无外乎三种RS232 串口直连、RS485 总线远传、以太网或 Modbus 接入 PLC。RS232 的好处是零成本、接线简单几乎所有称重仪表都留了串口缺点就是距离限制在 15 米左右而且一根线只能接一台仪表。磅房离秤台通常不超过 10 米一台工控机控制一二台磅RS232 完全够用。RS485 用在仪表离控制室比较远的场景比如配料站把秤台放在几十米外。C# 端操作 RS485 其实还是用 SerialPort只是把波特率降到 9600并且要按设备的地址码组帧。要注意的是 RS485 是半双工一发一收不能像 RS232 那样有数据就来事件很多协议要“发指令—等应答—再发下一帧”超时和重试都得自己做。真正让系统不单点失效的是加一路 Modbus。常见做法是仪表串口仍然是主通道工控机直接解析仪表主动上报的 ASCII 帧同时如果现场有 PLC或者现场用了 Modbus 继电器模块来控制道闸和红绿灯那就用 Modbus RTU 作为辅助通道专门读仪表重量和写继电器线圈。这样即使仪表串口线被老鼠咬断PLC 那一路还能把重量报上来系统进入降级模式继续过磅只是不联动抓拍。C# 里初始化串口是最先要写对的一段我一般这么写SerialPort sp new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One); sp.ReadTimeout 1000; sp.WriteTimeout 1000; sp.ReceivedBytesThreshold 1; sp.DataReceived OnDataReceived; sp.Open();参数说明波特率 9600 是绝大多数称重仪表的默认值8 数据位、无校验、1 停止位是 ASCII 仪表帧的标准配置。ReadTimeout 和 WriteTimeout 设成 1000ms是为了避免某个异常帧把线程卡死在同步读写里。ReceivedBytesThreshold 设为 1表示只要缓冲区里进来 1 个字节就触发 DataReceived 事件接收的完整性靠我们自己解析帧头帧尾而不是依赖驱动帮你拼包。如果接的是 Modbus 继电器模块控制道闸就是往串口写一条标准 Modbus RTU 帧。比如把 1 号继电器的线圈置位为 ONHex 报文是01 05 00 00 FF 00 8C 3AC# 这边只需要把字节数组写进同一个 SerialPortbyte[] relayOn new byte[] { 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x8C, 0x3A }; sp.Write(relayOn, 0, relayOn.Length);这段帧的组成是01 是设备地址05 是写单线圈功能码00 00 是线圈地址FF 00 表示置位8C 3A 是 CRC 校验。CRC 是现场最容易踩坑的地方很多模块因为 CRC 算错直接不响应。我建议把 CRC16 的计算封装成一个公共方法而不是手拼 Hex 字符串否则换一台设备就要重新对一遍手册。2.3 数据库选型SQLite 单机、SQL Server 多机、Access 只能活在老源码里地磅数据量不大一天几百条到两三千条但对“断电不丢数据、多客户端不锁库”这两个特性的要求非常高。给一个表参考方案适合场景主要问题SQLiteWAL 模式单机无人值守数据量小多客户端并发写时要自己加锁SQL Server Express2~5 台磅房客户端同时看报表需要单独装服务部署重一点Access老项目常见历史遗留源码改造多个进程同时写容易直接锁库MySQL / PostgreSQL有独立服务器做集团汇总地磅现场没必要除非上报平台如果只是一台工控机管两个磅我首选 SQLite不需要装服务断电恢复比 Access 强得多。连接串里加一句Journal ModeWAL;读和写就能并发不会出现那种“数据库正忙请稍候重试”的弹窗。等到要上多台客户端比如磅房放一台厂长办公室要实时看再换成 SQL Server Express连接串只是换掉 Data Source业务代码基本不动。这其实也是 C# 上位机面试里常被追问的问题为什么不用 Access答案不是 Access 不能跑而是无人值守场景断电概率太高Access 的 Jet 引擎一旦在写操作中断开修复成本远远大于换库。数据库的落库时机也别搞得太实时。我会在内存里开一个 ConcurrentQueue 缓存待写入记录每 3~5 秒批量插一批。这样串口抖动、界面刷新、打印卡顿都不会直接影响数据库写操作。后面第四章给完整代码。3. 读秤与稳定判断把“重量稳定”写成可复现的代码3.1 串口数据解析从一帧 ASCII 里把重量抠出来仪表协议各家略有差异但共性非常强通常是一行 ASCII 字符串包含正负号、重量数值、单位和一个结束符回车换行或 CR。很多国产仪表默认连续输出模式每秒输出 5~20 帧帧里除了重量还混合了站号、毛重/皮重标志。C# 端最稳妥的做法是维护一个 StringBuilder 缓冲区遇到结束符再按帧处理而不是收到一个字节就立刻解析。很多新手直接把sp.ReadExisting()的结果拿去正则匹配半包的时候就会把1234误当重量。先把串口事件做干净private StringBuilder _frameBuffer new StringBuilder(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { string chunk sp.ReadExisting(); _frameBuffer.Append(chunk); string text _frameBuffer.ToString(); int endIdx text.IndexOf(\n); if (endIdx 0) return; string line text.Substring(0, endIdx).Trim(); _frameBuffer.Remove(0, endIdx 1); double? weight TryParseWeight(line); if (weight.HasValue) { OnWeightArrived(weight.Value, DateTime.Now); } } private double? TryParseWeight(string line) { Match m Regex.Match(line, [-]?\d{1,7}(\.\d{1,3})?); if (!m.Success) return null; double raw double.Parse(m.Value, CultureInfo.InvariantCulture); // 常见仪表帧里负数表示低于零点撞击等瞬时值会导致重量突变 return raw -5000 ? null : raw; }这段代码的逻辑说明先用 StringBuilder 攒帧只遇到\n才认定一行完整然后从行里取出第一个数字字段用正则限定整数部分最多 7 位、小数最多 3 位这是 50 吨到 100 吨地磅的通用范围。负数低于 -5000 我直接丢弃因为那往往是传感器被撞击产生的瞬间冲击不是真实重量。正则不是万能不同厂家的帧头帧尾不一样但只要仪表支持“稳定标志位”尽量把稳定标志位也解析出来后面判断稳定就能省一半事。参数说明正则中的\d{1,7}要按磅的量程改。100 吨地磅最大显示 999997 位够用如果做的是汽车衡加铁路衡双量程建议直接读仪表 HLimit 参数。3.2 稳定判断三段式定时窗口、波动阈值与防抖仪表内部有滤波但那个滤波挡不住车辆在秤台上的蠕动。人站在磅上抖一下重量都能跳几十公斤。无人值守最怕的就是抖完之后锁了一个错误重量所以稳定判断必须自己做。我的实现是滑动窗口记录最近 3 秒内到达的所有重量值如果最大值和最小值的差不超过 20kg并且整段时间内没有任何一帧超量程就判定稳定。判别频率可以依赖仪表连续输出的节奏每秒至少 5 帧3 秒至少 15 个样本点。private Queuedouble _window new Queuedouble(); private DateTime _windowStart DateTime.Now; private const double STABLE_WINDOW_SECONDS 3.0; private const double STABLE_WAVE_KG 20.0; private const double MAX_OVERLOAD 60000.0; private bool IsStable(double weight) { DateTime now DateTime.Now; if ((now - _windowStart).TotalSeconds STABLE_WINDOW_SECONDS) { _window.Clear(); _windowStart now; } _window.Enqueue(weight); while ((now - _windowStart).TotalSeconds STABLE_WINDOW_SECONDS) { _window.Dequeue(); } if (_window.Count 10) return false; // 样本不足 if (weight MAX_OVERLOAD) return false; // 超量程直接否决 double min _window.Min(); double max _window.Max(); return (max - min) STABLE_WAVE_KG; }这段代码有几个值得调的点窗口 3 秒和波动阈值 20kg 是经验默认值不是万能值。老式模拟传感器磅电阻应变片一般 20kg 可以数字式传感器磅可以收到 5~10kg如果现场有大风或者秤台周围有重型车辆经过要把阈值放宽到 30~50kg否则永远稳定不了。样本数 10 是一个下界保证窗口里确实有足够数据排除仪表半包时只有一两帧的情况。如果现场发现“明明不动却一直不稳定”优先检查两件事是不是仪表输出里混入重量为零的空帧是不是半包解析把小数位丢了。我见过一个现场仪表输出12.345kg但协议里固定 5 位整数正则匹配把小数点后三位吃掉导致重量一直在 12 和 12345 之间跳最后把正则改成纯按分隔符解析才解决。这属于改数据比改代码快的问题把帧头协议打印到日志里看一遍就明白。3.3 过磅状态机车辆上磅、锁存与抬杆的边界稳定判断只是条件之一真正决定“能不能锁磅、能不能抬杆”的是一个状态机。我会把每个磅定义成这样的状态enum WeighState { Idle, // 磅上无车 CarEntering, // 地感触发车辆正在上磅 Weighing, // 光幕遮挡开始等待稳定 Stable, // 重量稳定可以锁存 Locked, // 已保存本次毛重或皮重 WaitingRelease // 等待车辆下磅 }状态转移不放在界面按钮里而是放进一个独立的判断方法由定时器每 200ms 触发一次。核心逻辑是只有光幕导通说明没有异物卡磅且车辆停正、重量稳定、车牌已经拿到三个条件同时满足才允许从 Weighing 转到 Stable然后立刻锁存重量并抓拍再进入 WaitingRelease。WaitingRelease 里要等重量掉到接近零点并且地感复位置零说明车辆已经下磅整个状态机才能回 Idle否则下一辆车上来会带着上一辆的残余重量。这里是整个系统最容易出逻辑漏洞的地方。我见过一个项目锁存之后不问“车是否下磅”就直接允许下一辆车开始结果前面那台车还没走完后面车头已经压上来毛重和皮重记录全错财务对账差出几十吨。状态机里用状态枚举做保鲜期也不行因为重量反馈是有延时的必须在 WaitingRelease 里重设一个超时计时器超过 30 秒仍未回零就产生“异常锁磅”告警而不是继续等。4. 无人值守数据闭环车牌识别、联动时序与打印兜底4.1 车牌识别接入SDK 回调与 HTTP 轮询二选一车牌识别摄像头一般有两种接入方式。第一种是厂方 SDK 提供回调事件事件里直接给车牌、置信度、抓拍图片第二种是摄像头自己做了 HTTP API上位机定期拉 JSON。选型原则SDK 回调响应快但 SDK 版本和运行库容易跟工控机冲突部署时最折腾HTTP 轮询多 200~500ms 延迟但跨摄像头品牌通用排障也简单。我多数项目会优先 HTTP 轮询把“取车牌”变成一个普通网络请求单独放一个线程。轮询代码大致长这样private async Taskstring FetchPlateAsync(string cameraApiUrl) { using HttpClient client new HttpClient(); client.Timeout TimeSpan.FromSeconds(3); string json await client.GetStringAsync(cameraApiUrl); using JsonDocument doc JsonDocument.Parse(json); JsonElement result doc.RootElement.GetProperty(data).GetProperty(plate); string plate result.GetString()?.Trim(); if (string.IsNullOrEmpty(plate)) return null; // 车牌字符串截取这里要按车牌规则清理省份简称 字母 数字 if (plate.Length 7) { return plate.Substring(0, 7).ToUpperInvariant(); } return null; }这段代码说明GetStringAsync 拿到的是摄像头 API 返回的 JSON字段路径data.plate是常见约定换品牌要按手册改。拿到车牌后我做的第一件事是 Trim 和截取前 7 位并转大写目的是把新能源牌照的 8 位车牌的汉字、字母、数字统一成一串避免同一辆车因为字符大小写不同产生两条记录。如果现场有“蓝牌后面带空格、绿牌超过 7 位”的情况建议在截取前先过滤掉空格和特殊字符。HTTP 轮询有一个陷阱摄像头 API 偶尔会返回空车牌比如晚上光线不好如果空车牌伴随重量稳定系统不能锁磅但也不能让车一直堵在磅上。我的做法是“车牌最多重试 3 次3 次都失败就放行并标记该条记录为无牌同时把抓拍图片序号写到数据库”让司磅员事后通过照片补录。完全卡死不放行往往会导致司机倒车冲磅比漏一次车牌更麻烦。4.2 防作弊联动光幕校验、道闸时序和语音播报无人值守地磅和普通地磅的最大区别是它必须防两类作弊车辆不完全上磅车头或车尾还在磅外就锁磅以及车辆冲磅高速驶上秤台导致瞬时冲击力被误判为毛重。光幕是防第一类的主力前后各一对对射光电只要任何一束光被遮挡说明秤台上方空间有车体这个状态要持续到重量稳定。这里注意地感线圈回路也可能有延迟光幕信号优先。道闸时序我一般这样定车辆触发入口地感入口道闸抬起前光幕遮挡说明车头已进入秤台区域重量稳定后锁磅完成出口道闸抬起。如果出口道闸抬早了后车会接着往前顶前车记录还没写库就被干扰。这里 C# 控制继电器就是往 Modbus 模块写线圈和 2.2 节是同一套接口只是把relayOn封装成SetRelay(int moduleAddr, int relayNo, bool enable)CRC 算好之后写串口。语音播报一般用外接的语音播报模块串口发一句 GBK 编码的文本就能读出来。常见做法是在锁存成功时播报“车辆称重完成请下磅”在重量不稳定时播报“车辆请停稳重量波动中”。语音不是花架子它能在司机看不到红绿灯位置的时候用声音把状态给司机减少大量按喇叭催人的异常事件。4.3 数据落库与打印兜底C# 与 Access 的边界以及打印失败不阻塞业务上一节说了 SQLite 是首选这里给具体的落库代码。核心是两条开 WAL 模式批量插入避免每一条记录都开一个事务。private ConcurrentQueueWeighRecord _pending new ConcurrentQueueWeighRecord(); private void BatchFlush(object state) { if (_pending.IsEmpty) return; using var conn new SqliteConnection(Data Sourceweigh.db); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText PRAGMA journal_modeWAL;; cmd.ExecuteNonQuery(); using var tx conn.BeginTransaction(); cmd.Transaction tx; cmd.CommandText INSERT INTO WeighRecords (PlateNo, GrossWeight, TareWeight, WeightTime, ImagePath) VALUES ($plate, $gross, $tare, $time, $image);; while (_pending.TryDequeue(out WeighRecord r)) { cmd.Parameters.Clear(); cmd.Parameters.AddWithValue($plate, r.PlateNo ?? ); cmd.Parameters.AddWithValue($gross, r.GrossWeight); cmd.Parameters.AddWithValue($tare, r.TareWeight); cmd.Parameters.AddWithValue($time, r.WeightTime.ToString(yyyy-MM-dd HH:mm:ss)); cmd.Parameters.AddWithValue($image, r.ImagePath ?? ); cmd.ExecuteNonQuery(); } tx.Commit(); }这里 BatchFlush 由Timer(3000)定时触发3 秒刷一批。加上 WAL 之后即使中途断电SQLite 也有能力从日志里恢复未完成的事务不容易像 Access 那样直接出现“不是有效的表”这类损坏。这是 C# 与 Access 组合最大的一道坎Access 在无人值守现场一断电经常整个文件被 Jet 引擎锁坏恢复数据要花半天。现在这个方案里内存队列是断电丢数据的最后一层代码里还要把待写记录同步写一份到本地文本追加日志作为后悔药。打印小票也要做兜底。热敏打印机走的是 Windows 打印驱动打印慢、缺纸、驱动崩溃都很常见。我会在过磅完成后先落库再把打印任务丢进队列由打印线程逐个消费打印失败只弹日志绝不让打印异常打断正常过磅流程。如果现场对票据有硬性要求打印队列要持久化到文件重启后能继续打印补打。5. 现场避坑重量跳动、不回零、断电翻车这些玄学问题5.1 重量数据跳动不是软件 bug 而是电缆问题现象仪表连续输出但界面上的重量一直在 20kg~80kg 之间无规律跳车辆停稳也跳。原因十有八九是传感器信号电缆太长、屏蔽层没有单端接地、或者电缆和动力线走在一起。其次是仪表滤波参数太浅。解决先看仪表端重量是否稳定仪表稳定但上位机不稳查串口线把仪表直接插到工控机 COM 口而不是经过延长线仪表也不稳把传感器电缆换成屏蔽双绞线屏蔽层在仪表端单点接地再在仪表里把滤波深度提高一档。这一个动作能解决现场 70% 跳动问题。5.2 实时重量不回零别急着写清零代码现象磅上没车重量却显示 200kg、500kg 或越来越大。原因最常见是雨后秤台基础沉降传感器受力不均还有可能是秤台四周的限位螺栓顶死秤体或者有异物卡在秤台边缘。直接把表清零下次过磅误差更大。解决先机械排查清扫秤台四周、松限位螺栓、检查基础确认机械没问题后在仪表里做标定而不是上位机清零。上位机里的“清零”功能要加权限和条件重量必须在 ±50kg 以内、且车辆已离开秤台满足这三个条件才允许发清零指令。没有条件的清零键等于给作弊开了一扇门。5.3 断电翻车内存缓存最坑SQLite WAL 能救一半但救不了全部现象突然停电重启后最近的十几条称重记录消失偶尔还出现“SQLite database disk image is malformed”。原因我把记录放在 ConcurrentQueue每 3 秒才批量落库断电时队列里没来得及写的数据全丢了而如果机器是直接拔电SQLite 旧版本或者没有 WAL 模式的库文件可能损坏。解决两件事一起做——连接串里开 WAL并把未落库队列同时追加到本地 CSV 备份文件每写一条就追加一行成本极低。重启后先加载 CSV 恢复队列再开启正常批量任务。这样“断电丢最后一批数据”的问题才算真正兜住而不是赌 UPS 一定撑得住。5.4 串口假死与乱码黑匣子式问题要看日志现象系统运行几天后串口不再触发 DataReceived或者收到的全是乱码杀进程重启后正常。原因很多仪表串口在线缆松动时会发出半包长时间半包会让驱动缓冲区残留垃圾数据另一个常见原因是 USB 转串口线质量差驱动休眠后不唤醒。解决给串口加一个每分钟的看门狗检查sp.IsOpen且DateTime.Now - lastFrameTime不超过 60 秒超时就 Close 再重新 Open。乱码问题优先换带 FTDI 芯片的 USB 转串口线并且把sp.ReadExisting()改成按字节读遇到无法解析的字节直接丢弃不要让垃圾数据进入 StringBuilder 缓冲区。这里的血泪经验是所有串口原始数据都要写进日志文件出了问题不是靠猜而是直接翻日志看帧格式。5.5 道闸放行太早车辆还没下磅就抬杆现象后车怼到前车尾部或毛重记录被后车重量覆盖。原因锁磅后直接抬杆没有等待车辆完全离开秤台也没有判断“当前磅上重量是否回零”。解决把 3.3 节的 WaitingRelease 状态做实锁磅后至少等待重量小于 200kg、地感复位再抬出口道闸。如果超时 30 秒还没下磅系统要主动告警到值班室而不是默默等待。这一个判断听着简单关系到的却是整个对账体系的准确度。参数建议值现场调节依据仪表滤波APW 18~20Hz数字式传感器可以更高稳定窗口3s大风或干扰大时放宽到 5s波动阈值20kg老式模拟磅放宽到 30~50kg串口看门狗60s 检查一次信号不好缩到 30s落库批量间隔3s数据量大也可以保持 3s6. 进阶实战看门狗、双机数据同步与远程排障6.1 给串口加一个自杀式看门狗长期运行的工控机最怕的不是蓝屏而是串口静默。前面避坑章节提到看门狗这里给出最小实现private DateTime _lastFrameTime DateTime.Now; private void WatchdogTick(object state) { if ((DateTime.Now - _lastFrameTime).TotalSeconds 60) { try { sp.Close(); } catch { } Thread.Sleep(200); try { sp.Open(); } catch { } } }配合一个Timer(60000)定时触发。Close 之后强制 Open如果 Open 失败说明 COM 口被其他进程占用或驱动掉了这时除了弹窗告警我会考虑让程序自动重启。这里注意 Close 前要把缓冲区清掉否则重启后第一帧是残包会被解析成错误重量。6.2 双机不必做热备做增量同步更实在很多需求方开口就要双机热备但地磅现场两台机器同时工作的场景极少真正常见的是“磅房一台工控机、办公室一台查询机”。不必上故障转移集群做增量同步就够了工控机每落库一条记录就把它同步写到一个共享目录的增量 JSON 文件查询机用定时任务扫描并导入本地库。同步失败不影响过磅因为源数据始终在工控机本地事后补拉即可。我在早期项目里直接把两台机器接到同一个数据库文件结果经常出现锁库。后来改成“工控机负责写、查询机只读副本”再也没出过问题。这个教训我一直记着地磅系统的核心诉求是“过磅不能停”而不是“两端永远一致”。6.3 用日志说话远程排障的最后一招无人值守现场人跑到磅房看故障的成本很高。我会在工控机上保留三类日志串口原始帧日志、业务状态变化日志、数据库写入日志按天滚动保留 30 天。远程排查时先看状态变化日志再对照串口原始帧多数问题十分钟内能定位。这也是 C# 上位机现场调试最值钱的习惯代码写得再难看日志写得好后期维护成本能降一半。这几年我最得意的一个改动就是给状态机每一步都打了时间戳日志。后来现场说“磅乱跳”远程翻日志发现是 WaitingRelease 超时被反复触发源头是地感线圈装得太靠近出口道闸。没这行日志那天就得跑两百公里。做无人值守系统先让系统自己会说话再谈无人。希望帮到你。本文还有配套的精品资源点击获取