Da dove partiamo
Nell'articolo sull'hardware mesh abbiamo
citato ZephCore due volte: ha già
i driver per i chip LoRa di nuova generazione (LR1110 e LR2021) e una build
per l'nRF54L15, che il firmware ufficiale non può supportare perché gira su
un core Arduino solo per nRF52. Lo abbiamo poi rimesso nel
percorso tra le risorse di awesome-meshcore,
fra i firmware alternativi.
Il punto però non è solo l'hardware. ZephCore cambia il modo in cui il
firmware passa il tempo. Il firmware ufficiale gira in un ciclo che
controlla tutto, di continuo. ZephCore invece aspetta che succeda qualcosa.
Questo articolo si basa sul codice di ZephCore 1.18.0 e sul ramo principale
di MeshCore di metà ottobre 2026.
Stesso protocollo, sotto è tutto diverso
ZephCore non è un fork del protocollo. Un nodo ZephCore parla con i nodi
MeshCore normali, si accoppia con le stesse app e risponde agli stessi
comandi della CLI. Il formato dei pacchetti in aria è bloccato a quello
ufficiale: il documento di progetto lo chiama frozen, e ogni funzione in
più deve restare locale al nodo.
Cambia quello che sta sotto. Al posto di Arduino c'è
Zephyr, un sistema operativo real-time per
microcontrollori della Linux Foundation. I driver radio, il Bluetooth, la
USB e il file system sono quelli di Zephyr. Il codice della mesh
(Dispatcher, Mesh, Packet, le identità) è invece copiato da MeshCore
quasi riga per riga: ogni modifica è marcata con un commento
// ZEPHCORE:, così un confronto con l'originale mostra solo le differenze
volute. Quando MeshCore esce con una versione nuova, ZephCore la importa.
La 1.18.0 di ZephCore contiene le modifiche della 1.18.0 ufficiale.
|
MeshCore ufficiale |
ZephCore |
| Base |
Arduino (core Adafruit su nRF52, Arduino-ESP32 su ESP32) |
Zephyr RTOS |
| Ciclo principale |
loop() chiamato di continuo |
thread che dorme finché arriva un evento |
| Radio |
RadioLib, l'interrupt alza un flag |
driver Zephyr, l'interrupt mette il pacchetto in coda e sveglia il thread |
| Ritardo prima di ritrasmettere |
txdelay fisso |
finestra adattiva |
| Processori |
nRF52, ESP32, RP2040, STM32 |
nRF52, nRF54L, ESP32, EFR32MG24, STM32WL, Linux |
| Ruoli |
companion, repeater, room server, KISS modem |
gli stessi più l'observer (MQTT) |
| Chi lo scrive |
il progetto MeshCore |
una persona che dirige, con il codice scritto al 99% da AI |
Il loop del firmware ufficiale
Un programma Arduino ha due funzioni: setup(), che parte una volta, e
loop(), che viene chiamata all'infinito. Il repeater ufficiale fa così. A
ogni giro loop():
- legge la seriale, un carattere alla volta, per vedere se è arrivato un
comando della CLI;
- chiama
the_mesh.loop(), che a sua volta chiede alla radio se ha finito
di trasmettere, se ha ricevuto un pacchetto, se c'è qualcosa da spedire;
- aggiorna sensori, display e orologio;
- se
powersaving è attivo e non c'è lavoro in sospeso, mette il
processore a dormire.
La radio non chiama nessuno. Quando l'SX1262 riceve un pacchetto, alza il
pin DIO1 e l'interrupt imposta un flag (STATE_INT_READY nel wrapper di
RadioLib). Sarà il giro successivo del loop ad accorgersene. È un modello
detto polling: il programma chiede di continuo «c'è qualcosa?».
Il polling è semplice da leggere e da correggere, ed è il motivo per cui
MeshCore gira su schede tanto diverse. Ha due costi. Il primo è l'energia:
se powersaving è spento, che è il default, il processore non si ferma mai.
Il secondo è che ogni pezzo di codice nel loop rallenta tutti gli altri:
se uno si blocca, si blocca tutto.
Va detto per correttezza: con powersaving on su nRF52 anche il firmware
ufficiale dorme fra un evento e l'altro, e si sveglia con qualunque
interrupt. Ma al risveglio rifà comunque tutto il giro, per scoprire cosa
è successo.
Niente loop: un thread che aspetta
In ZephCore la mesh vive in un solo thread, il thread principale di Zephyr.
Quel thread passa quasi tutto il tempo fermo in una chiamata,
k_event_wait(), che si sblocca solo quando qualcuno alza uno dei bit di
evento: pacchetto LoRa ricevuto, trasmissione finita, comando arrivato da
USB o Bluetooth, scadenza di manutenzione, azione del GPS.
Il percorso di un pacchetto ricevuto è questo:
- la radio alza DIO1;
- il driver Zephyr legge il pacchetto e lo scrive in un buffer circolare
da 8 posti;
- il driver alza l'evento
MESH_EVENT_LORA_RX;
- il thread principale si sveglia, guarda quali bit sono alzati e svuota il
buffer con lo stesso
Dispatcher del firmware ufficiale;
- finito il lavoro, torna ad aspettare.
Nessuno chiede di continuo «c'è qualcosa?». Quando non c'è niente, il
processore non ha niente da eseguire e Zephyr lo mette a dormire da solo.
Il codice ha una regola esplicita: lo stato della mesh si tocca solo dal
thread principale. Bluetooth, USB, GPS e driver radio girano in contesti
separati e passano i dati attraverso code ed eventi. È lo stesso principio
del firmware ufficiale, un solo flusso che possiede la mesh, ma senza il
giro a vuoto.
Le scadenze al posto del timer
Restava un pezzo di polling. Fino alla 1.16.7 il repeater ZephCore si
svegliava comunque ogni 5 secondi, per fare la manutenzione: misurare il
rumore di fondo, controllare il timer degli advert, rimettere a posto una
configurazione radio temporanea.
Dalla 1.16.8 il repeater è guidato dalle scadenze. Prima di addormentarsi
chiede «quando è il prossimo lavoro?» e programma un'unica sveglia per quel
momento. Una sveglia di sicurezza ogni 60 secondi copre il caso di una
scadenza dimenticata. Il documento di progetto riporta circa 0,07 risvegli
al secondo per un repeater fermo, cioè uno ogni 14 secondi circa. Oggi il
room server condivide lo stesso ciclo del repeater, e quindi le stesse
sveglie.
Il companion tiene il timer da 5 secondi, ed è una scelta dichiarata: deve
controllare cose che non hanno una scadenza, come l'avanzamento della lista
contatti verso l'app o il Bluetooth che smette di farsi vedere.
Una nota sui consumi: in un repeater la parte grossa la fa la radio,
sempre in ascolto, come avevamo scritto nell'articolo sull'hardware. Un
processore che dorme meglio aiuta, ma non cambia l'ordine di grandezza.
ZephCore ha anche una leva sulla radio: un ciclo di ascolto in cui il chip
LoRa sonnecchia fra un controllo del preambolo e l'altro. È opzionale e va
valutata scheda per scheda: dormire troppo vuol dire perdere pacchetti.
Le macchine a stati
Un firmware senza loop non può più scrivere «aspetta che la radio finisca»
come una riga dentro un ciclo. Ogni componente che ha un'attesa deve sapere
in che fase si trova e cosa la fa passare alla fase dopo. In pratica,
diventa una macchina a stati esplicita. In ZephCore ce ne sono diverse:
- La radio (
LoRaRadio): ricezione, ascolto prima di parlare,
trasmissione, ritorno in ricezione. Ogni passaggio è innescato da un
interrupt del chip, non da un controllo periodico.
- Il GPS: spento, in acquisizione, in attesa. Se è impostato un
intervallo, dopo 5 posizioni buone spegne il modulo e lo riaccende alla
scadenza successiva.
- Il Dispatcher: coda di trasmissione, controllo del canale libero,
duty cycle. È la stessa logica del firmware ufficiale.
La regola che le tiene insieme è scritta nel progetto: ogni attesa ha un
limite e un modo di uscirne. ZephCore non usa il watchdog hardware, il
timer che riavvia il processore se il programma smette di rispondere.
Ogni macchina a stati ha invece il proprio ripristino. Se il canale
risulta occupato per più di 4 secondi, la radio viene resettata e rimessa
in ricezione invece di trasmettere alla cieca. Se un chip LR1110 resta
bloccato senza interrupt per 12 secondi, un controllo separato lo
riconosce e lo riavvia.
È un approccio più faticoso da scrivere, ma che si può verificare. Il
nucleo della mesh gira anche sul computer, dentro una batteria di test
automatici con gli strumenti che trovano gli errori di memoria (ASan e
UBSan). Su un loop che controlla l'hardware a ogni giro sarebbe più
difficile.
Il ritardo adattivo al posto di txdelay
Nell'articolo sul tempo d'aria condiviso abbiamo
visto come funziona il ritardo casuale prima di ritrasmettere un flood: la
finestra è fissa, 5 × txdelay × tempo in aria, e alzare txdelay sui
repeater in collina riduce le collisioni al prezzo di più latenza.
ZephCore sostituisce quel parametro con tre meccanismi:
- Finestra adattiva. Il repeater misura quanto è affollato il suo
intorno e allarga o stringe la finestra di conseguenza. Il tetto è il
minore fra 2 secondi e 6 volte il tempo in aria del pacchetto.
- Rinvio a ogni doppione. Se mentre aspetta sente un altro nodo
ritrasmettere lo stesso pacchetto, rimanda la propria trasmissione di
una frazione del tempo in aria, fino a un massimo.
- Finestra corta per i companion. Un companion che non ripete usa una
finestra fissa fra 20 e 150 ms per i messaggi che parte lui.
La conseguenza pratica è importante: su ZephCore set txdelay,
set rxdelay e set direct.txdelay vengono accettati ma ignorati. Il
consiglio del nostro articolo precedente non si applica. L'unica leva è
set backoff.multiplier (default 0,2), che regola il rinvio per doppione;
get txdelay mostra lo stato del meccanismo adattivo.
Queste scelte non cambiano niente in aria: un nodo ZephCore in mezzo a nodi
ufficiali è solo un nodo che sceglie il momento in modo diverso. Non
abbiamo misure sul campo che dicano se una mesh mista funzioni meglio o
peggio. Se ne hai, scrivici.
Le altre differenze
- Più processori e chip radio. nRF52840, nRF54L15, la famiglia ESP32,
EFR32MG24 e STM32WL; per le radio SX126x, SX127x, LR1110 e LR2021. Con
l'LR2021 si può impostare una frequenza a 2,4 GHz. In Italia per ora non
serve: nell'articolo sull'hardware abbiamo visto perché.
- Un ruolo in più, l'observer. Un nodo ESP32 che ascolta e basta, e
pubblica tutto quello che sente su un server MQTT via Wi-Fi. Anche un
repeater ESP32 può mandare i pacchetti a MQTT mentre ripete.
- Sincronizzazione dell'ora. I nodi senza GPS possono mettersi d'accordo
sull'ora usando i timestamp firmati degli advert dei vicini. È spenta di
default (
set meshtimesync on).
- LED configurabili.
set leds off li spegne tutti; si può scegliere
cosa segnala ciascuno, per esempio un lampeggio a ogni pacchetto
ricevuto: a colpo d'occhio si vede se un repeater sente qualcosa.
- Gira anche su Linux. Lo stesso codice gira come programma normale su
un Raspberry Pi con un SX1262 collegato.
Prima di installarlo
- Le impostazioni non passano. Fra firmware ufficiale e ZephCore, in
entrambe le direzioni, contatti, canali e configurazione non vengono
conservati. Su un repeater vuol dire rifare password, nome, preset radio e
posizione. Se il nodo ha un'identità che i vicini conoscono, esportala
prima.
- Su nRF52 il bootloader resta quello. Si installa trascinando il file
UF2, come il firmware ufficiale, e si torna indietro allo stesso modo.
- L'installazione più semplice è il
web flasher di ZephCore, da Chrome o Edge.
- Le schede nRF sono le più provate. Il manutentore prova le release
soprattutto su schede nRF; le schede ESP32 sono provate in buona parte
dalla community.
- Il codice è scritto quasi tutto da AI. Lo dice il README stesso:
circa il 99% del codice è scritto da modelli AI, una persona decide cosa
costruire e prova i risultati su radio vere. Le decisioni sono
documentate, ci sono test automatici e i bug trovati vengono segnalati
anche al progetto ufficiale. Resta un progetto guidato da una persona
sola, e conviene saperlo.
- Non è il firmware ufficiale. Se chiedi aiuto nel gruppo, dillo
subito: un comportamento strano su ZephCore può non avere niente a che
vedere con MeshCore, e viceversa.
Cosa ci portiamo a casa
Per chi usa un companion, ZephCore è soprattutto un modo di avere MeshCore
su schede che il firmware ufficiale non supporta. Per chi gestisce un
repeater, è un esperimento interessante su due fronti: un processore che
dorme davvero quando non c'è traffico e un ritardo di ritrasmissione che
si adatta da solo invece di essere regolato a mano.
Il modo giusto di provarlo è su un nodo che controlli da vicino, non sul
repeater in cima alla collina che serve mezza provincia. E se lo provi,
raccontaci com'è andata nel gruppo: stabilità, consumi, cosa senti in più
o in meno.
Fonti