Monitoraggio ambientale con protocollo Modbus, Raspberry Pi e ESP32: guida essenziale con API REST

Introduzione

Questo progetto, incentrato sul protocollo Modbus, rappresenta un perfetto esempio di come la tecnologia moderna possa essere utilizzata per creare un sistema di monitoraggio ambientale efficiente e scalabile, utilizzando componenti economici e ampiamente disponibili come il Raspberry Pi e l’ESP32. Il cuore del sistema è un Raspberry Pi equipaggiato con un sensore DHT22, che misura ciclicamente la temperatura e l’umidità dell’ambiente circostante. Questi dati vengono poi trasmessi in tempo reale tramite il protocollo Modbus utilizzando una connessione WiFi. La scelta di Modbus come protocollo di comunicazione riflette la necessità di un metodo affidabile e standardizzato per la trasmissione dei dati in contesti industriali e domestici.

Il progetto non si limita alla semplice raccolta e trasmissione dei dati. Un modulo ESP32 riceve i dati dal Raspberry Pi via Wi-Fi e li processa ulteriormente. L’ESP32 è programmato per registrare i dati in un file di log all’interno del suo file system SPIFFS (SPI Flash File System). Ogni coppia di dati (temperatura e umidità) viene arricchita con un timestamp accurato, ottenuto da un server NTP (Network Time Protocol), garantendo che ogni registrazione sia associata all’ora esatta di rilevazione.

Un altro aspetto fondamentale del progetto è la gestione efficiente dello spazio di archiviazione. Poiché i dati vengono costantemente registrati, il file di log può crescere rapidamente. Per prevenire un uso eccessivo della memoria disponibile, l’ESP32 include un meccanismo che monitora continuamente la dimensione del file di log. Quando il file supera una dimensione predefinita, il sistema automaticamente rimuove le righe più vecchie, preservando solo i dati più recenti e rilevanti. Questo approccio assicura che il sistema possa funzionare in modo autonomo e continuo senza la necessità di interventi frequenti.

Infine, il progetto include tre API REST, accessibili via Wi-Fi, che permettono di interagire con il sistema in modo semplice e intuitivo. L’API datetime fornisce la data e l’ora attuale del sistema, sincronizzata tramite NTP, garantendo che gli utenti possano ottenere informazioni temporali accurate. L’API checkfile verifica la presenza del file di log nel file system SPIFFS, utile per operazioni di debug. L’API readfile consente di leggere il contenuto del file di log, permettendo agli utenti di accedere ai dati raccolti in modo remoto.

In sintesi, questo progetto non solo dimostra come utilizzare tecnologie open-source e componenti accessibili per creare un sistema di monitoraggio ambientale, ma evidenzia anche l’importanza della gestione dei dati e dell’accessibilità tramite interfacce web. Con la sua combinazione di robustezza, flessibilità e facilità d’uso, rappresenta un’implementazione pratica e versatile, applicabile a una vasta gamma di scenari sia industriali che domestici.

Come al solito il codice per l’ESP32 è stato scritto tramite l’ottimo IDE PlatformIO. Per ulteriori informazioni sul file system utilizzato, ti invito a dare uno sguardo all’articolo Gestione dei file su ESP32: SPIFFS e LittleFS a confronto.

Il protocollo Modbus

Modbus è un protocollo di comunicazione industriale nato nel 1979 per iniziativa di Modicon (ora Schneider Electric), una delle prime aziende a sviluppare PLC (Programmable Logic Controller). Il protocollo è stato progettato per consentire ai dispositivi di automazione industriale, come sensori, attuatori e controller, di comunicare tra loro in modo standardizzato ed efficiente.

Origine e scopo del Modbus

Il protocollo è nato in risposta alla necessità di un metodo universale per collegare diversi dispositivi in un sistema di automazione industriale. Prima della sua creazione, i produttori utilizzavano protocolli proprietari, rendendo difficile l’integrazione di dispositivi di diverse marche. Modbus è stato concepito come un protocollo aperto, facilmente implementabile su varie piattaforme, e progettato per funzionare su reti seriali come RS-232 e RS-485. Questo ha permesso una larga adozione, rendendo Modbus uno standard de facto nel settore.

Utilizzi del Modbus

Modbus viene utilizzato principalmente per collegare dispositivi di campo con un PLC o un sistema SCADA (Supervisory Control and Data Acquisition). È ideale per applicazioni dove è necessario monitorare e controllare variabili di processo come temperature, pressioni, livelli e velocità in tempo reale. Il protocollo supporta varie modalità, tra cui Modbus RTU, che è utilizzato su reti seriali, e Modbus TCP/IP, che permette la comunicazione su reti Ethernet moderne.

Perché Modbus è ancora in uso

Nonostante l’avvento di tecnologie più moderne, Modbus è ancora ampiamente utilizzato per diverse ragioni:

  • Semplicità: Modbus è facile da implementare e richiede risorse minime, rendendolo ideale per dispositivi embedded con capacità limitate.
  • Compatibilità: essendo uno standard aperto, Modbus è supportato da una vasta gamma di dispositivi e produttori, facilitando l’integrazione in sistemi esistenti.
  • Affidabilità: con decenni di utilizzo nel settore, Modbus ha dimostrato di essere un protocollo robusto e affidabile.
  • Flessibilità: Modbus può funzionare su varie reti (seriali, Ethernet, ecc.), rendendolo versatile per diverse applicazioni industriali.

Evoluzione e futuro

Negli anni, Modbus è stato adattato per funzionare su nuove reti, come Ethernet, e ha visto l’introduzione di versioni come Modbus Plus e Modbus TCP/IP. La sua flessibilità e la continua compatibilità con dispositivi legacy assicurano che rimanga rilevante anche in sistemi moderni. Tuttavia, con l’emergere di nuovi protocolli come OPC-UA, Modbus potrebbe gradualmente lasciare spazio a soluzioni più avanzate in applicazioni più complesse.

In sintesi, Modbus è un pilastro fondamentale nel mondo dell’automazione industriale, grazie alla sua semplicità, affidabilità e capacità di integrare una vasta gamma di dispositivi in un ambiente di controllo industriale.

Il nostro progetto prevede anche l’utilizzo di alcune API REST. Vediamo più in dettaglio di cosa si tratta.

Le API REST

Le API REST (Representational State Transfer) sono un paradigma di comunicazione molto diffuso, soprattutto nel contesto dell’Internet of Things (IoT), che permette l’interazione tra diversi sistemi attraverso un’interfaccia standardizzata e semplice da usare. Introdotte da Roy Fielding nei primi anni 2000, le API REST si basano su principi architetturali che sfruttano il protocollo HTTP per permettere a dispositivi, servizi e applicazioni di comunicare tra loro in modo scalabile, efficiente e indipendente dalla piattaforma.

Scopo delle API REST

Le API REST servono a esporre funzionalità di un sistema o servizio a un’altra applicazione, sia essa un’app mobile, un’applicazione web o un dispositivo IoT. Questo avviene tramite operazioni CRUD (Create, Read, Update, Delete) che mappano direttamente sulle operazioni HTTP: POST, GET, PUT/PATCH, DELETE. In pratica, queste API permettono di leggere, creare, modificare e cancellare risorse su un server remoto, il tutto utilizzando richieste HTTP semplici e facilmente comprensibili.

Tipi di API REST

