Master & Slave


(Freeware for education, third part)

"La vita, un film nella mia testa
Una pagina di versi che leggiamo
Le parole rimangono non dette
Il tempo, una corsa che non vinci mai
Guarda indietro a dove siamo stati
E getta la spugna"

Queste, se non sbaglio, erano le parole con cui iniziava la canzone dei Kiss, si intitolava Master & slave ed era il 1997! Sono passati tanti anni da allora, ma non ho mai smesso di cercare di imparare ad imparare!

Per imparare, ho capito che bisogna essere umili e saper chiedere ciò che vogliamo apprendere, alle giuste fonti: alle relazioni tra enti ed eventi.

Il vero “Maestro” è l’Universo, o il Multiverso, a seconda di come lo si voglia interpretare, l’interfaccia di comunicazione è il nostro corpo, almeno in prima approssimazione, gli strumenti attraverso i quali possiamo amplificare i nostri sensi, il protocollo è la matematica, il linguaggio che adottiamo, poiché coerente, per cercare di interpretare le relazioni tra “le cose”; noi siamo “Schiavi”, non possiamo far altro che reagire, rispondere a delle sollecitazioni, a dei messaggi, anche se lo facciamo in relazione a delle elaborazioni soggettive, come se, rispondessimo in modo personalizzato ad un messaggio comune a tutti.

Riallacciandomi ai precedenti due articoli, faccio ancora riferimento alla misura e al monitoraggio della temperatura, questa volta, adibita all’habitat, congiuntamente al valore della umidità relativa.

Ovviamente, faccio sempre riferimento alle basi riportate dal testo MMC: Misure – Monitoraggio – Controllo, sulle quali ho potuto implementare i precedenti due articoli pratici che si possono scaricare da questi siti:

https://romeoceccato.blogspot.com

https://independent.academia.edu/RomeoCeccato

Il libro di riferimento lo si può acquistare sulla piattaforma Amazon o su Lulu, in lingue diverse:

Un libro che tratta argomentazioni quali la sensoristica vegetale ed animale come base di partenza per poi svilupparsi nell’ambito dei processi di misura, monitoraggio e controllo, spaziando tra vari contesti, quali l’informatica, la fisica e la medicina, trattando inoltre, argomentazioni di cibernetica e intelligenza artificiale, con una particolare attenzione alla concettualistica matematica fondamentale inerente questo contesto.

Gli argomenti trattati dai precedenti articoli, hanno come grandezza di riferimento la temperatura, quindi il monitoraggio di questa grandezza e la trasmissione dei dati dallo strumento di misura, che funge da “Client” ad un “Server”, che esporrà i dati su una pagina web, e allo stesso tempo, potrebbe fungere da “Master”, per fornire i valori ai quali potrebbe far riferimento uno “Slave” adibito al processo di “Controllo”: lo slave, se tutto va bene, ubbidisce ai comandi impartiti dal master, attuando la sua “volontà” operativa.

In altre parole, l’architettura si basa su un modello informatico denominato master-slave dove un dispositivo “master” controlla uno o più dispositivi/processi “slave”.

Il master gestisce l’accesso alle risorse condivise e invia comandi, (tramite un BUS), mentre gli slave rispondono esclusivamente alle richieste del master, solitamente, risultando passivi… (e ubbidienti).

L’Architettura informatica

In questo tutorial didattico, l’architettura è composta da:

Un client che invia i dati di temperatura ed umidità relativa, ad intervalli regolari.

Un server, (server/master) che espone i valori del client ed altri parametri calcolati in relazione a questi dati, attraverso una interfaccia web.

Un master (master/server), che invia ad uno (o più dispositivi) slave, i parametri inerenti un attuatore.

Uno (o più dispositivi) slave, che gestiscono degli attuatori relativi alla tipologia di controllo implementata dal maser.

L’architettura proposta integra tre nodi principali con diverse interfacce fisiche e protocolli di comunicazione, coordinati da un’unità centrale che funge da Master e Server.

Descrizione dei Componenti e Topologia

L’architettura è strutturata come un sistema ibrido in cui il Raspberry Pi centrale agisce da ponte (gateway) tra una rete wireless e un bus seriale locale.

Il Client (Raspberry Pi Pico W): invia richieste o dati telemetrici tramite il modulo integrato CYW43439. Utilizza lo stack TCP/IP per comunicare via Wi-Fi.

Il Master/Server (Raspberry Pi o un PC): gestisce la logica di controllo. Riceve i dati dal client (funzionando come Server, ad esempio tramite Socket o MQTT) e interroga lo slave tramite protocollo Modbus RTU.

Lo Slave (Raspberry Pi Pico): connesso via USB, emula una porta seriale (CDC – Communication Device Class). Implementa la logica di uno “slave Modbus”, rispondendo alle interrogazioni del Master previa verifica dell’integrità dei dati.

Protocollo di Comunicazione e Integrità dei Dati

La comunicazione tra il Master e lo Slave Pico avviene simulando un sistema Modbus-RTU.

In questo contesto, ogni pacchetto (ADU – Application Data Unit) è composto da:

  • Indirizzo dello Slave (1 byte)
  • Codice Funzione (1 byte)
  • Dati (n byte)
  • CRC16 (2 byte)

Il CRC16 (Cyclic Redundancy Check) è fondamentale per rilevare errori di trasmissione sul cavo USB/Seriale.

Il polinomio standard utilizzato per Modbus è 0xA001 (rappresentazione riflessa di 0x8005).

Il calcolo del CRC segue il pseudo-codice per algoritmo iterativo:

	CRCinit = 0xFFFF
per ogni Byte:
		
	per i = 0 fino a i = 7
		se il bit meno significativo è 1 allora:
			
		altrimenti:
			

Flusso dei Dati (Data Flow)

Il processo di interazione segue una sequenza deterministica:

Fase 1 (Wireless): Il Pico W (Client) stabilisce una connessione con l’indirizzo IP del Raspberry Pi sulla porta dedicata.

Fase 2 (Elaborazione): Il Raspberry Pi riceve il pacchetto, valida la richiesta elabora le variabili da inviare al dispositivo slave e incapsula il comando nel formato Modbus RTU.

Fase 3 (Seriale): Il comando viene inviato tramite /dev/ttyACM0 (USB) allo Slave Pico.

Fase 4 (Validazione): Il Pico Slave riceve il frame, calcola il proprio CRC16 e lo confronta con quello ricevuto:

Se CRCcalc = CRCrec, lo Slave esegue il comando e risponde.

Se CRCcalcCRCrec, il pacchetto viene scartato per errore di integrità.

I componenti hardware

Il Client

Questo è un dispositivo costituito da un Raspberry pi pico-W al quale è stato connesso un sensore tipo DHT11 che è in grado di rilevare la temperatura e l’umidità relativa dell’ambiente circostante.

Il Raspberry pi pico-W:

Il Raspberry pi pico-w è un “Microcontrollore” della famiglia Raspberry.

Si potrebbe definirlo come un piccolo computer, su un singolo chip che integra al suo interno una CPU, memoria (RAM e ROM/Flash) e varie periferiche di ingresso/uscita (I/O), disponendo inoltre di una porta per il collegamento WIFI.

Per inciso, è giusto specificare che la differenza principale tra un microcontrollore (MPU, Micro Processor Unit) ed un microprocessore (CPU) consiste nel fatto che l’unità di elaborazione “CPU” (il microprocessore o Central Processing Unit)) richiede componenti esterni (memoria, I/O), mentre un microcontrollore è un chip singolo che integra CPU, memoria (RAM e ROM) e periferiche di input/output.

Questo rende i microcontrollori ideali per applicazioni embedded e a basso consumo come elettrodomestici, mentre i microprocessori sono usati per compiti generici e ad alte prestazioni come nei computer.

Questo è il suo layout:

I componenti del Raspberry pi pico – w si possono identificare nel seguente elenco:

RP2040 microcontroller chip designed by Raspberry Pi in the United Kingdom

  • Dual-core Arm Cortex M0+ processor, flexible clock running up to 133 MHz
  • 264KB of SRAM, and 2MB of on-board flash memory
  • USB 1.1 with device and host support
  • Low-power sleep and dormant modes
  • Drag-and-drop programming using mass storage over USB
  • 26 × multi-function GPIO pins
  • 2 × SPI, 2 × I2C, 2 × UART, 3 × 12-bit ADC, 16 × controllable PWM channels
  • Accurate clock and timer on-chip
  • Temperature sensor
  • Accelerated floating-point libraries on-chip
  • 8 × Programmable I/O (PIO) state machines for custom peripheral support
  • Wireless (802.11n), single-band (2.4 GHz)
  • WPA3
  • Soft access point supporting up to four clients
  • Bluetooth 5.2

I dispositivi Raspberry Pi Pico serie W supportano i linguaggi di programmazione C/C++ o MicroPython.

Sui dispositivi è presente il pulsante BOOTSEL che serve a mettere l’apparecchiatura in modalità bootloader, ovvero una modalità di archiviazione di massa, quando collegato a un computer tramite USB.

In questa modalità, il Pico appare come una chiavetta USB chiamata “RPI-RP2”, consentendo di trascinare e rilasciare facilmente il firmware o il codice “uf2” per aggiornarlo o programmarlo.

Per inciso, si specifica che esiste anche una versione della scheda Raspberry Pi Pico –W, il Pico WH che viene fornito con i pin header maschio già saldati, entrambi integrano il chip Infineon CYW43439 per la connettività Wi-Fi e Bluetooth.

Il sensore di temperatura e umidità DHT11

Questo è un sensore che fornisce in output un segnale digitale proporzionale alla temperatura e all’umidità misurata dal sensore stesso.

La tecnologia con cui è realizzato questo sensore assicura un’elevata affidabilità ed un’eccellente stabilità a lungo termine nonché tempi di reazione molto rapidi.

Ogni modulo DHT11 è accuratamente calibrato in laboratorio.


Le sue caratteristiche principali sono:

Il coefficiente di calibrazione è memorizzato in una memoria OTPinterna e tale valore è utilizzato durante il processo di acquisizione.

L’interfaccia seriale a singolo filo rende semplice e veloce l’integrazione del sensore in sistemi digitali.

Questo modulo può rilevare la temperatura in un range che va da 0°C fino a 50°C e permette di costruire un sistema di monitoraggio di temperatura ed umidità con costi contenuti.

Il sensore utilizza una tecnica digitale esclusiva che unita alla tecnologia di rilevamento dell’umidità, ne garantisce l’affidabilità e la stabilità.

I suoi elementi sensibili sono connessi con un processore 8-bit single-chip.

Schema di collegamento

  • Pin e alimentazione:
  • La tensione di alimentazione deve essere compresa tra 3-5.5V DC. Un condensatore da 100nF può essere inserito tra tra VDD e GND per il filtraggio dell’alimentazione.
  • Comunicazione e segnale: Dati single-bus viene utilizzato per la comunicazione tra MCU e di DHT11.

Trasmissione dati:

La comunicazione dei dati del DHT11 si basa su un protocollo digitale proprietario a filo singolo (single-bus) che richiede una sola linea dati (oltre a VCC e GND) per inviare 40 bitdi informazione (umidità, temperatura e checksum)(Single-Wire Two-Way):

  • Il Segnale di Start: Il microcontrollore (MCU) porta la linea dati a livello basso (GND) per almeno per svegliare il sensore, poi la porta in alto per 20-40 micro-secondi.
  • Risposta del sensore: Il DHT11 risponde portando la linea a livello basso per 80 micro-secondi, seguito da un livello alto per altri 80 micro-secondi.

Trasmissione dati (40 bit): Il sensore invia 5 byte (40 bit) serialmente:

  1. 8 bit: Umidità intera
  2. 8 bit: Umidità decimale (solitamente 0 su DHT11)
  3. 8 bit: Temperatura intera
  4. 8 bit: Temperatura decimale (solitamente 0 su DHT11)
  5. 8 bit: Checksum (somma dei 4 byte precedenti per verifica).

