【C#】一种优雅的基于winform的串口通信管理
serialPort.DataReceived、串口优雅管理
完整《C#串口通信系统》功能清单
Part 1 — SerialPortManager.cs —— 串口核心管理类
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Windows.Forms;/// <summary>
/// 专业版串口通信管理器
/// 支持:自动重连、自动超时检测、线程安全接收、发送失败重试、统一日志
/// </summary>
public class SerialPortManager
{private SerialPort _serialPort;private System.Timers.Timer _reconnectTimer; // 自动重连定时器public event Action<byte[]> DataReceived; // 串口数据接收事件public event Action<string> LogMessage; // 日志输出事件public bool IsOpen => _serialPort != null && _serialPort.IsOpen;public string PortName { get; private set; }public int BaudRate { get; private set; }public SerialPortManager(){_reconnectTimer = new System.Timers.Timer(5000); // 5秒检测一次串口状态_reconnectTimer.Elapsed += (sender, e) => ReconnectCheck();_reconnectTimer.Start();}public void Open(string portName, int baudRate = 115200){try{PortName = portName;BaudRate = baudRate;if (_serialPort == null){_serialPort = new SerialPort{PortName = portName,BaudRate = baudRate,Encoding = Encoding.UTF8};_serialPort.DataReceived += SerialPort_DataReceived;}if (!_serialPort.IsOpen){_serialPort.Open();Log($"✅ 串口 {portName} 打开成功");}}catch (Exception ex){Log($"❌ 打开串口失败:{ex.Message}");}}public void Close(){try{if (_serialPort != null){if (_serialPort.IsOpen){_serialPort.Close();Log($"❎ 串口 {PortName} 已关闭");}_serialPort.DataReceived -= SerialPort_DataReceived;_serialPort.Dispose();_serialPort = null;}}catch (Exception ex){Log($"❌ 关闭串口失败:{ex.Message}");}}public void Send(byte[] data){if (_serialPort == null || !_serialPort.IsOpen){Log("❗ 串口未打开,无法发送!");return;}int retryCount = 0;const int maxRetries = 3;while (retryCount < maxRetries){try{_serialPort.Write(data, 0, data.Length);Log("📤 数据发送成功");break;}catch (Exception ex){retryCount++;Log($"⚠️ 发送失败,重试{retryCount}/{maxRetries}次:{ex.Message}");Thread.Sleep(100); // 短暂等待}}if (retryCount == maxRetries){Log("❌ 发送失败,已达到最大重试次数!");}}private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e){try{int bytesToRead = _serialPort.BytesToRead;byte[] buffer = new byte[bytesToRead];_serialPort.Read(buffer, 0, bytesToRead);DataReceived?.Invoke(buffer); // 抛给外部}catch (Exception ex){Log($"❌ 接收数据失败:{ex.Message}");}}private void ReconnectCheck(){if (_serialPort != null && !_serialPort.IsOpen && !string.IsNullOrEmpty(PortName)){try{_serialPort.Open();Log($"🔄 检测到串口断开,自动重连成功!");}catch{Log($"🔄 正在尝试重连串口 {PortName}...");}}}private void Log(string message){LogMessage?.Invoke($"[{DateTime.Now:HH:mm:ss}] {message}");}
}
Part 2 — Form1.cs —— 界面调用端逻辑
// 引入
private SerialPortManager spManager = new SerialPortManager();
private List<string> serialLogs = new List<string>();
private System.Timers.Timer timeoutTimer;private void Form1_Load(object sender, EventArgs e)
{RefreshPorts();spManager.DataReceived += OnDataReceived;spManager.LogMessage += Log;
}private void openPortBtn_Click(object sender, EventArgs e)
{spManager.Open(comboBox1.Text, 115200);
}private void closePortBtn_Click(object sender, EventArgs e)
{spManager.Close();
}private void sendBtn_Click(object sender, EventArgs e)
{byte[] frame = BuildFrame();spManager.Send(frame);// 开始超时监控StartTimeoutMonitor(1500);
}private void saveLogBtn_Click(object sender, EventArgs e)
{SaveSerialLog();
}// 接收到串口数据
private void OnDataReceived(byte[] data)
{this.Invoke((Action)(() =>{timeoutTimer?.Stop();timeoutTimer?.Dispose();timeoutTimer = null;Log($"📥 收到数据:{BitConverter.ToString(data).Replace("-", " ")}");}));
}// 日志打印+记录
private void Log(string message)
{if (this.InvokeRequired){this.Invoke(new Action(() => Log(message)));return;}string logMessage = $"[{DateTime.Now:HH:mm:ss}] {message}";textBoxLog.AppendText(logMessage + Environment.NewLine);serialLogs.Add(logMessage);
}// 保存日志
private void SaveSerialLog()
{if (serialLogs.Count == 0){MessageBox.Show("暂无日志可保存!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);return;}string exePath = AppDomain.CurrentDomain.BaseDirectory;string logFolder = Path.Combine(exePath, "logs");if (!Directory.Exists(logFolder)){Directory.CreateDirectory(logFolder);}string fileName = $"串口日志_{DateTime.Now:yyyyMMdd_HHmmss}.log";string fullPath = Path.Combine(logFolder, fileName);File.WriteAllLines(fullPath, serialLogs, Encoding.UTF8);MessageBox.Show($"日志保存成功!\n路径:{fullPath}", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
}// 热插拔检测
protected override void WndProc(ref Message m)
{const int WM_DEVICECHANGE = 0x0219;const int DBT_DEVICEARRIVAL = 0x8000;const int DBT_DEVICEREMOVECOMPLETE = 0x8004;base.WndProc(ref m);if (m.Msg == WM_DEVICECHANGE){if (m.WParam.ToInt32() == DBT_DEVICEARRIVAL){Log("📥 串口设备已插入");RefreshPorts();}else if (m.WParam.ToInt32() == DBT_DEVICEREMOVECOMPLETE){Log("📤 串口设备已拔出");RefreshPorts();}}
}// 刷新串口列表
private void RefreshPorts()
{var ports = SerialPort.GetPortNames();comboBox1.Items.Clear();comboBox1.Items.AddRange(ports);if (ports.Length > 0){comboBox1.SelectedIndex = 0;}
}// 启动超时检测
private void StartTimeoutMonitor(int timeoutMs = 1000)
{if (timeoutTimer != null){timeoutTimer.Stop();timeoutTimer.Dispose();}timeoutTimer = new System.Timers.Timer(timeoutMs);timeoutTimer.Elapsed += (s, e) =>{timeoutTimer.Stop();timeoutTimer.Dispose();timeoutTimer = null;this.Invoke((Action)(() =>{Log("⚠️ 超时:设备未响应指令!");MessageBox.Show("设备未在限定时间内响应,请检查连接或设备状态。", "超时警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);}));};timeoutTimer.Start();
}
🔥 项目总览架构:
| 文件 | 作用 |
|---|---|
SerialPortManager.cs | 串口统一管理(发送/接收/重连/日志) |
Form1.cs | 上位机界面操作(开关串口/显示日志/保存日志) |
resources/ | 保存你的烧录数据/扫码数据等 |
logs/ | 保存串口完整操作日志 |
🎯 小结
| 功能 | 状态 |
|---|---|
| 自动检测热插拔刷新串口列表 | ✅ |
| 自动重连串口 | ✅ |
| 发送数据失败自动重试 | ✅ |
| 接收数据线程安全处理 | ✅ |
| 日志追踪全部收发 | ✅ |
| 一键保存日志 | ✅ |
| 超时无响应自动警告 | ✅ |
这已经是一个工业级、企业上位机必备的完整串口通信系统
扩展
serialPort.DataReceived
C# 串口通信,为什么要写:
serialPort.DataReceived += new SerialDataReceivedEventHandler(Sp_DataReceived);
如果不写,会有什么后果?
✅ 1. serialPort.DataReceived += ... 是什么意思?
它的意思是:
注册事件监听器,告诉系统:
“嘿!每当串口有数据到达(收到数据)的时候,请自动调用我的
Sp_DataReceived方法来处理!”
✅ 2. 如果不写这一行,会怎么样?
如果 不注册 DataReceived 事件,那么:
- 即使串口收到了设备发来的数据
- 程序 完全不会收到通知
Sp_DataReceived方法 永远不会被调用- 你就接收不到任何回传数据❗
- 整个上位机只能发送,没法接收数据
✅ 3. 这行代码详细拆解
serialPort.DataReceived += new SerialDataReceivedEventHandler(Sp_DataReceived);
也可以简化写成(完全一样效果):
serialPort.DataReceived += Sp_DataReceived;
意思就是:
把
Sp_DataReceived方法绑到串口的DataReceived事件上。
以后只要串口有新数据来,就会自动执行 Sp_DataReceived() 方法!
总结成一句话:
不注册
DataReceived事件,程序就完全收不到串口回来的数据。
✅ 最后标准流程回顾一下:
| 步骤 | 必要操作 | 作用 |
|---|---|---|
| 1 | 打开串口(serialPort.Open()) | 打通发送通道 |
| 2 | 注册 DataReceived 事件监听器 | 建立数据接收处理机制 |
| 3 | 实现 Sp_DataReceived 处理函数 | 真正处理收到的数据内容 |
相关文章:
【C#】一种优雅的基于winform的串口通信管理
serialPort.DataReceived、串口优雅管理 完整《C#串口通信系统》功能清单 Part 1 — SerialPortManager.cs —— 串口核心管理类 using System; using System.IO.Ports; using System.Text; using System.Threading; using System.Windows.Forms;/// <summary> /// 专业…...
ChatGPT之智能驾驶问题讨论
ChatGPT之智能驾驶问题讨论 1. 源由2. 问题:2.1 智能驾驶级别定义🚗 L2(部分自动化,Partial Automation)🤖 L3(有条件自动化,Conditional Automation)🛸 L4&a…...
K8S-证书过期更新
K8S证书过期问题 K8S证书过期处理方法 Unable to connect to the server: x509: certificate has expired or is not yet valid 1、查看证书有效期: # kubeadm certs check-expiration2、备份证书 # cp -rp /etc/kubernetes /etc/kubernetes.bak3、直接重建证书 …...
蓝桥杯第十五届真题——握手问题
#include<bits/stdc.h> using namespace std; int main() {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);int sum0;for(int i7;i<49;i){sumi;}cout<<sum;return 0; }...
5G_WiFi_CE_DFS
目录 一、规范要求 1、法规目录 2、定义 3、运行模式 4、主/从设备相关的运行行为及具体的动态频率选择(DFS)要求 5、产品角色确定测试项目 6、测试项目 测试项1:信道可用性检查(Channel Availability Check) …...
第二节:React 基础篇-受控组件 vs 非受控组件
一、场景题:设计一个实时搜索输入框,说明选择依据 受控组件 vs 非受控组件 核心区别 特征受控组件非受控组件数据管理由React状态(state)控制通过DOM元素(ref)直接访问更新时机每次输入触发onChange提交…...
springboot 处理编码的格式为opus的音频数据解决方案【java8】
opus编码的格式概念: Opus是一个有损声音编码的格式,由Xiph.Org基金会开发,之后由IETF(互联网工程任务组)进行标准化,目标是希望用单一格式包含声音和语音,取代Speex和Vorbis,且适用…...
RK3568 基于Gstreamer的多媒体调试记录
文章目录 1、环境介绍2、概念理清3、提前准备4、GStreamer编译5、GStreamer基础介绍6、视频播放初体验7、视频硬编码7.1、h2647.2、h265 8、视频硬解码8.1、解码视频并播放8.2、解码视频并播放带音频 1、环境介绍 硬件:飞凌ok3568-c开发板 软件:原厂rk…...
VS Code 的 .S 汇编文件里面的注释不显示绿色
1. 确认文件语言模式 打开 .S 文件后,查看 VS Code 右下角的状态栏,确认当前文件的识别模式(如 Assembly、Plain Text 等)。如果显示为 Plain Text 或其他非汇编模式: 点击状态栏中的语言模式(如 Plain Te…...
在 Wireshark 中如何筛选数据包
1. 显示过滤器(Display Filters) 显示过滤器用于 在已捕获的数据包中筛选,语法类似于编程语言中的条件表达式。 (1)基本过滤 表达式说明ip.addr 192.168.1.1显示所有涉及 192.168.1.1 的 IP 包ip.src 192.168.1.1…...
[MySQL]数据库与表创建
欢迎来到啾啾的博客🐱。 这是一个致力于构建完善 Java 程序员知识体系的博客📚。 它记录学习点滴,分享工作思考和实用技巧,偶尔也分享一些杂谈💬。 欢迎评论交流,感谢您的阅读😄。 本篇简单记录…...
5分钟读懂ArgoCD:在Kubernetes中实现持续部署
Kubernetes中的Argo CD介绍 Argo CD是用于Kubernetes的声明式GitOps持续交付工具。它遵循GitOps模式,以Git仓库作为定义所需应用程序状态的唯一真实来源,能在指定的目标环境中自动部署应用程序,并持续监控应用程序的运行状态,确保…...
cs224w课程学习笔记-第10课
cs224w课程学习笔记-第10课 异构图 前言一、异构图1、异构图定义2、异构图与同构图 二、异构图下的GNN1、GCN扩展至RGCN1.1 RGCN原理1.2 异构图的任务预测特点1.3 异构图任务预测基础案例 2、完整的异构图GCN三、异构图下的Transformer 前言 异构图的定义是节点内部存在类型不…...
OpenCV 图形API(26)图像滤波-----方框滤波函数boxFilter()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 使用方框滤波器模糊图像。 该函数使用以下内核来平滑图像: K α [ 1 1 … 1 1 1 … 1 ⋮ ⋮ ⋱ ⋮ 1 1 … 1 ] K \alpha \begin{b…...
安卓手机怎样开启双WiFi加速
1. 小米/Redmi手机 路径: 设置 → WLAN → 高级设置 → 双WLAN加速 操作: 开启功能后,可同时连接一个2.4GHz WiFi和一个5GHz WiFi(或两个不同路由器)。 可选择“智能选择”或手动指定辅助网络。 2. 华为/荣耀手机…...
大模型上下文协议MCP详解(2)—核心功能
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl1. 标准化上下文交互技术 1.1 实时数据接入能力 MCP(Model Context Protocol)通过标准化的接口,为 AI 模型提供了强大的实时数据接入能力,使其能够快速获取和处理来自不同数据源的实时信息。…...
剑指Offer(数据结构与算法面试题精讲)C++版——day8
剑指Offer(数据结构与算法面试题精讲)C版——day8 题目一:链表中环的入口节点题目二:两个链表的第1个重合节点题目三:反转链表附录:源码gitee仓库 题目一:链表中环的入口节点 这道题的有如下三个…...
【Qt】QxOrm:下载、安装、使用
1、下载源码 github地址:https://github.com/QxOrm/QxOrm 稳定版本下载:https://github.com/QxOrm/QxOrm/releases/tag/1.5.0 2、编译源码 QxOrm支持cmake编译(CMakeLists.txt)、Qt pro工程编译(QxOrm.pro) 以 QxOrm.pro 为例,编译生成的库,没有在 build-QxOrm-1.5…...
CISCO组建RIP V2路由网络
1.实验准备: 2.具体配置: 2.1根据分配好的IP地址配置静态IP: 2.1.1PC配置: PC0: PC1: PC2: 2.1.2路由器配置: R0: Router>en Router#conf t Enter configuration…...
【数学建模】(智能优化算法)鲸鱼优化算法(Whale Optimization Algorithm)详解与应用
鲸鱼优化算法(Whale Optimization Algorithm)详解与应用 文章目录 鲸鱼优化算法(Whale Optimization Algorithm)详解与应用1. 引言2. 算法原理2.1 生物学基础2.2 数学模型[^3]1. 包围猎物阶段2. 气泡网攻击(螺旋更新)3. 随机搜索猎物(全局探索…...
【深度洞察】解码饮料行业破局点:场景革命
当东鹏特饮以 “大瓶装 防尘盖” 精准解决货车司机的场景化需求,当农夫山泉通过 “冷藏版东方树叶” 打开年轻白领的早餐场景 —— 这些现象级案例背后,是饮料行业底层逻辑的深刻变革:真正的市场增量,藏在对消费场景的极致拆解中…...
工业科学级天文相机:跨界融合的高精密成像解决方案
随着国内科技的快速发展,工业相机领域正悄然兴起一场"天文级"的技术革命。这类兼具工业设备可靠性与天文观测精度的特殊相机,正在半导体制造、天文观测、空间探测等领域开辟新的应用疆域。其核心技术突破不仅体现在传感器性能的提升࿰…...
回文日期2
#include <bits/stdc.h> using namespace std; bool huiwen(int date) {int tempdate;int r0;while(temp>0){rr*10temp%10;temp/10;}return dater; }int main() {// 请在此输入您的代码int n,m;cin>>n>>m;int tempfn/100,tempem/100;int yearfn/10000,mon…...
Ubuntu搭建Pytorch环境
Ubuntu搭建Pytorch环境 例如:第一章 Python 机器学习入门之pandas的使用 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 Ubuntu搭建Pytorch环境前言一、Anaconda二、Cuda1.安装流程2、环境变量&#…...
图书管理系统(Python)
运行结果: 源代码: # 定义一个图书类 class Book: def __init__(self, title, author, isbn): self.title title self.author author self.isbn isbn def show_info(self): print(f"{self.title},{self.author},{self.isbn}") # 图书列表…...
大模型本地部署系列(3) Ollama部署QwQ[阿里云通义千问]
大家好,我是AI研究者, 今天教大家部署 一个阿里云通义千问大模型。 QwQ大模型简介 QwQ是由阿里云通义千问(Qwen)团队推出的开源推理大模型,专注于提升AI在数学、编程和复杂逻辑推理方面的能力。其核心特点包括&#x…...
操作系统 4.1-I/O与显示器
外设工作起来 操作系统让外设工作的基本原理和过程,具体来说,它概括了以下几个关键步骤: 发出指令:操作系统通过向控制器中的寄存器发送指令来启动外设的工作。这些指令通常是通过I/O指令(如out指令)来实现…...
前端-Vue3
1. Vue3简介 2020年9月18日,Vue.js发布版3.0版本,代号:One Piece(n 经历了:4800次提交、40个RFC、600次PR、300贡献者 官方发版地址:Release v3.0.0 One Piece vuejs/core 截止2023年10月,最…...
Facebook账号类型一览
对于跨境出海从业者来说,Facebook是必不可少的内容营销和广告投放平台。针对Facebook的营销策略和发挥空间都很丰富,因此了解Facebook账号的类型、特点、适用场景和相关工具还是很有用的。 一、账号类型及特点 1.小黑号 无主页、无好友、无历史操作&am…...
Kotlin 通用请求接口设计:灵活处理多样化参数
在 Kotlin 中设计一个通用的 ControlParams 类来处理不同的控制参数,有几种常见的方法:方案1:使用密封类(Sealed Class) sealed class ControlParamsdata class LightControlParams(val brightness: Int,val color: S…...
