IoT sensor network
CPE Lyon, team of 4, 2025 to 2026
A complete IoT chain built by four students for the Embedded Development & IoT module: micro:bit sensors, a custom secured radio protocol, a Python server, Google Firestore and an Android app. My part was the gateway firmware.
Goal
- Collect temperature, humidity, pressure and light from a connected object and send them over a secured radio link.
- Let an operator reconfigure the object's display remotely from an Android app.
- Cover the whole chain, from embedded firmware to cloud and mobile. Each of us owned one component: the object firmware, the gateway firmware (mine), the Python server and the Android app.
Architecture
Every link works both ways: measurements go up, configuration orders go down.
My part: the gateway firmware
- Two-way relay
Measurements go up to the server over USB serial, configuration frames go down to the object over 2.4 GHz.
- Frame filtering
Anything too short or missing VER=1 and ;CRC= is dropped: the 2.4 GHz band is full of stray packets.
- CRC-8 check
Every radio frame is checked, and a rejected frame gets an ERR frame with a real, valid checksum.
- Truncated frames fixed
The micro:bit's serial printf() buffer was too small: replaced with snprintf() and a send of the full ManagedString.
- Build system
The yotta project is split into firmware/gateway and firmware/object, selected by one field in module.json.
How it works
Protocol and security
- Readable protocol
KEY=VALUE fields separated by semicolons: easy to debug in a serial terminal, simple to parse on a micro:bit and in Python.
- Integrity, CRC-8
A CRC-8 (polynomial 0x07) on the body of every frame.
- Authentication
An AUTH field: a CRC-8 over the shared key, the object ID and the frame counter.
- Anti-replay
The server rejects any frame whose counter is not greater than the last valid one for that object.
Shared keys live in a keys.json file kept out of Git.
Data path
- 1
Object ↔ Gateway2.4 GHz
The object reads a BME280 sensor, shows the values on an OLED screen and sends a frame every 5 seconds.
- 2
Gateway ↔ ServerUSB serial
The gateway filters and checks each frame and relays it to the PC; configuration frames travel the other way.
- 3
Server ↔ FirestoreSDK
The Python server validates the CRC, the AUTH field and the counter, acknowledges the frame and stores the measurements in Firestore.
- 4
Firestore ↔ Android applive
The Android app (Kotlin, Jetpack Compose) listens to Firestore in real time. Confirming a new display order writes it to Firestore, the server turns it into a CONFIG frame, and the gateway relays it to the object.
VER=1;SRC=OBJ;DST=GTW;ID=01;TYPE=DATA;PAYLOAD=T:22,L:150,H:45,P:1013;TS=0042;AUTH=5F;CRC=BFProblems we solved
| Problem | Fix |
|---|---|
| Truncated serial frames: the serial printf() buffer was too small. | Replaced with snprintf() and a full-string send. |
| Endless BADCRC errors with no active object: stray radio packets reached the checks. | Length and marker filters. |
| ERR frames with an invalid checksum: the CRC was hardcoded to 00. | It is now computed for real. |
| No radio frame received at all: the default packet limit (38 bytes) was below our 75 to 85 byte frames. | Raised in config.json. |
| BADAUTH after every firmware update: the AUTH scheme had drifted between firmware and server. | The object must be reflashed after any change to it. |
Result
- Validated live
In a live demonstration: four measurements, authenticated radio transmission, cloud storage and remote reconfiguration of the display.
- Beyond the brief
A complete application protocol, a multi-threaded server with a Firestore listener, and an Android interface with charts, history and drag and drop.
What we'd improve next
- Persist the anti-replay counter across server restarts.
- Authenticate the CONFIG frames sent from the server to the object (they currently carry AUTH=00).
- Move from CRC-8 to CRC-16 for better robustness.
- Add end-to-end encryption on the radio link, which was out of scope for this academic project.
All the details
My part: the gateway firmware
- Two-way relay between the 2.4 GHz radio and the USB serial link to the PC: measurements go up to the server, configuration frames go down to the object.
- Frame filtering. The 2.4 GHz band is full of stray packets, so the gateway drops anything too short or missing the expected markers (VER=1 and ;CRC=).
- CRC-8 check on every radio frame, and ERR frames carrying a real, valid checksum when a frame is rejected.
- Fixed truncated serial frames: the micro:bit's serial printf() buffer was too small, so I replaced it with snprintf() and a send of the full ManagedString.
- Build system: I split the yotta project into firmware/gateway and firmware/object, selected by a single field in module.json, so nobody has to move files between builds.
Protocol and security
- A readable text protocol (KEY=VALUE fields separated by semicolons): easy to debug in a serial terminal and simple to parse on a micro:bit and in Python.
- Integrity: a CRC-8 (polynomial 0x07) on the body of every frame.
- Authentication: an AUTH field computed with a CRC-8 over the shared key, the object ID and the frame counter.
- Anti-replay: the server rejects any frame whose counter is not greater than the last valid one for that object.
- Shared keys live in a keys.json file kept out of Git.
Data path
- The object reads a BME280 sensor, shows the values on an OLED screen and sends a frame every 5 seconds.
- The Python server validates the CRC, the AUTH field and the counter, acknowledges the frame and stores the measurements in Firestore.
- The Android app (Kotlin, Jetpack Compose) listens to Firestore in real time. Confirming a new display order writes it to Firestore, the server turns it into a CONFIG frame, and the gateway relays it to the object.
Problems we solved
- Truncated serial frames: the serial printf() buffer was too small. Fixed with snprintf() and a full-string send.
- Endless BADCRC errors with no active object: stray radio packets reached the checks. Fixed with length and marker filters.
- ERR frames with an invalid checksum: the CRC was hardcoded to 00. It is now computed for real.
- No radio frame received at all: the default packet limit (38 bytes) was below our 75 to 85 byte frames. Raised in config.json.
- BADAUTH after every firmware update: the AUTH scheme had drifted between firmware and server. The object must be reflashed after any change to it.
Result
- Validated in a live demonstration: four measurements, authenticated radio transmission, cloud storage and remote reconfiguration of the display.
- We went beyond the brief with a complete application protocol, a multi-threaded server with a Firestore listener, and an Android interface with charts, history and drag and drop.
Contact
Email meLinkedInBased in Lyon, France
Last updated