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.