C# GUI refresh and async serial port communication

I'm trying to create an application which communicates with hardware via serial port and reports the results to the gui.

Currently moving through GUI is made by KeyEvents which trigger the drawing of the next "page" of GUI. However at one step (after the key is pressed) I need to draw new page and send few commands via serial port.

The command sending is done via :

port.Write(data, 0, data.Length);

I then wait for the answer by waiting for DataReceivedHandler to trigger - it just pins out that there is data awaiting and data is being processed in another method.

At first I just put sending & receiving command in the function drawing the page after the "draw parts" however it made it stuck - the data was being transfered, but the page wasn't drawn - it was frozen.

Then I made an async method :

private async void SendData()
{
  await Task.Run(() => serialClass.SendAndReceive(command));
  // process reply etc.
}

Which is used like that :

public void LoadPage()
{
  image = Image.FromFile(path);
  //do some stuff on image using Graphics, adding texts etc.
  picturebox1.Image = image;
  SendData();
}

It works fine, however I need to "reload" the page (to call again LoadPage). If I do it inside the async method like this :

private async void SendData()
{
  await Task.Run(() => serialClass.SendAndReceive(command));
  // process reply etc.
  LoadPage();
}

Then obviously the image won't be refreshed, though the data will be send via serial port. Is it possible to somehow check if async function was finished and trigger an event where I could reload the page?

So far I've tried using the BackGroundWorker Work Complete and Property Change. The data was send again, but the image wasn't reloaded. Any idea how I can achieve that?

Thanks in advance for the help, Best regards


You need to use a state machine and delegates to achieve what you are trying to do. See the code below, I recommend doing all this in a separate thread other then Main. You keep track of the state you're in, and when you get a response you parse it with the correct callback function and if it is what you are expecting you move onto the next send command state.

private delegate void CallbackFunction(String Response);    //our generic Delegate
private CallbackFunction CallbackResponse;                  //instantiate our delegate
private StateMachine currentState = StateMachine.Waiting;

SerialPort sp;  //our serial port

private enum StateMachine
{
    Waiting,
    SendCmd1,
    Cmd1Response,
    SendCmd2,
    Cmd2Response,
    Error
}

private void do_State_Machine()
{
    switch (StateMachine)
    {
        case StateMachine.Waiting:
            //do nothing
            break;
        case StateMachine.SendCmd1:
            CallbackResponse = Cmd1Response;    //set our delegate to the first response
            sp.Write("Send first command1");    //send our command through the serial port
            
            currentState = StateMachine.Cmd1Response;   //change to cmd1 response state
            break;
        case StateMachine.Cmd1Response:
            //waiting for a response....you can put a timeout here
            break;
        case StateMachine.SendCmd2:
            CallbackResponse = Cmd2Response;    //set our delegate to the second response
            sp.Write("Send command2");  //send our command through the serial port
            
            currentState = StateMachine.Cmd2Response;   //change to cmd1 response state
            break;
        case StateMachine.Cmd2Response:
            //waiting for a response....you can put a timeout here
            break;
        case StateMachine.Error:
            //error occurred do something
            break;
    }
}

private void Cmd1Response(string s)
{
    //Parse the string, make sure its what you expect
    //if it is, then set the next state to run the next command
    if(s.contains("expected"))
    {
        currentState = StateMachine.SendCmd2;
    }
    else
    {
        currentState = StateMachine.Error;
    }
}
    
private void Cmd2Response(string s)
{
    //Parse the string, make sure its what you expect
    //if it is, then set the next state to run the next command
    if(s.contains("expected"))
    {
        currentState = StateMachine.Waiting;
        backgroundWorker1.CancelAsync();
    }
    else
    {
        currentState = StateMachine.Error;
    }
}

//In my case, I build a string builder until I get a carriage return or a colon character.  This tells me
//I got all the characters I want for the response.  Now we call my delegate which calls the correct response
//function.  The datareceived event can fire mid response, so you need someway to know when you have the whole
//message.
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
    string CurrentLine = "";
    string Data = serialPortSensor.ReadExisting();

    Data.Replace("\n", "");

    foreach (char c in Data)
    {
        if (c == '\r' || c == ':')
        {
            sb.Append(c);

            CurrentLine = sb.ToString();
            sb.Clear();
            
            CallbackResponse(CurrentLine);  //calls our correct response function depending on the current delegate assigned
        }
        else
        {
            sb.Append(c);
        }
    }
}

I would put this in a background worker, and when you press a button or something you can set the current state to SendCmd1.

Button press

private void buttonStart_Click(object sender, EventArgs e)
{
    if(!backgroundWorker1.IsBusy)
    {
        currentState = StateMachine.SendCmd1;
        
        backgroundWorker1.RunWorkerAsync();
    }
}

Background worker do work event

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    while (true)
    {
        if (backgroundWorker1.CancellationPending)
            break;

        do_State_Machine();
        Thread.Sleep(100);
    }
}

edit: you can use invoke to update the GUI from your background worker thread.

this.Invoke((MethodInvoker)delegate
{
    image = Image.FromFile(path);
    //do some stuff on image using Graphics, adding texts etc.
    picturebox1.Image = image;
});