Unwrapping Sensor Wrappers – Part 1: Sensors

The SIGHTS software suite is designed to be very extensible when it comes to adding new sensors to your robot. We achieve this by using special Python classes called sensor wrappers to collect sensor data, and JavaScript classes called Graphs to display it. SIGHTS is shipped with many useful sensor wrappers for a lot of common sensors, but to use some less common sensors, you may need to write your own wrappers and graphs.

In this blog post, I’ll unpack one of the bundled SIGHTS sensor wrappers to show you how you can create your own. If you want to learn how to create a graph, check out the next post in this series.

What is a Sensor Wrapper?

As I briefly outlined above, a sensor wrapper is a Python class used to collect sensor data from a sensor. A sensor wrapper extends the SensorWrapper class and can import any other library to do the heavy lifting – the idea is that you don’t need to write libraries specifically for SIGHTS.

Take a look at the SensorWrapper class in sensor_wrapper.py (view on GitHub).

__init__(self, config)

The sensor constructor. Here we set up the logger for the sensor and an instance attribute to track when the sensor was last polled.

When a sensor is created by the SensorStream class, the sensor is provided with its definition in the configuration file. The SensorWrapper class handles a few required config values such as the sensor name and whether the sensor is enabled, but you can implement more configuration settings in your __init__ function when you extend SensorWrapper (which we will see when we create our own sensor wrapper below).

get_data(self) and get_initial(self)

These functions get data from the sensor. Both are unimplemented by the SensorWrapper class. get_initial() is called once when the sensor is initialised. get_data() is called every time the sensor is ready to be polled. Both are optional, but in most cases, you’ll want to implement at least get_data().

is_ready(self, now)

Given the current UNIX timestamp (now), is_ready() returns true if the sensor is ready to be polled using the period defined in the config.

Writing a Sensor Wrapper

Now we can write a sensor wrapper for a sensor that extends the SensorWrapper class described above. We’ll go through the DiskUsageWrapper (view on GitHub) since it works on all devices (no extra sensors required) and it implements all the functions in the SensorWrapper class.

Create your new Python file in the sights/src/sensors directory. You can name this whatever you want, but it’s probably a good idea to follow the existing convention with sensorname_wrapper.py.

First we import the required modules and libraries. The only required line here is the one importing SensorWrapper. We also import psutil because we’ll be using that to check the disk usage.

If any of the modules you import aren’t part of the Python Standard Library, don’t forget to add them to requirements.txt in sights/src so that the required library will be installed by pip during the installation process.

Now create a class extending the SensorWrapper class. It’s a good idea to follow the existing naming convention, with the name of the sensor wrapper in PascalCase.

The type_ attribute defines the name of the sensor type in the config. This means when a “disk_usage” sensor appears in a config file, SIGHTS will know to create a new DiskUsageWrapper to handle it. Check out the init function in the SensorStream class to see how this works (view on GitHub).

Now we can implement the functions in SensorWrapper. First up is the init function. Usually, all you will need to do is call the superclass constructor with SensorWrapper.__init__(self, config). In this case, however, we want to add an extra config option called “precision” which we’ll use to round our result to a certain number of decimal places before sending it to the interface. Sometimes you may want to initialise a I2C bus and sensor library here too (such as with the MLX90614Wrapper).

get_initial() is an optional function that sends initialisation data to the interface. It’s a good idea to implement the “limit” message if your sensor can report some kind of hard limit (such as the total amount of system memory for a RAM sensor, or in this case (for our disk usage sensor), the size of the disk). The “limit” message is used by circle graphs to calculate the percentage of fill.

You can send other initialisation data here too if required by simply adding another entry to the dictionary. It will be ignored by graphs on the interface unless implemented on the interface as well (see part 2 of this series).

Finally, we need to implement get_data(). This is the function that will poll the sensor and return the current reading.

Some sensors collect multiple readings. You can return these as dictionaries like the SGP30 sensor does (view on GitHub).

Adding a Sensor to the Config Schema

The configuration of each sensor can be slightly different, so it’s helpful to users if you create a config schema that defines how your sensor wrapper can be configured.

Locate the sensors definition in the config schema at sights/interface/js/sights.config.schema.js. You will notice that a sensor can be “any of” a list of sensors. All you need to do is add your sensor with its configuration values to the list of options.

Below is an example for the disk usage sensor wrapper we created above.

You may remember from earlier that some sensors such as the SGP30 sensor can return multiple values using a dictionary. So how do we let the user configure where to display each value from a multi-sensor individually?

Simply make display_on an object. Each property is the name of a reading, which is an array of graph IDs. Here’s an example using the same sensor.

Contributing your Own Sensor Wrapper to SIGHTS

If you’ve created a sensor wrapper for a sensor that’s not included with SIGHTS, in the spirit of open source collaboration, we’d love to include it in the official SIGHTS repository! The process is simple:

  1. Fork the repository at https://github.com/SFXRescue/sights
  2. Make your changes on your fork (add your new sensor wrapper and config schema changes!)
  3. Submit a pull request with your additions

It’s generally easier to focus on one feature per pull request, so if you have other unrelated changes you want to propose or you have multiple sensor wrappers to contribute, please consider making separate pull requests for each.

Further Reading

You can read the documentation about extending SIGHTS. Documentation is available offline when you install SIGHTS and is accessible through the interface. If you haven’t installed SIGHTS, you can still access this online at GitHub.

If there are major changes to the function of sensor wrappers and graphs, you may need to refer to the most up-to-date documentation at https://github.com/SFXRescue/sights/blob/master/docs/extending.md.

If the process for creating a sensor wrapper or graph changes significantly enough, we may post a new tutorial as well.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.