一、写在前面
本文所用例子为个人学习的小结,如有不足之处请各位多多海涵,欢迎小伙伴一起学习进步,如果想法可在评论区指出,我会尽快回复您,不胜感激!
所公布代码或截图均为运行成功后展示。
二、本文内容
基于桌面应用程序的用户界面框架Winform和C#开发的串口小工具,实现了:
1.串口通讯
2.用户界面可视化
3.根据串口返回的结果,切换工作状态,如:变换提示灯,弹窗警告等
4.配置数据由界面控件控制改为配置文件控制
三、开发环境
1.开发语言:C#
2.开发框架:.Net Framework 4.8
2.界面框架:Winform
3.开发工具:Visual Studio 2022
四、业务需求
部分业务需求:
自动连接夹具COM口,发送5,回复5
1:双手按下压按钮,治具合盖,发送6,夹具回复6,可以检查cover是否到位,发送1,开始扫码,夹具回复1,检测到barcode.txt文件,如果超时,发送4,夹具open,回复4。
2:检测到文件后,发送2,治具动作
3:如果治具动作到位,再发送7,夹具回复7,软件生成test.txt
4:机台开始测试,测试结束,机台会生成testend.txt,软件检测到testend.txt,生成airout.txt,开始退真空,软件检测到airoutok.txt,发送3,夹具全部对出,打开,回复3。
所有txt文件均是放在软件安装的目录下的,测试结束后全部清空
五、业务实现
业务实现分为两部分(只想看串口通讯的同学可以直接拉到5.4.3):
1.主界面的构建、提示盒子的构建、高光圆形Panel的构建
2.业务代码实现
(一)主界面的构建
主界面,图a
图a
使用MenuStrip控件构建菜单栏
使用Button控件构建播放、暂停、停止、生成三按钮、上下箭头
使用Label控件构建标识框
使用RichTextBox控件构建日志打印
使用Panel控件构建LED指示灯
(二)提示盒子的构建
消息盒子,图b
图b
改造Form使其满足需求,并随需要弹出。
public partial class CustomMessageBox : Form
{
public CustomMessageBox(string message)
{
InitializeComponent();
// 设置窗体的StartPosition属性为CenterParent
this.StartPosition = FormStartPosition.CenterParent;
// 设置窗体的内容
this.label1.Text = message;
this.ShowDialog();
}
private void button1_Click(object sender, EventArgs e)
{
this.Close();
this.DialogResult = DialogResult.OK;
}
}
(三)高光圆形Panel的构建
RoundPanel存在三种状态,图c:治具未通讯时;图d:治具打开时;图e:治具盒盖时
这里也可以使用PictureBox制作,我想练手重构控件,选择了把Panel改造为圆角Panel。
图c 图d 图e
创建并继承Panel,改造代码如下:
改变Color1的赋值可以切换状态,实现上方三色转换。
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public class GradientPanel : Panel
{
private Color color1;
private Color color2;
private float angle;
[Browsable(true)]
[Category("Custom")]
[Description("Custom text property.")]
[DefaultValue(typeof(Color), "Red")]
public Color Color1
{
get { return color1; }
set { color1 = value; }
}
[Browsable(true)]
[Category("Custom")]
[Description("Custom text property.")]
[DefaultValue(typeof(Color), "Yellow")]
public Color Color2
{
get { return color2; }
set { color2 = value; }
}
[Browsable(true)]
[Category("Custom")]
[Description("Custom text property.")]
[DefaultValue("0F")]
public float Angle
{
get { return angle; }
set { angle = value; }
}
protected override void OnPaintBackground(PaintEventArgs e)
{
using (LinearGradientBrush brush = new LinearGradientBrush(this.ClientRectangle, color1, color2, angle))
{
e.Graphics.FillRectangle(brush, this.ClientRectangle);
}
}
}
(四)业务代码实现
5.4.1 从配置文件获取并赋值
//获取当前执行程序所在目录
string currentDirectory = Directory.GetCurrentDirectory();
//获取同级目录下的config.ini
string filePath = Path.Combine(currentDirectory, "config.ini");
//读取文件
StreamReader r = new StreamReader(filePath);
string json = r.ReadToEnd();
//以json格式
dynamic config = JsonConvert.DeserializeObject(json);
//赋值
ComTextBox.Text = _Com = config["Com"];
_directoryPath = config["directoryPath"];
_serialPortOpenTimeoutcount = config["serialPortOpenTimeout"];
5.4.2定义更新状态方法
通过该方法控制循环中当前处于哪个阶段和状态。
//更新状态
private void StatusUpdate(CurrentStep cstep, CurrentStatus cstatus)
{
//当前阶段
currentStep = cstep;
//当前状态
currentStatus = cstatus;
}
CurrentStep和CurrentStatus为自定义的步骤和状态,为枚举类型。
//当前执行步骤
private enum CurrentStep
{
SoftwareInit,
SerialPortOpen,
SerialPortClose,
FixtureCoverOperate,
FixtureOpenOperate,
ScanOperate,
CheckBarcodeOperate,
DimmCpuOperate,
CheckTestIsEndOperate,
CheckAiroutOperate,
DeleteTestFileOperate,
}
//当前执行状态
public enum CurrentStatus
{
SoftwareIniting,
SoftwareInitOK,
SoftwareInitFail,
SerialPortConnecting,
SerialPortConnected,
SerialPortOpening,
SerialPortOpened,
SerialPortOpenFail,
SerialPortClosing,
SerialPortClosed,
SerialPortCloseFail,
FixtureCoverCommandsending,
FixtureCovering,
FixtureCovered,
FixtureCoverFail,
ScanCommandSending,
Scanning,
ScannOK,
ScannFail,
CheckingBarcode,
CheckBarcodeOK,
CheckBarcodeFail,
FixtureOpeneCommandSending,
FixtureOpening,
FixtureOpened,
FixtureOpenFail,
AllFixtureOpeneCommandSending,
AllFixtureOpening,
AllFixtureOpened,
AllFixtureOpenFail,
DimmCpuCommandsending,
DimmCpuLoading,
DimmCpuLoadOK,
DimmCpuLoadingFail,
TestEndFileGenerating,
TestEndFileGenerateOK,
TestEndFileGenerateFail,
AiroutFileGenerating,
AiroutFileGenerateOK,
AiroutFileGenerateFail,
AiroutOKFileGenerating,
AiroutOKFileGenerateOK,
AiroutOKFileGenerateFail,
AiroutOKFileButtonClickOK,
AllTestFileDeleting,
AllTestFileDeleteOK,
AllTestFileDeleteFail,
}
5.4.3 串口通讯
通过指定的串口名,定义好串口相符的波特率等属性,并添加接收数据的委托方法后,通过Open()方法开启串口。
//串口初始化
private bool SerialPortInit()
{
//串口初始化,正在打开串口
StatusUpdate(CurrentStep.SerialPortOpen, CurrentStatus.SerialPortConnecting);
//获取所有串口名
string[] allSp = SerialPort.GetPortNames();
Console.WriteLine("current COM list: " + string.Join(",", allSp));
try
{
//获取指定的通讯串口名
Com = ComTextBox.Text;
mySerialPort = new SerialPort(Com);
//定义串口的波特率等属性
mySerialPort.BaudRate = 115200;
mySerialPort.Parity = Parity.None;
mySerialPort.StopBits = StopBits.One;
mySerialPort.DataBits = 8;
mySerialPort.Handshake = Handshake.None;
//开启接收数据的委托
mySerialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
//打开串口
mySerialPort.Open();
if (mySerialPort.IsOpen)
{
//串口初始化,正在打开串口
StatusUpdate(currentStep, CurrentStatus.SerialPortConnected);
LogPrintFunction("COM口打开成功...");
return true;
}
}
catch (IOException ex)
{
StatusUpdate(currentStep, CurrentStatus.SerialPortOpenFail);
// 处理输入/输出错误
LogPrintFunction("输入/输出错误: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
StatusUpdate(currentStep, CurrentStatus.SerialPortOpenFail);
// 处理权限错误
LogPrintFunction("权限错误: " + ex.Message);
}
catch (InvalidOperationException ex)
{
StatusUpdate(currentStep, CurrentStatus.SerialPortOpenFail);
// 处理无效操作错误
LogPrintFunction("无效操作: " + ex.Message);
}
catch (Exception ex)
{
StatusUpdate(currentStep, CurrentStatus.SerialPortOpenFail);
LogPrintFunction("其他异常:" + ex.Message);
}
return false;
}
接收数据的方法定义:
private void DataReceivedHandler(
object sender,
SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
string indata = sp.ReadExisting();
Console.WriteLine("Data Received:" + indata);
if (indata == "5" )
{
//如果回复值等于5,则COM口通信成功
StatusUpdate(currentStep, CurrentStatus.SerialPortOpened);
}
else if (indata == "6" )
{
//如果回复值等于6,则治具已合盖
StatusUpdate(currentStep, CurrentStatus.FixtureCovered);
}
else if (indata == "1")
{
//如果回复值等于1,则扫码成功
StatusUpdate(currentStep, CurrentStatus.ScannOK);
}
else if (indata == "4" )
{
//如果回复值等于4,则夹具打开成功
StatusUpdate(currentStep, CurrentStatus.FixtureOpened);
}
else if (indata == "7" )
{
//如果回复值等于7,则CPU装载成功
StatusUpdate(currentStep, CurrentStatus.DimmCpuLoadOK);
}
else if (indata == "3" )
{
//如果回复值等于3或,则全部治具打开成功
StatusUpdate(currentStep, CurrentStatus.AllFixtureOpened);
}
}
5.4.4部分业务代码展示:
根据业务逻辑实现 发送命令,切换状态,使LED灯、提示文言、Log打印等。
下方为部分代码,具体请尝试自己实现吧~~你是最棒的!
private async Task WorkFlow()
{
FixtureOpenLabel.Text = "Fixture Open OK";
FixtureOpenRoundPanel.Color1 = PassColor;
StartButton.Enabled = false;
if (!GetConfigFile())
{
return;
}
while (true)
{
Console.WriteLine("cstep: " + currentStep + " cstatus: " + currentStatus);
//串口打开时
if (mySerialPort.IsOpen)
{
if (currentStep == CurrentStep.SerialPortOpen)
{
if (currentStatus == CurrentStatus.SerialPortConnected)
{
//发送5,等待回复 5ok
LogPrintFunction("COM口通信命令正在发送...");
CommandWrite("5", CurrentStatus.SerialPortOpening, "COM通信命令发送成功", CurrentStatus.SerialPortOpenFail, "COM通信命令发送失败");
}
else if (currentStatus == CurrentStatus.SerialPortOpening)
{
if (timeoutcount >= serialPortOpenTimeoutcount)
{
//通信超时
StatusUpdate(currentStep, CurrentStatus.SerialPortOpenFail);
LogPrintFunction("COM口通信已超时...");
}
else
{
timeoutcount += 1;
LogPrintFunction("COM口正在等待回复..." + timeoutcount + "s");
}
}
else if (currentStatus == CurrentStatus.SerialPortOpened)
{
//通信成功,切换治具盒盖模式
LogPrintFunction("COM口通信成功...");
timeoutcount = 0;
StatusUpdate(CurrentStep.FixtureCoverOperate, CurrentStatus.FixtureCoverCommandsending);
}
else if (currentStatus == CurrentStatus.SerialPortOpenFail)
{
//TODO 通信超时动作
break;
}
}
//省略了部分代码
}
else
{
//串口关闭了
StatusUpdate(currentStep, CurrentStatus.SerialPortClosed);
LogPrintFunction("串口被关闭了...");
break;
}
//间隔值500ms
await Task.Delay(1000);
}
StartButton.Enabled = true;
}
六、实机效果
6.1串口通讯成功
6.2治具合盖命令发送成功,治具已盒盖
6.3扫码成功
6.4测试完成,删除所有文件
6.5错误时的提示框
检测barcode.txt文件超时,弹出提示框,点击后,治具将恢复初始状态。
6.6配置文件展示
七、项目小结
通过该项目实现了c#的串口间通信,并熟悉了winform中控件的使用以及重构。对一个简单的业务进行实现,捋清逻辑关系,通过步骤和状态控制工作流程。
项目中还有很多不足之处,比如没有使用设计模式进行优化等,请各位海涵~
希望大家一起交流学习,共同成长!
八、感谢
感谢各位大佬的莅临,学习之路漫漫,吾将上下而求索。有任何想法请在评论区留言哦!
再次感谢!