Skip to content

About

Firmware MicroPython del dispositivo de un cultivo real de SmartPot (ESP32 o Wokwi) con MQTT sobre TLS

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

SmartPot-IoT-Python

Language / Idioma: English | Español


English

Project Status

Firmware CI CodeQL Advanced

Description

SmartPot-IoT-Python is the MicroPython firmware companion for rapid prototyping and simulation: MicroPython 1.22.0 on an ESP32 (1.23+ is supported too, but newer images restarted in Wokwi), simulated in Wokwi or flashed onto a physical board. For production physical deployments, see the canonical SmartPot-IoT Rust no_std firmware. It reads sensors, displays values on an LCD screen, publishes telemetry over MQTT over TLS, and executes commands arriving from SmartPot while acknowledging each one. If the network drops, it continues measuring, displaying, and turning off active actuators on schedule.

With a physical board or Wokwi, the crop is real to SmartPot: its readings come from this firmware and train continuous learning. No hardware or Wokwi open? Create a virtual crop in the PWA—it is simulated by SmartPot-DataGenerator using the same contract and cannot be converted into a real crop later.

flowchart LR
  sensors["DHT22 · light · pH<br/>TDS · substrate"] --> esp["ESP32<br/>MicroPython"]
  esp --> lcd["20x4 LCD<br/>readings & connection"]
  esp -->|"telemetry · status · ack"| broker["mqtt.smartpot.app:8883<br/>TLS"]
  broker -->|"commands"| esp
  esp --> act["Pump · UV light · Fan"]
Loading

Circuit

Component ESP32 Pin Reported Scale
DHT22 (air temperature and humidity) GPIO 15 °C and %
Light sensor (potentiometer in Wokwi) GPIO 34 0–2000 lux
pH sensor GPIO 35 0–14
TDS sensor GPIO 32 0–3000 ppm
Substrate moisture GPIO 33 0–100 %
Water pump (blue LED) GPIO 19 WATER_PUMP
UV light (purple LED) GPIO 18 UV_LIGHT
Fan (orange LED) GPIO 5 FAN
20x4 I2C LCD SCL 16 · SDA 17 —

Project Structure

SmartPot-IoT/
├── fs/                      # ESP32 filesystem
│   ├── main.py              # Boot: welcome screen, WiFi, NTP clock, and device loop
│   ├── device.py            # Loop: measures and turns off on schedule; publishes and obeys when online
│   ├── smartpot_client.py   # MQTT v1 contract and TLS verified with the CA in config.json
│   ├── sensors.py           # Analog sensors and DHT22
│   ├── actuators.py         # Actuators with timed automatic shutoff
│   ├── display.py           # LCD display: readings, actuators, and connection state
│   ├── utils.py             # NTP clock sync and console table with reading timestamp
│   ├── settings.py          # Loads config.json (shared format with the Rust firmware)
│   ├── config.example.json  # Template for config.json (untracked) with embedded broker CA
│   └── i2c_lcd.py, lcd_api.py
├── tests/                   # CPython tests with simulated MicroPython modules
├── diagram.json             # Wokwi circuit with MicroPython 1.22.0
├── wokwi.toml               # Local simulation: build/flasher_args.json (MicroPython 1.22.0 + fs/)
├── tools/build_flash.py     # Builds build/ with fs/ and config.json in MicroPython's LittleFS
├── start.py                 # Alternative: mounts fs/ via mpremote without rebuilding the image
└── pyproject.toml / uv.lock # Development tooling (littlefs-python, mpremote, pytest, ruff)

MQTT Contract

The device connects to mqtt.smartpot.app:8883 over TLS (1.2 or newer), verifies the broker certificate with SmartPot's public CA, and authenticates with username = crop ID and password = device key. The client ID is smartpot-<cropId> (the broker rejects empty IDs).

