上一篇中我们完成了一个串口助手的雏形,实现了基本发送和接收字符串功能,并将打开/关闭串口进行了异常处理,这篇就来按照流程,逐步将功能完善:

1、构思功能

  首先是接收部分,要添加一个“清空接收”的按钮来清空接收区;因为串口通信协议常用都是8bit数据(低7bit表示ASCII码,高1bit表示奇偶校验),作为一个开发调试工具,它还需要将这个8bit码用十六进制方式显示出来,方便调试,所以还需要添加两个单选框来选择ASCII码显示还是HEX显示;

  然后是发送部分,与之前对应,调试过程中还需要直接发送十六进制数据,所以也需要添加两个单选框来选择发送ASCII码还是HEX码;除了这个功能,还需要添加自动发送的功能,自动发送新行功能方便调试;

2、设计布局

  1)单选按钮控件(RadioButton)

    接收数据显示只能同时选中ASCII显示或者HEX显示,所以要用单选按钮控件,在同一组中(比如之前所讲述的容器)的单选按钮控件只能同时选中一个,刚好符合我们的要求;

  2复选框控件(CheckBox)

    这个通常被用于选择一些可选功能,比如是否显示数据接收时间,是否在发送时自送发送新行,是否开启自动发送功能等,它与之前的RadioButton都有一个很重要的属性 —— CHecked,若为false,则表示未被选中,若为true,则表示被选中;

  3)数值增减控件(NumericUpDown)

    显示用户通过单击控件上的上/下按钮可以增加和减少的单个数值,这里我们用来设置自动发送的间隔时长;

  4)定时器组件(Timer)

    这里之所以称为组件是因为它和之前的串口一样,都不能被用户直接操作;它是按用户定义的间隔引发事件的组件;

    Timer主要是Interval属性,用来设置定时值,默认单位ms;在设置定时器之后,可以调用Timer对象的start()方法和stop()方法来启动或者关闭定时器;在启动之后,Timer就会每隔Interval毫秒触发一次Tick事件,如果设置初始值为100ms,我们只需要设置一个全局变量i,每次时间到后i++,当i==10的时候,就表示计数值为1s(这里Timer的使用方法是不是和单片机相同^_^);

  整体设计出来的效果图如下:

3、搭建后台

  按照之前的思路,界面布局完成后,就要开始一个软件最重要的部分 —— 搭建后台:

1、状态栏串口状态显示

 这里直接添加代码即可,无需多言;

label6.Text = "串口已打开";
label6.ForeColor = Color.Green;
 label6.Text = "串口已关闭";
label6.ForeColor = Color.Red;

2、接收部分

  之前我们直接在串口接收事件中调用serialPort1.ReadExisting()方法读取整个接收缓存区,然后追加到接收显示文本框中,但在这里我们需要在底部状态栏显示接收字节数和发送字节数,所以就不能这样整体读取,要逐字节读取/发送并且计数;

  1)类的属性

  首先定义一个用于计数接收字节的变量,这个变量的作用相当于C语言中的全局变量,在C#中称之为类的属性,这个属性可以被这个类中的方法所访问,或者通过这个对象来访问,代码如下:

 public partial class Form1 : Form
{
private long receive_count = ; //接收字节计数, 作用相当于全局变量
.......
}

  2)按字节读取缓冲区

  首先通过访问串口的BytesToRead属性获取到接收缓冲区中数据的字节数,然后调用串口的Read(byte[ ] buffer, int offset, int count)方法从输入缓冲区读取一些字节并将那些字节写入字节数组中指定的偏移量处

 //串口接收事件处理
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
int num = serialPort1.BytesToRead; //获取接收缓冲区中的字节数
byte[] received_buf = new byte[num]; //声明一个大小为num的字节数据用于存放读出的byte型数据 receive_count += num; //接收字节计数变量增加nun
serialPort1.Read(received_buf,,num); //读取接收缓冲区中num个字节到byte数组中

       //未完,见下
}

  上一步我们将串口接收缓冲区中的数据按字节读取到了byte型数组received_buf中,但是要注意,这里的数据全部是byte型数据,如何显示到接收文本框中呢?要知道接收文本框显示的内容都是以字符串形式呈现的,也就是说我们追加到文本框中的内容必须是字符串类型,即使是16进制显示,也是将数据转化为16进制字符串类型显示的,接下来讲述如何将字节型数据转化为字符串类型数据; 

 3)字符串构造类型(StringBuilder)

  我们需要将整个received_buf数组进行遍历,将每一个byte型数据转化为字符型,然后将其追加到我们总的字符串(要发送到接收文本框去显示的那个完整字符串)后面,但是String类型不允许对内容进行任何改动,更何况我们需要遍历追加字符,所以这个时候就需要用到字符串构造类型(StringBuilder),它不仅允许任意改动内容,还提供了Append,Remove,Replace,Length,ToString等等有用的方法,这个时候再来构造字符串就显得很简单了,代码如下:

  public partial class Form1 : Form
{
private StringBuilder sb = new StringBuilder(); //为了避免在接收处理函数中反复调用,依然声明为一个全局变量
//其余代码省略
}
//串口接收事件处理
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
//接第二步中的代码
sb.Clear(); //防止出错,首先清空字符串构造器
//遍历数组进行字符串转化及拼接
foreach (byte b in received_buf)
{
sb.Append(b.ToString());
}
try
{
//因为要访问UI资源,所以需要使用invoke方式同步ui
Invoke((EventHandler)(delegate
{
textBox_receive.AppendText(sb.ToString());
label7.Text = "Rx:" + receive_count.ToString() + "Bytes";
}
)
);
}
  //代码省略
}