Formato dei bit:

  • Bit “0”: 50μs livello basso + 26-28μs livello alto.
  • Bit “1”: 50μs livello basso + 70μs livello alto.

Una volta trasmessi i 40 bit, la linea torna in alto e il sensore torna in modalità low-power.

È necessario attendere almeno 1 secondo tra due letture consecutive per garantire la stabilità.

Tipologia dei dati
L’umidità relativa (%RH è la misura percentuale del vapore acqueo presente nell’aria rispetto alla quantità massima che l’aria potrebbe trattenere alla stessa temperatura e pressione.
In altre parole, indica quanto l’aria è vicina alla saturazione (100% %RH).
specifiche:
• Dipendenza dalla temperatura: L’aria calda può trattenere più vapore acqueo dell’aria fredda. Di conseguenza, a parità di acqua contenuta, se la temperatura aumenta, l’umidità relativa diminuisce; se la temperatura diminuisce, l’umidità relativa aumenta.
• Significato del 100%: Quando l’umidità relativa raggiunge il 100%, l’aria è satura e si formano nebbia, rugiada o precipitazioni.
• Misurazione: Si calcola come rapporto percentuale tra la pressione parziale del vapor d’acqua e la pressione di vapor saturo.
• Differenza con l’umidità assoluta: Mentre l’umidità relativa varia con la temperatura, l’umidità assoluta indica la quantità effettiva di vapore in grammi per metro cubo d’aria, indipendentemente dalla temperatura.

Esempio pratico:
In inverno, l’aria esterna a 0°C con nebbia (100% u.r.) contiene meno acqua di quanta ne contenga la stessa aria riscaldata a 22°C in casa, che presenterà una bassa umidità relativa (es. 23%)
parametri di comfort:
Per garantire il comfort e la salute in ambienti interni, l’umidità relativa dovrebbe solitamente attestarsi tra il 40% e il 60%.
• Troppo alta (>80%): Aumenta la percezione del caldo o del freddo e favorisce la formazione di muffe.
• Troppo bassa (<30-40%): Può causare secchezza delle vie respiratorie e della pelle.

Il comfort termico ideale in casa si ottiene generalmente con una temperatura tra 19°C e 21°C in inverno e circa 26°C in estate, mantenendo l’umidità tra il 40% e il 60%.
Il benessere percepito, comunque, dipende non solo dalla temperatura dell’aria, ma anche dall’irraggiamento delle pareti, la velocità dell’aria e l’isolamento termico.
Ecco i dettagli fondamentali per temperatura e comfort:
Temperature ideali per stanza:
• Soggiorno/Cucina: 19-20°C, poiché sono aree attive e con produzione di calore.
• Camere da letto: 16-18°C per favorire la qualità del sonno.
• Bagno: 20-22°C, ideale per il comfort quando si è svestiti.
Cosa si intende come differenza tra temperatura e comfort percepito: la temperatura reale è quella sul termometro, mentre il comfort percepito include l’effetto delle pareti fredde (irraggiamento) o umidità elevata.
Se le pareti sono fredde, si sentirà freddo anche con 20°C.
Fattori chiave per il benessere:
• Umidità: L’umidità relativa ideale è tra 40% e 60%.
• Velocità dell’aria: Deve essere inferiore a 2m/s .
• Abbigliamento: Vestiti più pesanti possono far percepire fino a 4°C in più.
Consigli per il risparmio energetico: ridurre la temperatura interna (specialmente di notte) e mantenere un buon isolamento aiuta a ridurre la differenza con l’esterno, abbattendo i consumi del 6-7% per ogni grado in meno sopra i 21°C.
Il Raspberry pi pico-W, in questo caso, viene utilizzato ad esempio per simulare l’interfaccia fisica di un umano (o animale domestico), inerito in un ambiente.


Il Grafico del comfort

Il grafico del comfort termo-igrometrico ideale mostra una zona benessere con temperatura tra 20 e 24 °C in estate , mentre, in inverno, tra 20 e 22°C e umidità relativa tra 40% e 60%.
Livelli fuori da questo range (troppo secchi o umidi) compromettono la salute e il comfort, richiedendo ventilazione o deumidificazione.
Nel corso degli anni sono stati sviluppati diversi indici bioclimatici (per l’uomo come per gli animali) per esprimere il livello di disagio causato da condizioni meteoclimatiche sfavorevoli.
Il livello di stress termico degli animali, ad esempio, può essere valutato mediante l’utilizzo dell’indice Temperature Humidity Index – THI che permette di valutare la temperatura ambientale percepita in relazione ai valori dell’umidità relativa dell’aria (NOAA, 1976).
Lo stress dipende sia dall’entità del superamento del valore critico superiore del THI (variabile in relazione all’età, allarazza, etc.) sia dalla durata temporale di tale superamento.
Altri elementi di criticità sono rappresentati dalle modalità di passaggio dalla condizione di termo-neutralità a quella di caldo eccessivo e dalla possibilità di recupero che viene offerta agli animali dai valori del THI registrati nelle ore più fresche della giornata (ore notturne).
Di seguito riporto la formula più comunemente utilizzata per calcolare il THI (NOAA, 1976):
THI = [(1,8 x Ta) + 32] – (0.55 – 0.55 x Ur ) x [(1,8 x Ta) – 26]
dove
Ta = temperatura dell’aria (°C)
Ur = umidità relativa (%)

Ovvio che quelle che sono le condizioni di comfort per l’essere umano, magari non sono quelle ideali per altri tipi di animale. (p.i. si intende animale = essere dotato di anima).


Lo Slave

Questo è un dispositivo costituito da un Raspberry pi pico al quale è stato connesso un piccolo servomotore digitale 180/360 gradi servomotore tipo SG90 9G.


L’attuatore: servomotore SG90 9G

Una apparecchiatura, che in questo caso ci viene utile per simulare un dispositivo di apertura e chiusura di finestrature tipo flapper (o Hopper), è caratterizzato da dimensioni molto ridotte, pur conservando ottime performance di potenza, caratteristica che lo rende l’attuatore perfetto per piccoli robot e modelli dinamici.
Le sue caratteristiche si possono riassumere in questi dati:
• Peso: 9 grammi,
• alimentazione: da 3.3Vdc~ 6Vdc,
• coppia di torsione a 4,8V: 1,2 kg•cm,
• coppia di torsione a 6V: 1,5kg,
• rotazione: 160°,
• velocità a 4,8V: 0,12 sec/60°,
• velocità a 6V: 0,11 sec/60°,
• dimensioni (mm): 22,5x12x29.


La posizione di 0 gradi equivale a quella di 270 gradi, che si ottiene in base alla modulazione del “Duty Cycle”: Mappa 0 – 270 gradi a circa 0,5 ms (500000 ns) – 2,5 ms (250000 ns)

I componenti chiave del servomotore sono: un motore DC, un sistema di ingranaggi, un potenziometro (per il feedback di posizione) e una scheda di controllo integrata.

Feedback di Posizione: il potenziometro collegato all’albero invia costantemente la posizione attuale alla scheda interna, che regola il motore DC per raggiungere e mantenere l’angolo desiderato.


Controlli tipo PWM

(tratto dal libro MMC: Misure Monitoraggio Controllo)

I controlli tipo PWM, ovvero, controlli basati sulla modulazione di larghezza d’impulso sono dovuti ad una tecnica di controllo digitale nata per sostituire la regolazione di tensione e di corrente attraverso l’uso di reostati o potenziometri.

Fondamentalmente, la modulazione PWM, è dovuta ad un segnale in grado di regolare la tensione in uscita di un dispositivo di comando, a partire da una sorgente in corrente continua consentendo di limitare la potenza dissipata dal sistema elettrico.

Il funzionamento di un circuito PWM funziona come un treno di impulsi ad onda quadra (ON/OFF) ad alta velocità, generando un valore che dipende dal duty cycle, del segnale di comando, rispetto alla piena capacità del sistema.

La tecnologia PWM può essere applicata alla regolazione di sistemi elettrici con varie tipologie e funzionalità, a partire dagli alimentatori driver LED fino ai motori, alle valvole e alle pompe idrauliche.

Una applicazione molto frequente è quella utilizzata dagli Alimentatori switching: la modulazione PWM, in questo caso, è utilizzata per la regolazione degli alimentatori senza perdite di potenza, aumentando quindi il livello di efficienza.

Negli alimentatori switching il segnale PWM viene regolato in funzione della tensione in uscita, inducendo una retroazione di stabilizzazione al variare della corrente in ingresso.

In questo caso il piccolo servomotore (servocomando), simula il grado di apertura del sistema flapper, in relazione all’umidità relativa, rilevata dal client e dalla elaborazione del dato dovuta all’algoritmo del master.

Per permettere ad un servo motore il mantenimento della posizione, si deve inviare un impulso relativo alla posizione voluta e ripetere l’invio dell’impulso in maniera continuativa.

Normalmente si consiglia di ripetere il comando ad una frequenza compresa, tra i 50Hz ed i 100Hz, ovvero con periodo compreso tra i 10ms ed 20ms.

L’impulso positivo del segnale, in genere, deve avere una larghezza dell’ordine dei millisecondi, tale da permettere il posizionamento da un minimo angolare di 0 , ad un massimo, solitamente diπradianti.

Se, ad esempio la posizione di πradianti si ottiene con 1ms per dare la giusta coppia al motore e con 2ms, si ottiene una posizione stabile a 0 radianti, allora, la posizione centrale verrà ottenuta con la larghezza di 1.5ms

Quanto sopra è riportato dal seguente grafico:

Nel libro MMC: Misure – Monitoraggio – Controllo, è dedicato una apposito capitolo a questa particolare tecnica di comando:
Controlli tipo PWM 415
La generazione del Duty cycle 415
Sezione controllo TTL 417
Sezione inversione di marcia 418
Mini shield PWM 420
Comando per servomotore 421


Il Raspeberry pi pico

Il Raspberry Pi Pico è una scheda di sviluppo microcontrollore compatta ed economica basata sul chip RP2040, progettato dalla Raspberry Pi Foundation.

Dispone di un processore ARM Cortex-M0+ dual-core fino a 133 MHz, 264 KB di SRAM, 2 MB di memoria Flash, 26 pin GPIO multifunzione e supporto per C/C++ e MicroPython.

Questo video offre una panoramica delle caratteristiche principali del Raspberry Pi Pico:

  • Microcontrollore: RP2040 dual-core ARM Cortex M0+ fino a 133 MHz.
  • Memoria: 264 KB SRAM e 2 MB di memoria Flash QSPI integrata.
  • GPIO: 26 pin multifunzione, inclusi 3 ingressi analogici ADC a 12-bit.
  • Interfacce: 2×UART, 2×I2C, 2×SPI, 16 canali PWM.
  • USB: Supporto host e dispositivo USB 1.1.
  • Alimentazione: 5V via micro USB o 2-5V tramite pin VSYS.
  • Dimensioni: 21 x 51 mm, con pin saldabili direttamente (castellated)

ingressi analogici

Il Raspberry Pi Pico è il primo Raspberry Pi che ha ingressi analogici dalla fabbrica.

Viene fornito con un totale di 5 ingressi ADC (convertitore analogico digitale). Due dei quali sono utilizzati dal Pico per il suo sensore di temperatura interno e per il monitoraggio della tensione.

Quindi, come può essere utile avere un ingresso analogico? Beh, per esempio, uno dei miei progetti utilizzava un potenziometro per regolare la frequenza di commutazione di un relè. Così, il Raspberry Pi Pico sarebbe in grado di leggere un valore dal potenziometro e utilizzare i suoi valori nel programma che ho caricato su di esso.

Il Raspberry Pi Pico risolve i segnali ADC a 12 bit.

Altre caratteristiche includono:

  • SAR ADC (ADC ad approssimazione successiva)
  • 500 kS/s (con clock esterno a 48MHz)
  • Interfaccia DMA sugli ingressi ADC che può accedere alla memoria senza utilizzare la CPU.

IO programmabile (PIO)

Questa è una delle caratteristiche più interessanti del Raspberry Pi Pico ed è anche ciò che dà al Pico il dinamismo che alcuni altri microcontrollori non hanno.

PIO è un’interfaccia hardware che può essere programmata indipendentemente dai processori principali. Può quindi emulare molte interfacce diverse:

  • Porta parallela 8080 e 6800
  • I2C
  • 3 pin I2S
  • SDIO (interfaccia scheda SD)
  • SPI, DSPI, QSPI
  • UART
  • DPI o VGA (tramite rete di resistenze / DAC)

Le PIO State Machines sono completamente programmabili e dedicate esclusivamente all’I/O. Particolare attenzione è data alla tempistica precisa.

Per esempio, l’interfaccia PIO può essere utilizzata per impostare applicazioni time-critical (sensibili ai tempi), in modo stabile e affidabile, come quella richiesta in questo caso, dove questa apparecchiatura deve simulare un controllore collegato mediante protocollo Modbus.

Schema di collegamento

Il collegamento tra il servocomando ed il Raspberry pi pico è di semplice attuazione, come illustrato in figura:


Il Pinout del Raspberry pi pico:

Il Master/Server

Per questo scopo didattico, il dispositivo che potrebbe fungere da master e anche da server, potrebbe essere il PC, oppure, un Raspberry pi, ad esempio un pi4 model B.

Il Raspberry Pi 4 Model B è un mini-computer potente dotato di processore quad-core Broadcom BCM2711 (Cortex-A72) a 64-bit da 1.5GHz o 1.8GHz, RAM LPDDR4 da 1GB, 2GB, 4GB o 8GB, Gigabit Ethernet, Wi-Fi dual-band, Bluetooth 5.0, due porte USB 3.0, due porte micro-HDMI per il supporto a due monitor 4K e connettore USB-C per l’alimentazione.

Ecco i dettagli tecnici principali:

  • Processore: Broadcom BCM2711, Quad-core Cortex-A72 (ARM v8) 64-bit SoC @ 1.5GHz (o 1.8GHz nelle revisioni più recenti).
  • Memoria RAM: 1GB, 2GB, 4GB o 8GB LPDDR4-3200 SDRAM.
  • Connettività:
  • Wi-Fi 802.11ac dual-band (2.4GHz / 5.0GHz).
  • Bluetooth 5.0, BLE.
  • Gigabit Ethernet (cablata).
  • Porte I/O:
  • 2 porte USB 3.0 (super-speed).
  • 2 porte USB 2.0.
  • 2 porte micro-HDMI (supporto 4Kp60 o 2x 4Kp30).
  • 1 connettore GPIO a 40 pin (retrocompatibile).
  • 1 porta MIPI CSI per fotocamera.
  • 1 porta MIPI DSI per display.
  • Jack audio/composito da 3.5mm.
  • Video: VideoCore VI GPU (supporto OpenGL ES 3.0, 3.1, Vulkan 1.0).
  • Archiviazione: Slot per Micro-SD per il sistema operativo e dati.
  • Alimentazione: 5V DC via USB-C (minimo 3A), 5V DC via GPIO header.
  • Supporto PoE: Disponibile tramite PoE HAT separato.
  • Temperatura di funzionamento: 0 – 50°C.

Il Raspberry Pi 4 è nettamente più performante delle versioni precedenti, offrendo una velocità della CPU circa tre volte superiore rispetto al 3B+ e capacità multimediali avanzate.

Questo dispositivo verrà collegato allo slave mediate porta USB, mentre sarà in collegamento con il client, mediante WIFI, pertanto svolgerà contemporaneamente sia la funzione server verso il dispositivo client, che la funzione master, verso il dispositivo slave!

La seguente immagine riassume il caso:

Nel caso si utilizzasse un Raspberry come master/server, sarà comunque utile disporre di un PC, per gestire via VNC la configurazione della rete.

Il VNC (Virtual Network Computing) è un sistema open-source di condivisione desktop per il controllo remoto, basato sul protocollo RFB.

Questa tecnica, consente di visualizzare e interagire con l’interfaccia grafica di un computer (Server) da un altro dispositivo (Viewer/Client) tramite rete, ideale per assistenza tecnica o lavoro da remoto.

Il software

Alla base di questi tre progetti a scopo didattico c’è il linguaggio di programmazione Python, anche nella variante Micropython, utilizzata sia per il client che per lo slave realizzati con le versioni Raspberry pi pico-W e pico.


MicroPython


MicroPython è un’implementazione snella ed efficiente del linguaggio di programmazione Python3, che include un piccolo sottoinsieme della libreria standard Python ed è ottimizzata per l’esecuzione su microcontrollori e in ambienti con vincoli (tecnologia embedded).

MicroPython è ricco di funzionalità avanzate come prompt interattivo, interi a precisione arbitraria, chiusure, comprensione di lista, generatori, gestione delle eccezioni e molto altro.

Comunque, risulta sufficientemente compatto da adattarsi ed essere eseguito in soli 256 kB di spazio di codice e 16 kB di RAM.

MicroPython è scritto in C99 e l’intero core di MicroPython è disponibile per l’uso generale con la licenza MIT, molto liberale.

La maggior parte delle librerie e dei moduli di estensione (alcuni dei quali di terze parti) sono disponibili anche con licenza MIT o simili.

È possibile utilizzare e adattare liberamente MicroPython per uso personale, didattico e in prodotti commerciali.

MicroPython è sviluppato in modalità open source su GitHub e il codice sorgente è disponibile a tutti.

MicroPython impiega numerose tecniche di programmazione avanzate e numerosi accorgimenti per mantenere dimensioni compatte pur offrendo un set completo di funzionalità.

Alcuni degli elementi più importanti sono:

  • Altamente configurabile grazie alle numerose opzioni di configurazione in fase di compilazione
  • Supporto per numerose architetture (x86, x86-64, ARM, ARM Thumb, Xtensa)
  • Ampia suite di test con oltre 590 test e più di 18.500 casi di test individuali
  • Copertura del codice al 99,2% per il core e al 98,5% per il core più i moduli estesi
  • Tempo di avvio rapido dall’avvio al caricamento del primo script (150 microsecondi per arrivare a boot.py, su PYBv1.1 a 168 MHz)
  • Un garbage collector mark-sweep semplice, veloce e robusto per la memoria heap
  • Un’eccezione MemoryError viene generata se l’heap è esaurito
  • Un’eccezione RuntimeError viene generata se viene raggiunto il limite dello stack
  • Supporto per l’esecuzione di codice Python su un hard interrupt con latenza minima
  • Gli errori hanno un backtrace e riportano il numero di riga del codice sorgente
  • Ripiegamento delle costanti nel parser/compilatore
  • Puntatore Tagging per adattare piccoli interi, stringhe e oggetti in una parola macchina
  • Transizione trasparente da piccoli interi a grandi interi
  • Supporto per il modello a oggetti di boxing NaN a 64 bit
  • Supporto per float pieni a 30 bit, che non richiedono memoria heap
  • Un cross-compilatore e bytecode congelato, per avere script precompilati che non occupano RAM (ad eccezione degli oggetti dinamici che creano)
  • Multithreading tramite il modulo “_thread”, con un global-interpreter-lock opzionale (ancora in fase di sviluppo, disponibile solo su porte selezionate)
  • Un emettitore nativo che punta direttamente al codice macchina anziché alla macchina virtuale del bytecode
  • Assembler inline (attualmente solo set di istruzioni Thumb e Xtensa)

Installazione di MicroPython:

Come installare MicroPython su Raspberry Pi Pico e Pico-W:

  1. Scaricare il firmware: Scaricare il file .uf2 dal sito ufficiale MicroPython o dal sito Raspberry Pi. (Scegliere la verisone corretta per il tipo di Raspberry Pi Pico o Pico-W)
  2. Installazione: Tenere premuto il tasto BOOTSEL sul Pico, collegarlo al computer via USB e rilasciare il tasto. La scheda apparirà come un’unità di memoria (“RPI-RP2”).
  3. Caricamento: Trascinare il file .uf2 scaricato all’interno dell’unità RPI-RP2. Il Pico si riavvierà automaticamente con MicroPython.


Nota: MicroPython su Pico è ideale per chi cerca una programmazione più semplice rispetto al C/C++ per l’elettronica.

Il software del Client

Il Client è realizzato con la versione Pico-W, che trasmette i dati di temperatura ambientale e umidità relativa, via WIFI, questo è il listato:

import network
import urequests
import utime
import time
from machine import Pin
import dht
# --- CONFIGURATION ---
SSID = 'ssid'
PASSWORD = 'password'
SERVER_URL = 'http://10.100.100.100:5000/upload' # Replace with your RPi 4 IP address
DHT_PIN = 0 # GPIO pin connected to DHT11
# --- SETUP ---
wlan = network.WLAN(network.STA_IF)
sensor = dht.DHT11(Pin(DHT_PIN))
def blink(counter, on_off):
    led = Pin("LED", Pin.OUT)
    for _ in range(counter):
        led.value(1)
        utime.sleep_ms(on_off)
        led.value(0)
        utime.sleep_ms(on_off)
def connect_wifi():
    """Connect to the WiFi network."""
    wlan.active(True)
    wlan.connect(SSID, PASSWORD)
    print('Connecting to WiFi...')
    max_wait = 10
    while max_wait > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        max_wait -= 1
        print('waiting for connection...')
        utime.sleep(1)
    if wlan.status() != 3:
        raise RunutimeError('Network connection failed')
    else:
        print('Connected')
        status = wlan.ifconfig()
        print('IP = ' + status[0])
def send_data(temp, hum):
    """Send temperature and humidity to the server."""
    data = {'temperature': temp, 'humidity': hum}
    try:
        response = urequests.post(SERVER_URL, json=data)
        print('Data sent:', response.text)
        response.close()
    except Exception as e:
        print('Failed to send data:', e)


# --- MAIN LOOP ---
try:
    connect_wifi()
    while True:
        try:
            print('Reading sensor...')
            sensor.measure()
            temp = sensor.temperature()
            hum = sensor.humidity()          
            print(f'Temperature: {temp}°C, Humidity: {hum}%')
            blink(3,250)
            send_data(temp, hum)        
        except OSError as e:
            print('Failed to read sensor.')
        # Wait for 300 seconds (5 minutes)
        print('Waiting 300 seconds...')
        time.sleep(300)
except Exception as e:
    print('An error occurred:', e)

Nota: La funzione blink è interessante perché segnala il corretto funzionamento, ovvero, la trasmissione dei dati quando il pico-w non è collegato alla porta del PC, (o del Raspberry) in fase di test. (Il tempo di attesa può essere scelto in base alle esigenze).

Ulteriori approfondimenti si possono ricercare nei precedenti articoli di questa serie.

Il software dello Slave

Il dispositivo adibito a Slave, è un Raspberry pi pico, quello senza interfaccia WIFI, che dovrà quindi essere collegato al PC o ad un Raspberry Pi 4B, ad esempio, mediante la porta seriale USB.

La particolarità di questa connessione è quella di essere stata implementata per emulare un dispositivo di rete collegato mediante protocollo Modbus e di avere un controllo di ridondanza ciclico (CRC).

Questo algoritmo, genera un valore numerico, basato sul contenuto (binario) del dato, che viene aggiunto al file che sarà inviato.

Alla ricezione, viene ricalcolato il contenuto composto dal dato e dall’elemento “CRC” calcolato prima della trasmissione, se il calcolo rileva una difformità con l’originale, il dato risultarà corrotto.

Protocollo Modbus

Il protocollo Modbus è uno dei “nonni” dell’automazione industriale, sarà per questo che mi è simpatico, visto che è nato nel lontano 1979, ma io sono più vecchio… è stato sviluppato da Modicon, oggi Schneider Electric, ma possiamo dire che il vecchietto è ancora il linguaggio più diffuso per far comunicare dispositivi elettronici embedded.

La sua forza sta nella semplicità: è aperto, gratuito e relativamente facile da implementare.

In origine, Modbus utilizzava una struttura Master-Slave (collegamenti in rete cablata).

Nella terminologia moderna, con l’avvento di internet (specialmente per Modbus TCP), si parla di Client-Server:

  • Client (Master): il dispositivo che richiede le informazioni (es. un PLC o un software SCADA).
  • Server (Slave): il dispositivo che fornisce i dati o esegue i comandi (es. un sensore di temperatura, un inverter o un contatore di energia).

Il Client invia una richiesta e il Server risponde.

In una rete standard, i Server non parlano mai tra di loro e non iniziano mai una conversazione senza essere interrogati.

Si noti però che in questo caso specifico il Client non è un Master ed il Server non è uno Slave.

Esistono tre varianti principali del protocollo:

VarianteSupporto FisicoCaratteristiche
Modbus RTUSeriale (RS-485 o RS-232)Trasmissione binaria compatta. È la più comune in ambito industriale seriale.
Modbus ASCIISerialeUtilizza caratteri leggibili. Più lento, ma utile se il collegamento è instabile.
Modbus TCP/IPEthernetIncapsula i pacchetti Modbus all’interno di una rete TCP. Molto veloce e moderno.

Il Modello dei Dati

Modbus organizza le informazioni in quattro tabelle principali, chiamate “Registri”. Immaginale come delle caselle postali numerate da 1 a 65.535:

  1. Coils (Bobine): Valori Booleani (On/Off) in lettura e scrittura (es. accendere un motore).
  2. Discrete Inputs: Valori Booleani in sola lettura (es. lo stato di un finecorsa).
  3. Input Registers: Valori a 16-bit in sola lettura (es. temperatura letta da un sensore).
  4. Holding Registers: Valori a 16-bit in lettura e scrittura (es. impostazione di un setpoint).

Un pacchetto Modbus tipico contiene quattro elementi fondamentali:

  • Indirizzo del dispositivo: A chi sto parlando? (ID da 1 a 247).
  • Codice Funzione: Cosa voglio fare? (es. 03 per leggere un registro, 06 per scriverne uno).
  • Dati: Quali registri mi servono? Quanti?
  • CRC (Error Check): Un codice di controllo per assicurarsi che i dati non siano stati corrotti durante il viaggio.

Nonostante esistano protocolli più avanzati (come Profinet o EtherCAT), Modbus resta lo standard “universale”, il vecchietto è una garanzia di compatibilità di comunicazione tra prodotti, è il “minimo comune denominatore” che permette a macchine di produttori diversi di capirsi senza troppe difficoltà burocratiche, come si faceva una volta!

Il CRC (controllo di ridondanza ciclico)

In pratica è come una “firma digitale” applicata alla fine di un messaggio Modbus per assicurarsi che nessun bit sia stato alterato da interferenze elettriche o rumore sulla linea.

In Modbus RTU, si usa specificamente il CRC-16. Ecco un esempio pratico di come funziona nel “mondo reale”.

Immaginiamo che il Master voglia leggere un valore da un sensore (Server ID: 1).

  1. prima fase: composizione del messaggio (Senza CRC)->01 03 00 00 00 01
    • 01: Indirizzo dello Slave.
    • 03: Funzione (Leggi Holding Register).
    • 00 00: Indirizzo del registro di partenza.
    • 00 01: Quanti registri leggere (1).
  2. Seconda fase: il calcolo -> Il Master prende questi 6 byte e li fa passare attraverso un algoritmo matematico (una divisione polinomiale basata sul polinomio x16+x15+x2+1). Il risultato di questo calcolo è 84 0A.
  3. Terza fase: composizione del messaggio finale (quello inviato sul cavo)-> 01 03 00 00 00 01 84 0A

Ricezione del messaggio da parte dello Slave:

  • Scenario A (Successo): Il calcolo dello slave dà come risultato 84 0A. Questo corrisponde al CRC allegato dal Master? Sì -> lo Slave accetta il comando e risponde.
  • Scenario B (Errore): Un’interferenza elettrica trasforma il messaggio in 01 03 00 00 00 09 .... lo Slave ricalcola il CRC, ma il risultato non sarà più 84 0A. Lo Slave non accetta e non risponde, perché il dato è corrotto.
  • A questo punto ci possono essere degli atri tentativi di trasmissione della richiesta a cui si potrà eventualmente generare un errore.

Perché funziona? Perché usare una divisione polinomiale non è una scelta arbitraria: è un metodo matematico estremamente efficace per rilevare i tipi di errori che tipicamente colpiscono i cavi industriali (interferenze, scariche elettrostatiche o “burst” di rumore).

Perché funziona? Perché usare una divisione polinomiale non è una scelta arbitraria: è un metodo matematico estremamente efficace per rilevare i tipi di errori che tipicamente colpiscono i cavi industriali (interferenze, scariche elettrostatiche o “burst” di rumore).

Ecco perché funziona così bene:

In matematica, una sequenza di bit (come 1101) può essere vista come i coefficienti di un polinomio.

• Il messaggio 1101 diventa: 1×3+1×2+0x1+1×0.

• Questo trasforma un problema di “trasmissione dati” in un problema algebrico.

Il CRC utilizza un “divisore” fisso e noto (chiamato polinomio generatore). Per il Modbus RTU, questo polinomio è lo standard x16+x15+x2+1.

La divisione polinomiale funziona come una normale divisione tra numeri, ma con una differenza fondamentale: usa l’aritmetica modulo 2 (ovvero l’operazione logica XOR).

Il CRC è, tecnicamente, il resto di questa divisione.

  1. Il mittente divide il messaggio per il polinomio e ottiene un resto (il CRC).
  2. Il mittente “attacca” questo resto alla fine del messaggio originale.
  3. Il ricevente divide l’intero pacchetto (Messaggio + CRC) per lo stesso polinomio.

La magia matematica: Se i dati non sono stati alterati, il resto della divisione fatta dal ricevente deve essere zero.

Se il resto non è zero, significa che almeno un bit è cambiato durante il tragitto, semplice ed efficace direi, ma provate a farlo con i numeri binari… questa è la vera lezione!

Vediamo il listato utilizzato in questa sessione didattica, in MicroPython:

import machine
import utime
from machine import Pin, PWM
import sys
import select
import struct
# Costanti Modbus
SLAVE_ID = 101
FC_READ_HOLDING = 3
FC_WRITE_SINGLE = 6
# Pin configurazione
servo_pin = 0
pin_auto_man = 1 # 1 = Manuale, 0 = Automatico
pin_alarm = 15   # 1 = Allarme
# Inizializza Pin
led = Pin("LED", Pin.OUT)
pwm = PWM(Pin(servo_pin))
pwm.freq(50)
p_man = Pin(pin_auto_man, Pin.IN, Pin.PULL_DOWN)
p_alm = Pin(pin_alarm, Pin.IN, Pin.PULL_DOWN)

# Variabili di stato
current_angle = 0

def blink(counter, on_off):
    led = Pin("LED", Pin.OUT)
    for _ in range(counter):
        led.value(1)
        utime.sleep_ms(on_off)
        led.value(0)
        utime.sleep_ms(on_off)

def set_angle(angle):
    global current_angle
    # Map 0 - 270 degrees to roughly 0.5ms (500000ns) - 2.5ms (250000ns)
    # Duty for 50Hz (20ms period). 0.5ms / 20ms = 2.5% = 1638, 2.5ms = 12.5% = 8192
    if angle < 0: angle = 0
    if angle > 270: angle = 270 
    # 0 degree = 500000 ns, 270 degree = 2500000 ns -> range 2000000 ns
    ns = 500000 + int((angle / 270.0) * 2000000)
    pwm.duty_ns(ns)
    current_angle = angle
def get_status():
    if p_alm.value() == 1:
        return 2 # Allarme
    elif p_man.value() == 1:
        return 1 # Manuale
    else:
        return 0 # Automatico
def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)
print("Starting Modbus Slave...")
set_angle(0)
poll = select.poll()
poll.register(sys.stdin, select.POLLIN)
import micropython
# MOLTO IMPORTANTE: Disabilita il Ctrl-C (KeyboardInterrupt) causato dal byte 0x03
# che viene inviato dal Master come Function Code (FC_READ_HOLDING = 3)
micropython.kbd_intr(-1)
rx_buffer = bytearray()

