Vai al contenuto

IoT e MQTT

In questa lezione impari a

  • capire il modello publish/subscribe e il ruolo del broker
  • pubblicare messaggi su un topic con paho-mqtt
  • iscriversi ai topic, anche con i caratteri jolly + e
  • mostrare in una pagina Flask i dati che arrivano dai sensori

Un termometro in aula, un sensore di COβ‚‚, una lampadina "smart": sono oggetti dell'IoT (Internet of Things, Internet delle cose). Sono piccoli, spesso a batteria, e mandano pochi dati, molto spesso. Per loro HTTP Γ¨ troppo "pesante": si usa un protocollo piΓΉ leggero, MQTT.

Oggi costruiamo un piccolo impianto IoT completo, tutto in Python: dei sensori (simulati) che misurano l'aria delle aule e una dashboard che mostra le misure in tempo reale.

Il kit della lezione

File A cosa serve
broker.py il broker, l'"ufficio postale" dei messaggi. Non serve leggerlo
sensore.py un sensore finto: misura temperatura, umiditΓ  e COβ‚‚ e le pubblica
ascolta.py si iscrive ai messaggi e li stampa nel terminale
app.py la dashboard Flask
ORDINE-DI-AVVIO.txt in che ordine si avvia tutto
requirements.txt le librerie: flask e paho-mqtt

La prima volta: pip install -r requirements.txt.

Publish e subscribe

In MQTT nessuno parla direttamente con nessuno. Tutti parlano con il broker:

   PUBLISH                                               SUBSCRIBE
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  scuola/aula-5A/ambiente  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”   scuola/#   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ sensore  β”‚ ────────────────────────► β”‚         β”‚ ───────────► β”‚  ascolta.py  β”‚
  β”‚ aula-5A  β”‚                           β”‚ BROKER  β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                           β”‚         β”‚  scuola/+/…  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  scuola/lab-2/ambiente    β”‚         β”‚ ───────────► β”‚  dashboard   β”‚
  β”‚ sensore  β”‚ ────────────────────────► β”‚         β”‚              β”‚  (app.py)    β”‚
  β”‚ lab-2    β”‚                           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Chi manda dati pubblica (publish) un messaggio su un topic, cioΓ¨ un "canale" con un nome.
  • Chi vuole i dati si iscrive (subscribe) a quel topic.
  • Il broker riceve tutto e consegna ogni messaggio a chi Γ¨ iscritto.

È come un gruppo su un'app di messaggi: chi scrive non sa chi legge, e può leggere chiunque sia nel gruppo. Si possono aggiungere sensori e schermi senza toccare quelli che ci sono già.

Nei progetti veri il broker Γ¨ un programma professionale, ad esempio Mosquitto. Per non dover installare niente, nel kit c'Γ¨ broker.py: un broker semplificato scritto in Python che funziona allo stesso modo.

I topic

Il topic Γ¨ un nome con le "cartelle" separate da /, come un percorso:

scuola / aula-5A / ambiente
  β”‚        β”‚         β”‚
  β”‚        β”‚         └── che cosa misura
  β”‚        └──────────── dove
  └───────────────────── l'edificio

Chi si iscrive puΓ² usare due caratteri jolly:

Iscrizione Riceve
scuola/aula-5A/ambiente solo il sensore dell'aula 5A
scuola/+/ambiente + = un livello qualsiasi: l'ambiente di tutte le aule
scuola/# # = tutto quello che sta sotto scuola/

Passo 1: accendere il broker

Servono piΓΉ terminali aperti insieme. In VS Code: Terminale β†’ Nuovo terminale, oppure il pulsante + nel pannello del terminale.

Nel terminale 1:

python broker.py

Compare Broker MQTT acceso su 127.0.0.1:1883. Lasciatelo acceso: lì vedrete passare i messaggi. Il broker va sempre avviato per primo.

Se Windows chiede il permesso del firewall, potete rispondere Annulla: per ora il broker accetta solo collegamenti da questo PC.

Passo 2: l'ascoltatore (subscribe)

Nel terminale 2: python ascolta.py. Per ora non stampa niente: nessuno sta pubblicando. Ecco il codice:

import paho.mqtt.client as mqtt

BROKER = "localhost"
TOPIC = "scuola/#"              # tutto quello che inizia con scuola/

def collegato(client, userdata, flags, reason_code, properties):
    print("Collegato al broker. Mi iscrivo a", TOPIC)
    client.subscribe(TOPIC)

def messaggio(client, userdata, msg):
    testo = msg.payload.decode("utf-8")             # i byte diventano testo
    print(msg.topic, "->", testo)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = collegato           # "quando ti colleghi, chiama collegato"
client.on_message = messaggio           # "quando arriva un messaggio, chiama messaggio"
client.connect(BROKER, 1883)
client.loop_forever()                   # resta in ascolto per sempre

La novità sono le callback: le funzioni collegato e messaggio non le chiamiamo noi. Le "consegniamo" alla libreria (client.on_message = messaggio) e sarà lei a chiamarle quando succede qualcosa. È lo stesso principio degli eventi in JavaScript, come addEventListener("click", ...).

  • localhost vuol dire "questo PC"; 1883 Γ¨ la porta standard di MQTT.
  • msg.topic Γ¨ il topic del messaggio, msg.payload il contenuto (in byte: con .decode() diventa testo).

Passo 3: il sensore (publish)

Nel terminale 3: python sensore.py. Vi chiede un nome: premete Invio (si chiama aula-5A) oppure scrivete il nome che volete.

nome = input("Nome del sensore (es. aula-5A): ").strip() or "aula-5A"
TOPIC = f"scuola/{nome}/ambiente"

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.connect(BROKER, 1883)
client.loop_start()                     # gestisce la rete in sottofondo

temperatura = 21.0
co2 = 600

while True:
    temperatura = temperatura + random.uniform(-0.3, 0.3)
    co2 = co2 + random.randint(-20, 60)             # in aula la CO2 tende a salire
    co2 = max(400, min(co2, 1600))                  # restiamo tra 400 e 1600

    misura = {
        "temperatura": round(temperatura, 1),
        "umidita": random.randint(40, 60),
        "co2": co2,
    }
    client.publish(TOPIC, json.dumps(misura), retain=True)
    print("Inviato su", TOPIC, "->", misura)
    time.sleep(2)
  • Il sensore non ha un termometro vero: inventa misure realistiche con random, partendo da un valore e cambiandolo un po' ogni volta.
  • json.dumps(misura) trasforma il dizionario in testo JSON: MQTT trasporta solo testo (anzi, byte). Chi lo riceve farΓ  il contrario con json.loads.
  • retain=True chiede al broker di conservare l'ultimo messaggio del topic: chi si iscrive dopo lo riceve subito, senza aspettare la misura successiva.
  • max(400, min(co2, 1600)) tiene il valore tra 400 e 1600.

Guardate i tre terminali insieme: il sensore pubblica, il broker scrive scuola/aula-5A/ambiente -> 1 iscritti, l'ascoltatore stampa il messaggio.

La COβ‚‚ (anidride carbonica) si misura in ppm, parti per milione. All'aperto Γ¨ circa 420 ppm; in un'aula chiusa con 25 persone sale in fretta, e sopra i 1000 ppm Γ¨ ora di aprire le finestre.

Passo 4: la dashboard

Fermate ascolta.py (Ctrl + C) e al suo posto avviate python app.py, poi aprite http://127.0.0.1:5000. Il broker e il sensore restano accesi.

L'idea: la dashboard Γ¨ un altro iscritto, come ascolta.py. Invece di stampare i messaggi, li conserva in un dizionario, e Flask li mostra nella pagina.

  sensore ──► broker ──► app.py:  messaggio()  ──►  ultime = {"aula-5A": {...}, "lab-2": {...}}
                                                               β”‚
                         browser ◄── pagina HTML ◄── home() β—„β”€β”€β”˜
