Nooelec SDR with Modular Biped for Real-Time 433 MHz IoT Signal Processing
As the IoT landscape expands, more devices communicate on the 433 MHz frequency, a common frequency for home automation systems, remote controls, weather stations, and similar IoT devices. With the Modular Biped project, we can leverage an RTL-SDR USB dongle from Nooelec to scan for and process these 433 MHz transmissions, making data accessible for other modules through Python’s PubSub library.
This article introduces a custom Python module to interface with the Nooelec SDR, enabling data collection, processing, and publication over a PubSub bus. We’ll cover the SDR module setup, the configuration file, and example output to show how the module interprets and broadcasts data from nearby IoT devices.
Module Overview
The RTLSDR
Python module, created for the Modular Biped project, uses the rtl_433
software to collect data and re-broadcasts information on detected IoT signals. This module starts by initiating an HTTP stream from rtl_433
and listens to messages. Once configured, it is straightforward to publish or subscribe to specific topics via PubSub.
Here’s a breakdown of the components:
- Configuration File: Defines the IP and port for the HTTP stream, topics for subscribing and publishing data, and dependencies.
- Python Module: Manages the RTL-SDR’s processes and parses incoming data for real-time analysis and publication.
- Output Example: Shows how the module captures data like temperature, humidity, and battery status from various IoT devices.
Configuration File
The configuration file defines key settings and dependencies for the RTLSDR
module:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
rtl_sdr:
enabled: true
path: modules.network.rtlsdr.RTLSDR
config:
udp_host: "127.0.0.1"
udp_port: 8433
timeout: 70
topics:
publish_data: "sdr/data"
subscribe_listen: "sdr/listen"
subscribe_start: "sdr/start"
subscribe_stop: "sdr/stop"
dependencies:
unix:
- "rtl-433"
python:
- "pypubsub"
- "requests"
- Host and Port: Specifies the IP and port for the
rtl_433
HTTP stream. - Timeout: Sets a response timeout of 70 seconds.
- Topics: Defines PubSub topics for starting, stopping, and publishing data.
- Dependencies: Lists system and Python dependencies required by the module.
Python Module Code
The following Python code uses the rtl_433
tool to listen for 433 MHz signals, convert JSON data from these signals, and publish them over a PubSub network:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#!/usr/bin/env python3
import requests
import json
import subprocess
from time import sleep
from pubsub import pub
class RTLSDR:
def __init__(self, **kwargs):
self.udp_host = kwargs.get('udp_host', "127.0.0.1")
self.udp_port = kwargs.get('udp_port', 8433)
self.timeout = kwargs.get('timeout', 70)
self.topics = kwargs.get('topics')
self.rtl_process = None
pub.subscribe(self.start_rtl_433, self.topics['subscribe_start'])
pub.subscribe(self.listen_once, self.topics['subscribe_listen'])
pub.subscribe(self.stop_rtl_433, self.topics['subscribe_stop'])
def start_rtl_433(self):
"""Starts the rtl_433 process with HTTP (line) streaming enabled."""
if self.rtl_process is None:
try:
self.rtl_process = subprocess.Popen(
["rtl_433", "-F", f"http://{self.udp_host}:{self.udp_port}"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
print(f"Started rtl_433 on {self.udp_host}:{self.udp_port}")
except FileNotFoundError:
print("rtl_433 command not found. Please ensure rtl_433 is installed.")
else:
print("rtl_433 is already running.")
def stop_rtl_433(self):
"""Stops the rtl_433 process if it is running."""
if self.rtl_process:
self.rtl_process.terminate()
self.rtl_process.wait()
self.rtl_process = None
print("Stopped rtl_433 process.")
else:
print("rtl_433 is not currently running.")
def stream_lines(self):
"""Stream lines from rtl_433's HTTP API."""
url = f'http://{self.udp_host}:{self.udp_port}/stream'
headers = {'Accept': 'application/json'}
try:
response = requests.get(url, headers=headers, timeout=self.timeout, stream=True)
print(f'Connected to {url}')
for chunk in response.iter_lines():
yield chunk
except requests.ConnectionError:
print("Failed to connect to rtl_433 HTTP stream.")
self.stop_rtl_433()
def handle_event(self, line):
"""Process each JSON line from rtl_433."""
try:
data = json.loads(line)
print(data)
pub.sendMessage(self.topics['publish_data'], data=data)
label = data.get("model", "Unknown")
if "channel" in data:
label += ".CH" + str(data["channel"])
elif "id" in data:
label += ".ID" + str(data["id"])
if data.get("battery_ok") == 0:
print(f"{label} Battery empty!")
if "temperature_C" in data:
print(f"{label} Temperature: {data['temperature_C']}°C")
if "humidity" in data:
print(f"{label} Humidity: {data['humidity']}%")
except json.JSONDecodeError:
print("Failed to decode JSON line:", line)
def listen_once(self):
"""Listen to one chunk of the rtl_433 stream."""
for chunk in self.stream_lines():
chunk = chunk.rstrip()
if chunk:
self.handle_event(chunk)
def rtl_433_listen(self):
"""Listen to rtl_433 messages in a loop until stopped."""
self.start_rtl_433()
try:
while True:
try:
self.listen_once()
except requests.ConnectionError:
print("Connection failed, retrying in 5 seconds...")
sleep(5)
finally:
self.stop_rtl_433()
if __name__ == "__main__":
try:
sdr = RTLSDR(topics={
'subscribe_listen': 'sdr/listen',
'publish_data': 'sdr/data',
'subscribe_start': 'sdr/start',
'subscribe_stop': 'sdr/stop'
})
sdr.rtl_433_listen()
except KeyboardInterrupt:
print('\nExiting.')
sdr.stop_rtl_433()
Example Output
Below is an example of how the module interprets and outputs data. This example includes different devices detected by the SDR dongle, showing each device’s model, temperature, humidity, and other metadata:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
archie@archie:~/modular-biped $ python modules/network/rtlsdr.py
Started rtl_433 on 127.0.0.1:8433
Connected to http://127.0.0.1:8433/stream
{'time': '2024-10-28 11:39:04', 'model': 'Esperanza-EWS', 'id': 11, 'channel': 2, 'battery_ok': 1, 'temperature_F': 67.6, 'humidity': 0, 'mic': 'CRC'}
Esperanza-EWS.CH2 Humidity: 0%
{'time': '2024-10-28 11:39:57', 'model': 'Esperanza-EWS', 'id': 11, 'channel': 2, 'battery_ok': 1, 'temperature_F': 67.6, 'humidity': 0, 'mic': 'CRC'}
Esperanza-EWS.CH2 Humidity: 0%
{'time': '2024-10-28 11:40:50', 'model': 'Esperanza-EWS', 'id': 11, 'channel': 2, 'battery_ok': 1, 'temperature_F': 67.6, 'humidity': 0, 'mic': 'CRC'}
Esperanza-EWS.CH2 Humidity: 0%
{'time': '2024-10-28 11:41:28', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:42:36', 'model': 'Esperanza-EWS', 'id': 11, 'channel': 2, 'battery_ok': 1, 'temperature_F': 67.4, 'humidity': 0, 'mic': 'CRC'}
Esperanza-EWS.CH2 Humidity: 0%
{'time': '2024-10-28 11:43:06', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:06', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:07', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:07', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:10', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:10', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:12', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:12', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:13', 'model': 'Abarth-124Spider', 'type': 'TPMS', 'id': '150000c6', 'flags': '6e', 'pressure_kPa': 345.0, 'temperature_C': -48, 'status': 64, 'mic': 'CHECKSUM'}
Abarth-124Spider.ID150000c6 Temperature: -48°C
{'time': '2024-10-28 11:43:13', 'model': 'Abarth-124Spider', 'type': 'TPMS', 'id': '150000c6', 'flags': '6e', 'pressure_kPa': 345.0, 'temperature_C': -48, 'status': 64, 'mic': 'CHECKSUM'}
Abarth-124Spider.ID150000c6 Temperature: -48°C
{'time': '2024-10-28 11:43:32', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:32', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'}
Renault.IDfa6ec6 Temperature: -30°C
{'time': '2024-10-28 11:43:35', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:35', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:36', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:36', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:36', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
{'time': '2024-10-28 11:43:36', 'model': 'Truck', 'type': 'TPMS', 'id': '50000c66', 'wheel': 239, 'pressure_kPa': 0, 'temperature_C': 0, 'state': 1, 'flags': 10, 'mic': 'CHECKSUM'}
Truck.ID50000c66 Temperature: 0°C
Conclusion
This RTLSDR
module is an efficient way to expand the Modular Biped project with IoT connectivity using a Nooelec SDR USB dongle. By harnessing the 433 MHz frequency, this module enables real-time data publication over PubSub, allowing other modules to interact with and respond to nearby IoT devices seamlessly.