阅读前提醒,本文完整工程已上传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 调试准备
- 确保串口设备连接正常(可通过设备管理器确认端口号)
- 配置 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 常见问题解决
-
SQLite初始化失败:
- 检查
Program.cs
中是否调用Batteries.Init();
(SQLitePCL 必须初始化) - 确保
SQLitePCL.raw.bundle_e_sqlite3
包已正确安装
- 检查
-
串口无法打开:
- 确认端口号正确且未被其他程序占用
- 检查波特率、校验位等参数与设备匹配
-
InfluxDB写入失败:
- 验证
url
、token
、org
、bucket
正确性 - 通过 InfluxDB Web 界面的 "Data" 菜单测试连接
- 验证
五、测试效果展示
这里我使用了电脑上两个串口直接连接的方式(两个USB转TTL插入电脑,TTL线直连注意TXRX要交叉连接),使用COM3通过软件输出仿真数据,使用CM4接收并显示到界面,存储数据
5.1 运行界面展示
5.2 数据存储验证
- 本地SQLite :使用 SQLite Expert Personal 打开
sensor_data.db
,查看sensor_data
表数据
六、部署选项
6.1 生成可执行文件
- 在 Visual Studio 中右键项目 → "属性" → "生成" → 目标框架选择
.NET Framework 4.8
- 点击 "生成解决方案",输出文件位于
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)
- 实现报警阈值设置与提醒
- 支持多设备并发采集
希望本文能为上位机开发初学者提供清晰的实战指引,如有问题欢迎在下方评论区交流。
关注我,每周更新代码实战项目,共同学习进步!