ELAI S.r.l.

L’AI deve funzionare anche senza Internet: che cosa lasciare sul dispositivo?

Un caso riproducibile spiega quali funzioni mantenere locali, quanta memoria riservare e come recuperare lo storico dopo dodici ore senza rete.

L’AI deve funzionare anche senza Internet: che cosa lasciare sul dispositivo?

Quando cade la rete, quale lavoro deve continuare?

Una serra remota raccoglie misure ambientali e le invia a un servizio che segnala condizioni anomale. Se Internet manca per dodici ore, il problema non è soltanto mostrare un messaggio di errore: bisogna decidere se continuare a misurare, conservare lo storico e produrre un' indicazione locale. Lo stesso problema riguarda un impianto con rete intermittente. Questo approfondimento aiuta chi deve scegliere tra servizio remoto, dispositivo locale e architettura mista: quali dipendenze eliminare dal percorso indispensabile, quanta memoria riservare e come recuperare i dati senza confondere il passato con il presente.

La risposta non è «spostare tutta l' AI sulla scheda». Occorre separare funzioni immediate, archivio e analisi differibile. Dimostreremo con una simulazione eseguita che conservare dodici ore di dati può richiedere poco spazio e tuttavia perderne una parte se il budget è scelto male; inoltre il ritorno della rete non elimina istantaneamente l' arretrato. Non abbiamo provato sensori, firmware, modelli, memoria flash o protocolli reali. Il caso è ipotetico, i calcoli sono riproducibili e le prestazioni di qualunque dispositivo restano da misurare. Non è un progetto di controllo automatico della serra né una prova agronomica.

Separare una decisione utile da una connessione disponibile

Il percorso minimo comprende acquisire una misura, verificarne formato e qualità, applicare la funzione locale scelta e segnalare chiaramente il suo stato. Una soglia esplicita può bastare per un avviso semplice; un modello è giustificato solo se il problema richiede capacità ulteriori e una valutazione ne dimostra il vantaggio. Un' AI locale deve disporre anche di preprocessing, parametri, dati ausiliari e runtime: scaricare i pesi non elimina una chiamata remota nascosta. Una verifica concreta consiste nel disconnettere la rete in ambiente di prova e registrare quali dipendenze impediscono la funzione, senza dedurre il risultato dalla sola architettura disegnata.

Una funzione locale non deve fingere che tutto sia normale quando il sensore è guasto o mancano aggiornamenti indispensabili. Distinguere «misura valida», «dati insufficienti», «modello non disponibile» e «collegamento assente» consente all' operatore di capire il limite. Nel caso ipotetico l' uscita locale è un' indicazione, non un comando a una pompa: eventuali attuatori richiedono vincoli e verifiche propri. Le analisi che possono aspettare, come il confronto dello storico tra più serre, possono restare sul server. Il confine va scelto in base al lavoro da mantenere, non alla preferenza per cloud o embedded.

Quanta memoria serve per dodici ore senza rete?

Definiamo il contratto del dato prima di scegliere la scheda. Supponiamo un record ogni dieci secondi e 64 byte serializzati per record, includendo identificatore, tempo, valore e stato. La dimensione è ipotetica: non è quella di un oggetto Python, di un pacchetto di rete o di una pagina di database. Ogni requisito di integrità o metadato aggiuntivo va conteggiato nella serializzazione reale. Il periodo di dieci secondi non è una raccomandazione di campionamento per una coltura. Serve a rendere esplicito il rapporto tra frequenza e spazio.

La formula risponde alla domanda «quanti record arrivano durante l' interruzione?». lambda è il tasso in record al secondo, D la durata in secondi e b la dimensione in byte per record. B è il numero di record da conservare, S lo spazio per i soli record. Consideriamo l' intervallo che include l' inizio ma esclude la fine: campioni a 0, 10, …, 43190 secondi. Non confondiamo unità decimali e binarie: un KiB equivale a 1024 byte.

lambda = 1/10 = 0.1 records/s B = lambda * D = 0.1 * 43200 = 4320 records S = B*b = 4320*64 = 276480 bytes = 270 KiB

Un' area di 256 KiB contiene soltanto 4096 record da 64 byte. È quindi più piccola dei 270 KiB necessari, anche se a colpo d' occhio i due numeri sembrano vicini. Se si conserva il dato più recente eliminando il più vecchio, alla fine delle dodici ore mancano già 224 record. La scelta protegge il presente ma perde storia: non è automaticamente migliore di rifiutare il nuovo dato. Se lo scopo è ricostruire un evento iniziale, proprio quei record possono essere importanti. La politica di esaurimento deve derivare dall' uso dello storico.

270 KiB è soltanto il payload dell' arretrato, non il requisito totale della piattaforma. Vanno separati pesi del modello, attivazioni e picco RAM, buffer del protocollo, indice dei record e spazio persistente. Anche la memoria libera nominale può includere aree riservate o soggette a cancellazione per blocchi. Per questo non deduciamo da un conto sui record che una scheda specifica possa eseguire l' applicazione. La prima decisione utile è riservare un budget esplicito e poi verificarlo con il formato, il firmware e il supporto fisico scelti.

La rete torna: perché lo storico non è ancora sincronizzato

Supponiamo ora che il server possa acquisire stabilmente due record utili al secondo. È una capacità di servizio ipotetica, non una misura della radio o una velocità di collegamento in bit al secondo. Deve includere il collo di bottiglia effettivo del percorso fino all' esito utile; ritrasmissioni e attese possono ridurla. Nel frattempo la misura locale continua a produrre 0,1 record al secondo. L' arretrato scende per la differenza tra uscita e ingresso, non alla velocità di uscita da sola. È come svuotare un contenitore mentre un rubinetto continua a riempirlo: l' analogia spiega il bilancio, non rappresenta guasti e pacchetti.

mu = 2 records/s net_drain = mu - lambda = 1.9 records/s t_recovery ≈ B/(mu-lambda) = 4320/1.9 = 2273.6842 s

Il conto continuo dà circa 37,9 minuti, non 36 minuti ottenuti dividendo per due. Richiede mu maggiore di lambda: se sono uguali il vecchio arretrato non si riduce, se l' uscita è inferiore cresce ancora. La formula assume ritmi costanti, servizio immediatamente disponibile e nessun' altra interruzione. Per ondate di traffico o pause di riconnessione serve un modello temporale più ricco. Il criterio pratico è prevedere margine per recuperare lo storico, oltre alla capacità di sostenere il flusso normale: una connessione appena sufficiente quando tutto funziona può non recuperare mai un guasto.

La simulazione: tre capacità, stessi eventi

Per controllare la formula usiamo eventi discreti, cioè arrivi e partenze che accadono in istanti definiti. Un passo vale mezzo secondo. All' inizio e ogni venti passi nasce un record numerato. La rete non rimuove nulla fino al passo 86400, corrispondente a dodici ore; il primo completamento è mezzo secondo dopo. Poi si completa un invio a ogni passo finché la coda si svuota per la prima volta. Se arrivo e partenza coincidono, inseriamo prima il nuovo record. Quando il contenitore è pieno eliminiamo il più vecchio. Nessun numero casuale è coinvolto: non serve un seed.

Con 8192 posti la coda raggiunge 4321 record, non 4320: arriva un campione anche nell' istante di ritorno della rete, prima del primo invio. Il primo svuotamento avviene 2274 secondi dopo il ritorno, 37 minuti e 54 secondi, coerente con l' approssimazione continua. In quel momento sono stati generati e caricati 4548 record: ai 4320 dell' interruzione si aggiungono 228 nuovi campioni. Il fatto che il server riceva più record dell' arretrato iniziale non è una duplicazione: la misura non si è fermata durante il recupero. Il modello termina lì, non simula tutto il resto della giornata.

PostiSpazio (KiB)PersiGeneratiCaricatiRecupero (s)
102464.0329743741077538.5
4096256.0225453643112155.5
8192512.00454845482274.0

Con 4096 posti perdiamo 225 record: i 224 già persi nell' interruzione più quello espulso al ritorno della rete prima del primo completamento. Il recupero finisce in 2155,5 secondi, meno di quello senza perdite. Con 1024 posti finisce ancora prima, in 538,5 secondi, ma mancano 3297 record. È un risultato da leggere correttamente: «sincronizzazione più rapida» può nascondere dati eliminati. La tabella riporta anche generati e caricati: ogni riga ha una durata diversa perché il programma termina al primo svuotamento; non sono tre finestre temporali identiche su cui confrontare la produzione totale.

Coda simulata per tre capacità: una zona piatta può nascondere espulsioni. Nessuna memoria, rete o piattaforma hardware misurata.
Coda simulata per tre capacità: una zona piatta può nascondere espulsioni. Nessuna memoria, rete o piattaforma hardware misurata.

Nel grafico osservare il tratto piatto delle code piccole: non significa che il sensore abbia smesso di produrre, ma che ogni nuovo arrivo espelle il dato più vecchio. Dopo dodici ore le curve scendono; solo la capacità maggiore conserva tutti i record nel protocollo scelto. Gli assi rappresentano tempo di simulazione e numero di record, non misure di RAM o rete. La capienza dell' area serializzata non coincide con la memoria del processo Python: la deque contiene oggetti e metadati aggiuntivi, che non stiamo usando come misura di una scheda.

Dati salvati e messaggi ricevuti non sono la stessa garanzia

La specifica OASIS MQTT 5.0, standard del 7 marzo 2019, distingue livelli di consegna. Con QoS 1 possono esserci duplicati; una conferma del protocollo non implica che l' applicazione finale abbia già completato il proprio lavoro. Sono proprietà del protocollo, non risultati di un nostro test MQTT.

Per progettare il servizio di archivio proponiamo quindi un' identità applicativa stabile per ogni record, per esempio dispositivo, epoca di avvio e contatore persistente coerente. Il server può registrare quell' identità con vincolo di unicità e rendere ripetibile l' operazione senza aggiungere una seconda riga. Questo schema va verificato anche se una risposta si perde dopo la scrittura. Non basta riusare un contatore che riparte da zero dopo un reset: potrebbe identificare due campioni diversi come lo stesso. Anche la politica di cancellazione locale deve indicare quale conferma consente di liberare spazio; «il pacchetto è partito» non è una conferma dell' archivio.

La documentazione SQLite sull' atomic commit spiega che il comportamento dopo una caduta di alimentazione dipende anche dalle ipotesi sul supporto e sulle operazioni di flush. La modalità rollback e la modalità WAL usano meccanismi differenti. Non abbiamo provato SQLite né interruzioni di corrente.

La nostra coda vive soltanto nel processo di simulazione. Non dimostra sopravvivenza a reset, integrità della flash o atomicità della scrittura. Nel progetto reale occorre distinguere record acquisito, reso persistente, spedito e confermato; una ripartenza deve ricostruire lo stato senza cancellare dati non confermati. Database e log sono alternative da valutare con le risorse disponibili, non soluzioni intercambiabili da scegliere per slogan. Un microcontrollore e un sistema embedded Linux possono richiedere strategie diverse. La verifica decisiva interrompe alimentazione e comunicazione in punti controllati, con dati non operativi e un protocollo adatto al dispositivo.

Lo storico non deve comandare il presente

Riconnettere significa riportare sul server misure vecchie. Ogni record deve conservare il tempo di acquisizione distinto dal tempo di ricezione, oltre a qualità e versione della funzione locale. Altrimenti dodici ore di campioni caricati in pochi minuti possono sembrare un fenomeno appena accaduto. Il percorso di recupero dello storico dovrebbe essere distinto dal percorso di decisione corrente. Non rieseguire automaticamente vecchi comandi come se fossero richieste nuove: il significato di un avviso o di un' azione può scadere. Non stiamo proponendo un controllo sicuro di attuatori, ma separando due usi del dato che hanno requisiti diversi.

Che cosa verificare prima di scegliere la piattaforma

Il confronto fra alternative può ora essere concreto. Solo cloud: più semplice aggiornare l' analisi centrale, ma la funzione dipende dalla rete e una coda remota non conserva ciò che il dispositivo non ha mai inviato. Solo locale: la funzione può essere autonoma, ma aggiornamenti, risorse e manutenzione restano problemi. Misto: una funzione minima usa dati locali e archivia i record; il server aggiunge analisi differibile. Una regola senza AI può essere la funzione minima se soddisfa il problema. La simulazione confronta soltanto la coda, non misura qualità diagnostica o convenienza economica delle tre architetture.

