Creating a Serial Port Wrapper — C# .NET Framework
Serial communication in 2021?
So, you stumbled into this article probably because your boss has assigned you to a project where you are required to communicate with a device connected through a serial port in a computer. You might also be thinking: “Ugh, serials ports in 2021? Reading bytes in 2021? Are you joking with me? Am I going back to the '90s?”. Fear not, there are a bunch of applications and solutions today that still use serial communication. If you ever went to a supermarket and saw the cashier scanning your products with a barcode scanner, Most likely that reading was sent through a serial port to the cashier’s application.
I’ll first give you a brief introduction to what serial communication is and how it works and then we’ll do a wrapper class, in C#, for Microsoft’s .NET Framework System.IO.Ports.SerialPort class.
Brief explanation
Serial communication is made between 2 devices by connecting a male 9-pin connector (DB9) into a female 9-pin connector (also DB9). Serial ports are called COM Ports on a PC. Most modern personal computers are no longer equipped with these ports but as long as you have a USB port you can always buy a serial-to-USB converter to connect your device to your PC’s USB port.
I’ll go briefly go through the pinout because it is important to understand how it works. If you are not going to implement the whole protocol you may only connect 3 wires in the DB9 connectors, that would be RD, TD, and GND.
If you are implementing the whole protocol connect CTS to RTS and DTR to DSR.
Basic serial port settings
Baud rate — is the rate at which information is transferred. The most common value is 9600 bits per second.
Data bits — defines the number of bits used to represent one character of data. If transferring ASCII text, you can use 7 or 8 bits. For any other form of data, you need to use 8 bits.
Stop bits — Signals the end of a frame. Most commonly you set it to 1.
Parity — is the method of detecting errors in transmission. In most cases, you will set it to None (no parity bit is sent).
Flow control — used when a transmitter might send data faster than the receiver can process it. Flow control allows you to establish a handshaking method to handle this. In most cases, you will just set it to None.
Let’s start coding!
I’ll not guide you through the creation of the project because you should already be able to create a project and the point of the guide is to create a wrapper class for the serial port library that already exists in System.IO.
Before we begin with our wrapper class, let’s create a delegate and SerialWrapperDataReceivedArgs class. We’ll use the delegate to fire a new “data received” event with our args object. If you are unfamiliar with delegates please check Microsoft’s official website.
Create a new SerialWrapperDataReceivedArgs.cs file and replace its contents with:
namespace Demo.Wrappers.Serial
{
public class SerialWrapperDataReceivedArgs
{
public byte[] DataBytes { get; set; }
public string DataHex { get; set; } public SerialWrapperDataReceivedArgs(byte[] bytes, string data)
{
this.DataBytes = bytes;
this.DataHex = data;
}
}
}
Create a Delegates.cs file and replace its contents with:
namespace Demo.Wrappers.Serial
{
public delegate void SerialWrapperDataReceived(object sender, SerialWrapperDataReceivedArgs e);
}
Now that we’ve set up our auxiliary classes let’s create the wrapper.
Begin by creating a SerialPortWrapper.cs file.
On top of the page import System.IO and System.IO.Ports:
using System.IO;
using System.IO.Ports;
Add the properties needed to set up the serial port:
private SerialPort _serialPort;
public string PortName { get; set; }
public int BaudRate { get; set; }
public Parity Parity { get; set; }
public int DataBits { get; set; }
public StopBits StopBits { get; set; }
We’ll also be adding a MemoryStream object to store the received bytes and a Timer. The MemoryStream will hold the received bytes and we’ll use the Timer to define when we should trigger our delegate with a new ‘DataReceived’ message.
private MemoryStream _buffer = new MemoryStream();
private System.Timers.Timer t_DataReceivedTimeout;
So now that all our wrapper properties are all set and done, let’s create a constructor for our wrapper class.
public SerialWrapper(string port, int baudRate, Parity parity, int dataBits, StopBits stopBits, SerialWrapperDataReceived dataReceivedHandler)
{
this.PortName = port;
this.BaudRate = baudRate;
this.Parity = parity;
this.DataBits = dataBits;
this.StopBits = stopBits;
this.DataReceivedHandler = dataReceivedHandler;
// Timers settings and elapsed events definition
t_DataReceivedTimeout = new System.Timers.Timer(500);
t_DataReceivedTimeout.Elapsed += DataReceivedTimeout_Elapsed;
t_DataReceivedTimeout.AutoReset = false;// Serial Ports settings definitions
_serialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits);
_serialPort.Handshake = Handshake.None;
_serialPort.DataReceived += SerialPort_DataReceived;
}
Now create a start and stop method that will be used to signal our wrapper to start and stop, respectively, reading the serial port.
public void Start()
{
if (_serialPort.IsOpen) _serialPort.Close();
_serialPort.Open();
}public void Stop()
{
t_DataReceivedTimeout.Stop();
_serialPort.Close();
}
As you may have noticed, the SerialPort class we are implementing has a DataReceived delegate. This is the delegate that will fire every time the COM port on our PC receives new bytes. We assigned a method called ‘SerialPort_DataReceived’ to this delegate and before we get into its implementation I need to explain an important tidbit. As I said this DataReceived delegate is fired every time bytes are received but this does not mean that we’ll receive the full message in one go. Serial communications may be slow and if the message is too long and the DataReceived delegate will be fired multiple times. So we use our timer to make sure that we did not receive any new bytes for a while until we can assume we have a full message on our side. With that being said, here is the implementation of our SerialPort_DataReceived:
public void StationPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// If we received new data in the buffer we need to stop the timeout timers we have
if (t_StationDataReceivedTimeout.Enabled)
t_StationDataReceivedTimeout.Stop(); // Create a memory stream object that will contain the new data received
MemoryStream buffer = new MemoryStream();
byte[] receivedByte = new byte[1]; // Read byte by byte the data received and store it in the memory stream object
while (_stationSerialPort.BytesToRead > 0)
{
_stationSerialPort.Read(receivedByte, 0, 1);
buffer.Write(receivedByte, 0, 1);
} // Once we got all the new bytes, add the bytes to the internal buffer (memory stream object)
byte[] aux = buffer.ToArray();
_stationBuffer.Write(aux, 0, aux.Length); // Restart the timeout timers
t_StationDataReceivedTimeout.Start();
}
Stopping and restarting the timer makes it so that the timer is not triggered out of line and before all bytes had been read. Please be aware that we set the timeout timer to 500ms this is an incredibly long time for a computer and serial port so if you noticed that the timer is triggering with more information than what you were expecting, you need to lower it accordingly. For example, I have a project with very intensive communication and I had to set the timeout timer to 10ms.
Now let’s implement our timeout timer elapsed method:
#region byte array to hex string conversion via byte manipulation
public string ByteArrayToHex(byte[] bytes)
{
char[] c = new char[bytes.Length * 2];
int b;
for (int i = 0; i < bytes.Length; i++)
{
b = bytes[i] >> 4;
c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7));
b = bytes[i] & 0xF;
c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7));
}
return new string(c);
}
#endregionprivate void DataReceivedTimeout_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// If this event is fired it means we havent received any new data in the established time for the buffer.
byte[] currentBuffer = _buffer.ToArray();
_buffer.Close();
_buffer= new MemoryStream();
OnDataReceived(new SerialWrapperDataReceivedArgs(currentBuffer, ByteArrayToHex(currentBuffer)));
}
Now last but not least we need to implement our OnDataReceived method. This is the method that will fire our delegate ‘DataReceivedHandler’, notifying the class that will use our wrapper that there is a new message from the serial port.
public void OnDataReceived(SerialWrapperDataReceivedArgs e)
{
DataReceivedHandler.Invoke(this, e);
}
Congratulations! You are all set to start testing our brand new Serial Port wrapper class.
I believe that I may have been too brief about the details of serial ports but I just wanted to make a guide on how to implement the SerialPort class in c# and not dive too deep into the theory. You must read up about serial communication to understand better what happens in this type of communication and to be able to build upon what was written here.
Also, I would like to make a note that this is a simple implementation of the serial protocol, which means that this code only works if you using the rs232 protocol. If the device that you need to communicate with, uses the whole pinout of the serial protocol, you’ll need to make changes to the code to accommodate the RTS/CTS (Request to Send/ Clear to send) lines.
I hope you enjoyed the read and that I was helpful. If you have any doubts feel free to comment or contact me and I can try to help you. If you would like to see all the code in one place, please check out my repository on GitHub.