编写水文专业串口通讯软件的开发经历
- 一、关于开发 YAC9900 水位雨量 RTU 通讯软件
- 二、软件开发遇到的问题和困难
- 1、开发架构的适应
- 2、开发语言的学习
- 3、.net core 8 架构中串口构建的难点
- 4、YAC9900 水位雨量 RTU 通讯软件开发中的 UI 冻结
- 三、发现问题解决问题的具体办法
- 1、预置是否没有执行完 invoke 的 bool 开关,是否关闭串口的 bool 开关,是否连续发送命令的 bool 开关
- 2、在串口打开或关闭中处理上面第一项的 bool 关系
- 3、在串口SerialDataReceivedEventHandler(GetMessageFromEquipment)事件中处理中断
- 四、程序界面
一、关于开发 YAC9900 水位雨量 RTU 通讯软件
YAC9900 水位雨量 RTU 是长江一方公司开发的一款用于水文测量的水位雨量记录 RTU,能接入多种水位传感器。新版 YAC9900 主板重新设计后功能强大,但用于 YAC9900 通讯和设置参数的软件很旧,尽管用起来不错。于是开发一款新的软件,采用 Microsoft Visual Studio C# 开发,分别采用 .net core 8 架构和 .net Framework 4.8 进行编译。
二、软件开发遇到的问题和困难
1、开发架构的适应
以前学习 .net 开发,都是在 .net Framework 4.8 进行,这次迁移到 .net core 8 架构,学习了不少知识。.net core 8 语言更加简练和方便,提示更加全面。唯一的遗憾是编译后,产生的库文件太多,尽管程序用到库不多,但 Visual Studio 还没有智能到只生成程序依赖的库,所以一股脑的把很多库都给塞进了编译输出目录,其中很多都是不需要的库。.net core 8 架构可以编译发布产生单文件执行软件,这个单文件也是一股脑的把很多库都给包进了编译的单文件,导致单文件有 145M,其实不包含库程序不到 1M。
2、开发语言的学习
.net core 8 架构中,学习了很多,如 ? 和 ?? 运算符、三元条件运算符,索引和范围范围运算符 […index],替代了很多 if else 、 Substring 、IndexOf、LastIndexOf 语句,包括检索字符串 Contains 语句等等。学习了与 .net Framework 4.8 很多的不同点。
3、.net core 8 架构中串口构建的难点
.net core 8 架构中,System.IO.Ports 组件不再像 .net Framework 那样内置,需要通过管理 NuGet 程序包下载。高版本的.net core 8 架构更多涉及软件的安全性能,所以在串口访问中,对于数据处理的结果,运用到 UI 界面,不能再像 Visual Studio 2017 以前那样处理,处理不好就导致程序界面冻结、卡死、死锁,因为 UI 界面的刷新必须使用和考虑线程和委托的开始和终结。如何让 UI 主线程与串口通讯线程和委托互不影响和干扰,就很重要了。
4、YAC9900 水位雨量 RTU 通讯软件开发中的 UI 冻结
由于软件开发中采用各种通讯指令连续多发,在关闭串口或窗口时,极易发生 UI 冻结,软件死锁卡死,只能在任务管理器中终结进程。原因在于窗口通讯线程任务在进行中,没有处理好中断任务,导致线程打架引起程序 UI 冻结。
三、发现问题解决问题的具体办法
出现最大的问题就是 UI 界面刷新和界面冻结,关键在于串口的事件 SerialDataReceivedEventHandler 和 UI 界面刷新的委托 Invoke 处理完善。问题就得到完美的解决。
1、预置是否没有执行完 invoke 的 bool 开关,是否关闭串口的 bool 开关,是否连续发送命令的 bool 开关
public partial class Form1 : Form
{
string[] StationType = new string[] { "雨量站", "并行水位站", "并行水文站", "串行水位站", "串行水文站", "水温站" };
string[] Channel = new string[] { "无效", "PSTN", "北斗卫星", "GSM", "GPRS" };
string[] DebugMsg = new string[] { "打开", "关闭" };
string[] SensorType = new string[] {"SDI-12 WL3100 (HS40)","SDI-12 WFX-40 (伟思浮子式)","RS485 WFX-40 (伟思浮子式)","RS485 OTT (德国HACH)",
"RS485 ISO","Sens","RS485 MPM (麦克压阻式)","RS485 Tem","Sens8 (武汉环宇压阻式)","VEGA","Sens10 (XYJ固件VEGAM)","Sens11 (XYJ固件HXDLS)",
"VEGAM (XYJ固件HXRad)","Sens13 (XYJ固件WFX40G)","Sens14","Sens15","Sens16","Sens17","Sens18"};
private static SerialPort serialPort = new SerialPort();
private bool WhenInvokeg = false;//是否没有执行完invoke相关操作
private bool closing = false;//是否正在关闭串口,执行Application.DoEvents,并阻止再次invoke
private bool SendCommand = false;//是否连续发送命令
public Form1()
{
InitializeComponent();
InitializeCustom();
}
}
2、在串口打开或关闭中处理上面第一项的 bool 关系
关键在于及时取消连续发送命令 SendCommand = false,并中断串口 GetMessageFromEquipment 事件继续 closing = true 。
/// <summary>打开串口过程</summary>
private void OpenPort()
{
try
{
if (serialPort != null && serialPort.IsOpen)
{
SendCommand = false;//取消连续发送命令
closing = true;//是要关闭串口,中断串口 GetMessageFromEquipment 事件继续
serialPort.DataReceived -= GetMessageFromEquipment;
while (WhenInvokeg) Application.DoEvents();//执行完 invoke 才能关闭串口
serialPort.Close();
closing = false;//
StatusMessage2.Text = "";
StatusMessage4.Text = "已经关闭端口 " + serialPort.PortName;
StatusMessage6.Text = "可以将消息框内容转换为16进制了";
OpenPortToolButton.Image = Resources.Close;
}
else
{
if (serialPort == null) serialPort = new SerialPort();//如果不存在则新建端口
serialPort.ReadBufferSize = 4096;//缓冲大小
serialPort.WriteBufferSize = 4096;//缓冲大小
serialPort.PortName = _PortName; // 设置串口名称
serialPort.BaudRate = _BaudRate; // 设置波特率
serialPort.Parity = _Parity; // 设置奇偶校验
serialPort.DataBits = _DataBit; // 设置数据位数
serialPort.StopBits = _StopBits; // 设置停止位
serialPort.Handshake = _Handshake; // 设置握手协议
//serialPort.ReadIntervalTimeout = 100;
// serialPort.NewLine = "\r\n";//解释 ReadLine( )和WriteLine( )方法调用结束的值 默认值“\n”
//serialPort.RtsEnable = true;
// serialPort.Encoding = Encoding.GetEncoding("iso-8859-1"); //支持汉字显示//"GB2312"//"iso-8859-1"
serialPort.DataReceived += new SerialDataReceivedEventHandler(GetMessageFromEquipment);
serialPort.Open(); // 打开串口
StatusMessage2.Text = "";
StatusMessage4.Text = "已经打开端口 " + serialPort.PortName;
StatusMessage6.Text = "";
OpenPortToolButton.Image = Resources.Open;
}
}
catch (Exception ex)
{
if (serialPort == null) serialPort = new SerialPort();//如果不存在则新建端口
serialPort.PortName = _PortName; // 设置串口名称
serialPort.BaudRate = _BaudRate; // 设置波特率
StatusMessage2.Text = ex.Message;
//StatusMessage4.Text = Messaging(ex.Message);
// 处理异常消息(VS2022使用Trace进行调试显示)
//Trace.WriteLine(ex.Source); Trace.WriteLine(ex.StackTrace); Trace.WriteLine(ex.Message);
// Trace.WriteLine(ex.GetType().Name);Trace.WriteLine(ex.ToString());
//MessageBox.Show(ex.Message + "\r" + ex.Source + "\r" + ex.StackTrace, "错误消息");
}
}
3、在串口SerialDataReceivedEventHandler(GetMessageFromEquipment)事件中处理中断
关键在于 closing = true 中断串口 GetMessageFromEquipment 事件继续委托线程。使 WhenInvokeg = false 不再发生委托。
private void GetMessageFromEquipment(object sender, SerialDataReceivedEventArgs e)//从设备中获取信息(串口)
{
if (closing) return;//如果正在关闭,忽略操作,直接返回
string PortMessage = "";//串口消息
WhenInvokeg = true;//设置在委托调用标记,已经开始接收数据
if (InvokeRequired)
{//更新UI的同步委托
//BeginInvoke(new Action(() =>
Invoke(new Action(() =>
// Invoke((EventHandler)(delegate
{
try
{ // 更新UI的代码
StatusMessage4.Text = "串口正在通讯,线程委托正在更新UI界面!";
Application.DoEvents();
Delayed(PortBufferInterval);//等待缓冲数据
PortMessage = serialPort.ReadLine().Trim();//去前后空格和回车后的端口消息 ,读行
// int nums = serialPort.BytesToRead;
// byte[] receiveBytes = new byte[nums];
// serialPort.Read(receiveBytes, 0, nums);//读字节
// PortMessage = Encoding.ASCII.GetString(receiveBytes);
if (PortMessage.Length > 0)
{
switch (TabWorkbenches.SelectedIndex)
{
case 0:
case 1:
Yac9900PortMessaging(PortMessage);//消息处理和界面更新
break;
case 2:
break;
case 3:
break;
}
}
}
catch (Exception ex)
{ // 处理异常
StatusMessage2.Text = "程序线程上执行的委托异常!" + ex.Message;
//Trace.WriteLine(ex.Message);//VS2022使用 Trace 显示调试
//Console.WriteLine(ex.Message);
MessageBox.Show(ex.Message + "\r" + ex.Source + "\r" + ex.StackTrace, "错误消息");
}
finally
{
StatusMessage4.Text = "通讯串口消息已经完成! 等待新的指令或消息!";
WhenInvokeg = false;//没有调用了,UI可以关闭串口了。
}
}));
}
else
{
try
{ // 更新UI的代码
StatusMessage4.Text = "串口正在通讯,并更新UI界面!";
Application.DoEvents();
Delayed(PortBufferInterval);//等待缓冲数据
PortMessage = serialPort.ReadLine().Trim();
if (PortMessage.Length > 0)
{
switch (TabWorkbenches.SelectedIndex)
{
case 0:
case 1:
Yac9900PortMessaging(PortMessage);
break;
case 2:
break;
case 3:
break;
}
}
}
catch (Exception ex)
{
// 处理异常
StatusMessage2.Text = ex.Message;
// StatusMessage4.Text = Messaging(ex.Message);
//Trace.WriteLine(ex.Message);
//Console.WriteLine(ex.Message);
MessageBox.Show(ex.Message + "\r" + ex.Source + "\r" + ex.StackTrace, "错误消息");
}
finally
{
StatusMessage4.Text = "通讯串口消息已经完成! 等待新的指令或消息!";
WhenInvokeg = false;//没有调用了,UI可以关闭串口了。
}
}
}
4、在窗体关闭前处理消息
关键在于及时取消连续发送命令 SendCommand = false 。避免继续产生串口事件,产生委托线程打架。
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (serialPort.IsOpen)
{
try
{
if (WhenInvokeg)//如果没有委托,可以关闭程序
{
SendCommand = false;//取消连续发送命令
StatusMessage6.Text = "已取消剩下的发送命令!再次点击就退出!";
e.Cancel = true; // 暂时不能退出窗体,串口指令和消息完成后再关闭窗口
}
else
{
if (serialPort != null && serialPort.IsOpen) OpenPort(); // 关闭串口
e.Cancel = false;
}
}
catch (Exception ex)
{
MessageBox.Show("无法关闭串口:" + ex.Message);
}
}
}
5、串口连续指令发送的中断处理
关键在于 SendCommand = false 取消连续发生指令,不再发生新的串口事件。
/// <summary>执行可以读写的YAC9900命令</summary>
private void CanRwCommands()//可读写命令区
{
try
{
string setStr = "";
if (checkDT.Checked && SendCommand)//是否选中时间读写和允许发送指令,
{
if (CheckSet.Checked)//是否设置
{
if (CheckUseSystemTime.Checked)
{
setStr = DateTime.Now.ToString("yyyyMMddHHmmss").Trim();
}
else
{
setStr = DT_Picker.Value.ToString("yyyyMMddHHmmss").Trim();
}
}
serialPort.Write("DT" + setStr);
Delayed(SendCommandInterval);
}
if (checkStationCode.Checked && SendCommand)
{
//发送指令与上雷同
}
if (checkStorageWater.Checked && SendCommand)
{
//发送指令与上雷同
}
if (checkWaterBase.Checked && SendCommand)
{
//以下省略很多指令
}
}
catch (Exception ex)
{
// 处理异常
StatusMessage2.Text = ex.Message;
StatusMessage4.Text = Messaging(ex.Message);
Trace.WriteLine(ex.Source);
//Console.WriteLine(ex.Message);
//MessageBox.Show(ex.Message + "\r" + ex.Source + "\r" + ex.StackTrace, "错误消息");
}
}
四、程序界面
程序主界面:
串口设置界面,自动搜索串口集合,自动捕获 USB 串口插入: