您的位置:首页 > 文旅 > 美景 > C#学习笔记16:串口上位机数据绘图助手Plotter的开发

C#学习笔记16:串口上位机数据绘图助手Plotter的开发

2024/10/5 22:26:27 来源:https://blog.csdn.net/qq_64257614/article/details/141104757  浏览:    关键词:C#学习笔记16:串口上位机数据绘图助手Plotter的开发

今日尝试写一款窗口上位机数据绘图助手Plotter的开发,实现接收解析数据包进行画图的功能:

文章提供完整代码解释、设计点解释、测试效果图、完整工程下载

目录

控件摆放与使用控件大致介绍:

下载必要的Nuget程序包:

图表绘制相关代码逻辑:

字典存储每条曲线的 PointPairList:

绘制/更新曲线函数的编写:

调用写好的曲线绘制函数:

阶段绘制测试效果如下:

将时间戳作为X轴输入:

时间戳绘制效果如下:

清空图表按钮实现:

鼠标在数据点上获取精确值:

串口接收事件生成与衔接图表逻辑:

 单片机方面宏定义打印函数:

单片机串口发送示例:

接收数据字符串字段函数:

在串口接收事件中的调用解算:

 阶段性解算成果展示:

字典转换颜色字符串:

数据画图函数与非UI线程调用:

单条曲线绘图测试效果:

最终测试效果:

遇到的问题:

在非UI线程调用UI控件:

整体测试工程下载:


 

控件摆放与使用控件大致介绍:

控件最终摆放效果如下图:

大致使用了如下控件:

TableLayoutPanel              自动排列组件,辅助实现控件与窗体的同步缩放

group                                 控件分组组件,与TableLayoutPanel 组合使用

ZedGraphControl               图表组件,用于绘图

serialPort                           串口组件

checkbox                           选框

下载必要的Nuget程序包:

这次编写的串口绘图助手程序需要以下几个Nuget程序包的支持:

图表绘制相关代码逻辑:

这一板块部分主要想实现的是:

1.  绘制Y轴是数据,X轴是1ms级别时间戳的曲线

2.  能绘制不止一条曲线,且每条曲线颜色可以自定

3.  曲线是由点集构成,因此需要标注每个数据点

4.  鼠标移动到图表曲线的点上显示点的X,Y精确坐标

字典存储每条曲线的 PointPairList:

先定义全局变量,来存储每条曲线的列表浮点数据值:

// 有一个全局或类级别的字典来存储每条曲线的PointPairList 
private Dictionary<string, PointPairList> curves = new Dictionary<string, PointPairList>();

 

绘制/更新曲线函数的编写:

这个自定义的函数只需传入以下几个参数:

ZedGraph.ZedGraphControl zgc:图表控件名称

string label                                   :  曲线名称

double x, double y                       :   x,y坐标

Color color                                   :  曲线颜色

实现了有曲线就继续绘制,没曲线就根据传参数的曲线名称,创建一条曲线

        // 初始化或更新曲线(如果曲线不存在,则创建它),并标记每个点  // 使用圆形(SymbolType.Circle)作为标记类型。 //这里使用 SymbolType.Triangle 三角形标记  // 如果想要使用其他类型的标记,可以将 SymbolType.Circle 替换为想要的类型。private void InitializeOrUpdateCurve(ZedGraph.ZedGraphControl zgc, string label, double x, double y, Color color){GraphPane myPane = zgc.GraphPane;// 检查曲线是否已存在  if (!curves.ContainsKey(label)){// 曲线不存在,创建新的PointPairList并添加到GraphPane中  PointPairList list = new PointPairList();curves.Add(label, list);// 创建曲线并设置样式  LineItem myCurve = myPane.AddCurve(label, list, color, SymbolType.Triangle); // 假设想要三角形标记  // 设置标记的大小(您可以根据需要调整)  myCurve.Symbol.Size = 4;// 由于我们想要标记颜色与线条颜色相同,所以不需要额外设置  // 但为了确保,我们可以显式设置填充颜色为线条颜色  myCurve.Symbol.Fill.Type = FillType.Solid;myCurve.Symbol.Fill.Color = color;}// 获取对应的PointPairList并添加新点  PointPairList listToUpdate = curves[label];listToUpdate.Add(x, y);// 更新坐标轴并重新绘制图表  myPane.AxisChange();zgc.Refresh();}

 