Topic Direction Example
smartpot/v1/{cropId}/telemetry Publishes every 30 s {"temperature":23.5,"humidity":61,"brightness":820,"ph":6.12,"tds":790,"soilMoisture":64.2}
smartpot/v1/{cropId}/commands Subscribes (QoS 1) {"id":"…","actuator":"WATER_PUMP","action":"ACTIVATE","durationSeconds":15}
smartpot/v1/{cropId}/commands/ack Publishes (QoS 1) {"id":"…","status":"EXECUTED","message":"Water pump turned on for 15 s"}
smartpot/v1/{cropId}/status Retained & Last Will online / offline
smartpot/v1/{cropId}/capabilities Publishes after online (retained, QoS 1) {"schema":1,"device":{"firmware":"smartpot-iot-python/2.0.0","hardware":"esp32-devkit-c-v4","telemetry":{"formats":["json-v1"]}},"sensors":[…],"actuators":["WATER_PUMP","UV_LIGHT","FAN"]}

With durationSeconds, the actuator turns off automatically once the timer expires; without it, it stays on until receiving DEACTIVATE.

The device declares its six sensors and three actuators in .../capabilities after status=online and before subscribing to commands; the message is retained, uses schema 1, does not declare atmosphere or compact-v2, and stays under 2 KB. brightness and tds are published as JSON integers, never as 820.0 or 790.0. Command validation is strict: id must be a non-empty string (null, booleans, numbers, lists and objects are rejected), actuator and action must be non-empty strings, and durationSeconds must be a non-negative integer or absent. Commands are deduplicated by id over the last 16, so a QoS 1 redelivery repeats the previous ACK without toggling the hardware again.

Broker CA in config.json

Wokwi does not support .crt files, so the public CA is stored as PEM text inside config.json (BROKER.CA_CRT), the only device-specific file. The firmware:

  • converts it to DER, which mbedtls reads across all MicroPython builds;
  • passes it to umqtt.simple as expected by each version: ssl_params with cadata in MicroPython 1.22 and an SSLContext in 1.23+ (umqtt.simple in 1.22 ignores SSLContext and encrypts without verifying);
  • verifies the broker certificate and hostname after synchronizing the clock via NTP, because the ESP32 validates certificate expiration dates;
  • refuses to connect without a valid CA: it never sends the device key to an unverified broker.

Offline Operation

The loop runs every half second with or without network: it turns off timed actuators on schedule, reads sensors every interval_seconds, and shows them on the LCD and in the console with the reading timestamp. When connected to the broker, it also publishes telemetry and handles commands; every new broker session starts with a fresh reading. If the connection drops, it retries the broker and WiFi with exponential backoff and jitter (2 s to 60 s). The LCD redraws at once when an actuator or the broker link changes; its top-right corner shows an antenna when online and an X when offline.

Safety and Reliability

  • Actuator safety policy (shared with the Rust firmware and the contract fixture): durationSeconds must be an integer between 1 and 3600, otherwise the command gets a FAILED ACK with the reason. The water pump never runs without a limit: ACTIVATE without a duration uses 3600 s, and it switches off after 300 s without a broker session. The UV light and the fan keep indefinite activation. active_low: True supports relay boards that switch on with a low input; GPIO5 (fan) is a strapping pin that toggles at boot, so add a pull-down on the relay input.
  • Watchdog: machine.WDT (watchdog_seconds, default 30) is fed from the safety timer only while the loop keeps beating. If the loop stalls for 120 s, the outputs are switched off, SMARTPOT-FATAL watchdog task=loop stalled is printed and the board restarts; if the interpreter hangs, the hardware watchdog restarts it. 0 disables both while debugging from the REPL. Outputs are switched off first at boot and on any fatal error, which restarts the board.
  • Never blocked by the network: ACKs are published after check_msg returns, the socket times out after 15 s, the device pings the broker every 30 s (a ping left unanswered closes the session at the next one), and any client error counts as a lost connection. A hardware timer turns timed actuators off even while the loop waits on the network, and timers use a monotonic clock so NTP never stretches them.
  • Resilient LCD: telemetry is published before drawing; a missing or failing LCD prints LCD not available at 0x27 once, is replaced by a no-op and probed again every 10 s, and only changed rows are redrawn.
  • Sensors: every sensor is sampled every 2 s (the DHT22 minimum) and a value stays valid for three periods, so one failed DHT22 read does not blank the table. Each analog channel averages 16 raw 12-bit reads and scales them as raw / 4095, exactly like the Rust firmware (read_u16 approximates that scale and shifted the second decimal).
  • Same output as the Rust firmware: tests/fixtures/console_v1.json (identical in SmartPot-IoT) pins the console messages, the reading table, the LCD rows, the analog scale and the command rejection reasons for both firmwares.
  • Memory: gc.collect() runs before every TLS connection, SMARTPOT-HEAP reports the free heap (heap_min_free prints SMARTPOT-HEAP-LOW below a threshold), and repeated out-of-memory connection failures restart the board.
  • Plain-text markers for automated tests: SMARTPOT-READY, WIFI-UP, MQTT-UP, MQTT-DOWN, ACT, ACK, LCD, HEAP and FATAL.