while True:
    events = poll.poll(10) # 10ms timeout
    if events:
        try:
 # Legge uno o più byte diponibili per evitare il timeout/EOF block su stdin
           # Utilizza poll(0) per verificare la disponibilità senza bloccare
            while poll.poll(0):
                chunk = sys.stdin.buffer.read(1)
                if not chunk:
                    break
                rx_buffer.extend(chunk)     
            # Quando il buffer accumula almeno un frame teorico di 8 bytes
            while len(rx_buffer) >= 8:
                buffer = rx_buffer[:8]    
                # Check ID
                if buffer[0] == SLAVE_ID:
                    fc = buffer[1]       
                    # Check CRC: estrae esattamente gli ultimi 2 bytes del pacchetto di 8 bytes
                    received_crc = buffer[-2:]
                    calc_crc = crc16(buffer[:-2])           
                    if received_crc == calc_crc:
                        # Rimuovi l'intero pacchetto da 8 bytes dal buffer
                        rx_buffer = rx_buffer[8:]  
                        led.value(1) # Blink LED indicator on success processing             
                        if fc == FC_WRITE_SINGLE:
                            # Write single register
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            value = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 1:
                                set_angle(value)               
                            # Response is echo of request
                            sys.stdout.buffer.write(buffer)        
                        elif fc == FC_READ_HOLDING:
                            # Read holding registers
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            num_regs = struct.unpack('>H', buffer[4:6])[0]  
                            if reg_addr == 2 and num_regs == 1:
                                status = get_status()
                                resp = bytearray([SLAVE_ID, FC_READ_HOLDING, 2]) # 2 bytes of data
                                resp.extend(struct.pack('>H', status))
                                resp.extend(crc16(resp))
                                sys.stdout.buffer.write(resp)
                        
                        utime.sleep_ms(10)
                        blink(3,250)                   
                        led.value(0)
                    else:
                        # Se il CRC non corrisponde, spostiamo di 1 byte e riproviamo a sincronizzare
                        rx_buffer = rx_buffer[1:]
                else:
                    # ID Sbagliato: spostiamo di 1 byte
                    rx_buffer = rx_buffer[1:]           
        except Exception as e:
            # In caso d'errore resetta il buffer ed esce (e.g., struct unpack errore generico)
            rx_buffer = bytearray()
            blink(5,250)