Le API REST possono essere classificate in diversi tipi a seconda del tipo di risorsa che gestiscono:

  • API REST JSON: le risposte vengono fornite in formato JSON (JavaScript Object Notation), il formato più comune e leggibile per la trasmissione di dati strutturati.
  • API REST XML: anche se meno comune di JSON, alcune API REST utilizzano XML (eXtensible Markup Language) per rappresentare le risorse.
  • API REST Streaming: utilizzate per trasmettere grandi flussi di dati in tempo reale, come video o dati di sensori IoT.

Importanza delle API REST nell’IoT

Nel contesto dell’IoT, le API REST sono fondamentali per diverse ragioni:

  • Interoperabilità: consentono a dispositivi eterogenei di comunicare tra loro, indipendentemente dal loro sistema operativo o linguaggio di programmazione.
  • Scalabilità: grazie alla loro architettura leggera, le API REST sono facilmente scalabili, il che le rende ideali per ambienti IoT con un numero crescente di dispositivi connessi.
  • Facilità di implementazione: le API REST sono semplici da implementare e comprendere, rendendo più rapido lo sviluppo di applicazioni IoT.
  • Accessibilità: permettono ai dispositivi di essere accessibili da qualsiasi luogo, favorendo la gestione e il controllo remoto di sistemi IoT, attraverso reti globali come Internet.

Perché le API REST sono importanti

Le API REST rappresentano un ponte tra il mondo fisico e quello digitale. Nell’IoT, dove i dispositivi generano e consumano grandi quantità di dati, le API REST forniscono un metodo strutturato e standardizzato per accedere a questi dati e utilizzarli in applicazioni diverse. Facilitano l’integrazione tra sistemi nuovi e legacy, migliorando l’interoperabilità e riducendo la complessità dello sviluppo. Inoltre, grazie alla loro flessibilità, le API REST possono essere utilizzate per una vasta gamma di applicazioni, dal controllo domestico alla gestione di grandi impianti industriali.

Le API REST sono un elemento chiave per lo sviluppo di soluzioni IoT moderne. Offrono un’interfaccia universale che semplifica la comunicazione tra dispositivi e servizi, garantendo flessibilità, scalabilità e facilità di uso. Grazie alla loro adozione globale, rappresentano uno standard di fatto per la creazione di ecosistemi IoT interconnessi e interoperabili.

Se sei particolarmente interessato all’uso delle API REST, ti invito a dare un’occhiata all’articolo Come realizzare un server API REST con la ESP32 che spiega come creare un server web con la ESP32 in modo che questa esponga un set di API REST da utilizzare per interagire col dispositivo in modo da ricevere o mandare dei dati.

Per testare le API REST in questo progetto ci avvarremo di un noto software: Postman.

Postman

Postman è uno strumento potente e versatile utilizzato per testare e sviluppare API REST, essenziale per chiunque lavori con comunicazioni tra server e client, specialmente nell’ambito dell’Internet of Things (IoT). Grazie alla sua interfaccia intuitiva, Postman consente di inviare richieste HTTP di tipo GET, POST, PUT, DELETE e di visualizzare le risposte in tempo reale. Questo facilita il debugging e la verifica del corretto funzionamento delle API, rendendolo uno strumento indispensabile per sviluppatori e ingegneri.

Una delle principali caratteristiche di Postman è la sua capacità di simulare richieste API in modo preciso, permettendo di testare le interazioni tra diversi dispositivi o tra un dispositivo e un server remoto. Questo è particolarmente utile quando si sviluppano applicazioni IoT, dove i dispositivi devono spesso comunicare con servizi cloud o scambiarsi dati in tempo reale. Inoltre, Postman supporta la creazione di collezioni di richieste, che possono essere salvate e riutilizzate, rendendo il processo di test più efficiente e strutturato.

In sintesi, Postman non è solo un semplice client per testare API, ma un vero e proprio strumento di sviluppo che supporta l’intero ciclo di vita delle API, dalla progettazione al test, fino al monitoraggio. Nel contesto dell’IoT, la sua capacità di gestire e testare le API REST in modo efficiente lo rende un alleato indispensabile per garantire che i dispositivi possano comunicare tra loro senza problemi. Integrando Postman nel tuo workflow, puoi assicurarti che le API che sviluppi siano robuste, affidabili e pronte per essere implementate in ambienti di produzione.

Il DHT22

Il DHT22 è un sensore digitale utilizzato per misurare la temperatura e l’umidità, ampiamente diffuso per la sua semplicità d’uso e precisione. Internamente, il DHT22 è composto da un elemento capacitivo per misurare l’umidità e un termistore per rilevare la temperatura. Il sensore è in grado di fornire letture di umidità con una precisione del ±2-5% e di temperatura con una precisione di ±0.5°C, coprendo un range di -40°C a +80°C per la temperatura e un range di 0% a 100% per l’umidità.

Il DHT22 comunica tramite un protocollo seriale digitale unidirezionale che utilizza un singolo filo per trasmettere i dati. La comunicazione inizia con il microcontrollore che invia un segnale di start, seguito da una risposta del DHT22. Il sensore risponde inviando 40 bit di dati, che rappresentano l’umidità (16 bit), la temperatura (16 bit), e un checksum (8 bit) per verificare l’integrità dei dati trasmessi.

Il segnale di dati è codificato utilizzando un formato temporale in cui la durata di ciascun impulso determina se il bit trasmesso è un 0 o un 1. Questo protocollo, pur essendo semplice, richiede un’accurata gestione del timing da parte del microcontrollore per garantire la corretta lettura dei dati.

Il DHT22 è popolare per applicazioni che richiedono una misurazione affidabile della temperatura e dell’umidità in ambito domestico o industriale, grazie al suo equilibrio tra precisione, costo e facilità d’uso. Tuttavia, la velocità di lettura relativamente lenta (circa una volta ogni 2 secondi) lo rende meno adatto per applicazioni che richiedono aggiornamenti rapidi.

Di che componenti abbiamo bisogno?

La lista dei componenti non è particolarmente lunga:

  • una breadboard per connettere la Raspberry PI agli altri componenti
  • alcuni fili DuPont (maschio – maschio, maschio – femmina, femmina – femmina)
  • un sensore DHT22
  • un resistore da 4.7kΩ
  • una NodeMCU ESP32
  • una (micro) SD card da non più di 32GB formattata in FAT32
  • un eventuale dongle WiFi USB per la Raspberry
  • e, ovviamente, una Raspberry !

La SD card che ho usato è da 32GB. La ESP32 utilizzata è distribuita dall’azienda AZ-Delivery.

Il progetto è stato testato con successo su una Raspberry PI 3 Model B ma non è escluso che funzioni anche su altri modelli di Raspberry.

Realizzazione del progetto

Lo schema elettrico

Prima di realizzare il circuito vero e proprio diamo un’occhiata ai pinout della Raspberry utilizzata:

Pinout della Raspberry Pi 3 Model B
Pinout della Raspberry Pi 3 Model B

Vediamo quindi il pinout del sensore DHT22:

Pinout del DHT22
Pinout del DHT22

A questo punto possiamo vedere il collegamento del DHT22 alla Raspberry tramite un diagramma realizzato con Fritzing:

Schema del collegamento tra la Raspberry e il DHT22 del nostro datalogger su protocollo Modbus
Schema del collegamento tra la Raspberry e il DHT22 del nostro datalogger su protocollo Modbus

L’alimentazione è presa dalla linea a 3.3V mentre l’uscita dati del DHT22 è collegata al pin 7 della Raspberry, corrispondente al GPIO 4.