接下来我们运行看一下效果:

  可以看到,当我们发送字符“1”的时候,状态栏显示接收到1byte数据,表明计数正常,但是接收到的却是字符形式的“49”,这是因为接收到的byte类型的数据存放的就是ASCII码值,而调用byte对象的ToString()方法,由下图可看到,这个方法刚好又将这个ASCII值49转化成为了字符串“49”,而不是对应的ASCII字符'1';

  4)C#类库——编码类(Encoding Class)

  接着上一个问题,我们需要将byte转化为对应的ASCII码,这就属于解码(将一系列编码字节转换为一组字符的过程),同样将一组字符转换为一系列字节的过程称为编码;

  这里因为转换的是ASCII码,有两种方法实现:第一种采用Encoding类的ASCII属性实现,第二种采用Encoding Class的派生类ASCIIEncoing Class实现,我们采用第一种方法实现,然后调用GetString(Byte[ ])方法将整个数组解码为ASCII数组,代码如下:

 sb.Append(Encoding.ASCII.GetString(received_buf));  //将整个数组解码为ASCII数组

再次运行一下,可以看到正常显示:

  5)byte类型值转化为十六进制字符显示

  在第3节中我们分析了byte.ToString()方法,它可以将byte类型直接转化为字符显示,比如接收到的是字符1的ASCII码值是49,就将49直接转化为“49”显示出来,在这里,我们需要将49用十六进制显示,也就是显示“31”(0x31),这种转化并没有什么实质上的改变,只是进行了数制转化而已,所以采用格式控制的ToString(String)方法,具体使用方法见下图:

  这里我们需要将其转化为2位十六进制文本显示,另外,由于ASCII和HEX只能同时显示一种,所以我们还要对单选按钮是否选中进行判断,代码如下:

  if (radioButton2.Checked)
{
//选中HEX模式显示
foreach (byte b in received_buf)
{
sb.Append(b.ToString("X2") + ' '); //将byte型数据转化为2位16进制文本显示,用空格隔开
}
}
else
{
//选中ASCII模式显示
sb.Append(Encoding.ASCII.GetString(received_buf)); //将整个数组解码为ASCII数组
}

  再来运行看一下最终效果(先发送“Mculover66”加回车,然后发送“1”加回车):

  6)日期时间结构(DateTime Struct)

  当我们勾选上显示接收数据时间时,要在接收数据前加上时间,这个时间通过DateTime Struct来获取,首先还是声明一个全局变量:

private DateTime current_time = new DateTime();    //为了避免在接收处理函数中反复调用,依然声明为一个全局变量

  这个时候current_time是一个DateTime类型,通过调用ToString(String)方法将其转化为文本显示,具体选用哪种见下图:

  在显示的时候,依然要对用户是否选中进行判断,代码如下:

//因为要访问UI资源,所以需要使用invoke方式同步ui
Invoke((EventHandler)(delegate
{
if (checkBox1.Checked)
{
//显示时间
current_time = System.DateTime.Now; //获取当前时间
textBox_receive.AppendText(current_time.ToString("HH:mm:ss") + " " + sb.ToString()); }
else
{
//不显示时间
textBox_receive.AppendText(sb.ToString());
}
label7.Text = "Rx:" + receive_count.ToString() + "Bytes";
}
)
);

  再来运行看一下效果:

  7)清空接收按钮

  这里就不需要多说了,直接贴代码:

 private void button3_Click(object sender, EventArgs e)
{
textBox_receive.Text = ""; //清空接收文本框
textBox_send.Text = ""; //清空发送文本框
receive_count = ; //计数清零
label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; //刷新界面
}

