Зв’язок через Serial Port в C#
Автор: PsychoCoder, переклад.
Джерело:www.dreamincode.net
Ласкаво прошу до мого посібника про зв’язок через послідовний порт Serial Port в C#. Нещодавно я отримав безліч запитань про те, як відсилати і отримувати дані через послідовний порт, тому я подумав, що настав час написати статтю на цю тему.
Раніше в часи Visual Basic 6.0, використовували MSComm Control, який постачався з VB6, але в цього методу була проблема – потрібно було переконатися чи не забули ви включити цей компонент в ваш інсталяційний пакет, що насправді не така вже й велика проблема. Компонент робив саме те, що було потрібно для вашого завдання.
Потім ми познайомились з .Net 1.1. VB програмістам сподобався той факт, що Visual Basic врешті решт еволюціонував в ОО мову. Але згодом було виявлено – з усіма цими ОО можливостями, зв’язок через послідовний порт не був доступним. Тому знову VB розробники знову змушені були покладатися на MSComm Control з попередньої версії Visual Basic, хоча це теж не була надто велика проблема, але все ж трохи розчаровувало, що внутрішній спосіб зв’язку по послідовному порту не був запропонований разом з .net Framework. Ба навіть гірше, C# розробники повинні були покладатися на компонент з Visual Basic і на VB простір імен, якщо вони хотіли зв’язатись через послідовний порт.
Потім настав час .Net 2.0, і цього разу Microsoft додала System.IO.Ports простір імен Namespace, всередині якого був клас SerialPort. DotNet розробники врешті решт отримали внутрішній спосіб зв’язку з допомогою послідовного порта, без складнощів взаємодії із застарілим ActiveX OCX. Одним з найбільш корисних методів в класі SerialPort є метод GetPortNames Method. Він дозволяє вам отримати список портів (COM1,COM2, і т.п.) доступних для застосунків комп’ютера .
Тепер, коли ми маємо такий спосіб виходу із ситуації, давайте перейдемо до програмування нашого застосунку.
Як і у всіх моїх застосунках – функціональність буде відокремлена від представлення. Я роблю це створюючи класи Manager, які управляють функціональністю даних процесів. Цей підхід ми побачимо в коді класу CommunicationManager.
Як завжди, коли ви пишете в .Net спочатку необхідно додати посилання на простори імен (Namespace), які будуть використовуватись:
using System; using System.Text; using System.Drawing; using System.IO.Ports; using System.Windows.Forms;
В програмі я хочу надати можливість користувачу вибирати в якому вигляді надсилати повідомлення: символьному чи двійковому, для цього ми використовуємо перелік (enumeration), а також перелік для типів повідомлень: Incoming, Outgoing, Error і т.п. Основна мета використання цих переліків – зміна кольору тексту для користувача відповідно до типу повідомлення. Ось ці переліки:
#region Manager Enums /// <summary> /// enumeration to hold our transmission types /// </summary> public enum TransmissionType { Text, Hex } /// <summary> /// enumeration to hold our message types /// </summary> public enum MessageType { Incoming, Outgoing, Normal, Warning, Error }; #endregion
Далі йде список змінних: 6 з них для заповнення наших властивостей (Properties) класу, 2 інших для загальних потреб класу – їх потрібно зробити глобальними:
#region Manager Variables //property variables private string _baudRate = string.Empty; private string _parity = string.Empty; private string _stopBits = string.Empty; private string _dataBits = string.Empty; private string _portName = string.Empty; private TransmissionType _transType; private RichTextBox _displayWindow; //global manager variables private Color[] MessageColor = { Color.Blue, Color.Green, Color.Black, Color.Orange, Color.Red }; private SerialPort comPort = new SerialPort(); #endregion
Примітка: я завжди відокремлюю код в секції використовуючи #region … #endregion для того, щоб зробити простішим перегляд коду. Це питання дизайну і якщо не бажаєте – робити це не обов’язково .
Зараз ми маємо створити властивості нашого класу. Всі властивості в цьому класі є публічними читання/запис властивостями. Маємо властивості для наступних пунктів послідовного порта (Serial Port):
- Baud Rate: Міра швидкості послідовного зв’язку , грубо вона еквівалентна біту за секунду.
- Parity: Парна або непарна кількість 1 або 0 в бінарному коді, часто використовується для визначення цілісності даних ,особливо після передачі.
- Stop Bits: Біт який сигналізує кінець передачі блоку інформації.
- Data Bits: Кількість біт який використовується для представлення одного символу даних.
- Port Name: Порт з допомогою якого ми будемо здійснювати зв’язок, тобто COM1, COM2, і т.д.
Ми також маємо 2 властивості, що не відносяться до порту самі по собі, але визначають, як дані будуть відображені і який тип передачі використовувати :
#region Manager Properties /// <summary> /// Property to hold the BaudRate /// of our manager class /// </summary> public string BaudRate { get { return _baudRate; } set { _baudRate = value; } } /// <summary> /// property to hold the Parity /// of our manager class /// </summary> public string Parity { get { return _parity; } set { _parity = value; } } /// <summary> /// property to hold the StopBits /// of our manager class /// </summary> public string StopBits { get { return _stopBits; } set { _stopBits = value; } } /// <summary> /// property to hold the DataBits /// of our manager class /// </summary> public string DataBits { get { return _dataBits; } set { _dataBits = value; } } /// <summary> /// property to hold the PortName /// of our manager class /// </summary> public string PortName { get { return _portName; } set { _portName = value; } } /// <summary> /// property to hold our TransmissionType /// of our manager class /// </summary> public TransmissionType CurrentTransmissionType { get { return _transType; } set { _transType = value; } } /// <summary> /// property to hold our display window /// value /// </summary> public RichTextBox DisplayWindow { get { return _displayWindow; } set { _displayWindow = value; } } #endregion
Щоб мати можливість встановити значення створюваному об’єкту класу, нам потрібні конструктори (Constructors). Конструктори є вхідними точками вашого класу, і першим кодом який виконується при створенні об’єкту класу.
Ми маємо 2 конструктори для нашого управляючого класу, один задає значення нашим властивостям, інший задає пусті значення властивостям. Таким чином, ініціалізація значень запобігає виникненню NullReferenceException (виключення нульового посилання). Також ми додаємо EventHandler (обробник події) в конструктор – подія буде оброблена, як тільки з’являться очікуючі дані в буфері:
#region Manager Constructors /// <summary> /// Constructor to set the properties of our Manager Class /// </summary> /// <param name="baud">Desired BaudRate</param> /// <param name="par">Desired Parity</param> /// <param name="sBits">Desired StopBits</param> /// <param name="dBits">Desired DataBits</param> /// <param name="name">Desired PortName</param> public CommunicationManager(string baud, string par, string sBits, string dBits, string name, RichTextBox rtb) { _baudRate = baud; _parity = par; _stopBits = sBits; _dataBits = dBits; _portName = name; _displayWindow = rtb; //now add an event handler comPort.DataReceived += new SerialDataReceivedEventHandler(comPort_DataReceived); } /// <summary> /// Comstructor to set the properties of our /// serial port communicator to nothing /// </summary> public CommunicationManager() { _baudRate = string.Empty; _parity = string.Empty; _stopBits = string.Empty; _dataBits = string.Empty; _portName = "COM1"; _displayWindow = null; //add event handler comPort.DataReceived += new SerialDataReceivedEventHandler(comPort_DataReceived); } #endregion
Найголовніше, що ви маєте знати про послідовний порт – це запис даних в порт. Перше що ми робимо в нашому методі WriteData – визначаємо, який режим передачі вибрав користувач, оскільки символьні дані мають бути конвертовані в бінарний код, а потім знову в символьний для відображення користувачу.
Далі, ми повинні переконатися чи відкритий порт, для цього використовуємо IsOpen Property з класу SerialPort Class. Якщо порт не відкритий – відкриваємо, викликаючи Open Method з SerialPort Class. Для запису в порт використовуємо Write Method:
#region WriteData public void WriteData(string msg) { switch (CurrentTransmissionType) { case TransmissionType.Text: //first make sure the port is open //if its not open then open it if (!(comPort.IsOpen == true)) comPort.Open(); //send the message to the port comPort.Write(msg); //display the message DisplayData(MessageType.Outgoing, msg + "n"); break; case TransmissionType.Hex: try { //convert the message to byte array byte[] newMsg = HexToByte(msg); //send the message to the port comPort.Write(newMsg, 0, newMsg.Length); //convert back to hex and display DisplayData(MessageType.Outgoing, ByteToHex(newMsg) + "n"); } catch (FormatException ex) { //display error message DisplayData(MessageType.Error, ex.Message); } finally { _displayWindow.SelectAll(); } break; default: //first make sure the port is open //if its not open then open it if (!(comPort.IsOpen == true)) comPort.Open(); //send the message to the port comPort.Write(msg); //display the message DisplayData(MessageType.Outgoing, msg + "n"); break; break; } } #endregion
Зауважте, в цьому методі ми викликаємо три методи:
- HexToByte
- ByteToHex
- DisplayData
Ці методи є необхідними для нашого управляючого класу. HexToByte метод конвертує дані в двійковий формат, потім ByteToHex конвертує їх назад в hex формат для відображення. Останній метод DisplayData – вишиковує дані в потік створений для відображення даних, оскільки до UI засобів управління (UI – інтерфейс користувача) може мати доступ тільки потік, то він і створюється.
Спочатку розглянемо конвертування символів в двійковий формат:
#region HexToByte /// <summary> /// method to convert hex string into a byte array /// </summary> /// <param name="msg">string to convert</param> /// <returns>a byte array</returns> private byte[] HexToByte(string msg) { //remove any spaces from the string msg = msg.Replace(" ", ""); //create a byte array the length of the //divided by 2 (Hex is 2 characters in length) byte[] comBuffer = new byte[msg.Length / 2]; //loop through the length of the provided string for (int i = 0; i < msg.Length; i += 2) //convert each set of 2 characters to a byte //and add to the array comBuffer[i / 2] = (byte)Convert.ToByte(msg.Substring(i, 2), 16); //return the array return comBuffer; } #endregion
Тут ми конвертували наданий символьний рядок в масив байтів, потім метод WriteData посилає його в порт.
Для відображення, ми маємо конвертувати його назад в формат рядка, тому використовуємо метод ByteToHex :
#region ByteToHex /// <summary> /// method to convert a byte array into a hex string /// </summary> /// <param name="comByte">byte array to convert</param> /// <returns>a hex string</returns> private string ByteToHex(byte[] comByte) { //create a new StringBuilder object StringBuilder builder = new StringBuilder(comByte.Length * 3); //loop through each byte in the array foreach (byte data in comByte) //convert the byte to a string and add to the stringbuilder builder.Append(Convert.ToString(data, 16).PadLeft(2, '0').PadRight(3, ' ')); //return the converted value return builder.ToString().ToUpper(); } #endregion
Останній метод, від якого залежить метод WriteData, – це метод DisplayData . Тут ми використовуємо метод Invoke Method нашого компоненнта управління RichTextBox, який використовується для відображення даних, для того щоб створити новий обробник події EventHandler. Він у свою чергу створює новий Delegate, для встановлення властивостей , які ми хочемо, щоб були у нашого повідомлення і застосовує їх до значення, яке відображається:
#region DisplayData /// <summary> /// method to display the data to & from the port /// on the screen /// </summary> /// <param name="type">MessageType of the message</param> /// <param name="msg">Message to display</param> [STAThread] private void DisplayData(MessageType type, string msg) { _displayWindow.Invoke(new EventHandler(delegate { _displayWindow.SelectedText = string.Empty; _displayWindow.SelectionFont = new Font(_displayWindow.SelectionFont, FontStyle.Bold); _displayWindow.SelectionColor = MessageColor[(int)type]; _displayWindow.AppendText(msg); _displayWindow.ScrollToCaret(); })); } #endregion
Примітка: Зауважте, ми додали STAThread Attribute до нашого методу. Він використовується коли одно потоковий апартамент (рос.msdn –підрозділ) (single thread apartment) потребується компонентом, таким як RichTextBox.
Наступний метод, який ми розглянемо, використовується при відкритті порта на початковому етапі. Тут встановлюються BaudRate, Parity, StopBits, DataBits і PortName Properties класу SerialPort Class:
#region OpenPort public bool OpenPort() { try { //first check if the port is already open //if its open then close it if (comPort.IsOpen == true) comPort.Close(); //set the properties of our SerialPort Object comPort.BaudRate = int.Parse(_baudRate); //BaudRate comPort.DataBits = int.Parse(_dataBits); //DataBits comPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), _stopBits); //StopBits comPort.Parity = (Parity)Enum.Parse(typeof(Parity), _parity); //Parity comPort.PortName = _portName; //PortName //now open the port comPort.Open(); //display message DisplayData(MessageType.Normal, "Port opened at " + DateTime.Now + "\n"); //return true return true; } catch (Exception ex) { DisplayData(MessageType.Error, ex.Message); return false; } } #endregion
Далі глянемо на обробника події. Ця подія буде виконана, як тільки зявляться очікуючі дані в буфері. Цей метод має вигляд ідентичного до методу WriteData – бо він виконує точно таку ж саму роботу:
#region comPort_DataReceived /// <summary> /// method that will be called when theres data waiting in the buffer /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void comPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { //determine the mode the user selected (binary/string) switch (CurrentTransmissionType) { //user chose string case TransmissionType.Text: //read data waiting in the buffer string msg = comPort.ReadExisting(); //display the data to the user DisplayData(MessageType.Incoming, msg + "\n"); break; //user chose binary case TransmissionType.Hex: //retrieve number of bytes in the buffer int bytes = comPort.BytesToRead; //create a byte array to hold the awaiting data byte[] comBuffer = new byte[bytes]; //read the data and store it comPort.Read(comBuffer, 0, bytes); //display the data to the user DisplayData(MessageType.Incoming, ByteToHex(comBuffer) + "\n"); break; default: //read data waiting in the buffer string str = comPort.ReadExisting(); //display the data to the user DisplayData(MessageType.Incoming, str + "\n"); break; } } #endregion
Ми маємо 3 невеликих методи які залишилися, і їх, за відсутності кращого слова, можна назвати опціональними. Вони використовуються, щоб заповнити ComboBox на формі назвами доступних портів на комп’ютері, значеннями Parity і значеннями Stop Bit. Значення Parity і Stop Bit доступні в переліку починаючи з .Net Framework 2.0:
#region SetParityValues public void SetParityValues(object obj) { foreach (string str in Enum.GetNames(typeof(Parity))) { ((ComboBox)obj).Items.Add(str); } } #endregion #region SetStopBitValues public void SetStopBitValues(object obj) { foreach (string str in Enum.GetNames(typeof(StopBits))) { ((ComboBox)obj).Items.Add(str); } } #endregion #region SetPortNameValues public void SetPortNameValues(object obj) { foreach (string str in SerialPort.GetPortNames()) { ((ComboBox)obj).Items.Add(str); } } #endregion
Ось так, ви можете працювати зі зв’язком через послідовний порт в C#. Microsoft врешті решт дав нам внутрішні інструменти для виконання таких задач, і не потрібно більше покладатись на застарілі об’єкти.
Я публікую цей клас і приклад застосунку, щоб показати, як використовувати щойно вивчене. Те що я надаю підлягає GNU General Public License, тобто, ви можете модифікувати і поширювати, як ви вважаєте за потрібне, але ліцензійний заголовок має залишатися недоторканним. Я сподіваюсь цей посібник буде для вас корисним і інформативним, дякую за прочитання.
Вдалого програмування 🙂
Від себе.
В своїй програмі, автор забув про кнопку закривання послідовного порта, хоча і поставив її на форму. Далі я планую доповнити код для її функціонування, а також додатково розібрати код форми frmMain.cs.
Клацнемо двічі по кнопці cmdClose (напис на кнопці Close Port) – автоматично з’явиться заготовка обробника події клацання по кнопці в frmMain.cs:
private void cmdClose_Click(object sender, EventArgs e) { }
І в frmMain.Designer.cs автоматично створиться делегат події EventHandler, який призначає обробником події метод cmdClose_Click():
this.cmdClose.Click += new System.EventHandler(this.cmdClose_Click);
В фігурних дужках заготовки напишемо код:
private void cmdClose_Click(object sender, EventArgs e) { comm.ClosePort(); cmdOpen.Enabled = true; cmdClose.Enabled = false; cmdSend.Enabled = false; }
В ньому встановлюються властивості пов’язаних між собою кнопок відкривання, закривання порту і пересилання повідомлення. А також викликається метод ClosePort() об’єкта comm, тобто екземпляра нашого класу CommunicationManager. Відповідно, ми повинні написати код метода в цьому класі:
#region ClosePort</pre> public bool ClosePort() { //пробуємо зловити виключну ситуацію try { //спочатку перевіряємо чи порт уже відкритий //якщо відкритий то закриваємо його if (comPort.IsOpen == true) comPort.Close(); //відображаємо повідомлення про те, що порт закрито DisplayData(MessageType.Normal, "Port closed at " + DateTime.Now + "\n"); //повертаємо true return true; } //якщо зловили виключну ситуацію ex - то відобразимо її властивість Message catch (Exception ex) { DisplayData(MessageType.Error, ex.Message); return false; } } #endregion