Search code examples
c#winformsuser-interfaceserial-porttimeout

C# Wait for a Feedback with serialPort_DataReceived Event


I am communicating with a microcontroller on a serial port using Winform GUI, which I am developing.

I am sending a set of commands according to a predefined protocol and receiving feedback strings from the microcontroller.

I am wondering if there is a simple way to wait for a certain feedback after a command is sent.

E.g.

  1. Send a command
  2. Wait for a Set Amount of Time (could be a few seconds to several minutes)
  3. Display the feedback if in time and proceed to issue the next command/action

If the feedback is not received in time, the timeout will be triggered and failure message displayed. If the data comes back in time, the waiting method should be stopped immediately and proceed to the next course of action. I do not want to block the UI while awaiting a feedback as well.

I am using the following code for receiving the data.

    delegate void SetTextCallback(string text);

    private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            string receivedData = serialPort1.ReadExisting();
            SetText(receivedData);
        }
        catch (IOException exception)
        {
            MessageBox.Show(exception.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        catch (Exception exception)
        {
            MessageBox.Show(exception.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void SetText(string text)
    {
        if (this.textBoxReceive.InvokeRequired)
        {
            SetTextCallback d = SetText;
            this.Invoke(d, new object[] { text });
            //this.Invoke(new Action(() => { this.textBoxReceive.AppendText(text); }));

        }
        else
        {
            if (text.Length > 15)
            {
                CheckPosition(text); //To check for a position number from the feedback string
            }
            this.textBoxReceive.AppendText(text);

        }
    }

And here is my write method.

    private void SendCommand(int move, int trigger)
    {
        try
        {
            if (serialPort1.IsOpen)
            {
                string cmd = string.Empty;
                //Format: { “move”: 0, “trigger”: 0}
                cmd = "{ " + "\"move\": " + move + ","
                      + " \"trigger\": " + trigger 
                      + " }"
                      + Environment.NewLine;
                textBoxReceive.AppendText("Send:" + cmd + Environment.NewLine); // Display sent command
                serialPort1.DiscardOutBuffer();
                serialPort1.DiscardInBuffer();
                serialPort1.Write(cmd);

            }
            else if (serialPort1.IsOpen != true)
            {
                MessageBox.Show(@"Lost COM Port.", "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }

        }
        catch (IOException e)
        {
            MessageBox.Show(e.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);

        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "Message", MessageBoxButtons.OK, MessageBoxIcon.Error);

        }

    }

And I have a click-button method which I have been struggling with the delay (///Start waiting) like so.

    private void buttonDet_Click(object sender, EventArgs e)
    {

        ResetPositionMark();
        serialPort1.DiscardInBuffer(); 
        //Position 1 at Initial Stage
        textBoxStatus.AppendText("Position 1: Init/Home." + Environment.NewLine);
        SendCommand(0,1);   //Trigger LED1
        textBoxStatus.AppendText("LED1 triggered." + Environment.NewLine);
        Thread.Sleep(200);
        ///Camera Capture

        //============== Position 2 ==============
        SendCommand(2,0);   //Move to Position 2
        textBoxStatus.AppendText("Moving to Position 2..." + Environment.NewLine);
        **///Start waiting**
        if (timeout)
        {
            textBoxStatus.AppendText("Position 2 Timeout reached." + Environment.NewLine);
        }
        else
        {
            textBoxStatus.AppendText("Data received in time." + Environment.NewLine);
            textBoxStatus.AppendText("Position 2 OK." + Environment.NewLine);
            SendCommand(0, 2);  //Trigger LED2 once the motor reaches the position 2
            textBoxStatus.AppendText("LED2 triggered." + Environment.NewLine);
        }


        ///Camera Capture

        //============== Position 3 ==============
        SendCommand(3,0);   //Move to Position 3
        textBoxStatus.AppendText("Moving to Position 3..." + Environment.NewLine);
        **///Start waiting**
        if (timeout)
        {
            textBoxStatus.AppendText("Position 3 Timeout reached." + Environment.NewLine);
        }
        else
        {
            textBoxStatus.AppendText("Data received in time." + Environment.NewLine);
            textBoxStatus.AppendText("Position 3 OK." + Environment.NewLine);
            SendCommand(0, 3);  //Trigger LED3 once the motor reaches the position 2 
            textBoxStatus.AppendText("LED3 triggered." + Environment.NewLine);
        }

        ///Camera Capture


        SendCommand(1, 0);  //Move back to Home position (position 1)
        textBoxStatus.AppendText("Moving Home to Position 1..." + Environment.NewLine);
        **///Start waiting**
        if (timeout)
        {
            textBoxStatus.AppendText("Back to Home Timeout!" + Environment.NewLine);
        }
        else
        {
            textBoxStatus.AppendText("Data received in time." + Environment.NewLine);
            textBoxStatus.AppendText("Home now." + Environment.NewLine);
        }
    }

I am not well-versed with threading and ManualResetEvent, etc.

Please help to see how best to wait for the data, preferably with code samples.

Thanks a lot.


Solution

  • Here is a simple solution: https://1drv.ms/u/s!AnSTW4R3pQ5uitAnyiAKTscGPHpxYw

    The idea is that you create a separate thread when you start sending commands, that is done by:

                Task.Factory.StartNew(() => {
            }, _canecellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    

    The parameters here are mostly speaking for themselves:

    1. Is the a function that I want to execute on this thread/task
    2. Cancellation token, so that I can terminate the thread when I no longer need to send commands
    3. "LongRunning" option indicates that this will be long running task. You can read more on it here.
      1. Passing a default scheduler.

    Next what you need to do is to create an instance of AutoResetEvent. How it works in details you can read on MSDN. But in a nutshell it is a switch that has two state, open and closed. By default you want it to be closed, that is what false parameter in the constructor is for. In the event handler from the serial port (DataReceived) you want to "open" AutoResetEvent. So you do this:

    dataReceivedEvent.Set(); 
    

    Now, when you issue the command you wait for the AutoResetEvent to be "opened" and you specify a time you're willing to wait, like this:

    var succeeded = dataReceivedEvent.WaitOne(TimeSpan.FromSeconds(3));
    

    This means that if in 3 seconds AutoResetEvent is not opened, stop waiting and report a failure. Basically it returns false if it was not opened in the given time frame or true if it was. Since it is "Auto" reset event it will automatically "close" itself after finish waiting, so you don't have to reset manually.

    All the rest is what you already have. Using invoke to interact with a UI and reading/sending commands.

     public class Communicator
    {
        CancellationTokenSource _canecellationTokenSource = new CancellationTokenSource();
        List<Command> _scenario = new List<Command>(6)
        {
            Command.Trigger(1),
            Command.MoveTo(2),
            Command.Trigger(2),
            Command.MoveTo(3),
            Command.Trigger(3),
            Command.MoveTo(1)
        };
    
        public void Start(ListBox feedbackControl)
        {
            Task.Factory.StartNew(() => {
                var dataReceivedEvent = new AutoResetEvent(false);
                var ct = _canecellationTokenSource.Token;
                var controller = new DummyMicrocontroller();
                DataReceivedEventHandler onDataReceived = (cmd) => { dataReceivedEvent.Set(); };
                controller.DataReceived += onDataReceived;
                foreach (var cmd in _scenario)
                {
                    if (ct.IsCancellationRequested)
                    {
                        AddItemSafe(feedbackControl, $"Operation cancelled...");
                        break;
                    }
    
                    AddItemSafe(feedbackControl, cmd.GetMessage(Command.MessageType.Info));
                    controller.Send(cmd);
                    var succeeded = dataReceivedEvent.WaitOne(TimeSpan.FromSeconds(3));
                    var messageType = succeeded ? Command.MessageType.Success : Command.MessageType.Error;
                    AddItemSafe(feedbackControl, cmd.GetMessage(messageType));
                }
    
                AddItemSafe(feedbackControl, $"Finished executing scenario.");
    
                controller.DataReceived -= onDataReceived;
    
            }, _canecellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    
        }
    
        public void Stop(ListBox feedbackControl)
        {
            AddItemSafe(feedbackControl, $"Attempting to cancel...");
            _canecellationTokenSource.Cancel();
        }
    
        private void AddItemSafe(ListBox feedbackControl, object item)
        {
            if (feedbackControl.InvokeRequired)
            {
                feedbackControl.Invoke((MethodInvoker)delegate { AddItemSafe(feedbackControl, item); });
            }
            else
            {
                feedbackControl.Items.Add(item);
            }
        }
    }
    

    UI stays on it's own thread and is not affected. Since I don't have a microcontroller available I had to write a dummy simulator :)