IoT System for Environmental Monitoring at Nerdearla: architecture, hardware, and lessons learned

From the home lab to Nerdearla: how I built an IoT air-quality system that worked at an event with thousands of people — and a network of people that held me up (english version)

I'm very happy to finally share part of the work I've been doing over the last month for Nerdearla, together with the Nerdearla team — Eduardo Casarero, Emilio Nagy (Nachi), and Lole.

My intention in writing this is also to encourage more girls and women to dare to take on technical challenges like this one.

My main profession is DevOps. In my day-to-day work I mostly deal with infrastructure, automation, observability, and distributed systems.

IoT is something different, and I like it because it forces me to cross worlds that usually appear separate: software, infrastructure, electronics, hardware, and — above all — problems from the physical world.

Since I was a kid I've been interested in ecology and in the idea of understanding our environment better. Today I'm especially interested in thinking about how we can use technology to build a slightly better world: more accessible, more aware of its environment, and also kinder to people with sensory sensitivities and neurodivergences.

About a month ago I was invited to develop an IoT system to measure different environmental conditions during this year's Nerdearla, and then to share in a live interview how to build an air-quality measurement station simply and accessibly.

The truth is I was quite surprised when they wrote to me. I had been working with IoT as part of my Bachelor's thesis, but I didn't expect an opportunity like this to come up.

So I said yes.

I immediately turned to my colleagues from AWS Women in Cloud Buenos Aires (WiC), with whom I had been studying Infrastructure at the Universidad de la Marina Mercante. They encouraged me a lot and helped me find the courage to dive fully into the project.

As I started telling people what we were doing, I began to hear very personal reasons why this kind of measurement can matter. Someone told me their dad would love to know the humidity because it affects his joints. Another person told me they have asthma and would like to be able to access this information.

That made the project feel much more real.

From the home lab to the event

My interest in IoT started a long time ago. Last year I was working with a friend who is a primary school teacher to develop robotics ideas for her nieces. This interest sharpened this year with an IoT course with Agustín Candia at UNLP, which I took as an elective. I liked it so much that I started talking with Agustín and Laura Fava about doing my thesis with them at LINTI (Laboratorio de Investigación en Nuevas Tecnologías Informáticas de la UNLP).

Originally I was thinking about working with water flow meters for a municipality — a very concrete and current problem — when the Nerdearla possibility came up.

I asked Agustín and Laura whether the Nerdearla project could work as a pre-thesis. They told me that at LINTI they were also working on air-quality projects, and that opened a conversation about participating in the existing environmental measurement projects, and about how this could serve as previous experience.

What we have in common is an interest in how we can build environmental measurement systems that are accessible, open, and inexpensive for local communities and cooperatives, and that can eventually scale.

Some ideas also stayed outside this project. For example, I'm especially interested in the immutable signature of measurements, something I had explored in the IoT final project and that I'd like to keep researching.

Building for a real environment

Nerdearla presented challenges very different from the experiments I had done at home.

The scale of the place mattered. So did the fact that we had little time, limited hardware, and very little prior information about how the network and the physical environment would behave once tens of thousands of people were moving through the building.

In my experiments I had found that Wi-Fi transmission peaks when sending MQTT messages could occasionally cause brownouts on some ESP32 boards. A brownout is a momentary voltage drop: the radio asks for a current peak that the power supply can't deliver, and the board reboots.

Given the limited time available, I decided not to spend effort on solving battery operation, since from previous experience that also requires fine-tuning resource usage, for which there was no time. Instead, I focused on having reliable power supplies and on solving connectivity and storage problems with flash memory and other caching strategies.

We also had different ESP32 boards, including some cheaper ones with shorter-range Wi-Fi. All of them had 2.4 GHz Wi-Fi and Bluetooth. Even if we had had newer boards with 5 GHz Wi-Fi, which has other possibilities, that also requires more electricity. I wasn't convinced that investing in that would solve the connectivity problem at an event with so many people.

Agustín suggested that we first make sure we had robust power supplies and then work on the software solutions.

It was an important decision.

Infrastructure

The Nerdearla team wanted to have a centralized virtual host, moving away from the LAN setup for more autonomy during the prior tests.

We considered using a VM on their private server with access through a tunnel, but since the MQTT/Mosquitto scheme uses TCP directly and not HTTP, for this architecture a VM accessible from the Internet made more sense.

I proposed a small AWS (EC2) instance with a public IP, enough to run the node backend, the observability, and a small static website where attendees could check which rooms were cooler, had better air, or were quieter.

The backend is a simple Docker Compose. The data flow is a single line:

ESP32 nodes  --MQTT/TLS:443-->  Mosquitto  --Telegraf-->  InfluxDB  --Grafana-->  dashboards

A detail that shaped the whole design: on the venue's network the only reliable outbound port was 443. So I published the MQTT broker over TLS directly on 443, not over HTTP. This is raw encrypted MQTT, not WebSockets:

# mosquitto.conf
allow_anonymous false
password_file /mosquitto/config/passwd

# Internal listener (Docker network only, for Telegraf)
listener 1883 0.0.0.0

