Building a Custom Arduino Driver for DHT11 Sensors - Heros Devices 0.10.3 documentation

Skip to content
Back to top
View this page

# Building a Custom Arduino Driver for DHT11 Sensors

## Introduction

This tutorial demonstrates how to create a custom device driver for an Arduino board with two DHT11 sensors attached to it. The driver will communicate over a serial connection and assumes that your Arduino replies to commands like `t0`, `t1`, `h0`, and `h1` with the according sensor readings.

## Where to Put the Driver?

This does not really matter as long as it accessible from your boss installation. The easiest way is to download the herosdevices git repository and create your own branch

    git clone https://gitlab.com/atomiq-project/herosdevices.git
    cd herosdevices
    git checkout -b temperature_arduino

Note

If you want to contribute your device driver upstream to the community it makes sense to create your own fork of the herostools repository. Learn more about forks here.

Next, open the file `src/herosdevices/hardware/arduino.py` in your favourite text editor.

## Implementation

We start by importing the `herosdevices.core.templates.SerialDeviceTemplate` template, which provides the foundation for serial communication and let our class inherit from it.

    1from herosdevices.core.templates import SerialDeviceTemplate
    2
    3class TempArduino(SerialDeviceTemplate):
    4    """
    5    An Arduino with two DHT11 humidity/temperature sensors attached to it.
    6    """
    7    pass

Next, we add an `__init__` method to initialize the serial connection. Here we set up the serial connection with the correct baud rate and line termination. The explicit values depend on your Arduino implementation but the ones used here are typically the default values. Arguments of the `__init__` function (here for example `address`) are then latter defined in a JSON string which is passed to the device by boss on startup.

    1def __init__(self, address: str, *args, **kwargs):
    2    super().__init__(address, baudrate=9600, line_termination=b"\n", *args, **kwargs)

Now, we define the attributes for each sensor using `herosdevices.core.DeviceCommandQuantity`. These attributes represent the commands to get temperature and humidity readings from the Arduino.

     1from herosdevices.core import DeviceCommandQuantity
     2
     3class TempArduino(SerialDeviceTemplate):
     4    """
     5    An Arduino with two DHT11 humidity/temperature sensors attached to it.
     6    """
     7    temperature_0 = DeviceCommandQuantity(
     8        command_get="t0\r\n",
     9        dtype=float,
    10        unit="°C",
    11    )
    12    temperature_1 = DeviceCommandQuantity(
    13        command_get="t1\r\n",
    14        dtype=float,
    15        unit="°C",
    16    )
    17    humidity_0 = DeviceCommandQuantity(
    18        command_get="h0\r\n",
    19        dtype=float,
    20        unit="%",
    21    )
    22    humidity_1 = DeviceCommandQuantity(
    23        command_get="h1\r\n",
    24        dtype=float,
    25        unit="%",
    26    )

Each `DeviceCommandQuantity` defines a command to send to the Arduino (e.g., `"t0\n"` for the first temperature sensor), the expected data type (`float`) and the unit of measurement.

Note

The `herosdevices.core.DeviceCommandQuantity` supports more complex casting and extraction of values from the values returned by the Arduino. Refer to the documentation of `herosdevices.core.DeviceCommandQuantity` for more details.

This works for two sensors but for more sensors defining each sensor separately is kind of ugly. To do this in a more readable and maintainable way we can attach to the `__new__` method which is called to create an instance of the class.

     1from herosdevices.core import DeviceCommandQuantity
     2from herosdevices.helper import add_class_descriptor
     3
     4    def __new__(cls, *args, **kwargs):
     5        for i_channel in range(2):
     6            name_str_t = f"temperature_{i_channel}"
     7            name_str_h = f"humidity_{i_channel}"
     8            add_class_descriptor(
     9                cls, name_str_t, DeviceCommandQuantity(command_get=f"t{i_channel}", dtype=float, unit="°C")
    10            )
    11            add_class_descriptor(
    12                cls, name_str_h, DeviceCommandQuantity(command_get=f"t{i_channel}", dtype=float, unit="%")
    13            )
    14        return super().__new__(cls)

