目录
前言
项目介绍
项目技术
项目功能
项目特点
项目代码
项目效果
项目源码
总结
前言
工业现场的数据采集,听起来高大上,实际上就是跟串口、协议、数据表打交道。自己动手写一个采集程序,以前用C#做WinForms,遇到图表展示和数据存储,总得找第三方控件,要么收费,要么功能有坑。
.NET 8出来之后,生态比之前好多了。依赖注入、通用主机、配置系统这些后端常用的东西,现在桌面应用也能直接用。配合SQLite和Dapper做本地存储,NModbus处理串口通信,一套下来代码量不大,但该有的功能都有了。
这篇文章分享一个完整的温湿度监控系统实现,从硬件通信到界面展示,全部跑通,代码也开源。
项目介绍
一个基于.NET 8的Windows Forms桌面应用,通过Modbus RTU协议从传感器读取温湿度数据,实时展示曲线,支持数据存储、智能报警、历史查询和Excel导出。
整套系统用分层结构组织,依赖注入贯穿全局,仓储、服务、通信各层职责清晰。数据库用SQLite,ORM用Dapper,图表用WinForms自带的DataVisualization,没有任何付费组件。
适用场景:小型冷库、机房、温室大棚、药品存储环境等需要温湿度监控的场所。
项目技术
技术栈
组件 | 库/框架 | 用途 |
|---|---|---|
运行时 | .NET | 基础运行环境 |
UI框架 | Windows Forms | 桌面应用界面 |
图表 | WinForms.DataVisualization | 温湿度曲线图 |
DI容器 | Microsoft.Extensions.DependencyInjection | 依赖注入 |
通用主机 | Microsoft.Extensions.Hosting | 应用生命周期管理 |
配置 | Microsoft.Extensions.Configuration.Json | appsettings.json加载 |
数据库驱动 | Microsoft.Data.Sqlite | SQLite原生访问 |
ORM | Dapper | 轻量对象映射 |
Modbus通信 | NModbus + NModbus.Serial | Modbus RTU协议 |
Excel导出 | NPOI | .xlsx导出 |
日志 | NLog | 分级日志与文件轮转 |
测试 | xUnit + Moq | 单元测试 |
项目结构
│ ├── Configuration/ │ │ └── AppConfig.cs # 配置POCO │ ├── Models/ │ │ ├── SensorData.cs # 温湿度记录 │ │ ├── AlarmRecord.cs # 报警记录 │ │ ├── UserInfo.cs # 用户信息 │ │ └── OperationLog.cs # 操作日志 │ ├── Data/ │ │ ├── SqliteConnectionFactory.cs # 连接工厂 │ │ ├── SqliteDateTimeTypeHandler.cs │ │ ├── DatabaseInitializer.cs # 首次建库建表 │ │ └── Repositories/ # 仓储实现 │ ├── Communication/ │ │ ├── Interfaces/ISensorClient.cs │ │ └── ModbusRtuClient.cs │ ├── Services/ │ │ ├── ModbusService.cs # 采集引擎 │ │ ├── AlarmService.cs # 阈值判定 │ │ ├── UserService.cs # 登录验证 │ │ ├── ExportService.cs # Excel导出 │ │ └── OperationLogService.cs │ ├── Presentation/ │ │ ├── LoginForm.cs │ │ ├── MainForm.cs │ │ ├── HistoryForm.cs │ │ └── SettingsForm.cs │ ├── Program.cs │ ├── appsettings.json │ └── nlog.config ├── TempMonitor.Tests/ # 测试项目 └── TempMonitor.slnx项目功能
实时采集
Modbus RTU协议通信,Slave=1,寄存器地址0x0300
原始值除以10转换为实际温湿度
采集间隔可配置,默认1000ms
可视化图表
双Y轴显示,温度左侧,湿度右侧
X轴为真实时间戳
超过120个数据点自动滚动
智能报警
四个独立阈值:温度上下限、湿度上下限
30秒同类型防抖,避免报警刷屏
按阈值偏离程度分级:Warning和Critical
数据管理
SQLite本地持久化,WAL模式提升并发性能
按时间范围查询历史数据
NPOI导出Excel,三列:时间、温度、湿度
定期清理过期数据,保留天数可配置
系统管理
PBKDF2加盐哈希存储密码
首次启动自动创建admin账号,随机密码写入日志
Admin/User双角色,非Admin隐藏管理功能
关键操作记录审计日志
串口保护
应用退出自动释放COM口
采集失败自动重连,10秒限频
连续失败触发断连告警
项目特点
分层架构:展示层、服务层、通信层、数据层各司其职,接口与实现分离
依赖注入贯穿全局:所有服务通过构造函数注入,单元测试友好
生产者-消费者采集模型:Timer定时生产数据,ConcurrentQueue解耦消费,防止积压
看门狗自恢复:通信异常时自动重连,无需人工干预
安全性:密码哈希存储,时序比较防时序攻击
零商业组件:全部使用.NET开源生态,无授权费用
项目代码
主程序入口,配置通用主机和依赖注入:
internal static class Program { [STAThread] static void Main() { ApplicationConfiguration.Initialize(); var host = Host.CreateDefaultBuilder() .ConfigureAppConfiguration((context, config) => { config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); }) .ConfigureServices((context, services) => { var config = context.Configuration.GetSection("AppConfig").Get<AppConfig>() ?? new AppConfig(); services.AddSingleton(config); services.AddSingleton<ISensorClient, ModbusRtuClient>(); services.AddSingleton<SqliteConnectionFactory>(); services.AddSingleton<DatabaseInitializer>(); services.AddScoped<ISensorDataRepository, SensorDataRepository>(); services.AddScoped<IAlarmRecordRepository, AlarmRecordRepository>(); services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IOperationLogRepository, OperationLogRepository>(); services.AddScoped<IModbusService, ModbusService>(); services.AddScoped<IAlarmService, AlarmService>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IExportService, ExportService>(); services.AddScoped<IOperationLogService, OperationLogService>(); services.AddScoped<LoginForm>(); services.AddScoped<MainForm>(); services.AddScoped<HistoryForm>(); services.AddScoped<SettingsForm>(); }) .Build(); var init = host.Services.GetRequiredService<DatabaseInitializer>(); init.EnsureDatabaseCreated(); var login = host.Services.GetRequiredService<LoginForm>(); if (login.ShowDialog() != DialogResult.OK) return; Application.Run(host.Services.GetRequiredService<MainForm>()); } }Modbus通信封装,读取温湿度寄存器:
public class ModbusRtuClient : ISensorClient, IDisposable { private SerialPort? _serialPort; private IModbusMaster? _master; private readonly AppConfig _config; private readonly object _lock = new(); private bool _disposed; public ModbusRtuClient(AppConfig config) { _config = config; } public void Connect() { lock (_lock) { if (_master != null) return; _serialPort = new SerialPort( _config.SerialPort.PortName, _config.SerialPort.BaudRate, Parity.None, 8, StopBits.One ); _serialPort.ReadTimeout = _config.SerialPort.ReadTimeout; _serialPort.WriteTimeout = _config.SerialPort.WriteTimeout; _serialPort.Open(); _master = ModbusSerialMaster.CreateRtu(_serialPort); } } public async Task<(double Temperature, double Humidity)> ReadSensorDataAsync() { const byte slaveId = 1; const ushort registerAddress = 0x0300; const ushort registerCount = 2; if (_master == null) throw new InvalidOperationException("Modbus master not connected"); var result = await _master.ReadHoldingRegistersAsync(slaveId, registerAddress, registerCount); var tempRaw = result[0]; var humidityRaw = result[1]; return (tempRaw / 10.0, humidityRaw / 10.0); } public void Dispose() { if (_disposed) return; lock (_lock) { _master?.Dispose(); _serialPort?.Dispose(); _master = null; _serialPort = null; } _disposed = true; } }报警服务阈值判定逻辑:
public class AlarmService : IAlarmService { private readonly AppConfig _config; private readonly IAlarmRecordRepository _alarmRepo; private readonly ILogger<AlarmService> _logger; private readonly Dictionary<string, DateTime> _lastAlarmTime = new(); private readonly TimeSpan _debounceInterval = TimeSpan.FromSeconds(30); public AlarmService(AppConfig config, IAlarmRecordRepository alarmRepo, ILogger<AlarmService> logger) { _config = config; _alarmRepo = alarmRepo; _logger = logger; } public async Task CheckAlarmsAsync(SensorData data) { var checks = new List<(string Type, double Value, double Threshold, string Level)> { ("TempHigh", data.Temperature, _config.Alarm.TempHigh, "Critical"), ("TempLow", data.Temperature, _config.Alarm.TempLow, "Critical"), ("HumidHigh", data.Humidity, _config.Alarm.HumidityHigh, "Critical"), ("HumidLow", data.Humidity, _config.Alarm.HumidityLow, "Critical") }; foreach (var check in checks) { bool triggered = check.Type.Contains("High") ? check.Value > check.Threshold : check.Value < check.Threshold; if (!triggered) continue; if (!_lastAlarmTime.TryGetValue(check.Type, out var last) || (DateTime.Now - last) > _debounceInterval) { var level = CheckLevel(check.Value, check.Threshold); var record = new AlarmRecord { AlarmType = check.Type, AlarmValue = check.Value, Level = level, RecordTime = DateTime.Now, Confirmed = false }; await _alarmRepo.AddAsync(record); _lastAlarmTime[check.Type] = DateTime.Now; _logger.Warn($"Alarm triggered: {check.Type} = {check.Value}, level: {level}"); } } } private string CheckLevel(double value, double threshold) { var delta = Math.Abs(value - threshold); var range = Math.Abs(threshold * 0.2); return delta > range ? "Critical" : "Warning"; } }项目效果
主界面左侧为实时数值显示,右侧为Chart曲线。温度用红色线条,湿度用蓝色线条,双Y轴刻度清晰。点击「开始采集」后,数据每秒钟更新一次,曲线平滑滚动。
实时数值面板显示当前温度、湿度、采集状态和最近一次报警信息。状态指示灯绿色表示正常,红色表示通讯异常或触发报警。
历史查询界面按时间段筛选数据,查询结果以表格形式呈现。导出功能一键生成.xlsx文件,包含时间、温度、湿度三列,方便做日报周报。
登录界面简洁,首次启动自动创建管理员账号,随机密码打印在日志文件中,既安全又不增加配置负担。
设置界面可修改串口号、波特率、采集间隔和四个报警阈值,修改后实时生效,无需重启应用。
项目源码
仓库包含完整的源代码、SQLite建表脚本、测试项目和编译好的Release包。Visual Studio 2022及以上版本可直接打开解决方案运行。
GitHub:https://github.com/poiuyttt/TempMonitor
总结
做这个项目最大的感受是,.NET 8让WinForms重新焕发了活力。以前WinForms被人诟病的耦合度高、难以测试的问题,通过依赖注入和分层架构基本解决了。通用主机的引入让桌面应用也能享受到后台服务同样的配置管理和生命周期控制。
Modbus通信这块,NModbus库封装得比较完善,不用从底层去处理CRC校验和报文拼装,专注业务逻辑就好。SQLite + Dapper的组合在桌面场景下足够用,不用单独装数据库,部署时只有一个exe和一个db文件,清爽。
如果你也在做类似的数据采集项目,这个架构可以直接拿过去改改用。硬件侧换成别的协议,比如CAN或MQTT,只需要替换ISensorClient的实现类,上层代码不用动。
整套系统的代码量不算大,核心逻辑都在,没有过度设计,刚好够用。对于小型工控项目或实验室环境监控来说,比买商业组态软件划算得多。
引入地址