1

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.

TLT
  • 45
  • 8
  • I am trying to do the same, can you please guide me on the same – Prashant Pimpale Mar 07 '19 at 12:44
  • @PrashantPimpale, you could elaborate more on your problem? If it is the delay you want, you could use async and await with Task.Delay(delayInMs) method. E.g. await Task.Run(async () { await Task.Delay(delayInMs); }); – TLT Mar 12 '19 at 03:35
  • Have a look at my question:https://stackoverflow.com/questions/55026042/wait-for-response-from-the-serial-port-and-then-send-next-data – Prashant Pimpale Mar 12 '19 at 05:24

1 Answers1

1

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 :)

BerserkerDotNet
  • 186
  • 1
  • 5
  • Thank you so much for your help, BerserkerDotNet! As I am unfamiliar with all these multi-threading terms/concepts, could you please simplify it to novice level on which method is doing what and which one calls what and how to modify my methods consequently? – TLT Jul 12 '17 at 05:00
  • I've updated my answer to be more explanatory, hope it helps. It should not be hard for you to update you code to use the solution I provided. Replace DummyMicrocontroller by your real serialport driven controller and consider having array of commands before hand so that you don't have to duplicate code and issue one command after the other. – BerserkerDotNet Jul 12 '17 at 06:21
  • Thank you for being patient with me. I am learning a lot from you. It helps greatly. Another question: the timeout here is fixed to 3 seconds. What if I need to send a command and the PCB/uC reports to me half an hour later after whatever action it is supposed to do has been completed? Because each command will cause uC to do different things, hence different action takes variable amount of time. – TLT Jul 12 '17 at 07:27
  • please enlighten me more as I have been trying the logic and seem to understand it well, but when I tried to adapt it to my code, I am basically stuck. @BerserkerDotNet. – TLT Jul 12 '17 at 08:43
  • I've updated the solution to fit your code a bit more. Now I have a scenario that lists all the commands that need to be executed. So the only thing you need to is to replace DummyMicrocontroller with a real one that uses serial port and that should be enough. I think that is as close as I can get to your scenario :) P.S. Package on OneDrive is also updated. – BerserkerDotNet Jul 13 '17 at 04:46
  • Thank you for your kind help @BerserkerDotNet! Greatly appreciate it. I am now trying to put my serial port setup into DummyMicrocontroller.cs so that I could test-run the program and see line by line how it functions. Obviously, I have been using single thread till now and this is quite advanced for me. :-) – TLT Jul 14 '17 at 04:59
  • My serial port stuff are located on the form itself, like Port Numbers and Baud Rate so that the user may choose and click Connect button. So in commanding.cs, I placed the form-related stuff. My send command(write) method's placed in DummyMicrocontroller class. Am I doing right? – TLT Jul 14 '17 at 05:24
  • I am attaching my current code which I have modified using ManualResetEvent. I know it is not good enough yet. I am trying to call serial port from DummyMicrocontrooler.cs but still having some errors. Sorry for asking very basic questions as I am still learning C Sharp. [link](https://drive.google.com/file/d/0BxnagGOzOX2lNF9JMktfakh4dmc/view) – TLT Jul 14 '17 at 05:37
  • I couldn't manage to fit my serialport settings into the DummyMicrocontroller.cs. Could you care to explain how I access the winform stuff from that class? Thank you for your guidance. – TLT Jul 16 '17 at 18:54
  • [Here](https://1drv.ms/u/s!AnSTW4R3pQ5uitBuXnZVI9rZm3-MmA) is modified version of your code. There are a few TODOs left for you to fill in, but otherwise it should work. I have no controller that I can communicate with other COM port, but at least I can see that timeout is working. – BerserkerDotNet Jul 18 '17 at 02:46
  • Wow. Thank you for your kind help, @BerserkerDotNet. Btw, the link is not available when I click it. Probably, you haven't changed the permission yet? – TLT Jul 18 '17 at 04:20
  • Glad to receive help from people like you, @BerserkerDotNet! A wonderful solution, and I have learnt a great deal from you!! Can't thank you enough! Made my day :-) – TLT Jul 18 '17 at 07:56
  • Glad I can help :) – BerserkerDotNet Jul 18 '17 at 17:21
  • @BerserkerDotNet I am also trying to do the same but unable to get the solution – Prashant Pimpale Mar 07 '19 at 12:47