È importante alimentare il DHT22 a 3.3V in quanto se il pin 7 dovesse ricevere un segnale a 5V si danneggerebbe irrimediabilmente.

Preparazione della Raspberry

Per poter utilizzare la Raspberry è necessario fare alcuni passi preliminari ed installare alcuni software.

Iniziamo subito con l’installazione del sistema operativo.

Il sistema operativo scelto è una distribuzione fatta apposta per funzionare su tutti i tipi di Raspberry, anche le più datate. I test sono stati fatti su una Raspberry PI 3 Model B.

Se la Raspberry non ha connessione Wireless nativa puoi usare un dongle WiFi da inserire in una delle sue prese USB.

Scarichiamo e installiamo il sistema operativo su SD card

Scarica l’ultima versione del sistema operatico all’indirizzo https://www.raspberrypi.com/software/operating-systems/

e portati alla sezione Raspberry Pi OS (Legacy). Scaricherai una versione che non ha ambiente grafico in modo che sia la più leggera possibile:

La distribuzione scelta
La distribuzione scelta

Il file scaricato sarà compresso in formato xz. Per decomprimerlo su Linux dovrai prima installare il tool:

sudo dnf install xz           su CentOS/RHEL/Fedora Linux.
sudo apt install xz-utils     su Ubuntu/Debian

e poi dare la riga di comando:

xz -d -v filename.xz

dove filename.xz è il nome del file che hai appena scaricato contenente il sistema operativo.

Su Windows sarà sufficiente utilizzare uno fra questi tool: 7-Zip, winRAR, WinZip.

Il risultato sarà un file con estensione img che è l’immagine da flashare sulla SD card della Raspberry.

Per flashare l’immagine sulla SD card userai il tool Balena Etcher che funziona sia su Linux che su Windows che su MACOS.

Il suo utilizzo è molto semplice: è sufficiente selezionare l’immagine da flashare, la SD card di destinazione e premere il pulsante Flash.

Ecco come appare la sua interfaccia:

L'interfaccia del tool Balena Etcher
L’interfaccia del tool Balena Etcher

A sinistra viene impostata l’immagine da flashare, al centro la SD card da flashare, a destra il pulsante per iniziare l’operazione di flashing.

Alla fine della operazione la SD card conterrà due partizioni: boot e rootfs. Nel gestore dei dispositivi su Linux appare un menu del genere:

Menu dei dispositivi su Linux
Menu dei dispositivi su Linux

Anche Windows mostrerà un menu del genere: dal tuo file explorer, alla voce Questo computer vedrai le 2 partizioni.

Ora, con un editor di testo, crea sul tuo computer un file che chiamerai wpa_supplicant.conf e che editerai in questo modo:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=«your_ISO-3166-1_two-letter_country_code»
network={
     ssid="«your_SSID»"
     psk="«your_PSK»"
     key_mgmt=WPA-PSK
}

Dovrai sostituire le seguenti voci:

  • «your_ISO-3166-1_two-letter_country_code» con l’identificativo del tuo paese (per esempio per l’Italia è IT)
  • «your_SSID» con il nome SSID della tua rete WiFi
  • «your_PSK» con la password della rete WiFi

A questo punto, dovrai creare un file vuoto che chiamerai ssh (senza nessuna estensione).

Le nuove distribuzioni non hanno il classico utente pi con password raspberry quindi, per poter entrare in SSH, dobbiamo provvedere in altro modo.

Con una Raspberry funzionante dobbiamo creare un file di nome userconf che conterrà l’utente che vogliamo creare con una versione criptata della password che gli vogliamo attribuire. Il formato sara quindi username:password-hash.

Supponiamo di voler tenere l’utente pi, dobbiamo creare la password-hash. Supponiamo di voler creare l’hash della password raspberry, sempre nella Raspberry dove abbiamo creato il file userconf. Dobbiamo dare il comando, da shell, seguente:

echo "raspberry" | openssl passwd -6 -stdin

Questo comando restituirà l’hash della password raspberry. Per esempio potrebbe essere una stringa così:

$6$ROOQWZkD7gkLyZRg$GsKVikua2e1Eiz3UNlxy1jsUFec4j9wF.CQt12mta/6ODxYJEB6xuAZzVerM3FU2XQ27.1tp9qJsqqXtXalLY.

Questo è l’hash della password raspberry che ho calcolato sulla mia Raspberry.

Il nostro file userconf quindi conterrà la seguente stringa:

pi:$6$ROOQWZkD7gkLyZRg$GsKVikua2e1Eiz3UNlxy1jsUFec4j9wF.CQt12mta/6ODxYJEB6xuAZzVerM3FU2XQ27.1tp9qJsqqXtXalLY.

NOTA BENE: è necessario calcolare l’hash con una Raspberry perché l’hash calcolato col computer utilizza un altro algoritmo che non consentirebbe alla Raspberry che stiamo preparando di riconoscere la password.

Alternativamente puoi scaricare dal link qua sotto il file userconf che ho creato io per avere utente pi con password raspberry.

Ora apri la partizione boot sulla SD card e copia dentro i tre files wpa_supplicant.conf, ssh e userconf. Rimuovi in sicurezza la SD card dal computer e inseriscila nella Raspberry.

Accendi la Raspberry, aspetta qualche minuto. Per poterti loggare in ssh alla Raspberry, dovrai scoprire quale sia il suo IP (quello che il router le ha assegnato tramite DHCP).

Per fare questo è sufficiente dare il comando da una shell del pc:

ping raspberrypi.local 

valido sia su Linux che su Windows (previa installazione di Putty su Windows).

Nel mio PC la Raspberry risponde così:

Risposta della Raspberry al ping
Risposta della Raspberry al ping

Ciò mi fa capire che l’IP assegnato è 192.168.43.27.

Alternativamente puoi usare il tool Angry IP Scanner oppure puoi accedere alle impostazioni del tuo router per vedere i dispositivi collegati via WiFi e scoprire che IP ha la Raspberry.

Per poter loggarti sulla Raspberry in ssh dai il comando da shell (ovviamente nel tuo caso l’IP sarà diverso da questo):

con password raspberry. Su Windows è necessario Putty.

Una volta dentro la Raspberry dai i seguenti comandi per fare l’update del software:

sudo apt update
sudo apt upgrade

La password è sempre raspberry.

Configuriamo la timezone

Per configurare la timezone dai il comando:

sudo raspi-config

alla shell della Raspberry. Supponiamo di voler impostare il fuso orario di Roma (io qui farò l’esempio del fuso orario di Roma dato che vivo in Italia, tu dovrai usare il fuso orario del tuo Paese).

Apparirà una schermata così:

Schermata iniziale del comando sudo raspi-config
Schermata iniziale del comando sudo raspi-config

Seleziona l’opzione sulla localizzazione e dai Ok:

Selezionata l'opzione sulla localizzazione
Selezionata l’opzione sulla localizzazione

Seleziona poi l’opzione sulla timezone e dai Ok:

Selezionata l'opzione della timezone
Selezionata l’opzione della timezone

Seleziona ora l’area geografica e dai Ok:

Selezionata l'area geografica
Selezionata l’area geografica

Infine seleziona la città e dai Ok:

Selezionata la città
Selezionata la città

Ecco fatto!

Riavvia la Raspberry dando il comando:

sudo reboot

e, dopo pochi minuti, rientra in ssh come hai fatto prima.

Dai il comando

date

