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.