【C#避坑实战系列文章15】C# WinForm 上位机开发:解决串口粘包+LiveCharts卡顿+InfluxDB存储(免费代码+仿真工具)

阅读前提醒,本文完整工程已上传CSDN资源库 (0积分下载!点个关注呗),包含所有源码、测试代码与详细注释,可直接转到资源库下载测试。

在工业自动化、物联网设备监控等场景中,上位机作为数据采集与可视化的核心工具,承担着连接硬件设备与数据平台的重要角色。本文将通过实战案例,详细讲解如何使用 C# WinForm 开发一套完整的上位机系统,实现从串口数据采集、实时曲线展示,到本地 SQLite 存储与 InfluxDB 时序库持久化的全流程,并提供完整代码与部署指南。

文章目录

    • 一、项目概述与环境搭建
      • [1.1 项目功能清单](#1.1 项目功能清单)
      • [1.2 开发环境准备](#1.2 开发环境准备)
        • [1.2.1 基础工具](#1.2.1 基础工具)
        • [1.2.2 依赖库安装](#1.2.2 依赖库安装)
      • [1.3 项目架构](#1.3 项目架构)
      • [1.4 核心功能与关键点](#1.4 核心功能与关键点)
    • 二、核心功能实现详解
      • [2.1 串口数据采集模块(SerialReader)](#2.1 串口数据采集模块(SerialReader))
        • [2.1.1 核心代码实现](#2.1.1 核心代码实现)
        • [2.1.2 关键逻辑说明](#2.1.2 关键逻辑说明)
      • [2.2 实时曲线展示(基于LiveCharts)](#2.2 实时曲线展示(基于LiveCharts))
        • [2.2.1 图表初始化(MainForm)](#2.2.1 图表初始化(MainForm))
        • [2.2.2 动态更新曲线](#2.2.2 动态更新曲线)
      • [2.3 本地数据持久化(SQLite)](#2.3 本地数据持久化(SQLite))
        • [2.3.1 数据库初始化(LocalStorage)](#2.3.1 数据库初始化(LocalStorage))
        • [2.3.2 数据插入实现](#2.3.2 数据插入实现)
      • [2.4 时序数据库集成(InfluxDB)](#2.4 时序数据库集成(InfluxDB))
        • [2.4.1 InfluxDB客户端初始化](#2.4.1 InfluxDB客户端初始化)
        • [2.4.2 数据写入实现](#2.4.2 数据写入实现)
    • 三、界面设计与交互逻辑(MainForm)
      • [3.1 界面布局](#3.1 界面布局)
      • [3.2 核心交互逻辑](#3.2 核心交互逻辑)
    • 四、代码调试与常见问题解决
      • [4.1 调试准备](#4.1 调试准备)
      • [4.2 常见问题解决](#4.2 常见问题解决)
    • 五、测试效果展示
      • [5.1 运行界面展示](#5.1 运行界面展示)
      • [5.2 数据存储验证](#5.2 数据存储验证)
    • 六、部署选项
      • [6.1 生成可执行文件](#6.1 生成可执行文件)
      • [6.2 环境依赖打包](#6.2 环境依赖打包)
      • [6.3 运行环境要求](#6.3 运行环境要求)
    • 七、工程下载与结语

一、项目概述与环境搭建

1.1 项目功能清单

本上位机系统实现以下核心功能:

  • 串口通信:支持动态选择串口、波特率,实时接收传感器数据
  • 数据解析:基于自定义协议解析串口数据(格式:SENSOR,CHx,时间戳,数值
  • 实时可视化:通过折线图动态展示多通道传感器数据
  • 数据持久化:
    • 本地存储:使用 SQLite 保存历史数据
    • 时序存储:集成 InfluxDB 实现高吞吐时序数据存储
  • 状态监控:实时显示连接状态、数据日志与错误信息

1.2 开发环境准备

1.2.1 基础工具
  • 开发IDE:Visual Studio 2019/2022(社区版免费)
  • 框架:.NET Framework 4.8(兼容多数Windows环境)
  • 版本控制:Git(可选,用于代码管理)
1.2.2 依赖库安装

通过 NuGet 包管理器安装以下依赖:

  • LiveCharts.WinForms:用于实时曲线绘制(版本 0.9.7 及以上)
  • Microsoft.Data.Sqlite:SQLite 数据库操作(版本 6.0.0 及以上)
  • SQLitePCL.raw.bundle_e_sqlite3:SQLite 原生依赖(解决跨平台兼容问题)

1.3 项目架构

SerialMonitor/

├─ SerialMonitor.sln

├─ SerialMonitor/ // WinForms 应用

│ ├─ Program.cs

│ ├─ MainForm.cs

│ ├─ MainForm.Designer.cs

│ ├─ SerialReader.cs

│ ├─ DataPoint.cs

│ ├─ InfluxClient.cs

│ └─ LocalStorage.cs // SQLite wrapper

├─ scripts/

│ └─ docker-compose.yml // 启动 InfluxDB + Grafana(可选)

└─ README.md

1.4 核心功能与关键点

**串口读取:**使用 SerialPort.DataReceived 事件或专用后台线程 + BaseStream.ReadAsync 来读取,注意粘包/断包与编码(ASCII/二进制)问题。

**解析协议:**定义简单文本协议(行结束 \n),或实现帧头+长度二进制解析。演示使用文本协议 SENSOR,CH1,123.45\n。

线程安全更新 UI:WinForms 的控件只能在 UI 线程操作,需用 BeginInvoke/Invoke。

**实时绘图:**用 LiveCharts的 WinForms 控件,动态 AddPoint 并滚动显示

**持久化:**本地 SQLite(默认,简单上手)数据库存储

二、核心功能实现详解

2.1 串口数据采集模块(SerialReader)

串口通信是上位机与硬件设备交互的基础,本模块负责串口初始化、数据接收与协议解析。

2.1.1 核心代码实现
csharp 复制代码
// SerialReader.cs 核心片段
public class SerialReader : IDisposable
{
    private SerialPort _serialPort;
    private readonly StringBuilder _buffer = new StringBuilder();
    
    // 数据接收与错误事件
    public event Action<DataPoint> OnDataReceived;
    public event Action<string> OnError;

    // 初始化串口参数
    public SerialReader(string portName, int baudRate = 115200,
                       Parity parity = Parity.None, int dataBits = 8,
                       StopBits stopBits = StopBits.One)
    {
        _serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
        {
            Encoding = Encoding.ASCII,
            ReadTimeout = 500,
            WriteTimeout = 500,
            NewLine = "\n"
        };
        _serialPort.DataReceived += SerialPort_DataReceived;
        _serialPort.ErrorReceived += SerialPort_ErrorReceived;
    }

    // 数据接收处理
    private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            string incomingData = _serialPort.ReadExisting();
            _buffer.Append(incomingData);
            ProcessBuffer(); // 解析完整行数据
        }
        catch (Exception ex)
        {
            OnError?.Invoke($"读取数据错误: {ex.Message}");
        }
    }

    // 协议解析(格式:SENSOR,CH1,1622025600,23.45)
    private void ParseLine(string line)
    {
        var parts = line.Split(',');
        if (parts.Length >= 4 && parts[0] == "SENSOR")
        {
            if (long.TryParse(parts[2], out long timestamp) &&
                double.TryParse(parts[3], out double value))
            {
                var dataPoint = new DataPoint
                {
                    Channel = parts[1],
                    Time = DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime,
                    Value = value
                };
                OnDataReceived?.Invoke(dataPoint); // 触发数据接收事件
            }
        }
    }
}
2.1.2 关键逻辑说明
  • 采用事件驱动模式:通过 OnDataReceived 传递解析后的数据,OnError 反馈异常
  • 缓冲区处理:使用 StringBuilder 缓存不完整数据,按换行符分割完整报文
  • 协议解析:严格匹配 SENSOR,CHx,时间戳,数值 格式,确保数据有效性

2.2 实时曲线展示(基于LiveCharts)

实时曲线是数据可视化的核心,通过 LiveCharts 实现多通道数据的动态展示。

2.2.1 图表初始化(MainForm)
csharp 复制代码
// MainForm.cs 图表初始化
private CartesianChart InitializeChart()
{
    return new CartesianChart
    {
        Series = new SeriesCollection(),
        AxisX = new AxesCollection
        {
            new Axis
            {
                Title = "时间",
                LabelFormatter = value => 
                {
                    // 格式化X轴为时间格式(HH:mm:ss)
                    var allTimes = _timeStampsMap.Values.SelectMany(v => v).OrderBy(t => t).ToList();
                    return value < allTimes.Count ? allTimes[(int)value].ToString("HH:mm:ss") : "";
                }
            }
        },
        AxisY = new AxesCollection
        {
            new Axis { Title = "数值", LabelFormatter = value => value.ToString("F2") }
        },
        LegendLocation = LegendLocation.Top
    };
}
2.2.2 动态更新曲线
csharp 复制代码
// 接收数据后更新图表
private void UpdateChart(DataPoint dataPoint)
{
    // 为新通道创建曲线系列
    if (!_seriesMap.ContainsKey(dataPoint.Channel))
    {
        var color = Color.FromArgb(_random.Next(50, 255), _random.Next(50, 255), _random.Next(50, 255));
        var series = new LineSeries
        {
            Title = dataPoint.Channel,
            Values = new ChartValues<double>(),
            Stroke = new SolidColorBrush(Color.FromArgb(color.A, color.R, color.G, color.B)),
            Fill = Brushes.Transparent
        };
        _seriesMap[dataPoint.Channel] = series;
        _chart.Series.Add(series);
    }

    // 添加数据并限制最大点数(防止内存溢出)
    var lineSeries = _seriesMap[dataPoint.Channel];
    lineSeries.Values.Add(dataPoint.Value);
    if (lineSeries.Values.Count > MaxDataPoints)
    {
        lineSeries.Values.RemoveAt(0); // 移除最旧数据
    }
    _chart.Update(); // 刷新图表
}

2.3 本地数据持久化(SQLite)

使用 SQLite 实现本地数据存储,适合离线场景下的历史数据查询。

2.3.1 数据库初始化(LocalStorage)
csharp 复制代码
// LocalStorage.cs 数据库初始化
private void InitializeDatabase()
{
    using (var connection = new SqliteConnection(_connectionString))
    {
        connection.Open();
        // 创建传感器数据表
        string createTableSql = @"
        CREATE TABLE IF NOT EXISTS sensor_data (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            channel TEXT NOT NULL,
            time TEXT NOT NULL,
            value REAL NOT NULL,
            created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
        );";
        using (var command = new SqliteCommand(createTableSql, connection))
        {
            command.ExecuteNonQuery();
        }
    }
}
2.3.2 数据插入实现
csharp 复制代码
// 插入数据点到SQLite
public void Insert(DataPoint dataPoint)
{
    using (var connection = new SqliteConnection(_connectionString))
    {
        connection.Open();
        string insertSql = @"
        INSERT INTO sensor_data (channel, time, value)
        VALUES (@channel, @time, @value);";
        using (var command = new SqliteCommand(insertSql, connection))
        {
            command.Parameters.AddWithValue("@channel", dataPoint.Channel);
            command.Parameters.AddWithValue("@time", dataPoint.Time.ToString("o")); // ISO 8601格式
            command.Parameters.AddWithValue("@value", dataPoint.Value);
            command.ExecuteNonQuery();
        }
    }
}

2.4 时序数据库集成(InfluxDB)

InfluxDB 专为时序数据设计,适合高频率传感器数据的长期存储与分析。

2.4.1 InfluxDB客户端初始化
csharp 复制代码
// InfluxClient.cs 初始化配置
public void Initialize(string url, string token, string org, string bucket)
{
    _writeUrl = $"{url.TrimEnd('/')}/api/v2/write?org={Uri.EscapeDataString(org)}&bucket={Uri.EscapeDataString(bucket)}&precision=s";
    _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {token}");
    _isInitialized = true;
}
2.4.2 数据写入实现
csharp 复制代码
// 异步写入数据到InfluxDB
public async Task WritePointAsync(DataPoint dataPoint)
{
    // 构建InfluxDB行协议(measurement,tag=value field=value timestamp)
    string lineProtocol = $"sensor,channel={EscapeTagValue(dataPoint.Channel)} value={dataPoint.Value} {new DateTimeOffset(dataPoint.Time).ToUnixTimeSeconds()}";
    var content = new StringContent(lineProtocol, Encoding.UTF8);
    var response = await _httpClient.PostAsync(_writeUrl, content);
    
    if (!response.IsSuccessStatusCode)
    {
        string errorContent = await response.Content.ReadAsStringAsync();
        throw new Exception($"写入失败: {response.StatusCode} - {errorContent}");
    }
}

三、界面设计与交互逻辑(MainForm)

3.1 界面布局

主界面采用上下分割布局:

  • 上半部分:串口配置区(串口选择、波特率、连接按钮)+ 日志显示区
  • 下半部分:实时曲线展示区

核心布局代码:

csharp 复制代码
// MainForm.cs 界面布局
private void SetupUI()
{
    var splitContainer = new SplitContainer
    {
        Dock = DockStyle.Fill,
        Orientation = Orientation.Horizontal,
        SplitterDistance = 200 // 上半部分高度
    };
    
    // 上半部分:串口配置与日志
    var topPanel = splitContainer.Panel1;
    topPanel.Controls.Add(new FlowLayoutPanel // 串口配置控件
    {
        Controls = { portComboBox, baudRateComboBox, startButton, statusLabel }
    });
    topPanel.Controls.Add(logTextBox); // 日志文本框
    
    // 下半部分:图表
    splitContainer.Panel2.Controls.Add(_chart);
    _chart.Dock = DockStyle.Fill;
}

3.2 核心交互逻辑

  • 串口连接/断开:通过 StartButton_Click 事件切换状态
  • 数据流转:串口接收数据 → 解析为 DataPoint → 触发图表更新 + 本地存储 + 时序库存储
csharp 复制代码
// 数据接收后处理流程
private void OnSerialDataReceived(DataPoint dataPoint)
{
    UpdateLog($"收到数据: {dataPoint.Channel} - {dataPoint.Value}"); // 更新日志
    UpdateChart(dataPoint); // 更新图表
    LocalStorage.Instance.Insert(dataPoint); // 本地存储
    if (InfluxClient.Instance.IsInitialized)
    {
        Task.Run(() => InfluxClient.Instance.WritePointAsync(dataPoint)); // 异步写入InfluxDB
    }
}

四、代码调试与常见问题解决

4.1 调试准备

  1. 确保串口设备连接正常(可通过设备管理器确认端口号)
  2. 配置 InfluxDB 连接参数(在 MainForm 初始化时添加配置界面或硬编码测试):
csharp 复制代码
// 示例:在MainForm加载时初始化InfluxClient
private void MainForm_Load(object sender, EventArgs e)
{
    InfluxClient.Instance.Initialize(
        url: "http://localhost:8086",
        token: "你的InfluxDB令牌",
        org: "sensor_org",
        bucket: "sensor_data"
    );
}

4.2 常见问题解决

  1. SQLite初始化失败

    • 检查 Program.cs 中是否调用 Batteries.Init();(SQLitePCL 必须初始化)
    • 确保 SQLitePCL.raw.bundle_e_sqlite3 包已正确安装
  2. 串口无法打开

    • 确认端口号正确且未被其他程序占用
    • 检查波特率、校验位等参数与设备匹配
  3. InfluxDB写入失败

    • 验证 urltokenorgbucket 正确性
    • 通过 InfluxDB Web 界面的 "Data" 菜单测试连接

五、测试效果展示

这里我使用了电脑上两个串口直接连接的方式(两个USB转TTL插入电脑,TTL线直连注意TXRX要交叉连接),使用COM3通过软件输出仿真数据,使用CM4接收并显示到界面,存储数据

5.1 运行界面展示


5.2 数据存储验证

六、部署选项

6.1 生成可执行文件

  1. 在 Visual Studio 中右键项目 → "属性" → "生成" → 目标框架选择 .NET Framework 4.8
  2. 点击 "生成解决方案",输出文件位于 bin\Release 目录

6.2 环境依赖打包

  • 必要文件:
    • 生成的 SerialMonitorNetFramework.exe
    • SQLite 依赖:e_sqlite3.dll(自动复制到输出目录)
    • 配置文件:建议创建 app.config 存储 InfluxDB 连接参数

6.3 运行环境要求

  • 操作系统:Windows 7 及以上
  • 安装 .NET Framework 4.8 运行时(下载地址
  • InfluxDB 服务需提前启动(远程部署时需确保网络可达)

七、工程下载与结语

完整工程已上传CSDN资源库(0积分下载!点个关注呗) ,包含所有源码、测试代码与详细注释,可直接转到资源库下载测试。

本项目通过模块化设计实现了串口数据采集、实时可视化与多端存储的完整流程,可根据实际需求扩展功能:

  • 增加数据导出(Excel/CSV)
  • 实现报警阈值设置与提醒
  • 支持多设备并发采集

希望本文能为上位机开发初学者提供清晰的实战指引,如有问题欢迎在下方评论区交流。

关注我,每周更新代码实战项目,共同学习进步!

相关推荐
hqwest3 小时前
QT肝8天08--主界面设计
开发语言·qt·上位机·qt开发·ui设计
奔跑吧邓邓子3 小时前
【C++实战(64)】C++ 邂逅SQLite3:数据库编程实战之旅
数据库·c++·sqlite·实战·sqlite3·数据库编程
ajassi200015 小时前
开源 C# 快速开发(十六)数据库--sqlserver增删改查
windows·开源·c#
大飞pkz19 小时前
【设计模式】观察者模式
开发语言·观察者模式·设计模式·c#
唐青枫19 小时前
深入掌握 FluentMigrator:C#.NET 数据库迁移框架详解
c#·.net
李宥小哥20 小时前
C#基础08-面向对象
开发语言·c#
李宥小哥20 小时前
C#基础07-类与对象
服务器·数据库·c#
包达叔1 天前
仿NewLife的XmlConfig类实现Json配置文件
c#·json·newlife
大飞pkz1 天前
【设计模式】解释器模式
开发语言·设计模式·c#·解释器模式