C++ Sensor Interface

Because it's easier if they all speak the same language

Posted on March 26, 2020

A problem arose when I was given the task to develop a new sensor platform for the company I worked for at the time. This platform had to have the ability to modularly switch out sensors during runtime. In software this meant to keep track of the availability and status of all sensors and act accordingly.

Some of the sensors used on the new platform had already been used in other projects, so to ease the development those were reused. Some were pretty consistent naming and function wise, but this wasn’t the case for all of them. Building a working system with these becomes unreadable and bloated really fast, as can be seen in the example below.

Sensor1 s1;
Sensor2 s2;

s1_enabled = false;
s2_enabled = false;

void setup()
{
    Wire.begin();
    s1_enabled = s1.begin();

    Serial.begin(115200);
    s2_enabled = s2.begin();
    s2.set_some_parameter(variable)
}

void loop()
{
    if (s1_enabled)
    {
        if (s1.read_measurement())
        {
            // Add the values to a datapoint
            // Do some logging
        }
        else
        {
            // Error handling, disable sensor?
            // Do some error logging
        }
    }

    // Rince and repeat for the other sensors
}

So what would be a better solution?

What I wanted to make was a kind of manager, something I could add all the sensors to and iterate over them. So I needed a way to uniformalise all the sensor drivers. Que a search around google and stack overflow and the conclusion was, I need an interface class.

This class with virtual functions would be inherited by a sensor wrapper which combines the drivers functionality into one function, which is universal between the wrappers. The functions I needed every wrapper to do were:

  • Initialise the sensor
  • Get the measurements and store them in a datapoint
  • Reset the sensor
  • Log the most recent measurement (debugging)
  • And log its enable status (debugging)

The implementation of the ISensor class looked like this:

class ISensor
{
public:
    virtual boolean init() = 0;
    virtual boolean get_measurements() = 0;
    virtual boolean reset() = 0;
    virtual void log_measurements() = 0;
    virtual void log_status() = 0;

    bool enabled;
}

When implemented the first example can be written like this:


Sensor1_wrapper s1;
Sensor2_wrapper s2;

// Array of the sensor interface
ISensor *sensors[] = {&s1, &s2};
const uint8_t number_of_sensors = sizeof(sensors) / sizeof(sensors[0]);

void setup()
{
  for (int i = 0; i < number_of_sensors; i++) // Check all initialised sensors.
  {
    if (!sensors[i]->enabled) // If the sensor is not enabled.
    {
      sensors[i]->init();	    // Try initialisating the sensor.
      sensors[i]->log_status(); // And log the sensor status.
    }
  }
}

void loop()
{
for (int i = 0; i < number_of_sensors; i++) // Check all initialised sensors.
  {
    if (sensors[i]->enabled) // If the sensor is enabled.
    {
      if (sensors[i]->get_measurements()) // Start a measurement.
      {
        sensors[i]->log_measurements(); // And log the measurement if the measurement was succesfull.
      }
      else // If the measurement fails.
      {
        sensors[i]->reset();		 // Reset the sensor.
        sensors[i]->enabled = false; // Disable the sensor till the next sensor check.
        sensors[i]->log_status();	 // And log the sensor status.
      }
    }
  }
}

The “magic” happens in the sensors array. This array of type ISensor contains the pointers to the wrapper classes we want to use. By iterating over this array and calling the virtual functions, the correct startup sequence for every sensor can be executed while maintaining readability. The second example might not look shorter, but when adding a sensor only the array has to be extended. The rest of the code scales automatically.