# Public listener: MQTT over TLS on 443 (the only port the venue lets out)
listener 443 0.0.0.0
certfile /mosquitto/config/certs/fullchain.pem
keyfile  /mosquitto/config/certs/privkey.pem

The certificate is from Let's Encrypt. In each node's firmware I embed the public root (ISRG Root X1) to validate the broker's chain, so the connection is encrypted end to end without depending on an intermediate proxy.

I chose Telegraf instead of Node-RED because the requirements were relatively simple and, for this case, a configuration-file-based approach fit better. Each node publishes a JSON to a topic of the form iot/<type>/<device>/telemetry; Telegraf consumes it, turns type and device into tags, and uses the payload's ts as the InfluxDB timestamp:

# telegraf.conf
[[inputs.mqtt_consumer]]
  servers   = ["tcp://mosquitto:1883"]
  topics    = ["iot/+/+/telemetry"]
  data_format    = "json"
  tag_keys       = ["type", "device"]
  json_time_key  = "ts"         
  json_time_format = "unix"
  name_override  = "telemetry"

[[outputs.influxdb]]
  urls     = ["http://influxdb:8086"]
  database = "iot"

Having the timestamp set by the node (and not the server) is key: a reading delayed by a Wi-Fi drop is inserted into InfluxDB at the moment it actually happened, not when it arrived. Below I explain how the firmware takes advantage of this.

Observability is Grafana, exposed only on localhost and published outward through a tunnel, never opening an extra port. The static site is served by nginx:alpine, and a small Python script queries InfluxDB every 10 seconds and writes a latest.json that the page reads same-origin.

I also had a small creative moment: I gave the site a poetic name with Andean influence, and designed a logo inspired by ideas of environmental awareness, and I worked on the page design with a friend who helped me to lighten the workload.

The backend is provisioned with Ansible. The playbook also includes basic hardening measures: SSH by key only, no root, and fail2ban.

# ansible/roles/hardening/tasks/main.yml
- name: Harden sshd_config (key only, no root)
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^#?\s*{{ item.key }}\s+'
    line:   '{{ item.key }} {{ item.value }}'
    validate: /usr/sbin/sshd -t -f %s
  loop:
    - { key: PasswordAuthentication, value: 'no' }
    - { key: PermitRootLogin,        value: 'no' }
  notify: restart sshd

- name: Install fail2ban
  ansible.builtin.apt: { name: fail2ban, state: present, update_cache: true }

Secrets live encrypted with SOPS and are injected into .env at provision time, never in the repo.

Firmware: a single base, several boards

Then came the physical part.

We had to get enclosures for the nodes. Some we bought from an online store and others were 3D printed by a volunteer.

We coordinated which components to buy based on the measurements the team wanted to obtain. We ended up using SCD40/SCD41 sensors (CO₂, temperature, and humidity) and INMP441 MEMS microphones (noise level via I2S).

Everything was shipped to my home and I started soldering and wiring.

The microphones were particularly hard to solder. I soldered the first one backwards so I could see the labels more easily.

It worked anyway.

After that I got the hang of it and was able to finish all the nodes on time and right side up, already knowing the labels.

I also had some quite frustrating moments with the cheaper boards. I added a board of my own and bought some ESP32-C3s to replace others. The problem is that each ESP32 variant (C3, S3, classic NodeMCU) has different pins and even different I2S peripherals. Instead of maintaining three firmwares, I maintain just one and select the board with a #define before flashing:

#define BOARD_S3       1
#define BOARD_NODEMCU  2
#define BOARD_C3       3

#define BOARD BOARD_C3   // <-- change per board

#if   BOARD == BOARD_S3
  #define SCD_SDA 8
  #define SCD_SCL 9
  #define I2S_BCK 5
  #define I2S_WS  4
  #define I2S_DIN 6
#elif BOARD == BOARD_NODEMCU
  #define SCD_SDA 21
  #define SCD_SCL 22
  #define I2S_BCK 26
  #define I2S_WS  25
  #define I2S_DIN 33
#elif BOARD == BOARD_C3
  #define SCD_SDA 4     // avoids the C3's strapping / flash / USB pins
  #define SCD_SCL 5
  #define I2S_BCK 6
  #define I2S_WS  7
  #define I2S_DIN 10
#else
  #error "Define BOARD = BOARD_S3 / BOARD_NODEMCU / BOARD_C3"
#endif

Details I didn't expect came up. The same INMP441 microphone read by each chip's I2S peripheral leaves the 24-bit sample in a different position within the 32-bit word, so the same bit shift gives a different level per board. That's why I added a MIC_DB_OFFSET to each variant, which I calibrated live against a sound-level-meter app on my phone.

In a future iteration I'd like to be able to count on a professional sound level meter at the same installation location.

Surviving brownouts: store-and-forward on the node

The real risk at the event was losing data when the Wi-Fi became intermittent or when a radio peak rebooted a board. I solved it on the node itself, with a store-and-forward pattern similar to a database snapshot: a queue in RAM that drains to MQTT, and every 5 minutes, if it changed, a dump to flash memory (LittleFS) to survive a reboot.

Two decisions make it work.