Installation Guide

1. Create the crop in SmartPot

In smartpot.app, create a real crop and choose its vessel type (pot, NFT pipes, vertical tower, or floating raft). The application displays the device key only once along with the connection guide (physical ESP32 or Wokwi) and the ready-to-use config.json containing the WiFi network, the crop ID and the broker CA; if you lose the key, generate a new one from the Device tab.

2. Configure the firmware

cp fs/config.example.json fs/config.json

Fill in WIFI, crop_id, and device_key (the guide values) in config.json; the BROKER block already contains the broker CA. Both firmwares read the same config.json format, so you can paste the guide's block as is. config.json is in .gitignore: never commit the device key to the repository or leave it visible in a public Wokwi project.

3a. Browser Simulation

Open the Wokwi project, replace its files with those in fs/ (including your config.json with the CA) and diagram.json (which uses MicroPython 1.22.0, the same image as wokwi.toml), and run. The Wokwi-GUEST network has Internet access. The firmware also runs and verifies the broker on 1.23 or newer, but those images kept restarting in Wokwi.

3b. Local Simulation (Wokwi in the IDE or wokwi-cli)

uv sync
cp config.example.json fs/config.json   # once; or the block from the connection guide
uv run python tools/build_flash.py      # again after every change in fs/
# start the Wokwi simulation: it boots straight into main.py

