Зв’язок через Serial Port в C#

This entry was posted by Опубліковано: 01.05.2015

terminalАвтор: 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 властивості, що не відносяться до порту самі по собі, але визначають, як дані будуть відображені і який тип передачі використовувати :

Сирець Manager Properties

#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 (обробник події) в конструктор – подія буде оброблена, як тільки з’являться очікуючі дані в буфері:

Сирець Manager Constructors

#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:

Сирець WriteData

#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

Сирець для завантаження.

Comments are closed.