尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C# WinForm工业上位机实战:Modbus温湿度采集与SQLite存储

发布时间:2026/9/29 18:05:01

资讯中心
01
ARTICLE

C# WinForm工业上位机实战:Modbus温湿度采集与SQLite存储

C# WinForm工业上位机实战:Modbus温湿度采集与SQLite存储
简介这是一套基于 C# WinForm 开发的工业级温湿度监控上位机源码面向希望掌握上位机开发、Modbus 通信与本地数据管理的初中级开发者可用于学习完整的工业监控场景搭建。项目通过串口与下位机进行 Modbus RTU 数据交互界面实时显示温湿度数值并用 Chart 控件绘制支持自动缩放的趋势曲线报警模块可设置高低温、高低湿阈值状态栏按颜色区分报警状态报警列表记录历史事件以避免 MessageBox 阻塞。采集数据自动持久化到 SQLite 数据库支持加载历史记录并导出为 Excel 文件串口与阈值等设置保存在 config.json 中重启后自动恢复窗体缩放时控件自动对齐。资源包共 22 个文件以 11 个 cs 源码、2 个 config、2 个 resx、2 个 json 及 sln、csproj、pdf 说明文档等为主压缩包约 326KB结构清晰便于按模块阅读。已有 131 人学习适合作为课程设计或工程入门的参考范例。1. 温湿度上位机到底在工厂里干什么从一根 RS485 线说起车间里一台老化柜旁边挂着四个温湿度探头值班师傅每两小时拿本子抄一次数抄错一位整批物料就得复检。这个场景我见过太多次也是「基于 C# WinForm 开发的工业级设备配套上位机软件」最真实的起点。它要干的事说穿了就四件通过 Modbus 通信把传感器寄存器里的原始值读上来在界面上实时刷新按固定周期落到数据库再提供历史查询和导出。听起来简单但「工业级」三个字意味着它得连续跑几个月不崩、断线能自愈、数据不能丢、界面不能卡。适合谁做会一点 C#、手上有 Modbus 温湿度传感器、需要给设备配一套采集软件的人。下面我按自己实际搭过的一套方案把选型、代码、参数和踩过的坑讲清楚。2. Modbus 通信层怎么搭从寄存器地址到能跑通的第一次读取2.1 先搞清楚你的传感器是 RTU 还是 TCP动手写代码之前必须先把物理层和协议层确认清楚这一步错了后面全白搭。常见温湿度传感器分两类一类是 RS485 接口走 Modbus RTU需要 USB 转 485 模块接到电脑另一类是自带网口走 Modbus TCP直接插网线。判断方法很简单看设备铭牌和说明书上的接口类型RS485 就是 A/B 两根线网口就是 RJ45。RTU 和 TCP 在代码层面的差别主要在「传输载体」RTU 走SerialPort需要配波特率、数据位、停止位、校验位TCP 走TcpClient只需要 IP 和端口Modbus TCP 默认 502。但两者的「报文结构」是同一套思路——功能码、起始地址、寄存器数量、CRCRTU 有TCP 没有因为 TCP 自带校验。我一般会先用 Modbus Poll 这类调试工具手动读一次确认能读到数据再写代码。这一步能排除掉 80% 的接线和参数问题。调试时重点看三样从站地址Slave ID常见默认 1、功能码读保持寄存器是 03读输入寄存器是 04、寄存器起始地址注意有些厂家文档写的是 40001 这种「PLC 地址」实际协议地址要减 1。2.2 用 NModbus 封装一个能自愈的读取类C# 里操作 Modbus 最省事的是 NModbus 这个库RTU 和 TCP 都支持。下面是我实际用的一个 RTU 读取封装核心思路是把串口打开、读取、异常重连都收在一个类里上层只管调Read()。using System; using System.IO.Ports; using NModbus; public class ModbusRtuReader : IDisposable { private SerialPort _port; private IModbusSerialMaster _master; private readonly byte _slaveId; private readonly string _portName; private readonly int _baudRate; public ModbusRtuReader(string portName, int baudRate, byte slaveId) { _portName portName; _baudRate baudRate; _slaveId slaveId; Open(); } private void Open() { _port new SerialPort(_portName, _baudRate, Parity.None, 8, StopBits.One); _port.ReadTimeout 1000; // 读超时 1 秒工业现场别设太短 _port.WriteTimeout 1000; _port.Open(); var factory new ModbusFactory(); _master factory.CreateRtuMaster(_port); _master.Transport.ReadTimeout 1000; _master.Transport.WriteTimeout 1000; _master.Transport.Retries 2; // 失败重试 2 次 } // 读温湿度起始地址 0读 2 个寄存器温度、湿度各占一个 public (float temp, float humidity) Read() { try { ushort[] regs _master.ReadHoldingRegisters(_slaveId, 0, 2); // 很多传感器是「实际值 × 10」存成整数这里除 10 还原 float temp regs[0] / 10.0f; float humidity regs[1] / 10.0f; return (temp, humidity); } catch (Exception) { // 读失败就重连一次这是工业现场最实用的自愈手段 Close(); Open(); throw; } } private void Close() { try { _master?.Dispose(); } catch { } try { _port?.Close(); } catch { } } public void Dispose() Close(); }逻辑说明构造函数里直接Open()把串口和 master 一起建好。Read()里用ReadHoldingRegisters读两个寄存器regs[0]是温度、regs[1]是湿度。关键在catch里——一旦读失败先关再开下次调用就是新连接。这个「失败即重连」的策略比在界面上弹一堆错误框实用得多。参数说明ReadTimeout设 1000ms 是因为 485 总线在长距离或干扰大时响应会慢设 100ms 会频繁超时Retries 2是 NModbus 自带的传输层重试和上面的重连是两层保险slaveId一定要和传感器拨码或配置一致多设备挂同一条 485 总线时每个从站地址必须唯一。2.3 采集线程和界面刷新怎么解耦新手最容易翻车的地方是在串口DataReceived事件或定时器里直接改Label.Text。串口事件跑在非 UI 线程直接碰控件会抛跨线程异常而且读一次要几十毫秒卡在 UI 线程上界面就一顿一顿的。我的做法是后台开一个采集线程或System.Threading.Timer按周期读数据读到的值塞进一个ConcurrentQueue或直接Invoke回 UI。更稳的是用BackgroundWorker或Task加CancellationToken界面只负责从队列取最新值刷新。private CancellationTokenSource _cts; private readonly ConcurrentQueue(float t, float h) _queue new(); private async Task CollectLoop(CancellationToken token) { while (!token.IsCancellationRequested) { try { var (t, h) _reader.Read(); _queue.Enqueue((t, h)); } catch { /* 已重连等下一轮 */ } await Task.Delay(1000, token); // 1 秒采一次按需调整 } } // UI 定时器里刷新50ms 取一次队列最新值 private void UiTimer_Tick(object sender, EventArgs e) { if (_queue.TryDequeue(out var latest)) { lblTemp.Text ${latest.t:F1} ℃; lblHumidity.Text ${latest.h:F1} %RH; } }逻辑说明采集和刷新彻底分开采集线程只管读和入队UI 定时器只管出队和显示。这样即使某次读取卡了 1 秒界面也不会冻结。Task.Delay用token是为了退出时能干净地取消。参数说明采集周期 1 秒对温湿度足够温湿度变化慢没必要 100ms 采一次UI 刷新 50ms 是给人眼看的再快也没意义。队列用ConcurrentQueue是因为跨线程普通Queue会有并发问题。3. 数据存储怎么选SQLite 落地、表结构和写入节奏3.1 为什么工业上位机我优先选 SQLite数据库这块热词里 MySQL、SQLite、数据库同步工具都有人问。我的经验是单机上位机、数据量每天几万条以内SQLite 是最省心的选择——零安装、单文件、备份就是拷一个文件。MySQL 适合多台上位机往同一个服务器汇总的场景但你要额外维护服务、账号、网络对一台设备配套软件来说太重了。SQLite 在 C# 里用System.Data.SQLite或Microsoft.Data.Sqlite都行我一般用后者配合 Dapper 写起来干净。下面建表语句是我实际用的结构CREATE TABLE IF NOT EXISTS SensorData ( Id INTEGER PRIMARY KEY AUTOINCREMENT, DeviceId TEXT NOT NULL, -- 设备编号多探头时区分 Temp REAL NOT NULL, -- 温度摄氏度 Humidity REAL NOT NULL, -- 湿度百分比 CollectAt TEXT NOT NULL -- 采集时间ISO8601 字符串 ); CREATE INDEX IF NOT EXISTS idx_collectat ON SensorData(CollectAt);逻辑说明DeviceId是为了以后接多个探头预留的一开始就加上省得后面改表。时间存 ISO8601 字符串yyyy-MM-dd HH:mm:ss而不是时间戳是因为查询和肉眼排查都方便SQLite 的字符串比较对 ISO 格式是按时间顺序的。idx_collectat索引是给历史查询用的按时间段查数据没索引会全表扫。参数说明REAL存浮点温湿度够用如果对精度极敏感可以存整数×10但温湿度没必要。AUTOINCREMENT保证 Id 单调递增方便按 Id 分页。3.2 批量写入和事务别一条一条 Insert血泪经验一开始我是一条数据一个INSERT跑一天下来数据库文件碎片多、写入还慢。正确做法是攒一批再写用事务包起来。public void SaveBatch(ListSensorRecord records) { using var conn new SqliteConnection(_connStr); conn.Open(); using var tran conn.BeginTransaction(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO SensorData(DeviceId,Temp,Humidity,CollectAt) VALUES(d,t,h,c); var pD cmd.CreateParameter(); pD.ParameterName d; cmd.Parameters.Add(pD); var pT cmd.CreateParameter(); pT.ParameterName t; cmd.Parameters.Add(pT); var pH cmd.CreateParameter(); pH.ParameterName h; cmd.Parameters.Add(pH); var pC cmd.CreateParameter(); pC.ParameterName c; cmd.Parameters.Add(pC); foreach (var r in records) { pD.Value r.DeviceId; pT.Value r.Temp; pH.Value r.Humidity; pC.Value r.CollectAt.ToString(yyyy-MM-dd HH:mm:ss); cmd.ExecuteNonQuery(); } tran.Commit(); }逻辑说明参数对象在循环外创建、循环内只改Value避免每次INSERT都重新解析 SQL。整个批次包在一个事务里SQLite 在事务内的写入是顺序落盘的比自动提交快一个数量级。参数说明批次大小我一般设 1050 条太小没意义太大内存占用高且崩溃时丢得多。写入周期可以和采集周期错开比如采集 1 秒一次、每 30 秒批量落一次盘。3.3 历史查询和导出的最小实现查询界面通常给一个时间范围加设备号返回列表绑到DataGridView。这里有个热词里提到的场景——DataGridView把 0/1 显示成 CheckBox温湿度用不上但如果你后面加了「报警状态」字段就会遇到思路是给列设DataGridViewCheckBoxColumn并在CellFormatting里转换。public ListSensorRecord Query(DateTime from, DateTime to, string deviceId) { using var conn new SqliteConnection(_connStr); return conn.QuerySensorRecord( SELECT * FROM SensorData WHERE CollectAt BETWEEN f AND t AND DeviceId d ORDER BY CollectAt DESC, new { f from.ToString(yyyy-MM-dd HH:mm:ss), t to.ToString(yyyy-MM-dd HH:mm:ss), d deviceId }).ToList(); }逻辑说明用 Dapper 的QueryT直接映射到实体省掉手写DataReader。BETWEEN配合索引能走范围扫描。导出 CSV 时注意用Encoding.UTF8带 BOM否则 Excel 打开中文会乱码这是很多人踩过的坑。参数说明时间范围查询一定要带上DeviceId条件否则多设备数据混在一起。如果数据量很大百万级考虑按天分表或定期归档SQLite 单表几百万行查询会明显变慢。4. 避坑与排查那些让上位机半夜崩掉的细节4.1 串口被占用程序重启后打不开现象软件崩溃后重新打开报「拒绝访问 COM3」。原因上一次进程没完全退出串口句柄没释放或者调试时 VS 还挂着旧进程。解决在FormClosing里显式Dispose串口对象并且用try/catch包住调试时先在任务管理器确认没有残留进程。更稳的做法是程序启动时先尝试打开失败就提示用户拔插一次 USB 转 485 模块。4.2 读到的温湿度是 6553.5 这种离谱值现象温度显示 6553.5 或 -1。原因寄存器解析方式不对。有的传感器温度是「有符号 16 位整数 ×10」直接按ushort读负数会变成很大的正数。解决先转short再除 10short raw (short)regs[0];。另外确认寄存器地址有没有偏移文档写 40001 的实际要读地址 0。4.3 界面跑几小时后越来越卡现象内存持续上涨界面刷新变慢。原因定时器里不断 new 对象、事件没解绑、DataGridView每次查询都重新绑定大量行。解决采集队列设上限满了丢最旧的DataGridView用VirtualMode或分页定时器用同一个实例别反复 new。我一般会在界面上放一个「已运行时长 内存占用」的小标签方便现场判断。4.4 数据库文件被锁写入报 database is locked现象多线程同时写 SQLite 报锁。原因SQLite 默认同一时刻只允许一个写。解决所有写入走同一个队列或加锁别在采集线程和 UI 线程同时写。连接字符串里可以加CacheShared和设置busy_timeout但根本上还是要把写入串行化。4.5 断线后数据出现大段空白现象网络或 485 抖动后历史曲线中间断了一截。原因重连期间没补采且没有标记。解决重连成功后记录一次「通信恢复」事件查询时把断档区间标出来别让用户以为是传感器坏了。如果业务要求不能丢就得在设备侧带存储或加本地缓存补传这是另一个话题了。5. 让这套上位机更耐用的几个进阶习惯先说一个具体技巧把「通信质量」做成可观测指标。我在采集类里加了两个计数器——成功次数和失败次数界面上显示成功率。一旦成功率掉到 95% 以下基本就是接线松动或干扰变大比等它彻底断了再排查主动得多。private long _ok, _fail; public double SuccessRate (_ok _fail) 0 ? 1 : (double)_ok / (_ok _fail); // Read() 成功分支里 _okcatch 里 _fail这个改动很小但现场维护时价值很大。配合日志用NLog或简单写文件每次重连都记一条带时间戳的记录事后能还原出通信抖动的规律——比如是不是每天下午某台大设备启动时干扰。再一个是配置外置。串口号、波特率、从站地址、采集周期、数据库路径全部写进一个config.json别硬编码。现场换电脑、换探头时改配置就行不用重新编译。用System.Text.Json反序列化一个配置类几行代码的事。public class AppConfig { public string PortName { get; set; } COM3; public int BaudRate { get; set; } 9600; public byte SlaveId { get; set; } 1; public int CollectIntervalMs { get; set; } 1000; public string DbPath { get; set; } data.db; }最后说验证方法。别只在办公室用调试工具跑通就交付一定要做三件事连续跑 24 小时看内存和数据库增长拔掉 485 线再插上看能不能自动恢复把采集周期调到 100ms 压一压看界面还跟不跟得上。这三关过了才算摸到「工业级」的边。我自己最大的教训是一开始总想着把功能做全结果通信稳定性没打牢现场跑两天就崩返工成本远大于当初多花半天做重连和日志。所以现在我做这类上位机永远是先把「读得到、存得下、断能连」这三件事做扎实再去堆界面和报表。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。