Questo è un software di simulazione di una apparecchiatura che comunica in Modbus, con supporto di controllo CRC, scritto in MicroPython per un Raspberry pi pico.

Anche in questo caso la funzione Blink, serve per constatare se il software main.py cicli correttamente.

Come si può notare, oltre che a ricevere il valore di un angolo di posizionamento del servocomando, trasmette anche il suo stato di lavoro: manuale o automatico oppure uno stato di allarme.

Il software Master/Server

Questo è il software cardine del sistema utilizzato per questa lezione didattica, può essere installato sul PC oppure su un Raspberry Pi 4B, se si vuole proprio costruire una piccola “Intranet”.

Dove in il termine “Intranet” è una rete privata completamente isolata dalla rete esterna (Internet) a livello di servizi offerti (es. tramite LAN), rimanendo dunque a solo uso interno, ma in grado eventualmente di comunicare con la rete esterna e altre reti attraverso opportuni sistemi di comunicazione (protocollo TCP/IP, estendendosi anche con collegamenti WAN e VPN).

Per questa applicazione verrà ovviamente utilizzato il linguaggio Python, ed in particolare, la libreria Flask.

Flask è un framework leggero per applicazioni web WSGI. È progettato per rendere l’avvio rapido e semplice, con la capacità di scalare fino ad applicazioni complesse.

Flask si basa sul toolkit WSGI Werkzeug , sul motore di template Jinja e sul toolkit Click CLI.

Grazie all’eccellente supporto di Python per la reflection, si può accedere al pacchetto per capire dove sono archiviati i template e i file statici.

