Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sample serial port comms code using Async API in .net 4.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();
        }

    }
}
like image 271
Tom Bushell Avatar asked Apr 22 '14 21:04

Tom Bushell


3 Answers

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);
            }
        }
    }
}
like image 113
noseratio Avatar answered Nov 02 '22 08:11

noseratio


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();
like image 39
Evil Dog Pie Avatar answered Nov 02 '22 10:11

Evil Dog Pie


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.

like image 1
Ben Voigt Avatar answered Nov 02 '22 08:11

Ben Voigt