Using Meadow’s SensorService to Optimize Sensor Reads into a Single Thread

As modern .NET developers, we’re gotten used to seeing and using Tasks in our code. They do a great job of abstracting away the complexity and dangers of old school multi-threaded code. If you’ve got experience creating complex threaded applications using Threads you definitely know what I mean. If you haven’t, don’t feel bad, you’re probably better off for it – it’s generally just a lot of frustratingly-hard debugging and failures that are super hard to reproduce.

The problem with Task.

With that said, while Tasks and async code have improved things, they can also create their own set of problems, especially in low-resource environments where without care, you can exhaust the available threads and also spend unnecessary CPU cycles switching thread contexts. It’s for this very reason that we created a shared thread called the SensorService that enables you to put all of your sensor reads into a single thread .

To understand the problem, let’s examine PollingSensorBase, the base class for many sensors in Meadow.Foundation. The pseudocode basically looks like the following:

new Task(async () =>
{
    while (true)
    {
        Conditions = await ReadSensor();
    }
}, TaskCreationOptions.LongRunning)
.Start();

This is beautiful, reasonable, and modern C# code. And for a desktop application, you’d probably never give it a second thought. On Meadow, if you’re just reading one sensor, it’s also a reasonable pattern.

The challenge here is that the Task is marked as LongRunning, which tells the runtime to always create a dedicated thread for it instead of delegating it to the Task thread pool.

Thread Exhaustion

Why is this a problem? Well, the Meadow F7-based platforms are using a micro-RTOS POSIX kernel called NuttX, and that RTOS is designed for running on embedded devices with limited resources. One of the constraints it has is the number of threads the entire system is allowed to create – in our current configuration that number is 32. That means that in total no more than 32 threads can be created. That number includes not just the application but also drivers, the base class libraries (e.g. the Task thread pool), the OS itself (e.g. networking), and even HCOM (the USB serial communication used to talk with your development machine). 32 threads really isn’t a lot, and if every sensor consumes one of those slots, a solution with several sensors can exhaust this pretty quickly.

There’s also the overhead of managing context switching between threads. And even if they were in a thread pool, there’s still management overhead of picking up and putting down threads.

Moving to a Single-Threaded Solution

What’s the solution, then? Let’s assume that many of the sensors in your solution have a reasonably slow read period – on the order of several seconds or more between reads. There’s no reason that those reads couldn’t be done in a thread that iterates through all of the sensors with a single loop. Something like this (again, this is simplified pseudocode):

new Thread(async () =>
{
    while (true)
    {
        foreach(var sensor in sensorList)
        {
            sensor.Conditions = await sensor.ReadSensor();
        }
    }
}
.Start();

Getting it for Free with SensorService

In our drive to mature the Meadow platform, Meadow version 1.5 now provides your applications the ability to opt-in to this single-thread behavior by using the new SensorService. By default, when a sensor that derives from PollingSensorBase is created, it will get the usual Task-per-sensor behavior that Meadow.Foundation sensors have always had. However, if you register the sensor with the new SensorService, the polling behavior will automatically get migrated to a single polling thread for you. No extra work or code is required on your part, it just happens.

var mySensor = new BME688(Device.CreateI2CBus());
Resolver.SensorService.RegisterSensor(mySensor);

Two things to note here:

  1. The sensor value update period has a granularity of 1 second. If your sensor is defined with an UpdateInterval of less than 1 second then calling RegisterSensor will have no effect, meaning it will continue to use its own Task. It also means that if you have the period set to something that is not evenly divisible by 1 second – for example 2.5 seconds – it will wait until the next second after the defined UpdateInterval to refresh. basically the interval always gets rounded up to the next second.
  2. This only applies to sensors that implement IPollingSensor. You can register other sensors with the SensorService and still use the service capabilities for sensor discovery and resolution, but only IPollingSensors will get migrated to the shared poll thread.

If you have any questions about Tasks and Threads and their impacts in Meadow, or would like to see additional features around platform sensors and sensor discovery, feel free to reach out to us on our public Slack channel or create a Feature Request.