Flask utilizza il sistema di routing Werkzeug, progettato per ordinare automaticamente le route in base alla complessità. Ciò significa che è possibile dichiarare le route in un ordine arbitrario e funzioneranno comunque come previsto. Questo è un requisito fondamentale per implementare correttamente il routing basato sui decoratori, poiché questi potrebbero essere eseguiti in un ordine indefinito quando l’applicazione è suddivisa in più moduli.

Jinja, ha un sistema di filtri esteso, un modo specifico di gestire l’ereditarietà dei template, supporta blocchi riutilizzabili (macro) utilizzabili sia all’interno dei template che dal codice Python, supporta il rendering iterativo dei template, offre una sintassi configurabile e altro ancora.

Flask supporta inoltre “async” le coroutine per le funzioni di vista eseguendo la coroutine su un thread separato anziché utilizzare un ciclo di eventi sul thread principale, in sintesi, dunque, l’idea alla base di Flask è quella di costruire una solida base per tutte le applicazioni, tutto il resto dipende dalle estensioni.

La struttura delle directory potrebbe essere qualcosa di simile:

Il listato di app.py

Il “core” della applicazione di Flask:

from flask import Flask, render_template, request, jsonify
from datetime import datetime
import json
import os
import serial
import struct
import threading
import time

slave_status = "In attesa..."
DATA_FILE = 'sensor_data.json'
DEFAULT_DATA = {
    'temperature': None,
    'humidity': None,
    'freq': None,
    'ang': None,
    'inverter_1': "spento",
    'servo_flap': "spento",
    'ang-input': None,
    'freq-input-1': None,
    'vent-1': "spento",
    'vent-2': "spento",
    'last_updated': None
}
app = Flask(__name__)


def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)

def load_data():
    if os.path.exists(DATA_FILE):
        print("read data file")
        try:
            with open(DATA_FILE, 'r') as f:
                return json.load(f)
        except Exception as e:
            print(f"Error loading data: {e}")
            # Fallback to default if load fails
            return DEFAULT_DATA.copy()
    else:
        # File doesn't exist, create it with default data
        try:
            with open(DATA_FILE, 'w') as f:
                json.dump(DEFAULT_DATA, f)
            print(f"Created new data file: {DATA_FILE}")
            return DEFAULT_DATA.copy()

        except Exception as e:
            print(f"Error creating data file: {e}")
            return DEFAULT_DATA.copy()

latest_data = load_data()

def modbus_loop():
    global slave_status
    print("start ciclo")
    while True:
        try:
            ang = latest_data.get('ang')
            if ang is not None:
                addr = 101
                func = 6
                reg = 1
                
                # Send Angle
                frame = bytearray([addr, func])
                frame.extend(struct.pack('>H', reg))
                # Usa il valore assoluto poiché l'angolo calcolato per l'interfaccia può 			essere negativo,
             	     # e il formato '>H' (unsigned short) richiede numeri positivi.
                frame.extend(struct.pack('>H', abs(int(ang))))
                frame.extend(crc16(frame))
                

                try:
                    with serial.Serial('/dev/ttyACM0', 9600, timeout=1) as ser:
                        ser.write(frame)
                        time.sleep(0.5)
                        
                        # Read status: FC 03 (Read Holding Registers), Reg 0x0002
                        func_read = 3
                        reg_read = 2
                        frame_read = bytearray([addr, func_read])
                        frame_read.extend(struct.pack('>H', reg_read))
                        frame_read.extend(struct.pack('>H', 1))
                        frame_read.extend(crc16(frame_read))
                        
                        ser.write(frame_read)
                        time.sleep(0.5)
                        if ser.in_waiting:
                            resp = ser.read_all()
                            if len(resp) >= 7 and resp[0] == addr and resp[1] == func_read:
                                val = struct.unpack('>H', resp[3:5])[0]
                                if val == 0:
                                    slave_status = "automatico"
                                elif val == 1:
                                    slave_status = "manuale"
                                elif val == 2:
                                    slave_status = "allarme"
                                else:
                                    slave_status = "sconosciuto"

                            else:
                                slave_status = "Errore Risposta"
                        else:
                            slave_status = "Timeout Slave"
                except Exception as e:
                    print(f"Serial Error: {e}")
                    slave_status = "Errore Seriale"
        except Exception as e:
            print(f"Modbus loop error: {e}")
            
        time.sleep(60)

threading.Thread(target=modbus_loop, daemon=True).start()

# Alarm thresholds
TEMP_MIN = 5.0
TEMP_MAX = 25.0
#HUM_MIN = 20.0
#HUM_MAX = 75.0

def calculate_frequency(temperature):
    """
    Calculate frequency based on temperature using linear interpolation.
    - <= TEMP_MIN: 20Hz
    - >= TEMP_MAX: 50Hz
    """
    if temperature is None:
        freq = None
        return freq
        
    if temperature <= TEMP_MIN:
        freq = 20
        return freq
    elif temperature >= TEMP_MAX:
        freq = 50 
        return freq
    else:
        # Linear interpolation: y = y1 + ((x - x1) * (y2 - y1) / (x2 - x1))
        # x = temperature, x1 = TEMP_MIN, x2 = TEMP_MAX
        # y1 = 20, y2 = 50
        print("temperatura =", temperature)
        freq = int(20.0 + ((temperature - TEMP_MIN) * (50.0 - 20.0) / (TEMP_MAX - TEMP_MIN)))
        return freq

def calculate_angular_value(humidity):
    """
    Calcola l'angolo del flap in base all'umidità percentuale.
    
    Args:
        hum (float): Valore di umidità percentuale (0-100)
    
    Returns:
        float: Angolo del flap in gradi
    """
    # Validazione dell'input
    if humidity < 0 or humidity > 100:
        raise ValueError("L'umidità deve essere compresa tra 0 e 100")
    
    # Caso umidità < 10%: flap orizzontale (0 gradi)
    if humidity < 10:
        return 0.0
    
    # Caso umidità > 90%: flap verticale (-90 gradi)
    elif humidity > 90:
        return -90.0
    
    # Caso intermedio (10% - 90%): interpolazione lineare
    else:
        # Mappatura lineare: 10% -> 0 gradi, 90% -> -90 gradi
        # Pendenza: (-90 - 0) / (90 - 10) = -90/80 = -1.125 gradi per punto percentuale
        ang = -1.125 * (humidity - 10)
        return ang


@app.route('/')
def index():
    """Render the dashboard with current data and alarm status."""
    global latest_data, slave_status
    data = latest_data.copy()
    alarms = {
        'temp_alarm': None
    }
    
    if data['temperature'] is not None:
        if data['temperature'] < TEMP_MIN:
            alarms['temp_alarm'] = 'min' # Low temperature alarm
        elif data['temperature'] > TEMP_MAX:
            alarms['temp_alarm'] = 'max' # High temperature alarm
            
    if data['humidity'] is not None:
        pass


    return render_template('index.html', data=data, alarms=alarms, slave_status=slave_status)
