跳到正文
Hacker News· intimaria·· 9 天前AI 评分17

Nerdearla 环境监测 IoT 系统:用 ESP32 搭建空气质量测量站

Sistema IoT para monitoreo ambiental en Nerdearla

AI 导读

DevOps 工程师 Inti María Tidball 为 Nerdearla 大会开发了一套基于 ESP32 的 IoT 环境监测系统,用于测量会场空气质量等环境条件。

正文 · AI 翻译

2026年10月6日 — 约14分钟阅读

作者 Inti María Tidball。GitHub,LinkedIn。

我非常高兴终于能分享我上个月为 Nerdearla 所做的一部分工作,与 Nerdearla 团队——Eduardo Casarero、Emilio Nagy(Nachi)和 Lole 一起。

我写这篇文章的初衷也是希望这段经历能鼓励更多女孩和女性勇于迎接这样的技术挑战。

我的主业是 DevOps。日常工作中我主要围绕基础设施、自动化、可观测性和分布式系统。

IoT 则有所不同,我喜欢它,因为它迫使我跨越那些通常看似分离的领域:软件、基础设施、电子、硬件,尤其是现实世界中的问题。

我从小就对生态学以及更好地理解我们周围环境这一想法感兴趣。如今我特别感兴趣的是思考如何利用技术来构建一个稍微更好的世界:更无障碍、更了解自身环境,也对有感官敏感和神经多样性的人更友善。

大约一个月前,我被邀请开发一个 IoT 系统,用于在今年 Nerdearla 期间测量各种环境条件,随后又受邀在一场直播采访中分享如何简单且低成本地搭建一个空气质量测量站。

说实话,当他们联系我时我非常惊讶。我一直在把 IoT 作为我学士学位论文的一部分来研究,但没想到会出现这样的机会。

于是我说了 yes。

我立刻求助于我在 AWS Women in Cloud(WiC)的伙伴们,我曾和她们一起在国立商船大学学习基础设施。她们给了我极大的鼓励,帮助我找到勇气全身心投入这个项目。

当我开始向人们讲述我们正在做的事情时,我开始听到这类测量之所以重要的非常个人的原因。有人告诉我,他爸爸会很想了解湿度,因为湿度会影响他的关节。另一个人告诉我,她有哮喘,希望能获取这些信息。

这让这个项目感觉真实了许多。

从家庭实验室到活动现场

我对 IoT 的兴趣由来已久。去年我一直在和一位当小学老师的朋友合作,为她的侄女们开发机器人相关的想法。今年这种兴趣因我在 UNLP 选修的 Agustín Candia 的 IoT 课程而变得更加强烈。我非常喜欢这门课,于是开始与 Agustín 和 Laura Fava 商量,在他们所在的 LINTI(UNLP 新信息技术研究所)做我的论文。

原本我在考虑为某个市政做水流量计——一个非常具体且现实的问题——这时 Nerdearla 的机会出现了。

我问 Agustín 和 Laura,Nerdearla 的项目能否作为预论文。他们告诉我,LINTI 也在做空气质量相关的项目,这开启了一场关于参与现有环境测量项目的对话,而这可以作为前期经验。

我们共同关心的是,如何为本地社区和合作社构建可及、开放且经济的环测系统,并且最终能够规模化。

这个项目之外还有一些想法。例如,我特别感兴趣的是测量的不可篡改签名,这是我在IoT期末作业中探索过的,我很想继续研究。

为真实环境而构建

Nerdearla提出的挑战与我在家里做的实验截然不同。

场地规模很重要。同样重要的是,我们时间很少、硬件有限,而且对于数万人在这栋楼里流动后网络和物理环境会如何表现,我们几乎没有先验信息。

在我的实验中,我发现发送MQTT消息时Wi-Fi的传输峰值偶尔会导致某些ESP32开发板出现brownout(电压暂降)。Brownout是电压的瞬时跌落:射频模块需要电流尖峰,而电源无法提供,于是开发板重启。

