C#串口调试工具 (WPF/MVVM结构完整示例版)
前文
由于经常用到串口调试, 尽管有现成的软件, 因为前端时间涉及一个二次开发, 就因为一个RtsEnable设置, 折腾半天, 网上各种版本的也很多, 功能扩展的很开也多。所以现在自己做了一个够用版,基于自己的需求,简单的实现发送接收功能, 至于那些扩展功能可以自己根据需求添加。
正文
先上个运行效果图:
项目架构
该实例用的GalaSoft.Mvvm, 该插件可以直接在NuGet中并且添加。
1.串口参数 , 为了方便, 端口号并没有用动态加载的方式, 如下枚举结构:
/// <summary> /// 端口号 /// </summary> public enum Port { COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9, COM10, COM11, COM12, COM13, COM14, COM15, COM16, COM17, COM18, COM19, COM20, COM21, COM22, COM23, COM24, COM25, COM26, COM27, COM28, COM29, COM30 } /// <summary> /// 奇偶校验 /// </summary> public enum CheckMode { None = 0, Odd = 1, Even = 2, Mark = 3, Space = 4 } /// <summary> /// 停止位 /// </summary> public enum StopBit { One=1, Two=2, OnePointFive=3, }
2.串口参数配置类 ,
作用: 主要用于绑定界面的参数选项。
/// <summary> /// 串口参数设置类 /// </summary> public class ComParameterConfig : ViewModelBase { public ComParameterConfig() { Port = System.Enum.GetValues(typeof(Port)); CheckMode = System.Enum.GetValues(typeof(CheckMode)); StopBit = System.Enum.GetValues(typeof(StopBit)); BaudRate = new List<int>() { 110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 38400, 56000, 57600, 115200, }; DataBit = new List<int>() { 6, 7, 8 }; } private Array port; private Array checkMode; private Array stopBit; private List<int> dataBit; private List<int> baudRate; /// <summary> /// 端口 /// </summary> public Array Port { get { return port; } set { port = value; RaisePropertyChanged(); } } /// <summary> /// 校验模式 /// </summary> public Array CheckMode { get { return checkMode; } set { checkMode = value; RaisePropertyChanged(); } } /// <summary> /// 停止位 /// </summary> public Array StopBit { get { return stopBit; } set { stopBit = value; RaisePropertyChanged(); } } /// <summary> /// 波特率 /// </summary> public List<int> BaudRate { get { return baudRate; } set { baudRate = value; RaisePropertyChanged(); } } /// <summary> /// 数据位 /// </summary> public List<int> DataBit { get { return dataBit; } set { dataBit = value; RaisePropertyChanged(); } } }
3.当前配置参数类
作用: 用于保存当前的串口参数、串口功能开关接收数据等业务。
核心代码:
/// <summary> /// 当前配置参数 /// </summary> public class CurrentParameter : ViewModelBase { #region Private private int baudRdate = 9600; private int dataBit = 8; private Port port; private CheckMode checkMode; private StopBit stopBit = StopBit.One; private SerialPort serialPort; private string dataReceiveInfo; private string sendData; private bool isOpen; private bool receiveFormat16 = true; private bool sendFormat16 = true; private int sendCount; private int receiveCount; #endregion #region UI绑定参数 /// <summary> /// 发送数量 /// </summary> public int SendCount { get { return sendCount; } set { sendCount = value; RaisePropertyChanged(); } } /// <summary> /// 接收数量 /// </summary> public int ReceiveCount { get { return receiveCount; } set { receiveCount = value; RaisePropertyChanged(); } } /// <summary> /// 接收区16进制 /// </summary> public bool ReceiveFormat16 { get { return receiveFormat16; } set { receiveFormat16 = value; RaisePropertyChanged(); } } /// <summary> /// 接收区数据 /// </summary> public string DataReceiveInfo { get { return dataReceiveInfo; } set { dataReceiveInfo = value; RaisePropertyChanged(); } } /// <summary> /// 发送数据 /// </summary> public string SendData { get { return sendData; } set { sendData = value; RaisePropertyChanged(); } } /// <summary> /// 发送区16进制 /// </summary> public bool SendFormat16 { get { return sendFormat16; } set { sendFormat16 = value; RaisePropertyChanged(); } } #endregion #region 串口参数信息 /// <summary> /// 开关 /// </summary> public bool IsOpen { get { return isOpen; } set { isOpen = value; RaisePropertyChanged(); } } /// <summary> /// 数据位 /// </summary> public int DataBit { get { return dataBit; } set { dataBit = value; RaisePropertyChanged(); } } /// <summary> /// 波特率 /// </summary> public int BaudRdate { get { return baudRdate; } set { baudRdate = value; RaisePropertyChanged(); } } /// <summary> /// 端口 /// </summary> public Port Port { get { return port; } set { port = value; RaisePropertyChanged(); } } /// <summary> /// 校验 /// </summary> public CheckMode CheckMode { get { return checkMode; } set { checkMode = value; RaisePropertyChanged(); } } /// <summary> /// 停止位 /// </summary> public StopBit StopBit { get { return stopBit; } set { stopBit = value; RaisePropertyChanged(); } } /// <summary> /// COM /// </summary> public SerialPort SerialPort { get { return serialPort; } set { serialPort = value; RaisePropertyChanged(); } } #endregion #region 串口操作方法 /// <summary> /// 开启串口 /// </summary> /// <returns></returns> public bool Open() { if (serialPort != null && serialPort.IsOpen) { return Close(); } try { serialPort = new SerialPort(); serialPort.DataBits = this.DataBit; serialPort.StopBits = ComHelper.GetStopBits(this.StopBit.ToString()); serialPort.Parity = ComHelper.GetParity(this.CheckMode.ToString()); serialPort.PortName = this.Port.ToString(); serialPort.RtsEnable = true; serialPort.DataReceived += SerialPort_DataReceived; serialPort.Open(); if (serialPort.IsOpen) return IsOpen = true; else return IsOpen = false; } catch (Exception ex) { MessageBox.Show(ex.Message); } return IsOpen = false; } /// <summary> /// 关闭串口 /// </summary> /// <returns></returns> public bool Close() { try { if (serialPort.IsOpen) { serialPort.Close(); return IsOpen = serialPort.IsOpen; } else { return IsOpen = serialPort.IsOpen; } } catch (Exception ex) { MessageBox.Show(ex.Message); return IsOpen = false; } } /// <summary> /// 发送数据 /// </summary> public void Send() { if (SendFormat16) { byte[] bytes = CRC.StringToHexByte(SendData); this.SerialPort.Write(bytes, 0, bytes.Length); SendCount = bytes.Length; //不做增量 } else { this.SerialPort.Write(SendData); SendCount = SendData.Length; } Messenger.Default.Send("", "PlaySendFlashing"); } /// <summary> /// 清空接收区 /// </summary> public void Clear() { this.DataReceiveInfo = string.Empty; } /// <summary> /// 清空发送区和缓存区 /// </summary> public void ClearText() { this.SendData = string.Empty; this.SendCount = 0; this.ReceiveCount = 0; } #endregion /// <summary> /// 返回事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { Messenger.Default.Send("", "PlayReciveFlashing"); byte[] readBuffer = new byte[SerialPort.ReadBufferSize]; SerialPort.Read(readBuffer, 0, readBuffer.Length); ReceiveCount = SerialPort.ReceivedBytesThreshold; //不做增量 if (ReceiveFormat16) { //不做增量 DataReceiveInfo = CRC.ByteToString(readBuffer, true); } else { DataReceiveInfo = Encoding.ASCII.GetString(readBuffer); } } }
4.核心MainViewModel类
作用: 关联首页的上下文, 通过DataContext绑定, 关联界面元素、命令等作用。
public class MainViewModel : ViewModelBase { /// <summary> /// Initializes a new instance of the MainViewModel class. /// </summary> public MainViewModel() { ComParameterConfig = new ComParameterConfig(); CurrentParameter = new CurrentParameter(); } private ComParameterConfig comParameter; /// <summary> /// 参数类 /// </summary> public ComParameterConfig ComParameterConfig { get { return comParameter; } set { comParameter = value; RaisePropertyChanged(); } } private CurrentParameter currentParameter; /// <summary> /// 当前配置参数 /// </summary> public CurrentParameter CurrentParameter { get { return currentParameter; } set { currentParameter = value; RaisePropertyChanged(); } } #region Command private RelayCommand _ToOpen; public RelayCommand ToOpen { get { if (_ToOpen == null) { _ToOpen = new RelayCommand(Open); } return _ToOpen; } set { _ToOpen = value; } } /// <summary> /// 根据配置打开端口 /// </summary> public void Open() { this.CurrentParameter.Open(); } private RelayCommand _ToClick; public RelayCommand ToClick { get { if (_ToClick == null) { _ToClick = new RelayCommand(Click); } return _ToClick; } set { _ToClick = value; } } /// <summary> /// 发送数据 /// </summary> public void Click() { this.CurrentParameter.Send(); } private RelayCommand _ToClear; public RelayCommand ToClear { get { if (_ToClear == null) { _ToClear = new RelayCommand(Clear); } return _ToClear; } set { _ToClear = value; } } /// <summary> /// 清空接收区 /// </summary> public void Clear() { this.CurrentParameter.Clear(); } private RelayCommand _ToClearText; public RelayCommand ToClearText { get { if (_ToClearText == null) { _ToClearText = new RelayCommand(ClearText); } return _ToClearText; } set { _ToClearText = value; } } /// <summary> /// 清空界面值 /// </summary> public void ClearText() { this.CurrentParameter.ClearText(); } #endregion }
5.CRC校验核心类
作用:主要实现数据校验, 含ModbusCR标准校验
/// <summary> /// CRC校验 /// </summary> public class CRC { #region CRC16 public static byte[] CRC16(byte[] data) { int len = data.Length; if (len > 0) { ushort crc = 0xFFFF; for (int i = 0; i < len; i++) { crc = (ushort)(crc ^ (data[i])); for (int j = 0; j < 8; j++) { crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1); } } byte hi = (byte)((crc & 0xFF00) >> 8); //高位置 byte lo = (byte)(crc & 0x00FF); //低位置 return new byte[] { hi, lo }; } return new byte[] { 0, 0 }; } #endregion #region ToCRC16 public static string ToCRC16(string content) { return ToCRC16(content, Encoding.UTF8); } public static string ToCRC16(string content, bool isReverse) { return ToCRC16(content, Encoding.UTF8, isReverse); } public static string ToCRC16(string content, Encoding encoding) { return ByteToString(CRC16(encoding.GetBytes(content)), true); } public static string ToCRC16(string content, Encoding encoding, bool isReverse) { return ByteToString(CRC16(encoding.GetBytes(content)), isReverse); } public static string ToCRC16(byte[] data) { return ByteToString(CRC16(data), true); } public static string ToCRC16(byte[] data, bool isReverse) { return ByteToString(CRC16(data), isReverse); } #endregion #region ToModbusCRC16 public static string ToModbusCRC16(string s) { return ToModbusCRC16(s, true); } public static string ToModbusCRC16(string s, bool isReverse) { return ByteToString(CRC16(StringToHexByte(s)), isReverse); } public static string ToModbusCRC16(byte[] data) { return ToModbusCRC16(data, true); } public static string ToModbusCRC16(byte[] data, bool isReverse) { return ByteToString(CRC16(data), isReverse); } #endregion #region ByteToString public static string ByteToString(byte[] arr, bool isReverse) { try { byte hi = arr[0], lo = arr[1]; return Convert.ToString(isReverse ? hi + lo * 0x100 : hi * 0x100 + lo, 16).ToUpper().PadLeft(4, '0'); } catch (Exception ex) { throw (ex); } } public static string ByteToString(byte[] arr) { try { return ByteToString(arr, true); } catch (Exception ex) { throw (ex); } } #endregion #region StringToHexString public static string StringToHexString(string str) { StringBuilder s = new StringBuilder(); foreach (short c in str.ToCharArray()) { s.Append(c.ToString("X4")); } return s.ToString(); } #endregion #region StringToHexByte private static string ConvertChinese(string str) { StringBuilder s = new StringBuilder(); foreach (short c in str.ToCharArray()) { if (c <= 0 || c >= 127) { s.Append(c.ToString("X4")); } else { s.Append((char)c); } } return s.ToString(); } private static string FilterChinese(string str) { StringBuilder s = new StringBuilder(); foreach (short c in str.ToCharArray()) { if (c > 0 && c < 127) { s.Append((char)c); } } return s.ToString(); } /// <summary> /// 字符串转16进制字符数组 /// </summary> /// <param name="hex"></param> /// <returns></returns> public static byte[] StringToHexByte(string str) { return StringToHexByte(str, false); } /// <summary> /// 字符串转16进制字符数组 /// </summary> /// <param name="str"></param> /// <param name="isFilterChinese">是否过滤掉中文字符</param> /// <returns></returns> public static byte[] StringToHexByte(string str, bool isFilterChinese) { string hex = isFilterChinese ? FilterChinese(str) : ConvertChinese(str); //清除所有空格 hex = hex.Replace(" ", ""); //若字符个数为奇数,补一个0 hex += hex.Length % 2 != 0 ? "0" : ""; byte[] result = new byte[hex.Length / 2]; for (int i = 0, c = result.Length; i < c; i++) { result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); } return result; } #endregion }
WPF技术点:
1.自定义样式按钮
<Style x:Key="CommonButtonBase" TargetType="{x:Type Button}"> <Setter Property="BorderBrush" Value="Transparent"/> <Setter Property="BorderThickness" Value="0"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Padding" Value="1"/> <Setter Property="Cursor" Value="Hand"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="4" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true"> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="#000000"/> <Setter Property="Opacity" Value="0.1"/> </Trigger> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Foreground" Value="#FFFF00"/> </Trigger> <Trigger Property="IsMouseOver" Value="false"> <Setter Property="Foreground" Value="White"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type Button}" x:Key="Btn0093EABase" BasedOn="{StaticResource CommonButtonBase}"> <Setter Property="Background" Value="#0093EA"/> <Setter Property="Foreground" Value="White"/> <Setter Property="FontSize" Value="22"/> <Setter Property="Height" Value="40"/> <Setter Property="Margin" Value="5"/> </Style>
2.转换器用于绑定按钮
public class FontConverters : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value != null && bool.TryParse(value.ToString(), out bool result)) { if (result) { return "关闭串口"; } } return "打开串口"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
//用于绑定UI的颜色状态显示
public class ColorConverters : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value != null && bool.TryParse(value.ToString(), out bool result)) { if (result) { return new SolidColorBrush((Color)System.Windows.Media.ColorConverter.ConvertFromString("#2E8B57")); } } return new SolidColorBrush((Color)System.Windows.Media.ColorConverter.ConvertFromString("#FF6347")); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
3.引用字体
<TextBlock Text="" Margin="20 5 0 5" FontFamily="pack://application:,,,/Font/#iconfont" Foreground="White" FontSize="30" VerticalAlignment="Center"/>
4.绑定命令和元素
<TextBlock Text="端 口:" Style="{DynamicResource TxtComStyle}"/> <ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" SelectedItem="{Binding CurrentParameter.Port}" ItemsSource="{Binding ComParameterConfig.Port}" /> <TextBlock Text="波 特 率:" Style="{DynamicResource TxtComStyle}"/> <ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" SelectedItem="{Binding CurrentParameter.BaudRdate}" ItemsSource="{Binding ComParameterConfig.BaudRate}" /> <TextBlock Text="数 据 位:" Style="{DynamicResource TxtComStyle}"/> <ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" SelectedItem="{Binding CurrentParameter.DataBit}" ItemsSource="{Binding ComParameterConfig.DataBit}" /> <TextBlock Text="校 验 位:" Style="{DynamicResource TxtComStyle}"/> <ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" SelectedItem="{Binding CurrentParameter.CheckMode}" ItemsSource="{Binding ComParameterConfig.CheckMode}" /> <TextBlock Text="停 止 位:" Style="{DynamicResource TxtComStyle}"/> <ComboBox Grid.Row="0" Grid.Column="2" Style="{StaticResource ComboBoxStyle}" SelectedItem="{Binding CurrentParameter.StopBit}" ItemsSource="{Binding ComParameterConfig.StopBit}" /> <TextBlock Text="状 态:" Style="{DynamicResource TxtComStyle}"/> <TextBlock Text="" Style="{DynamicResource TxtComStyle1}"
Foreground="{Binding CurrentParameter.IsOpen,Converter={StaticResource ColorConverters}}" />
写在最后
主项目的结构图 , 如下: