Nano 33 IoT e BME280: un nodo di misura con invio HTTPS — Parte 2

Immagine generata.

Abstract

Un nodo di misura connesso non deve limitarsi a leggere un sensore e inviare immediatamente il valore ottenuto. Deve riconoscere l’assenza del dispositivo, scartare risultati impossibili, acquisire più campioni, applicare un filtro, controllare la rete, verificare la risposta del server e stabilire tempi differenti per il ciclo ordinario e per i tentativi successivi a un errore.

La procedura descritta utilizza una Arduino Nano 33 IoT e un BME280 collegato mediante bus I²C. Il sensore fornisce temperatura, umidità relativa e pressione atmosferica. La pressione viene normalizzata al livello del mare mediante una quota configurabile. Un ingresso analogico misura inoltre la tensione di alimentazione attraverso un partitore resistivo.

Per ogni ciclo vengono ottenuti dieci gruppi validi. Le quattro serie vengono ordinate separatamente; per ciascuna vengono eliminati i due valori più bassi e i due più alti, quindi viene calcolata la media dei sei valori centrali. I risultati vengono trasmessi tramite HTTPS con una richiesta POST. Il programma è organizzato come macchina a stati, non utilizza delay(), riprova dopo cinque minuti in caso di errore e conta soltanto gli invii realmente completati.

Obiettivo della procedura

Il nodo deve eseguire automaticamente la seguente sequenza:

  1. inizializzare la porta seriale, il bus I²C e l’ADC;
  2. cercare il BME280 agli indirizzi 0x76 e 0x77;
  3. leggere temperatura, umidità, pressione locale e tensione;
  4. normalizzare la pressione alla quota configurata;
  5. rifiutare l’intero gruppo quando almeno un parametro non è valido;
  6. ottenere dieci gruppi validi entro cinquanta tentativi;
  7. ordinare separatamente le quattro serie;
  8. eliminare i due valori inferiori e i due superiori;
  9. calcolare quattro medie sui sei valori centrali;
  10. ripetere la validazione sui risultati filtrati;
  11. connettere il Wi-Fi soltanto dopo il completamento della misura;
  12. inviare i quattro risultati mediante HTTPS;
  13. controllare il codice HTTP restituito dal server;
  14. attendere trenta minuti dopo un invio riuscito;
  15. riprovare dopo cinque minuti in caso di errore;
  16. riavviare la scheda dopo ventiquattro invii riusciti.

Acquisizione, filtro, comunicazione e attesa sono quindi fasi distinte. Un errore di misura non produce un invio; un errore di rete non viene conteggiato come ciclo completato.

Componenti utilizzati

  • Arduino Nano 33 IoT;
  • modulo BME280 compatibile con alimentazione a 3,3 V;
  • resistenza superiore da 680 kΩ;
  • resistenza inferiore da 57 kΩ;
  • alimentazione da controllare;
  • rete Wi-Fi a 2,4 GHz;
  • server HTTPS con uno script di ricezione.

La Nano 33 IoT utilizza livelli logici a 3,3 V. Il BME280 deve essere alimentato e collegato in modo compatibile con tali livelli. La tensione da controllare non viene applicata direttamente ad A0, ma ridotta mediante il partitore resistivo.

Collegamenti del BME280

Il collegamento I²C utilizza i pin A4 e A5 della Nano 33 IoT:

BME280 VIN/VCC  -> 3V3
BME280 GND      -> GND
BME280 SDA      -> A4 / SDA
BME280 SCL      -> A5 / SCL

Il pin del modulo indicato come VIN o VCC viene collegato al pin 3V3 della scheda. Non deve essere confuso con il pin VIN della Nano 33 IoT.

I collegamenti devono essere eseguiti a scheda non alimentata. Un’inversione tra SDA e SCL, un corto verso massa o un modulo difettoso può bloccare il bus I²C durante l’inizializzazione.

Partitore per la misura della tensione

La tensione di alimentazione viene ridotta prima di raggiungere l’ingresso analogico A0:

alimentazione da misurare
        |
      680 kΩ
        |
        +----------> A0
        |
       57 kΩ
        |
       GND

Nel programma i valori sono dichiarati esplicitamente:

const uint8_t PIN_TENSIONE = A0;

const float R_SUPERIORE_OHM = 680000.0f;
const float R_INFERIORE_OHM = 57000.0f;

const float RAPPORTO_PARTITORE =
  (R_SUPERIORE_OHM + R_INFERIORE_OHM) /
  R_INFERIORE_OHM;

Con questi valori il rapporto teorico è circa 12,93. Il dato calcolato deve comunque essere verificato con un multimetro: tolleranze delle resistenze, riferimento ADC reale, impedenza del partitore e cablaggio possono produrre uno scostamento sistematico.

Librerie e preparazione dell’ambiente

Lo sketch utilizza le seguenti librerie:

#include <SPI.h>
#include <WiFiNINA.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <math.h>

Nell’IDE deve essere selezionata la scheda Arduino Nano 33 IoT. Devono essere installate la libreria WiFiNINA, la libreria Adafruit BME280 e la dipendenza Adafruit Unified Sensor.

Prima dell’uso di WiFiSSLClient il certificato del dominio HTTPS deve essere caricato nel modulo NINA mediante lo strumento di aggiornamento firmware e certificati dell’IDE. Il certificato deve corrispondere al dominio configurato in serverHost.

Configurazione dei tempi

const unsigned long INTERVALLO_CICLO_MS =
  30UL * 60UL * 1000UL;

const unsigned long RETRY_ERRORE_MS =
  5UL * 60UL * 1000UL;

const unsigned long INTERVALLO_LETTURE_MS =
  300UL;

const unsigned long WIFI_TIMEOUT_MS =
  90000UL;

const unsigned long HTTP_TIMEOUT_MS =
  15000UL;
  • 30 minuti dopo un invio riuscito;
  • 5 minuti dopo un errore di sensore, Wi-Fi o HTTP;
  • 300 millisecondi tra due gruppi di letture;
  • 90 secondi per ottenere la connessione Wi-Fi;
  • 15 secondi per ricevere la prima riga HTTP.

Il suffisso UL forza i calcoli nel tipo unsigned long, coerente con il valore restituito da millis().

Assenza di attese bloccanti

Il programma non utilizza delay(). Ogni attesa viene controllata mediante una differenza tra valori di millis():

if (millis() - ultimaLettura <
    INTERVALLO_LETTURE_MS) {
  return;
}

Lo stesso metodo viene utilizzato per il timeout Wi-Fi, il timeout HTTP e l’intervallo prima del ciclo successivo. La sottrazione tra valori unsigned long mantiene il funzionamento anche quando il contatore di millis() ritorna a zero.

Macchina a stati

Il programma è suddiviso in cinque stati:

enum StatoSistema {
  STATO_AVVIO_CICLO,
  STATO_LETTURE,
  STATO_ATTESA_WIFI,
  STATO_ATTESA_HTTP,
  STATO_ATTESA_CICLO
};

StatoSistema stato = STATO_AVVIO_CICLO;

Il loop() richiama esclusivamente la funzione associata allo stato corrente:

void loop() {
  switch (stato) {

    case STATO_AVVIO_CICLO:
      avviaCiclo();
      break;

    case STATO_LETTURE:
      gestisciLetture();
      break;

    case STATO_ATTESA_WIFI:
      gestisciWiFi();
      break;

    case STATO_ATTESA_HTTP:
      gestisciHTTP();
      break;

    case STATO_ATTESA_CICLO:
      if (millis() - inizioAttesaCiclo >=
          intervalloAttesaAttuale) {
        stato = STATO_AVVIO_CICLO;
      }
      break;
  }
}

Inizializzazione della scheda

Nel setup() vengono inizializzati la porta seriale, l’ingresso analogico, la risoluzione ADC e il bus I²C:

void setup() {
  Serial.begin(115200);

  unsigned long inizioAttesaSeriale = millis();

  while (!Serial &&
         millis() - inizioAttesaSeriale < 10000UL) {
  }

  pinMode(PIN_TENSIONE, INPUT);
  analogReadResolution(12);
  Wire.begin();

  bmeDisponibile = inizializzaBME();
  stato = STATO_AVVIO_CICLO;
}

L’attesa della porta seriale è limitata a dieci secondi. In questo modo i primi messaggi sono visibili quando il monitor seriale è aperto, ma il nodo continua comunque a funzionare anche senza un computer collegato.

Ricerca dell’indirizzo I²C

I moduli BME280 possono utilizzare l’indirizzo 0x76 oppure 0x77. Il programma prova entrambi:

bool inizializzaBME() {
  if (bme.begin(0x76)) {
    return true;
  }

  if (bme.begin(0x77)) {
    return true;
  }

  return false;
}

Se nessun indirizzo risponde, il ciclo non produce valori sostitutivi. Il sistema attende cinque minuti e tenta nuovamente l’inizializzazione.

Pressione locale e pressione al livello del mare

Il BME280 restituisce la pressione locale. Per confrontarla con una pressione meteorologica riferita al livello del mare viene applicata una correzione basata sulla quota:

const float ALTITUDINE_M = 160.0f;

float normalizzaPressione(float pressioneLocale) {
  return pressioneLocale /
    powf(
      1.0f - (ALTITUDINE_M / 44330.0f),
      5.255f
    );
}

ALTITUDINE_M deve essere sostituita con la quota della specifica installazione. Il valore ottenuto è una pressione calcolata al livello del mare; non è una seconda misura diretta del sensore.

Configurazione dell’ADC

La risoluzione viene impostata a 12 bit:

analogReadResolution(12);

Il fondo scala numerico diventa quindi 4095:

const float ADC_RIFERIMENTO_V = 3.300f;
const float ADC_FONDO_SCALA = 4095.0f;

La tensione presente su A0 viene calcolata dal valore RAW e poi moltiplicata per il rapporto del partitore:

float leggiTensioneAlimentazione() {
  int raw = analogRead(PIN_TENSIONE);

  float tensioneA0 =
    ((float)raw * ADC_RIFERIMENTO_V) /
    ADC_FONDO_SCALA;

  return tensioneA0 * RAPPORTO_PARTITORE;
}

La prima chiamata a analogRead() viene scartata come precauzione aggiuntiva prima della misura utilizzata. Tuttavia, con un partitore ad alta impedenza questo accorgimento non sostituisce la verifica del tempo di acquisizione dell’ADC: nel SAMD21 il tempo di campionamento è configurabile tramite SAMPCTRL.SAMPLEN e deve essere adeguato all’impedenza della sorgente per garantire la precisione richiesta.

Limiti di validità

Prima del filtro ogni gruppo deve rispettare tutti i limiti:

const float TEMP_MIN_VALIDA = -10.0f;
const float TEMP_MAX_VALIDA = 50.0f;

const float HUM_MIN_VALIDA = 0.0f;
const float HUM_MAX_VALIDA = 100.0f;

const float PRESS_SLM_MIN_VALIDA = 850.0f;
const float PRESS_SLM_MAX_VALIDA = 1100.0f;

const float TENSIONE_MIN_VALIDA = 0.0f;
const float TENSIONE_MAX_VALIDA = 5.2f;

Le soglie non rappresentano l’intero campo fisico dichiarato dal produttore. Sono limiti operativi scelti per impedire che valori incompatibili con l’installazione entrino nella serie temporale.

bool misuraValida(
  float temperatura,
  float umidita,
  float pressioneSLM,
  float tensione
) {
  return (
    isfinite(temperatura) &&
    isfinite(umidita) &&
    isfinite(pressioneSLM) &&
    isfinite(tensione) &&

    temperatura >= TEMP_MIN_VALIDA &&
    temperatura <= TEMP_MAX_VALIDA &&

    umidita >= HUM_MIN_VALIDA &&
    umidita <= HUM_MAX_VALIDA &&

    pressioneSLM >= PRESS_SLM_MIN_VALIDA &&
    pressioneSLM <= PRESS_SLM_MAX_VALIDA &&

    tensione >= TENSIONE_MIN_VALIDA &&
    tensione <= TENSIONE_MAX_VALIDA
  );
}

Acquisizione dei gruppi di misura

Ogni tentativo produce quattro grandezze:

float temperatura = bme.readTemperature();
float umidita = bme.readHumidity();
float pressioneLocale = bme.readPressure() / 100.0f;
float pressioneSLM = normalizzaPressione(pressioneLocale);
float tensione = leggiTensioneAlimentazione();

Un gruppo viene memorizzato soltanto se tutte le grandezze sono valide:

if (misuraValida(
      temperatura,
      umidita,
      pressioneSLM,
      tensione
    )) {

  lettureTemperatura[numeroLettureValide] =
    temperatura;

  lettureUmidita[numeroLettureValide] =
    umidita;

  letturePressioneSLM[numeroLettureValide] =
    pressioneSLM;

  lettureTensione[numeroLettureValide] =
    tensione;

  numeroLettureValide++;
}

Non vengono quindi costruite serie con campioni provenienti da istanti differenti. Se la pressione è errata, non vengono conservate separatamente temperatura e umidità dello stesso tentativo.

Dieci gruppi validi e cinquanta tentativi massimi

const uint8_t NUM_LETTURE_VALIDE = 10;
const uint8_t MAX_TENTATIVI_LETTURA = 50;

Il nodo continua a campionare fino a ottenere dieci gruppi validi. Ogni tentativo, valido o scartato, incrementa numeroTentativi. Se il limite di cinquanta viene raggiunto, il sensore viene considerato non affidabile per quel ciclo e non viene effettuato alcun invio.

if (numeroTentativi >=
    MAX_TENTATIVI_LETTURA) {

  bmeDisponibile = false;
  terminaCiclo(RETRY_ERRORE_MS);
}

Ordinamento delle quattro serie

Temperatura, umidità, pressione e tensione vengono ordinate separatamente mediante la stessa funzione:

void ordinaValori(float valori[]) {
  for (uint8_t i = 0;
       i < NUM_LETTURE_VALIDE - 1;
       i++) {

    for (uint8_t j = i + 1;
         j < NUM_LETTURE_VALIDE;
         j++) {

      if (valori[j] < valori[i]) {
        float temporaneo = valori[i];
        valori[i] = valori[j];
        valori[j] = temporaneo;
      }
    }
  }
}

Dopo l’ordinamento le posizioni 0 e 1 contengono i due valori più bassi, mentre le posizioni 8 e 9 contengono i due valori più alti.

Filtro mediante media troncata

La media viene calcolata esclusivamente sulle posizioni da 2 a 7:

float mediaTroncata(float valori[]) {
  ordinaValori(valori);

  float somma = 0.0f;

  for (uint8_t i = 2; i <= 7; i++) {
    somma += valori[i];
  }

  return somma / 6.0f;
}

Il metodo elimina complessivamente il 40% dei campioni: due in coda inferiore e due in coda superiore. Riduce l’influenza di disturbi impulsivi, ma non corregge un errore sistematico presente in tutti i campioni.

temperaturaMedia =
  mediaTroncata(lettureTemperatura);

umiditaMedia =
  mediaTroncata(lettureUmidita);

pressioneSLMMedia =
  mediaTroncata(letturePressioneSLM);

tensioneMedia =
  mediaTroncata(lettureTensione);

Seconda validazione dopo il filtro

I risultati filtrati vengono controllati nuovamente prima della connessione Wi-Fi:

if (!misuraValida(
      temperaturaMedia,
      umiditaMedia,
      pressioneSLMMedia,
      tensioneMedia
    )) {

  bmeDisponibile = false;
  terminaCiclo(RETRY_ERRORE_MS);
  return;
}

Questa seconda verifica impedisce che un valore finale non finito o fuori soglia venga trasmesso in seguito a un problema di calcolo o di memoria.

Separazione tra misura e connessione Wi-Fi

Il Wi-Fi viene attivato soltanto dopo il completamento del filtro. La connessione viene avviata con credenziali generiche negli esempi pubblicati:

const char* ssid = "NOME_RETE";
const char* password = "PASSWORD_RETE";
void avviaWiFi() {
  client.stop();
  WiFi.disconnect();

  inizioStato = millis();

  WiFi.begin(ssid, password);

  stato = STATO_ATTESA_WIFI;
}

Le credenziali reali non devono essere inserite nell’articolo, pubblicate nel repository o lasciate in copie dello sketch distribuite a terzi.

Verifica della connessione Wi-Fi

Lo stato WL_CONNECTED viene associato alla presenza di un indirizzo IP non nullo:

bool ipValido(IPAddress ip) {
  return (
    ip[0] != 0 ||
    ip[1] != 0 ||
    ip[2] != 0 ||
    ip[3] != 0
  );
}

bool wifiPronto() {
  return (
    WiFi.status() == WL_CONNECTED &&
    ipValido(WiFi.localIP())
  );
}

Se la connessione non viene completata entro novanta secondi, il contatore degli errori Wi-Fi viene incrementato. Dopo dieci errori consecutivi la scheda viene riavviata.

Preparazione dei dati POST

Il server e il percorso vengono indicati senza riferimenti personali:

const char* serverHost = "server.example";
const char* serverPath =
  "/ricezione/logger.php";

const uint16_t serverPort = 443;

Il corpo della richiesta utilizza il formato application/x-www-form-urlencoded:

String postData =
  "temp=" + String(temperaturaMedia, 2);

postData +=
  "&hum=" + String(umiditaMedia, 2);

postData +=
  "&press=" + String(pressioneSLMMedia, 2);

postData +=
  "&volt=" + String(tensioneMedia, 3);

Un corpo valido può assumere la forma:

temp=21.42&hum=54.18&press=1015.36&volt=4.187

Invio tramite HTTPS

La connessione cifrata utilizza un WiFiSSLClient:

WiFiSSLClient client;
if (!client.connect(serverHost, serverPort)) {
  registraErroreHTTP(
    "[HTTPS] connessione fallita"
  );
  return;
}

Dopo l’apertura della connessione viene costruita manualmente la richiesta HTTP/1.1:

client.print("POST ");
client.print(serverPath);
client.println(" HTTP/1.1");

client.print("Host: ");
client.println(serverHost);

client.println(
  "User-Agent: Nano-33-IoT-BME280"
);

client.println(
  "Content-Type: application/x-www-form-urlencoded"
);

client.print("Content-Length: ");
client.println(postData.length());

client.println("Connection: close");
client.println();

client.print(postData);

Content-Length deve corrispondere al numero esatto di caratteri presenti in postData. La riga vuota separa le intestazioni HTTP dal corpo della richiesta.

Controllo della risposta HTTP

Il nodo legge la prima riga della risposta, per esempio:

HTTP/1.1 200 OK

Il codice numerico viene estratto senza dipendere dal testo successivo:

int estraiCodiceHTTP(const String& riga) {
  if (!riga.startsWith("HTTP/")) {
    return -1;
  }

  int primoSpazio = riga.indexOf(' ');

  if (primoSpazio < 0) {
    return -1;
  }

  if (riga.length() < primoSpazio + 4) {
    return -1;
  }

  return riga.substring(
    primoSpazio + 1,
    primoSpazio + 4
  ).toInt();
}

L’invio viene considerato riuscito per qualsiasi codice compreso tra 200 e 299. Soltanto in questo caso il contatore degli invii completati viene incrementato.

Errori, retry e riavvio

Sono gestiti separatamente:

  • BME280 assente;
  • numero insufficiente di gruppi validi;
  • risultato filtrato fuori soglia;
  • timeout Wi-Fi;
  • connessione HTTPS non riuscita;
  • risposta HTTP non valida;
  • connessione chiusa senza risposta;
  • timeout della risposta.

Dopo un errore il ciclo viene ripetuto dopo cinque minuti:

terminaCiclo(RETRY_ERRORE_MS);

Dopo un invio riuscito l’attesa è invece di trenta minuti:

terminaCiclo(INTERVALLO_CICLO_MS);

Il riavvio viene eseguito dopo dieci errori Wi-Fi consecutivi, cinque errori HTTP consecutivi oppure ventiquattro invii riusciti. I tentativi falliti non incrementano il contatore dei cicli completati.

Ricezione e validazione sulla VPS

La cifratura HTTPS protegge il trasporto, ma non dimostra che i valori ricevuti siano fisicamente plausibili. Il server deve ripetere i controlli prima di scrivere il file storico.

<?php
declare(strict_types=1);

date_default_timezone_set("UTC");
header("Content-Type: text/plain; charset=UTF-8");

$logfile = __DIR__ . "/dati.csv";

function errore(int $codice, string $messaggio): void {
    http_response_code($codice);
    echo "ERRORE: $messaggio\n";
    exit;
}

$tempRaw  = $_POST["temp"]  ?? "";
$humRaw   = $_POST["hum"]   ?? "";
$pressRaw = $_POST["press"] ?? "";
$voltRaw  = $_POST["volt"]  ?? "";

if (
    $tempRaw === "" ||
    $humRaw === "" ||
    $pressRaw === "" ||
    $voltRaw === ""
) {
    errore(400, "parametro mancante");
}

if (
    !is_numeric($tempRaw) ||
    !is_numeric($humRaw) ||
    !is_numeric($pressRaw) ||
    !is_numeric($voltRaw)
) {
    errore(400, "parametro non numerico");
}

$temp  = (float)$tempRaw;
$hum   = (float)$humRaw;
$press = (float)$pressRaw;
$volt  = (float)$voltRaw;

if (!is_finite($temp) || $temp < -10.0 || $temp > 50.0) {
    errore(400, "temperatura fuori scala");
}

if (!is_finite($hum) || $hum < 0.0 || $hum > 100.0) {
    errore(400, "umidita fuori scala");
}

if (!is_finite($press) || $press < 850.0 || $press > 1100.0) {
    errore(400, "pressione fuori scala");
}

if (!is_finite($volt) || $volt < 0.0 || $volt > 5.2) {
    errore(400, "tensione fuori scala");
}

$line = sprintf(
    "%s\t%.2f\t%.2f\t%.2f\t%.3f\n",
    date("Y-m-d H:i:s"),
    $temp,
    $hum,
    $press,
    $volt
);

if (file_put_contents(
        $logfile,
        $line,
        FILE_APPEND | LOCK_EX
    ) === false) {
    errore(500, "scrittura file");
}

echo "OK: $line";
?>

Il timestamp viene prodotto dal server in UTC. Il file contiene data e ora, temperatura, umidità, pressione e tensione, separati da tabulazioni. La scrittura utilizza LOCK_EX per impedire sovrapposizioni tra richieste simultanee.

Formato della riga archiviata

Una riga valida assume la forma:

2026-08-01 15:10:13    39.20    28.27    1012.09    4.168

I campi sono:

  1. timestamp UTC prodotto dalla VPS;
  2. temperatura in gradi Celsius;
  3. umidità relativa in percentuale;
  4. pressione normalizzata al livello del mare in hPa;
  5. tensione ricostruita in volt.

Il valore della tensione viene conservato con tre decimali, mentre la visualizzazione grafica può essere arrotondata senza modificare il dato originale archiviato.

Sequenza procedurale completa

  1. avviare la porta seriale a 115200 baud;
  2. attendere la porta seriale per un massimo di dieci secondi;
  3. configurare A0 come ingresso;
  4. configurare l’ADC a 12 bit;
  5. avviare il bus I²C;
  6. cercare il BME280 a 0x76;
  7. se necessario cercarlo a 0x77;
  8. azzerare i contatori delle letture;
  9. acquisire temperatura, umidità, pressione locale e tensione;
  10. calcolare la pressione al livello del mare;
  11. scartare l’intero gruppo se almeno un parametro non è valido;
  12. ripetere ogni 300 millisecondi;
  13. ottenere dieci gruppi validi entro cinquanta tentativi;
  14. ordinare separatamente le quattro serie;
  15. eliminare quattro valori per ogni serie;
  16. calcolare la media dei sei valori centrali;
  17. validare nuovamente i quattro risultati;
  18. attivare il Wi-Fi;
  19. verificare connessione e indirizzo IP;
  20. aprire la connessione TLS sulla porta 443;
  21. costruire il corpo POST;
  22. inviare intestazioni e dati;
  23. leggere la prima riga della risposta;
  24. considerare riusciti i codici da 200 a 299;
  25. incrementare il contatore soltanto dopo un invio riuscito;
  26. attendere trenta minuti in condizioni normali;
  27. attendere cinque minuti dopo un errore;
  28. riavviare dopo ventiquattro invii riusciti.

Controlli prima della messa in servizio

  • verificare che il modulo risponda a 0x76 o 0x77;
  • controllare che SDA e SCL non siano invertiti;
  • misurare con un multimetro la tensione reale;
  • confrontare il multimetro con il valore calcolato da A0;
  • verificare il valore reale delle due resistenze;
  • impostare la quota dell’installazione;
  • controllare dal monitor seriale i gruppi scartati;
  • verificare che si ottengano dieci gruppi validi;
  • controllare l’RSSI Wi-Fi;
  • verificare la presenza del certificato TLS;
  • controllare la risposta HTTP del server;
  • verificare l’ultima riga scritta nel file CSV;
  • provare intenzionalmente un valore fuori scala sul server;
  • verificare che il server risponda con errore 400.

Limiti della misura

Il filtro riduce l’effetto di campioni isolati, ma non corregge:

  • riscaldamento del sensore dovuto alla scheda o al contenitore;
  • esposizione diretta alla radiazione solare;
  • ventilazione insufficiente;
  • quota impostata in modo errato;
  • deriva del BME280;
  • errore sistematico del riferimento ADC;
  • tolleranza delle resistenze del partitore;
  • cadute di tensione lungo il cablaggio;
  • condensa o infiltrazioni;
  • interruzioni della rete;
  • modifiche hardware non annotate nella serie storica.

Una sostituzione della scheda, del BME280, del cablaggio o del contenitore deve essere registrata. Le serie ottenute prima e dopo la modifica non devono essere presentate come perfettamente omogenee senza una verifica comparativa.

Sketch completo

Il codice seguente contiene esclusivamente identificatori generici per rete e server. Prima del caricamento devono essere sostituiti NOME_RETE, PASSWORD_RETE, server.example e il percorso del ricevitore.

/*
  Nano 33 IoT - Nodo meteo BME280

  COLLEGAMENTI BME280
  --------------------------------------------------
  VCC  -> alimentazione compatibile con il modulo
  GND  -> GND
  SDA  -> SDA
  SCL  -> SCL

  MISURA DELLA TENSIONE
  --------------------------------------------------
  Alimentazione da misurare -> R1 680 kOhm -> A0
  A0                        -> R2 57 kOhm  -> GND

  FUNZIONAMENTO
  --------------------------------------------------
  - BME280 su I2C, ricerca automatica 0x76 / 0x77.
  - Pressione normalizzata al livello del mare, quota 160 m.
  - ADC a 12 bit.
  - Acquisizione di 10 gruppi validi.
  - Intervallo di 300 ms tra due gruppi.
  - Massimo 50 tentativi.
  - Ogni gruppo contiene:
      temperatura
      umidita relativa
      pressione normalizzata al livello del mare
      tensione reale di alimentazione
  - Se un parametro e fuori intervallo, viene scartato
    l'intero gruppo.
  - Ordinamento separato delle quattro serie.
  - Eliminazione dei 2 valori piu bassi e dei 2 piu alti.
  - Media dei 6 valori centrali.
  - Wi-Fi connesso soltanto per l'invio.
  - Invio HTTPS ogni 30 minuti.
  - Dopo un errore sensore, Wi-Fi o HTTP riprova dopo 5 minuti.
  - Campi POST:
      temp
      hum
      press
      volt
  - Nessun delay().
  - Riavvio dopo:
      10 errori Wi-Fi consecutivi
      5 errori HTTP consecutivi
      24 invii riusciti, circa 12 ore
*/

#include <SPI.h>
#include <WiFiNINA.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <math.h>


// ==================================================
// CONFIGURAZIONE DEI TEMPI
// ==================================================

const unsigned long INTERVALLO_CICLO_MS =
  30UL * 60UL * 1000UL;          // 30 minuti

const unsigned long RETRY_ERRORE_MS =
  5UL * 60UL * 1000UL;           // 5 minuti

const unsigned long INTERVALLO_LETTURE_MS =
  300UL;                         // 300 millisecondi

const unsigned long WIFI_TIMEOUT_MS =
  90000UL;                       // 90 secondi

const unsigned long HTTP_TIMEOUT_MS =
  15000UL;                       // 15 secondi


// ==================================================
// CONFIGURAZIONE DEL BME280
// ==================================================

const float ALTITUDINE_M = 160.0f;


// ==================================================
// CONFIGURAZIONE DELLA MISURA DI TENSIONE
// ==================================================

const uint8_t PIN_TENSIONE = A0;

const float ADC_RIFERIMENTO_V = 3.300f;
const float ADC_FONDO_SCALA = 4095.0f;

const float R_SUPERIORE_OHM = 680000.0f;
const float R_INFERIORE_OHM = 57000.0f;

const float RAPPORTO_PARTITORE =
  (R_SUPERIORE_OHM + R_INFERIORE_OHM) /
  R_INFERIORE_OHM;


// ==================================================
// CONFIGURAZIONE DEL FILTRO
// ==================================================

const uint8_t NUM_LETTURE_VALIDE = 10;
const uint8_t MAX_TENTATIVI_LETTURA = 50;


// ==================================================
// LIMITI DI VALIDITA
// ==================================================

const float TEMP_MIN_VALIDA = -10.0f;
const float TEMP_MAX_VALIDA = 50.0f;

const float HUM_MIN_VALIDA = 0.0f;
const float HUM_MAX_VALIDA = 100.0f;

/*
  La pressione controllata e quella normalizzata
  al livello del mare.
*/
const float PRESS_SLM_MIN_VALIDA = 850.0f;
const float PRESS_SLM_MAX_VALIDA = 1100.0f;

/*
  Il limite superiore di 5,2 V lascia margine
  per le tolleranze del riferimento ADC e del partitore.
*/
const float TENSIONE_MIN_VALIDA = 0.0f;
const float TENSIONE_MAX_VALIDA = 5.2f;


// ==================================================
// LIMITI DI ERRORE E RIAVVIO
// ==================================================

const uint8_t MAX_WIFI_FAIL = 10;
const uint8_t MAX_HTTP_FAIL = 5;
const uint8_t REBOOT_CICLI = 24;


// ==================================================
// RETE E SERVER
// ==================================================

const char* ssid = "NOME_RETE";
const char* password = "PASSWORD_RETE";

const char* serverHost = "server.example";
const char* serverPath =
  "/ricezione/logger.php";

const uint16_t serverPort = 443;


// ==================================================
// STATI DEL PROGRAMMA
// ==================================================

enum StatoSistema {
  STATO_AVVIO_CICLO,
  STATO_LETTURE,
  STATO_ATTESA_WIFI,
  STATO_ATTESA_HTTP,
  STATO_ATTESA_CICLO
};

StatoSistema stato = STATO_AVVIO_CICLO;


// ==================================================
// OGGETTI
// ==================================================

Adafruit_BME280 bme;
WiFiSSLClient client;


// ==================================================
// VETTORI DELLE LETTURE
// ==================================================

float lettureTemperatura[NUM_LETTURE_VALIDE];
float lettureUmidita[NUM_LETTURE_VALIDE];
float letturePressioneSLM[NUM_LETTURE_VALIDE];
float lettureTensione[NUM_LETTURE_VALIDE];


// ==================================================
// VARIABILI GENERALI
// ==================================================

bool bmeDisponibile = false;

uint8_t numeroLettureValide = 0;
uint8_t numeroTentativi = 0;

uint8_t wifiFail = 0;
uint8_t httpFail = 0;
uint8_t cicli = 0;

float temperaturaMedia = 0.0f;
float umiditaMedia = 0.0f;
float pressioneSLMMedia = 0.0f;
float tensioneMedia = 0.0f;

unsigned long ultimaLettura = 0;
unsigned long inizioStato = 0;
unsigned long inizioAttesaCiclo = 0;
unsigned long intervalloAttesaAttuale = INTERVALLO_CICLO_MS;

String rigaHTTP;


// ==================================================
// PROTOTIPI
// ==================================================

bool inizializzaBME();

bool ipValido(IPAddress ip);
bool wifiPronto();

bool misuraValida(
  float temperatura,
  float umidita,
  float pressioneSLM,
  float tensione
);

float leggiTensioneAlimentazione();
float normalizzaPressione(float pressioneLocale);

void riavvia(const char* motivo);
void terminaCiclo(unsigned long intervalloAttesaMs);

void avviaCiclo();
void gestisciLetture();

void ordinaValori(float valori[]);
float mediaTroncata(float valori[]);
void calcolaRisultato();

void avviaWiFi();
void gestisciWiFi();

void avviaHTTPS();
void gestisciHTTP();

int estraiCodiceHTTP(const String& riga);
void registraErroreHTTP(const char* motivo);


// ==================================================
// INIZIALIZZAZIONE DEL BME280
// ==================================================

bool inizializzaBME() {
  Serial.println("[BME280] ricerca indirizzo 0x76");

  if (bme.begin(0x76)) {
    Serial.println("[BME280] trovato a 0x76");
    return true;
  }

  Serial.println("[BME280] ricerca indirizzo 0x77");

  if (bme.begin(0x77)) {
    Serial.println("[BME280] trovato a 0x77");
    return true;
  }

  Serial.println("[BME280] ERRORE: sensore non trovato");
  return false;
}


// ==================================================
// CONTROLLO DELL'INDIRIZZO IP
// ==================================================

bool ipValido(IPAddress ip) {
  return (
    ip[0] != 0 ||
    ip[1] != 0 ||
    ip[2] != 0 ||
    ip[3] != 0
  );
}


// ==================================================
// CONTROLLO DELLA CONNESSIONE WI-FI
// ==================================================

bool wifiPronto() {
  return (
    WiFi.status() == WL_CONNECTED &&
    ipValido(WiFi.localIP())
  );
}


// ==================================================
// NORMALIZZAZIONE DELLA PRESSIONE
// ==================================================

float normalizzaPressione(float pressioneLocale) {
  return pressioneLocale /
    powf(
      1.0f - (ALTITUDINE_M / 44330.0f),
      5.255f
    );
}


// ==================================================
// LETTURA DELLA TENSIONE DI ALIMENTAZIONE
// ==================================================

float leggiTensioneAlimentazione() {
  int raw = analogRead(PIN_TENSIONE);

  float tensioneA0 =
    ((float)raw * ADC_RIFERIMENTO_V) /
    ADC_FONDO_SCALA;

  return tensioneA0 * RAPPORTO_PARTITORE;
}


// ==================================================
// VALIDAZIONE COMPLETA DI UN GRUPPO
// ==================================================

bool misuraValida(
  float temperatura,
  float umidita,
  float pressioneSLM,
  float tensione
) {
  return (
    isfinite(temperatura) &&
    isfinite(umidita) &&
    isfinite(pressioneSLM) &&
    isfinite(tensione) &&

    temperatura >= TEMP_MIN_VALIDA &&
    temperatura <= TEMP_MAX_VALIDA &&

    umidita >= HUM_MIN_VALIDA &&
    umidita <= HUM_MAX_VALIDA &&

    pressioneSLM >= PRESS_SLM_MIN_VALIDA &&
    pressioneSLM <= PRESS_SLM_MAX_VALIDA &&

    tensione >= TENSIONE_MIN_VALIDA &&
    tensione <= TENSIONE_MAX_VALIDA
  );
}


// ==================================================
// RIAVVIO DELLA SCHEDA
// ==================================================

void riavvia(const char* motivo) {
  client.stop();
  WiFi.disconnect();

  Serial.println();
  Serial.println(motivo);

  NVIC_SystemReset();
}


// ==================================================
// AVVIO DI UN NUOVO CICLO
// ==================================================

void avviaCiclo() {
  numeroLettureValide = 0;
  numeroTentativi = 0;

  Serial.println();
  Serial.println("========================================");

  Serial.print("[CICLO] invii riusciti ");
  Serial.print(cicli);
  Serial.print("/");
  Serial.println(REBOOT_CICLI);

  if (!bmeDisponibile) {
    bmeDisponibile = inizializzaBME();

    if (!bmeDisponibile) {
      Serial.println(
        "[CICLO] misura impossibile: BME280 assente"
      );

      terminaCiclo(RETRY_ERRORE_MS);
      return;
    }
  }

  /*
    La prima lettura viene eseguita immediatamente.
  */
  ultimaLettura =
    millis() - INTERVALLO_LETTURE_MS;

  stato = STATO_LETTURE;
}


// ==================================================
// ACQUISIZIONE DELLE LETTURE
// ==================================================

void gestisciLetture() {
  if (
    millis() - ultimaLettura <
    INTERVALLO_LETTURE_MS
  ) {
    return;
  }

  ultimaLettura = millis();

  float temperatura =
    bme.readTemperature();

  float umidita =
    bme.readHumidity();

  float pressioneLocale =
    bme.readPressure() / 100.0f;

  float pressioneSLM =
    normalizzaPressione(pressioneLocale);

  float tensione =
    leggiTensioneAlimentazione();

  numeroTentativi++;

  Serial.print("[LETTURA] tentativo ");
  Serial.print(numeroTentativi);

  Serial.print("  T=");
  Serial.print(temperatura, 2);

  Serial.print("  UR=");
  Serial.print(umidita, 2);

  Serial.print("  Pslm=");
  Serial.print(pressioneSLM, 2);

  Serial.print("  V=");
  Serial.print(tensione, 3);


  // ------------------------------------------------
  // Controllo di validita del gruppo completo
  // ------------------------------------------------

  if (
    misuraValida(
      temperatura,
      umidita,
      pressioneSLM,
      tensione
    )
  ) {
    lettureTemperatura[numeroLettureValide] =
      temperatura;

    lettureUmidita[numeroLettureValide] =
      umidita;

    letturePressioneSLM[numeroLettureValide] =
      pressioneSLM;

    lettureTensione[numeroLettureValide] =
      tensione;

    numeroLettureValide++;

    Serial.print("  VALIDA ");
    Serial.print(numeroLettureValide);
    Serial.print("/");
    Serial.println(NUM_LETTURE_VALIDE);

  } else {

    Serial.println("  SCARTATA");
  }


  // ------------------------------------------------
  // Ottenute 10 letture valide
  // ------------------------------------------------

  if (
    numeroLettureValide >=
    NUM_LETTURE_VALIDE
  ) {
    calcolaRisultato();

    /*
      Secondo controllo dopo il filtro.
    */
    if (
      !misuraValida(
        temperaturaMedia,
        umiditaMedia,
        pressioneSLMMedia,
        tensioneMedia
      )
    ) {
      Serial.println(
        "[FILTRO] ERRORE: risultato finale non valido"
      );

      bmeDisponibile = false;
      terminaCiclo(RETRY_ERRORE_MS);
      return;
    }

    avviaWiFi();
    return;
  }


  // ------------------------------------------------
  // Massimo numero di tentativi raggiunto
  // ------------------------------------------------

  if (
    numeroTentativi >=
    MAX_TENTATIVI_LETTURA
  ) {
    Serial.println(
      "[SENSORE] ERRORE: letture valide insufficienti"
    );

    bmeDisponibile = false;
    terminaCiclo(RETRY_ERRORE_MS);
  }
}


// ==================================================
// ORDINAMENTO CRESCENTE
// ==================================================

void ordinaValori(float valori[]) {
  for (
    uint8_t i = 0;
    i < NUM_LETTURE_VALIDE - 1;
    i++
  ) {
    for (
      uint8_t j = i + 1;
      j < NUM_LETTURE_VALIDE;
      j++
    ) {
      if (valori[j] < valori[i]) {
        float temporaneo = valori[i];

        valori[i] = valori[j];
        valori[j] = temporaneo;
      }
    }
  }
}


// ==================================================
// MEDIA DEI SEI VALORI CENTRALI
// ==================================================

float mediaTroncata(float valori[]) {
  ordinaValori(valori);

  float somma = 0.0f;

  /*
    Vengono utilizzate le posizioni:
    2, 3, 4, 5, 6 e 7.
  */
  for (uint8_t i = 2; i <= 7; i++) {
    somma += valori[i];
  }

  return somma / 6.0f;
}


// ==================================================
// CALCOLO DEI RISULTATI FILTRATI
// ==================================================

void calcolaRisultato() {
  temperaturaMedia =
    mediaTroncata(lettureTemperatura);

  umiditaMedia =
    mediaTroncata(lettureUmidita);

  pressioneSLMMedia =
    mediaTroncata(letturePressioneSLM);

  tensioneMedia =
    mediaTroncata(lettureTensione);

  Serial.println("[FILTRO] risultati finali");

  Serial.print("[TEMPERATURA] ");
  Serial.print(temperaturaMedia, 2);
  Serial.println(" C");

  Serial.print("[UMIDITA] ");
  Serial.print(umiditaMedia, 2);
  Serial.println(" %");

  Serial.print("[PRESSIONE SLM] ");
  Serial.print(pressioneSLMMedia, 2);
  Serial.println(" hPa");

  Serial.print("[TENSIONE] ");
  Serial.print(tensioneMedia, 3);
  Serial.println(" V");
}


// ==================================================
// AVVIO DELLA CONNESSIONE WI-FI
// ==================================================

void avviaWiFi() {
  client.stop();
  WiFi.disconnect();

  Serial.println("[WiFi] connessione");

  inizioStato = millis();

  WiFi.begin(ssid, password);

  stato = STATO_ATTESA_WIFI;
}


// ==================================================
// ATTESA DEL WI-FI
// ==================================================

void gestisciWiFi() {
  if (wifiPronto()) {
    wifiFail = 0;

    Serial.print("[WiFi] OK IP: ");
    Serial.println(WiFi.localIP());

    Serial.print("[WiFi] RSSI: ");
    Serial.print(WiFi.RSSI());
    Serial.println(" dBm");

    avviaHTTPS();
    return;
  }

  if (
    millis() - inizioStato >=
    WIFI_TIMEOUT_MS
  ) {
    WiFi.disconnect();

    if (wifiFail < 255) {
      wifiFail++;
    }

    Serial.print("[WiFi] FALLITO ");
    Serial.print(wifiFail);
    Serial.print("/");
    Serial.println(MAX_WIFI_FAIL);

    if (wifiFail >= MAX_WIFI_FAIL) {
      riavvia(
        "[REBOOT] troppi errori WiFi"
      );
    }

    terminaCiclo(RETRY_ERRORE_MS);
  }
}


// ==================================================
// CONNESSIONE HTTPS E INVIO DEL POST
// ==================================================

void avviaHTTPS() {
  Serial.println("[HTTPS] connessione");

  client.stop();
  client.setTimeout(HTTP_TIMEOUT_MS);

  if (!client.connect(serverHost, serverPort)) {
    registraErroreHTTP(
      "[HTTPS] connessione fallita"
    );

    return;
  }

  Serial.println("[HTTPS] connesso");

  String postData =
    "temp=" + String(temperaturaMedia, 2);

  postData +=
    "&hum=" + String(umiditaMedia, 2);

  postData +=
    "&press=" + String(pressioneSLMMedia, 2);

  postData +=
    "&volt=" + String(tensioneMedia, 3);

  client.print("POST ");
  client.print(serverPath);
  client.println(" HTTP/1.1");

  client.print("Host: ");
  client.println(serverHost);

  client.println(
    "User-Agent: Nano-33-IoT-BME280"
  );

  client.println(
    "Content-Type: application/x-www-form-urlencoded"
  );

  client.print("Content-Length: ");
  client.println(postData.length());

  client.println("Connection: close");
  client.println();

  client.print(postData);

  rigaHTTP = "";

  inizioStato = millis();

  stato = STATO_ATTESA_HTTP;
}


// ==================================================
// ATTESA DELLA RISPOSTA HTTP
// ==================================================

void gestisciHTTP() {
  while (client.available()) {
    char carattere = client.read();

    if (carattere == '\n') {
      rigaHTTP.trim();

      Serial.print("[HTTPS] ");
      Serial.println(rigaHTTP);

      int codice =
        estraiCodiceHTTP(rigaHTTP);

      client.stop();
      WiFi.disconnect();

      if (codice >= 200 && codice < 300) {
        httpFail = 0;

        Serial.println("[HTTP] POST riuscito");

        if (cicli < 255) {
          cicli++;
        }

        terminaCiclo(INTERVALLO_CICLO_MS);
        return;
      }

      registraErroreHTTP(
        "[HTTP] risposta non valida"
      );

      return;
    }

    if (
      carattere != '\r' &&
      rigaHTTP.length() < 100
    ) {
      rigaHTTP += carattere;
    }
  }

  if (
    !client.connected() &&
    !client.available()
  ) {
    registraErroreHTTP(
      "[HTTP] connessione chiusa senza risposta"
    );

    return;
  }

  if (
    millis() - inizioStato >=
    HTTP_TIMEOUT_MS
  ) {
    registraErroreHTTP(
      "[HTTP] timeout risposta"
    );
  }
}


// ==================================================
// ESTRAZIONE DEL CODICE HTTP
// ==================================================

int estraiCodiceHTTP(const String& riga) {
  if (!riga.startsWith("HTTP/")) {
    return -1;
  }

  int primoSpazio =
    riga.indexOf(' ');

  if (primoSpazio < 0) {
    return -1;
  }

  if (
    riga.length() <
    primoSpazio + 4
  ) {
    return -1;
  }

  return riga.substring(
    primoSpazio + 1,
    primoSpazio + 4
  ).toInt();
}


// ==================================================
// GESTIONE DEGLI ERRORI HTTP
// ==================================================

void registraErroreHTTP(const char* motivo) {
  client.stop();
  WiFi.disconnect();

  if (httpFail < 255) {
    httpFail++;
  }

  Serial.println(motivo);

  Serial.print("[HTTP] errori consecutivi ");
  Serial.print(httpFail);
  Serial.print("/");
  Serial.println(MAX_HTTP_FAIL);

  if (httpFail >= MAX_HTTP_FAIL) {
    riavvia(
      "[REBOOT] troppi errori HTTP"
    );
  }

  terminaCiclo(RETRY_ERRORE_MS);
}


// ==================================================
// FINE DEL CICLO
// ==================================================

void terminaCiclo(unsigned long intervalloAttesaMs) {
  client.stop();
  WiFi.disconnect();

  Serial.print("[CICLO] invii riusciti ");
  Serial.print(cicli);
  Serial.print("/");
  Serial.println(REBOOT_CICLI);

  /*
    Il riavvio periodico viene contato sugli invii riusciti.
    I retry non anticipano il riavvio.
  */
  if (cicli >= REBOOT_CICLI) {
    riavvia(
      "[REBOOT] completati 24 invii riusciti"
    );
  }

  intervalloAttesaAttuale = intervalloAttesaMs;
  inizioAttesaCiclo = millis();
  stato = STATO_ATTESA_CICLO;

  if (intervalloAttesaAttuale == INTERVALLO_CICLO_MS) {
    Serial.println(
      "[CICLO] prossimo invio tra 30 minuti"
    );
  } else {
    Serial.println(
      "[RETRY] nuovo tentativo tra 5 minuti"
    );
  }
}


// ==================================================
// SETUP
// ==================================================

void setup() {
  Serial.begin(115200);

  unsigned long inizioAttesaSeriale = millis();

  while (!Serial &&
         millis() - inizioAttesaSeriale < 10000UL) {
  }

  pinMode(PIN_TENSIONE, INPUT);

  analogReadResolution(12);

  Wire.begin();

  Serial.println();
  Serial.println(
    "=== BOOT NANO 33 IOT BME280 ==="
  );

  bmeDisponibile =
    inizializzaBME();

  stato = STATO_AVVIO_CICLO;
}


// ==================================================
// LOOP PRINCIPALE
// ==================================================

void loop() {
  switch (stato) {

    case STATO_AVVIO_CICLO:
      avviaCiclo();
      break;

    case STATO_LETTURE:
      gestisciLetture();
      break;

    case STATO_ATTESA_WIFI:
      gestisciWiFi();
      break;

    case STATO_ATTESA_HTTP:
      gestisciHTTP();
      break;

    case STATO_ATTESA_CICLO:

      if (
        millis() - inizioAttesaCiclo >=
        intervalloAttesaAttuale
      ) {
        stato = STATO_AVVIO_CICLO;
      }

      break;
  }
}

Conclusioni

La Nano 33 IoT permette di concentrare acquisizione, filtraggio e trasmissione in un nodo compatto. Il BME280 fornisce tre grandezze ambientali mediante I²C, mentre A0 viene utilizzato per controllare la tensione di alimentazione attraverso un partitore resistivo.

La procedura non invia la prima lettura disponibile. Ottiene dieci gruppi validi, elimina i valori estremi, calcola quattro medie troncate e ripete il controllo di validità prima di connettere il Wi-Fi. La risposta del server viene verificata e soltanto un codice HTTP da 200 a 299 incrementa il contatore degli invii riusciti.

La VPS applica un secondo filtro prima della scrittura. La qualità della serie temporale dipende quindi dall’intera catena: sensore, collegamenti, alimentazione, calibrazione, firmware, rete, ricevitore e archiviazione. Nessuno di questi elementi può essere sostituito dalla sola presenza di molti decimali nel risultato.

Fonti principali

Arduino, documentazione tecnica della Nano 33 IoT.
Microchip Technology, documentazione tecnica del microcontrollore ATSAMD21G18A.
u-blox, documentazione tecnica del modulo NINA-W102.
Bosch Sensortec, datasheet del sensore BME280.
Arduino, documentazione della libreria WiFiNINA.
Adafruit, documentazione della libreria Adafruit BME280.
RFC 9110, HTTP Semantics.
Codice operativo del nodo di misura e dello script di ricezione descritti nell’articolo.