
简介这是一套基于C#语言实现的无人值守地磅称重系统设计源码面向需要构建自动化称重管理方案的开发人员与行业运维者用于解决传统人工过磅流程中效率低下、记录易错、监管滞后等痛点可作为可直接参考的完整工程范例。压缩包总计一百三十七个文件大小仅二点四四兆包含九十一个C#源代码文件、二十一个XAML界面文件与八个配置文件并附有解决方案、资源、设置等辅助文件其中C#文件承载核心业务逻辑与设备通信XAML文件用于绘制操作界面配置文件便于按需调整参数整体目录结构清晰便于定位功能模块。当前已有四百一十四人学习下载适合具备一定C#基础、希望深入桌面应用开发或工业自动化领域的读者。通过研读这套源码可以掌握无人值守称重的流程调度、界面布局设计、设备对接方式、配置管理机制等关键实现并能在现有基础上快速二次开发接入真实地磅设备并部署上线。1. 无人值守地磅称重系统不只是把仪表重量读进电脑每天早高峰的物流园门口车辆排成长队司磅员坐在小屋里重复着“刷卡、对车牌、看重量、按键打印”的机械动作一场夜班下来错磅、人情磅、作弊磅时有发生。所谓基于C#开发的无人值守地磅称重系统本质就是把司磅员那套工作流搬到软件里车牌识别自动绑定车辆红外对射判断车是否完全上磅串口连续读取仪表重量并做稳定判定道闸联动放行重量、时间、抓拍图片全部落库。它解决的不是“称重”本身而是称重环节里的防作弊、效率和数据可信度。适合正在做工厂物流信息化、准备对接ERP/MES的开发者或者想用C#快速搭一套工业上位机原型的人。这套系统真正难的部分不在界面而在异常流程处理和防作弊逻辑整篇都会围绕这两点展开。2. 从硬件拓扑到C#工程结构无人值守地磅的设计骨架2.1 业务闭环与硬件拓扑谁在跟软件打交道无人值守地磅的系统边界不在电脑上而在设备层。一次完整过磅涉及的地磅仪表、红外对射栅栏、车牌识别摄像头、道闸控制器、LED引导屏、红绿灯每一类设备都通过不同接口跟C#程序通信。常见做法是串口接仪表和道闸RS232/RS485网络接口接摄像头和LED屏红外对射信号走开关量采集卡或者直接进PLC的DI点再由PLC通过Modbus把状态交给上位机。我一般会先画一张设备清单把每个设备的通信方式、数据格式、控制方向列成表再动手写代码。比如仪表是“主动连续上报”还是“请求应答”道闸是“脉冲抬杆”还是“电平保持”红外是“常闭触点串进道闸回路”还是“只做软件判断”这些决定了C#代码里是写一个串口监听线程还是一个定时轮询器。很多项目翻车都翻在这里硬件厂家说“支持二次开发”但连不上、协议对不上、信号电平不匹配的问题全在工程阶段爆发。2.2 C#技术栈选型为什么用WinForm而不是Web无人值守地磅的上位机程序最常见的落地形态是WinForm .NET Framework或 .NET Core 的 Windows Forms少数用WPF。选择WinForm不是因为它新而是因为工业现场对部署和维护的要求很朴素一台工控机、一块触屏、开机自启、稳定跑半年不用管。Web系统当然能做但串口访问、设备驱动、开机自启、异常断线重连这些能力WinForm天然集成少一层浏览器和网络服务的中间环节故障面更小。如果预估业务复杂度高、界面需要频繁重绘WPF的绑定机制写起来更顺手。但我个人在多数地磅项目里还是倾向WinForm原因很实际现场维护的同事对WinForm的排错方式更熟悉C#里SerialPort、BackgroundWorker、Timer这些组件在WinForm里就是拖拽即用学习成本低。这个取舍不一定最优但在地磅这类“稳定压倒一切”的场景里非常实用。2.3 源码的模块划分别把串口逻辑塞进按钮事件“设计源码”的关键在于代码结构能不能支撑现场长期迭代。最怕看到的是把所有逻辑写在Form1.cs里串口收到数据直接操作UI控件现场加一台摄像头就得改主窗体。我常用的模块划分是四层设备层DeviceLayer封装串口、Modbus、HTTP相机等通信细节对外只暴露读写接口和事件。业务层BusinessLayer实现称重状态机、防作弊规则、车辆绑定、数据校验。数据层DataLayer负责数据库读写、事务控制、离线缓存。表现层UI只做界面展示和用户操作入口不写任何业务逻辑。每一层内部用接口对接至少做到“换一个牌子的仪表只改设备层的一个类”。这个设计不是说要多优雅而是地磅系统的硬件更换频率远高于软件把变化隔离在单一模块里能省掉后面大量的重构工作。3. 串口、车牌与状态机把称重流程拆成可维护的代码3.1 串口采集地磅仪表数据协议解析与重试机制地磅仪表最常见的通信方式是RS232串口数据格式一般是ASCII码波特率9600或192008位数据位、1位停止位、无校验。仪表有两种上报模式一种是主动连续发送每秒几帧到几十帧另一种是上位机发命令后仪表应答。无人值守场景强烈建议用主动上报模式因为上位机不需要知道车辆何时上磅只要持续接收就能捕捉重量变化。下面这段代码是串口接收线程的基础骨架用队列把数据从事件线程转移到处理线程避免UI卡顿private SerialPort _serialPort; private ConcurrentQueuestring _weightQueue new ConcurrentQueuestring(); private CancellationTokenSource _cts new CancellationTokenSource(); private void InitSerialPort(string portName, int baudRate) { _serialPort new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _serialPort.DataReceived (s, e) { // 事件线程里只做收数据入队不做解析 string data _serialPort.ReadExisting(); _weightQueue.Enqueue(data); }; _serialPort.Open(); } private void ProcessWeightLoop() { Task.Run(() { StringBuilder buffer new StringBuilder(); while (!_cts.IsCancellationRequested) { while (_weightQueue.TryDequeue(out string chunk)) { buffer.Append(chunk); // 按帧尾符截取完整帧常见的是回车符或换行符 string completeFrame ExtractCompleteFrame(buffer); if (completeFrame ! null) { HandleWeightFrame(completeFrame); } } Thread.Sleep(50); } }); }ConcurrentQueue用来做线程安全的数据中转ReadExisting一次性读出当前缓冲区所有数据。这里有个容易忽略的点仪表数据是字节流一帧数据可能被拆成两次到达所以必须用缓冲区和帧尾符做粘包处理。ExtractCompleteFrame的作用就是从字节流里截出完整的一帧返回null表示还没收齐等下一段数据来再拼。重量帧的解析要看仪表的具体协议。常见格式是“STGS0015000kg”这类ASCII串头两个字母是命令类型中间是符号和数字末尾是单位。解析时要优先找数字部分用正则或固定位置截取都可以但要注意负数重量比如车辆上磅瞬间仪表可能显示负值不能直接丢弃某些皮重逻辑依赖这个值。3.2 车牌识别对接HTTP轮询和状态锁定车牌识别摄像头普遍提供HTTP接口常见做法是摄像头持续抓拍识别服务把结果保存在自己的队列里上位机按一定频率去拉取。拉取到的结果里包含车牌号、置信度、抓拍时间、图片URL。无人值守流程里车牌识别不是“每帧都处理”而是“在特定状态才处理”。private async Taskstring PollPlateNumberAsync(string cameraApiUrl, int timeoutMs 3000) { using (HttpClient client new HttpClient()) { client.Timeout TimeSpan.FromMilliseconds(timeoutMs); try { // 拉取当前识别结果接口返回JSON string json await client.GetStringAsync(cameraApiUrl /latest); var result JsonConvert.DeserializeObjectPlateResult(json); if (result ! null result.Confidence 85) { // 置信度低于阈值视为无效 return result.PlateNumber; } } catch (Exception ex) { LogHelper.Error(车牌识别轮询失败, ex); } return null; } }轮询频率设在500ms到1000ms之间比较合适太快会占用摄像头并发通道太慢则车辆通过时可能漏识。关键点是结果必须带置信度阈值我一般设85分低于这个值的车牌不直接拒绝而是进入人工复核队列。流程上的坑是道闸放下时摄像头也可能拍到来车识别到的是隔壁车道的车。所以车牌识别的触发必须和业务状态绑定——只有当前状态是“等待入场车辆”并且地感线圈或雷达检测到新车辆时才去轮询摄像头结果识别到后立即把状态推进并锁定避免同一辆车重复绑定。3.3 称重状态机把过磅流程变成一张可转移的图无人值守地磅流程里最容易乱的就是状态。一辆车从进厂到出厂经历“待入场→车牌识别→上磅→重量稳定→防作弊检查→数据保存→抬杆放行→离开”任何一个环节卡住都影响整个车道通行。用状态机管理是所有可靠实现的共同选择。public enum WeighingState { Idle 0, // 无车空闲 WaitingPlate, // 等待车牌识别 VehicleOnScale, // 车辆上磅中 WeightStable, // 重量已稳定 AntiCheatCheck, // 防作弊检查 DataSaved, // 称重数据已保存 GateOpening, // 道闸正在抬起 WaitingLeave // 等待车辆离开 } private WeighingState _currentState WeighingState.Idle; private void OnVehicleDetected() { // 地感或红外检测到车辆进入 if (_currentState WeighingState.Idle) { TransitionTo(WeighingState.WaitingPlate); StartPlateRecognition(); } } private void TransitionTo(WeighingState newState) { Console.WriteLine(${DateTime.Now:HH:mm:ss} 状态切换: {_currentState} - {newState}); _currentState newState; SaveStateToLocalStore(); // 关键状态必须持久化防断电丢失 }状态转移的触发源是事件车辆检测事件、车牌识别完成事件、重量稳定事件、道闸抬杆完成事件。每个事件的响应函数里先判断当前状态是否合法接收这个事件再做动作。非法事件必须记录日志而不是静默丢弃这是排查现场问题的重要依据。SaveStateToLocalStore这一步容易被忽略。现场断电是常态如果状态只存在内存里重启后程序不知道上一辆车称到一半车道会一直卡住或者误放行。我一般把状态和车辆绑定信息写到本地SQLite或JSON文件启动时做一次现场恢复。3.4 重量稳定判定别一收到数据就当最终重量仪表数据是连续波动的车没停稳时重量可能跳几十公斤。简单的做法是“连续N帧差值小于阈值就认为稳定”但去重时要注意仪表的上报频率。比如仪表每秒上报10帧连续5帧稳定等于0.5秒这个时间太短大车惯性大还没停稳如果仪表每秒上报2帧连续5帧等于2.5秒就比较合理。private Queuedouble _recentWeights new Queuedouble(5); private bool IsWeightStable(double newWeight, double toleranceKg 20) { _recentWeights.Enqueue(newWeight); while (_recentWeights.Count 5) { _recentWeights.Dequeue(); } if (_recentWeights.Count 5) { return false; } double max _recentWeights.Max(); double min _recentWeights.Min(); return (max - min) toleranceKg; }toleranceKg参数就是稳定判定的核心设太大会把还没停稳的车当稳定设太小则风一吹就判定失败。20公斤是我在普通地磅上的常用值但如果现场有风力干扰或者秤台机械振动较大需要适当放宽到30-50公斤。稳定时长建议硬性要求不少于2秒而不是只看帧数。判断稳定后还要加一个“保持稳定”的确认逻辑连续稳定1秒后才进入下一步避免车辆刚好在临界状态来回跳动。4. 从入场到出厂完整跑通一次无人值守过磅4.1 入场登记道闸控制与车辆绑定车辆到达入口地感线圈检测信号触发程序从Idle状态切换。此时道闸保持关闭LED屏显示“请等待”。车牌识别完成后程序把车牌号和当前时间绑定为一个过磅会话然后抬杆放行。这里有个顺序问题是先抬杆再上磅还是先确认车牌再抬杆现场经验是先确认车牌再抬杆防止未识别车辆直接冲上磅台。道闸控制有两种实现一种是串口/继电器直接控制道闸控制器一种是走开关量输出。串口控制需要在代码里拼协议指令常见的是“抬杆”“落杆”“停”三个命令。下面以串口为例private void ControlGate(GateAction action) { byte[] cmd action switch { GateAction.Raise new byte[] { 0xAA, 0x01, 0x00 }, // 抬杆指令 GateAction.Lower new byte[] { 0xAA, 0x02, 0x00 }, // 落杆指令 GateAction.Stop new byte[] { 0xAA, 0x03, 0x00 }, // 停止指令 _ null }; if (cmd null) return; _gatePort.Write(cmd, 0, cmd.Length); Thread.Sleep(300); // 给道闸控制器一个响应间隔 }不同道闸品牌的指令集不一样但套路相同串口发一串十六进制字节控制器返回状态帧。现场调试时先不要写业务代码而是先用串口助手确认“发抬杆指令道闸有反应”再进行程序对接。好多开发直接在程序里调失败了又不知道是串口没通、指令不对、还是道闸故障排查效率极低。车辆绑定是用一个会话对象保存当前流程的上下文车牌号、入场时间、首次称重、二次称重、抓拍图片路径。这个对象在车辆离开后销毁或归档但数据要落库留痕因为后期对账、追溯都依赖它。4.2 称重判定与红外防作弊重量和状态的校验链车辆上磅后程序持续接收仪表数据并做稳定判定。稳定后进入防作弊检查这一步是无人值守系统的灵魂。常见作弊手段有两种车轮不完全上磅前轮或后轮在磅外和车辆在磅上时有人踩磅、压磅。红外对射栅栏专门用来检测第一种情况一般装在磅体前后两端车辆任何一部分遮挡红外线程序就判定为未完全上磅并阻止称重保存。private bool CheckAntiCheat(double stableWeight, bool frontInfraredBlocked, bool rearInfraredBlocked) { // 红外被遮挡说明还有部件在磅台之外 if (frontInfraredBlocked || rearInfraredBlocked) { LogHelper.Warn($防作弊失败: 前红外{frontInfraredBlocked}, 后红外{rearInfraredBlocked}); return false; } // 重量异常低也视为可疑常见单车皮重最小2吨低于此值极大概率没完全上磅 if (stableWeight 2000) { LogHelper.Warn($防作弊失败: 重量异常{stableWeight}kg); return false; } return true; }红外信号的获取依赖开关量采集。常见做法是用一个串口IO模块或Modbus RTU读DI状态读上来的是布尔量。代码上没有太多玄学真正的问题是红外安装位置和数量。前后各一对是最低配中间间隔大、车长跨度大的磅需要多加一对红外做交叉验证。红外的安装高度也有讲究太低容易被地面夹角误触发太高又可能被车厢中部遮挡一般离地面30厘米左右比较稳妥。防作弊通过后重量判定结果才可落库。同时抓拍两张照片车头含车牌特写和整车侧面全景图片路径存入数据库。这两张照片不是为了好看是后期纠纷时唯一的事实依据。4.3 数据落库与LED屏引导把结果交出去称重完成的瞬间程序需要同步干三件事把重量数据写进数据库、把结果通过串口或网络发给LED屏显示、控制道闸抬杆放行。这三件事的顺序有讲究先落库再抬杆。如果先抬杆车就开走了数据库写入失败就什么证据都没了。private void SaveWeighingRecord(WeighingSession session, double grossWeight) { using (var conn new SqlConnection(_connString)) { conn.Open(); using (var tx conn.BeginTransaction()) { try { // 插入称重主表记录 string insertSql INSERT INTO weigh_records (plate_number, gross_weight, weigh_time, front_image, side_image, operator, status) VALUES (plate, gross, time, frontImg, sideImg, operator, 1); using (var cmd new SqlCommand(insertSql, conn, tx)) { cmd.Parameters.AddWithValue(plate, session.PlateNumber); cmd.Parameters.AddWithValue(gross, grossWeight); cmd.Parameters.AddWithValue(time, DateTime.Now); cmd.Parameters.AddWithValue(frontImg, session.FrontImagePath); cmd.Parameters.AddWithValue(sideImg, session.SideImagePath); cmd.Parameters.AddWithValue(operator, AUTO); cmd.ExecuteNonQuery(); } tx.Commit(); LedDisplay.Show($车牌:{session.PlateNumber} 毛重:{grossWeight / 1000.0:F2}吨); } catch (Exception ex) { tx.Rollback(); LogHelper.Error(称重记录保存失败禁止抬杆, ex); throw; } } } }事务在这里起作用主表插入失败则后续动作取消道闸不抬杆。这里的关键点是operator字段无人值守时存的是“AUTO”但数据审计时要能区分自动和人工干预的记录所以这个字段不能省。LED屏的显示指令通常是走TCP或RS485格式一般是“显示内容刷新命令”。因为屏的协议五花八门建议在设备层单独封装一个LedClient里面只暴露Show(string line1, string line2)方法不同牌子换实现业务层不关心。5. 无人值守地磅的常见坑现象、原因与解决方案5.1 串口偶尔丢数据重量跳变甚至卡死现象系统运行几小时后重量值突然不动仪表端显示正常程序端却收不到任何数据重启程序恢复正常但隔几小时又复发。原因有两个层面。第一层是硬件串口线过长、没有用屏蔽线、仪表和电脑没有共地导致信号衰减或干扰偶发丢字节。第二层是软件串口事件线程里做了耗时操作比如直接写数据库阻塞了数据接收缓冲区溢出后数据被丢弃。解决方式是硬软结合。硬件上换屏蔽双绞线长度控制在15米内仪表端加隔离器。软件上严格遵守“事件线程只入队工作线程再处理”的原则同时给串口接收加上超时重试如果连续3秒没有收到任何帧主动关闭并重开串口这叫“串口自愈”。不要小看这个重开逻辑工业串口设备经常出现死锁周期性地重置串口是通用解法。5.2 重量“漂移”风一吹重量就变现象车已经停稳程序却判定重量不稳定仪表上一个数一个数地跳差值远大于正常范围。常见原因是秤台机械结构问题限位螺栓卡死、传感器受力不均但软件侧也有一份责任稳定判定参数不合适。仪表数字漂移幅度跟传感器量程有关100吨地磅和30吨地磅的波动幅度完全不同。把容差设成固定值就是一个设计错误应该按量程比例计算。解决方式是重量容差用相对值的方式实现比如量程的万分之三而不是固定20公斤。另外增加一个“稳定计时器”从第一次进入容差范围开始计时持续2秒才判定稳定把瞬时抖动过滤掉。5.3 红外被遮挡却未触发防作弊现象车辆明显没有完全上磅程序却正常完成称重并抬杆放行。原因基本出现在硬件安装层面。第一红外的安装高度不合适车辆底部某个部位恰好从探测间隙穿过没有遮挡光线。第二只装了一对红外在车辆斜停上磅时前后桥可能都不在红外轴线上。解决方式是做成“多对红外状态联动”。在磅台四角各装一对红外程序里对四路信号做逻辑与任何一路被遮挡就判定为未完全上磅。另外红外信号进软件时要做延时滤波避免车辆抖动瞬间产生的毛刺信号被当成正常信号。红外被异物树叶、拉绳长期遮挡时软件也要轮询检查并触发报警而不是用户到现场才发现。5.4 车牌识别误触发识别到的是上一辆车现象A车已经上磅在称重B车从旁边车道经过系统突然重新绑定车牌把A车的称重结果记到B车名下。原因很明确车牌识别服务是持续工作的上位机轮询时没有判断当前业务状态任何识别结果都去更新会话。地磅尤其要警惕这种“串台”因为一个秤台可能挨着道路摄像头视角里能拍到多辆路过车辆。解决方式是在业务层加一个“识别结果有效窗口”。只有车辆检测信号地感、雷达或红外触发了入场状态后才开始轮询车牌识别到结果后立即停止轮询并锁定车牌直到车辆离场才解除锁定。整个流程里车牌识别的生命周期是“触发起始、识别即锁、离场解锁”不该在整个程序生命周期里持续监听。5.5 断电恢复后车辆卡在中间状态现象称重过程中突然断电重启程序后车道上的车既不能前进也不能后退程序不知道这辆车处于什么阶段。原因就是状态机只存在于内存里断电后一切回到起点。无人值守系统必须考虑“现场恢复”能力。解决方式是把核心状态和会话数据实时落盘。我用的是SQLite本地文件大小和读写性能都够用关键字段包括当前状态、车牌号、进入时间、当前稳定重量。程序启动时先检查SQLite里是否有未完成会话有则恢复到对应状态继续处理如果会话长时间无进展还需要一个超时机制车辆在磅上超过10分钟没有任何操作自动通知远程管理员。不要想着断电瞬间保存所有数据意外断电时任何程序都来不及完成复杂保存所以必须“边运行边落盘”把持久化动作穿插在日常状态流转中而不是依赖退出时的清理动作。6. 进阶给无人值守地磅加上远程监控与断电补传系统在本地跑通只是第一步真正让它可靠的是运维层面的机制。我习惯在基础流程跑通后做三件事设备心跳监控、本地数据缓存补传、分级日志。设备心跳通过一个后台线程实现每5秒写入一条心跳记录设备编号、时间、串口状态、红外状态、道闸状态、当前业务状态。远程端每分钟拉取一次连续3次没有心跳就触发报警。这个机制看起来简单但真能省掉大量现场跑动的成本——设备离线、假死、通信中断时软件侧第一时间就知道而不是等现场打电话来。本地缓存补传是应对网络不稳定的方案。无人值守地磅经常部署在偏远厂区上位机和服务器之间走内网也可能抖动。我的做法是称重记录先写本地SQLite同时写入一个待同步表后台线程定时比如每30秒尝试推送中心数据库。推送成功后把待同步表的状态标记为已同步未同步的数据在网线恢复后自动补传。这个机制保证了数据不丢也避免了一次网络抖动导致车辆堵在门口。日志分级方面我现在所有代码都遵循“三本账”原则设备日志串口帧、命令收发、业务日志状态切换、防作弊判定、车辆绑定、异常日志代码异常、数据库错误。三个日志写到不同文件并按天滚动。排障时先看设备日志确认硬件通信是否正常再看业务日志确认流程在哪一步中断最后才是代码异常。这个习惯救了我好几次等到现场老司机跟我说“程序死了”的时候日志已经是唯一的破案线索。最后说一个偏执但有用的习惯所有状态切换代码都先写一行日志、再切换状态、再执行动作。不要觉得日志拖慢性能地磅系统毫秒级的开销根本感知不到但排查问题时日志里的一行状态记录能省三小时现场蹲守。希望这篇文章能帮你在做无人值守地磅时少走几步弯路让系统的第一次上线就能扛住现场的真实压力。本文还有配套的精品资源点击获取