The biggest problem for me at the beginning was deciding how to read data from the port. Obviously, we can't do the reading synchronously, in the case of a TTY-like program that just doesn't make any sense. I had two real options for asychronous operation:
- Create a worker thread that reads from the port, sleeps for a certain timeout, and then repeats.
- Use the DataReceived handler on the SerialPort object to automatically call a callback when incoming data is received.
On top of all those reasons, as if I need another one, I feel like using an explicitly-managed thread here is a particularly inelegant solution. In short, I decided to use the DataReceived event handler to do my port reading (when I put my port into asynchronous read mode, that is).
That question out of the way, I needed to decide how to do input buffering. Do I buffer by line (SerialPort.ReadLine), "buffer" by character (SerialPort.ReadChar), or do I not buffer (SerialPort.ReadExisting)? On the one hand I need to support both buffered and non-buffered input. On the other hand I don't want to be stacking up DataReceived events with Timeout events endlessly, because that could create a stability problem.
My design goes as follows: I use the DataReceived event handler to signal me when data is ready. From within the handler I use SerialPort.ReadExisting to read the incoming data into a buffer, and from there slice and dice the input into the forms needed by modules higher up in my program. I admit that this may not be the most attractive or elegant code I have ever written, but it has demonstrated itself to be very performant and robust solution for my needs:
private string readBuffer;
private SerialPort port;
private bool unBuffNeedNewline = false;
private void InitPort()
{
this.port = new SerialPort();
this.port.DataReceived += new SerialDataReceivedHandler(DataReceiver_Handler);
}
private void DataReceiver_Handler(object sender, SerialDataReceivedEventArgs e)
{
if(this.bufferMode == ConnectionHelpers.ReadBuffering.LineBuffered)
this.DataReceiverLineBuffered();
else
this.DataReceiverCharBuffered();
}
private void DataReceiverLineBuffered()
{
try {
string x;
lock(this.readBuffer) {
x = this.readBuffer + this.port.ReadExisting();
}
char[] delim = new char[2] {'\n', '\r'};
while(x.Length > 1 && (x.Contains("\n") || x.Contains("\r"))) {
String[] ary = x.Split(delim, 2);
this.ReadHandler(ary[0], true);
x = ary[1] + this.port.ReadExisting();
}
lock(this.readBuffer) {
this.readBuffer = x;
}
} catch(Exception e) {
this.StatusReporter("Serial Read Error: " + e.ToString());
}
}
private void DataReceiverCharBuffered()
{
try {
string x;
lock(this.readBuffer) {
x = this.readBuffer + this.port.ReadExisting();
}
while(x.Length >= 1) {
string c = x.Substring(0, 1);
if(c == "\r" || c == "\n") {
this.unBuffNeedNewline = true;
x = x.Substring(1) + this.port.ReadExisting();
}
else if(this.unBuffNeedNewline) {
this.ReadHandler("", true);
this.unBuffNeedNewline = false;
} else {
this.ReadHandler(c, false);
x = x.Substring(1) + this.port.ReadExisting();
}
}
} catch(Exception e) {
this.StatusReporter("Serial Read Error: " + e.ToString());
}
}
The function "ReadHandler" takes two arguments: The string value that's read (without carriage return or linefeed characters) and a boolean value indicating whether the given string is followed by a newline or not. Further up the call chain the display function will take that information to display timestamps and do other per-line formatting fanciness. It's worth noticing that since we are calling from within an event handler, ReadHandler probably needs to call BeginInvoke to pass it's incoming data to the GUI (that's what my code does, anyway).
The way I lock the readBuffer repeatedly was something that I figured out later. When data is incoming quickly (like 115200 baud or higher) we can get multiple DataReceived events being triggered and stacking up if we lock through the entire function. This can cause noticeable program slowdowns when a number of event handlers acquire the lock subsequently. Instead, I lock smaller parts of the code and let additional handler instances run concurrently but with no input. Throughput performance is better in these cases, and is not noticeably different otherwise.
So that's my implementation of a buffered asynchronous serial port reader in C#. It's not perfect, but it gets the job done, has pretty good throughput, and is relatively fault tolerant.
Hi Andrew! What is ConnectionHelpers.ReadBuffering.LineBuffered? Tnx
ReplyDeleteAh good question. I should have been more clear. ConnectionHelpers.ReadBuffering.LineBuffered is an enum value that specifies that the incoming data should be line buffered. Alternatively, I had defined ConnectionHelpers.ReadBuffering.CharBuffered values to describe streams that should be sent character-at-a-time.
ReplyDeletesorry but it's a .net library?
ReplyDeleteno .NET library. All of this is my own creation. I'm only showing part of it here for brevity.
ReplyDelete