Implement Gateway class that discovers Nordic Thingy:52 nodes via BLE
and publishes sensor data to MQTT broker on each notification received.
- Automatic node discovery via BLE service UUID (ef680100)
- GATT notifications for temperature, humidity and CO2
- Publish immediately on reception to {gateway_id}/{mac}/update
- Connection timeout to avoid blocking on unreachable nodes
- Disconnection detection and automatic reintegration into scan
- Logging with DEBUG/INFO/WARNING/ERROR levels
Assisted-by: Claude:claude-sonnet-4-6 — debugging BLE parallel connections (BlueZ InProgress error), GATT UUID discovery (ef680100 vs ef680200), byte decoding for temperature/humidity/CO2, async connection timeout implementation
Gateway — BLE to MQTT
This component runs on a Raspberry Pi 4 and acts as the communication bridge between the Nordic Thingy:52 sensor nodes and the rest of the system. It reads environmental data from the nodes over BLE and publishes it to a local MQTT broker (Mosquitto) in a structured JSON format.
Role in the architecture
Thingy:52 nodes --> (BLE) --> Raspberry Pi --> (MQTT) --> Database / ML / Notifications
Dependencies
Install the required Python libraries:
pip3 install bleak paho-mqtt --break-system-packages
Install and start the Mosquitto MQTT broker:
sudo apt install -y mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
Usage
python3 gateway.py
At startup, the script asks for the room ID (e.g. C1, A2, B5). It then automatically discovers all Thingy:52 nodes in range by filtering BLE advertising packets on the Nordic Configuration service UUID (ef680100). Each detected node is assigned a name based on the room ID and a counter (e.g. C1_thingy1, C1_thingy2).
To run the gateway in the background and keep it running after closing SSH:
nohup python3 gateway.py > log.txt 2>&1 &
MQTT topic structure
classroom/{room_id}/{node_id}
Example: classroom/C1/C1_thingy1
Message format
Each message is published as a JSON object with the following structure:
{
"timestamp": "2026-03-26T13:24:05.176072+00:00",
"room_id": "C1",
"node_id": "C1_thingy1",
"sensors": {
"co2_ppm": 400,
"temperature_c": 25.37,
"humidity_pct": 44
}
}
| Field | Type | Description |
|---|---|---|
| timestamp | string (ISO 8601 UTC) | Time of measurement, added by the gateway |
| room_id | string | Identifier of the monitored room |
| node_id | string | Identifier of the Thingy:52 node |
| co2_ppm | integer | eCO2 concentration in parts per million |
| temperature_c | float | Indoor air temperature in degrees Celsius |
| humidity_pct | integer | Relative humidity in percent |
Output files
For each session, two local files are created in the gateway directory:
data_{room_id}.csv— comma-separated file for local analysisdata_{room_id}.json— JSON file following the database team format
Publishing interval
The default publishing interval is 5 minutes (300 seconds).
This can be adjusted by modifying the INTERVAL variable in gateway.py.
Utility scripts
scan.py— scans for nearby BLE devices and prints their name and MAC addresscheck_uuid.py— connects to Thingy:52 nodes and prints their advertised service UUIDs
Notes on the CO2 sensor
The Thingy:52 uses a CCS811 sensor which measures eCO2 (equivalent CO2), estimated from volatile organic compound (VOC) levels rather than directly measuring CO2 concentration. Values should be interpreted as indicative trends rather than precise measurements, particularly during the first 24 to 48 hours of operation while the sensor calibrates.
Overnight test results
An overnight test was conducted with 2 Thingy:52 nodes placed in two separate rooms (windows closed, 7h session, 5 minute interval).
- Room 1: 4 occupants (2 adults, 2 children)
- Room 2: unoccupied
CO2 rose progressively from 400 ppm to a peak of 1071 ppm in the occupied room, consistent with human respiration in a confined space. The unoccupied room remained stable between 400 and 465 ppm, confirming that variations in Room 1 are directly linked to human presence.
Note: the CCS811 sensor measures eCO2 estimated from VOC levels, not direct CO2. Occasional spikes (e.g. 877 ppm at 02:45, 1071 ppm at 03:05) may be caused by factors such as perspiration or movement near the sensor and should be interpreted with caution.
