5

Can anyone point me to a working example that uses the .net 4.5 Async API (async, await, task<>, ReadAsync, etc) to do serial communications?

I've tried to adapt an existing event driven serial sample, and am getting all kinds of horrible behavior - "port in use by other application" errors, VS2013 debugger throwing exceptions and locking up - which usually require a PC reboot to recover from.

edit

I've written my own sample from scratch. It's a simple Winforms project that writes to the Output window. Three buttons on the form - Open Port, Close Port, and Read Data. The ReadDataAsync method calls SerialPort.BaseStream.ReadAsync.

As of now, it will read data from the port, but I'm running into problems making it robust.

For example, if I unplug the serial cable, open the port, and click Read Data twice, I will get an System.IO.IOException (which I kind of expect), but my app stops responding.

Worse, when I try to stop my program, VS2013 throws up a "Stop Debugging in Progress" dialog, which never completes, and I can't even kill VS from Task Manager. Have to reboot my PC every time this happens.

Not good.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.IO.Ports;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private SerialPort _serialPort;

        public Form1()
        {
            InitializeComponent();
        }

        private void openPortbutton_Click(object sender, EventArgs e)
        {
                try
                {
                    if (_serialPort == null )
                        _serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);

                    if (!_serialPort.IsOpen)
                        _serialPort.Open();

                    Console.Write("Open...");
                }
                catch(Exception ex)
                {
                    ClosePort(); 
                    MessageBox.Show(ex.ToString());
                }
        }

        private void closePortButton_Click(object sender, EventArgs e)
        {
            ClosePort();
        }

        private async void ReadDataButton_Click(object sender, EventArgs e)
        {
            try
            {
                await ReadDataAsync();
            }
            catch (Exception ex)
            {
                ClosePort();
                MessageBox.Show(ex.ToString(), "ReadDataButton_Click");
            }
        }

        private async Task ReadDataAsync()
        {
            byte[] buffer = new byte[4096];
            Task<int> readStringTask = _serialPort.BaseStream.ReadAsync(buffer, 0, 100);

            if (!readStringTask.IsCompleted)
                Console.WriteLine("Waiting...");

            int bytesRead = await readStringTask;

            string data = Encoding.ASCII.GetString(buffer, 0, bytesRead);

            Console.WriteLine(data);
        }


        private void ClosePort()
        {
            if (_serialPort == null) return;

            if (_serialPort.IsOpen)
                _serialPort.Close();

            _serialPort.Dispose();

            _serialPort = null;

            Console.WriteLine("Close");
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            ClosePort();
        }

    }
}
Tom Bushell
  • 5,865
  • 4
  • 45
  • 60
  • SerialPort does not have async methods because of it's inherently async nature, it uses threads to handle communitacions, so just using its methods and events should be enough. Post your code to review it and see what's wrong. Also add the errors. – Gusman Apr 22 '14 at 21:22
  • Other SO posts imply that I can use things like SerialPort.BaseStream.ReadAsync, etc. I am calling this method. Are you saying this will not work? – Tom Bushell Apr 22 '14 at 21:25
  • Well, never though of using the underliying stream with SerialPort, but I suspect it will not work correctly (maybe I'm wrong), I have used during years the SerialPort and using it with the Write and Read functions and the DataReceived event should be enough and already asynchronous. – Gusman Apr 22 '14 at 21:27
  • The sample project I started with uses the DataRecieved event. I find the UI locks up when a lot of data comes in, so I'm looking for a more robust approach. – Tom Bushell Apr 22 '14 at 21:34
  • @Gusman: I don't get it. The Write and Read functions are synchronous and blocking. That doesn't sound very "inherently async" to me. – spender Apr 22 '14 at 21:36
  • Nope, Write will store data in a buffer and it will be sent by the underliying driver, and Read must be used when a DataReceived event is fired, you will read the data from a Buffer, so yes, in that moment you will lock, but the same time you will lock with the result of an async method. – Gusman Apr 22 '14 at 22:04
  • And also, from MSDN SerialPort DataReceived Event: *"The DataReceived event is raised on a secondary thread when data is received from the SerialPort object."* – Gusman Apr 22 '14 at 22:07
  • 6
    Using `BaseStream.ReadAsync` is exactly the right way to use serial ports in .NET, if you are forced to use the BCL SerialPort class. (Even better is to use the Win32 API.) I have a blog post coming out on this exact topic, after my supervisor approved it. – Ben Voigt Apr 23 '14 at 22:55
  • @BenVoigt - you said you had a blog post pending...how close is that to release? Would love to see it. – Tom Bushell Apr 28 '14 at 21:14
  • @Tom: It hasn't been approved yet, but it will appear [here](http://www.sparxeng.com/blog/feed) when it is. – Ben Voigt Apr 28 '14 at 21:42
  • 2
    I think this is the blog post he was referring to: http://www.sparxeng.com/blog/software/must-use-net-system-io-ports-serialport – VoteCoffee Sep 26 '14 at 19:57
  • @VoteCoffee: Indeed, that's the one. – Ben Voigt Oct 06 '14 at 16:40

3 Answers3

3

I'd use TaskCompletionSource<> to wrap SerialDataReceivedEvent, something like this (untested):

using System;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;

class PortDataReceived
{
    public static async Task ReadPort(SerialPort port, CancellationToken token)
    {
        while (true)
        {
            token.ThrowIfCancellationRequested();

            await TaskExt.FromEvent<SerialDataReceivedEventHandler, SerialDataReceivedEventArgs>(
                (complete, cancel, reject) => // get handler
                    (sender, args) => complete(args),
                handler => // subscribe
                    port.DataReceived += handler,
                handler => // unsubscribe
                    port.DataReceived -= handler,
                (complete, cancel, reject) => // start the operation
                    { if (port.BytesToRead != 0) complete(null); },
                token);

            Console.WriteLine("Received: " + port.ReadExisting());
        }
    }

    public static void Main()
    {
        SerialPort port = new SerialPort("COM1");

        port.BaudRate = 9600;
        port.Parity = Parity.None;
        port.StopBits = StopBits.One;
        port.DataBits = 8;
        port.Handshake = Handshake.None;

        port.Open();

        Console.WriteLine("Press Enter to stop...");
        Console.WriteLine();

        var cts = new CancellationTokenSource();
        var task = ReadPort(port, cts.Token);

        Console.ReadLine();

        cts.Cancel();
        try
        {
            task.Wait();
        }
        catch (AggregateException ex)
        {
            Console.WriteLine(ex.InnerException.Message);
        }

        port.Close();
    }

    // FromEvent<>, based on http://stackoverflow.com/a/22798789/1768303
    public static class TaskExt
    {
        public static async Task<TEventArgs> FromEvent<TEventHandler, TEventArgs>(
            Func<Action<TEventArgs>, Action, Action<Exception>, TEventHandler> getHandler,
            Action<TEventHandler> subscribe,
            Action<TEventHandler> unsubscribe,
            Action<Action<TEventArgs>, Action, Action<Exception>> initiate,
            CancellationToken token) where TEventHandler : class
        {
            var tcs = new TaskCompletionSource<TEventArgs>();

            Action<TEventArgs> complete = (args) => tcs.TrySetResult(args);
            Action cancel = () => tcs.TrySetCanceled();
            Action<Exception> reject = (ex) => tcs.TrySetException(ex);

            TEventHandler handler = getHandler(complete, cancel, reject);

            subscribe(handler);
            try
            {
                using (token.Register(() => tcs.TrySetCanceled()))
                {
                    initiate(complete, cancel, reject);
                    return await tcs.Task;
                }
            }
            finally
            {
                unsubscribe(handler);
            }
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thanks, I'll try this. You think this is a better approach than using SerialPort.BaseStream.ReadAsync? – Tom Bushell Apr 23 '14 at 15:51
  • @TomBushell, I think this gives more fine control over arriving data. If you need some buffering (like to specify how many bytes you want to read asynchronously), then you could use `ReadAsync`. Are you having any issues with `ReadAsync`, specifically? – noseratio Apr 23 '14 at 20:16
  • I'm having some success with ReadAsync now, and it's a much simpler approach. However, VS2013 locks up sometimes when hit exceptions...this async stuff may not be quite ready for prime time... :( – Tom Bushell Apr 23 '14 at 20:29
  • @TomBushell, in my experience, `async/await` support is quite mature. Maybe you could update the question with the code which gives you locks. – noseratio Apr 23 '14 at 20:44
  • Glad to hear it! I've added sample code, and some more details to my question. – Tom Bushell Apr 23 '14 at 22:42
  • I'm not going to downvote this answer because it's a nice effort, but the reality is that it uses the buggiest parts of `SerialPort`. Please, don't do it this way. – Ben Voigt Apr 23 '14 at 22:57
  • @BenVoigt, my experience with `SerialPort` so far has been limited to a hobbyist project sending USSD commands via GSM modem. The above approach has worked though. Could you provide any more details on the buggy behavior? Also looking forward to your blog post, it'd be great if you linked it here. – noseratio Apr 23 '14 at 23:07
  • BTW, I was using `DataReceived` event and `port.Read(.., port.BytesToRead)`. – noseratio Apr 23 '14 at 23:13
  • 5
    @Noseratio: `DataReceived` isn't reliable and `BytesToRead` isn't accurate. Most serial ports these days are on USB, not the PCI bus. On USB, sending individual bytes up to the host is *extremely* wasteful, so the driver doesn't know the device has data in its read buffer until you actually try to read it. Besides that there are a ton of race conditions. If you have a received byte and a framing error, you'll get events on two threads, and the handlers will race or even run at the exact same time. And no way to know the order they happened. – Ben Voigt Apr 23 '14 at 23:21
  • 4
    @Noseratio: Oh here's a fun fact I discovered: Checking `BytesToRead` clears all the error flags... Nothing like a little silent failure to make you scratch your head. – Ben Voigt Apr 23 '14 at 23:25
  • 1
    @BenVoigt, good point on buffering and error flags, tks. Regarding the races, I can see that possibly happening in the above code. In my hobby project though, it was a thread with serializing synchronization context (something like [this](http://stackoverflow.com/a/23176791/1768303)). So no concurrent reads. The same would apply to a UI thread. – noseratio Apr 23 '14 at 23:45
  • 1
    @Noseratio: Even when you use a mutex to prevent the event handlers from running simultaneously, they still can happen out of order, because each is on its own worker thread. That's what I meant by the race. – Ben Voigt Apr 24 '14 at 05:52
  • 1
    @BenVoigt, actualy I used `BlockingCollection` and consumer/producer logic, so all reads happen on the same thread, regardless of where `DataReceived` is fired. The order of events doesn't matter, as all I do is `port.Read(BytesToRead)`. – noseratio Apr 24 '14 at 06:24
  • 1
    I now tested the above code with a simple crossover cable and Prolific USB-to-serial dongle, and it works fine too. Each `await` continuation indeed happens on separate thread (I believe it's the driver's IOCP thread), but reads never overlap (because `await` continuations are sequential). I admit it's a very primitive test, but it works as expected :) – noseratio Apr 24 '14 at 06:34
  • 1
    @Noseratio: But received data and receive errors are delivered out-of-order, especially on busy systems. The problem with threading errors is that no amount of testing can guarantee they aren't there :( Only good design can prevent then. – Ben Voigt Apr 24 '14 at 14:13
  • 1
    `TaskCompletionSource` is a good direction to go. If someone is skeptical about `DataReceived` event, try this [SerialPortStream](https://www.nuget.org/packages/SerialPortStream/) package. – Felix Jan 20 '17 at 09:02
3

I've had similar problems closing a SerialPort from the UI thread. The following MSDN blog suggests that it's due to a deadlock between the UI thread and the native thread doing the closing. http://blogs.msdn.com/b/bclteam/archive/2006/10/10/top-5-serialport-tips-_5b00_kim-hamilton_5d00_.aspx

Putting the close into a separate task fixed it for me. (The method is implemented in a Protocol container class in my project and called when a UI button is clicked, the IDispose interface invoked or the main window is closed.)

    public Task Close()
    {
        // Close the serial port in a new thread
        Task closeTask = new Task(() => 
        {
            try
            {
                serialPort.Close();
            }
            catch (IOException e)
            {
                // Port was not open
                throw e;
            }
        });
        closeTask.Start();

        return closeTask;
    }

... and then in my UI command ...

        // The serial stream is stopped in a different thread so that the UI does
        // not get deadlocked with the stream waiting for events to complete.
        await serialStream.Close();
Evil Dog Pie
  • 2,300
  • 2
  • 23
  • 46
  • By the way, I know that the 'throw e;' loses all the call stack and 'throw;' would be better - it's like that at the moment because it's a nice place to put a soft breakpoint. :-) – Evil Dog Pie Apr 28 '14 at 15:13
  • ...and mine is a Prolific USB to RS232 cable as well. – Evil Dog Pie Apr 28 '14 at 15:19
  • Is "serialStream" an instance of your Protocol container class? – Tom Bushell Apr 28 '14 at 17:14
  • Tom - Yes, it's a modbus RTU master layer 1 class. The layer 2 (address and CRC) and layer 7 (function codes) have their own classes so that it can be extended to support ASCII and modbus TCP later in the project. The layer 2 is still a bit glitchy due to the limited timeout and asynchronous behaviour of the SerialPort class combined with the less-than-seamless interaction with the USB (the CDC device class is great for streaming bulk data, but not so useful for the small, time-delimited packets of modbus RTU - HID would have been more appropriate, I think). – Evil Dog Pie Apr 29 '14 at 07:51
  • My requirements are much simpler, but I'm already finding dealing with the SerialPort class more complicated than I expected. And have also had bad experiences with the Win32 serial API, and FTDI USB over serial drivers. Windows + Serial != fun. – Tom Bushell Apr 29 '14 at 20:42
1

I think a good deal of your problem is having the user trigger ReadDataAsync, and allowing it to be triggered while there is still a read in progress (from your description).

The right way to do this is to start a read when the serial port is opened, and in the completion handler for the read, start another one (well, check that the completion wasn't caused by closure of the port).

Simultaneous reads from a serial port are useless anyway, because you can't control the order in which incoming data will be handed off to complete routines.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Any idea why VS handles the thrown exception so poorly? Suppose it could be just a bug in VS, but still... – Tom Bushell Apr 23 '14 at 23:25
  • @TomBushell: When you "unplug the serial cable", that means you have the serial port connected to your computer, but not to anything on the other end? Or you are unplugging the serial port itself from the computer (typical with USB serial ports)? – Ben Voigt Apr 23 '14 at 23:27
  • I'm pulling the serial cable off the serial connector on the instrument I'm trying to control. I leave the USB to serial adapter plugged in to the computer. I do this to prevent any data coming in, and force the ReadAsync to wait. – Tom Bushell Apr 23 '14 at 23:32
  • @Tom: What chipset is the USB to serial? FTDI? PL2303? Have you updated the drivers for that? What you have sounds like a thread stuck in a kernel-mode driver call. – Ben Voigt Apr 23 '14 at 23:39
  • Just tried to update drivers from Device Manager, but did not find anything newer (though the current drivers are dated 2009...hmmm). Don't know chipset...driver shows as ATEN USB to Serial Bridge, from Prolific Technologies. – Tom Bushell Apr 23 '14 at 23:51
  • Also, running Win8 64 bit, if that makes a difference. – Tom Bushell Apr 23 '14 at 23:52
  • @Tom: If you can find the USB VID and PID, that should help us find the right driver. I'm quite sure there are newer versions. Device Manager searches Windows Update, but that rarely finds the latest driver. – Ben Voigt Apr 24 '14 at 05:54
  • USB\VID_0557&PID_2008&REV_0300 USB\VID_0557&PID_2008 Device Manager - Hardware IDs – Tom Bushell Apr 24 '14 at 18:28
  • 3.3.7 does seem to be the latest driver for the ATEN models. If you're concerned about this problem I definitely would check whether it also occurs with other USB-serial converters. Try an FTDI-based one or any of those that are USB CDC compliant and load with the Microsoft usbser.sys – Ben Voigt Apr 24 '14 at 18:48