Per verificare la decisione occorre un test sul dispositivo scelto con formato e versioni dichiarati: misurare picco RAM separato dai pesi, spazio realmente disponibile e overhead di archivio; latenza media, percentili e scadenza della funzione locale anche durante la sincronizzazione; potenza in watt ed energia per inferenza o decisione utile in joule, distinguendo acquisizione e radio. Sottoporre il sistema a un' interruzione più lunga del budget verifica la politica di saturazione. Ripetere dopo aggiornamento e reset verifica la continuità dello stato. Qui sono esperimenti proposti senza risultati, non misure implicite della scheda fotografata.

Leggere il codice e rispondere alla domanda iniziale

Nel codice simulate usa deque come contenitore ordinato, genera identificatori crescenti e rimuove il più vecchio quando pieno. Una rimozione è un completamento di servizio ipotizzato, non un pacchetto MQTT o una conferma di database. Le asserzioni impongono conservazione dei conteggi e assenza di sovrapposizione tra persi e caricati; verificano inoltre i risultati 225 perdite e 2274 secondi senza perdite. L' assenza di duplicati è una proprietà di questa simulazione, non una garanzia di rete. Il numero di passi determina il costo del programma; occupazione Python e durata del calcolo non sono benchmark embedded.

La conclusione è operativa: far restare sul dispositivo ciò che serve a produrre un' indicazione locale valida e dichiararne i limiti; conservare dati e stato con un budget calcolato; prevedere capacità di recupero superiore al flusso normale. Nel nostro esempio 256 KiB di payload non bastano per dodici ore e una coda più piccola si svuota prima perché ha perso dati. Una capacità di 8192 record evita perdite nel modello, ma non dimostra resistenza a corrente interrotta o adeguatezza di un prodotto. La decisione architetturale è ora motivata; la scelta della scheda e del modello richiede le prove ancora mancanti. Questo approfondimento non annuncia prodotti o installazioni embedded EL-AI.

Fonti e codice riproducibile

OASIS — MQTT 5.0, 7 March 2019; sections 4.3–4.6.

SQLite — Atomic Commit; storage assumptions and sections 8–9.

import json,sys
from collections import deque
from pathlib import Path
# One tick is 0.5 seconds. New sample precedes completion at coincident events.
OUTAGE_TICKS=86400
RECORD_BYTES=64
def simulate(capacity):
 q=deque();lost=[];uploaded=[];maxq=0;trace=[];generated=0;end=None
 for tick in range(120001):
  if tick%20==0:
   if len(q)>=capacity:lost.append(q.popleft())
   q.append(generated);generated+=1;maxq=max(maxq,len(q))
  if tick>OUTAGE_TICKS and q:uploaded.append(q.popleft())
  if tick%120==0:trace.append([tick/2,len(q)])
  if tick>OUTAGE_TICKS and not q:end=tick/2;break
 trace.append([end,0])
 assert generated==len(lost)+len(uploaded)+len(q)
 assert not(set(lost)&set(uploaded)) and len(uploaded)==len(set(uploaded))
 return dict(capacityRecords=capacity,capacityBytes=capacity*RECORD_BYTES,lost=len(lost),uploaded=len(uploaded),generated=generated,maxQueue=maxq,drainSeconds=end-OUTAGE_TICKS/2,trace=trace)
runs=[simulate(c) for c in [1024,4096,8192]]
assert runs[2]['lost']==0 and runs[2]['drainSeconds']==2274 and runs[2]['maxQueue']==4321
assert runs[1]['lost']==225
assert abs(4320/(2-.1)-2273.684210526316)<1e-9
assert 4096*64==262144 and 4320*64==276480
result=dict(python=sys.version.split()[0],seed=None,scope='Discrete deterministic buffer simulation only; no sensor, model, MQTT client, flash or device tested',samplePeriodSeconds=10,assumedRecordBytes=64,outageSeconds=43200,assumedServiceRecordsPerSecond=2,fluidApproxDrainSeconds=4320/(2-.1),runs=runs)
Path(__file__).with_name('results.json').write_text(json.dumps(result,indent=2),encoding='utf8')
print(json.dumps([{k:v for k,v in x.items() if k!='trace'} for x in runs]))

Codice, dati e istruzioni · JSON. Calcoli didattici eseguiti con Python 3.14.0; figure con Matplotlib 3.11.2. Analisi con assistenza AI, senza dichiarare peer review o revisione umana. Copertina originale ImageGen, illustrativa: non documenta persone, sedi o installazioni EL-AI. Fonti consultate il 27 ottobre 2026.