IoT Alarm with Rust, MQTT, ESPHome and Telegram
A complete IoT alarm system integrating hardware and software using modern open-source technologies.
Read more: IoT Alarm with Rust, MQTT, ESPHome and TelegramIn the Buenos Aires metropolitan area, 40.6 % of the drinking water that gets delivered is never accounted for. Not all of it leaks away — the point is that nobody knows where it goes. And only 19 % of dwellings have a connected meter, so most users are billed a flat rate computed from the property's characteristics rather than from what they actually consume. Without metering there is no way to spot a leak inside a home, no way to manage demand, and no way to tell how much of that 40 % is network loss and how much is waste.
That is the problem we took on for our capstone project in Electronics Engineering at FIUBA: design, build and validate a complete water telemetering system, from the device that goes inside the meter box to the server that receives the data. I defend it on 7 September, and I wanted to write down what we built and, more to the point, what broke along the way.
This was a two-person project. I took the central server and the circuit board; Gonzalo Puy took the firmware, which I also worked on. We wrote the report together.
The system has four pieces. A battery-powered IoT device that sits inside the household meter box and connects to the flowmeter's pulse emitter, where each pulse stands for a fixed number of litres. A firmware that counts those pulses and wakes once a day to report. A custom PCB carrying the microcontroller, the cellular module and its antenna. And a central server, the head-end system, that receives the readings, persists them and makes them available to the utility.
The constraint that shapes every other decision is that the meter box has no mains power. The device runs on a battery and has to last more than eight years untouched, because sending a crew to swap cells house by house wrecks the economics of the whole thing. Everything else — the choice of microcontroller, of modem, the design of the protocol — follows from that.
The device spends nearly its whole life asleep. An STM32L031K6, a Cortex-M0+ with 32 KB of flash, stays in Stop mode counting pulses through an interrupt, and the Quectel BG95-M3 cellular module is fully powered down. Once a day the real-time clock wakes it, the modem comes up, attaches to the network, sends the report, and everything goes back to sleep.
We characterised the consumption with a Nordic Power Profiler Kit II, measuring the microcontroller and modem domains separately. The numbers for a full cycle:
Against a 17 Ah lithium thionyl chloride cell, 0.3 Ah a year clears the eight-year target with a wide margin. The interesting result is what becomes the limiting factor: no longer the device's consumption but the battery's own self-discharge. Once a design reaches that point, squeezing the firmware further stops buying you anything.
Standard smart metering systems speak DLMS/COSEM, the IEC 62056 family. The full stack is large: on a device with 32 KB of flash and 8 KB of RAM it simply did not fit, and forcing it would have eaten the memory budget the rest of the system needed.
The decision was to keep the standard's semantics without its stack: COSEM objects and
OBIS addressing, carried over a minimal binary protocol of our own. One of our devices will not
interoperate with a commercial DLMS concentrator, but the data it produces and the way you ask for it are
conceptually the same, so moving to the full stack later is a change of transport rather than a redesign.
A periodic session is seven messages: HANDSHAKE, READ, WRITE and a
closing ACK, with their responses.
The head-end system is written in Rust. The fair question is why a thesis prototype needs clustering at all, and the answer is in the battery paragraph above: if the server goes down or saturates, devices never complete their session, sit waiting, and retry. Every retry turns the radio back on. A server availability problem is paid for in battery across the entire fleet — and that battery cannot be topped up without sending a crew.
Nodes discover each other through gossip and watch each other with SWIM failure detection; when a node drops, its devices are delegated to another. The scheduler spreads the fleet across 48 hourly buckets and assigns each new device to the least loaded one, so sessions spread through the day instead of piling up. Persistence is PostgreSQL, and the whole system publishes metrics to Prometheus with Grafana dashboards on top.
To validate it we ran three nodes — one on the development server and two on a local workstation, joined over a private network — against a single database, and threw 80 simulated devices at them in three waves. In test mode the scheduler books sessions five minutes out instead of at the daily bucket hour, so you can watch many complete cycles in a short window.
This is the part of the project I got the most out of, and the part that shows up in no architecture diagram.
The protocol was designed as a pull: the server opens the connection towards the device during its wake window. It is the right model, because it leaves the server in charge of when each meter talks. It assumes exactly one thing — that the device is reachable at a stable address.
With a commercial SIM on a public APN, it is not. The carrier hands out dynamic IPv6 addresses that change between activations, so the address a device announces at registration is already stale by its next window. And the traffic crosses a carrier-grade NAT: a connection opened from the server finds no translation entry and is dropped before it ever reaches the device. The design was sound; the network just would not have it.
The way out was to temporarily invert who starts the session. The device already keeps an
outbound socket open towards the server, and that socket does cross the CGNAT, leaving a pinhole the server
can answer through. We added one message, SESSION_START_REQUEST, whose only job is to tell the
server to fire the session over that socket. The rest of the protocol and the firmware's entire state
machine are reused untouched.
What mattered most to me was that it left no debt behind. All of that logic is compile-time conditional, behind a flag in the firmware and a feature in the server: build without them and the original model comes back, intact. The production fix is not a software one but a commercial one — a private APN with a fixed IP per device and a private interconnection with the carrier. That is the lesson: in an IoT system, network provisioning is an architectural decision, and you negotiate it at the start of the project rather than once the prototype is already running.
The STM32L031K6 was picked for its consumption, and for the meter's normal operation the choice was a good one: application logic, modem state machines and the protocol all fit comfortably. The limit showed up with two features the protocol already had a place for. Over-the-air updates need two firmware images resident at once plus a bootloader to validate and switch them, which does not fit in a single 32 KB bank. Message authentication competes for that same memory, on a core with no crypto acceleration.
There was no elegant solution here — there was an accepted, documented trade-off. Both features were left
pending, with their slot reserved in the protocol (the EXECUTE operation and the MAC field)
and a concrete path forward: a second hardware revision on an STM32L4, which the measured autonomy margin
can absorb without putting the eight years at risk. The expensive lesson:
size a microcontroller's memory for the functionality of the product's whole life, not for the
first version's.
The first implementation of the message receiver was sequential: take one datagram, process it end to end — validation, database write, response — and only then go back to listening. It was fine for functional testing and plainly inadequate for a burst: a mass rollout, or every device retrying together after an outage.
While the loop is busy writing to PostgreSQL, incoming UDP datagrams pile up in the socket buffer until it overflows, and the operating system drops them with no notification. Every lost message is a device that exhausts its wait window, retries, and powers the radio up again. That is where it gets interesting: in a battery-powered system, a concurrency decision on the server translates into years of service life in the field.
The receiver was redesigned as a concurrent dispatcher on Tokio: the main loop only receives and decodes, handing each message to an independent task. A semaphore with a configurable cap provides the backpressure that keeps a burst from exhausting memory or database connections. Validated against waves of concurrent registrations, with no messages lost.
I designed the PCB in KiCad: microcontroller, cellular module with its impedance matching and antenna, power from the battery with the reservoir supercapacitor that absorbs the 411 mA transmission peaks, and the ground planes. It has to fit inside the household box, be sealed against water, and meet electromagnetic compatibility requirements.
The full report covers all of this at a depth a post cannot: the comparison of LPWAN technologies, the theoretical energy budget, the protocol designed message by message, the board design, and the economic feasibility analysis. It is written in Spanish.
Download the reportPDF, about 14 MB
What I take away is not any one of the pieces but having had to make them work together: low-power electronics, radio frequency, embedded firmware, protocol design, distributed systems and economic assessment. They are disciplines a degree exercises separately, and this project let you skip none of them. The three problems above all came from the same root — a decision taken in one of those layers surfacing, many months later, as a symptom in another.
The defence is on 7 September 2026.
A complete IoT alarm system integrating hardware and software using modern open-source technologies.
Read more: IoT Alarm with Rust, MQTT, ESPHome and Telegram