3、发送部分

  首先为了避免发送出错,启动时我们将发送按钮失能,只有成功打开后才使能,关闭后失能,这部分代码简单,自行编写;

  1)字节计数 + 发送新行

  有了上面的基础,实现这两个功能就比较简单了,要注意Write和WriteLine的区别:

  2)正则表达式的简单应用

  这是一个很重要很重要很重要的知识 —— 正则表达式!我们希望发送的数据是0x31,所以功能应该被设计为在HEX发送模式下,用户输入“31”就应该发送0x31,这个不难,只需要将字符串每2个字符提取一下,然后按16进制转化为一个byte类型的值,最后调用write(byte[ ] buffer,int offset,int count)将这一个字节数据发送就可以,那么,当用户同时输入多个十六进制字符呢该符合发送呢?

  这个时候就需要用到正则表达式了,用户可以将输入的十六进制数据用任意多个空格隔开,然后我们利用正则表达式匹配空格,并替换为“”,相当于删除掉空格,这样对整个字符串进行遍历,用刚才的方法逐个发送即可!

  完整的发送代码如下:

private void button2_Click(object sender, EventArgs e)
{
byte[] temp = new byte[];
try
{
//首先判断串口是否开启
if (serialPort1.IsOpen)
{
int num = ; //获取本次发送字节数
//串口处于开启状态,将发送区文本发送 //判断发送模式
if (radioButton4.Checked)
{
//以HEX模式发送
//首先需要用正则表达式将用户输入字符中的十六进制字符匹配出来
string buf = textBox_send.Text;
string pattern = @"\s";
string replacement = "";
Regex rgx = new Regex(pattern);
string send_data = rgx.Replace(buf, replacement); //不发送新行
num = (send_data.Length - send_data.Length % ) / ;
for (int i = ; i < num; i++)
{
temp[] = Convert.ToByte(send_data.Substring(i * , ), );
serialPort1.Write(temp, , ); //循环发送
}
//如果用户输入的字符是奇数,则单独处理
if (send_data.Length % != )
{
temp[] = Convert.ToByte(send_data.Substring(textBox_send.Text.Length-,), );
serialPort1.Write(temp, , );
num++;
}
//判断是否需要发送新行
if (checkBox3.Checked)
{
//自动发送新行
serialPort1.WriteLine("");
}
}
else
{
//以ASCII模式发送
//判断是否需要发送新行
if (checkBox3.Checked)
{
//自动发送新行
serialPort1.WriteLine(textBox_send.Text);
num = textBox_send.Text.Length + ; //回车占两个字节
}
else
{
//不发送新行
serialPort1.Write(textBox_send.Text);
num = textBox_send.Text.Length;
}
} send_count += num; //计数变量累加
label8.Text = "Tx:" + send_count.ToString() + "Bytes"; //刷新界面
}
}
catch (Exception ex)
{
serialPort1.Close();
//捕获到异常,创建一个新的对象,之前的不可以再用
serialPort1 = new System.IO.Ports.SerialPort();
//刷新COM口选项
comboBox1.Items.Clear();
comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
//响铃并显示异常给用户
System.Media.SystemSounds.Beep.Play();
button1.Text = "打开串口";
button1.BackColor = Color.ForestGreen;
MessageBox.Show(ex.Message);
comboBox1.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
comboBox4.Enabled = true;
comboBox5.Enabled = true;
}
}

下面来看看运行效果:

  3)定时器组件(Timer)

  自动发送功能是我们搭建的最后一个功能了,第2节介绍定时器组件的时候已经说过,这个定时器和单片机中的定时器用法基本一样,所以,大致思路如下:当勾选自动发送多选框的时候,将右边数值增减控件的值赋给定时器作为定时值,同时将右边数值选择控件失能,然后当定时器时间到后,重新定时器值并调用发送按钮的回调函数,当为勾选自动发送的时候,停止定时器,同时使能右边数值选择控件,代码如下:

private void checkBox2_CheckedChanged(object sender, EventArgs e)
{
if (checkBox2.Checked)
{
//自动发送功能选中,开始自动发送
numericUpDown1.Enabled = false; //失能时间选择
timer1.Interval = (int)numericUpDown1.Value; //定时器赋初值
timer1.Start(); //启动定时器
label6.Text = "串口已打开" + " 自动发送中...";
}
else
{
//自动发送功能未选中,停止自动发送
numericUpDown1.Enabled = true; //使能时间选择
timer1.Stop(); //停止定时器
label6.Text = "串口已打开"; }
} private void timer1_Tick(object sender, EventArgs e)
{
//定时时间到
button2_Click(button2, new EventArgs()); //调用发送按钮回调函数
}

运行一下看一下效果:

  