La Raspberry dovrebbe ora mostrare data e ora corrette.

Impostiamo l’IP statico

Per fare in modo che la Raspberry abbia sempre lo stesso indirizzo IP, dobbiamo impostarlo in modo che sia statico. Nei miei test l’ho impostato a 192.168.1.190. Se non facessimo così, il router le assegnerebbe un IP diverso ad ogni riavvio il che ci costringerebbe ogni volta a cambiare l’indirizzo IP del server Modbus nello sketch dell’ESP32.

Procederemo in due passi:

  • imposteremo l’IP fisso nella Raspberry
  • imposteremo il router in modo che riservi quell’indirizzo alla nostra Raspberry

Per il primo punto, dai il comando:

nano /etc/dhcpcd.conf

per aprire il file dhcpcd.conf ed editarlo.

Alla fine del file dovrai aggiungere un blocco del genere:

interface [INTERFACE]
static_routers=[ROUTER IP]
static domain_name_servers=[DNS IP]
static ip_address=[STATIC IP ADDRESS YOU WANT]/24

dove:

  • [INTERFACE] è il nome dell’interfaccia WiFi (nel nostro caso sarà wlan0)
  • [ROUTER IP] è l’indirizzo del nostro router (in genere è qualcosa del tipo 192.168.0.1 oppure 192.168.1.1). Lo puoi trovare entrando nell’interfaccia di amministrazione del tuo modem/router
  • [DNS IP] è l’indirizzo del server DNS, che in genere coincide con il parametro [ROUTER IP] del modem/router
  • [STATIC IP ADDRESS YOU WANT] è l’indirizzo IP che vogliamo assegnare come IP fisso alla Raspberry

Quindi, supposto che [ROUTER IP] = [DNS IP] = 192.168.1.1 e che [STATIC IP ADDRESS YOU WANT] = 192.168.1.190, il blocco avrà un aspetto del genere:

interface wlan0
static_routers=192.168.1.1
static domain_name_servers=192.168.1.1
static ip_address=192.168.1.190/24

Riavvia la Raspberry sempre col comando

sudo reboot

e poi accedi nuovamente in ssh, questa volta con IP 192.168.1.190.

Come secondo passo imposteremo il router in modo che riservi l’indirizzo 192.168.1.190 alla nostra Raspberry. Ogni modem/router è diverso dagli altri ma più o meno si somigliano. Mostrerò qui come appare il mio.

Per entrare digito l’indirizzo 192.168.1.1 (perche il mio modem ha questo IP) sul browser e, dopo aver dato la password di amministratore, arrivo alla schermata principale. Da qui devo cercare la schermata relativa al controllo degli accessi.

Aggiunta di un IP statico per la Raspberry
Aggiunta di un IP statico per la Raspberry

Ci sarà un pulsante di aggiunta di un IP statico: aggiungi l’IP scelto abbinato al MAC address della scheda WiFi della Raspberry. Ti consiglio comunque di consultare il manuale di istruzioni del tuo modem/router per questa operazione.

Verifica ora che la Raspberry si connetta alla rete dando il comando:

ping www.google.com

Se ottieni la risposta al ping la rete è connessa. Se ottieni un messaggio del tipo “Network is unreachable” dai il comando

sudo route add default gw [ROUTER IP]  

dove [ROUTER IP] è il gateway che nel nostro caso è l’IP del router, cioè 192.168.1.1

A questo punto siamo pronti per poter installare le librerie necessarie. Iniziamo installando un tool di Python molto utile che si chiama pip dando il comando:

sudo apt-get install python3-pip

Una volta terminata l’installazione procediamo ad installare la libreria vera e propria che leggerà il DHT22 dando il comando:

sudo pip3 install Adafruit_DHT

Se vuoi subito testare il sensore per essere sicuro che tutto funzioni puoi prendere il seguente script:

import Adafruit_DHT

# Sensor configuration
DHT_SENSOR = Adafruit_DHT.DHT22
DHT_PIN = 4  # GPIO4 of Raspberry Pi

def read_dht22():
    humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)
    if humidity is not None and temperature is not None:
        return round(temperature, 2), round(humidity, 2)
    else:
        return None, None

if __name__ == "__main__":
    temp, hum = read_dht22()
    if temp is not None and hum is not None:
        print(f"Temperature: {temp}°C, Humidity: {hum}%")
    else:
        print("Error reading DHT22")

copiarlo e incollarlo in un file sulla home della tua Raspberry che potrai chiamare, per esempio, testdht.py. Per creare il file puoi dare il comando:

touch testdht.py

e poi editarlo con nano dando il comando:

nano testdht.py

Per salvare il file con nano dai il comando CTRL + O e per uscire da nano dai il comando CTRL + X.

Dando il comando sudo python testdht.py lo script dovrà stampare i valori correnti di temperatura ed umidità. Se non lo fa ricontrolla i collegamenti del sensore.

NOTA BENE: assicurati che il terminale di alimentazione del DHT22 sia collegato al pin 1 (quello della alimentazione a 3.3V) della Raspberry in modo da non danneggiare il pin 7 dove è collegata l’uscita del sensore. Nel caso il pin 7 dovesse ricevere un segnale a 5V si danneggerebbe irrimediabilmente.

Continuiamo installando la libreria per il ModBus dando il comando:

sudo pip3 install pymodbus==2.5.3

A questo punto, sempre nella home della Raspberry, crea un file dal nome modbus_server_raspberry.py e, usando nano, riempilo col seguente codice:

from pymodbus.server.sync import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
import random
import time
import logging
from threading import Thread
import struct
import Adafruit_DHT

# Sensor configuration
DHT_SENSOR = Adafruit_DHT.DHT22
DHT_PIN = 4  # GPIO4 of Raspberry Pi

logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)

def updating_writer(context):
    while True:

        humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)
        temperature = round(temperature, 2)
        humidity = round(humidity, 2)

        temp_int = struct.unpack('>I', struct.pack('>f', temperature))[0]
        temp_high = (temp_int >> 16) & 0xFFFF
        temp_low = temp_int & 0xFFFF

        humidity = int(humidity)

        context[0].setValues(3, 0, [temp_high, temp_low, humidity])
        
        log.debug(f"Updated registers with temperature: {temperature} and humidity: {humidity}")
        time.sleep(1)

store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [17]*100),
    co=ModbusSequentialDataBlock(0, [17]*100),
    hr=ModbusSequentialDataBlock(0, [17]*100),
    ir=ModbusSequentialDataBlock(0, [17]*100))
context = ModbusServerContext(slaves=store, single=True)

identity = ModbusDeviceIdentification()
identity.VendorName = 'pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl = 'http://github.com/bashwork/pymodbus/'
identity.ProductName = 'pymodbus Server'
identity.ModelName = 'pymodbus Server'
identity.MajorMinorRevision = '1.0'

updater = Thread(target=updating_writer, args=(context,))
updater.start()

try:
    StartTcpServer(context, identity=identity, address=("0.0.0.0", 502))
except Exception as e:
    log.error(f"Error starting server: {e}")

Lancialo ora col comando

sudo python3 modbus_server_raspberry.py

Dovresti avere una uscita di questo tipo:

Lo script modbus_server_raspberry.py in funzione sulla Raspberry
Lo script modbus_server_raspberry.py in funzione sulla Raspberry