@app.route('/upload', methods=['POST'])
def upload_data():
    """Receive sensor data from the client."""
    try:
        content = request.json
        print(f"Received upload request: {content}") # Debug print
        data_updated = False
        
        # Update temperature and humidity if present
        temp = content.get('temperature')
        hum = content.get('humidity')
        
        if temp is not None:
            latest_data['temperature'] = float(temp)
            latest_data['freq'] = calculate_frequency(float(temp))
            data_updated = True
            
        if hum is not None:
            latest_data['humidity'] = float(hum)
            latest_data['ang'] = calculate_angular_value(float(hum))
            data_updated = True

        # Update specific fields if present in the request
        fields_to_update = [
            'inverter_1',
            'freq-input-1',
            'vent-1', 'vent-2',
        ]
        
        for field in fields_to_update:
            if field in content:
                latest_data[field] = content[field]
                data_updated = True

        if data_updated:
            latest_data['last_updated'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            # Save to JSON file
            try:
                with open(DATA_FILE, 'w') as f:
                    json.dump(latest_data, f)
            except Exception as e:
                print(f"Error saving data: {e}")
                return jsonify({"status": "error", "message": f"Error saving data: {str(e)}"}), 500
                
            print(f"Data updated and saved.")
            return jsonify({"status": "success", "message": "Data received and saved"}), 200
        else:
            return jsonify({"status": "ignored", "message": "No valid data fields found"}), 200
            
    except Exception as e:
        print(f"Error processing upload: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500

#@app.route('/2026/server_master/client/server_cp', methods=['POST'])

if __name__ == '__main__':
    # Run server on all interfaces, port 5000
    app.run(host='0.0.0.0', port=5000, debug=True)
    app.run(debug=True, use_reloader=False) # Aggiungi use_reloader=False

Questo è il listato della versione di test riferito alla libreria Flask, scritto in Python di cui consiglio una analisi approfondita, facendo riferimento anche alla documentazione della libreria specifica.

Si notino in particolare le due funzioni:

def calculate_frequency(temperature):
def calculate_angular_value(humidity):

Che sono fondamentali per la generazione dei controlli di frequenza, ad esempio riferiti ad un inverter collegato in modalità Modbus, o ad un controllo servomotore, come decritto nel software dello Slave simulato dal Raspberry Pico.

Tutti i dati a cui fa riferimento questo software sono contenuti in un flie “Json”, che verrebbe generato, qualora non esistesse:

DATA_FILE = 'sensor_data.json'
DEFAULT_DATA = {
    'temperature': None,
    'humidity': None,
    'freq': None,
    'ang': None,
    'inverter_1': "spento",
    'servo_flap': "spento",
    'ang-input': None,
    'freq-input-1': None,
    'vent-1': "spento",
    'vent-2': "spento",
    'last_updated': None
}

I file JSON (.json) sono formati di file testuali basati su JavaScript Object Notation, usati per strutturare e scambiare dati tra server e applicazioni web.

Leggeri e leggibili, memorizzano informazioni in coppie chiave-valore, come sopra riportato.

La app.py espone i valori mediante una pagina web di riferimento con l’istruzione:

 return render_template('index.html', data=data, alarms=alarms, slave_status=slave_status)

Questa pagina di riferimento, generata dall’applicazione di test, al momento risulta incompleta, poiché potrebbe essere implementata anche con lo stato dell’inverter (al quale si farebbe riferimento mediante un secondo dispositivo Slave.

Il suo listato, riportato come file HTML, è il seguente e si trova nella directory “/templates”:


Il listato di index.html

<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>IoT Dashboard - Raspberry Pi</title>
    <!-- Refresh page every 30 seconds to fetch new data -->
    <meta http-equiv="refresh" content="30">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <!-- Google Fonts for premium look -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <style>
        .gauge-container {
            width: 100%;
            max-width: 150px;
            margin: auto;
        }
        .needle {
            transform-origin: 50px 50px;
            transition: transform 1s ease-out;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>Monitoraggio Ambientale</h1>
            <p>Ultimo aggiornamento: {{ data.last_updated if data.last_updated else 'In attesa di dati...' }}</p>
        </header>

        <main class="dashboard">
            <!-- Temperature Card -->
            <div class="card {% if alarms.temp_alarm %}alarm-{{ alarms.temp_alarm }}{% endif %}">
                <div class="icon">🌡️</div>
                <h2>Temperatura</h2>
                <div class="value">
                    {% if data.temperature is not none %}
                    {{ data.temperature }} °C
                    <br>
                    <span style="font-size: 0.5em;">Freq: {{ data.freq }} Hz</span>
                    {% else %}
                    --
                    {% endif %}
                </div>
                <div class="status">
                    {% if alarms.temp_alarm == 'min' %}
                    ⚠️ ALLARME: Temperatura Bassa (< 5°C) {% elif alarms.temp_alarm=='max' %} ⚠️ ALLARME: Temperatura
                        Alta (> 25°C)
                        {% else %}
                        Normale
                        {% endif %}
                </div>
            </div>

            <!-- Humidity Card -->
            <div class="card {% if alarms.hum_alarm %}alarm-{{ alarms.hum_alarm }}{% endif %}">
                <div class="icon">💧</div>
                <h2>Umidità</h2>
                <div class="value">
                    {% if data.humidity is not none %}
                    {{ data.humidity }} %
                    {% else %}
                    --
                    {% endif %}
                </div>
                <!-- flap -->
                <div class="gauge-container" style="margin-top: 15px;">
                    <svg viewBox="0 0 100 100" class="gauge">
                        <!-- Terzo quadrante: da Sinistra (10, 50) a Basso (50, 90) -->
                        <path d="M 10 50 A 40 40 0 0 0 50 90" fill="none" stroke="#ddd" stroke-width="5" />
                        <!-- Map ang_val (0 to -90) to rotation (-90 to -180 degrees) -->
                        {% set ang_val = data.ang | float if data.ang is not none else 0 %}
                        {% set gauge_angle = -90 + ang_val %}
                        <!-- CSS styling applied safely avoiding parsing errors -->
                        <line class="needle needle-dynamic" x1="50" y1="50" x2="50" y2="10" stroke="red"
                            stroke-width="2" data-rotation="{{ gauge_angle }}" />
                        <circle cx="50" cy="50" r="3" fill="#333" />
                        <script>
                            document.querySelector('.needle-dynamic').style.transform = `rotate(${document.querySelector('.needle-dynamic').dataset.rotation}deg)`;
                        </script>
                    </svg>
                    <div style="font-size: 0.8em; margin-top: 5px;">Angolo: {{ data.ang if data.ang is not none else
                        'N/A' }}°</div>
                </div>
                <div class="status"
                    style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255,255,255,0.1);">
                    <strong>Stato Slave:</strong> {{ slave_status }}
                </div>
            </div>

            <!-- Inverter Section -->
            <section class="inverter-container">
                <h2>Inverter</h2>
                <div class="inverter-grid">
                    <!-- Inverter 1 -->
                    <div class="inverter" id="inv-1">
                        <div class="comandi-sinistra">
                            <button class="btn-manuale" onclick="setInverterMode(1, 'manuale')">Manuale</button>
                            <div class="freq-input-wrapper" id="freq-input-wrapper-1" style="display: none;">
                                <input type="number" class="freq-input" id="freq-input-1" placeholder="Hz"
                                    onchange="updateFreqDisplay(1)">
                            </div>
                            <button class="btn-automatico active"
                                onclick="setInverterMode(1, 'automatico')">Automatico</button>
                            <button class="btn-spento" onclick="setInverterMode(1, 'spento')">Spento</button>
                        </div>
                        <div class="icona-destra">
                            <i class="fas fa-microchip inverter-icon verde" id="inv-icon-1"></i>
                            <div class="freq-display" id="freq-display-1">0 Hz</div>
                        </div>
                    </div>
            </section>
            <!-- Ventilatori Section -->
            <section class="ventilatori-container">
                <h2>Ventilatori</h2>
                <div class="ventilatori-grid">
                    <!-- 8 Fans -->
                    <!-- Simulating different states as requested/implied -->
                    <i class="fas fa-fan ventilatore-icon spento" id="vent-1" onclick="toggleFan(1)"></i>
                    <i class="fas fa-fan ventilatore-icon spento" id="vent-2" onclick="toggleFan(2)"></i>
                </div>
            </section>

        </main>

        <script>
            // Store server-calculated frequency
            // Using 'safe' filter or default to 0 if None/null
            const serverFreq = Number("{{ data.freq if data.freq is not none else 0 }}");

            // State tracking (optional, but good for cleanliness)
            const inverterStates = {
                1: '{{ data.inverter_1 if data.inverter_1 else "spento" }}'
            };

            const freqInputs = {
                1: '{{ data["freq-input-1"] if data["freq-input-1"] is not none else "" }}'
            };

            function updateServer(dataParam) {
                fetch('/upload', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(dataParam)
                }).then(response => {
                    if (!response.ok) console.error("Update failed", response);
                }).catch(err => console.error("Update error", err));
            }

            function setInverterMode(id, mode, skipServerUpdate = false) {
                inverterStates[id] = mode;

                if (!skipServerUpdate) {
                    let reqData = {};
                    reqData['inverter_' + id] = mode;
                    updateServer(reqData);
                }

                // Reset buttons
                document.querySelector(`#inv-${id} .btn-manuale`).classList.remove('active');
                document.querySelector(`#inv-${id} .btn-automatico`).classList.remove('active');
                document.querySelector(`#inv-${id} .btn-spento`).classList.remove('active');

                // Set active button
                document.querySelector(`#inv-${id} .btn-${mode}`).classList.add('active');

                // Handle Icon Color, Input Visibility, and Frequency Display
                const icon = document.getElementById(`inv-icon-${id}`);
                const inputWrapper = document.getElementById(`freq-input-wrapper-${id}`);
                const freqDisplay = document.getElementById(`freq-display-${id}`);




                // Remove all color classes first
                icon.classList.remove('spento', 'verde', 'azzurro', 'rosso', 'nero');

                if (mode === 'manuale') {
                    icon.classList.add('azzurro');
                    inputWrapper.style.display = 'block';
                    // Show input value
                    const val = document.getElementById(`freq-input-${id}`).value;
                    freqDisplay.innerText = val + ' Hz';

                } else if (mode === 'automatico') {
                    icon.classList.add('verde');
                    inputWrapper.style.display = 'none';
                    // Show calculated frequency
                    freqDisplay.innerText = serverFreq + ' Hz';

                } else if (mode === 'spento') {
                    icon.classList.add('nero');
                    inputWrapper.style.display = 'none';
                    // Show 0 Hz
                    freqDisplay.innerText = '0 Hz';
                }
            }
            function updateFreqDisplay(id) {
                // Only update display if we are in Manual mode
                if (inverterStates[id] === 'manuale') {
                    const val = document.getElementById(`freq-input-${id}`).value;
                    document.getElementById(`freq-display-${id}`).innerText = val + ' Hz';
                    let reqData = {};
                    reqData['freq-input-' + id] = val;
                    updateServer(reqData);
                }
            }
            // Optional: Toggle fan states for demo
            function toggleFan(id) {
                const el = document.getElementById(`vent-${id}`);
                let state = '';
                if (el.classList.contains('attivo')) {
                    el.classList.remove('attivo');
                    el.classList.add('spento');
                    state = 'spento';
                } else if (el.classList.contains('spento')) {
                    el.classList.remove('spento');
                    el.classList.add('avaria');
                    state = 'avaria';
                } else {
                    el.classList.remove('avaria');
                    el.classList.add('attivo');
                    state = 'attivo';
                }

                let reqData = {};
                reqData['vent-' + id] = state;
                updateServer(reqData);
            }

            // Initialize displays on load based on correct HTML state sent from the server
            document.addEventListener('DOMContentLoaded', () => {
                // Initialize all inverters that are dynamically updated
                for (let id = 1; id <= 2; id++) {
                    const inputEl = document.getElementById(`freq-input-${id}`);
                    if (freqInputs[id] !== "") {
                        inputEl.value = freqInputs[id];
                    }
                    setInverterMode(id, inverterStates[id], true);
                }
                // Initialize fans based on server state
                const fanStates = {
                    1: '{{ data["vent-1"] if data["vent-1"] else "spento" }}',
                    2: '{{ data["vent-2"] if data["vent-2"] else "spento" }}'
                };
                for (let id = 1; id <= 2; id++) {
                    const fanEl = document.getElementById(`vent-${id}`);
                    fanEl.classList.remove('spento', 'attivo', 'avaria');
                    fanEl.classList.add(fanStates[id]);
                }
            });
        </script>

        </main>

        <footer>
            <p>Server: Raspberry Pi 4B | Client: Pico W</p>
        </footer>
    </div>
</body>

</html>

Questo listato, comprende del codice Javascript che viene inserito nei file HTML utilizzando i tag

<script> e </script>.

Questo metodo permette di includere istruzioni direttamente nella pagina (inline) o di collegare file esterni .js.

Il tag può essere posizionato all’interno di <head> o alla fine del <body>, pratica quest’ultima raccomandata per migliorare le prestazioni di caricamento, come in questo caso.

Una pagina web che si rispetti ha anche uno stile, che in questo caso fa riferimento al seguente link:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">


La pagina di stile HTML utilizza i fogli di stile CSS (Cascading Style Sheets) per definire l’aspetto, il layout e la formattazione, separando il contenuto dalla presentazione. I CSS permettono di controllare colori, font, dimensioni e disposizioni, rendendo le pagine più leggere e responsabili.


Il listato del file ‘style.css’

:root {
    --bg-color: #0f172a;
    /* Dark blue/slate background */
    --card-bg: #1e293b;
    --text-primary: #e8bd6c;;
    --text-secondary: #94a3b8;
    --accent-color: #38bdf8;
    /* Cyan */
    --alarm-min: #0ea5e9;
    /* Blue for cold/low */
    --alarm-max: #ef4444;
    /* Red for hot/high */
    --alarm-bg-min: rgba(14, 165, 233, 0.2);
    --alarm-bg-max: rgba(239, 68, 68, 0.2);
    --success-color: #22c55e;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    font-family: 'Outfit', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-primary);
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
}
.container {
    width: 100%;
    max-width: 800px;
    padding: 2rem;
    text-align: center;
}

header h1 {
    font-weight: 600;
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
    background: linear-gradient(to right, var(--accent-color), #818cf8);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

header p {
    color: var(--text-secondary);
    margin-bottom: 3rem;
    font-size: 1.1rem;
}
.dashboard {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 2rem;
}
.card {
    background-color: var(--card-bg);
    border-radius: 1rem;
    /* slightly smaller radius */
    padding: 1rem;
    /* Reduced from 2rem */
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
    transition: transform 0.3s ease, box-shadow 0.3s ease;
    border: 1px solid rgba(255, 255, 255, 0.1);
}
.card:hover {
    transform: translateY(-5px);
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
}
.icon {
    font-size: 2rem;
    /* Reduced from 3rem */
    margin-bottom: 0.5rem;
    /* Reduced margin */
}
.card h2 {
    font-weight: 200;
    color: var(--text-secondary);
    font-size: 1rem;
    /* Reduced from 1.2rem */
    text-transform: uppercase;
    letter-spacing: 0.1em;
    margin-bottom: 0.5rem;
}
.value {
    font-size: 1.5rem;
    /* Reduced from 4rem */
    font-weight: 600;
    margin-bottom: 0.5rem;
}


.status {
    padding: 0.25rem 0.75rem;
    /* Reduced padding */
    border-radius: 9999px;
    background-color: rgba(255, 255, 255, 0.05);
    display: inline-block;
    font-size: 0.8rem;
    /* Reduced font size */
}

/* Alarm States */
.card.alarm-min {
    border-color: var(--alarm-min);
    background-color: var(--alarm-bg-min);
}
.card.alarm-min .value {
    color: var(--alarm-min);
}
.card.alarm-max {
    border-color: var(--alarm-max);
    background-color: var(--alarm-bg-max);
}
.card.alarm-max .value {
    color: var(--alarm-max);
}


footer {
    margin-top: 4rem;
    color: var(--text-secondary);
    font-size: 0.8rem;
}
@media (max-width: 600px) {
    header h1 {
        font-size: 2rem;
    }
    .value {
        font-size: 3rem;
    }
}
section {
    background: #000080;
    padding: 20px;
    margin-bottom: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
    /* NEW: Force sections to be full width in the grid to stack vertically */
    grid-column: 1 / -1;
    width: 100%;
}
/* Stile per Inverter, Ventilatori, Finestre */
.inverter-grid,
.ventilatori-grid {
    display: grid;
    gap: 20px;
    justify-content: center;
    margin-top: 1rem;
}


/* Force 1 columns for inverters as requested "in una unica riga" */
.inverter-grid {
    grid-template-columns: repeat(1, 1fr);
}

/* Force 2 columns for fans as requested "in una unica riga" */
.ventilatori-grid {
    grid-template-columns: repeat(2, 1fr);
    align-items: center;
    justify-items: center;
}

.inverter {
    background-color: #fff;
    border-radius: 12px;
    padding: 15px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    display: flex;
    /* Flex row to put buttons left, icon right */
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

/* Left controls (Buttons) */
.comandi-sinistra {
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: 60%;
    /* Occupy left side */
}
/* Buttons Styling */
.comandi-sinistra button {
    padding: 8px;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-weight: 600;
    font-size: 0.9rem;
    background-color: #e2e8f0;
    /* Default Gray (Inactive) */
    color: #475569;
    transition: all 0.2s;
}

/* Active Button States */
/* Spento active -> Black */
.comandi-sinistra button.btn-spento.active {
    background-color: #000000;
    color: #ffffff;
}
/* Automatico active -> Green */
.comandi-sinistra button.btn-automatico.active {
    background-color: var(--success-color);
    /* Green */
    color: #ffffff;
}
/* Manuale active -> Cyan (Azzurro) */
.comandi-sinistra button.btn-manuale.active {
    background-color: var(--accent-color);
    /* Cyan */
    color: #0f172a;
    /* Dark text for contrast */
}
/* Input field wrapper */
.freq-input-wrapper {
    margin-top: -4px;
    /* Tighten up spacing */
    margin-bottom: 4px;
}
.freq-input {
    width: 100%;
    padding: 6px;
    border: 1px solid #cbd5e1;
    border-radius: 6px;
    text-align: center;
}
/* Right side (Icon + Display) */
.icona-destra {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 40%;
}
.inverter-icon {
    font-size: 3.5rem;
    /* Large icon */
    margin-bottom: 8px;
    transition: color 0.3s;
}

.freq-display {
    font-weight: bold;
    color: var(--text-secondary);
}

/* Icon Colors */
.inverter-icon.spento {
    color: #000000;
}

.inverter-icon.verde {
    color: var(--success-color);
}
/* Auto */
.inverter-icon.azzurro {
    color: var(--accent-color);
}
/* Manual */
.inverter-icon.rosso {
    color: var(--alarm-max);
}

/* Alarm */
.inverter-icon.nero {
    color: #000000;
}

/* Fan Icons */
.ventilatore-icon {
    font-size: 2.5rem;
    cursor: pointer;
    transition: color 0.3s;
}

.ventilatore-icon.spento {
    color: #000000;
}

/* Black */
.ventilatore-icon.attivo {
    color: #3b82f6;
}
/* Blue */
.ventilatore-icon.avaria {
    color: var(--alarm-max);
}
/* Red */
.ventilatore-icon.blu {
    color: #3b82f6;
}
/* Responsive adjustments */
@media (max-width: 900px) {
    .inverter-grid {
        grid-template-columns: repeat(2, 1fr);
        /* 2x2 on tablets */
    }
    .ventilatori-grid {
        grid-template-columns: repeat(4, 1fr);
        /* 4x2 on tablets */
    }
}
@media (max-width: 600px) {
    .inverter-grid {
        grid-template-columns: 1fr;
        /* Stack on mobile */
    }
    .ventilatori-grid {
        grid-template-columns: repeat(4, 1fr);
    }
}


Per finire, riporto l’aspetto grafico della pagina web:

In questa immagine si può notare la presenza di un allarme per temperatura elevata, un ventilatore in funzionamento normale (colore azzurro) ed un ventilatore in avaria (colore rosso).

L’inverter in modalità automatico (alla massima frequenza) ed il servomotore in modalità automatico (con angolazione calcolata).


Il file app.py a terminale, contestualmente, rimanda le seguenti

172.18.92.148 - - [15/Mar/2026 21:39:24] "POST /upload HTTP/1.0" 200 -
127.0.0.1 - - [15/Mar/2026 21:39:24] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [15/Mar/2026 21:39:24] "GET /static/style.css HTTP/1.1" 304 -
Received upload request: {'humidity': 45, 'temperature': 26}
Data updated and saved.

Corollario

Il progetto che viene riproposto in questo articolo, in fase di realizzazione, con l’utilizzo di supporti come Visual Studio Code (abbreviato in VS Code) che è un editor di codice sorgente gratuito, leggero e potente sviluppato da Microsoft per Windows, macOS e Linux, supporta nativamente linguaggi come JavaScript, TypeScript e Node.js, ed è altamente estendibile tramite plugin per programmare in Python, C++, Java e altro ancora, ma nel gestire la comunicazione con il Rasperry Pi Pico, collegato alla porta seriale virtuale su USB, mediante il file dispositivo “/dev/ttyACM0” potrebbe dare dei problemi a causa di conflitti di comunicazione.

Per altri versi, il file installabile sul Raspberry Pi Pico, che se nominato main.py, si attiva automaticamente all’accensione del dispositivo, ha una peculiarità di risposta tale da rendere difficile, in fase di test, il suo blocco, per effettuare eventuali modifiche.
Quindi anche se nominato diversamente, anche se fatto girare dalla directory locale del PC, una volta lanciato non si lascia stoppare, bisogna staccare fisicamente il dispositivo per apportare eventuali modifiche al software, almeno questo è quanto è successo a me, con un PC con sistema operativo Ubuntu.
Per la programmazione di dispositivi embedded, come il Raspberry Pico, o, in alternativa, Arduino Nano, oppure Adafruit Feather, Pimoroni Tiny 2040, tutte architetture su base RP2040, ma anche ESP32, o altre schede di sviluppo, i migliori IDE per programmare in MicroPython includono Thonny (consigliato per principianti, supporta Raspberry Pi Pico), VS Code con estensioni (potente e flessibile), e PyCharm (ideale per progetti complessi).
Queste piattaforme offrono strumenti essenziali come evidenziazione della sintassi, debug e gestione dei file sul microcontrollore.
Il mio consiglio, sia per i principianti, ma anche per chi ci smanetta da tempo, è quello di provare ad utilizzare le “Macchine inferenziali”, come preferisco nominarle io, ovvero, quelle piattaforme che permettono l’integrazione di agenti A.I. per lo sviluppo del codice.
Io, non lo nascondo, per poter realizzare questi contributi, in modo da velocizzare il processo, mi sono affidato ad Anigravity di Google che è un IDE (Integrated Development Environment) all’avanguardia basato sull’intelligenza artificiale, che utilizza agenti autonomi per sviluppare, testare e correggere software.
Basato su VS Code, permette di gestire più progetti in parallelo, monitorare le azioni dell’IA e generare codice tramite prompt.

Le caratteristiche Principali di Google Antigravity sono:

  • Agenti IA Autonomi: L'IA non si limita a suggerire codice, ma pianifica, scrive, esegue il debug e interagisce con il terminale e il browser per creare applicazioni.
  • Vibe Coding e Sviluppo: Consente di descrivere il risultato desiderato in linguaggio naturale, lasciando che l'IA gestisca l'implementazione tecnica.
  • Gestione Multi-Agente: Permette di lanciare più agenti in parallelo per diversi progetti, monitorando le attività tramite una "inbox" dedicata.
  • Integrazione Avanzata: Include un'interfaccia a tre sezioni: file, chat con l'IA e anteprima del browser (Antigravity Browser Control) per il testing in tempo reale.
  • Sicurezza: Introduce i "Safety Rails" per impedire azioni irreversibili senza conferma.
  • Compatibilità: Supporta modelli avanzati tra cui Gemini Pro, Claude e varianti open source.

Che dire? A questo punto, da vecchio cibernetico quale sono, “Getto la spugna!”
La “Macchina inferenziale”, sicuramente ha letto molti più testi di me, ha ripassato molto più codice di quanto ne abbia scritto io e, lo ammetto, mi ha aiutato parecchio!
E’ vero che, al codice generato ci ho dovuto metter mano, per adattarlo alle mie esigenze, ma devo dire che sono rimasto stupefatto dalle potenzialità che queste macchine hanno raggiunto in questo ultimo periodo.
Sono convinto che, l’affiancamento di questi strumenti agli operatori del settore informatico, non sminuisce il lavoro umano: se da un lato vi è un cambio di paradigma nel realizzare il software, dall’altro rimangono essenziali e necessarie le competenze per coloro che debbono o vogliono realizzare determinate strutture logiche.
Io non mi sento assolutamente sminuito nell’utilizzare questi strumenti, rimango semmai affascinato dalle loro potenzialità emergenti, che, devo ammettere, potrebbero essere tali da far perdere il controllo di certe operazioni, qualora queste operazioni fossero complesse e delegate completamente, in fiducia alle potenzialità di sviluppo e decisionali che strutture algoritmiche potrebbero adottare in autonomia.

Suggerimenti di sviluppo: simulazione di inverter

Prendendo spunto da questa sessione didattica, potrebbe essere interessante l’implementazione di un altro dispositivo Slave che potrebbe simulare un inverter per il controllo dei ventilatori in frequenza, messo anche questo in comunicazione con protocollo Modubs, similmente a quello già proposto, magari utilizzando un Raspberry Pi Pico, dotato di stepper motors.


Il 28BYJ-48

Questo è un motore passo passo unipolare a 5 fili molto utilizzato per applicazioni robotiche.

Questi motori forniscono una buona coppia anche in stato di stop fintanto che viene fornita alimentazione al motore.

Sulla scheda di controllo trova posto l’integrato ULN2003A che include un array di 7 transistor Darlington, ognuno in grado di gestire una corrente di 500mA con tensioni nell’ordine dei 50V

Specificche del Motore

  • Diametro: 28 mm / 1,1 pollici.
  • Voltaggio: 5V
  • Lunghezza: 274 mm / 10,8 pollici.
  • Angolo di passo: 5.625 x 1/64
  • Rapporto di riduzione: 1/64

Principio di funzionamento

Un motore passo-passo unipolare a 5 fili funziona alimentando in sequenza quattro bobine tramite un polo comune, gestito dal driver ULN2003A.

Il driver agisce come interruttore, attivando transistor Darlington che collegano a massa ciascuna fase, attirando il rotore magnetico secondo una sequenza (es. “full-step” o “half-step”) per ottenere rotazioni precise:

La sequenza di eccitazione del controllo avviene attivando una bobina alla volta (Wave Drive) o due alla volta (Full Step), generando un campo magnetico che attira il rotore, producendo il movimento di un “passo”.


l motore passo-passo 28BYJ-48 ha un angolo di passo di 5,625°/64 in modalità mezzo passo.

Ciò significa che il motore ha un angolo di passo di 5,625°, quindi necessita di 360°/5,625° = 64 passi in modalità mezzo passo.

In modalità passo intero: 64/2 = 32 passi per completare una rotazione.

L’albero di uscita è azionato tramite un rapporto di trasmissione di 64:1, questo significa che l’albero, completa una rotazione se il motore interno ruota 64 volte.

Pertanto il motore dovrà compiere 32 × 64 = 2048 passi affinché l’albero completi una rotazione completa, quindi si otterrà una precisione di 360°/2048 passi = 0,18°/passo.

Questo motorino, può raggiunge una velocità massima in uscita dal riduttore di circa 15-20 giri al minuto.

Schema di collegamento

Un esempio di schema di cablaggio viene riportato dalla seguente immagine:

Generazione del software

In questo caso ne approfitto per descrivere le operazioni necessarie per effettuare la procedura che ho adottato per la generazione di questo software:

Il prompt necessario

“generare un codice in MicroPython per un secondo dispositivo Slave con SLAVE_ID = 102, nominandolo main_inverter.py utilizzando il protocollo Modbus con CRC, come già fatto per il programma main.py nella directory /slave , coordinato con l’applicazione app.py per simulare un inverter, mediante Raspberry pi pico che dovrà controllare uno stepper motor 28BYJ-48 con driver ULN2003A per modulare la velocità massima di 20 RPM, in base al valore in frequenza 50Hz e la velocità minima 5 RPM per la frequenza 20Hz, la frequenza è calcolata dalla funzione def calculate_frequency(temperature):

Se l’impostazione dell’inverter settato su app.py è manuale, viene inoltrata la frequenza inpostata manuale.

La funzione di calcolo per il comando di velocità del 28BYJ-48 è calcolata da una fuzione definita “speed” in main_inverter.py

Se il pin 0 del Raspberry pi pico è a 1, il main_inverter.py invia il comando “servo_flap”: “avaria”.

La risposta di Antigravity

  • Analyzed main.py
  • Analyzed app.py

Initiating Data Analysis

I’m now diving deeper into the specifics, formulating my approach to extract the necessary information and ensure the next phase is set for success. The process of information analysis is underway.

Refining Execution Steps

\n\n

I’m now focusing on the script’s core functionalities, making sure that Modbus slave implementation and motor control are seamlessly integrated. The next step is to ensure proper response to Modbus commands and the correct implementation of motor speed control based on the input data. I’m also ensuring the correct logic for handling manual frequency input.

Mapping Alarm States

\n\n

My focus is now mapping the alarm state logic, specifically how the “avaria” state is communicated via Modbus registers. I’m examining the connection between the Raspberry Pi Pico’s pin 0 and the Modbus status register within app.py, ensuring the proper interpretation and response to this alarm condition, and I’m correlating this with the main.py implementation.

Defining Slave Communication

\n\n

I’m now determining how the inverter’s code will communicate with another Modbus slave, specifically addressing the creation and coordination of the MicroPython script and its interaction with the existing application. I’m focusing on defining communication protocols and ensure proper integration of the components. I’m starting to generate the required MicroPython code, and will then update the application code to match the same.

Implementing Stepper Control

I am now proceeding with implementing the stepper control logic, making sure it integrates smoothly within the existing system. The core of this involves formulating and writing the code and making sure the motor control and Modbus communication are non-blocking. The code will focus on the motor speed using step sequences.

Defining Motor Behavior

I’m now determining how to define the motor’s operational logic, focusing on establishing the relation between frequency and motor speed, along with the calculation of appropriate step delays. I’m making certain the code addresses alarm states through the appropriate input pin to the Raspberry Pi Pico.

Processing Modbus Frames

\n\n

I’m now focusing on parsing incoming Modbus frames, checking the slave ID, function code, and CRC to ensure data integrity. The logic is being written to handle valid frames, and any invalid data is being discarded. I’m extracting data and determining the correct function code to ensure correct processing.

Responding to Read Requests

\n\n

I’m currently focused on responding correctly to read requests within the Modbus protocol, specifically when retrieving the device status. I’m verifying the input data, constructing the response frame, and ensuring the data is correctly formatted, so it can be sent back through the communication channel.

Adjusting Motor Control

\n\n

I’m now refining the motor control logic within the program. I’m focusing on ensuring the step delay is properly managed, based on external frequency inputs. The goal is to make sure the motor responds correctly to stop signals, and the system is properly handling different operational modes.

Implementing Inverter Modbus Slave

Create MicroPython Modbus Slave code for an inverter (main_inverter.py) controlling a stepper motor and coordinate it with app.py.

Writing main_inverter.py and updating app.py

Ho creato il file main_inverter.py all’interno della directory /slave (configurato con SLAVE_ID = 102 e l’implementazione del controllo per il motore 28BYJ-48 con la funzione speed). Inoltre, ho aggiornato il file /master/app.py per coordinare le comunicazioni in Modbus anche con il secondo slave, inviando la frequenza manuale o calcolata a seconda della modalità e gestendo lo stato di avaria letto.

Puoi verificare i file ed eseguirli. Se ci sono altre modifiche o test da fare, fammi sapere!

Quindi, ha generato questo listato:


Listato del simulatore inverter

import machine
import utime
from machine import Pin
import sys
import select
import struct

# Costanti Modbus
SLAVE_ID = 102
FC_READ_HOLDING = 3
FC_WRITE_SINGLE = 6

# Pin configurazione
pin_alarm = 0 # Pin 0 per avaria
IN1 = Pin(28, Pin.OUT)
IN2 = Pin(27, Pin.OUT)
IN3 = Pin(26, Pin.OUT)
IN4 = Pin(22, Pin.OUT)

pins = [IN1, IN2, IN3, IN4]

# Sequenza a mezzo passo (half-step) per 28BYJ-48
step_sequence = [
    [1,0,0,0],
    [1,1,0,0],
    [0,1,0,0],
    [0,1,1,0],
    [0,0,1,0],
    [0,0,1,1],
    [0,0,0,1],
    [1,0,0,1]
]

p_alm = Pin(pin_alarm, Pin.IN, Pin.PULL_DOWN)
led = Pin("LED", Pin.OUT)

current_freq = 0
step_index = 0
last_step_time = utime.ticks_us()
step_delay_us = 0 

def speed(freq):
    """
    Calcola il delay in microsecondi per ogni passo del motore 28BYJ-48.
    Frequenza da 20Hz a 50Hz, Velocità da 5 RPM a 20 RPM.
    """
    if freq < 20:
        return 0 # Fermo
    if freq > 50:
        freq = 50
    # Equazione lineare: rpm = 5 + (freq - 20) * (15 / 30)
    rpm = 5.0 + (freq - 20.0) * 0.5
    # 4096 passi per rivoluzione completa (half-step)
    steps_per_sec = (rpm * 4096) / 60.0
    if steps_per_sec > 0:
        return int(1000000 / steps_per_sec)
    return 0

def get_status():
    if p_alm.value() == 1:
        return 2 # Allarme / Avaria
    else:
        return 0 # Normale

def crc16(data):
    crc = 0xFFFF
    for char in data:
        crc ^= char
        for _ in range(8):
            if crc & 0x0001:
                crc = (crc >> 1) ^ 0xA001
            else:
                crc >>= 1
    return struct.pack('<H', crc)

print("Starting Modbus Slave Inverter...")

poll = select.poll()
poll.register(sys.stdin, select.POLLIN)

import micropython
micropython.kbd_intr(-1)

rx_buffer = bytearray()

while True:
    events = poll.poll(0) # 0ms timeout, non bloccante
    if events:
        try:
            while poll.poll(0):
                chunk = sys.stdin.buffer.read(1)
                if not chunk:
                    break
                rx_buffer.extend(chunk)
            
            while len(rx_buffer) >= 8:
                buffer = rx_buffer[:8]
                if buffer[0] == SLAVE_ID:
                    fc = buffer[1]
                    received_crc = buffer[-2:]
                    calc_crc = crc16(buffer[:-2])
                    
                    if received_crc == calc_crc:
                        rx_buffer = rx_buffer[8:]
                        led.value(1)
                        

                        if fc == FC_WRITE_SINGLE:
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            value = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 1:
                                current_freq = value
                                step_delay_us = speed(current_freq)
                                
                            sys.stdout.buffer.write(buffer)
                            
                        elif fc == FC_READ_HOLDING:
                            reg_addr = struct.unpack('>H', buffer[2:4])[0]
                            num_regs = struct.unpack('>H', buffer[4:6])[0]
                            if reg_addr == 2 and num_regs == 1:
                                status = get_status()
                                resp = bytearray([SLAVE_ID, FC_READ_HOLDING, 2])
                                resp.extend(struct.pack('>H', status))
                                resp.extend(crc16(resp))
                                sys.stdout.buffer.write(resp)    
                        led.value(0)
                    else:
                        rx_buffer = rx_buffer[1:]
                else:
                    rx_buffer = rx_buffer[1:]
        except Exception as e:
            rx_buffer = bytearray()   
    # Motor control logic
    if step_delay_us > 0:
        now = utime.ticks_us()
        if utime.ticks_diff(now, last_step_time) >= step_delay_us:
            last_step_time = now
            for i in range(4):
                pins[i].value(step_sequence[step_index][i])
            step_index = (step_index + 1) % 8
    else:
        for i in range(4):
            pins[i].value(0)

La mia considerazione, quando ho letto il codice prodotto in relazione al prompt che ho proposto alla macchina è stata: quanto tempo ci avrei messo a “digitare questo codice”? Quanto tempo ci avrei messo a pensare come strutturare questa logica? Antigravity ci ha messo 46 secondi!

Prima ancora di provare ad installare il software sul Raspberry Pico, per vedere “se gira”, mi sono accorto che, rispetto allo schema che ho indicato, aveva selezionato dei pin diversi per la sequenza stepper, ed ho modificato quella parte del codice, in modo da adattarlo allo schema.

Poi ho iniziato a rileggere il codice, dall’inizio, per cercare di capire quali librerie avesse utilizzato:

import machine
import utime
from machine import Pin
import sys
import select
import struct

ed ho incominciato ad andare alla ricerca di approfondimenti:

La libreria “machine” in MicroPython è il modulo fondamentale per interagire con l’hardware del microcontrollore.

Consente di gestire pin GPIO (input/output), segnali PWM, protocolli di comunicazione (I2C, SPI, UART), ADC, timer e il reset del sistema.

È essenziale per il controllo diretto delle periferiche elettroniche…

di seguito ho potuto constatare che MicroPython utilizza “utime” (e altri moduli con prefisso “u“, come ujson o uos) invece del modulo standard time di Python principalmente per ragioni di efficienza, dimensione e compatibilità con risorse limitate…

Similmente a Python, la libreria “sys” in MicroPython fornisce funzioni specifiche per interagire con l’interprete e l’hardware, come sys.argv (argomenti), sys.exit() (uscita), sys.stdin/stdout/stderr (I/O standard) e sys.path (ricerca moduli).

È fondamentale per gestire il flusso di esecuzione e le risorse limitate del microcontrollore.

La libreria “select” in MicroPython (spesso referenziata come uselect) permette di monitorare in modo efficiente più stream di I/O (socket, file, UART) contemporaneamente, attendendo che diventino pronti per la lettura o la scrittura.

È fondamentale per la programmazione asincrona, evitando il blocco del ciclo principale.

La libreria “struct” in MicroPython (spesso implementata come ustruct) converte tra valori Python e struct C rappresentate come oggetti bytes.

È essenziale per gestire dati binari compatti, fondamentale in sistemi embedded, offrendo funzionalità di pack() (impacchettamento) e unpack() (disimpacchettamento) con stringhe di formato.

Di seguito sono passato all’aanalisi della sequenza delle funzioni:

def speed(freq):…
def get_status():…
def crc16(data):…

Poi, alla logica del ciclo di lavoro…

Insomma, se, da una parte non mi sono dedicato alla scrittura del codice, dall’altra, ho avuto modo di approfondire meglio la ramificazione del software necessario per sviluppare questa piccola applicazione, non è fare “Copia – incolla”, lavorare con il “vibe coding” significa sviluppare software utilizzando l’IA generativa (come LLM) per scrivere codice tramite descrizioni in linguaggio naturale, concentrandosi sulla logica e sul risultato (“vibe“) piuttosto che sulla sintassi manuale.

È un approccio rapido e collaborativo, ideale per prototipi e automazioni, dove l’umano controlla e l’IA genera, non viene “impigrita” la mente, anzi! Chi ha buona volontà, fantasia ed è motivato, scopre che questo approccio stimola a spaziare e ad approfondire moltissimo il contesto di lavoro. Velocizzare il processo, non significa “facilitare” il compito, significa dar modo di “migliorare la prestazione”, richiede ulteriori competenze, non “deresponabilizza”! “Utilizzare un acceleratore al posto dei pedali, significa non solo, andare più veloci, richiede riflessi più pronti, maggiore attenzione al codice (della strada), e… maggiore equilibrio (mentale)”.

Ulteriori sviluppi

In relazione a quanto sopra, potrei suggerire anche di sviluppare la parte Master/Server, per esempio partendo da Questo:

L’idea di partenza era proprio questa, fare una applicazione per simulare la gestione del monitoraggio ambientale ed il controllo di una serie di quattro inverter che azionano due ventilatori ciascuno, di un dispositivo di controllo per finestrature a flapper, magari associando i dati anche di una centralina di controllo ambientale esterna (non visibile nei dati riportati nella pagina web di esempio), una gestione del clima di un ambiente per ricovero animale, per esempio, dove è importante anche il ricambio d’aria.

Le figure in rosso sono apparecchiature in avaria, quelle in nero, sono apparecchiature ferme, mentre in azzurro, sono le apparecchiature funzionanti.

Qualcosa che può essere riassunto molto sinteticamente dalle seguenti immagini:

Ricovero animali
Quadro elettrico controllo clima ambientale

Grazie per aver avuto la pazienza di leggere.

Romeo Ceccato

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Translate »