|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Nooelec SDR with Modular Biped for Real-Time 433 MHz IoT Signal Processing |
| 4 | +date: 2024-10-28 11:50 |
| 5 | +categories: [Guides, Software, SDR] |
| 6 | +tags: [guide, SDR, IoT, python] |
| 7 | +--- |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +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. |
| 12 | + |
| 13 | +### Module Overview |
| 14 | + |
| 15 | +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. |
| 16 | + |
| 17 | +Here’s a breakdown of the components: |
| 18 | + |
| 19 | +1. **Configuration File:** Defines the IP and port for the HTTP stream, topics for subscribing and publishing data, and dependencies. |
| 20 | +2. **Python Module:** Manages the RTL-SDR's processes and parses incoming data for real-time analysis and publication. |
| 21 | +3. **Output Example:** Shows how the module captures data like temperature, humidity, and battery status from various IoT devices. |
| 22 | + |
| 23 | +### Configuration File |
| 24 | + |
| 25 | +The configuration file defines key settings and dependencies for the `RTLSDR` module: |
| 26 | + |
| 27 | +```yaml |
| 28 | +rtl_sdr: |
| 29 | + enabled: true |
| 30 | + path: modules.network.rtlsdr.RTLSDR |
| 31 | + config: |
| 32 | + udp_host: "127.0.0.1" |
| 33 | + udp_port: 8433 |
| 34 | + timeout: 70 |
| 35 | + topics: |
| 36 | + publish_data: "sdr/data" |
| 37 | + subscribe_listen: "sdr/listen" |
| 38 | + subscribe_start: "sdr/start" |
| 39 | + subscribe_stop: "sdr/stop" |
| 40 | + dependencies: |
| 41 | + unix: |
| 42 | + - "rtl-433" |
| 43 | + python: |
| 44 | + - "pypubsub" |
| 45 | + - "requests" |
| 46 | +``` |
| 47 | +
|
| 48 | +- **Host and Port**: Specifies the IP and port for the `rtl_433` HTTP stream. |
| 49 | +- **Timeout**: Sets a response timeout of 70 seconds. |
| 50 | +- **Topics**: Defines PubSub topics for starting, stopping, and publishing data. |
| 51 | +- **Dependencies**: Lists system and Python dependencies required by the module. |
| 52 | + |
| 53 | +### Python Module Code |
| 54 | + |
| 55 | +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: |
| 56 | + |
| 57 | +```python |
| 58 | +#!/usr/bin/env python3 |
| 59 | +
|
| 60 | +import requests |
| 61 | +import json |
| 62 | +import subprocess |
| 63 | +from time import sleep |
| 64 | +from pubsub import pub |
| 65 | +
|
| 66 | +class RTLSDR: |
| 67 | + def __init__(self, **kwargs): |
| 68 | + self.udp_host = kwargs.get('udp_host', "127.0.0.1") |
| 69 | + self.udp_port = kwargs.get('udp_port', 8433) |
| 70 | + self.timeout = kwargs.get('timeout', 70) |
| 71 | + self.topics = kwargs.get('topics') |
| 72 | + self.rtl_process = None |
| 73 | + pub.subscribe(self.start_rtl_433, self.topics['subscribe_start']) |
| 74 | + pub.subscribe(self.listen_once, self.topics['subscribe_listen']) |
| 75 | + pub.subscribe(self.stop_rtl_433, self.topics['subscribe_stop']) |
| 76 | +
|
| 77 | + def start_rtl_433(self): |
| 78 | + """Starts the rtl_433 process with HTTP (line) streaming enabled.""" |
| 79 | + if self.rtl_process is None: |
| 80 | + try: |
| 81 | + self.rtl_process = subprocess.Popen( |
| 82 | + ["rtl_433", "-F", f"http://{self.udp_host}:{self.udp_port}"], |
| 83 | + stdout=subprocess.PIPE, |
| 84 | + stderr=subprocess.PIPE |
| 85 | + ) |
| 86 | + print(f"Started rtl_433 on {self.udp_host}:{self.udp_port}") |
| 87 | + except FileNotFoundError: |
| 88 | + print("rtl_433 command not found. Please ensure rtl_433 is installed.") |
| 89 | + else: |
| 90 | + print("rtl_433 is already running.") |
| 91 | +
|
| 92 | + def stop_rtl_433(self): |
| 93 | + """Stops the rtl_433 process if it is running.""" |
| 94 | + if self.rtl_process: |
| 95 | + self.rtl_process.terminate() |
| 96 | + self.rtl_process.wait() |
| 97 | + self.rtl_process = None |
| 98 | + print("Stopped rtl_433 process.") |
| 99 | + else: |
| 100 | + print("rtl_433 is not currently running.") |
| 101 | +
|
| 102 | + def stream_lines(self): |
| 103 | + """Stream lines from rtl_433's HTTP API.""" |
| 104 | + url = f'http://{self.udp_host}:{self.udp_port}/stream' |
| 105 | + headers = {'Accept': 'application/json'} |
| 106 | + try: |
| 107 | + response = requests.get(url, headers=headers, timeout=self.timeout, stream=True) |
| 108 | + print(f'Connected to {url}') |
| 109 | + for chunk in response.iter_lines(): |
| 110 | + yield chunk |
| 111 | + except requests.ConnectionError: |
| 112 | + print("Failed to connect to rtl_433 HTTP stream.") |
| 113 | + self.stop_rtl_433() |
| 114 | +
|
| 115 | + def handle_event(self, line): |
| 116 | + """Process each JSON line from rtl_433.""" |
| 117 | + try: |
| 118 | + data = json.loads(line) |
| 119 | + print(data) |
| 120 | + pub.sendMessage(self.topics['publish_data'], data=data) |
| 121 | + label = data.get("model", "Unknown") |
| 122 | + if "channel" in data: |
| 123 | + label += ".CH" + str(data["channel"]) |
| 124 | + elif "id" in data: |
| 125 | + label += ".ID" + str(data["id"]) |
| 126 | +
|
| 127 | + if data.get("battery_ok") == 0: |
| 128 | + print(f"{label} Battery empty!") |
| 129 | + if "temperature_C" in data: |
| 130 | + print(f"{label} Temperature: {data['temperature_C']}°C") |
| 131 | + if "humidity" in data: |
| 132 | + print(f"{label} Humidity: {data['humidity']}%") |
| 133 | +
|
| 134 | + except json.JSONDecodeError: |
| 135 | + print("Failed to decode JSON line:", line) |
| 136 | +
|
| 137 | + def listen_once(self): |
| 138 | + """Listen to one chunk of the rtl_433 stream.""" |
| 139 | + for chunk in self.stream_lines(): |
| 140 | + chunk = chunk.rstrip() |
| 141 | + if chunk: |
| 142 | + self.handle_event(chunk) |
| 143 | +
|
| 144 | + def rtl_433_listen(self): |
| 145 | + """Listen to rtl_433 messages in a loop until stopped.""" |
| 146 | + self.start_rtl_433() |
| 147 | + try: |
| 148 | + while True: |
| 149 | + try: |
| 150 | + self.listen_once() |
| 151 | + except requests.ConnectionError: |
| 152 | + print("Connection failed, retrying in 5 seconds...") |
| 153 | + sleep(5) |
| 154 | + finally: |
| 155 | + self.stop_rtl_433() |
| 156 | +
|
| 157 | +if __name__ == "__main__": |
| 158 | + try: |
| 159 | + sdr = RTLSDR(topics={ |
| 160 | + 'subscribe_listen': 'sdr/listen', |
| 161 | + 'publish_data': 'sdr/data', |
| 162 | + 'subscribe_start': 'sdr/start', |
| 163 | + 'subscribe_stop': 'sdr/stop' |
| 164 | + }) |
| 165 | + sdr.rtl_433_listen() |
| 166 | + except KeyboardInterrupt: |
| 167 | + print('\nExiting.') |
| 168 | + sdr.stop_rtl_433() |
| 169 | +``` |
| 170 | + |
| 171 | +### Example Output |
| 172 | + |
| 173 | +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: |
| 174 | + |
| 175 | +```shell |
| 176 | +archie@archie:~/modular-biped $ python modules/network/rtlsdr.py |
| 177 | +Started rtl_433 on 127.0.0.1:8433 |
| 178 | +Connected to http://127.0.0.1:8433/stream |
| 179 | +{'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'} |
| 180 | +Esperanza-EWS.CH2 Humidity: 0% |
| 181 | +{'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'} |
| 182 | +Esperanza-EWS.CH2 Humidity: 0% |
| 183 | +{'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'} |
| 184 | +Esperanza-EWS.CH2 Humidity: 0% |
| 185 | +{'time': '2024-10-28 11:41:28', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 186 | +Renault.IDfa6ec6 Temperature: -30°C |
| 187 | +{'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'} |
| 188 | +Esperanza-EWS.CH2 Humidity: 0% |
| 189 | +{'time': '2024-10-28 11:43:06', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 190 | +Renault.IDfa6ec6 Temperature: -30°C |
| 191 | +{'time': '2024-10-28 11:43:06', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 192 | +Renault.IDfa6ec6 Temperature: -30°C |
| 193 | +{'time': '2024-10-28 11:43:07', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 194 | +Renault.IDfa6ec6 Temperature: -30°C |
| 195 | +{'time': '2024-10-28 11:43:07', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 196 | +Renault.IDfa6ec6 Temperature: -30°C |
| 197 | +{'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'} |
| 198 | +Truck.ID50000c66 Temperature: 0°C |
| 199 | +{'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'} |
| 200 | +Truck.ID50000c66 Temperature: 0°C |
| 201 | +{'time': '2024-10-28 11:43:12', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 202 | +Renault.IDfa6ec6 Temperature: -30°C |
| 203 | +{'time': '2024-10-28 11:43:12', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 204 | +Renault.IDfa6ec6 Temperature: -30°C |
| 205 | +{'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'} |
| 206 | +Abarth-124Spider.ID150000c6 Temperature: -48°C |
| 207 | +{'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'} |
| 208 | +Abarth-124Spider.ID150000c6 Temperature: -48°C |
| 209 | +{'time': '2024-10-28 11:43:32', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 210 | +Renault.IDfa6ec6 Temperature: -30°C |
| 211 | +{'time': '2024-10-28 11:43:32', 'model': 'Renault', 'type': 'TPMS', 'id': 'fa6ec6', 'flags': '05', 'pressure_kPa': 192.0, 'temperature_C': -30, 'mic': 'CRC'} |
| 212 | +Renault.IDfa6ec6 Temperature: -30°C |
| 213 | +{'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'} |
| 214 | +Truck.ID50000c66 Temperature: 0°C |
| 215 | +{'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'} |
| 216 | +Truck.ID50000c66 Temperature: 0°C |
| 217 | +{'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'} |
| 218 | +Truck.ID50000c66 Temperature: 0°C |
| 219 | +{'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'} |
| 220 | +Truck.ID50000c66 Temperature: 0°C |
| 221 | +{'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'} |
| 222 | +Truck.ID50000c66 Temperature: 0°C |
| 223 | +{'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'} |
| 224 | +Truck.ID50000c66 Temperature: 0°C |
| 225 | +``` |
| 226 | + |
| 227 | +### Conclusion |
| 228 | + |
| 229 | +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. |
0 commit comments