NOTA: ancora non abbiamo messo in funzione lo sketch nella ESP32 quindi l’output dello script python è abbastanza semplice. Quando ci sarà in funzione anche lo sketch per la ESP32 i messaggi sulla shell della Raspberry saranno più complessi in quanto comprenderanno anche i messaggi di DEBUG della comunicazione fra i due dispositivi.

Vediamo ora come funziona lo script python.

Inizialmente vengono importate le librerie necessarie:

from pymodbus.server.sync import StartTcpServer
from pymodbus.device import ModbusDeviceIdentification
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
import random
import time
import logging
from threading import Thread
import struct
import Adafruit_DHT

Viene poi configurato il DHT22 per funzionare sul GPIO 4 e configurato il sistema di logging per il debug (i messaggi che vengono stampati dallo script sulla shell della Raspberry):

# Sensor configuration
DHT_SENSOR = Adafruit_DHT.DHT22
DHT_PIN = 4  # GPIO4 of Raspberry Pi

logging.basicConfig()
log = logging.getLogger()
log.setLevel(logging.DEBUG)

Troviamo poi la funzione updating_writer che è il centro di tutto. Il suo contenuto viene eseguito in un ciclo infinito in quanto si trova nel blocco

while True:

Inizialmente vengono letti i valori di temperatura e umidità dal sensore:

humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN)
temperature = round(temperature, 2)
humidity = round(humidity, 2)

Segue poi una elaborazione del valore della temperatura (che è un float):

temp_int = struct.unpack('>I', struct.pack('>f', temperature))[0]
temp_high = (temp_int >> 16) & 0xFFFF
temp_low = temp_int & 0xFFFF

questa porzione di codice esegue una serie di operazioni per convertire un valore float (in questo caso la temperatura) in una rappresentazione binaria a 32 bit, e poi suddivide questi 32 bit in due parti da 16 bit ciascuna. Questo è utile quando si devono trasmettere i dati su un protocollo che supporta solo registri a 16 bit, come Modbus.

Il valore della umidità viene convertito in intero per semplificare il codice in quanto, come intero, non ha bisogno di ulteriori elaborazioni:

humidity = int(humidity)

L’istruzione seguente:

context[0].setValues(3, 0, [temp_high, temp_low, humidity])

scrive i valori di temperatura e umidità in un blocco di memoria specifico del sistema Modbus. In particolare, memorizza due parti della temperatura e il valore dell’umidità nei registri di holding (registri di mantenimento), a partire dall’indirizzo 0. Questo permette ai dati di essere facilmente accessibili o trasmessi ad altri dispositivi nella rete.

Seguono una funzione di log che stampa un messaggio contenente i valori correnti di temperatura e umidità e una istruzione di temporizzazione che introduce una pausa di un secondo:

log.debug(f"Updated registers with temperature: {temperature} and humidity: {humidity}")
time.sleep(1)

Terminata la funzione arriviamo al blocco:

store = ModbusSlaveContext(
    di=ModbusSequentialDataBlock(0, [17]*100),
    co=ModbusSequentialDataBlock(0, [17]*100),
    hr=ModbusSequentialDataBlock(0, [17]*100),
    ir=ModbusSequentialDataBlock(0, [17]*100))
context = ModbusServerContext(slaves=store, single=True)

Questa porzione di codice configura il contesto Modbus per un dispositivo slave. In particolare:

  • ModbusSlaveContext definisce un’area di memoria organizzata in quattro blocchi di dati: di (Discrete Inputs), co (Coils), hr (Holding Registers), e ir (Input Registers). Ciascun blocco è inizializzato con 100 registri, tutti con il valore predefinito 17.
  • ModbusSequentialDataBlock è utilizzato per gestire questi blocchi di dati, permettendo l’accesso sequenziale ai registri.
  • context = ModbusServerContext(slaves=store, single=True) crea un contesto server che utilizza lo slave configurato sopra, indicando che il server gestisce un singolo slave.

Questo setup è essenziale per permettere al dispositivo di gestire le richieste Modbus in modo strutturato e accessibile.

La seguente parte:

identity = ModbusDeviceIdentification()
identity.VendorName = 'pymodbus'
identity.ProductCode = 'PM'
identity.VendorUrl = 'http://github.com/bashwork/pymodbus/'
identity.ProductName = 'pymodbus Server'
identity.ModelName = 'pymodbus Server'
identity.MajorMinorRevision = '1.0'

configura l’identità del dispositivo Modbus. Questi dettagli vengono utilizzati per identificare chiaramente il dispositivo in una rete Modbus, facilitando la gestione e il riconoscimento del dispositivo da parte di altri nodi nella rete.

Il codice seguente:

updater = Thread(target=updating_writer, args=(context,))
updater.start()

crea e avvia un thread separato per eseguire la funzione updating_writer in parallelo con il resto del programma per poter aggiornare i registri Modbus in modo continuo e indipendente dal resto del codice, migliorando l’efficienza e la reattività del sistema.

L’ultima parte:

try:
    StartTcpServer(context, identity=identity, address=("0.0.0.0", 502))
except Exception as e:
    log.error(f"Error starting server: {e}")

avvia un server Modbus TCP che ascolta tutte le interfacce di rete (“0.0.0.0“) sulla porta 502, utilizzando il contesto e l’identità del dispositivo precedentemente definiti. Se si verifica un errore, viene registrato un messaggio di errore che include la descrizione dell’eccezione.

Creiamo il progetto PlatformIO

Abbiamo già visto la procedura di creazione di un progetto PlatformIO nell’articolo Come creare un progetto per NodeMCU ESP8266 con PlatformIO.

Anche se si riferisce alla board ESP8266, la procedura è simile.
Semplicemente, nella scelta della piattaforma, dovrai scegliere la AZ-Delivery ESP-32 Dev Kit C V4.

Non installare nessuna delle librerie indicate nell’articolo.

Ora modifica il file platformio.ini per aggiungere queste due righe:

monitor_speed = 115200
upload_speed = 921600

in modo che il file abbia un aspetto del genere:

[env:az-delivery-devkit-v4]
platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
monitor_speed = 115200
upload_speed = 921600

e aggiungi le librerie SPI, SPIFFS, NTPClient, eModbus e WiFiManager in modo che il file abbia un aspetto del genere:

[env:az-delivery-devkit-v4]
platform = espressif32
board = az-delivery-devkit-v4
framework = arduino
monitor_speed = 115200
upload_speed = 921600
lib_deps = 
	SPI
	SPIFFS
    arduino-libraries/NTPClient
    https://github.com/eModbus/eModbus.git
    wnatth3/WiFiManager @ 2.0.16-rc.2

Puoi scaricare il progetto dal link seguente:

decomprimerlo, prendere il file main.cpp e sostituirlo al posto di quello che hai nel progetto precedentemente creato.

Poi, nel progetto che hai creato, crea una cartella di nome data allo stesso livello della cartella src e crea al suo interno un file vuoto che chiamerai logdata.txt. Questo file verrà trasferito (con una procedura che vedremo fra poco) nel file system SPIFFS interno alla board.

Come trasferire il file logdata.txt verso il file system SPIFFS

L’operazione è abbastanza semplice. È necessario aprire un nuovo terminale su PlatformIO col tasto indicato in figura:

Pulsante per aprire un nuovo terminale
Pulsante per aprire un nuovo terminale

scrivere il seguente comando:

pio run --target uploadfs

e premere il tasto INVIO. Se tutto sarà andato bene il file verrà trasferito sul file system SPIFFS. Se necessario, interrompi la visualizzazione tramite Serial Monitor in quanto potrebbe entrare in conflitto con l’operazione di caricamento dato che utilizza (e certe volte monopolizza) la porta di comunicazione.

