Spesso i dati prelevati da sensori devono essere utilizzati da applicazioni esterne alla scheda che comanda il sensore per vari motivi: analisi, decision making, semplice salvataggio in un database. Diventa chiaro che in queste situazioni, avere tutta la logica di un’applicazione all’interno di un programma caricato su una piattaforma come Arduino è non solo scomodo ma anche impossibile.
In questo articolo viene esplorata una metodologia per approcciarsi alla costruzione di applicazioni di questo tipo in Python3, basandosi sulla libreria antimait. L’approccio programmatico sarà simile a quello già introdotto nei precedenti articoli, basato sulla scrittura di classi DataReceiver per la ricezione, elaborazione ed eventuale inoltro dei dati.
Parte 1 – Progettazione e strumenti
Si vuole realizzare un sistema che sia in grado di captare la presenza di una persona, salvando dati inerenti la durata della presenza in una stanza e comunicando i cambi di stato tramite uno schermo lcd.
I dispositivi utilizzati per implementare questo tipo di architettura sono i seguenti:
- Una scheda Arduino (in questi caso verrà utilizzato un Elegoo Uno con microcontrollore ATMega328P, un Arduino Uno va più che bene);
- Un sensore di presenza PIR HC-SR501 (di cui abbiamo parlato entrando profondamente nei dettagli in questo articolo);
- Un display LCD1602, in coppia con un potenziometro da 10KΩ;
- Un Computer su cui implementare la logica dell’applicazione, che potrebbe essere un Raspberry Pi per ottenere massima portabilità.
La scheda Arduino acquisirà dati ad intervalli regolari per poi inoltrarli ai piani alti dell’architettura qualora si identifichi un cambio di stato; all’interno di ogni ciclo di esecuzione, dopo avere acquisito i dati, si andrà a controllare se ci sono dei dati in ingresso dall’applicativo Python che devono essere utilizzati per modificare il messaggio esposto sullo schermo.
L’applicazione Python attenderà i dati inoltrati dalla scheda per salvarli in un file testuale di logging all’interno della macchina su cui si trova in esecuzione, per poi mandare un messaggio da mostrare su schermo contenente informazioni sull’ultimo cambio di stato.
Il tutto è riassunto nel diagramma di attività sotto riportato.
Parte 2 – Arduino
Come anticipato nella sezione precedente, per implementare quanto descritto viene utilizzata una piattaforma della famiglia Arduino assieme ad un display LCD 1602 ed un sensore di presenza HC-SR501.
Per interagire con lo schermo, si utilizza la libreria LiquidCrystal.h; un oggetto istanza della classe LiquidCrystal permette di potere interfacciarsi con la periferica. Quest’oggetto va inizializzato nella seguente maniera:
LiquidCrystal lcd(RS, EN, D4, D5, D6, D7);
dove D4, D5, D6, D7 sono i pin Arduino da collegare ai pin dello schermo con questi nomi, EN è il pin enable dello schermo e RS è il pin register select.
Schema delle connessioni su breadboard
Utilizzando il solito approccio programmatico di Arduino, si definisce una funzione setup con connessione seriale a 9600 baud, inizializzando il pin di input del sensore ed una variabile booleana per tenere traccia dei cambi di stato. Viene, inoltre, definita una funzione per scrivere sullo schermo in maniera precisa; la funzione prende in ingresso una stringa da scrivere e la mette in uscita sull’lcd dopo avere spostato il cursore di scrittura all’inizio dello schermo ed aver pulito l’uscita da eventuali precedenti messaggi.
void setup() { Serial.begin(9600); pinMode(PIR, INPUT); presence = false; lcd.begin(16, 2); } void update_lcd(String msg) { lcd.setCursor(0, 0); lcd.clear(); lcd.print(msg); }
La funzione loop implementa quanto descritto nel diagrama di attività:
const int PIR = 8; bool presence; int value_read; String msg; void loop() { value_read = digitalRead(PIR); if(value_read == HIGH && !presence) { Serial.println("on"); presence = true; } else if (value_read == LOW && presence) { Serial.println("off"); presence = false; } if(Serial.available()) { msg = Serial.readString(); update_lcd(msg); } delay(100); }
Ogni 100 millisecondi viene letto il valore in entrata sul pin del sensore PIR e, se si registra un cambio di stato, la variabile associata viene aggiornata in maniera concorde al cambio, e viene comunicato un messaggio tramite seriale, che verrà raccolto dall’applicazione Python: la sintassi di questo mini protocollo è molto semplice e ristretta alle due sole stringhe on e off. Dopo questa fase, si effettua un controllo sulla disponibilità di dati in ingresso tramite seriale dall’applicazione Python e si aggiorna lo schermo nel caso in cui si trovino dati pronti.
N.B. il pin VO dello schermo è connesso ad un potenziometro da 10KΩ per aggiustare il contrasto. Bisognerà trovare il livello giusto da sé per far sì che le scritte sullo schermo appaiano leggibili.
La breadboard completa di ogni componente
Parte 3 – Python3/antimait
Fino ad ora si è visto come utilizzare antimait tramite le classi già incluse al suo interno (come antimait.Printer, antimait.plotting.Plotter), collegandole al volo tramite la funzione on_connect alle interfacce dei dispositivi connessi. Per implementare le funzionalità di cui si ha bisogno per la realizzazione di questa applicazione utilizzeremo le classi astratte della libreria per scrivere delle classi personalizzate per questo scopo.
Il solito approccio all’associazione DataReceiver/DataSource è applicato a questo contesto:
gw = antimait.Gateway() def on_connect(interface: antimait.CommInterface, description: str): logger = Logger(format_filename(description), gw, interface.ifc_id) interface.attach(logger) gw.on_connect = on_connect try: gw.listen_forever() except KeyboardInterrupt: gw.close() print("Bye!")
Questa volta però, si può osservare come l’oggetto passato a interface.attach sia di tipo Logger e prenda come ingresso nel suo costruttore, tre campi: il nome del file su cui salvare i dati ottenuti, un riferimento al gateway ed uno all’identificativo dell’interfaccia a cui è attaccato.
Logger è una classe che estende antimait.DataReceiver ed ha il ruolo di fungere da endpoint per la ricezione dati, di salvarli su un file testuale e di comandare lo schermo a seconda dei dati letti. In questi casi è utile dividere le varie operazioni da effettuare in diverse funzioni o metodi d’istanza per avere la possibilità di modificarle e implementarle in maniera isolata.
Qui sotto sono riportate i metodi privati che si occupano della scrittura su file e della determinazione del messaggio da scrivere:
def _append(self, msg: str): with open(self._filename, "a") as file: file.write("{}\n".format(msg))
def _set_presence(self, presence: bool) -> str: presence = "start" if presence else "end" timestamp = datetime.datetime.now().strftime("%d-%m-%y_%I-%M-%S") msg = "Presence\t{}\t{}".format(presence, timestamp) self._append(msg) return timestamp
- _append, aggiunge una riga al file specificato nel costruttore, aprendolo in modalità “a”, che sta, per l’appunto, per append;
- _set_presence, genera il messaggio da scrivere sul file in base al cambio di stato verificatosi ed al momento in cui esso si è verificato, includendo un timestamp.
Ogni DataReceiver deve includere al suo interno un’implementazione di update, per rispettare il contratto stipulato con l’estensione della classe:
def update(self, action: antimait.Comm, **update: str): if action == antimait.Comm.DATA: try: presence = update["data"].strip() except KeyError: raise KeyError("You need to specify the data keyword") if presence not in {"on", "off"}: raise ValueError("presence can only be 'on' or 'off'") presence_val = True if presence == "on" else False timestamp = self._set_presence(presence_val) timestamp = timestamp[timestamp.index("_")+1:].replace("-", ":") msg = "{} {}".format(presence, timestamp) self._gw.forward(self._device, msg)
L’update qui presente è simile a quelli già osservati negli esempi base visti nell’introduzione alla libreria, ma presenta delle aggiunte interessanti:
- i valori di update[“data”] sono stati ristretti alle sole stringhe on e off, per evitare di avere comportamenti scorretti nel caso di ricezioni di refusi;
- il messaggio generato per essere visualizzato dallo schermo riporta il timestamp precedente con le sole informazioni sull’ora, i minuti e i secondi dell’ultimo cambio di stato;
- si va ad utilizzare la funzione forward.
forward è una funzione di antimait.Gateway con la seguente firma:
def forward(self, dest: str, msg: str) -> None
Il suo effetto è quello di inoltrare il messaggio msg all’interfaccia identificata dall’id dest utilizzando il meccanismo di comunicazione preferenziale dell’interfaccia, che è un aspetto di cui non bisogna preoccuparsi quando si utilizza questa funzione.
All’interno di Gateway è presente una funzione simile che ha però l’effetto di mandare il messaggio in broadcast a tutte le interfacce connesse in questo momento al gateway, dal nome broadcast:
def broadcast(self, msg: str) -> None
Queste due funzioni sono disponibili in antimait a partire dalla versione 0.2.3, nel caso in cui si sia in possesso di una versione precedente, è possibile effettuare l’aggiornamento della libreria tramite:
pip install antimait --upgrade
Il tutto può essere reperito dalla nostra pagina ufficiale github al seguente link, all’interno del quale sono anche presenti le istruzioni per scaricare e testare il programma sulle vostre piattaforme.
Per ora è tutto, nel caso in cui ci fosse bisogno di chiarimenti o ci siano dei dubbi su alcuni passaggi dell’articolo non esitate a lasciare un commento qui sotto o scriverci tramite i i recapiti che si possono trovare nella pagina Contatti.