Sistema completo: firmware ESP32 (PlatformIO) · broker MQTT (Mosquitto) · backend Node.js (Express, mqtt.js, WebSocket, compilazione PlatformIO) · dashboard React.
┌─────────┐ MQTT ┌───────────┐ MQTT ┌──────────────────────┐ WS+REST ┌───────────┐
│ ESP32 │◄───────►│ Mosquitto │◄───────►│ Backend Node/Express │◄─────────►│ React app │
└────┬────┘ └───────────┘ │ └ pio run (compila) │ └───────────┘
│ HTTP(S) GET /firmware/<job>.bin?t=<token> └ serve .bin │
└──────────────────────────────────────────────┴─────────────┘
| Topic | Direzione | Payload |
|---|---|---|
iot/<id>/telemetry |
dev → cloud | {uptime_s,temp_c,heap_free,heap_min,rssi,fw,interval_s,...} |
iot/<id>/status |
dev → cloud (retained + LWT) | {online,fw,ip} |
iot/<id>/config/set |
cloud → dev | {interval_s?,temp_offset_c?,led?} |
iot/<id>/config/state |
dev → cloud (retained) | stato reale dopo l'applicazione |
iot/<id>/cmd/ota |
cloud → dev | {job_id,url,md5,size} |
iot/<id>/cmd/reboot |
cloud → dev | {} |
iot/<id>/ota/status |
dev → cloud | {state: downloading|success|failed, progress, job_id, error} |
- La dashboard invia
POST /api/compile-and-flash {deviceId, code, flash}. - Il backend valida il sorgente, copia il template firmware in una dir temporanea per-job, sostituisce solo
src/main.cpp, passa la configurazione eFW_VERSION=<jobId>tramite macro/variabili d'ambiente al build ed eseguepio run. - Il
.binviene salvato con MD5 e token monouso; il backend pubblicacmd/ota. - L'ESP32 scarica via HTTP(S), verifica MD5 (
Update.h), riavvia. - Il job passa a
donequando il dispositivo ritorna online confw == jobId.
IotCore (Wi-Fi, MQTT, NVS, OTA) resta nel template e non viene mai sostituito: l'utente modifica solo main.cpp, che deve chiamare iot::begin() (il backend rifiuta sorgenti che non lo fanno).
cp .env.example .env # compila DOMAIN, PUBLIC_URL, API_TOKEN, Wi-Fi, credenziali MQTT
# utenti Mosquitto (backend + device)
docker run --rm -it -v "$PWD/mosquitto:/mosquitto/config" eclipse-mosquitto \
mosquitto_passwd -c /mosquitto/config/passwd backend
docker run --rm -it -v "$PWD/mosquitto:/mosquitto/config" eclipse-mosquitto \
mosquitto_passwd /mosquitto/config/passwd device
chmod 0700 mosquitto/passwd
docker compose up -d --build # il primo build scarica toolchain ESP32 (~1 GB), poi e' in cacheApri https://<DOMAIN> (Caddy gestisce il certificato) e accedi con API_TOKEN.
Apri le porte 80, 443 e 1883 (o 8883 con TLS) sul firewall.
cd firmware
pio run -t upload && pio device monitorDurante il flash PlatformIO richiederà interattivamente i dati Wi-Fi (WIFI_SSID, WIFI_PASS) e preleverà automaticamente i parametri del broker dalla web app. Nessun file secrets.h con credenziali in chiaro viene memorizzato su disco.
Da qui in poi tutti gli aggiornamenti passano dalla dashboard.
cd backend && npm install && API_TOKEN=dev PUBLIC_URL=http://<ip-lan>:3000 \
MQTT_URL=mqtt://localhost:1883 WIFI_SSID=x DEVICE_MQTT_HOST=<ip-lan> DEVICE_MQTT_USER=device DEVICE_MQTT_PASS=x \
npm run dev # richiede `pio` nel PATH
cd frontend && npm install && npm run dev # http://localhost:5173- Compilare codice C++ di terzi e' esecuzione su input non fidato. Il compose limita memoria/CPU/PID, rimuove le capability e usa un utente non-root; il processo
pioriceve un ambiente senza segreti;platformio.inie' del template (l'utente non puo' aggiungereextra_scriptsolib_deps); gli#includecon path assoluti/..sono bloccati. Esponi la dashboard solo a utenti fidati:API_TOKENequivale a privilegi di amministratore. Per multiutente reale servono sandbox per-job (gVisor/Firecracker) e egress di rete limitato. - TLS: abilita Mosquitto su 8883 e
DEVICE_MQTT_TLS=1. SenzaMQTT_CA_CERTil firmware usasetInsecure()(cifra ma non autentica il server): definisciMQTT_CA_CERTper la produzione. Lo stesso vale per il download OTA https (qui protetto da MD5 + token monouso). - Rischio brick logico: un
main.cppfunzionante ma con bug (watchdog, crash prima diiot::begin()) richiede ri-flash USB. Mitigazione consigliata: abilitare l'app rollback di ESP-IDF (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE, non disponibile nel core Arduino precompilato senza ricompilarlo) oppure testare prima su un dispositivo di prova. - Dati: stato e storico sono in memoria nel backend (si azzerano al riavvio). Per persistenza aggiungi un DB time-series (InfluxDB/TimescaleDB).
- SSID/password con caratteri speciali vengono serializzati con
JSON.stringifynel literal C; evita caratteri non ASCII/controllo. - Il token WebSocket e' nella query string: assicurati che il reverse proxy non logghi gli URL completi.
Per provarlo subito su una sola rete locale senza Docker: vedi LOCAL.md.