鉴于时间有限,我决定不投入精力去解决电池供电的问题,因为根据以往经验,这还需要对资源使用进行精细配置,而当时没有时间。相反,我专注于拥有可靠的电源,并用闪存和其他缓存策略来解决连接和存储问题。

我们还有不同的ESP32开发板,包括一些更便宜的、Wi-Fi范围更小的。它们都支持2.4 GHz Wi-Fi和蓝牙。即使我们有支持5 GHz Wi-Fi的更新开发板——这有其他可能性——也需要更多电力。我不确定在这方面投入是否能解决一个如此多人参与的活动中的连接问题。

Agustín建议我们先确保有稳健的电源,然后再处理软件解决方案。

这是一个重要的决定。

基础设施

Diagrama de flujo de la arquitectura de código
代码架构流程图

Nerdearla团队希望有一个集中式的虚拟主机,脱离局域网设置,以便在前期测试中获得更大的自主性。

我们考虑过在他们私有服务器上使用虚拟机并通过隧道访问,但由于MQTT/Mosquitto方案直接使用TCP而非HTTP,对于这种架构,从互联网可访问的虚拟机更有意义。

我提议使用一个带公网IP的小型AWS (EC2)实例,足以运行节点后端、可观测性以及一个小型静态网站,让参会者可以查询哪些房间更凉爽、空气更好或更安静。

后端是一个简单的Docker Compose。数据流只有一条线:

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

一个制约了整个设计的细节:在场地网络中,唯一可靠的出站端口是443。因此我把MQTT broker直接通过TLS发布在443端口上,而不是通过HTTP。这是加密的原始MQTT,不是WebSockets:

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

# Listener interno (solo red de Docker, para Telegraf)
listener 1883 0.0.0.0

# Listener público: MQTT over TLS en el 443 (el único puerto que el venue deja salir)
listener 443 0.0.0.0
certfile /mosquitto/config/certs/fullchain.pem
keyfile  /mosquitto/config/certs/privkey.pem

证书来自Let’s Encrypt。在每个节点的固件中,我嵌入了公共根证书(ISRG Root X1)来验证broker的证书链,这样连接就是端到端加密的,不依赖中间代理。

我选择 Telegraf 而不是 Node-RED,因为需求相对简单,而且在这种情况下,基于配置文件的方式更合适。每个节点向一个 topic 发布 JSON,格式为 iot/<type>/<device>/telemetry;Telegraf 消费它,把 type 和 device 转换为 tags,并使用 payload 中的 ts 作为 InfluxDB 的时间戳:

# 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"

时间戳由节点(而不是服务器)来提供是关键:因 Wi-Fi 中断而延迟的读数会以它实际发生的时间插入 InfluxDB,而不是它到达的时间。下面我会解释固件如何利用这一点。

可观测性用的是 Grafana,仅在 localhost 上暴露,并通过隧道对外发布,绝不额外开放端口。静态站点由一个 nginx:alpine 提供服务,一个小型 Python 脚本每 10 秒查询一次 InfluxDB,并写入一个 latest.json,页面以 same-origin 方式读取它。

我也有过一个小小的创意时刻:我给站点起了一个带有安第斯影响的诗意名字,设计了一个受环保意识理念启发的 logo,并和一位朋友一起打磨页面设计,他帮我分担了工作量。

后端用 Ansible 进行配置。playbook 还包含基本的 加固 措施:SSH 仅用密钥、禁止 root,以及 fail2ban。