The imported add_class_descriptor function takes care that the attributes are added in a way that they are directly visible to HEROS and are accessible from remote (e.g. with `obj.temperature_0` from the HERO Monitor.

Finally, we can add a `_observale_data` method so the class can be used for automatic observable data recording in combination with the `herostools.actor.statemachine.HERODatasourceStateMachine`.

    1    def _observable_data(self):
    2        return {
    3            "temperature_0": (self.temperature_0, "°C"),
    4            "temperature_1": (self.temperature_1, "°C"),
    5            "humidity_0": (self.humidity_0, "%"),
    6            "humidity_1": (self.humidity_1, "%"),
    7        }

Again this feels clumsy for more than one sensor. An easy way to make it more maintainable and also add some configuratability in the case you not always want to log everything is to introduce a dictionary `observables` which is then used to determine which observables are collected. Also one can define which observables are logged by default in the `__new__` method.

The complete file including the observables mechanics looks now as follows:

     1from herosdevices.core.templates import SerialDeviceTemplate
     2from herosdevices.core import DeviceCommandQuantity
     3from herosdevices.helper import add_class_descriptor
     4
     5class TempArduino(SerialDeviceTemplate):
     6    """
     7    An Arduino with two DHT11 humidity/temperature sensors attached to it.
     8    """
     9
    10    observables: dict
    11
    12    def __new__(cls, *args, **kwargs):
    13        for i_channel in range(2):
    14            name_str_t = f"temperature_{i_channel}"
    15            name_str_h = f"humidity_{i_channel}"
    16            add_class_descriptor(
    17                cls, name_str_t,
    18                DeviceCommandQuantity(command_get=f"t{i_channel}", dtype=float, unit="°C")
    19            )
    20            add_class_descriptor(
    21                cls, name_str_h,
    22                DeviceCommandQuantity(command_get=f"t{i_channel}", dtype=float, unit="%")
    23            )
    24            cls.default_observables[name_str_t] = {"name": name_str_t, "unit": "°C"}
    25            cls.default_observables[name_str_h] = {"name": name_str_h, "unit": "%"}
    26        return super().__new__(cls)
    27
    28    def __init__(self, address: str, *args, observables: dict | None,  **kwargs):
    29        self.observables = observables if observables is not None else self.default_observables
    30        super().__init__(address, baudrate=9600, line_termination=b"\n", *args, **kwargs)
    31
    32    def _observable_data(self):
    33        data = {}
    34        for attr, description in self.observables.items():
    35            data[description["name"]] = (getattr(self, attr), description["unit"])
    36        return data

## Installation and Configuration

To start the driver as a HERO, the easiest way is to use boss boss. The following JSON code creates and instance of the driver, including the observable logging functionality. Put that code into a file `arduino.json` somewhere to your liking.

Note

For production use, we recommend setting up a CouchDB to organise your JSON files to be accessible from everywhere in the network.

     1{
     2  "_id": "temp-arduino",
     3  "classname": "herosdevices.hardware.arduino.TempArduino",
     4  "arguments": {
     5    "address": "/dev/ttyUSB0"
     6  },
     7  "datasource": {
     8    "async": false,
     9    "interval": 300
    10  }
    11}

This configuration queries the Arduino every 300 seconds and emits temperature/humidity data via the `observable_data` event. For more information on data handling with datasources check the `heros.datasource.datasource.LocalDatasourceHERO` documentation.

Now install boss in a python virtual environment and execute the following command within the virtual environment to start the arduino HERO

    python -m boss.starter -u file:./arduino.json

You should now see something like the following output in the command line

    2025-09-09 14:20:49,566 boss: Reading device(s) from ['file:./arduino.json']
    2025-09-09 14:20:49,566 boss: refreshing HERO source file:./arduino.json
    2025-09-09 14:20:49,623 boss: creating HERO with name temp-arduino from class herosdevices.hardware.arduino.TempArduino failed: No module named 'herosdevices.hardware.arduino'
    2025-09-09 14:20:49,623 boss: Starting BOSS

You can now also see your device “temp-arduino” in HERO Monitor and read out temperatures and humidities from there.