目录前言项目介绍项目技术项目功能项目特点项目代码项目效果项目源码总结前言工业现场的数据采集听起来高大上实际上就是跟串口、协议、数据表打交道。自己动手写一个采集程序以前用C#做WinForms遇到图表展示和数据存储总得找第三方控件要么收费要么功能有坑。.NET 8出来之后生态比之前好多了。依赖注入、通用主机、配置系统这些后端常用的东西现在桌面应用也能直接用。配合SQLite和Dapper做本地存储NModbus处理串口通信一套下来代码量不大但该有的功能都有了。这篇文章分享一个完整的温湿度监控系统实现从硬件通信到界面展示全部跑通代码也开源。项目介绍一个基于.NET 8的Windows Forms桌面应用通过Modbus RTU协议从传感器读取温湿度数据实时展示曲线支持数据存储、智能报警、历史查询和Excel导出。整套系统用分层结构组织依赖注入贯穿全局仓储、服务、通信各层职责清晰。数据库用SQLiteORM用Dapper图表用WinForms自带的DataVisualization没有任何付费组件。适用场景小型冷库、机房、温室大棚、药品存储环境等需要温湿度监控的场所。项目技术技术栈组件库/框架用途运行时.NET基础运行环境UI框架Windows Forms桌面应用界面图表WinForms.DataVisualization温湿度曲线图DI容器Microsoft.Extensions.DependencyInjection依赖注入通用主机Microsoft.Extensions.Hosting应用生命周期管理配置Microsoft.Extensions.Configuration.Jsonappsettings.json加载数据库驱动Microsoft.Data.SqliteSQLite原生访问ORMDapper轻量对象映射Modbus通信NModbus NModbus.SerialModbus 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协议通信Slave1寄存器地址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).GetAppConfig() ?? new AppConfig(); services.AddSingleton(config); services.AddSingletonISensorClient, ModbusRtuClient(); services.AddSingletonSqliteConnectionFactory(); services.AddSingletonDatabaseInitializer(); services.AddScopedISensorDataRepository, SensorDataRepository(); services.AddScopedIAlarmRecordRepository, AlarmRecordRepository(); services.AddScopedIUserRepository, UserRepository(); services.AddScopedIOperationLogRepository, OperationLogRepository(); services.AddScopedIModbusService, ModbusService(); services.AddScopedIAlarmService, AlarmService(); services.AddScopedIUserService, UserService(); services.AddScopedIExportService, ExportService(); services.AddScopedIOperationLogService, OperationLogService(); services.AddScopedLoginForm(); services.AddScopedMainForm(); services.AddScopedHistoryForm(); services.AddScopedSettingsForm(); }) .Build(); var init host.Services.GetRequiredServiceDatabaseInitializer(); init.EnsureDatabaseCreated(); var login host.Services.GetRequiredServiceLoginForm(); if (login.ShowDialog() ! DialogResult.OK) return; Application.Run(host.Services.GetRequiredServiceMainForm()); } }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 ILoggerAlarmService _logger; private readonly Dictionarystring, DateTime _lastAlarmTime new(); private readonly TimeSpan _debounceInterval TimeSpan.FromSeconds(30); public AlarmService(AppConfig config, IAlarmRecordRepository alarmRepo, ILoggerAlarmService 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及以上版本可直接打开解决方案运行。GitHubhttps://github.com/poiuyttt/TempMonitor总结做这个项目最大的感受是.NET 8让WinForms重新焕发了活力。以前WinForms被人诟病的耦合度高、难以测试的问题通过依赖注入和分层架构基本解决了。通用主机的引入让桌面应用也能享受到后台服务同样的配置管理和生命周期控制。Modbus通信这块NModbus库封装得比较完善不用从底层去处理CRC校验和报文拼装专注业务逻辑就好。SQLite Dapper的组合在桌面场景下足够用不用单独装数据库部署时只有一个exe和一个db文件清爽。如果你也在做类似的数据采集项目这个架构可以直接拿过去改改用。硬件侧换成别的协议比如CAN或MQTT只需要替换ISensorClient的实现类上层代码不用动。整套系统的代码量不算大核心逻辑都在没有过度设计刚好够用。对于小型工控项目或实验室环境监控来说比买商业组态软件划算得多。引入地址