# ansible/roles/hardening/tasks/main.yml
- name: Endurecer sshd_config (solo key, sin 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: Instalar fail2ban
  ansible.builtin.apt: { name: fail2ban, state: present, update_cache: true }

密钥用 SOPS 加密保存,并在配置时注入到 .env,绝不进入仓库。

固件:一个基础,多种板子

接下来是硬件部分。

我们得给节点弄到外壳。有些是在网店买的,另一些是由一位志愿者用 3D 打印的。

我们根据团队想要获得的测量数据来协调购买哪些组件。最终使用了 SCD40/SCD41 传感器(CO₂、温度和湿度)以及 INMP441 MEMS 麦克风(通过 I2S 测量噪声水平)。

他们把东西都寄到了我家,我开始焊接和接线。

麦克风特别难焊。第一个我焊反了,这样能更容易看清标签。

它照样能用。

后来我上手了,就能按时把所有节点都焊完,而且因为已经熟悉了标签,都是正着焊的。

我在一些更便宜的板子上也遇到过相当令人沮丧的时刻。我加了一块自己的板子,并买了一些 ESP32-C3 来替换其他的。问题是每种 ESP32 变体(C3、S3、经典 NodeMCU)的引脚不同,甚至 I2S 外设也不同。与其维护三个固件,我只维护 一个,并在烧录前用 #define 选择板子:

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

#define BOARD BOARD_C3   // <-- se cambia por placa

#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     // evita strapping / flash / USB del C3
  #define SCD_SCL 5
  #define I2S_BCK 6
  #define I2S_WS  7
  #define I2S_DIN 10
#else
  #error "Definí BOARD = BOARD_S3 / BOARD_NODEMCU / BOARD_C3"
#endif

出现了一些我没预料到的细节。同一个 INMP441 麦克风,由每个芯片的 I2S 外设读取时,24 位采样在 32 位字中的位置不同,所以同样的位移在不同板子上会得到不同的电平。因此我给每个变体加了一个 MIC_DB_OFFSET,并对照手机上的声级计应用进行了实时校准。

在下一轮迭代中,我希望能在同一安装位置用上专业声级计。

在电压跌落中存活:节点上的存储转发

该活动的真正风险在于当 Wi-Fi 变得不稳定或无线电尖峰导致某块板子重启时丢失数据。我在节点本身解决了这个问题,采用了一种类似数据库快照的 存储转发 模式:一个在 RAM 中的队列,排空到 MQTT,并且每 5 分钟,如果发生变化,就转储到闪存(LittleFS)中,以便在重启后存活。

有两个决策让它得以运作。

首先,一点一点地排空,绝不突发。Wi-Fi 突发正是会在边缘电源上再次触发 brownout 的原因,所以我每 500 毫秒最多发送 2 条消息:

// Drena de a poco: una ráfaga de WiFi puede re-disparar el brownout.
// Hasta 2 mensajes cada 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;  // sin ACK: reintenta después
    bufHead = (bufHead + 1) % BUF_CAP;
    bufCount--;
  }
}

其次,每个读数都带有自己的时间戳。一个在队列中滞留的测量值会迟到,但它会以实际发生的时间进入 InfluxDB,因此时间序列没有空洞或偏移。队列在 RAM 中最多保存约 12 小时的数据;如果满了,就覆盖最旧的;而单调的 seq 可以检测是否丢失了任何消息。

与此同时,日子一天天过去。系统还没有完成。

在一些朋友的帮助下,我得以测试这个分布式系统,这给了我足够的信心把它带到活动中。

终于,时刻到来了

出于对客户的责任,我不能请假一整天全程参与 setup,所以我远程发送了一些指令。

一开始,节点什么也没发送。

问题原来是 Wi-Fi 网络名称中的一个重音字符。

他们改了它。

然后,突然间,所有节点都上线了。

太不可思议了。

El sitio en vivo, consultado desde el navegador
从浏览器访问的实时站点
El sitio estático sirviendo en un display del evento
在活动显示屏上提供服务的静态站点
Mediciones en Grafana para personal (backroom del evento)
为工作人员准备的 Grafana 测量数据(活动后台)

