前言
根据前两个例子,我们现在已经能凑活地接收到一条完整的响应数据了。我们想再添加一个读取按钮,并将返回的响应数据处理一下,将其填到我们预期的输入框中。在“我一言,你一语”的业务场景中,我们可以定义一个标识变量,在接收事件中进行区分各个操作的解析方法。接下来,我们继续完善接收数据流程,并添加新需求。
环境
IDE: VS 2019
环境: .NET Core 3.1
依赖: System.IO.Ports、thinger.DataConvertLib
测试工具: Modbus Slave
结构
SerialPortDemo-03
└── CRC.cs
└── MarinForm.cs
└── Program.cs
界面
定义变量
private SerialPort serialPort = new SerialPort();
// 接收数据缓存
private MemoryStream memoryBuffer = new MemoryStream();
// 串口打开标识
private bool open = false;
// 操作标识
private int action = -1; // 0 读取; 1 请求解析读取
// 按钮控制列表
private List<Button> buttonList = new List<Button>();
控件初始化
private void MainForm_Load(object sender, EventArgs e)
{
// 获取当前可识别的串口
string[] port = System.IO.Ports.SerialPort.GetPortNames();
// 初始化界面
comboBoxPortName.Items.AddRange(port);
// 如果有数据显示第0个
comboBoxPortName.SelectedIndex = comboBoxPortName.Items.Count > 0 ? 0 : -1;
comboBoxBaudRate.Items.Add(1200);
comboBoxBaudRate.Items.Add(4800);
comboBoxBaudRate.Items.Add(9600);
comboBoxBaudRate.Items.Add(14400);
comboBoxBaudRate.Items.Add(19200);
comboBoxBaudRate.Items.Add(38400);
comboBoxBaudRate.SelectedItem = 9600;
// 按钮加入控制列表
buttonList.Add(buttonRead);
buttonList.Add(buttonRead1);
}
打开串口
private void buttonOpen_Click(object sender, EventArgs e)
{
try
{
if (!open)
{
serialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPortDataReceived);
serialPort.PortName = comboBoxPortName.Text;
serialPort.BaudRate = int.Parse(comboBoxBaudRate.Text);
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Parity = Parity.None;
serialPort.Open();
buttonOpen.Text = "关闭串口";
open = true;
}
else
{
serialPort.Close();
buttonOpen.Text = "打开串口";
open = false;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
接收事件
private void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
serialPort = (SerialPort)sender;
byte[] bytebuffer = new byte[serialPort.BytesToRead];
serialPort.Read(bytebuffer, 0, bytebuffer.Length);
// 将接收到的数据写入缓存
memoryBuffer.Write(bytebuffer);
Debug.WriteLine("-------");
Debug.WriteLine("mem length:" + memoryBuffer.Length);
// 判断缓存长度 至少包括数据长度
if (memoryBuffer.Length <= 2)
{
return;
}
// 整包数据长度
byte length = (byte)(ByteLib.GetByteFromByteArray(memoryBuffer.ToArray(), 2) + 5);
Debug.WriteLine("length:" + length);
// 判断缓存长度
if (memoryBuffer.Length < length)
{
return;
}
byte[] temp = new byte[length - 2 ];
byte[] tempCRC = new byte[2];
Array.Copy(memoryBuffer.ToArray(), temp, temp.Length );
Array.Copy(memoryBuffer.ToArray(), temp.Length , tempCRC, 0, 2);
byte[] needCheckTemp = CRC.crc16(temp);
Debug.WriteLine(StringLib.GetHexStringFromByteArray(temp) + " / " + StringLib.GetHexStringFromByteArray(tempCRC) + " ");
Debug.WriteLine(StringLib.GetHexStringFromByteArray(needCheckTemp));
Debug.WriteLine(!(tempCRC[0] == needCheckTemp[length - 2] && tempCRC[1] == needCheckTemp[length - 1]));
Debug.WriteLine(tempCRC[0] + " " + needCheckTemp[length - 2] + " " + tempCRC[1] + " " + needCheckTemp[length - 1]);
// 数据长度满足条件,判断校验位
if (!(tempCRC[0] == needCheckTemp[length - 2] && tempCRC[1] == needCheckTemp[length - 1]))
{
// 缓存清空
memoryBuffer = new MemoryStream();
// 操作标识重置
action = -1;
// 按钮恢复
Invoke(new Action(() => buttonList.ForEach(button => button.Enabled = true)));
return;
}
// 判断操作
switch (action)
{
case 0:
// 接收事件与界面并不是同一个线程;所以需要用Invoke()方法同步更新界面
Invoke(new Action(() => textBoxLog.AppendText("收到响应:" + StringLib.GetHexStringFromByteArray(temp) + "\r\n")));
MessageBox.Show("读取完成");
break;
case 1:
// 接收事件与界面并不是同一个线程;所以需要用Invoke()方法同步更新界面
Invoke(new Action(() => {
// 响应的数据
// 01 03 06 00 00 00 02 00 00 80 B5
// 0x01 -- 从机地址; 0x03 -- 功能码; 0x06 -- 数据长度
// 0x00 0x00 -- 第一个寄存器的值
// 0x00 0x02 -- 第二个寄存器的值
// 0x00 0x00 -- 第三个寄存器的值
// 0x80 0xB5 -- 校验位
byte[] data = new byte[6];
Array.Copy(temp, 3, data, 0, 6);
// 将数据填到对应输入框中
textBoxAddress1.Text = ShortLib.GetShortFromByteArray(data).ToString();
textBoxAddress2.Text = ShortLib.GetShortFromByteArray(data, 2).ToString();
textBoxAddress3.Text = ShortLib.GetShortFromByteArray(data, 4).ToString();
textBoxLog.AppendText("收到响应:" + StringLib.GetHexStringFromByteArray(memoryBuffer.ToArray()) + "\r\n");
MessageBox.Show("读取完成");
}));
break;
}
// 缓存清空
memoryBuffer = new MemoryStream();
// 操作标识重置
action = -1;
// 按钮恢复
Invoke(new Action(()=> buttonList.ForEach(button => button.Enabled = true)));
}
catch (Exception)
{
throw;
}
}
发送读取命令
private void buttonRead_Click(object sender, EventArgs e)
{
try
{
// 按钮失效
buttonList.ForEach(button => button.Enabled = false);
// 定义操作标识
action = 0;
// 0x01 -- 从机地址; 0x03 -- 功能码; 0x00 -- 高位起始地址
// 0x00 -- 低位起始地址; 0x00 -- 高位读取寄存器个数
// 0x09 -- 低位读取寄存器个数
// 0x85 0xCC -- 校验位
byte[] request = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x64, 0x44, 0x21 };
//byte[] request = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x9, 0x85, 0xCC };
textBoxLog.AppendText("发送请求:" + StringLib.GetHexStringFromByteArray(request) + "\r\n");
serialPort.Write(request, 0, request.Length);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
发送读取命令1
private void buttonRead1_Click(object sender, EventArgs e)
{
try
{
// 按钮失效
buttonList.ForEach(button => button.Enabled = false);
// 定义操作标识
action = 1;
byte[] request = new byte[] { 0x01, 0x03, 0x00, 0x01, 0x00, 0x03, 0x54, 0x0B };
// 缓存清空
memoryBuffer = new MemoryStream();
textBoxLog.AppendText("发送请求:" + StringLib.GetHexStringFromByteArray(request) + "\r\n");
serialPort.Write(request, 0, request.Length);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
测试
启动Modbus Slave
使用Modbus Slave连接到虚拟串口中,并开启100个寄存器作为测试使用,然后将寄存器地址2的值改为2。