NOTA: è importante che il file logdata.txt si trovi, come già detto all’inizio, in una cartella di nome data allo stesso livello della cartella src.

Il caricamento dello sketch, invece, segue la via normale.

Funzionamento dello sketch

Vediamo ora come funziona lo sketch.

Lo sketch inizia con l’inclusione delle librerie necessarie:

#include <Arduino.h>

#include <SPI.h>
#include <ModbusClientTCP.h>
#include <NTPClient.h>
#include <SPIFFS.h>
#include <WebServer.h>
#include <WiFiManager.h> 

Viene poi istanziato un webserver che sta in ascolto sulla porta 80 e che gestisce le API REST:

WebServer server(80);

Segue poi la riga:

#define PRINTFILE

che definisce una “macro”. Tale macro ci serve per attivare, a guisa di debug, la stampa del file presente nel file system in modo da poterlo visualizzare.

Le parti deputate a questa stampa sono racchiuse dentro dei blocchi #ifdef PRINTFILE….#endif. Se la macro PRINTFILE è definita, tali pezzi di codice vengono inclusi nello sketch e, di conseguenza, compilati assieme a tutto il resto. Se la macro PRINTFILE non è definita, come nel caso seguente:

// #define PRINTFILE

in cui è commentata, le parti nei blocchi #ifdef PRINTFILE….#endif non faranno parte dello sketch e non verranno compilate. Di conseguenza il file non verrà stampato sul Serial Monitor.

Seguono poi la variabile storageFilename che contiene il nome del file di storage, la costante maxFileSize che ne definisce le dimensioni massime e la costante logInterval che stabilisce ogni quanti millisecondi la dimensione del file debba essere verificata:

String storageFilename = "/logdata.txt";

const size_t maxFileSize = 1024; // Max size of the storage file
const unsigned long logInterval = 5000; // Storage file size check interval in milliseconds

Segue un blocco #ifdef PRINTFILE….#endif di cui abbiamo parlato prima:

#ifdef PRINTFILE
  const unsigned long printInterval = 20000; // Print interval of storage file contents in milliseconds (for debugging only)
#endif

Se la macro PRINTFILE è definita, la riga const unsigned long printInterval = 20000; viene inclusa nel file e compilata. Essa definisce la costante printInterval che stabilisce ogni quanti millisecondi debba essere stampato il file di storage sul Serial Monitor.

Seguono poi le definizioni delle variabili previousLogMillis e previousPrintMillis usate per il calcolo delle tempistiche rispettivamente del controllo delle dimensioni del file e della stampa del file stesso:

unsigned long previousLogMillis = 0;
unsigned long previousPrintMillis = 0;

In seguito vengono definiti il wifiClient che viene usato per istanziare il client Modbus MBclient, l’ntpUDP per il server NTP da cui ricaviamo data e ora e il timeClient che setta un server europeo con l’UTC per l’Italia (che tiene conto dell’ora legale perchè è in vigore al momento della scrittura di questo articolo) e l’intervallo di tempo di sincronizzazione (60000 millisecondi):

WiFiClient wifiClient;
ModbusClientTCP MBclient(wifiClient);
WiFiUDP ntpUDP;
// Sets an offset of 3600 * 2 = 7200 for daylight saving time (3600 for standard time), referring to Italy
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 7200, 60000);

Seguono poi i prototipi delle funzioni removeOldEntries, checkFileSize, printFileContent che sono implementate più sotto:

void removeOldEntries();
void checkFileSize();
void printFileContent();

Incontriamo poi la funzione più importante dello sketch, la funzione handleData:

void handleData(ModbusMessage response, uint32_t token) {
    Serial.print("Response received with token: ");
    Serial.println(token);

    if (response.getError() == SUCCESS) {
        float temperature;
        uint16_t humidity;
        uint16_t tempHigh, tempLow;

      if (response.get(3, tempHigh) && response.get(5, tempLow) && response.get(7, humidity)) {
          uint32_t tempRaw = (tempHigh << 16) | tempLow;
          temperature = *(float*)&tempRaw;

          Serial.printf("Temperature: %.2f, Humidity: %d\n", temperature, humidity);

          unsigned long epochTime = timeClient.getEpochTime();
          struct tm *ptm = gmtime((time_t *)&epochTime);

          // Formats the date and time in YYYY:MM:DD:HH:MM:SS format
          char timeBuffer[20];
          sprintf(timeBuffer, "%04d:%02d:%02d:%02d:%02d:%02d",
                  ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
                  ptm->tm_hour, ptm->tm_min, ptm->tm_sec);

          Serial.println(timeBuffer);

          File file = SPIFFS.open(storageFilename, FILE_APPEND);
          if (file) {
            file.printf("%s:%.2f:%d\n", timeBuffer, temperature, humidity);
            file.close();
          } else {
            Serial.println("Failed to open file for writing");
          }
      } else {
          Serial.println("Error retrieving values from response");
      }
  } else {
      Serial.print("Failed to read Modbus registers. Error: ");
      Serial.println(response.getError());
  }
}

Tale funzione riceve i dati dal Modbus, se la ricezione fallisce stampa un messaggio di errore altrimenti definisce le variabili usate per l’umidità (di tipo intero senza segno a 16 bit) e per la temperatura (di tipo float) che si ottiene dopo opportuna ricezione delle due parti a 16 bit che la compongono e che saranno scritte nelle due variabili tempHigh e tempLow (anche esse di tipo intero senza segno a 16 bit):

float temperature;
uint16_t humidity;
uint16_t tempHigh, tempLow;

Segue poi un blocco if che con la sua condizione e le righe seguenti decodifica i valori ricevuti dal Modbus:

if (response.get(3, tempHigh) && response.get(5, tempLow) && response.get(7, humidity)) {
          uint32_t tempRaw = (tempHigh << 16) | tempLow;
          temperature = *(float*)&tempRaw;

          Serial.printf("Temperature: %.2f, Humidity: %d\n", temperature, humidity);

Questo if verifica se i dati dai registri Modbus possono essere correttamente letti. La funzione response.get() estrae i valori dai registri specificati (3, 5 e 7) e li assegna alle variabili tempHigh, tempLow, e humidity. La condizione sarà vera solo se tutte e tre le letture vanno a buon fine.

La riga successiva combina i due valori a 16 bit, tempHigh e tempLow, in un unico valore a 32 bit. tempHigh viene spostato a sinistra di 16 bit (<< 16), mentre tempLow rimane nei 16 bit meno significativi. Il risultato è un valore a 32 bit che rappresenta la temperatura in formato binario.

Nella riga successiva il valore combinato tempRaw viene reinterpretato come un float. Questo avviene forzando il compilatore a considerare tempRaw come un puntatore a un float, quindi convertendo il valore binario di 32 bit in un numero decimale a virgola mobile (la temperatura reale).

L’ultima riga evidenziata sopra stampa i valori di temperatura e umidità sul Serial Monitor. La temperatura viene visualizzata con due cifre decimali (%.2f), mentre l’umidità, che è un intero, viene visualizzata senza decimali (%d).

Sempre nel blocco if segue la parte dedicata alla marca temporale:

unsigned long epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);

// Formats the date and time in YYYY:MM:DD:HH:MM:SS format
char timeBuffer[20];
sprintf(timeBuffer, "%04d:%02d:%02d:%02d:%02d:%02d",
        ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday,
        ptm->tm_hour, ptm->tm_min, ptm->tm_sec);