当人们开始进入时,Wi-Fi 对某些节点开始变得不稳定。但即便如此,我们仍然获得了足够的指标来比较不同的空间。这就是节点中的缓冲区发挥作用的地方:当时无法发送的内容被保存下来,之后在正确的时间进入。

看着测量数据随着建筑逐渐填满而时刻变化,真是令人着迷。这使得 Nerdearla 的工作人员能够采取措施防止过度拥挤,并在会议间隙为房间通风。

还有几个小故事让我记忆犹新。

有一次,我带着一些额外的节点进入 Nerdearla,最终和一位非常喜欢电子学的参会者聊了起来,他从事电子工作,制作密室逃脱。他看了看节点,开始辨认组件,甚至包括板子的型号。他甚至认出了我焊反的麦克风,我们笑了一阵。

我们去安静的共享办公区安装一个额外的节点,活动的一位主要演讲者——令我羞愧的是,我当时没有认出他——把他的位置和插线板让给了我们,以便我们安装它。

他对我说:“I think it’s important to measure the CO₂, and I really need to get some sunlight.”。直到我去看他的演讲,我才意识到他的重要性。

我感到被理解,尽管各自的原因不同。

我精疲力竭——已经好几天睡眠严重不足——一开始甚至不清楚发生了什么。但随着反馈开始到来,到周五中午,我终于觉得一切都在相当流畅地运转。

我还设计了一个用于测量电力消耗的节点,配有 SCT-013 电流钳和 ADS1115 ADC。第二天,当我们找到合适的安装位置后,就让它运行起来了。尽管它在这个地方(柱子很多,周围人来人往)的放置产生了不少连接问题,我们还是获得了一些测量数据。

有一次,我不得不借一把电烙铁来修理一个元件,他们把我带到一个安静的空间,在那里我和一些对整个过程着迷的工作人员聊了聊。

Soldando el nodo de medición de corriente
焊接电流测量节点

在一个挤满人的地方看到整个系统运行,真是令人无比激动。

最初只是我在家庭实验室里做的小实验,最终变成了一个在真实环境中运行的分布式 IoT 系统,有着真实的限制条件,还有人在使用结果。

而且我在这个过程中学到了非常多。

致谢

如果没有 Javier Perales 的无条件支持,这一切都不可能实现。他帮我设计了网页,头两天来陪我,之后还去我家照顾我那只独自留下的猫咪。还有 Federico Meza,一位电工朋友,他在电力测量及其安装方面给了我们后勤上的帮助。

感谢 Nerdearla 的 Eduardo、Nachi 和 Lole;感谢 LINTI 的 Agustín 和 Laura;感谢帮助处理硬件的志愿者们;感谢帮我测试系统的朋友们;以及所有鼓励我对这个项目说“是”的人。

我还要感谢我在 AWS Women in Cloud 的伙伴们,她们作为 community partners 参加了 Nerdearla。在紧张的节奏中,她们中的几位——尤其是 Silvina Belén Aponte Díaz——主动过来找我、给我支持,这给了我很大的支撑。我衷心感谢 Guadalupe Tulín,她在后勤方面陪伴我,还提供了有见地的设计和推广支持;特别要提到 Dai Rodríguez、Alejandra Leuno 和 Laura Bolaños,她们在过程中和之后都支持、鼓励了我。Delia Alomo 和我分享了她的经验,并鼓励我在社区里发表。

我还要特别感谢我的工作团队,他们在我去安装期间以及为了让我能全程参与搭建而给予了我支持。

拥有这样的支持带来了巨大的不同:它让我能够把原本作为学业一部分在试验的东西带到真实环境中。

希望更多女孩和女性敢于迎接这些挑战。我一开始也有疑虑和恐惧,但最终在一个真实活动中让一个系统运行了起来。如果我能做到,更多人也能做到。

我非常感谢所有让这一切成为可能的人。


本文也发表在 AWS Builder Center:大型活动中的环境监测 IoT 系统。

来源:Hacker News · sysarmy.com