温湿度监控系统:C# + Modbus RTU 实现车间环境数据实时采集与曲线分析

温湿度监控系统:C# + Modbus RTU 实现车间环境数据实时采集与曲线分析 车间环境温湿度是生产质量的核心影响因素电子、制药、食品加工等场景下温湿度超标会直接导致良品率下降、原料失效。传统人工巡检效率低、数据滞后无法做到实时预警与趋势分析。基于Modbus RTU的总线式采集方案布线简单、抗干扰能力强配合C#上位机实现数据可视化与曲线分析是中小型车间监控的高性价比落地方案。本文从硬件接线、协议封装、采集服务到曲线展示完整拆解一套可直接复用的工程实现包含实际项目中踩过的硬件坑、通信坑与性能优化点。方案架构与技术选型整体采用三层分层架构感知层通过RS485总线挂载多台温湿度变送器采集层负责Modbus协议解析与数据校验应用层实现数据展示、曲线分析与告警管理各层职责解耦便于扩展多类型传感器与上层系统对接。核心技术选型硬件RS485接口温湿度变送器支持Modbus RTU、屏蔽双绞线、USB转RS485模块UI框架WPF CommunityToolkit.Mvvm遵循MVVM分层架构保证可维护性通信协议栈NModbus工业级成熟Modbus库原生支持RTU/TCP模式内置CRC校验曲线控件ScottPlot.WPF高性能轻量绘图库实时滚动无卡顿适合工业高频数据场景数据存储SQLite轻量本地数据库无需额外部署服务支持历史数据快速查询Modbus RTU 通信层封装Modbus RTU是工业现场最通用的串行总线协议通过RS485物理层可实现最远1200米传输一条总线最多挂载254台设备。通信层的核心是屏蔽底层协议细节向上提供统一的寄存器读取接口。协议参数与寄存器映射工业级温湿度变送器默认参数通常统一实际开发时需对照设备手册确认从站地址默认1可通过配置软件修改串口参数波特率9600、数据位8、停止位1、无校验温度寄存器40001保持寄存器量程-40~125℃分辨率0.1℃有符号数湿度寄存器40002保持寄存器量程0~100%RH分辨率0.1%RH无符号数踩坑提示Modbus寄存器地址分为逻辑地址与偏移地址手册上的40001是逻辑地址实际通信时偏移量从0开始即40001对应地址0地址偏移1位就会读错数据。通信服务接口与实现封装独立的Modbus RTU服务包含连接管理、寄存器读取、异常重试能力上层业务无需关心协议细节。public interface IModbusRtuService : IDisposable { bool IsConnected { get; } bool Connect(string portName, int baudRate 9600, int dataBits 8, Parity parity Parity.None, StopBits stopBits StopBits.One); void Disconnect(); ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count); }public class ModbusRtuService : IModbusRtuService { private SerialPort _serialPort; private IModbusMaster _modbusMaster; public bool IsConnected _serialPort?.IsOpen ?? false; public bool Connect(string portName, int baudRate, int dataBits, Parity parity, StopBits stopBits) { try { _serialPort new SerialPort(portName, baudRate, parity, dataBits, stopBits) { ReadTimeout 1000, WriteTimeout 1000 }; _serialPort.Open(); _modbusMaster ModbusSerialMaster.CreateRtu(_serialPort); _modbusMaster.Transport.Retries 3; _modbusMaster.Transport.WaitToRetryMilliseconds 20; return true; } catch (Exception ex) { Disconnect(); throw new InvalidOperationException($Modbus RTU连接失败{ex.Message}, ex); } } public ushort[] ReadHoldingRegisters(byte slaveId, ushort startAddress, ushort count) { if (!IsConnected) throw new InvalidOperationException(Modbus设备未连接); return _modbusMaster.ReadHoldingRegisters(slaveId, startAddress, count); } public void Disconnect() { _modbusMaster?.Dispose(); if (_serialPort ! null) { if (_serialPort.IsOpen) _serialPort.Close(); _serialPort.Dispose(); } } public void Dispose() Disconnect(); }数据采集服务设计采集服务是系统的核心负责定时轮询设备、数据解析校验、异常处理与状态上报采用事件驱动模式解耦采集逻辑与业务逻辑。采集流程设计采用定时轮询机制单总线多节点依次读取避免总线冲突加入数据校验与异常重试保证数据可靠性。采集服务核心实现轮询周期默认1秒可根据场景配置内置滑动平均滤波消除现场电磁干扰导致的数据抖动异常事件向上抛出由UI层统一处理。public class TemperatureHumidityCollector : IDisposable { private readonly IModbusRtuService _modbusService; private readonly System.Timers.Timer _collectTimer; private readonly Queuefloat _tempFilterQueue new Queuefloat(5); private readonly Queuefloat _humidFilterQueue new Queuefloat(5); private const int DefaultPollInterval 1000; public event EventHandlerSensorData DataReceived; public event EventHandlerstring ErrorOccurred; public TemperatureHumidityCollector(IModbusRtuService modbusService) { _modbusService modbusService; _collectTimer new System.Timers.Timer(DefaultPollInterval); _collectTimer.Elapsed OnCollectTimerElapsed; } public void Start() { if (!_modbusService.IsConnected) throw new InvalidOperationException(请先建立Modbus连接); _collectTimer.Start(); } public void Stop() _collectTimer.Stop(); private void OnCollectTimerElapsed(object sender, ElapsedEventArgs e) { try { // 读取从站1的2个保持寄存器温度湿度 var registers _modbusService.ReadHoldingRegisters( slaveId: 1, startAddress: 0, count: 2); // 量程转换分辨率0.1温度为有符号数 float rawTemp (short)registers[0] * 0.1f; float rawHumid registers[1] * 0.1f; // 合法性校验超出物理量程直接丢弃 if (rawTemp -40 || rawTemp 125 || rawHumid 0 || rawHumid 100) { ErrorOccurred?.Invoke(this, 数据超出量程已丢弃); return; } // 滑动平均滤波消除干扰抖动 float temperature FilterValue(rawTemp, _tempFilterQueue, 5); float humidity FilterValue(rawHumid, _humidFilterQueue, 5); DataReceived?.Invoke(this, new SensorData { Temperature temperature, Humidity humidity, Timestamp DateTime.Now }); } catch (Exception ex) { ErrorOccurred?.Invoke(this, $采集异常{ex.Message}); } } private float FilterValue(float value, Queuefloat queue, int windowSize) { queue.Enqueue(value); if (queue.Count windowSize) queue.Dequeue(); return queue.Average(); } public void Dispose() { _collectTimer?.Stop(); _collectTimer?.Dispose(); _modbusService?.Dispose(); } }MVVM 层与实时曲线实现基于MVVM模式实现UI与业务逻辑分离使用ScottPlot实现高性能实时滚动曲线保留最近30分钟数据支持缩放、拖拽查看。ViewModel 状态管理通过ObservableObject实现属性通知通过RelayCommand绑定界面操作采集事件通过Dispatcher调度到UI线程更新避免跨线程操作异常。public partial class MainViewModel : ObservableObject { private readonly TemperatureHumidityCollector _collector; private readonly ListSensorData _historyData new ListSensorData(); private const int MaxHistoryPoints 1800; // 1秒1点保留30分钟 [ObservableProperty] private float _currentTemperature; [ObservableProperty] private float _currentHumidity; [ObservableProperty] private bool _isCollecting; [ObservableProperty] private string _statusMessage 未连接; public IRelayCommand ConnectCommand { get; } public IRelayCommand StartCollectCommand { get; } public IRelayCommand StopCollectCommand { get; } public Plot ChartPlot { get; } new Plot(); public MainViewModel(TemperatureHumidityCollector collector) { _collector collector; _collector.DataReceived OnDataReceived; _collector.ErrorOccurred OnErrorOccurred; ConnectCommand new RelayCommand(ExecuteConnect); StartCollectCommand new RelayCommand(ExecuteStart, () !IsCollecting); StopCollectCommand new RelayCommand(ExecuteStop, () IsCollecting); InitChart(); } private void InitChart() { ChartPlot.Title(车间温湿度实时趋势); ChartPlot.XLabel(时间); ChartPlot.YLabel(数值); ChartPlot.Legend(Alignment.UpperRight); ChartPlot.XAxis.DateTimeFormat(true); } private void OnDataReceived(object sender, SensorData data) { CurrentTemperature data.Temperature; CurrentHumidity data.Humidity; lock (_historyData) { _historyData.Add(data); if (_historyData.Count MaxHistoryPoints) _historyData.RemoveAt(0); } // 后台优先级更新曲线避免阻塞UI Application.Current.Dispatcher.InvokeAsync( RefreshChart, DispatcherPriority.Background); } private void RefreshChart() { lock (_historyData) { double[] times _historyData.Select(d d.Timestamp.ToOADate()).ToArray(); double[] temps _historyData.Select(d (double)d.Temperature).ToArray(); double[] humids _historyData.Select(d (double)d.Humidity).ToArray(); ChartPlot.Clear(); ChartPlot.AddScatter(times, temps, Color.Red, 2, label: 温度(℃)); ChartPlot.AddScatter(times, humids, Color.Blue, 2, label: 湿度(%RH)); } } }曲线性能优化工业场景长时间运行容易出现曲线卡顿核心优化点有三个一是限制历史数据点数量超出后移除旧点二是降低UI刷新频率采集和渲染分离比如采集1秒一次曲线每2秒刷新一次三是复用数据数组避免频繁创建大对象导致内存碎片。告警与历史数据分析阈值告警机制支持配置温湿度上下限阈值数据超限时触发告警可扩展声光报警器、PLC联动、消息推送等输出方式。告警事件支持防抖避免数据波动导致的频繁告警。历史数据查询使用SQLite存储分钟级聚合数据支持按时间范围查询并生成历史曲线可用于追溯环境变化、分析质量问题与环境的关联。数据保留策略可配置原始数据保留7天小时级统计数据保留3个月降低存储压力。常见问题排查与避坑1. 通信完全失败优先排查硬件RS485的A、B线是否接反总线两端是否接120Ω终端电阻USB转485模块驱动是否正常再检查软件参数串口号、波特率、校验位、从站地址是否与设备一致。2. 数据跳变与乱码大概率是现场电磁干扰远离变频器、电机等强电设备使用屏蔽双绞线且屏蔽层单端接地软件层面增加滑动平均滤波、异常值剔除严重时可降低波特率提升稳定性。3. 多节点采集冲突同一总线上多个设备不能同时通信必须采用轮询机制节点之间预留10ms以上间隔避免总线数据冲突节点数量较多时可适当延长轮询周期。4. 串口资源占用程序异常退出后串口未释放下次启动提示“端口已被占用”。一定要在Dispose方法和全局异常处理中加入串口关闭逻辑必要时通过系统进程强制释放端口。5. 长时间运行内存上涨注意释放非托管资源及时清理历史数据集合ScottPlot避免每次刷新都重新创建图层复用绘图对象数据库操作使用using保证连接及时释放。工程化落地建议断线自动重连通信失败后采用指数退避策略自动重试短时间内多次失败则延长重试间隔避免频繁占用总线恢复后自动继续采集。数据分级存储高频原始数据短期留存聚合数据长期保存兼顾查询性能与存储成本。日志分级记录通信日志、数据日志、告警日志分开存储异常时可快速定位问题环节。多类型传感器扩展基于Modbus接口抽象可无缝接入压力、液位、光照等其他类型传感器只需新增寄存器映射与解析逻辑。上位机联动可通过OPC UA、Modbus TCP等协议对接PLC或MES系统实现温湿度超标自动触发空调、除湿机等设备调节。总结这套方案硬件成本低、部署快、稳定性高非常适合中小型车间的环境监控场景。Modbus RTU的总线通用性配合C#上位机的灵活开发能力既能满足实时采集的要求又能通过曲线分析掌握环境变化趋势为生产质量管控提供数据支撑。实际项目中可根据现场规模与业务需求灵活扩展测点数量、传感器类型与上层系统对接能力。