Continuously reading from serial port asynchronously properly

11,321

You need to change:

private void serialConnectClick(object sender, EventArgs e)

to

private async void serialConnectClick(object sender, EventArgs e)

...and change the call to UpdateMessageBox(); to:

await UpdateMessageBox().ConfigureAwait(true);

UpdateMessageBox should use ConfigureAwait(true) in order to capture the current context (UI) else the messageTextBox.Text += message; would execute on a different thread:

public async Task UpdateMessageBox()
{
    messageTextBox.Text += "Reading data from serial.";
    while (_serialConnected)
    {
        string message = await SerialReadLineAsync(serial).ConfigureAwait(true);
        messageTextBox.Text += message;
    }
}

In SerialReadLineAsync, you can change:

await serialPort.BaseStream.ReadAsync(buffer, 0, 1);

...to:

await serialPort.BaseStream.ReadAsync(buffer, 0, 1).ConfigureAwait(false);

...because SerialReadLineAsync statements are not related to the UI.

The general tip is "async all the way" which means any method that awaits an async method also needs to be made async and in turn awaited. Repeat this up the call-stack.

Share:
11,321
Xander Luciano
Author by

Xander Luciano

JavaScript & C# developer, working to improve CAD + CAM engineering. Core focus: Vue.js, and Three.js, and Electron.

Updated on July 20, 2022

Comments

  • Xander Luciano
    Xander Luciano almost 2 years

    I'm just going to preface this by saying I just started trying to use Async today, so I have a very limited understanding of what I am doing.

    I was previously using threads to read data from a serial port, process that data into data to write, and then writing it. I had 1 thread reading data and placing it into a buffer, another thread processing data from a buffer, and a final thread writing it. This way if I clicked a button, I could send additional data.

    Now I am just trying to learn and ensure I am doing everything properly, so I am trying to read data from the serial port, and add that data to a multilined textbox. Here's the code:

    Connect to serial port, if successful, call UpdateMessageBox which is async:

    private void serialConnectClick(object sender, EventArgs e)
    {
        if (!_serialConnected)
        {
            _serialConnected = SerialConnect(portCombo.Text, int.Parse(baudCombo.Text));
            if (!_serialConnected)
            {
                portCombo.SelectedIndex = 0;
                messageTextBox.Text += "Failed to connect.\r\n";
                return;
            }
    
            serialConnectBtn.Text = "Disconnect";
            serialStatusLabel.Text = "Serial: Connected";
            serialStatusLabel.ForeColor = Color.Blue;
            messageTextBox.Text += "Connected\r\n";
            VDIportCombo.Enabled = false;
            soundSuccess.Play();
            UpdateMessageBox(); // this is an async function
        }
    }
    

    This continuously calls ReadLineAsync and adds the result to the textbox:

    public async Task UpdateMessageBox()
    {
        messageTextBox.Text += "Reading data from serial.";
        while (_serialConnected)
        {
            string message = await SerialReadLineAsync(serial);
            messageTextBox.Text += message;
        }
    }
    

    And this does ReadAsync on the SerialPort.BaseStream, and only returns data when we get a full line (denoted by a newline character):

    async Task<string> SerialReadLineAsync(SerialPort serialPort)
    {
        byte[] buffer = new byte[1];
        string result = string.Empty;
        Debug.WriteLine("Let's start reading.");
    
        while (true)
        {
            await serialPort.BaseStream.ReadAsync(buffer, 0, 1);
            result += serialPort.Encoding.GetString(buffer);
    
            if (result.EndsWith(serialPort.NewLine))
            {
                result = result.Substring(0, result.Length - serialPort.NewLine.Length);
                result.TrimEnd('\r','\n');
                Debug.Write(string.Format("Data: {0}", result));
                result += "\r\n";
                return result;
            }
        }
    }
    

    Am I doing everything correctly? Can I call an async method from the UI thread? Visual studios is telling me I should use await, but it is just suggesting that.

    I want other code to run on the UI thread while it is continuously reading, so I don't want to await the UpdateMessageBox function. If this were a thread, I would just want the read thread to operate in the background, and I would just do myReadThread.Start(), is there something similar for async?

    EDIT: Just to clarify, this code does work, but I want to know if it's the "proper" way to do what I am doing.

  • Xander Luciano
    Xander Luciano almost 8 years
    Ah, didn't know I could make that function async! Is there a way I don't have to await the UpdateMessageBox? When I include the await keyword, I get IOexceptions when I press the button a second time (which currently is programmed to close the serial port)
  • Xander Luciano
    Xander Luciano almost 8 years
    For anyone in the future, you can! Just add .ConfigureAwait(false); so, it should be UpdateMessageBox().ConfigureAwait(false);
  • MickyD
    MickyD almost 8 years
    You sure can. Just be careful about using ConfigureAwait(false) during a UI method else the statement immediately following could potentially executed on a different thread. Otherwise, if you don't care about what thread to use, use ConfigureAwait(false). Tell me more