调用写好的曲线绘制函数:

对于该函数的调用,我放置了一个按键控件,然后定义了几个全局变量,每次按下按键,都会将全局变量数值增加,然后作为图表曲线的数值绘制在对应名称的曲线上:

以下引用案例:

创建/继续绘制俩条图表曲线:MyCurve 红色 与MyCurve2 蓝色,按键每次按下将全局变量数值增加,绘制到曲线上

        //绘制测试private void Test_button_Click(object sender, EventArgs e){//My_Plotter(1, 2);newX++; newY++;newX1++;newY1++;DrawNewPoint(zedGraphControl1, "MyCurve", newX, newY, Color.Red);DrawNewPoint(zedGraphControl1, "MyCurve2", newX1, newY1, Color.Blue);}

阶段绘制测试效果如下:

 

将时间戳作为X轴输入:

这里我想做到,从第一个数据开始计时,x轴是ms级别,我只需要传Y轴数值看其随着时间变化就行了:

 先定义一些必要的变量如下:

        // 声明一个DateTime变量来存储系统时间  private DateTime firstDataPointTime = DateTime.MinValue; // 初始化为一个不可能的值 (这个变量用于获取第一个变量出现时的时间)double timeStampInMilliseconds;        //毫秒级计数bool firstDataPoint_flag = false;    //记录是否获取了第一个数据DateTime newDataPointTime;           // 新时间

然后将获取时间的逻辑加入测试按钮:

        //绘制测试private void Test_button_Click(object sender, EventArgs e){newY1++; newY++;当捕获到一个新的数据点时  DateTime newDataPointTime = DateTime.Now; // 捕获新数据点的时间// 计算新数据点与第一个数据点之间的时间差(毫秒)  if (firstDataPoint_flag == false){// 记录第一个数据点的时间  firstDataPointTime = DateTime.Now; // 或者使用 DateTime.UtcNow 如果你想要 UTC 时间DrawNewPoint(zedGraphControl1, "MyCurve",0, newY, Color.Red);DrawNewPoint(zedGraphControl1, "MyCurve2",0, newY1, Color.Blue);firstDataPoint_flag = true;}else if (firstDataPoint_flag == true){当捕获到一个新的数据点时  //newDataPointTime = DateTime.Now; // 捕获新数据点的时间  timeStampInMilliseconds = (newDataPointTime - firstDataPointTime).TotalMilliseconds;//timeStampInMilliseconds = currentTime.Ticks / TimeSpan.TicksPerMillisecond;DrawNewPoint(zedGraphControl1, "MyCurve", timeStampInMilliseconds, newY, Color.Red);DrawNewPoint(zedGraphControl1, "MyCurve2", timeStampInMilliseconds, newY1, Color.Blue);}}

 

时间戳绘制效果如下:

如图,我点击绘制测试按钮,越快线条越急抖,越慢线条越平缓

清空图表按钮实现:

这个没啥好注意的,主要是我实现的过程中忘记清空曲线字典列表了,导致清空一次后没法在此生成曲线:

        //清空图表private void clear_button_Click(object sender, EventArgs e){ClearChart(zedGraphControl1);firstDataPoint_flag = false;    //记录获取第一个数据 状态置零}// 清空图表中的所有曲线  private void ClearChart(ZedGraph.ZedGraphControl zgc){GraphPane myPane = zgc.GraphPane;// 遍历并删除所有曲线  while (myPane.CurveList.Count > 0){myPane.CurveList.RemoveAt(0);}myPane.AxisChange();// 刷新图表以显示更改  zgc.Refresh();// 最后别忘记清理字典ClearCurvesDictionary();}

 

鼠标在数据点上获取精确值:

这个只需要开启它的一个属性就行:

开了之后就是这样的效果:711是毫秒,后面的小数点就是微秒数

 

串口接收事件生成与衔接图表逻辑:

对于检测端口卡顿、打开串口、发送数据等操作在之前的 串口助手窗体程序的制作中就已经实现了,本文主要讲串口接收部分怎么去衔接图表的逻辑。

 

 单片机方面宏定义打印函数:

这里为了方便用户使用,进行了数据发送相关的宏定义:

// 宏定义的PRINT函数,第一个传入曲线名称,第二个传入曲线颜色,第三个传入你需要打印的数值
// 应用示例例: t1=95;  PRINT(plot1,Red,"%d",t1);  
// 曲线名称:plot1       曲线颜色:红色      数据数值:t1变量的值
// 只需有这个宏定义就行了,后续的使用就是类似于:  PRINT(plot1,Red,"%d",t1); 这样的格式  
// 如果你颜色参数传输了没有定义的颜色,则默认曲线为 蓝色
// 可用颜色字段如下: 
// 
#define PRINT(title,color,fmt, args...) printf("{"#title"}""{"#color"}"fmt"\n", ##args)

 

单片机串口发送示例:

以下截图示例了如何用单片机串口发送你想要绘制的曲线、曲线颜色以及数据:

最终发送出去的数据会是这样的格式:

 

接收数据字符串字段函数:

这需要先定义一些全局变量进行接收数据字符串字段等:

        string title;//串口获取的曲线名称string color;//串口获取的颜色字段double value;//串口接受的数据
        private void ParseData(string data){string formattedLogMessage;       //转接字符串用// 假设数据格式为 "{title}{color}value",其中value是一个可以转换为double的数值  // 第一步:分割数据为三部分  string[] parts = data.Split(new[] { '}', '{' }, StringSplitOptions.RemoveEmptyEntries);if (parts.Length != 3){// 数据格式错误  Console.WriteLine("Invalid data format: " + data);return;}// 第二步:提取title和color  title = parts[0];color = parts[1];// 第三步:尝试将最后一部分转换为double  if (double.TryParse(parts[2], out value)){// 成功转换//Console.WriteLine($"Title: {title}, Color: {color}, Value: {value}");formattedLogMessage = string.Format("Title: {0}, Color: {1}, Value: {2}", title, color, value);myaddlog(0, formattedLogMessage);// 这里可以根据需要处理title, color, 和value}else{// 转换失败  //Console.WriteLine("Failed to parse value from data: " + data);myaddlog(1, "本次数据转换失败:丢包......");}}

 

在串口接收事件中的调用解算:

首先这个事件时需要在serial1的事件(黄色图表)中打开启用的:

然后这一步不是最终实现,只是阶段性测试接收与解算是否成功:

        //串口接收逻辑private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e){SerialPort sp = (SerialPort)sender;string indata = sp.ReadExisting(); // 读取所有可用的数据  // 假设每次接收的数据都是完整的一行,或者你可以通过特定的字符(如换行符'\n')来分割数据  // 这里我们使用'\n'作为数据结束的标志,根据你的实际情况可能需要调整  string[] lines = indata.Split('\n');foreach (string line in lines){if (!string.IsNullOrWhiteSpace(line)){// 解析数据  ParseData(line);}}}

 阶段性解算成果展示:

这里在日志中发现了阶段性接收解算的成功:

 

字典转换颜色字符串:

单片机发送的颜色是字符串,而不是我们上位机中的Color.Red属性,因此需要定义字典来转换一下:

        // 创建一个颜色字典  private Dictionary<string, Color> colorDictionary = new Dictionary<string, Color>{{ "Red", Color.Red },{ "Green", Color.Green },{ "Blue", Color.Blue },{ "Black",Color.Black},{ "Aqua",Color.Aqua},{ "Beige",Color.Beige},{ "AliceBlue",Color.AliceBlue},{ "AntiqueWhite",Color.AntiqueWhite},// 可以根据需要添加更多颜色  };

可以如下方式来使用字典:

            Color colorValue=Color.Blue;//记录颜色变量 (默认蓝色)colorDictionary.TryGetValue(color, out colorValue);//这句会尝试将颜色字符与字典进行匹配,并传给colorValue

 

数据画图函数与非UI线程调用:

这是个比较抽像的问题,因为UI的绘制与串口接收不在同一个线程

如果尝试从非UI线程(即不是创建控件的线程,通常是主线程)访问UI控件就会抛出System.InvalidOperationException异常,提示“线程间操作无效: 从不是创建控件‘zedGraphControl1’的线程访问它。”

为了解决这个问题,需要确保所有对UI控件的访问都在UI线程上执行

有两个方法都允许你在控件的UI线程上执行委托(delegate)

Control.InvokeControl.BeginInvoke方法

BeginInvoke是异步的,而Invoke是同步的。

以下是一个示例,展示了如何在非UI线程中安全地调用UI线程上的方法,以更新zedGraphControl1控件:

这是要根据你需要调用的函数的传参等情况进行编写的,下面先放出需要跨线程调用的更新zedGraphControl1控件的函数:

     private void DrawNewPoint(ZedGraph.ZedGraphControl zgc, string label, double x, double y, Color color)
       //这是在非UI线程中调用的方法  private void UpdateGraphFromNonUiThread(ZedGraph.ZedGraphControl zgc, string label, double x, double y, Color color){// 检查zedGraphControl1是否已创建并且句柄已分配  if (zedGraphControl1.InvokeRequired){// 使用BeginInvoke在UI线程上异步执行UpdateGraph方法  zedGraphControl1.BeginInvoke(new Action<ZedGraph.ZedGraphControl, string, double, double, Color>(DrawNewPoint), zgc,label,x,y,color);}else{// 如果已经在UI线程上,则直接调用UpdateGraph方法  DrawNewPoint(zgc,label,x,y,color);}  }

然后就可以使用这个线程委托来调用数据绘图了:

        //数据画图函数private void plotData(){Color colorValue=Color.Blue;//记录颜色变量 (默认蓝色)colorDictionary.TryGetValue(color, out colorValue);//这句会尝试将颜色字符与字典进行匹配,并传给colorValue// 计算新数据点与第一个数据点之间的时间差(毫秒)  if (firstDataPoint_flag == false){// 记录第一个数据点的时间  firstDataPointTime = DateTime.Now; // 或者使用 DateTime.UtcNow 如果你想要 UTC 时间//传参: zedGraphControl1 图表,"MyCurve" 曲线名, X数值,Y数值,颜色UpdateGraphFromNonUiThread(zedGraphControl1,title, 0, value, colorValue);firstDataPoint_flag = true;}else if (firstDataPoint_flag == true){当捕获到一个新的数据点时  DateTime newDataPointTime = DateTime.Now; // 捕获新数据点的时间timeStampInMilliseconds = (newDataPointTime - firstDataPointTime).TotalMilliseconds;UpdateGraphFromNonUiThread(zedGraphControl1,title, timeStampInMilliseconds, value, colorValue);}}

单条曲线绘图测试效果:

单片机写了一个程序,能发送一个让 t1 变量加到50,再从50减到0,以此循环

并发送的程序:

然后实测效果符合情况:

最终测试效果:

这次测试比之前的阶段测试添加了一条变量的曲线来绘制,它与之前的变量变换情况相反:

也是符合情况的:

最后提一嘴:别忘记这个单片机端的宏定义:

// 宏定义的PRINT函数,第一个传入曲线名称,第二个传入曲线颜色,第三个传入你需要打印的数值
// 应用示例例: t1=95;  PRINT(plot1,Red,"%d",t1);  
// 曲线名称:plot1       曲线颜色:红色      数据数值:t1变量的值
// 只需有这个宏定义就行了,后续的使用就是类似于:  PRINT(plot1,Red,"%d",t1); 这样的格式  
// 如果你颜色参数传输了没有定义的颜色,则默认曲线为 蓝色
// 可用颜色字段如下: 
// 
#define PRINT(title,color,fmt, args...) printf("{"#title"}""{"#color"}"fmt"\n", ##args)

遇到的问题:

在非UI线程调用UI控件:

这是我尝试在串口中断的线程中更新数据到图表时产生的报错:

System.InvalidOperationException:“线程间操作无效: 从不是创建控件“zedGraphControl1”的线程访问它。”

这是个比较抽像的问题,因为UI的绘制与串口接收不在同一个线程

如果尝试从非UI线程(即不是创建控件的线程,通常是主线程)访问UI控件就会抛出System.InvalidOperationException异常,提示“线程间操作无效: 从不是创建控件‘zedGraphControl1’的线程访问它。”

为了解决这个问题,需要确保所有对UI控件的访问都在UI线程上执行

解决这个问题的具体实现在 数据画图函数与非UI线程调用: 这一节

整体测试工程下载:

这里先说声抱歉,我之前不会做上位机时,下载了类似功能的串口助手,能绘图,被迫付费使用了一阵子,这里资源设置为9.9付费了... (朋友免费~~)

https://download.csdn.net/download/qq_64257614/89631031?spm=1001.2014.3001.5503

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com