C# WPF燃气报警器记录读取串口工具
概要
- 符合国标文件《GB+15322.2-2019.pdf》串口通信协议定义;
- 可读取燃气报警器家用版设备历史记录信息等信息;
串口帧数据
串口通信如何确定一帧数据接收完成是个麻烦事,本文采用最后一次数据接收完成后再过多少毫秒认为一帧数据接收完成,开始解析出来。每次接收到数据更新一次recvTimems 。定时器mTimer定时周期10毫秒,定时器回调函数里判断接收时间超过20ms(这个时间的长短和串口波特率有关)认为一帧数据接收完成。接收数据时间差未超过20ms则将接收数据追加到rxBuf数据缓冲,
csharp
long recvTimems = 0; // 用于计算串口接收完一帧数据
int rxLen = 0; // 串口接收到的数据长度
byte[] rxBuff = new byte[128]; // 串口数据接收缓存
private static Timer mTimer; // 定时器,10ms执行一次
mTimer = new Timer(recvTimerCalback, null, 0, 10); // 创建并启动定时器
private void recvTimerCalback(object obj)
{
//Console.WriteLine("timer callback!" + recvTimems);
this.Dispatcher.Invoke(new Action(()=> {
// UI线程分离,更新UI数据显示
if (((Environment.TickCount - recvTimems) >= 20) && (rxLen > 0))
{
// 串口接收时间超过X ms认为一帧数据接收完成
这里解析处理接收数据.....
// 一帧数据处理结束清空数据缓冲
Array.Clear(rxBuff, 0, rxBuff.Length);
rxLen = 0;
}
}));
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort comPort = (SerialPort)sender;
try
{
recvTimems = Environment.TickCount; // 更新串口数据接收时间
//rxBuff[rxLen] = mSerialPort.ReadByte();
int readCnt = mSerialPort.BytesToRead;
Console.WriteLine("in readCnt:" + readCnt);
mSerialPort.Read(rxBuff, rxLen, readCnt);
rxLen += readCnt;
Console.WriteLine("out rxLen:" + rxLen);
}
catch (Exception ce)
{
MessageBox.Show(ce.Message);
}
}
布局文件
XAML
html
<Window x:Class="GasAlarmTestTool.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GasAlarmTestTool"
mc:Ignorable="d"
Title="燃气报警器接口工具" Height="600" Width="800">
<Grid>
<TabControl>
<TabItem>
<TabItem.Header>
<StackPanel>
<Label>串口设置</Label>
</StackPanel>
</TabItem.Header>
<StackPanel Orientation="Horizontal">
<GroupBox Header="串口" Width="200">
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">端口号</Label>
<ComboBox x:Name="comboBoxCOM" Margin="3" Height="20" Width="130" DragDrop.Drop="comboBoxCOM_Drop"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">波特率</Label>
<ComboBox x:Name="comboBoxBaudRate" Margin="3" Height="20" Width="130" SelectedIndex="0" IsEditable="True" >
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">数据位</Label>
<ComboBox x:Name="comboBoxDataBit" Margin="3" Height="20" Width="130" SelectedIndex="3">
<ComboBoxItem>5</ComboBoxItem>
<ComboBoxItem>6</ComboBoxItem>
<ComboBoxItem>7</ComboBoxItem>
<ComboBoxItem>8</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">停止位</Label>
<ComboBox x:Name="comboBoxStopBit" Margin="3" Height="20" Width="130" SelectedIndex="0">
<ComboBoxItem>1位</ComboBoxItem>
<ComboBoxItem>1.5位</ComboBoxItem>
<ComboBoxItem>2位</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">校验位</Label>
<ComboBox x:Name="comboBoxSdd" Margin="3" Height="20" Width="130" SelectedIndex="0">
<ComboBoxItem>无校验</ComboBoxItem>
<ComboBoxItem>奇校验</ComboBoxItem>
<ComboBoxItem>偶校验</ComboBoxItem>
<ComboBoxItem>1 校验</ComboBoxItem>
<ComboBoxItem>0 校验</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="3" Height="25" Width="50">流控位</Label>
<ComboBox x:Name="comboBoxlik" Margin="3" Height="20" Width="130" SelectedIndex="0" >
<ComboBoxItem>无流控</ComboBoxItem>
<ComboBoxItem>RTS/CTS</ComboBoxItem>
<ComboBoxItem>XON/XOFF</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="btnOpenCloseCom" Margin="3" Width="80" Height="25" Click="btnOpenCloseCom_Click">打开串口</Button>
<Button x:Name="btnClearRecv" Margin="3" Width="80" Height="25" Click="btnClearRecv_Click">清空接收</Button>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="接收数据" MinWidth="590" MaxWidth="1000">
<ScrollViewer>
<ScrollViewer.Content>
<TextBlock x:Name="textBlockRecv" MinWidth="300" MaxWidth="1000"></TextBlock>
</ScrollViewer.Content>
</ScrollViewer>
</GroupBox>
</StackPanel>
</TabItem>
<TabItem>
<TabItem.Header>
<StackPanel>
<Label>数据读取</Label>
</StackPanel>
</TabItem.Header>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<GroupBox Header="记录总数">
<StackPanel Orientation="Vertical">
<Button x:Name="btnReadRecCount" Margin="3" Padding="5" Click="btnReadRecCount_Click">读取记录总数</Button>
<StackPanel Orientation="Horizontal">
<Label>报警记录总数:</Label>
<TextBox x:Name="txtBoxWarningCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>报警恢复记录总数:</Label>
<TextBox x:Name="txtBoxWarningRecoveryCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>故障记录总数:</Label>
<TextBox x:Name="txtBoxFaultCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>故障恢复记录总数:</Label>
<TextBox x:Name="txtBoxFaultRecoryCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>掉电记录总数:</Label>
<TextBox x:Name="txtBoxPoweroffCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>上电记录总数:</Label>
<TextBox x:Name="txtBoxPoweronCount">0</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>传感器失效记录总数:</Label>
<TextBox x:Name="txtSensorInvalidCount">0</TextBox>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Header="日期时间">
<StackPanel Orientation="Vertical">
<Button x:Name="btnReadDateTime" Click="btnReadDateTime_Click">读取日期时间</Button>
<TextBox x:Name="txtBoxDateTime">0000-00-00 00:00</TextBox>
</StackPanel>
</GroupBox>
</StackPanel>
<StackPanel Orientation="Vertical">
<GroupBox Header="读取指定记录">
<StackPanel Orientation="Horizontal">
<ComboBox x:Name="cmboxRecordType" Width="120" SelectedIndex="0">
<ComboBoxItem>报警记录</ComboBoxItem>
<ComboBoxItem>报警恢复记录</ComboBoxItem>
<ComboBoxItem>故障记录</ComboBoxItem>
<ComboBoxItem>故障恢复记录</ComboBoxItem>
<ComboBoxItem>掉电记录</ComboBoxItem>
<ComboBoxItem>上电记录</ComboBoxItem>
<ComboBoxItem>传感器失效记录</ComboBoxItem>
</ComboBox>
<TextBox x:Name="txtBoxNumber" Width="50">1</TextBox>
<Button x:Name="btnReadRecord" Click="btnReadRecord_Click" ClickMode="Press">读取记录</Button>
<TextBox x:Name="txtBoxRecordInfo">0000/00/00 00:00</TextBox>
</StackPanel>
</GroupBox>
</StackPanel>
</StackPanel>
</TabItem>
</TabControl>
</Grid>
</Window>
代码文件
csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace GasAlarmTestTool
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private int warningCnt = 0;
private int warningRecoveryCnt = 0;
private int faultCnt = 0;
private int faultRecoveryCnt = 0;
private int poweroffCnt = 0;
private int poweronCnt = 0;
private int sensorInvalidCnt = 0;
long recvTimems = 0; // 用于计算串口接收完一帧数据
int rxLen = 0; // 串口接收到的数据长度
byte[] rxBuff = new byte[128]; // 串口数据接收缓存
private static Timer mTimer; // 定时器,10ms执行一次
SerialPort mSerialPort = new SerialPort();
private StringBuilder lineBuilder = new StringBuilder(); // 用于存放串口接收数据
private List<string> baudrateList = new List<string> {
"4800",
"9600",
"19200",
"38400",
"57600",
"115200",
"256000",
"1000000",
"2000000",
"3000000"
};
public MainWindow()
{
InitializeComponent();
// 获取所有可用串口端口,并添加到comboBoxCOM
string[] ports = System.IO.Ports.SerialPort.GetPortNames();
comboBoxCOM.ItemsSource = ports;
comboBoxCOM.SelectedIndex = 0; // 默认选择索引
comboBoxBaudRate.ItemsSource = baudrateList; // 波特率设置combobox数据源
mTimer = new Timer(recvTimerCalback, null, 0, 10); // 创建并启动定时器
}
/// <summary>
/// 窗口关闭处理
/// </summary>
/// <param name="e"></param>
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (MessageBox.Show("确定要退出吗?", "确认", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
{
// 用户选择"否",取消关闭
e.Cancel = true;
}
mSerialPort.Close();
mTimer.Dispose();
base.OnClosing(e);
}
private string RecordInfoString(byte[] buf)
{
if (buf.Length < 11) {
return "";
}
int index = buf[4];
int year = buf[5] << 8 | buf[6];
int month = buf[7];
int day = buf[8];
int hour = buf[9];
int minute = buf[10];
return year.ToString() + "/" + month.ToString() + "/" + day.ToString() + " " + hour.ToString() + ":" + minute.ToString();
}
/// <summary>
/// 串口接收处理定时器回调
/// </summary>
/// <param name="obj"></param>
private void recvTimerCalback(object obj)
{
//Console.WriteLine("timer callback!" + recvTimems);
this.Dispatcher.Invoke(new Action(()=> {
// UI线程分离,更新UI数据显示
if (((Environment.TickCount - recvTimems) >= 20) && (rxLen > 0))
{
// 串口接收时间超过X ms认为一帧数据接收完成
uint HEAD = rxBuff[0];
uint C1 = rxBuff[1];
uint C2 = rxBuff[2];
uint L = rxBuff[3];
uint CS = rxBuff[L + 4];
uint END = rxBuff[L + 5];
// 打印串口结束缓存数据
uint _CS = 0; // 计算接收数据校验和
uint index = 0;
UInt32 temp = 0;
for (int i = 0; i < rxLen; i++)
{
if (i < (rxLen - 2))
{
temp += rxBuff[i];
}
Console.WriteLine(rxBuff[i].ToString("X2"));
}
_CS = (uint)(temp % 0xFF);
if ((0xAA == rxBuff[0]) && (0x55 == END))
{
// TODO: 接收到一帧完整数据
Console.WriteLine("RS232 RECV ONE FRAME!" + CS.ToString("X2") + ", " + _CS.ToString("X2"));
if (CS == _CS)
{
// TODO: CHECKSUM8 校验和正确
Console.WriteLine("CheckSum OK");
if (0x00 == C2)
{
if (0x00 == C1)
{
// TODO: 查询各类记录总数
warningCnt = rxBuff[4]; // 探测器报警记录总数
warningRecoveryCnt = rxBuff[5]; // 探测器报警恢复记录总数
faultCnt = rxBuff[6]; // 探测器故障记录总数
faultRecoveryCnt = rxBuff[7]; // 探测器故障恢复记录总数
poweroffCnt = rxBuff[8]; // 探测器掉电记录总数
poweronCnt = rxBuff[9]; // 探测器上电记录总数
sensorInvalidCnt = rxBuff[10]; // 传感器失效记录总数
txtBoxWarningCount.Text = warningCnt.ToString();
txtBoxWarningRecoveryCount.Text = warningRecoveryCnt.ToString();
txtBoxFaultCount.Text = faultCnt.ToString();
txtBoxFaultRecoryCount.Text = faultRecoveryCnt.ToString();
txtBoxPoweroffCount.Text = poweroffCnt.ToString();
txtBoxPoweronCount.Text = poweronCnt.ToString();
}
}
else if (0x01 == C2)
{
// 报警记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x02 == C2)
{
// 报警恢复记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x03 == C2)
{
// 故障记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x04 == C2)
{
// 故障恢复记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x05 == C2)
{
// 掉电记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x06 == C2)
{
// 上电记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x07 == C2)
{
// 传感器失效记录
txtBoxRecordInfo.Text = RecordInfoString(rxBuff);
}
else if (0x08 == C2)
{
// TODO: 日期时间
int year = rxBuff[4] << 8 | rxBuff[5];
int month = rxBuff[6];
int day = rxBuff[7];
int hour = rxBuff[8];
int minute = rxBuff[9];
txtBoxDateTime.Text = year.ToString() + "/" + month.ToString() + "/" + day.ToString() + " "+hour.ToString() + ":"+minute.ToString();
}
}
else
{
// TODO: CHECKSUM8 校验和有误
Console.WriteLine("CheckSum ERR");
}
}
Array.Clear(rxBuff, 0, rxBuff.Length);
rxLen = 0;
}
}));
}
/// <summary>
/// 串口数据接收函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort comPort = (SerialPort)sender;
try
{
recvTimems = Environment.TickCount; // 更新串口数据接收时间
//rxBuff[rxLen] = mSerialPort.ReadByte();
int readCnt = mSerialPort.BytesToRead;
Console.WriteLine("in readCnt:" + readCnt);
mSerialPort.Read(rxBuff, rxLen, readCnt);
rxLen += readCnt;
Console.WriteLine("out rxLen:" + rxLen);
#if false
byte[] data = new byte[mSerialPort.BytesToRead];
mSerialPort.Read(data, 0, data.Length);
Console.WriteLine(data.Length);
foreach (byte b in data)
{
Console.WriteLine(b.ToString("X2"));
}
#endif
}
catch (Exception ce)
{
MessageBox.Show(ce.Message);
}
}
private void btnOpenCloseCom_Click(object sender, RoutedEventArgs e)
{
if (mSerialPort.IsOpen)
{
mSerialPort.Close();
btnOpenCloseCom.Content = "打开串口";
Console.WriteLine("关闭串口成功");
Debug.WriteLine("关闭串口成功");
comboBoxBaudRate.IsEnabled = true;
comboBoxCOM.IsEnabled = true;
comboBoxDataBit.IsEnabled = true;
comboBoxStopBit.IsEnabled = true;
comboBoxSdd.IsEnabled = true;
comboBoxlik.IsEnabled = true;
}
else
{
mSerialPort.PortName = comboBoxCOM.SelectedItem.ToString();
mSerialPort.BaudRate = 4800; // 波特率
mSerialPort.DataBits = 8; // 数据位
mSerialPort.StopBits = StopBits.One; // 停止位
mSerialPort.Parity = Parity.None; // 校验位
mSerialPort.Handshake = Handshake.None;
//mSerialPort.ReadTimeout = 1500; // 读超时
//mSerialPort.Encoding = Encoding.UTF8; // 编码方式
//mSerialPort.RtsEnable = true;
mSerialPort.DataReceived += SerialPort_DataReceived;
Console.WriteLine("baudrate SelectedIndex:" + comboBoxBaudRate.SelectedIndex);
Console.WriteLine("baudrate SelectedValue:" + comboBoxBaudRate.SelectedValue);
Console.WriteLine("baudrate Text:" + comboBoxBaudRate.Text);
if (comboBoxBaudRate.SelectedIndex < 0)
{
mSerialPort.BaudRate = Convert.ToInt32(comboBoxBaudRate.Text);
}
else
{
switch (comboBoxBaudRate.SelectedValue)
{
case "4800":
mSerialPort.BaudRate = 4800;
break;
case "9600":
mSerialPort.BaudRate = 9600;
break;
case "19200":
mSerialPort.BaudRate = 19200;
break;
case "38400":
mSerialPort.BaudRate = 38400;
break;
case "57600":
mSerialPort.BaudRate = 57600;
break;
case "115200":
mSerialPort.BaudRate = 115200;
break;
case "256000":
mSerialPort.BaudRate = 256000;
break;
case "1000000":
mSerialPort.BaudRate = 1000000;
break;
case "2000000":
mSerialPort.BaudRate = 2000000;
break;
case "3000000":
mSerialPort.BaudRate = 3000000;
break;
default:
MessageBox.Show("波特率设置有误!");
break;
}
}
Console.WriteLine("端口:" + mSerialPort.PortName + ",波特率:" + mSerialPort.BaudRate);
// mSerialPort.Write("Hello world"); // 写字符串口
// mSerialPort.Write(new byte[] { 0xA0, 0xB0, 0xC0}, 0, 3); // 写入3个字节数据
//Debug.WriteLine("Hello world");
//MessageBox.Show("端口名:" + mSerialPort.PortName);
try
{
mSerialPort.Open();
btnOpenCloseCom.Content = "关闭串口";
Console.WriteLine("打开串口成功");
Debug.WriteLine("打开串口成功");
comboBoxBaudRate.IsEnabled = false;
comboBoxCOM.IsEnabled = false;
comboBoxDataBit.IsEnabled = false;
comboBoxStopBit.IsEnabled = false;
comboBoxSdd.IsEnabled = false;
comboBoxlik.IsEnabled = false;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
private void btnClearRecv_Click(object sender, RoutedEventArgs e)
{
lineBuilder.Clear();
textBlockRecv.Text = lineBuilder.ToString();
}
private void comboBoxCOM_Drop(object sender, DragEventArgs e)
{
string[] ports = System.IO.Ports.SerialPort.GetPortNames();
comboBoxCOM.ItemsSource = ports;
comboBoxCOM.SelectedIndex = 0;
}
private void comboBoxBaudRate_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ComboBoxItem obj = (ComboBoxItem)sender;
}
private void btnReadRecCount_Click(object sender, RoutedEventArgs e)
{
if (mSerialPort.IsOpen)
{
mSerialPort.Write(new byte[] { 0xAA, 0x00, 0x00, 0x00, 0xAA, 0x55 }, 0, 6); // 写入3个字节数据
}
else
{
MessageBox.Show("请先打开串口!");
}
}
private void btnReadDateTime_Click(object sender, RoutedEventArgs e)
{
if (mSerialPort.IsOpen)
{
mSerialPort.Write(new byte[] { 0xAA, 0x00, 0x08, 0x00, 0xB2, 0x55 }, 0, 6);
}
else
{
MessageBox.Show("请先打开串口!");
}
}
private void btnReadRecord_Click(object sender, RoutedEventArgs e)
{
int index = Convert.ToInt32(txtBoxNumber.Text);
if (index < 1 || index > 255)
{
MessageBox.Show("记录索引:1~255!");
}
else
{
byte C1 = (byte)Convert.ToInt32(txtBoxNumber.Text);
byte C2 = (byte)(cmboxRecordType.SelectedIndex);
byte CS = (byte)((0xAA + C1 + C2)%0xFF);
if (mSerialPort.IsOpen)
{
byte[] Data = new byte[16];
Data[0] = 0xAA;
Data[1] = C1; // 指定记录条数
Data[2] = (byte)(C2 + 1); // C2 记录分类从1开始
Data[3] = 0x00;
Data[4] = CS;
Data[5] = 0x55;
mSerialPort.Write(Data, 0, 6);
}
else
{
MessageBox.Show("请先打开串口!");
}
}
}
}
}
运行效果