April 3, 2026
3m 37s

I live in Medellín, Colombia, a city where, when you look up, you can’t tell if those are clouds in the sky or smog. I wanted to know if the air I’m breathing every day is actually clean, or if I should be doing something about it.
My solution was to get a development board. I chose the nRF52840 over the ESP32. Although both boards support Bluetooth, AirNode—an open-source environmental IoT sensor I’ve been building—is designed to spend most of its life sleeping and only wake up to collect data and talk to a nearby phone. For that use case, the nRF52840 was a much better fit.
It took me a couple of weeks to adapt to this new system, mainly because coming from NXP and ESP32 development, the workflow felt unfamiliar at first. One of the biggest differences was the device tree that separates hardware description from application code. Eventually, I realized it solved a problem I always had: hardware configuration mixed with application logic.
For AirNode, I used the Device Tree to configure GPIOs, the I2C bus, and attached sensors. The BME680, for example, is defined as:
bme680: bme680@76 {
compatible = "bosch,bme680";
status = "okay";
reg = <0x76>;
};
This tells Zephyr that a BME680 sensor is connected at address 0x76 on the I2C bus and that it should use the appropriate driver. Hardware configuration stays in one place, making it much easier to move the application between boards.
For this project, I selected the BME680 to measure temperature, humidity, and pressure, and PMSA003I to measure particulate matter: PM1.0, PM2.5, PM10. I initially chose the PMS5003, but it transfers data via UART. The PMSA003I does the same job using I2C, so I saved two extra lines (in bulk, this is a real money decision).
AirNode spends most of its life doing absolutely nothing. Once the sensors and communication interfaces were working, the next step was to reduce power consumption.
The power management system deactivates peripherals and unnecessary clocks, just keeping the RTC active. The RTC is responsible for waking up the system at predefined intervals. When the device wakes up, it activates the sensors, reads the data, transmits it via Bluetooth, and goes back to sleep.
This approach is important because the device spends most of the time waiting rather than actively processing data. By remaining asleep between measurements, it significantly reduces consumption, leaving the device with an average current of 13.03 uA over a 5-minute interval.
While measuring current consumption, I noticed the numbers were higher than expected. The culprit wasn’t the sensors or the radio; it was the debug logs. Disabling them in production will have a real impact on battery consumption.
After validating that each sensor was working correctly, the next challenge was making the data available via Bluetooth.
I could have used WiFi or LoRa. WiFi would have simplified cloud connectivity, but it would have required infrastructure. LoRa offers long-range, but it also requires a gateway nearby. AirNode is designed to stay close to a smartphone, so BLE was the obvious choice.
AirNode periodically advertises its presence, allowing nearby phones to discover it and connect. Once connected, the sensor data is exposed through the Environmental Sensing Service (ESS), a standard BLE service designed for environmental measurements.
Each measurement is a separate characteristic. The app can read values or subscribe to updates as new data comes in.
The BME680 has driver support in Zephyr. The PMSA003I doesn't. I had two options: switch sensors or implement the missing software layer myself.
I decided to write a simple driver that initializes the sensor, parses particulate matter measurements, and validates each frame using the device's checksums.
The checksum helps validate the integrity of the transmitted data. Each frame includes a checksum calculated by the sensor itself. If the value doesn’t match the checksum calculated in the firmware, the data is discarded as it may be corrupted.
For a long time, I thought that the firmware was the product. If the sensors were reading correctly and the data looked good on a terminal, the job was done.
Working with AirNode changed that perspectiveThe sensors were reading, BLE was transmitting, and the data was flowing, but nobody could see it. That’s when it hit me.
This project reinforced something I had started to learn: firmware is only one piece of the puzzle. If you don’t have a layer to visualize data and communicate to a user, even the most sophisticated embedded system is just a microcontroller displaying measurements every time it boots.
Perhaps the biggest lesson is that users don’t care about drivers, protocols, or power management systems. They care about whether it solves a problem they actually have.
AirNode is evolving, but at the very least, it has already answered the question I started with: Is the air in Medellín actually clean?
Now I know, and honestly, I’m deciding what to do about it.