Search code examples
c#methodsparametersserial-port

Passing original public variables as parameters of method to be continually checked


I'm almost certain this question has been asked before, but probably not in the way I'm going to ask it.

I'm trying to make a simple method called DAwaiter that waits until a specific public bool variable is made true by another thread or event. I am not trying to pass a new copy of the variable into the method, I am trying to continually test the value of a specific, already-instanced variable.
I could solve this issue by making every task, such as the Home task, have its own individual copy of DAwaiter, but I'm lazy and don't want to have to update every single command individually if I can help it.

I've tried using ref bool waiter but couldn't wrap my head around how to use it effectively. I've tried using object waiter as well and am unsure why it's not working.

Here's my example:

public class MyForm : Form
{
    ArduinoComms AConnection = new ArduinoComms();

    private void homeButton_Click(object sender, EventArgs e)
    {
        Task.Run(AConnection.Home);
    }
}


public class ArduinoComms 
{
    public SerialPort Port = new SerialPort(/*parameters here*/); //creates and instances an internal serial port.

    Port.DataReceived = new SerialDataReceivedEventHandler(Port_ReceivedData);

    public bool XDone, YDone, Homed, Ready, Stopped, Locked = false; //initializes a lot of bools

    string NewDataContent = "Default newDataContent - should be inaccessible. If you see this, an error has occurred.";
    

    public void Home()
    {
        Homed = false;
        Ready = false;
        XDone = false;
        YDone = false;
        Logger("Beginning home");
        //WriteMyCommand(2); sends command to arduino.
        if (!DAwaiter(Homed, true, 250)) { return; }
        
        Logger("finished home, beginning backoff");
        XDone = false;
        YDone = false;
        //WriteMyCommand(0, backoff);
        //WriteMyCommand(1, backoff);
        if (!DAwaiter(XDone && YDone, true, 250)) { return; }

        Logger("Finished home");
        Ready = true;
    }

    internal bool DAwaiter(object waiter, bool expectedValue, int delay)
    {
        bool waiterVal = (bool)waiter;

        CancellationToken ct = cts.Token;

        Logger($"{Homed}");
        Logger($"Beginning DAwaiter");
        while (waiterVal != expectedValue)
        {
            Logger($"awaiting... {waiterVal}");
            Thread.Sleep(delay);
            if (ct.IsCancellationRequested)
            {
                Logger($"Cancelled DAwaiter.");
                return false;
            }
            waiterVal = waiter;
        }
        Logger($"DAwaiter finished true.");
        return true;
    }

    private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        SerialPort spL = (SerialPort)sender; //instances an internal object w/ same type as the event's sender.
        byte[] buf = new byte[spL.BytesToRead]; //instantiates a buffer of appropriate length.
        spL.Read(buf, 0, buf.Length); //reads from the sender, which inherits the data from the sender, which *is* our serial port.
        NewDataContent = $"{System.Text.Encoding.ASCII.GetString(buf)}"; //assembles the byte array into a string.
        Logger($"Received: {NewDataContent}"); //prints the result for debug.
        string[] thingsToParse = NewDataContent.Split('\n'); //splits the string into an array along the newline in case multiple responses are sent in the same message.

        foreach (string thing in thingsToParse) //checks each newline instance individually.
        {
            switch (thing)
            {
                case string c when c.Contains("Home done"): //checks incoming data for the arduino's report phrase "Home done" when it is homed.
                    Homed = true;
                    Logger($"Homed {Homed}");
                    break;

                default: break; //do nothing
            }
        }
    }
    
    public void Logger(string message)
    {
        //log the message.
    }
}

In this example, I'm using DAwaiter to wait until the arduino on the other end of the SerialPort responds with a home signal. It registers this "home" signal by turning the public variable Homed = true . However, the DAwaiter method is only passing the starting value of Homed , which means it will never stop awaiting.

  1. Is there a way I can make this pass the variable, so it will constantly test whether Homed == true in the if-statement in DAwaiter ?

  2. Is this expandable to multiple variables, such as a condition where I wait for Axis1 && Axis2 == true ?

Any advice is much appreciated.