Serial.println(timeBuffer);

Questa parte crea la marca temporale in formato YYYY:MM:DD:HH:MM:SS da affiancare, nel file di storage, alle scritture di temperatura e umidità.

Segu poi la scrittura vera e propria sul file che viene aperto in modalità append. Su di esso, man mano che arrivano i dati, viene scritta una riga contenente la marca temporale e i valori di temperatura e umidità. Tutti i valori sono separati dal carattere “:” in modo da creare un file CSV:

File file = SPIFFS.open(storageFilename, FILE_APPEND);
          if (file) {
            file.printf("%s:%.2f:%d\n", timeBuffer, temperature, humidity);
            file.close();
          } else {
            Serial.println("Failed to open file for writing");
          }

Segue poi la funzione setup.

Viene subito inizializzata la porta seriale:

// Open serial communications and wait for port to open:
Serial.begin(115200);
while (!Serial) {
; // wait for serial port to connect.
}
Serial.println("Serial is ready.");

e poi viene inizializzato il file system SPIFFS:

if (!SPIFFS.begin(true)) {
Serial.println("An error has occurred while mounting SPIFFS");
return;
}
Serial.println("SPIFFS mounted successfully");


delay(1000);

In caso di errore di inizializzazione dello SPIFFS, l’esecuzione si interrompe.

Segue poi la parte che gestisce la connessione WiFi:

// WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
// it is a good practice to make sure your code sets wifi mode how you want it.


//WiFiManager, Local intialization. Once its business is done, there is no need to keep it around
WiFiManager wm;

// reset settings - wipe stored credentials for testing
// these are stored by the esp library
// wm.resetSettings();

// Automatically connect using saved credentials,
// if connection fails, it starts an access point with the specified name ( "AutoConnectAP"),
// if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
// then goes into a blocking loop awaiting configuration and will return success result

bool res;
// res = wm.autoConnect(); // auto generated AP name from chipid
res = wm.autoConnect("AutoConnectAP"); // anonymous ap
//res = wm.autoConnect("ESP32_AP","password"); // password protected ap

if(!res) {
    Serial.println("Failed to connect");
    // ESP.restart();
} 
else {
    //if you get here you have connected to the WiFi    
    Serial.println("Connected...yeey :)");
    Serial.println("My WiFi IP is: ");
    Serial.print(WiFi.localIP());
    Serial.println();
}

Come si può vedere contiene varie impostazioni relative al server WiFi per configurare prima il dispositivo come Access Point e poi, una volta inserite le credenziali di rete, per collegarlo alla nostra rete wireless. Vedremo più in dettaglio come si collega la ESP32 alla rete WiFi in un successivo paragrafo.

Segue ora la gestione delle tre API REST che sono readfile, datetime e checkfile:

// API REST readfile: reads the data stored by the file system
server.on("/readfile", HTTP_GET, []() {
    Serial.println("API REST readfile...");
    String fileContent;
    File file = SPIFFS.open(storageFilename, FILE_READ);
    if (file) {
        while (file.available()) {
            fileContent += char(file.read());
        }
        file.close();
        server.send(200, "text/plain", fileContent);
    } else {
        server.send(500, "text/plain", "Failed to open file");
    }
});

La readfile apre il file in lettura, lo legge carattere per carattere e lo restituisce in uscita al client.

