ARTICLE DETAIL

资讯详情

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

C#串口称重数据采集与SQLite存储实战

C#串口称重数据采集与SQLite存储实战 1. 项目缘起与整体设计思路1.1 一个“土得掉渣”的需求为什么值得认真做现场称重数据采集这件事听起来一点都不性感。没有大屏可视化没有云端协同甚至连个像样的界面都未必需要。但如果你在工厂车间、物流仓库、养殖场或者废品回收站待过就会明白一个朴素的道理称重数据如果靠人工抄写出错率能高到让你怀疑人生。我接到的第一个称重项目是给一个饲料厂做配料记录。现场三台电子秤工人每称完一袋原料要低头看仪表、拿笔抄在纸质表格上一天下来几百条记录。月底统计的时候字迹潦草、漏抄、抄错的问题层出不穷财务和仓管互相扯皮。老板的原话是“能不能让电脑自己把重量记下来别让人插手”这个需求翻译成技术语言就是把电子秤串口输出的重量数据实时读取、解析、存库并且保证长期稳定运行。听起来简单但真正落地的时候坑一个接一个。我前后在现场调试了三天最终这套小工具稳定跑了两年多中间除了换过一次串口线没有出过任何软件层面的故障。这篇文章不讲虚的就把这套串口称重小工具的核心设计、关键代码、踩坑经验和盘托出。涉及的技术栈包括C# 上位机开发、RS232/RS485 串口通信、SQLite 本地存储以及现场调试必须掌握的一套排查方法。无论你是刚接触串口编程的新手还是做过类似项目但总被稳定性问题困扰的老手相信都能从中找到可以直接复用的东西。1.2 为什么选 C# SQLite 串口这套组合做上位机开发语言选择其实不少。C 性能好但开发效率低Python 写起来快但打包部署和长期运行稳定性让人头疼LabVIEW 做仪器控制很专业但授权成本和维护门槛摆在那里。我最终选C#理由很实际开发效率与运行稳定性的平衡C# 的SerialPort类封装成熟事件驱动模型写起来顺手.NET Framework 在 Win7/Win10 上部署几乎没有额外依赖。现场维护方便编译出来就是一个 exe拷到工控机上双击就能跑。出了问题远程或者现场改几行代码重新编译五分钟搞定。SQLite 的零配置优势现场工控机往往没有专门的数据库服务器装 MySQL 或 SQL Server 既重又麻烦。SQLite 就是一个文件备份就是复制粘贴十万条以内的数据查询毫秒级响应完全够用。至于串口本身RS232 和 RS485 是电子秤最常用的两种物理层。RS232 适合短距离、单设备RS485 适合多设备组网、长距离传输。这套工具两种都支持后面会详细讲区别和配置要点。这里插一句如果你用的是 USB 转串口线芯片方案很关键。FTDI 和 CH340 是现场最常见的两种FTDI 稳定性好但价格高CH340 便宜但偶尔会掉驱动。我一般建议客户用 FTDI 方案的线省下来的调试时间远比线材差价值钱。1.3 整体架构三层结构各司其职这套工具的结构非常清晰就三层第一层串口通信层。负责打开串口、配置参数波特率、数据位、停止位、校验位、接收原始字节流。这一层的核心任务是“不漏数据、不丢包”。第二层协议解析层。电子秤输出的不是裸数字而是带格式的字符串。常见的有“ST,GS,00123.45kg”这种带状态和单位的也有“00123.45”这种纯数字的。解析层要把这些字符串变成程序能处理的浮点数。第三层数据存储与展示层。解析后的重量数据写入 SQLite同时在界面上实时显示。存储层要考虑写入频率、事务处理和异常恢复。三层之间通过事件和队列解耦串口收数据不阻塞界面界面刷新不影响串口接收。这个设计是长期稳定运行的基础。2. 核心细节解析与实操要点2.1 串口参数配置别小看这几个选项串口通信的第一步是打开串口并配置参数。电子秤的说明书上一般会写明默认参数但现场经常遇到说明书丢失或者参数被改过的情况。这时候就需要用串口调试助手去“猜”参数。标准配置通常是参数常见值说明波特率9600 / 4800 / 19200电子秤最常用 9600数据位8几乎都是 8 位停止位1少数设备用 2 位校验位None / Even / Odd需要试流控None基本不用在 C# 里配置串口的代码如下SerialPort port new SerialPort(COM3, 9600, Parity.None, 8, StopBits.One); port.ReadTimeout 500; port.WriteTimeout 500; port.DataReceived Port_DataReceived; port.Open();看起来简单但有几个细节必须注意第一DataReceived事件不是在主线程触发的。这意味着你不能在事件处理函数里直接操作界面控件否则会抛跨线程异常。正确做法是用Invoke或者把数据丢到队列里由主线程处理。第二ReadTimeout不要设得太短。有些电子秤是连续发送模式有些是收到指令才回复。如果是指令回复模式超时太短会导致读不到完整数据。第三打开串口前先检查是否被占用。Win7 下经常出现串口被其他程序占着的情况可以用System.IO.Ports.SerialPort.GetPortNames()列出可用串口但更可靠的方法是直接尝试打开捕获异常。实操心得现场调试时我习惯先用串口调试助手确认电子秤在发数据并且参数正确。确认无误后再用 C# 程序去读。这样能把“串口配置问题”和“程序逻辑问题”分开排查效率高很多。2.2 数据接收如何做到不丢包、不粘包串口数据接收是整套工具最核心也最容易出问题的地方。常见的问题有三个丢包、粘包、断帧。丢包通常是因为接收缓冲区太小或者处理太慢。C# 的SerialPort默认缓冲区是 4096 字节对于称重数据来说够用。但如果你的DataReceived事件处理函数里做了耗时操作比如写数据库就会导致后续数据来不及处理。我的做法是在DataReceived事件里只做一件事——把收到的字节追加到一个线程安全的缓冲区里然后立即返回。真正的解析和存储交给后台线程处理。private readonly object _lock new object(); private readonly Listbyte _buffer new Listbyte(); private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e) { int bytesToRead port.BytesToRead; byte[] temp new byte[bytesToRead]; port.Read(temp, 0, bytesToRead); lock (_lock) { _buffer.AddRange(temp); } }粘包问题是指多条数据连在一起到达。比如电子秤连续发送“00123.45\r\n00124.00\r\n”如果一次性收到就需要按分隔符拆开。大多数电子秤用\r\n作为帧结束符也有用\r或\n的。断帧问题是指一条数据被拆成两次到达。比如“00123.45\r\n”第一次收到“00123.”第二次收到“45\r\n”。这时候就需要一个缓冲区把两次的数据拼起来再解析。我的处理逻辑是维护一个字符串缓冲区每次收到新数据就追加进去然后按\r\n分割。分割出来的完整帧送去解析最后一段不完整的留在缓冲区里等下次数据。private string _frameBuffer ; private void ProcessBuffer() { string data; lock (_lock) { data Encoding.ASCII.GetString(_buffer.ToArray()); _buffer.Clear(); } _frameBuffer data; string[] frames _frameBuffer.Split(new[] { \r\n }, StringSplitOptions.None); for (int i 0; i frames.Length - 1; i) { if (!string.IsNullOrWhiteSpace(frames[i])) { ParseFrame(frames[i]); } } _frameBuffer frames[frames.Length - 1]; }这个逻辑看起来简单但它是保证不丢帧、不粘帧的关键。我见过太多项目因为没处理好这个环节导致重量数据偶尔跳变或者丢失。2.3 协议解析从字符串到浮点数电子秤输出的格式五花八门但归纳起来无非几种纯数字型00123.45或00123.45带状态型ST,GS,00123.45kgST 表示稳定GS 表示毛重带单位型00123.45 kg自定义协议有些厂家会用十六进制帧需要按字节解析对于文本协议解析的核心是提取数字部分并转换为浮点数。我一般用正则表达式private static readonly Regex WeightRegex new Regex(([-]?\d\.?\d*)); private void ParseFrame(string frame) { Match match WeightRegex.Match(frame); if (match.Success) { if (double.TryParse(match.Groups[1].Value, out double weight)) { // 处理重量数据 OnWeightReceived(weight); } } }但这里有个坑有些电子秤在稳定之前会发送波动数据。比如称重过程中重量从 0 慢慢跳到 123.45中间会经过 50.2、80.7、110.3 等值。如果全部记录数据库里会多出很多无意义的中间值。我的做法是只记录稳定状态的数据。如果协议里有状态位比如 ST 表示稳定就判断状态位。如果没有就用一个简单的滤波逻辑——连续 N 次读到的重量差值小于阈值才认为是稳定值。private double _lastWeight 0; private int _stableCount 0; private const double StableThreshold 0.05; private const int StableRequired 3; private void OnWeightReceived(double weight) { if (Math.Abs(weight - _lastWeight) StableThreshold) { _stableCount; if (_stableCount StableRequired) { SaveWeight(weight); _stableCount 0; } } else { _stableCount 0; } _lastWeight weight; }这个逻辑在实际运行中效果很好既避免了记录波动值又不会漏掉真正的称重数据。2.4 SQLite 存储十万条数据毫秒级查询的秘密SQLite 的轻量是出了名的但要用好还是有几个关键点。第一建表时字段类型要选对。重量数据用REAL类型时间戳用TEXT存 ISO8601 格式设备编号用INTEGER。不要用VARCHAR存数字查询时类型转换会拖慢速度。CREATE TABLE IF NOT EXISTS WeightRecords ( Id INTEGER PRIMARY KEY AUTOINCREMENT, DeviceId INTEGER NOT NULL, Weight REAL NOT NULL, Unit TEXT DEFAULT kg, RecordTime TEXT NOT NULL, IsStable INTEGER DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_recordtime ON WeightRecords(RecordTime); CREATE INDEX IF NOT EXISTS idx_deviceid ON WeightRecords(DeviceId);第二写入要用事务批量提交。如果每条数据都单独INSERTSQLite 会频繁写磁盘速度慢且伤硬盘。我的做法是攒够 50 条或者超过 2 秒就批量提交一次。private readonly ListWeightRecord _pendingRecords new ListWeightRecord(); private DateTime _lastFlush DateTime.Now; private void SaveWeight(double weight) { _pendingRecords.Add(new WeightRecord { DeviceId _currentDeviceId, Weight weight, RecordTime DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss) }); if (_pendingRecords.Count 50 || (DateTime.Now - _lastFlush).TotalSeconds 2) { FlushToDatabase(); } } private void FlushToDatabase() { if (_pendingRecords.Count 0) return; using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var transaction conn.BeginTransaction()) { using (var cmd conn.CreateCommand()) { cmd.CommandText INSERT INTO WeightRecords (DeviceId, Weight, RecordTime) VALUES (d, w, t); cmd.Parameters.Add(new SQLiteParameter(d)); cmd.Parameters.Add(new SQLiteParameter(w)); cmd.Parameters.Add(new SQLiteParameter(t)); foreach (var record in _pendingRecords) { cmd.Parameters[d].Value record.DeviceId; cmd.Parameters[w].Value record.Weight; cmd.Parameters[t].Value record.RecordTime; cmd.ExecuteNonQuery(); } } transaction.Commit(); } } _pendingRecords.Clear(); _lastFlush DateTime.Now; }第三查询要利用索引。十万条数据如果按时间范围查询有索引和没索引的差距是几十倍。我实测过十万条数据按时间范围查询有索引的情况下 20ms 以内返回没索引要 800ms 以上。实操心得SQLite 数据库文件建议放在 SSD 上如果工控机只有机械硬盘写入频率要适当降低。另外定期做VACUUM可以压缩数据库文件但不要频繁做一个月一次足够。3. 实操过程与核心环节实现3.1 从零搭建项目结构与依赖新建一个 C# WinForms 项目目标框架选 .NET Framework 4.7.2 或以上。需要引入的 NuGet 包System.Data.SQLiteSQLite 的 .NET 驱动Newtonsoft.Json配置文件读写可选但推荐项目结构建议这样组织WeightTool/ ├── Core/ │ ├── SerialPortManager.cs // 串口管理 │ ├── FrameParser.cs // 协议解析 │ └── WeightFilter.cs // 滤波逻辑 ├── Data/ │ ├── DatabaseManager.cs // SQLite 操作 │ └── WeightRecord.cs // 数据模型 ├── UI/ │ └── MainForm.cs // 主界面 └── Config/ └── app.config // 串口参数配置这样分层的好处是串口逻辑、解析逻辑、存储逻辑互相独立改一处不影响其他部分。现场调试时如果发现是解析问题只改FrameParser就行不用动其他代码。3.2 串口管理类的完整实现串口管理类负责串口的打开、关闭、接收和异常处理。核心代码如下public class SerialPortManager : IDisposable { private SerialPort _port; private readonly object _lock new object(); private readonly Listbyte _byteBuffer new Listbyte(); private string _frameBuffer ; public event Actionstring FrameReceived; public event Actionstring ErrorOccurred; public bool IsOpen _port ! null _port.IsOpen; public void Open(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits) { Close(); try { _port new SerialPort(portName, baudRate, parity, dataBits, stopBits); _port.ReadTimeout 500; _port.WriteTimeout 500; _port.DataReceived OnDataReceived; _port.ErrorReceived OnErrorReceived; _port.Open(); } catch (Exception ex) { ErrorOccurred?.Invoke($打开串口失败: {ex.Message}); throw; } } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { int bytesToRead _port.BytesToRead; if (bytesToRead 0) return; byte[] temp new byte[bytesToRead]; _port.Read(temp, 0, bytesToRead); lock (_lock) { _byteBuffer.AddRange(temp); } ProcessBuffer(); } catch (Exception ex) { ErrorOccurred?.Invoke($接收数据异常: {ex.Message}); } } private void ProcessBuffer() { string data; lock (_lock) { data Encoding.ASCII.GetString(_byteBuffer.ToArray()); _byteBuffer.Clear(); } _frameBuffer data; string[] frames _frameBuffer.Split(new[] { \r\n }, StringSplitOptions.None); for (int i 0; i frames.Length - 1; i) { if (!string.IsNullOrWhiteSpace(frames[i])) { FrameReceived?.Invoke(frames[i].Trim()); } } _frameBuffer frames[frames.Length - 1]; } private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e) { ErrorOccurred?.Invoke($串口错误: {e.EventType}); } public void Close() { if (_port ! null) { try { _port.DataReceived - OnDataReceived; _port.ErrorReceived - OnErrorReceived; if (_port.IsOpen) _port.Close(); } catch { } _port.Dispose(); _port null; } } public void Dispose() { Close(); } }这个类有几个设计要点异常处理要全面。串口操作随时可能因为线缆松动、设备断电等原因抛异常。每个可能出错的地方都要捕获并通过事件通知上层而不是让程序崩溃。关闭串口时要先解绑事件。否则在关闭过程中可能还会触发DataReceived导致访问已释放的对象。缓冲区要加锁。DataReceived在后台线程触发而ProcessBuffer可能被多个线程调用不加锁会出现数据竞争。3.3 数据库管理类的实现数据库管理类负责建表、插入和查询。核心代码如下public class DatabaseManager { private readonly string _connectionString; public DatabaseManager(string dbPath) { _connectionString $Data Source{dbPath};Version3;; InitializeDatabase(); } private void InitializeDatabase() { using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var cmd conn.CreateCommand()) { cmd.CommandText CREATE TABLE IF NOT EXISTS WeightRecords ( Id INTEGER PRIMARY KEY AUTOINCREMENT, DeviceId INTEGER NOT NULL, Weight REAL NOT NULL, Unit TEXT DEFAULT kg, RecordTime TEXT NOT NULL, IsStable INTEGER DEFAULT 1 ); CREATE INDEX IF NOT EXISTS idx_recordtime ON WeightRecords(RecordTime); CREATE INDEX IF NOT EXISTS idx_deviceid ON WeightRecords(DeviceId); ; cmd.ExecuteNonQuery(); } } } public void BatchInsert(ListWeightRecord records) { if (records null || records.Count 0) return; using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var transaction conn.BeginTransaction()) { using (var cmd conn.CreateCommand()) { cmd.CommandText INSERT INTO WeightRecords (DeviceId, Weight, Unit, RecordTime, IsStable) VALUES (d, w, u, t, s); cmd.Parameters.Add(new SQLiteParameter(d)); cmd.Parameters.Add(new SQLiteParameter(w)); cmd.Parameters.Add(new SQLiteParameter(u)); cmd.Parameters.Add(new SQLiteParameter(t)); cmd.Parameters.Add(new SQLiteParameter(s)); foreach (var record in records) { cmd.Parameters[d].Value record.DeviceId; cmd.Parameters[w].Value record.Weight; cmd.Parameters[u].Value record.Unit ?? kg; cmd.Parameters[t].Value record.RecordTime; cmd.Parameters[s].Value record.IsStable ? 1 : 0; cmd.ExecuteNonQuery(); } } transaction.Commit(); } } } public ListWeightRecord QueryByTimeRange(DateTime start, DateTime end, int deviceId -1) { var result new ListWeightRecord(); using (var conn new SQLiteConnection(_connectionString)) { conn.Open(); using (var cmd conn.CreateCommand()) { if (deviceId 0) { cmd.CommandText SELECT * FROM WeightRecords WHERE RecordTime start AND RecordTime end AND DeviceId did ORDER BY RecordTime; cmd.Parameters.Add(new SQLiteParameter(did, deviceId)); } else { cmd.CommandText SELECT * FROM WeightRecords WHERE RecordTime start AND RecordTime end ORDER BY RecordTime; } cmd.Parameters.Add(new SQLiteParameter(start, start.ToString(yyyy-MM-dd HH:mm:ss))); cmd.Parameters.Add(new SQLiteParameter(end, end.ToString(yyyy-MM-dd HH:mm:ss))); using (var reader cmd.ExecuteReader()) { while (reader.Read()) { result.Add(new WeightRecord { Id reader.GetInt32(0), DeviceId reader.GetInt32(1), Weight reader.GetDouble(2), Unit reader.GetString(3), RecordTime reader.GetString(4), IsStable reader.GetInt32(5) 1 }); } } } } return result; } }这个类里BatchInsert是性能关键。我实测过单条插入 1000 条数据需要约 4 秒批量插入每 50 条一个事务只需要 0.3 秒左右差距十几倍。3.4 主界面与线程模型主界面负责显示实时重量、设备状态和操作按钮。关键是要处理好跨线程更新。public partial class MainForm : Form { private SerialPortManager _portManager; private DatabaseManager _dbManager; private readonly ListWeightRecord _pendingRecords new ListWeightRecord(); private readonly object _recordLock new object(); private System.Windows.Forms.Timer _flushTimer; public MainForm() { InitializeComponent(); _dbManager new DatabaseManager(weights.db); _portManager new SerialPortManager(); _portManager.FrameReceived OnFrameReceived; _portManager.ErrorOccurred OnSerialError; _flushTimer new System.Windows.Forms.Timer { Interval 2000 }; _flushTimer.Tick (s, e) FlushRecords(); _flushTimer.Start(); } private void OnFrameReceived(string frame) { // 解析帧提取重量 double weight ParseWeight(frame); if (weight 0) return; // 更新界面跨线程 if (InvokeRequired) { BeginInvoke(new Action(() UpdateWeightDisplay(weight))); } else { UpdateWeightDisplay(weight); } // 加入待写入队列 lock (_recordLock) { _pendingRecords.Add(new WeightRecord { DeviceId _currentDeviceId, Weight weight, RecordTime DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss) }); } } private void FlushRecords() { ListWeightRecord toWrite; lock (_recordLock) { if (_pendingRecords.Count 0) return; toWrite new ListWeightRecord(_pendingRecords); _pendingRecords.Clear(); } try { _dbManager.BatchInsert(toWrite); } catch (Exception ex) { // 写入失败把数据放回队列 lock (_recordLock) { _pendingRecords.InsertRange(0, toWrite); } ShowError($数据库写入失败: {ex.Message}); } } private void UpdateWeightDisplay(double weight) { lblWeight.Text weight.ToString(F2) kg; lblLastTime.Text DateTime.Now.ToString(HH:mm:ss); } }这个线程模型的核心是串口事件线程只负责收数据和入队定时器线程负责批量写库界面线程负责显示。三者通过锁和队列解耦互不阻塞。4. 常见问题与排查技巧实录4.1 串口打不开先查这五个地方串口打不开是现场调试最高频的问题。我总结了一个排查顺序排查项检查方法常见原因串口号是否正确设备管理器查看USB转串口线插拔后串口号会变串口是否被占用尝试用调试助手打开其他程序占着串口线缆是否接对检查DB9针脚2、3脚接反交叉线 vs 直连线设备是否上电看设备指示灯电子秤没开机驱动是否正常设备管理器有无黄色感叹号CH340驱动未安装Win7 下查看串口被哪个程序占用可以用微软的Process Explorer在Find菜单里搜索串口名能定位到占用进程。或者用handle.exe命令行工具。踩坑记录有一次现场调试串口死活打不开。查了半天发现是客户工控机上装了一个老版本的称重软件后台服务一直在占用串口。卸载后问题解决。所以现场调试第一件事就是确认没有其他程序在抢串口。4.2 数据乱码九成是波特率或校验位不对串口收到的数据如果是乱码基本可以断定是通信参数不匹配。排查方法确认波特率。电子秤说明书上一般会写常见的是 9600。如果说明书丢了可以试 4800、19200、38400。确认校验位。None、Even、Odd 都试一遍。确认数据位和停止位。绝大多数是 8N18数据位、无校验、1停止位。如果参数都对但还是乱码检查线缆是否过长。RS232 理论传输距离 15 米实际超过 10 米就可能不稳定。RS485 可以到 1200 米但需要终端电阻。4.3 数据偶尔丢失检查缓冲区处理逻辑数据偶尔丢失通常不是串口本身的问题而是程序处理逻辑有缺陷。常见原因DataReceived事件里做了耗时操作。比如直接写数据库导致后续数据来不及读。缓冲区没有加锁。多线程同时读写Listbyte会导致数据损坏。帧分割逻辑有误。比如用\n分割但实际是\r\n导致帧不完整。我的建议是在DataReceived里只做字节追加其他所有处理都放到独立线程。这样即使解析和存储偶尔慢一点也不会丢数据。4.4 SQLite 写入慢试试这三个优化SQLite 默认配置下每次INSERT都会等磁盘确认速度很慢。优化方法第一使用事务批量提交。这是最有效的优化没有之一。第二调整PRAGMA synchronous。默认是FULL改成NORMAL可以大幅提升写入速度代价是断电时可能丢失最后几条数据。对于称重记录来说这个代价可以接受。PRAGMA synchronous NORMAL; PRAGMA journal_mode WAL;第三定期VACUUM。删除数据后SQLite 文件不会自动缩小VACUUM可以整理碎片、压缩文件。但VACUUM会锁库建议在非工作时间执行。4.5 长期运行稳定性内存泄漏与句柄泄漏这套工具稳定运行两年的关键是避免了内存泄漏和句柄泄漏。几个必须注意的点串口关闭时要解绑事件。不解绑的话SerialPort对象无法被 GC 回收。数据库连接要用using。确保每次用完都释放。定时器要记得停止和释放。窗体关闭时Timer如果不停止会一直触发。日志要定期清理。如果程序写日志文件要设置滚动策略否则日志文件会撑满硬盘。我一般会在程序里加一个简单的自检逻辑每小时检查一次内存占用如果超过 200MB 就记录警告。正常运行情况下这套工具的内存占用稳定在 50MB 左右。4.6 多设备 RS485 组网地址与终端电阻如果现场有多台电子秤用 RS485 组网比 RS232 更合适。RS485 是总线结构一条双绞线可以挂多台设备。关键配置每台设备要有唯一地址。电子秤一般支持地址设置通过仪表按键或配置软件修改。总线两端要接终端电阻。通常 120 欧姆接在总线最远的两端。上下拉电阻。RS485 总线在空闲时电平不确定需要加上下拉电阻通常 4.7kΩ确保空闲时为高电平。实操心得RS485 组网时如果通信不稳定先检查终端电阻和上下拉电阻。我遇到过一条总线挂了 8 台秤其中一台地址重复导致整个总线通信时好时坏。排查了半天才发现是地址冲突。5. 现场调试的三天与两年的运行观察5.1 第一天打通串口确认数据格式第一天到现场先不写代码用串口调试助手把电子秤的数据读出来。这一步的目的是确认三件事串口号、通信参数、数据格式。我一般会打开调试助手设置好参数然后让工人正常称重几次。观察数据的变化规律稳定值是什么格式波动值是什么格式有没有状态位。这一步看起来简单但绝对不能跳过。我见过太多人直接写代码结果因为数据格式理解错误返工重来。5.2 第二天编写解析与存储逻辑确认数据格式后第二天开始写代码。核心是解析逻辑和存储逻辑。这一天的工作量最大但因为有第一天的数据样本写起来很快。我会先用一个简单的控制台程序验证解析逻辑确认能正确提取重量值再集成到 WinForms 界面里。这样能把解析问题和界面问题分开排查。5.3 第三天现场联调与异常测试第三天把程序部署到工控机上进行联调。重点测试异常场景拔掉串口线程序是否报错但不崩溃电子秤断电再上电程序是否能自动恢复数据库文件被占用程序是否提示错误连续运行几小时内存是否稳定这些异常测试是保证长期稳定运行的关键。很多程序在正常流程下没问题一遇到异常就崩溃就是因为异常处理没做好。5.4 两年运行观察什么变了什么没变这套工具从部署到现在稳定运行了两年多。中间只换过一次串口线被叉车压断了软件层面没有出过任何故障。数据库文件从最初的几 MB 增长到现在的 200 多 MB查询速度依然很快。我每个月会远程备份一次数据库文件顺便检查一下程序日志看看有没有异常记录。现场工人已经习惯了这套工具称重数据自动记录月底导出 Excel 对账效率比手工抄写提升了不止一个档次。老板后来还介绍了一个同行过来我又用同样的方案给另一家厂做了一套基本上三天调试、长期稳定运行已经成了我的标准交付流程。这套工具的核心其实就四招串口接收不丢包、协议解析不跳变、SQLite 批量写入不卡顿、异常处理不崩溃。把这四件事做好剩下的就是现场调试的耐心和细心了。
返回列表