Stabilito il modo in cui il mediatore deve funzionare, si può procedere con la realizzazione vera e propria dell’applicativo. Prima di iniziare, alcune importanti premesse:
- Questo articolo entrerà nei dettagli della scrittura di codice ma non vuole essere una guida sul come programmare; molti particolari verranno semplificati e non si entrerà nello specifico di alcune meccaniche dei linguaggi prescelti. Per delle guide vere e proprie sulla programmazione, si consiglia di visitare gli articoli pubblicati sotto la sezione Tutorial.
- Per motivazioni legate al punto precedente, implementare alcuni aspetti di cui si è discusso negli articoli precedenti risulterebbe eccessivamente complesso e prolisso: si preferirà, quindi, l’esposizione di una metodologia semplice ma funzionale di implementazione del mediatore, discutendo di aspetti più complessi come problemi a sé stanti, trattabili come spunti per approfondire sul tema e migliorare autonomamente l’applicativo presentato.
- La scelta delle tecnologie per l’implementazione è direttamente legata al volere offrire una soluzione accessibile anche a chi non sia esperto del settore: altre soluzioni che utilizzano tecnologie differenti saranno presentate in articoli futuri.
Come introdotto più volte in precedenza, gli strumenti prescelti per l’implementazione effettiva del mediatore sono il linguaggio Python 3, in particolare la libreria paho-mqtt che implementa un client MQTT, che sarà il protocollo di comunicazione M2M prescelto, e la piattaforma Raspberry Pi 3 come macchina su cui eseguire il tutto.
La configurazione del Raspberry Pi 3 è stata già trattata su Antima, come è già stato anche trattato l’argomento MQTT e come utilizzarlo tramite python; di seguito alcuni link utili, facenti riferimento ad articoli pubblicati sul nostro sito, che potrebbero essere delle letture da effettuare prima di proseguire con il resto di questo articolo:
Come configurare un Raspberry Pi 3 B con Raspbian
Utilizzare MQTT con Python (Parte 1): La classe Client
Utilizzare MQTT con Python (Parte 2): Callback e Loop
Protocolli applicativi e rete locale
Per quanto riguarda le semplificazioni strutturali poco prima introdotte, si prenderà in considerazione un solo tipo di sensore ed un solo tipo di attuatore; ciò permette di semplificare in maniera notevole due aspetti, quello della tabella di stato e quello della traduzione.
Inoltre, alcuni aspetti interni del mediatore possono essere aggregati nel caso in cui si abbia a che fare con poche operazioni; verrà esposta una soluzione completa che unisce le operazioni di arrivo, traduzione ed inoltro per uno scenario semplice, approfondendo in maniera meno completa, dal punto di vista del codice, per quanto riguarda architetture più complesse.
Negli articoli precedenti, si è fatto riferimento al contesto applicativo ed alla struttura dei topic MQTT da utilizzare. Introducendo queste semplificazioni nell’insieme delle specifiche del progetto, si possono considerare due gruppi di topic con la seguente struttura:
- Quello dei sensori con la struttura sensor/id_stanza/id_sensore;
- Quello degli attuatori con la struttura actuator/id_area.
La tabella degli stati è una struttura che deve contenere i dati aggiornati letti dai sensori, e rappresenta lo stato di ogni stanza, preso in considerazione in ogni istante di tempo. Se un dato in arrivo da una stanza differisce da quanto contenuto precedentemente nella stessa, lo stato viene aggiornato e il cambiamento viene inoltrato al topic degli attuatori della stanza in questione.
Questa tabella può essere rappresentata in python con una sua struttura dati nativa, il dizionario, che altro non è che una mappa che associa una chiave ad un valore in maniera univoca: le chiavi rappresenteranno gli identificativi delle stanze (id_stanza nei topic introdotti qui sopra) e i valori saranno costituiti dai dati acquisiti, che rappresenteranno cosa sta accadendo in quella stanza, il suo stato per l’appunto.
Detto ciò, si può considerare di avere un solo tipo di sensore, quello di temperatura trattato in precedenza in questa serie, che invia la temperatura attuale nella stanza in cui è posizionato ad intervalli di tempo regolari. Il dato letto deve essere poi tradotto in un’azione da attuare dall’attuatore, che in questo caso sarà la lampada intelligente realizzata precedentemente.
Conoscendo le modalità tramite cui i due elementi inviano e ricevono i dati, si può scrivere una semplice funzione python che implementi il traduttore in maniera elementare, ossia semplicemente specificando in maniera statica quali siano le regole di traduzione:
def to_color(data): if data < 10: return 0 elif data >= 10 and data < 15: return 1 elif data >= 15 and data < 20: return 2 elif data >= 20 and data < 25: return 3 elif data >= 25 and data < 30: return 4 return 5 |
A questo punto si può pensare alla realizzazione della funzione di callback da associare all’evento “arrivo di un messaggio al subscriber“. Bisognerà individuare la stanza da cui il messaggio arriva, aggiornare la tabella dello stato se si riceve un nuovo valore ed, in tal caso, inoltrare il cambiamento agli attuatori della stanza.
Una piccola accortezza va però presa in considerazione: quando l’applicativo è in funzione, più sensori potrebbero mandare contemporaneamente un messaggio al subscriber, il che comporta che più funzioni on_message proveranno a modificare lo stato allo stesso tempo! Per evitare comportamenti errati, bisogna far sì che lo stato sia modificato da una sola funzione alla volta, utilizzando un meccanismo di mutua esclusione.
from threading import Lock BASETOPIC = "actuator" table = dict() mutex = Lock()
room = msg.topic.split("/")[1] value = str(msg.payload.decode()) print("Received {} from room {}".format(value, room)) with mutex: if not(room in table) or (room in table and not (value == table[room])): table[room] = value client.publish("{}/{}".format(BASETOPIC, room), to_color(int(value))) |
La riga room = msg.topic.split("/")[1]
fa sì che la variabile room contenga l’identificativo della stanza da cui proviene il messaggio: msg.topic
contiene il topic del messaggio appena ricevuto, split restituisce una lista di sotto-stringhe del topic delimitate dal separatore /
; se il topic è del tipo actuator/area_id/sensor_id allora split restituirà ["actuator", "area_id", "sensor_id"]
, accedendo all’indice [1]
si recuperare quindi l’identificativo della stanza. L’id della stanza è necessario per due motivi: la tabella di stato ha come indici gli id delle stanze, quindi servirà per accedere al valore della specifica stanza, ed inoltre servirà per inoltrare il messaggio agli attuatori della stanza specifica. L’implementazione del meccanismo di mutua esclusione è affidata all’oggetto multiprocessing.Lock, che implementa un classico mutex. La sintassi with mutex:
esprime in maniera sintetica che all’interno di questo blocco with può essere presente una sola funzione on_message in esecuzione alla volta (e sì, per i più esperti, sarebbe più corretto parlare di thread) e corrisponde alla sintassi più prolissa mutex.Acquire() ... mutex.Release()
.
Infine la condizione contenuta nell’if fa sì che il meccanismo di aggiornamento dello stato ed inoltro del messaggio agli attuatori avvenga solo nel caso di modifica dello stato stesso. L’inoltro del messaggio è rappresentato dall’operazione di publish che prevede due parametri in ingresso: il topic su cui pubblicare (altro motivo per cui era necessario recuperare l’id della stanza!) ed il payload, che in questo caso conterrà il valore che è stato modificato, tradotto nel formato comprensibile per l’attuatore.
A questo punto è tutto pronto, basterà scrivere un paio di righe di inizializzazione delle strutture:
from paho.mqtt.client import Client CLIENTID = "Mediator" HOST = "localhost" SUBTOPIC = "sensor/#" def main(): client = Client(CLIENTID) client.connect(HOST) client.on_message = on_message client.subscribe(SUBTOPIC) client.loop_forever() if __name__ == "__main__": main() |
In questo ultimo estratto viene inizializzato un client MQTT con id Mediator, broker raggiungibile da localhost (il Mediator ed il broker verranno quindi fatti girare sulla stessa macchina, il Rapsberry Pi), che sottoscriverà al topic sensor/# e che, alla ricezione di un messaggio da questo topic, eseguirà la funzione on_message definita in precedenza.
La riga client.loop_forever()
gestisce il traffico MQTT tramite l’inizializzazione di un loop, come spiegato in questo articolo.
Il codice completo con tutti i blocchi appena descritti è disponibile a questo link.
Questo è quanto per quanto riguarda la prima fase del progetto, tutto funziona a dovere in questa veste basilare e semplice!
Nei prossimi articoli si andranno ad approfondire alcuni aspetti più tecnici e complessi, quali studi di performance e di strutture più complesse per rendere la rete più dinamica e ricca.