Costruire un datalogger remoto con Raspberry e Arduino
Il progetto che ho realizzato riguarda il monitoraggio di temperatura e umidità all’interno di un locale tecnico. I dati rilevati da un sensore DHT22 vengono acquisti da una Arduino MKR Zero e tramite una shield MKR ETH inviati a un computer Raspberry PI 4 che dopo averli acquisiti li memorizza all’interno di una tabella SQLite.
Il protocollo utilizzato per l’invio dei dati si basa su UDP come abbiamo visto in questo articolo.
I dati sarebbero potuti essere memorizzati in un semplice file di testo ma ho preferito usare un database in modo tale da estrarli in modo rapido utilizzando semplici query. Il database contiene per ora una tabella contenente i seguenti campi:
Nome Campo | Tipo Campo |
---|---|
ID |
INTEGER PRIMARY KEY AUTOINCREMENT
|
Data |
TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
Temperatura
|
VARCHAR(50) NOT NULL
|
Umidita
|
VARCHAR(50) NOT NULL
|
Il codice seguente gira sul Raspberry Pi 4; ho inserito diversi commenti che spiegano le varie istruzioni contenute nel programma:
import sqlite3 import socket import threading import time #IP computer locale UDP_IP_ADDR = "192.168.178.50" #Porta computer locale UDP_PORT_NUMBER = 20002 ip_port_Data = (UDP_IP_ADDR, UDP_PORT_NUMBER) def client_Receive_Data(): print("Avvio THREAD Receive Data...") serversocket1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) serversocket1.bind(("192.168.178.30", 20001)) msgToSend = str.encode("DB_OK") #crea connessione al database connection = sqlite3.connect('lopro.db') cursor = connection.cursor() try: while True: data, address = serversocket1.recvfrom(1024) strData = bytes.decode(data).split(":") sql = """ INSERT INTO tbl_log(Temperatura, Umidita) VALUES ('{}', '{}');""".format(strData[0], strData[1]) print(sql) cursor.execute(sql) connection.commit() #invia al device una risposta serversocket1.sendto(msgToSend, ip_port_Data) time.sleep(1) except: print("errore") msgToSend = str.encode("ERRORE") serversocket1.sendto(msgToSend, ip_port_Data) connection.close() def crea_tabella(): #una volta creata la tabella possiamo evitare di chiamare #questa funzione ad ogni esecuzione del programma #crea connessione al database connection = sqlite3.connect('lopro.db') cursor = connection.cursor() #Se esiste la tabella cancellala #cursor.execute("DROP TABLE IF EXISTS tbl_log") # Crea la tabella se non esiste table = """ CREATE TABLE IF NOT EXISTS tbl_log ( ID INTEGER PRIMARY KEY AUTOINCREMENT, Data TIMESTAMP DEFAULT CURRENT_TIMESTAMP, Temperatura VARCHAR(50) NOT NULL, Umidita VARCHAR(50) NOT NULL ); """ cursor.execute(table) cursor.close() connection.close() def list_rows(): print("list rows..") #crea connessione al database connection = sqlite3.connect('lopro.db') sql = """ SELECT * FROM tbl_log;""" cursor = connection.cursor() cursor.execute(sql) rows = cursor.fetchall() for row in rows: print(row) # Close the connection connection.close() if __name__=="__main__": #Rimuovere il commento per creare la tabella #crea_tabella() #list_rows() #Avvio un thred print("Start program") t1 = threading.Thread(target=client_Receive_Data) t1.start()
Questo è invece il codice che gira sulla Arduino MKR Zero; ho inserito diversi commenti che spiegano le varie istruzioni contenute nel programma:
#include <SPI.h> #include <Ethernet.h> #include <EthernetUdp.h> #include <DHT.h> #define DHTPIN 6 #define DHTTYPE DHT22 // creo un MAC address e un IP address per la scheda Arduino byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(192, 168, 178, 50); //creo un ip address per il raspberry IPAddress ip_remote(192, 168, 178, 30); //creo una porta per la scheda Arduino unsigned int localPort = 20002; //creo degli array di char per la ricezione e l'invio dei //dati via udp char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; char chrBuffer[11]; //altre variabili String strTempUmid = ""; byte ciclo = 0; // Creo degli oggetti per l'utilizzo di udp e del //sensore dht EthernetUDP Udp; DHT dht(DHTPIN, DHTTYPE); void setup() { //inizializzo lo shield mkr eth Ethernet.init(5); //inizializzo la scheda di rete Ethernet.begin(mac, ip); //inizializzo la seriale Serial.begin(9600); // verifico la ethernet shield if (Ethernet.hardwareStatus() == EthernetNoHardware) { Serial.println("MKR ETH non presente o configurata in modo errato!"); while (true) { delay(1); } } //inizializzo l'oggetto udp Udp.begin(localPort); //inizializzo l'oggetto dht dht.begin(); } void loop() { //verifico presenza pacchetti udp in arrivo int packetSize = Udp.parsePacket(); //se coi sono pacchetti if (packetSize) { //Serial.println(packetSize); //leggo i pacchetti e gli inserisco nell'array di char Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE); //printo i dati ricevuti Serial.println(packetBuffer); } //leggo umidità e temperatura dal sensore dht float h = dht.readHumidity(); float t = dht.readTemperature(); //creo una stringa del tipo temp:umid strTempUmid = ""; strTempUmid.concat(t); strTempUmid.concat(":"); strTempUmid.concat(h); //printo la stringa sul ser mon Serial.println(strTempUmid); //converto la stringa in un array di char strTempUmid.toCharArray(chrBuffer, strTempUmid.length()); //inizializzo la connessione con ip e porta del Raspberry Udp.beginPacket(ip_remote, 20001); //Scrivo i dati nel buffer Udp.write(chrBuffer); //invio i dati Udp.endPacket(); //attendo un minuto prima del prossimo invio for (ciclo = 0; ciclo < 60; ciclo++) delay(1000); }
Ogni minuto viene inviata una unica stringa contenente i dati di temperatura e umidita che il codice python provvederà a separate in due dati distinti per essere caricati sul database.
Memorizzare solo le variazioni significative su Raspberry SQLite
Possiamo ottimizzare lo spazio sulla tabella (e di conseguenza sul filesystem del Raspberry) memorizzando solo i dati che hanno una variazione significativa. In questo modo il monitoraggio rimane costante perché comunque l’Arduino legge e invia i dati ogni minuto ma posso risparmiare spazio sul database salvando solo le variazioni significative. Sulla tabella memorizzo anche il timestamp e quindi posso ricostruire l’andamento di temperatura e umidità interpolando i dati.
Il codice seguente mostra la modifica alla funzione client_Receive_Data() in modo tale da memorizzare solo i dati che hanno una variazione di 0.5 rispetto ai dati della lettura precedente rilevata dalla scheda Arduino:
def client_Receive_Data(): print("Avvio THREAD Receive Data...") serversocket1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) serversocket1.bind(("192.168.178.30", 20001)) # crea connessione al database connection = sqlite3.connect('lopro.db') cursor = connection.cursor() #creo due variabili per memorizzare i dati delle #letture di temp e umid temp_prec = 0 umid_prec = 0 try: while True: data, address = serversocket1.recvfrom(1024) strData = bytes.decode(data).split(":") #verifico se i dati appena letti differiscono di 0.5 dai valori della lettuare precedente if (abs(temp_prec - float(strData[0])) > 0.5) or (abs(umid_prec - float(strData[1])) > 0.5) : print("Variazione di Temp e Hum") sql = """ INSERT INTO tbl_log(Temperatura, Umidita) VALUES ('{}', '{}');""".format(strData[0], strData[1]) #memorizzo i dati per poterli confrontare con quelli #che arrivano dalla scheda arduino temp_prec = float(strData[0]) umid_prec = float(strData[1]) #visualizzo la stringa sql print(sql) #eseguo la stringa sql insert avviando una transazione cursor.execute(sql) #confermo la transazione connection.commit() # invia al device una risposta msgToSend = str.encode("DB_OK") serversocket1.sendto(msgToSend, ip_port_Data) else: print("Nessuna variazione di Temp e Hum") # invia al device una risposta msgToSend = str.encode("DB_NOK") serversocket1.sendto(msgToSend, ip_port_Data) time.sleep(1) except: print("errore") msgToSend = str.encode("ERRORE") serversocket1.sendto(msgToSend, ip_port_Data) connection.close()
L’utilizzo congiunto di Raspberry e Arduino ci permette di realizzare progetti che superano i limiti dati dalle singole schede.