TOPIC = "scuola/+/ambiente"         # + = qualsiasi aula
ultime = {}                         # nome del sensore -> ultima misura

def messaggio(client, userdata, msg):
    try:
        misura = json.loads(msg.payload)            # testo JSON -> dizionario
    except ValueError:
        return                                      # non Γ¨ JSON: lo ignoriamo
    nome = msg.topic.split("/")[1]                  # "scuola/aula-5A/ambiente" -> "aula-5A"
    misura["ora"] = datetime.now().strftime("%H:%M:%S")
    ultime[nome] = misura                           # sostituisce la misura precedente
  • msg.topic.split("/") divide il topic in pezzi: ["scuola", "aula-5A", "ambiente"]. Il pezzo [1] Γ¨ il nome dell'aula.
  • ultime[nome] = misura sostituisce la misura vecchia: nel dizionario c'Γ¨ sempre solo l'ultima di ogni sensore.
  • Il try serve se qualcuno pubblica un messaggio che non Γ¨ JSON: lo saltiamo invece di bloccare tutto.
client.loop_start()                 # MQTT in sottofondo, in un thread a parte

@app.route("/")
def home():
    sensori = sorted(ultime.items())                # in ordine alfabetico
    return render_template("index.html", sensori=sensori)

app.run(debug=True, use_reloader=False)
  • loop_start() (e non loop_forever()) fa lavorare MQTT in sottofondo, in un thread separato: cosΓ¬ Flask resta libero di rispondere alle pagine. Due cose in parallelo, come nei programmi di quarta con i thread.
  • use_reloader=False: in modalitΓ  debug Flask normalmente avvia il programma due volte (per ricaricarlo quando cambiate il codice). Qui avremmo due collegamenti MQTT; cosΓ¬ no. L'unico svantaggio: dopo una modifica ad app.py dovete fermarlo e riavviarlo voi.

Nel template, un ciclo crea una scheda per ogni sensore:

{% for nome, m in sensori %}
  <div class="scheda">
    <h2>🏫 {{ nome }}</h2>
    <p class="valore">🌑️ {{ m.temperatura }} °C</p>
    <p>πŸ’§ UmiditΓ  {{ m.umidita }} %</p>
    <p>🫁 COβ‚‚ {{ m.co2 }} ppm</p>
  </div>
{% endfor %}

E in base.html una riga fa ricaricare la pagina da sola ogni 3 secondi, senza JavaScript:

<meta http-equiv="refresh" content="3">

Tutta la classe sullo stesso broker