First, drain little by little, never in a burst. A Wi-Fi burst is exactly what re-triggers the brownout on a marginal power supply, so I send at most 2 messages every 500 ms:

// Drain little by little: a WiFi burst can re-trigger the brownout.
// At most 2 messages every 500ms = ~4/s.
void bufDrain() {
  static uint32_t lastDrain = 0;
  if (bufCount == 0 || !mqtt.connected() || millis() - lastDrain < 500) return;
  lastDrain = millis();
  for (int k = 0; k < 2 && bufCount > 0 && mqtt.connected(); k++) {
    char payload[320];
    buildPayload(payload, sizeof(payload), ring[bufHead]);
    if (!mqtt.publish(TOPIC, payload)) break;  // no ACK: retry later
    bufHead = (bufHead + 1) % BUF_CAP;
    bufCount--;
  }
}

Second, each reading travels with its own timestamp. A measurement that sat delayed in the queue arrives late, but it enters InfluxDB at the moment it actually happened, so the time series has no gaps or shifts. The queue holds up to ~12 hours of data in RAM; if it fills, it overwrites the oldest; and the monotonic seq makes it possible to detect whether any message was lost.

Meanwhile, the days kept going by. The system still wasn't finished.

I was able to test the distributed system with the help of some friends, which gave me the confidence I needed to take it to the event.

And finally the moment arrived

Out of responsibility to clients, I couldn't ask for the day off work to be there during the whole setup, so I was sending some instructions remotely.

At first, the nodes weren't sending anything.

The problem turned out to be an accented character in the Wi-Fi network name.

They changed it.

And, suddenly, all the nodes came online.

It was incredible.

When people started coming in, the Wi-Fi became intermittent for some nodes. But even so we were able to get enough metrics to compare the different spaces. This is where the buffer on the node did its job: what couldn't be sent at the moment was stored and came in later, at its correct time.

It was fascinating to watch the measurements change moment by moment as the building filled up. This allowed the Nerdearla staff to take measures to prevent overcrowding and refresh the rooms between sessions.

There were also a couple of anecdotes that stuck with me.

At one point I came into Nerdearla with some extra nodes and ended up talking with an attendee who is very fond of electronics and who works building escape rooms. He looked at the nodes and started identifying the components, even the board models. He even recognized the microphone I had soldered backwards, and we laughed for a while.

We went to install an extra node in the quiet coworking area and one of the event's main speakers — whom, to my embarrassment, I didn't recognize at the time — gave up his seat and his power strip so that we could install it.

He told me, “I think it's important to measure the CO₂, and I really need to get some sunlight.” Only when I went to see his panel did I realize how important he was.

I felt understood, though each of us for different reasons.

I was exhausted — I had been sleeping very little for days — and at first I didn't even have a clear notion of what was happening. But as feedback started to come in, by noon on Friday I finally felt that everything was working fairly smoothly.

I had also designed a node to measure electrical consumption, with an SCT-013 current clamp and an ADS1115 ADC. We were able to get it running during the second day, once we found a suitable place to install it, and although its placement in the space (with many columns and surrounded by people coming and going) caused several connectivity problems, we managed to get some measurements.

At one point I had to ask for a soldering iron to fix a component, and they guided me to a quiet space where I talked with some of the venue's workers who were fascinated by the process.

Seeing the whole system working in a place full of people was incredibly exciting.

What had started as small experiments in my home lab ended up becoming a distributed IoT system running in a real environment, with real constraints and people using the results.

And I learned a huge amount in the process.

Acknowledgments

And none of this would have been possible without the unconditional support of Javier Perales, who helped me with the website design, came to accompany me during the first two days, and afterwards went to my home to take care of my kitty, who had been left alone. Federico Meza was also there, an electrician friend who gave us a logistical hand with the electrical measurement and its installation.

Thanks to Eduardo, Nachi, and Lole from Nerdearla; to Agustín and Laura at LINTI; to the volunteers who helped with the hardware; to my friends who helped me test the system; and to all the people who encouraged me to say yes to this project.

I also want to thank my colleagues from AWS Women in Cloud, who were at Nerdearla as community partners. In the middle of the intense pace, having several of them — especially Silvina Belén Aponte Díaz — come up to find me to give me support was something that held me up a lot. I want to thank from the heart Guadalupe Tulín, who accompanied me on the logistics side, in addition to her judicious design and outreach support; and a special mention to Dai Rodríguez, Alejandra Leuno, and Laura Bolaños, who supported and stood by me during and after. Delia Alomo shared her experience with me and encouraged me to publish in the community.

And I especially want to thank my work team, who covered for me while I went to do the installation and so that I could be present during the setup.

Having that kind of support made a huge difference: it allowed me to take something I had been experimenting with as part of my studies and bring it into a real environment.

I hope more girls and women dare to take on challenges like these. I started with doubts and fears, but I ended with a system running at a real event. If I could, many more can.

I'm very grateful to all the people who made this possible.


This article is also published in Spanish: Sistema IoT para monitoreo ambiental en un evento de gran escala.

Also available on the Sysarmy blog: IoT, Nerdearla y monitoreo ambiental.