// API REST datetime: returns the current system date and time in YYYY:MM:DD:HH:MM:SS
server.on("/datetime", HTTP_GET, []() {
Serial.println("API REST datetime...");
unsigned long epochTime = timeClient.getEpochTime();
struct tm *ptm = gmtime((time_t *)&epochTime);

char timeBuffer[20];
sprintf(timeBuffer, "%04d:%02d:%02d:%02d:%02d:%02d", 
        ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, 
        ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
String dateTimeString = String(timeBuffer) + "\n";
server.send(200, "text/plain", dateTimeString);
});

La datetime legge il server NTP e ricava data e ora, restituendoli al client in formato YYYY:MM:DD:HH:MM:SS.

// API REST checkfile: checks if the storage file exists
server.on("/checkfile", HTTP_GET, []() {
Serial.println("API REST checkfile...");
if (SPIFFS.exists(storageFilename)) {
    server.send(200, "text/plain", "File exists\n");
} else {
    server.send(404, "text/plain", "File not found\n");
}
});

La checkfile controlla se il file di storage è presente o no. È usata come debug in caso di malfunzionamenti dello storage.

Vien poi avviato il webserver per le API REST:

  server.begin();
  Serial.println("Web Server started");

Viene poi avviato il server Modbus:

MBclient.onDataHandler(handleData);
MBclient.setTarget(IPAddress(192, 168, 1, 190), 502);  // Sets the IP address and port of the Raspberry Pi that transmits ModBus data
MBclient.begin();
Serial.println("Modbus TCP Client started");

In esso viene impostato l’IP della Raspberry (abbiamo già visto in un paragrafo precedente come rendere fisso l’IP della Raspberry) e la porta 502, tipica del protocollo Modbus.

Viene infine avviato il timeClient:

timeClient.begin();

Segue poi la funzione loop.

Inizialmente viene richiamata la funzione del webserver che gestisce le richieste REST e definita la variabile currentMillis col valore corrente in millisecondi:

server.handleClient();

unsigned long currentMillis = millis();

Segue poi un blocco if che ogni logInterval millisecondi esegue la funzione checkFileSize:

if (currentMillis - previousLogMillis >= logInterval) {
    previousLogMillis = currentMillis;
    checkFileSize();
}

Segue poi il blocco

#ifdef PRINTFILE
    if (currentMillis - previousPrintMillis >= printInterval) {
    previousPrintMillis = currentMillis;
    printFileContent();
    }
#endif

che viene incluso e compilato, come già detto più sopra, se la macro PRINTFILE è definita e che serve a stampare il file di storage sul Serial Monitor ogni printInterval millisecondi.

Infine viene fatto l’update del timeClient e mandata una richiesta Modbus dal client (nel nostro caso l’ESP32) al dispositivo server (la nostra Raspberry) per leggere i dati contenuti nei registri di mantenimento (Holding Registers) del server:

timeClient.update();

Serial.println("Sending Modbus request...");
MBclient.addRequest(1, 1, READ_HOLD_REGISTER, 0, 3);

delay(1000);

Terminato il loop abbiamo le ultime tre funzioni.

void checkFileSize() {
  File file = SPIFFS.open(storageFilename, FILE_READ);
  if (file) {
    size_t fileSize = file.size();
    Serial.print("Current file size: ");
    Serial.println(fileSize);
    file.close();

    if (fileSize > maxFileSize) {
      removeOldEntries();
    }
  }
}

La funzione checkFileSize controlla la dimensione del file. Se essa è maggiore del valore impostato in maxFileSize, chiama la funzione removeOldEntries. La costante maxFileSize è impostata a 1024. Ovviamente questo valore può essere diminuito o aumentato (tenendo però conto del fatto che il file system del dispositivo è limitato e un valore eccessivamente grande potrebbe portare ad un blocco dello stesso). Consiglio di variare per tentativi questo valore per trovare quello ottimale ai propri scopi.

void removeOldEntries() {
  Serial.println("File too big. Resizing...");
  File file = SPIFFS.open(storageFilename, FILE_READ);
  if (file) {
    String content;
    while (file.available()) {
      content += char(file.read());
    }
    file.close();

    int linesToRemove = 10; // Number of rows to remove
    int index = 0;
    for (int i = 0; i < linesToRemove; i++) {
      index = content.indexOf('\n', index) + 1;
    }
    content = content.substring(index);

    file = SPIFFS.open(storageFilename, FILE_WRITE);
    if (file) {
      file.print(content);
      file.close();
    }
  }
}

La funzione removeOldEntries apre il file e ne rimuove le prime linesToRemove righe, cioè le più vecchie. In questo caso il valore è pari a 10 ma può essere cambiato secondo la necessità.

L’ultima funzione

void printFileContent() {
  File file = SPIFFS.open(storageFilename, FILE_READ);
  if (file) {
    Serial.println("File content:");
    while (file.available()) {
      Serial.write(file.read());
    }
    file.close();
    Serial.println();
  } else {
    Serial.println("Failed to open file for reading");
  }
}

legge il file di storage e ne stampa il contenuto sul Serial Monitor.

Una volta che abbiamo capito come funziona lo sketch non ci resta che caricarlo sulla ESP32 (senza dimenticarci di caricare il file logdata.txt con la procedura esposta sopra). Attiviamo il Serial Monitor e connettiamo la board al WiFi seguendo il prossimo paragrafo.

Come connettere la board ad Internet

Dopo aver caricato lo sketch sulla board, apri il Serial Monitor per vedere i messaggi provenienti dal dispositivo.

Per prima cosa la board va in modalità Access Point e ci fornirà un indirizzo IP che useremo a breve. Questa operazione serve per connettere la board ad Internet senza dover inserire nel codice i parametri della rete WiFi (SSID e password).

La board ci fornisce il suo indirizzo IP
La board ci fornisce il suo indirizzo IP

In questo caso l’indirizzo IP è 192.168.4.1.

A questo punto la ESP32 è in modalità Access Point (con SSID AutoConnectAP) e dobbiamo connettere il nostro computer alla rete AutoConnectAP. Se andiamo nel menu delle reti del nostro computer, dovremmo vedere anche la rete AutoConnectAP nell’elenco delle reti wireless.

Lista delle reti WiFi disponibili
Lista delle reti WiFi disponibili

Connetti il computer alla rete AutoConnectAP. Quindi vai sul tuo browser e inserisci l’IP precedentemente fornito dalla ESP32 (che in questo esempio è 192.168.4.1)

Vedrai una schermata come questa:

La schermata del browser per scegliere la rete
La schermata del browser per scegliere la rete

Clicca il bottone ConfigureWiFi. Ti mostrerà le reti disponibili:

Lista delle reti disponibili
Lista delle reti disponibili

Scegli la SSID della tua rete:

Scegli la tua rete
Scegli la tua rete

Inserisci la password della tua rete e clicca il bottone save:

Inserisci la password
Inserisci la password

La risposta della board
La risposta della board

Il modulo ESP32 conserva memorizzati i parametri di accesso anche se lo spegni, li ricorderà al riavvio e si ricollegherà automaticamente senza dover ripetere questa procedura. Solo se lo resetti rimuovendo il commento da questa riga

// wm.resetSettings();

perderà i parametri di connessione.

Fra i vari messaggi che verranno stampati sul Serial Monitor, la libreria WiFiManager ci indicherà anche quale IP le è stato assegnato dal modem WiFi.

Nota Bene: il dispositivo può memorizzare solo una rete. Se successivamente lo colleghi a un’altra rete, dimenticherà le impostazioni della rete precedente.

È importante segnarsi l’IP comunicato dalla ESP32 e che le è stato assegnato dal modem WiFi perché ci servirà per usare le API REST.

A questo punto puoi lanciare lo script python sulla Raspberry come abbiamo già visto col comando sudo python3 modbus_server_raspberry.py

L’output dello script si mostrerà molto più complesso della volta precedente in quanto contiene anche i messaggi di debug della comunicazione fra i due dispositivi:

Output dello script python
Output dello script python

Il Serial Monitor di PlatformIO ci mostrerà invece un output di questo genere:

Output dello sketch su PlatformIO
Output dello sketch su PlatformIO

Se la macro PRINTFILE è definita vedrai la stampa del file di storage sul Serial Monitor ogni printInterval millisecondi.

Possiamo vederne il funzionamento nel video seguente:

Testiamo le API REST

Una volta che la ESP32 è stata connessa alla rete WiFi ci fornirà tramite il Serial Monitor di PlatformIO il suo indirizzo IP, come visibile nella figura seguente:

Ricaviamo l'IP della board
Ricaviamo l’IP della board

In questo caso l’IP assegnato dal router alla board è 192.168.1.128. Non è assolutamente detto che nel tuo caso venga assegnato lo stesso IP (anzi, molto probabilmente sarà diverso). Tale IP ci servirà per comporre le API REST.

Come già anticipato più sopra, per interagire con la board abbiamo bisogno di un software apposito che si chiama Postman. Dopo aver installato il programma, siamo pronti ad usarlo.

Ecco come si presenta la sua schermata iniziale:

Schermata iniziale di Postman
Schermata iniziale di Postman

Nella finestra principale si trova una barra in cui dovrai inserire l’API.

Alla sinistra di questa barra c’è un menù a tendina che consente di scegliere il tipo di API (per esempio GET, POST, PUT…).

Scegli ora il tipo GET e inserisci l’API checkfile che avrà formato:

http://IP_ESP32/checkfile

Per esempio, in questo caso, essendo l’IP assegnato 192.168.1.128, l’URL dell’API sarà:

http://192.168.1.128/checkfile

Ovviamente tu dovrai mettere l’indirizzo IP assegnato alla tua ESP32.

Premi il tasto Send sulla destra.

L’API restituirà un messaggio che dirà se il file di storage è presente o no, come mostrato nella immagine seguente:

API checkfile
API checkfile

Ora proviamo l’API datetime con

http://192.168.1.128/datetime

Vediamo il risultato nell’immagine seguente:

API datetime
API datetime

Proviamo ora l’API readfile:

http://192.168.1.128/readfile
API readfile
API readfile

Il video seguente ci dà una dimostrazione dal vivo del funzionamento delle API rest:

Concludendo, questo progetto rappresenta un esempio pratico e completo di come integrare tecnologie open-source per creare un sistema di monitoraggio ambientale efficiente e automatizzato. Utilizzando componenti come il Raspberry Pi, l’ESP32, e il protocollo Modbus, siamo riusciti a costruire un sistema in grado di raccogliere dati, memorizzarli in modo sicuro, e renderli accessibili tramite API REST. La semplicità di implementazione, combinata con la flessibilità e la potenza di queste tecnologie, dimostra come sia possibile realizzare soluzioni IoT avanzate con strumenti accessibili. Questo progetto non solo illustra l’interoperabilità tra dispositivi, ma evidenzia anche l’importanza di una gestione efficiente dei dati e della loro accessibilità, caratteristiche fondamentali in qualsiasi sistema IoT moderno.

Newsletter

Se vuoi essere aggiornato sui nuovi articoli, iscriviti alla newsletter. Prima dell’iscrizione alla newsletter leggi la pagina Privacy Policy (UE)

Se ti vuoi disiscrivere dalla newsletter clicca sul link che troverai nella mail della newsletter.

Inserisci il tuo nome
Inserisci la tua email

0 0 votes
Valutazione articolo
guest
0 Commenti
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
Torna in alto