C#上位机开发(四)—— SerialAssistant功能完善的更多相关文章

  1. C#上位机开发(二)—— Hello,World

    上一篇大致了解了一下单片机实际项目开发中上位机开发部分的内容以及VS下载与安装,按照编程惯例,接下来就是“Hello,World!” 1.新建C#项目工程 首先选择新建Windows窗体应用(.NET ...

  2. 上位机开发之西门子PLC-S7通信实践

    写在前面: 就目前而言,在中国的工控市场上,西门子仍然占了很大的份额,因此对于上位机开发而言,经常会存在需要与西门子PLC进行通信的情况.然后对于西门子PLC来说,通信方式有很多,下面简单列举一下: ...

  3. Winform 快速开发框架,上位机开发,工控机程序开发,CS程序开发

    1.当客户让你做个CS程序时,当你手上一穷二白,所有都要重复造轮,你是不是很烦. 2.但如果有一个通用的,快速开发框架,就可以把你从这些基础的工作解救出来,你专注做业务就好了. 3.本人其中一个项目的 ...

  4. 上位机开发之三菱Q系列PLC通信实践

    经常关注我们公众号或者公开课的学员(如果还没有关注的话,左上角点击一波关注)应该知道,我们会经常使用西门子PLC,其实对于其他品牌的PLC,我们都会讲到,包括三菱.欧姆龙.基恩士.松下及国产台达.信捷 ...

  5. 上位机开发之三菱FX3U以太网通信实践

    上次跟大家介绍了一下上位机与三菱Q系列PLC通信的案例,大家可以通过点击这篇文章:上位机开发之三菱Q系列PLC通信实践(←戳这里) 今天以三菱FX3U PLC为例,跟大家介绍一下,如何实现上位机与其之 ...

  6. 【专题教程第8期】基于emWin模拟器的USB BULK上位机开发,仅需C即可,简单易实现

    说明:1.如果你会emWin话的,就可以轻松制作上位机.做些通信和控制类上位机,比使用C#之类的方便程度一点不差,而且你仅会C语言就可以.2.并且成功将emWin人性化,可以做些Windows系统上的 ...

  7. USBCAN的使用和上位机开发(MFC)

    USBCAN使用手册 参见:https://blog.51cto.com/12572800/2062839 1. USB CAN软件安装与硬件接线 USB CAN是常用的CAN测试工具.它的软件资料存 ...

  8. 周立功USBCAN-II 上位机开发(MFC)

    使用的USB转CAN的设备是周立功的USBCAN-II,在购买的时候,会有上位机二次开发的库文件.例程和API文档等材料,可以参考. 1.库函数的调用 首先,把库函数文件都放在工作目录下.库函数文件总 ...

  9. C#上位机开发(一)—— 了解上位机

    在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形.温度等).用户控制(LED,继电器等),下位机(单片机)与 上位机之间要进行数据通信的两种方式都是基于串口的: USB转串口 — ...

随机推荐

  1. 题解报告:hdu 2844 & poj 1742 Coins(多重部分和问题)

    Problem Description Whuacmers use coins.They have coins of value A1,A2,A3...An Silverland dollar. On ...

  2. 题解报告:hdu1995汉诺塔V(递推dp)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1995 Problem Description 用1,2,...,n表示n个盘子,称为1号盘,2号盘,. ...

  3. jmeter(一)工具介绍(一)

    一.JMeter 介绍 Apache JMeter是100%纯JAVA桌面应用程序,被设计为用于测试客户端/服务端结构的软件(例如web应用程序).它可以用来测试静态和动态资源的性能,例如:静态文件, ...

  4. xUtils 简介

    ## xUtils简介* xUtils 包含了很多实用的android工具.* xUtils 最初源于Afinal框架,进行了大量重构,使得xUtils支持大文件上传,更全面的http请求协议支持(1 ...

  5. Jquery插件jqprint-0.3.js实现打印

    1.首先引用Jquery和jqprint-0.3.js(依赖于Jquery的) <script language="javascript" src="jquery- ...

  6. vim设置默认显示行号

    vim /root/.vimrc 设置在当前登录用户根目录下,.vimrc文件本身不存在,创建后之间添加下面配置保存即可 set number

  7. hibernate 批量插入数据

    如题,有两种方法 1)使用FLUSH 2)使用JDBC 分别来解释: 1)hibernate在进行数据库操作的时候,都要有事务支持的.可能你曾遇到过,没有加事务,程序会报错的情况. 而事务每次提交的时 ...

  8. C# 方法 虚方法的调用浅谈 引用kdalan的博文

    我们在面试中经常碰到有关多态的问题,之前我也一直被此类问题所困扰,闹不清到底执行哪个方法. 先给出一道简单的面试题,大家猜猜看,输出是?     public class A    {         ...

  9. vue2.0 路由知识一(路由的创建的全过程)

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  10. vue2.0 静态prop和动态prop

    动态prop: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <t ...