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", ...).
localhostvuol dire "questo PC";1883è la porta standard di MQTT.msg.topicè il topic del messaggio,msg.payloadil 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 conjson.loads.retain=Truechiede 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] = misurasostituisce la misura vecchia: nel dizionario c'Γ¨ sempre solo l'ultima di ogni sensore.- Il
tryserve 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 nonloop_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 adapp.pydovete 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:
- In
sensore.pycambiate una riga:BROKER = "192.168.1.50"(l'indirizzo che vi dΓ il docente). - Avviate il sensore con un nome solo vostro (il vostro cognome, per esempio), altrimenti due sensori con lo stesso nome si sovrascrivono.
- 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.