做过工业采集和边缘物联网项目的朋友,应该都懂多传感器接入的糟心。
这边STM32通过串口发温湿度,那边ESP32连WiFi走MQTT传环境数据,还有USB-CDC的ADC设备一路吐高频采样值——接口不统一,协议各玩各的,连数据格式都千差万别。
很多项目写到最后,采集层的代码堆得乱七八糟,接收、解析、存库、刷界面全揉在一块,数据量稍微一大,UI直接卡死,排查问题都不知道从哪下手。更别说还要考虑时序数据持久化、本地备份、跨平台部署这些事。
最近在做边缘计算节点的项目,基于C#搭了一套多协议传感器数据采集平台,把串口、WiFi、USB三类传感器统一接入,用事件驱动模式彻底拆解开数据采集、业务处理和UI展示,再配上InfluxDB做时序存储、LiveCharts跑实时曲线,跑了几个月稳定性和性能都在线。
这篇就把完整的设计思路和核心实现拆出来,都是实际项目踩坑踩出来的干货。
一、技术选型:为什么选这一套组合?
选型的核心思路很简单:够用、稳定、开发效率高,不搞花里胡哨的技术堆砌。
- C# + .NET:开发效率高,类型安全,支持桌面、控制台、服务多种部署形态。.NET 6+原生跨平台,Windows工业电脑和Linux边缘网关都能跑,一套代码复用。
- MQTTnet:.NET生态最成熟的MQTT库,轻量高性能,客户端和服务端都支持。本地做Broker或者对接外部MQTT服务器都方便,比自己手写Socket稳定太多。
- LiveCharts:专为.NET设计的图表库,数据绑定简单,支持实时滚动更新。不用自己啃GDI+画曲线,几行代码就能出专业的实时波形。
- InfluxDB:时序数据库首选,针对带时间戳的传感器数据做了深度优化,写入和查询性能甩关系型数据库几条街,非常适合海量历史数据存储。
- SQLite:嵌入式轻量数据库,不用额外装服务,随程序走。作为本地备份兜底,断网或者InfluxDB异常的时候保证数据不丢失。
二、整体架构:事件驱动,四层解耦
整个平台的核心设计思想就是「职责分离,事件驱动」,把原来拧成一团的逻辑拆成四层,层与层之间只通过事件传递数据,不做直接调用。
从上到下分别是:
- 采集接入层:对接各类硬件接口,负责原始数据接收和协议解析,统一转换成标准数据模型后抛出事件。
- 数据总线层:全局事件中枢,接收所有采集器的数据,分发给下游订阅者,是解耦的核心。
- 业务处理层:包含可视化展示和持久化存储,各自订阅数据总线,按需处理数据,互不干扰。
- 存储层:时序数据主存InfluxDB + 本地备份SQLite,双写保证数据可靠性。
这种架构最大的好处就是,采集线程永远不会因为刷UI、存数据库被阻塞,UI也不会因为采集量大而卡顿。新增传感器或者新增业务逻辑,只需要加对应的模块,不用动原有代码,扩展性极强。
三、核心模块实现细节
1. 统一数据模型:先把格式对齐
不管什么接口、什么协议的传感器,上来之后先转成统一的数据结构,后面所有层都只认这个模型,彻底屏蔽底层硬件差异。
public class SensorData { /// <summary> /// 设备唯一标识 /// </summary> public string DeviceId { get; set; } /// <summary> /// 测量类型:Temperature/Humidity/Voltage等 /// </summary> public string MeasureType { get; set; } /// <summary> /// 采样数值 /// </summary> public double Value { get; set; } /// <summary> /// 采集时间戳 /// </summary> public DateTime Timestamp { get; set; } = DateTime.Now; }2. 多协议采集层:三类传感器接入实现
(1)串口采集(STM32温湿度)
串口是最常用也最容易出问题的接口,核心坑点是粘包和分包。这里采用事件驱动接收,按自定义帧协议解析,解析完整帧再抛出数据。
public class SerialPortCollector { private readonly SerialPort _serialPort = new SerialPort(); public event Action<SensorData> OnDataReceived; public void Start(string portName, int baudRate) { _serialPort.PortName = portName; _serialPort.BaudRate = baudRate; _serialPort.DataReceived += SerialPort_DataReceived; _serialPort.Open(); } private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { int count = _serialPort.BytesToRead; byte[] buffer = new byte[count]; _serialPort.Read(buffer, 0, count); // 按帧头帧尾协议解析,处理粘包分包 if (!TryParseFrame(buffer, out double temp, out double humi)) return; // 解析完成后抛出事件 OnDataReceived?.Invoke(new SensorData { DeviceId = "STM32_01", MeasureType = "Temperature", Value = temp }); } }(2)WiFi传感器(ESP32 + MQTT)
WiFi类传感器一般走MQTT协议,用MQTTnet客户端订阅主题,解析JSON报文即可。重点是要做断线自动重连,生产环境网络波动很常见。
public class MqttSensorCollector { private readonly IMqttClient _mqttClient; public event Action<SensorData> OnDataReceived; public async Task Start(string brokerIp, int port, string topic) { var factory = new MqttFactory(); _mqttClient = factory.CreateMqttClient(); // 接收消息处理 _mqttClient.ApplicationMessageReceivedAsync += e => { string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); var data = JsonSerializer.Deserialize<SensorData>(payload); OnDataReceived?.Invoke(data); return Task.CompletedTask; }; // 断线重连 _mqttClient.DisconnectedAsync += async e => { await Task.Delay(3000); await _mqttClient.ReconnectAsync(); }; // 连接并订阅 await _mqttClient.ConnectAsync(new MqttClientOptionsBuilder() .WithTcpServer(brokerIp, port) .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) .Build()); await _mqttClient.SubscribeAsync(topic); } }(3)USB-CDC 高速ADC采集
USB-CDC本质是虚拟串口,但采样频率高、数据量大,不适合每条数据都抛事件。一般做法是在内置缓冲区攒一批数据,解析完成后批量抛出,减少事件触发开销。
3. 数据总线:全局事件中枢
用一个静态类做全局事件总线,所有采集器把数据发到这里,所有消费者从这里订阅。采集端和消费端完全不知道对方存在,彻底解耦。
public static class DataBus { /// <summary> /// 数据发布事件 /// </summary> public static event Action<SensorData> DataPublished; /// <summary> /// 发布数据 /// </summary> public static void Publish(SensorData data) { DataPublished?.Invoke(data); } /// <summary> /// 订阅数据 /// </summary> public static void Subscribe(Action<SensorData> handler) { DataPublished += handler; } } // 采集器注册:所有采集器的数据都汇入总线 serialCollector.OnDataReceived += DataBus.Publish; mqttCollector.OnDataReceived += DataBus.Publish; usbCollector.OnDataReceived += DataBus.Publish;4. 实时可视化:LiveCharts 动态滚动曲线
UI层用LiveCharts做实时曲线,两个核心优化点:一是限制数据点数量实现自动滚动,二是X轴格式化为真实时间。
很多人用LiveCharts越用越卡,大多是因为数据点只加不删,或者每次加数据都强制全量刷新。
// 可观测集合,绑定到曲线控件 public ObservableCollection<MeasurePoint> TemperaturePoints { get; set; } = new(); // 订阅数据总线 DataBus.Subscribe(data => { if (data.MeasureType != "Temperature") return; // 切回UI线程更新 this.BeginInvoke(() => { TemperaturePoints.Add(new MeasurePoint { Time = data.Timestamp, Value = data.Value }); // 保留最近300个点,超出自动移除,实现滚动效果 if (TemperaturePoints.Count > 300) TemperaturePoints.RemoveAt(0); }); }); // X轴配置为时间格式 AxisX = new Axis { LabelFormatter = v => DateTime.FromOADate(v).ToString("HH:mm:ss"), Separator = new Separator { Step = TimeSpan.FromSeconds(10).ToOADate() } };5. 持久化层:InfluxDB + SQLite 双存储策略
传感器数据是典型的时序数据,InfluxDB写入查询效率最高,但不能只存一份。采用「主存+本地备份」双写策略,保证数据不丢。
InfluxDB 批量写入
绝对不要来一条写一条,单条写入开销极大。用队列攒数据,定时批量写入,性能能提升几十倍。
public class InfluxDbWriter { private readonly Queue<SensorData> _queue = new Queue<SensorData>(); private readonly Timer _flushTimer; public InfluxDbWriter() { // 1秒刷新一次,批量写入 _flushTimer = new Timer(FlushBatch, null, 1000, 1000); } public void Write(SensorData data) { lock (_queue) { _queue.Enqueue(data); } } private void FlushBatch(object state) { SensorData[] batch; lock (_queue) { batch = _queue.ToArray(); _queue.Clear(); } if (batch.Length == 0) return; // 调用InfluxDB.Client批量写入 // 具体写入代码省略 } }SQLite 本地备份
同时异步写入本地SQLite数据库,作为兜底。SQLite无需额外服务,文件型数据库,非常适合边缘设备本地存储。写入策略同样是批量提交,降低磁盘IO。
四、生产环境踩坑与性能优化
1. UI线程阻塞问题
这是新手最容易踩的坑:直接在采集线程里操作控件,或者在数据接收事件里做耗时操作。
事件驱动架构本身已经把采集和UI分开了,只要记住所有UI更新都通过BeginInvoke切到UI线程,且只做最少的界面操作,基本不会出现界面卡死。
2. 高频采集的内存控制
ADC这类每秒上千条的高频数据,不要每条都触发事件,先在采集层做降采样或者批量打包再上报。
曲线展示不要无限累加数据点,保留最近一段时间的即可,否则内存会持续上涨。
3. MQTT断线重连
MQTTnet默认不会自动重连,网络一波动就永久断开。一定要监听Disconnected事件,做指数退避重连,生产环境必备。
4. InfluxDB写入优化
- 必须批量写入,建议每批100~1000条
- Tag只放索引字段(设备ID、测量类型),数值全部放Field
- 不要频繁建表,相同类型数据共用一个Measurement
5. 跨平台部署注意事项
- Linux下串口路径是/dev/ttyUSB0这种格式,要做配置化适配
- Linux串口需要用户权限,把运行用户加到dialout组
- 边缘网关可以用控制台程序+Systemd托管,开机自启
五、部署方案:适配边缘计算节点
这套平台支持两种部署模式,代码基本不用改:
- 本地监控模式:采集、存储、UI都在一台Windows工业电脑上,用WinForms/WPF做界面,适合车间、机房本地监控场景。
- 边缘网关模式:采集+本地存储部署在Linux边缘节点,无UI,以后台服务运行,数据定时同步到云端,适合分布式多点采集。
因为基于.NET跨平台框架,两种模式只需要改配置文件,核心代码完全复用,部署非常灵活。
六、总结
这套方案不是什么高大上的架构,但胜在简单、实用、踩过坑。对于绝大多数工业传感器采集、环境监测、设备状态监控的场景,完全够用,开发效率比从零搓接口、画控件高太多。
如果你也在做多协议传感器接入,被UI卡顿、代码耦合、数据存储混乱的问题困扰,非常推荐试试事件驱动+时序数据库的思路。不用搞复杂的中间件,几行代码就能把架构理清楚。