Solution

  • Looking at the intent of your code, you appear to be using DAwaiter as a sort of roll your own synchronization object. It might be easier to use one of the ready-made synchronization objects like SemaphoreSlim.

    I made a simulation/mock up of how you could convert the bools to semaphores and await them asynchronously. (Since you have MyForm : Form I did the example in Winforms as one might infer, but the actual semaphore code is portable).

    logger


    Main Window

    public partial class MyForm : Form
    {
        public MyForm()
        {
            InitializeComponent();
            ArduinoComms = new ArduinoComms();
            ArduinoComms.Log += (sender, e) =>
            {
                richTextBox.AppendText($@"{DateTime.Now:hh\:mm\:ss\.ffff}: {e.Message}{Environment.NewLine}");
                richTextBox.SelectionStart = richTextBox.Text.Length;
                richTextBox.ScrollToCaret();
            };
            buttonHome.Click += async (sender, e) =>
            {
                UseWaitCursor = true;
                buttonHome.Enabled = false;
                await ArduinoComms.Home();
                buttonHome.Enabled = true;
                UseWaitCursor = false;
                // Cursor may need to be "nudged" to redraw
                Cursor.Position = new Point(Cursor.Position.X + 1, 0);
            };
        }
        ArduinoComms ArduinoComms { get; }
    }
    

    ArduinoComms Sim with Semaphores

    {
        #region S I M
        class MockSerialPort
        {
            public MockSerialPort(byte[] simBuffer) => SimBuffer = simBuffer;
            public byte[] SimBuffer { get; }
        };
        #endregion S I M
    
        public ArduinoComms()
        {
            Port.DataReceived += Port_DataReceived;
        }
    
        public SerialPort Port = new SerialPort(/*parameters here*/); //creates and instances an internal serial port.
    
        public SemaphoreSlim XDone = new SemaphoreSlim(1, 1);
        public SemaphoreSlim YDone = new SemaphoreSlim(1, 1);
        public SemaphoreSlim Homed = new SemaphoreSlim(1, 1);
        public SemaphoreSlim Ready = new SemaphoreSlim(1, 1);
        public SemaphoreSlim Stopped = new SemaphoreSlim(1, 1);
        public SemaphoreSlim Locked = new SemaphoreSlim(1, 1);
    
        string NewDataContent = "Default newDataContent - should be inaccessible. If you see this, an error has occurred.";
    
        public async Task Home()
        {
            Logger($"Beginning home");
            try
            {
                // Await for any previous calls to clear
                await Homed.WaitAsync(timeout: TimeSpan.FromSeconds(10));
                // Send command to arduino as Fire and Forget.
                _ = MockWriteMyCommand(2); 
                await Homed.WaitAsync();
            }
            finally
            {
                Homed.Release();
            }
            Logger("finished home, beginning backoff");
            try
            {
                await XDone.WaitAsync(timeout: TimeSpan.FromSeconds(10));
                _ = MockWriteMyCommand(0, backoff: true);
                await XDone.WaitAsync();
            }
            finally
            {
                XDone.Release();
            }
            try
            {
                await YDone.WaitAsync(timeout: TimeSpan.FromSeconds(10));
                _ = MockWriteMyCommand(1, backoff: true);
                await YDone.WaitAsync();
            }
            finally
            {
                YDone.Release();
            }
            Logger($"Finished home{Environment.NewLine}");
        }
        Random _rando = new Random(Seed: 1); // Seed is for repeatability during testing
        private async Task MockWriteMyCommand(int cmd, bool? backoff = null)
        {
            switch (cmd)
            {
                case 0:
                    await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
                    Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes($"XDone backoff={backoff}")), default);
                    break;
                case 1:
                    await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
                    Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes($"YDone backoff={backoff}")), default);
                    break;
                case 2:
                    await Task.Delay(TimeSpan.FromSeconds(_rando.Next(1, 4)));
                    Port_DataReceived(new MockSerialPort(System.Text.Encoding.ASCII.GetBytes("Home done")), default);
                    break;
                default:
                    Debug.Fail("Unrecognized command");
                    break;
            }
        }
    
        private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            byte[] buf;
            switch (sender?.GetType().Name)
            {
                case nameof(SerialPort):
                    var spL = (SerialPort)sender;
                    buf = new byte[spL.BytesToRead]; //instantiates a buffer of appropriate length.
                    spL.Read(buf, 0, buf.Length); //reads from the sender, which inherits the data from the sender, which *is* our serial port.
                    break;
                case nameof(MockSerialPort):
                    var mspL = (MockSerialPort)sender;
                    buf = mspL.SimBuffer;
                    break;
                default: throw new NotImplementedException();
            }
    
            NewDataContent = $"{System.Text.Encoding.ASCII.GetString(buf)}"; //assembles the byte array into a string.
            Logger($"Received: {NewDataContent}"); //prints the result for debug.
            string[] thingsToParse = NewDataContent.Split('\n'); //splits the string into an array along the newline in case multiple responses are sent in the same message.
    
            foreach (string thing in thingsToParse) //checks each newline instance individually.
            {
                switch (thing)
                {
                    case string c when c.Contains("Home done"): //checks incoming data for the arduino's report phrase "Home done" when it is homed.
                        Homed.Release();
                        Logger($"Homed");
                        break;
                    case string c when c.Contains("XDone"): 
                        XDone.Release();
                        Logger($"XDone");
                        break;
                    case string c when c.Contains("YDone"): 
                        YDone.Release();
                        Logger($"YDone");
                        break;
    
                    default: break; //do nothing
                }
            }
        }
        public event EventHandler<LoggerMessageArgs> Log;
        public void Logger(string message) => Log?.Invoke(this, new LoggerMessageArgs(message));
    }
    
    public class LoggerMessageArgs
    {
        public LoggerMessageArgs(string message) => Message = message;
    
        public string Message { get; }
    }