Wednesday, February 2, 2022

[SOLVED] EventHandler to pass continuous asynchronous I2C data for use in ViewModel

Issue

I'm using an I2C sensor connected to a Raspberry Pi and need to collect and report continuous data from that bus and pass it along so that I can process the data and update the GUI with the sensor readings. I am using .NET Core so that I can be cross platform on linux and can't find resources on Async I2C collection on the Pi.

I currently have data being pulled from the I2C bus correctly and know how to implement/update the ViewModel, I just don't know how to pull the data continuously and on a new thread.

I have experience doing similar work in Windows with using System.IO.Ports which already has DataReceivedEventHandlers built in, but as I am staring from scratch here I have hit a bit of a road block. This is my first time diving into async tasks and event handlers which by themselves can be a bit confusing.

2 Main Issues so far: How to collect I2C data as async or on new thread? How to pass it to synchronous method on different thread?

The code below is what I have so far.

I2CInitialize.cs

public class I2CInitialize
{
    public event EventHandler<I2CDataEventArgs> NewI2CDataRecieved;
        
    ///Need to call readingi2c in new thread and pass continuous i2c sensor data
    public static string readingi2c()
        {
            int i = 0
            while (true)
                {
                     ///Taking advantage of using's dispose feature so that memory isn't consumed on loop
                     using (var i2cBus = new I2CBus(bus))
                        {
                            ///Driver to get I2C bytes from bus sits here. Returns bytes and converts to string
                            string result;
                            result = "I2CDataString";
                            return result;
                         }
                 }
        }

///Calls readingi2c() and passes continuous string "data" to subscribers            
public void I2C_DataRecieved()
    {
        string data = readingi2c();

         if (NewI2CDataRecieved != null)
             NewI2CDataRecieved(this, new I2CDataEventArgs(data));          
    }


public class I2CDataEventArgs : EventArgs
    {
        public I2CDataEventArgs(string dataInByteArray)
        {
            Data = dataInByteArray;
        }
        public string Data;
    }
}

MainWindow.cs

public class MainWindow : Window
{
    public void _NewI2CDataRecieved(object sender, I2CDataEventArgs e)
        {
            string RawStr = e.ToString();
            ///Update MainWindowViewModel with continuously passed I2C sensor data string "RawStr"
        }

    public MainWindow()
        {
            var _i2cInitialize = new I2CInitialize();
            _i2cInitialize.NewI2CDataRecieved += new EventHandler<I2CDataEventArgs>(_NewI2CDataRecieved);

            _i2cInitialize.I2C_DataRecieved();
        }
}

Solution

I was able to take the above approach and apply recommendations from here in reference to avoiding Thread.Abort. It appears that this method has not been a recommended way of terminating a thread for a while. My current approach added a boolean to end the thread. This is a highly debated way of approaching this problem because the thread has to fully complete instead of immediately being terminated. For my application, it only needs to complete getting 16 bytes from an I2C sensor which is essentially instantaneous and has no real world impacts on compute cycles and/or end user experience. I also made I2CInitialize into a Singleton so that the thread can be started and stopped from the same instance anywhere in the application.

I2CInitialize.cs

public sealed class I2CInitialize
{
    //Start Singleton instance
    public static I2CInitialize Instance { get; private set; }

    private I2CInitialize() { }

    static I2CInitialize() { Instance = new I2CInitialize(); }
        
    public event EventHandler<I2CDataEventArgs> NewI2CDataRecieved;
    private Thread i2cThread;    

    //Set this bool to true or false to run new thread
    public bool i2cReadBool;
 
    public void readingi2c()
       {
          //If bool is true run continuously and send data
          while (i2cReadBool)
          {
             //Keep this using keyword for disposing of continuous data
             using (var i2cBus = new I2CBus(bus))
                {
                   //Get data from I2C Bus here
                   string result;
                   result = "I2CDataString";
                   //With the question mark you can omit the if-statement.
                   EventHandler<I2CDataEventArgs> handler = NewI2CDataRecieved;
                   handler?.Invoke(this, new I2CDataEventArgs(result));                    
                }
           }
       } 

    //Starts the thread
    public void I2C_StartReceiveData()
    {
        if (i2cThread == null || !i2cThread.IsAlive)
        {
                i2cReadBool = true;
                ThreadStart ts = new ThreadStart(() => readingi2c());
                i2cThread = new Thread(ts);
                i2cThread.Start();
        }
    }

    //Stops the thread
    //After adding 'using' reference to this namespace you can call this 
    //function to stop the thread from anywhere:
    //I2CInitialize.Instance.I2C_StopReceiveData();
    public void I2C_StopReceiveData()
    {
        if (i2cReadBool)
        {
            i2cReadBool = false;
        }
    }
}

MainWindow.cs

public class MainWindow : Window
{
    public void _NewI2CDataRecieved(object sender, I2CDataEventArgs e)
    {
        string RawStr = e.Data;
        //Update UI from RawStr string here
    }

    public MainWindow()
    {
        //Call I2CInitialize with Sinleton allows us to call from anywhere
        //using I2CInitialize.Instance

        I2CInitialize.Instance.NewI2CDataRecieved += _NewI2CDataRecieved;

        I2CInitialize.Instance.I2C_StartReceiveData();
    }
}

To stop/end the thread you simply need to call I2CInitialize.Instance.I2C_StopReceiveData(); Make sure to add this to a closing event of your app window so that the thread doesn't continue to run after main app window has been closed.



Answered By - ryano
Answer Checked By - Robin (WPSolving Admin)