tools/build_flash.py writes build/flasher_args.json: the official MicroPython 1.22.0 image at 0x1000 and a LittleFS image with fs/*.py and fs/config.json at 0x200000, the offset of the vfs partition. MicroPython mounts it at boot and runs main.py by itself, like the Rust firmware with its ELF. build/ is not versioned because it carries the device key. To try a change without rebuilding, start the simulation and run uv run python start.py once the console shows >>> or the firmware is running: it stops main.py, mounts fs/ via mpremote (port 4000) and runs it from your computer.

Tests

uv run ruff check .
uv run pytest

The suite contains 135 tests and verifies the MQTT contract against the shared v1 fixture (sensor/actuator names and the legacy LIGHTS alias), integer telemetry and capability declaration, strict command validation, 16-id deduplication and ACK handling, timed actuator shutoff (including offline), offline loop and retry intervals, sensor scaling, LCD display, console timestamp table, and broker verification with the DER CA under both MicroPython 1.22 and 1.23+ umqtt.simple signatures.

Documentation

The firmware only speaks MQTT with the broker; everything else is decided by the platform. Component documentation is in docs/ (also in DOCX and PDF), with diagrams in docs/diagrams. The technical documentation details the MQTT contract, TLS connection, and circuit:

License

This project is licensed under the MIT License.


Español

Estado del Proyecto

Firmware CI CodeQL Advanced

Descripción

SmartPot-IoT-Python es el firmware compañero en MicroPython para prototipado rápido y simulación: MicroPython 1.22.0 sobre un ESP32 (también admite 1.23 o posterior, pero esas imágenes se reiniciaban en Wokwi), simulado en Wokwi o grabado en una placa física. Para despliegues físicos en producción, consulta el firmware canónico en Rust no_std SmartPot-IoT. Lee los sensores, muestra los valores en una pantalla LCD, publica la telemetría por MQTT sobre TLS y ejecuta los comandos que llegan desde SmartPot, confirmando cada uno. Si se cae la red, sigue midiendo, mostrando y apagando a tiempo lo que esté encendido.

Con placa o en Wokwi, el cultivo es real para SmartPot: sus lecturas vienen de este firmware y entrenan el aprendizaje continuo. ¿Sin hardware ni Wokwi abierto? Crea en la PWA un cultivo virtual: lo simula SmartPot-DataGenerator con el mismo contrato, y no se puede convertir en real después.

flowchart LR
  sensors["DHT22 · light · pH<br/>TDS · substrate"] --> esp["ESP32<br/>MicroPython"]
  esp --> lcd["20x4 LCD<br/>readings & connection"]
  esp -->|"telemetry · status · ack"| broker["mqtt.smartpot.app:8883<br/>TLS"]
  broker -->|"commands"| esp
  esp --> act["Pump · UV Light · Fan"]
Loading

Circuito

Componente Pin ESP32 Escala enviada
DHT22 (temperatura y humedad del aire) GPIO 15 °C y %
Sensor de luz (potenciómetro en Wokwi) GPIO 34 0–2000 lux
Sensor de pH GPIO 35 0–14
Sensor de TDS GPIO 32 0–3000 ppm
Humedad del sustrato GPIO 33 0–100 %
Bomba de agua (LED azul) GPIO 19 WATER_PUMP
Luz ultravioleta (LED morado) GPIO 18 UV_LIGHT
Ventilador (LED naranja) GPIO 5 FAN
LCD 20x4 I2C SCL 16 · SDA 17 —

Estructura del Proyecto

SmartPot-IoT/
├── fs/                      # Sistema de archivos del ESP32
│   ├── main.py              # Arranque: bienvenida, WiFi, hora y el ciclo del dispositivo
│   ├── device.py            # Ciclo: mide y apaga a tiempo siempre; publica y obedece con conexión
│   ├── smartpot_client.py   # Contrato MQTT v1 y TLS verificado con la CA de config.json
│   ├── sensors.py           # Sensores analógicos y DHT22
│   ├── actuators.py         # Actuadores con apagado automático por duración
│   ├── display.py           # Pantalla LCD: lecturas, actuadores y estado de la conexión
│   ├── utils.py             # Hora por NTP y tabla por consola con la hora de cada lectura
│   ├── settings.py          # Carga config.json (formato común con el firmware Rust)
│   ├── config.example.json  # Plantilla de config.json (no se versiona) con la CA del broker en texto
│   └── i2c_lcd.py, lcd_api.py
├── tests/                   # Pruebas con CPython y módulos de MicroPython simulados
├── diagram.json             # Circuito de Wokwi con MicroPython 1.22.0
├── wokwi.toml               # Simulación local: build/flasher_args.json (MicroPython 1.22.0 + fs/)
├── tools/build_flash.py     # Genera build/ con fs/ y config.json en el LittleFS de MicroPython
├── start.py                 # Alternativa: monta fs/ con mpremote sin regenerar la imagen
└── pyproject.toml / uv.lock # Herramientas de desarrollo (littlefs-python, mpremote, pytest, ruff)

Contrato MQTT

El dispositivo se conecta a mqtt.smartpot.app:8883 con TLS (1.2 o superior), verifica el certificado del broker con la CA pública de SmartPot y se autentica con usuario = id del cultivo y la clave del dispositivo. El client id es smartpot-<cropId> (el broker rechaza ids vacíos).

Tópico Sentido Ejemplo
smartpot/v1/{cropId}/telemetry Publica cada 30 s {"temperature":23.5,"humidity":61,"brightness":820,"ph":6.12,"tds":790,"soilMoisture":64.2}
smartpot/v1/{cropId}/commands Recibe (QoS 1) {"id":"…","actuator":"WATER_PUMP","action":"ACTIVATE","durationSeconds":15}
smartpot/v1/{cropId}/commands/ack Publica (QoS 1) {"id":"…","status":"EXECUTED","message":"Water pump turned on for 15 s"}
smartpot/v1/{cropId}/status Retenido y última voluntad online / offline
smartpot/v1/{cropId}/capabilities Publica tras online (retenido, QoS 1) {"schema":1,"device":{"firmware":"smartpot-iot-python/2.0.0","hardware":"esp32-devkit-c-v4","telemetry":{"formats":["json-v1"]}},"sensors":[…],"actuators":["WATER_PUMP","UV_LIGHT","FAN"]}

Con durationSeconds el actuador se apaga solo al cumplirse el tiempo; sin él queda encendido hasta recibir DEACTIVATE.

El dispositivo declara sus seis sensores y tres actuadores en .../capabilities después de status=online y antes de suscribirse a commands; el mensaje es retenido, usa el esquema 1, no declara atmosphere ni compact-v2 y ocupa menos de 2 KB. brightness y tds se publican como enteros JSON, nunca como 820.0 o 790.0. La validación de comandos es estricta: id debe ser una cadena no vacía (se rechazan null, booleanos, números, listas y objetos), actuator y action deben ser cadenas no vacías, y durationSeconds debe ser un entero no negativo o estar ausente. Los comandos se deduplican por id en los últimos 16, así que un reenvío QoS 1 repite el ACK anterior sin accionar el hardware otra vez.

La CA del broker en config.json

Wokwi no admite archivos .crt, así que la CA pública viaja como texto en config.json (BROKER.CA_CRT), el único archivo propio de cada dispositivo. El firmware:

  • la convierte a DER, que mbedtls lee en cualquier compilación de MicroPython;
  • se la entrega a umqtt.simple como lo pide cada versión: ssl_params con cadata en MicroPython 1.22 y un SSLContext desde 1.23 (el umqtt.simple de 1.22 ignora el SSLContext y cifra sin verificar);
  • verifica el certificado y el nombre del broker, y antes sincroniza la hora por NTP, porque el ESP32 comprueba las fechas del certificado;
  • sin una CA válida no se conecta: nunca entrega la clave a un broker sin verificar.

Sin conexión

El ciclo corre cada medio segundo con o sin red: apaga a tiempo los actuadores, lee los sensores cada interval_seconds y los muestra en la LCD y en la consola, con la hora de la lectura. Con el broker conectado, además publica la telemetría y atiende los comandos; cada sesión nueva con el broker empieza con una lectura fresca. Si la conexión se cae, reintenta el broker y el WiFi con espera exponencial y dispersión (de 2 s a 60 s). La pantalla se redibuja al momento cuando cambia un actuador o la conexión con el broker; su esquina muestra una antena en línea y una equis sin conexión.

Seguridad y fiabilidad

  • Política de actuadores (compartida con el firmware Rust y con el fixture del contrato): durationSeconds debe ser un entero entre 1 y 3600; si no, el comando recibe un ACK FAILED con el motivo. La bomba de agua nunca queda encendida sin límite: un ACTIVATE sin duración usa 3600 s y se apaga tras 300 s sin sesión con el broker. La luz UV y el ventilador conservan el encendido indefinido. active_low: True admite módulos de relé que se activan con nivel bajo; GPIO5 (ventilador) es un pin de strapping que conmuta al arrancar, así que conviene un pull-down en la entrada del relé.
  • Watchdog: machine.WDT (watchdog_seconds, 30 por defecto) se alimenta desde el temporizador de seguridad solo mientras el ciclo late. Si el ciclo se detiene 120 s, se apagan las salidas, se imprime SMARTPOT-FATAL watchdog task=loop stalled y la placa se reinicia; si se cuelga el intérprete, la reinicia el watchdog de hardware. 0 desactiva ambos para depurar desde el REPL. Las salidas se apagan antes que nada al arrancar y ante cualquier error fatal, que reinicia la placa.
  • La red nunca bloquea el ciclo: los ACK se publican cuando check_msg termina, el socket tiene un tiempo límite de 15 s, el dispositivo envía PINGREQ cada 30 s (un PINGREQ sin respuesta cierra la sesión en el siguiente) y cualquier error del cliente cuenta como conexión perdida. Un temporizador de hardware apaga los actuadores temporizados aunque el ciclo esté esperando a la red, y los temporizadores usan un reloj monótono para que NTP nunca los alargue.
  • LCD resistente: la telemetría se publica antes de dibujar; un LCD ausente o averiado imprime una vez LCD not available at 0x27, se sustituye por uno nulo y se vuelve a buscar cada 10 s, y solo se reescriben las filas que cambian.
  • Sensores: todos se muestrean cada 2 s (el mínimo del DHT22) y un valor sigue siendo válido durante tres periodos, así que una lectura fallida del DHT22 no deja la tabla en blanco. Cada canal analógico promedia 16 lecturas crudas de 12 bits y las escala como raw / 4095, igual que el firmware Rust (read_u16 aproxima esa escala y movía el segundo decimal).
  • Misma salida que el firmware Rust: tests/fixtures/console_v1.json (idéntico en SmartPot-IoT) fija los mensajes de consola, la tabla de lecturas, las filas del LCD, la escala analógica y los motivos de rechazo de comandos de los dos firmwares.
  • Memoria: gc.collect() se ejecuta antes de cada conexión TLS, SMARTPOT-HEAP informa del heap libre (heap_min_free imprime SMARTPOT-HEAP-LOW por debajo de un umbral) y los fallos de memoria repetidos al conectar reinician la placa.
  • Marcadores de texto para pruebas automáticas: SMARTPOT-READY, WIFI-UP, MQTT-UP, MQTT-DOWN, ACT, ACK, LCD, HEAP y FATAL.

Guía de Instalación

1. Crear el cultivo en SmartPot

En smartpot.app crea un cultivo real y elige su forma (maceta, tubos NFT, torre o balsa). La aplicación muestra una sola vez la clave del dispositivo junto con la guía de conexión (ESP32 físico o Wokwi) y el config.json listo, con la red WiFi, el id del cultivo y la CA del broker; si pierdes la clave, genera una nueva desde la pestaña Dispositivo.

2. Configurar el firmware

cp fs/config.example.json fs/config.json

Completa en config.json WIFI, crop_id y device_key (los valores de la guía); el bloque BROKER ya trae la CA del broker. Ambos firmwares leen el mismo formato de config.json, así que puedes pegar tal cual el bloque de la guía. config.json está en .gitignore: nunca subas la clave al repositorio ni la dejes visible en un proyecto público de Wokwi.

3a. Simulación en el navegador

Abre el proyecto de Wokwi, reemplaza sus archivos por los de fs/ (con tu config.json, que ya trae la CA) y por el diagram.json, que usa MicroPython 1.22.0 (la misma imagen que wokwi.toml), y ejecuta. La red Wokwi-GUEST tiene salida a Internet. El firmware también funciona y verifica el broker en 1.23 o posterior, pero esas imágenes se reiniciaban en Wokwi.

3b. Simulación local (Wokwi en el IDE o wokwi-cli)

uv sync
cp config.example.json fs/config.json   # una vez; o el bloque de la guía de conexión
uv run python tools/build_flash.py      # otra vez tras cada cambio en fs/
# inicia la simulación de Wokwi: arranca directamente en main.py

tools/build_flash.py escribe build/flasher_args.json: la imagen oficial de MicroPython 1.22.0 en 0x1000 y una imagen LittleFS con fs/*.py y fs/config.json en 0x200000, el offset de la partición vfs. MicroPython la monta al arrancar y ejecuta main.py solo, como el firmware Rust con su ELF. build/ no se versiona porque lleva la clave del dispositivo. Para probar un cambio sin regenerar la imagen, inicia la simulación y ejecuta uv run python start.py cuando la consola muestre >>> o el firmware ya esté corriendo: detiene main.py, monta fs/ con mpremote (puerto 4000) y lo ejecuta desde tu computador.

Pruebas

uv run ruff check .
uv run pytest

La suite contiene 135 pruebas y verifica el contrato MQTT contra la fixture v1 compartida (nombres de sensores y actuadores y el alias heredado LIGHTS), la telemetría entera y la declaración de capacidades, la validación estricta de comandos, la deduplicación de los últimos 16 ids y el ACK, el apagado por tiempo de los actuadores (también sin conexión), el ciclo sin red con sus reintentos, la escala de los sensores, la pantalla, la tabla con la hora y la verificación del broker con la CA en DER, tanto con el umqtt.simple de MicroPython 1.22 como con el de 1.23 en adelante.

Documentación

El firmware solo habla MQTT con el broker; todo lo demás lo decide la plataforma. Su documentación propia está en docs/ (también en DOCX y PDF), con sus diagramas en docs/diagrams. La documentación técnica detalla el contrato MQTT, la conexión con TLS y el circuito.

  • Diagramas interactivos: los pines del ESP32 y los módulos del firmware, en español e inglés, con enlaces a las líneas exactas del código

Licencia

Este proyecto está bajo la licencia MIT.

About

Firmware MicroPython del dispositivo de un cultivo real de SmartPot (ESP32 o Wokwi) con MQTT sobre TLS

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages