Home > Net >  Waiting for a serial message before proceeding in C#
Waiting for a serial message before proceeding in C#

Time:02-23

I'm currently working on an interface between C# and an arduino project. I'm using the serial port to send and receive messages from the arduino and so far everything looks fine.

However I came to a point, where I want to send a string to the arduino and wait for a response until I'm going to send over the next string.

I figured that I cannot simply timeout here since my RichTextField is also going to freeze and the "OK*" message cannot be read from there.

I've read about some other solutions online but since I'm very new to C# I hope that somebody can point me in the right direction.

Best regards

//Edited the post with a full code example:

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


        namespace interface_test
        {
            public partial class Form1 : Form
            {

                string serialDataIn;

                public Form1()
                {
                    InitializeComponent();

                    Enable_Console();

                    comboBox_baudRate.Text = "115200";
                    comboBox_comPort.Text = "COM3";
                    string[] portLists = SerialPort.GetPortNames();
                    comboBox_comPort.Items.AddRange(portLists);
                }

                        private void Form1_Load(object sender, EventArgs e)
                {

                }

                /* erase textbox contents */
                private void button_clearLog_Click(object sender, EventArgs e)
                {
                    richTextBox_receiveMsg.Text = "";
                }

                /* listening to the serial port */
                private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
                {
                    serialDataIn = serialPort1.ReadExisting();
                    this.Invoke(new EventHandler(ShowData));
                }

                /* process the serial data */
                public void ShowData(object sender, EventArgs e)
                {
                    string trimmedMsg = serialDataIn;
                    richTextBox_receiveMsg.Text  = trimmedMsg;

                    if (trimmedMsg.Contains("Initialization done."))
                    {
                        button_detectCEM.Enabled = false;
                    }
                }

                /* open COM-port to arduino */
                private void button_openCom_Click(object sender, EventArgs e)
                {
                    try
                    {
                        serialPort1.PortName = comboBox_comPort.Text;
                        serialPort1.BaudRate = Convert.ToInt32(comboBox_baudRate.Text);
                        serialPort1.Open();
                        Enable_Console();

                        button_detectCEM.Enabled = true;
                    }
                    catch (Exception error)
                    {
                        MessageBox.Show(error.Message);
                    }
                }

                /* close COM-port to arduino */
                private void button_closeCom_Click(object sender, EventArgs e)
                {
                    if (serialPort1.IsOpen)
                    {
                        try
                        {
                            serialPort1.Close();

                            Enable_Console();
                        }
                        catch (Exception error)
                        {
                            MessageBox.Show(error.Message);
                        }
                    }
                }

                
                private void button_fileBrowser_Click(object sender, EventArgs e)
                {
                    openAndWrite();
                }

                public void openAndWrite()
                {
                    /* load a .txt file */
                        
                    OpenFileDialog openFileDialog1 = new OpenFileDialog();
                    openFileDialog1.InitialDirectory = "c:\\";
                    openFileDialog1.Filter = "TXT files (*.txt)|*.txt";
                    openFileDialog1.FilterIndex = 0;
                    openFileDialog1.RestoreDirectory = true;
                    string selectedFile = "";

                    if (openFileDialog1.ShowDialog() == DialogResult.OK)
                    {
                        selectedFile = openFileDialog1.FileName;
                    }

                    /* parse file and store it in data[] */
                    byte[] data;

                    using (FileStream fsSource = new FileStream(selectedFile, FileMode.Open, FileAccess.Read))
                    {
                        data = new byte[fsSource.Length];
                        int numBytesToRead = (int)fsSource.Length;
                        int numBytesRead = 0;
                        while (numBytesToRead > 0)
                        {
                            // Read may return anything from 0 to numBytesToRead.
                            int n = fsSource.Read(data, numBytesRead, numBytesToRead);

                            // Break when the end of the file is reached.
                            if (n == 0)
                                break;

                            numBytesRead  = n;
                            numBytesToRead -= n;
                        }
                        numBytesToRead = data.Length;
                    }

                for (int i = 0; i < BitConverter.ToInt32(blockLength, 0); i  )
                {
                    data[i] = data[offset   i];
                }

                    
                    /* write data block */
                        
                    offset = 0;

                    while (offset < data.Length)
                    {
                        byte[] dataString = new byte[6];
                        string blockMsg = "FFFFFF";

                        int start = offset;
                        offset  = 6;

                        if (offset > data.Length) offset = data.Length;
                        for (int i = 0; i < 6; i  )
                        {
                            if ((start   i) < data.Length) dataString[i] = data[start   i];
                        }

                        blockMsg  = BitConverter.ToString(dataString).Replace("-", string.Empty);
                        blockMsg  = "*";


                        serialPort1.Write(blockMsg);
                            
                        //Wait for "OK*" before next Message...

                    }
                }
            }
        }
    }

CodePudding user response:

There's a few ways that you could solve this. My approach would be to make use of the TaskCompletionSource class and a lambda function. Here's a rough example, you'll need to make the method containing this code async, and potentially have it return a Task to await it all the way up the chain.

var tcs = new TaskCompletionSource<string>();

serialPort1.DataReceived  = (s, e) =>
{
   SerialPort sp = (SerialPort)s;
   string newData = sp.ReadExisting();
   tcs.SetResult(newData);
}

serialPort1.Write(Msg);
var dataFromPort = await tcs.Task;

I'm away from an IDE, nor do I have access to all your code, so without the wider context it's hard to know exactly what else you might need. But I've done some Arduino work in the past, and this is how I solved the problem.

EDIT :: Thinking about it, you might (test it first) run into a problem because that event isn't being unsubscribed from and the code is running in a loop. IF that turns out to be a problem, you could make use of C#'s Local Functions to handle the event subscription, like this:

var tcs = new TaskCompletionSource<string>();

void localHandler(object s, SerialDataReceivedEventArgs e)
{
    serialPort1.DataReceived -= localHandler;
    SerialPort sp = (SerialPort)s;
    string newData = sp.ReadExisting();
    tcs.SetResult(newData);
};

serialPort1.DataReceived  = localHandler
serialPort1.Write(Msg);
var dataFromPort = await tcs.Task;

It might look a little strange, but it's certainly cleaner (IMO) then needing to keep track of instance variables and manually manage threads.

EDIT2 :: Tested when I was sat at a computer, if I understand your requirement correctly, this should do everything you need.

CodePudding user response:

This may take you in the right direction.

It might be useful in this case to separate the data received from the display update thread. Since the data received is an event, it will be coming in on its own thread, likely from some component - in this case the serial port.

The form, and all the display objects on it, run on a single thread, owned by the form.

You can use a timer as a background thread to make the leap between what was received and what you want to do/display.

For example, one way is to use a concurrent queue and a timer to poll the status of the queue.

    using System.Collections.Concurrent;

    private ConcurrentQueue<string> dataIn = new ConcurrentQueue<string>();
    private System.Threading.Timer timer = new System.Threading.Timer(CheckQue,null,TimeSpan.FromSeconds(1),TimeSpan.FromSeconds(1));
    private void CheckQue(object state)
    {
        while(dataIn.TryDequeue(out var data))
        {
            ShowData(data);

            if (data.Contains("OK*"))
            {
                //Do Something
                //Better yet raise an event that does something
               Msg  = BitConverter.ToString(dataString).Replace("-", string.Empty);
               Msg  = "*";

               serialPort1.Write(Msg);
            }
        }
    }

Then modify your above code to add to the queue when data is received

    private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
    {
        dataIn.Enqueue(SerialPort1.ReadExisting());
    }

And since your using a windows form, you need to make sure you don't get a cross-threaded operation when updating it, by marshaling the update onto the form's thread like so.

    public static void ShowData(string data)
    {
        if (this.InvokeRequired)
        {
            var del = new MethodInvoker(() => ShowData(data));
            try
            {
                this.Invoke(del);
                return;
            }
            catch
            { }
        }

        richTextBox_receiveMsg.Text  = data;

    }

EDIT based on your revised question: To modify for sending blocks of a file you could added another Que

private ConcurrentQueue<string> dataOut = new ConcurrentQueue<string>();

Then when you read the file, do whatever processing you need and put the final output in the queue and finish by sending the first block.

        public void openAndWrite()
        {
        /* load a .txt file */

        .
        .
        .

           while (offset < data.Length)
           {
            .
            .
            
            blockMsg  = BitConverter.ToString(dataString).Replace("-", string.Empty);
            blockMsg  = "*";

            dataOut.Enqueue(blockMsg);
           }
           
          SendBlock();
        }

Where SendBlock looks like this

    private void SendBlock()
    {
        if(dataOut.TryDequeue(out var data))
        {
            serialPort1.Write(data);
        }
    }

Then just modify the timer handler to send blocks as needed

    private void CheckQue(object state)
    {
        while (dataIn.TryDequeue(out var data))
        {
            ShowData(data);

            if (data.Contains("OK*"))
            {
                SendBlock();
            }
        }
    }
  • Related