Finora ognuno ha il suo broker sul suo PC. Il bello di MQTT si vede quando tutti i sensori della classe parlano con un solo broker: il docente ne accende uno sulla rete del laboratorio e vi dΓ  il suo indirizzo, ad esempio 192.168.1.50. Allora:

  1. In sensore.py cambiate una riga: BROKER = "192.168.1.50" (l'indirizzo che vi dΓ  il docente).
  2. Avviate il sensore con un nome solo vostro (il vostro cognome, per esempio), altrimenti due sensori con lo stesso nome si sovrascrivono.
  3. Sulla dashboard del docente, proiettata alla lavagna, compaiono le schede di tutta la classe.

Non servono il vostro broker nΓ© la vostra dashboard: solo il sensore.

A casa, se volete provare con Internet, esistono broker pubblici di prova come test.mosquitto.org. Sono pubblici: chiunque puΓ² leggere i vostri messaggi, quindi niente dati personali e un topic con un nome strano. Dalla rete della scuola potrebbero essere bloccati.

Se qualcosa non va

Problema Causa Soluzione
ConnectionRefusedError (sensore o ascolta) il broker non Γ¨ acceso avviate prima python broker.py
Il broker non risponde (app.py) come sopra come sopra
La porta 1883 è già occupata il broker è già acceso in un altro terminale va bene così: usate quello
ModuleNotFoundError: No module named 'paho' libreria non installata pip install -r requirements.txt
AttributeError: … CallbackAPIVersion Γ¨ installata una versione vecchia di paho-mqtt pip install --upgrade paho-mqtt
La dashboard dice "In attesa dei dati" nessun sensore acceso, o sensore su un altro broker avviate sensore.py e controllate che BROKER sia uguale nei due file
Avete cambiato app.py ma la pagina non cambia il riavvio automatico Γ¨ spento Ctrl + C e di nuovo python app.py

Missioni

⭐ Missione 1 – Due aule

Aprite un altro terminale e avviate un secondo sensore con un nome diverso (per esempio lab-2): sulla dashboard compare una seconda scheda. Poi in sensore.py fate partire la temperatura da 25 Β°C e mandate una misura ogni 5 secondi invece di 2. Sono due numeri da cambiare.

⭐ Missione 2 – Ascoltare una sola aula

Con due sensori accesi, cambiate il TOPIC di ascolta.py in modo che stampi solo i messaggi del sensore lab-2. Poi provate scuola/+/ambiente: che differenza c'Γ¨ con scuola/#? Scrivete la risposta in un commento nel file.

⭐⭐ Missione 3 – Aprite le finestre!

Nelle schede della dashboard, sotto la COβ‚‚, aggiungete un'etichetta: Β«Aprite le finestre!Β» (classi etichetta rossa) se la COβ‚‚ supera 1000 ppm, altrimenti Β«Aria buonaΒ» (classi etichetta verde). Si fa solo nel template, con un if. Aspettate un minuto: la COβ‚‚ del sensore sale da sola.

⭐⭐ Missione 4 – Il sensore di luce

Aggiungete al sensore una nuova misura, la luce in lux (un numero a caso tra 200 e 800), e mostratela nella dashboard: "πŸ’‘ Luce 540 lux". Servono una riga in sensore.py e una nel template. Notate che app.py non va toccato: perchΓ©?

⭐⭐⭐ Missione 5 – Accendi la ventola

Finora i messaggi vanno solo dal sensore alla dashboard. Fateli andare anche al contrario: in ogni scheda un pulsante Β«Accendi la ventolaΒ» pubblica il messaggio ventola sul topic scuola/<nome>/comandi. Il sensore si iscrive a quel topic e, quando riceve il comando, stampa "πŸŒ€ Ventola accesa" e fa scendere la COβ‚‚ di 150 ppm a ogni misura, finchΓ© arriva sotto 500.

Aiutino

In app.py: una route @app.route("/ventola/<nome>", methods=["POST"]) che fa client.publish(f"scuola/{nome}/comandi", "ventola") e poi return redirect(url_for("home")). Nel template: <form method="post" action="{{ url_for('ventola', nome=nome) }}"><button>Accendi la ventola</button></form>. In sensore.py: una variabile ventola = False, una funzione def comando(client, userdata, msg): che fa global ventola e ventola = True, poi client.on_message = comando e client.subscribe(f"scuola/{nome}/comandi") dopo il connect. Nel ciclo: if ventola: co2 = co2 - 150 e, quando co2 < 500, ventola = False.

⭐⭐⭐ Missione 6 – La memoria

La dashboard ricorda solo l'ultima misura. Fate in modo che per ogni sensore tenga le ultime 10 temperature e mostri nella scheda la minima, la massima e la media.

Aiutino

Un secondo dizionario storico = {} (nome β†’ lista). In messaggio: storico.setdefault(nome, []).append(misura["temperatura"]) e poi storico[nome] = storico[nome][-10:] (gli ultimi 10). Nella route preparate per ogni sensore min(lista), max(lista) e round(sum(lista) / len(lista), 1), oppure aggiungeteli direttamente alla misura prima di salvarla in ultime.