Risoluzione dei problemi#
«Sono passato alla modalità Station, ho salvato, riavviato, e non succede niente.»#
Leggi il log di avvio — questo percorso è molto strumentato:
esp_wifi_connect()è legale solo una volta che la stazione è davvero avviata (WIFI_EVENT_STA_START). La connessione è emessa da quel gestore e ogni tentativo registra il suo risultato.Se nessuno slot di Client Wi-Fi è abilitato con un SSID, il firmware scarica ogni slot e ti dice qual è l’errore («abilitato, ma il SSID è VUOTO» vs «ha un SSID, ma “Enable” non è spuntato»).
Solo-STA senza nulla a cui unirsi ripiega su AP+STA così che l’amministrazione web resti attiva.
I codici di ragione di disconnessione sono registrati:
Ragione |
Significato |
|---|---|
15, 204 |
password sbagliata |
201 |
SSID non visibile: nome sbagliato, fuori portata, o solo 5 GHz |
2 / 8 / 200 |
roaming ordinario / cadute dal lato AP |
«L’AP non associa affatto.»#
Un wifi_config_t azzerato lascia pmf_cfg.capable = false, e gli AP
WPA3 / WPA2-con-PMF-richiesto rifiutano tale stazione. Il firmware imposta
capable, non required, che funziona contro AP vecchi e nuovi.
«L’avvio si blocca ~5 secondi.»#
Atteso: modem_init() si blocca mentre ModemCalibrateSampleRate() misura il
clock reale dell’ADC. Una volta per avvio.
«I beacon all’avvio non trasmettono.»#
Atteso: aprs_service_start() gira prima di modem_init(), quindi i beacon
precoci vengono scartati con un log di debug fino a s_modemReady.
«Il LOOP TEST fallisce con “nessun pacchetto ricevuto di ritorno”.»#
Controlla l’attenuazione dell’ADC: il DAC oscilla il rail completo mentre
un’attenuazione di 0 dB misura solo ~0–1,1 V, tagliando il tono oltre la capacità
del demodulatore di agganciare. Il componente cabla ADC_ATTEN_DB_12, che è
corretto; se lo hai sovrascritto, ripristinalo. Conferma anche il cavo di loop
GPIO25 → GPIO33.
«L’IGate dice unverified.»#
aprs_mycall / aprs_passcode sbagliati. Il banner è registrato; anche la
riga di login esatta, inclusa la stringa di filtro, così che un filtro malformato
sia visibile subito.
«Tutto funziona ma aprs.fi non mostra la mia stazione.»#
Beacon: abilita il beacon di posizione e almeno uno di loc2rf / loc2inet,
e imposta coordinate reali. Ritrasmettere traffico non ti annuncia mai.
«9600 Bd perde frame.»#
Quella è la patologia che la frequenza dell’ADC, la dimensione del frame di
conversione e la separazione dei core sono stati cambiati per correggere (vedi
La catena di segnale DSP). Se hai sovrascritto MODEM_ADC_SAMPLERATE,
MODEM_ADC_CONV_FRAME, MODEM_DAC_TIMER_CORE o MODEM_ADC_ISR_CORE,
ripristinali. Conferma anche di alimentare audio piatto/di discriminatore.
«Il LED del PTT resta acceso in riposo.»#
La logica del PTT è corretta; la sua polarità è una costante di compilazione, e la
definizione di scheda distribuita è MODEM_PTT_ACTIVE_HIGH=1 (attivo-alto) nel
CMakeLists.txt di livello superiore. Attivo-alto significa che
riposo/non-attivato aziona il pin basso e attivato lo aziona alto;
attivo-basso è l’immagine speculare, quindi a riposo il pin resta alto e un LED su
quel pin resta acceso. Se il LED segue l’opposto di quanto ti aspetti, il tuo
stadio di pilotaggio inverte (un optoisolatore sì; un semplice NPN low-side no):
porta la macro all’altro valore e fai una ricompilazione pulita completa — il
valore è incorporato in afsk.c, quindi una compilazione incrementale non lo
recepirà.
«Il log di avvio dice gpio: conflict found for GPIO[26].»
=============================================================
Una chiamata a gpio_config() rivendica ogni pad che configura e avvisa
quando ne trova uno già rivendicato da un’altra chiamata; il pad viene
comunque configurato, quindi la riga è informativa.
GPIO26 è il pad del PTT nella definizione di scheda distribuita, ed è
configurato due volte di proposito: ptt_force_idle() in main.c lo porta
al livello di riposo come prima azione di app_main(), perché la linea di
manipolazione non resti flottante dopo il reset, e AFSK_init() lo
riconfigura all’avvio del modem, circa un secondo e mezzo dopo. La seconda
chiamata restituisce prima la rivendicazione, quindi la riga non dovrebbe
comparire. Se compare, è stato usato un ESP-IDF senza l’header interno
esp_gpio_reserve.h, e l’avviso è innocuo.
Un conflitto su qualsiasi altro pin è invece reale: due funzioni puntano
allo stesso pad. Controllate il pin di allarme messaggi nella pagina Message,
l’unico che la configurazione memorizza, e i pin fissi del CMakeLists.txt
radice (ADC, DAC, PTT, LED di stato).
«Telegram smette di rispondere dopo un po” di funzionamento, con “mbedtls_ssl_fetch_input” o “Socket is not connected” nel log.»#
Il percorso di polling mantiene aperta la connessione HTTPS verso l’API di
Telegram tra un ciclo e l’altro, così un long poll che non restituisce nulla
non paga un nuovo handshake TLS ogni dieci secondi. Se quella connessione
resta inattiva abbastanza a lungo, il peer o un NAT intermedio può chiuderla
in silenzio; il socket resta quindi obsoleto anche se localmente nulla se n’è
accorto. telegram_bot_client_call() tratta un errore di trasporto proprio
come segnale di questo: chiude forzatamente la connessione e ritenta la
richiesta su un socket appena aperto, fino a tre tentativi totali con una
pausa che cresce tra loro, così una singola sessione obsoleta si recupera da
sola entro la stessa chiamata. Se l’errore continua a ripetersi su ogni
tentativo, è la rete stessa a essere giù e non un singolo socket obsoleto;
verifica la connettività Wi-Fi/Internet e il token del bot.
«sendMessage fallisce con “ESP_ERR_HTTP_CONNECT” subito dopo l’arrivo di un aggiornamento, preceduto da “Dynamic Impl: alloc(…) failed”.»#
Un handshake TLS nuovo chiede all’heap i propri buffer di record come
allocazioni singole di pochi kilobyte ciascuna, quindi a decidere se riesce è
il più grande blocco contiguo libero, non l’heap libero totale.
L’allocatore dell’ESP-IDF registra il rifiuto come Dynamic Impl: alloc(...)
failed, mbedTLS lo trasforma in mbedtls_ssl_handshake returned -0x008D
e il trasporto vede ESP_ERR_HTTP_CONNECT.
Una sessione TLS viva trattiene un blocco di dimensione paragonabile finché
viene mantenuta, perciò il firmware non ne tiene mai due insieme. Il gestore
di trasmissione lavora con il keep-alive disattivato ed è quindi vuoto nel
momento in cui la chiamata ritorna, e la connessione di polling viene
rilasciata da telegram_release_poll_connection() subito prima di
qualunque richiesta in uscita, che è il momento che conta: una risposta parte
subito dopo l’arrivo di un lotto di aggiornamenti, con il payload e l’albero
decodificato ancora in memoria. Il polling paga un handshake in più al ciclo
successivo e nulla d’altro.
Ogni tentativo fallito viene registrato con l’heap libero e il blocco libero
più grande in quell’istante. Se il blocco più grande è ampiamente sopra i
quattro kilobyte e la chiamata fallisce comunque, il guasto è il collegamento
e non l’heap. Se non lo è, al dispositivo manca davvero memoria contigua:
abbassa rx_buffer_size sui gestori del client, oppure riduci quello che il
resto del firmware trattiene in quel momento.
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN non è la leva che sembra. È un tetto sul
record che il peer può inviare, e la catena di certificati presentata da
Telegram si aggira sui quattro kilobyte in un solo record, quindi abbassarlo
sotto quella cifra non risparmia memoria: fa fallire l’handshake del tutto, a
ogni tentativo e con qualsiasi stato dell’heap.
CONFIG_MBEDTLS_DYNAMIC_FREE_CONFIG_DATA lo è davvero, e non è gratuita. Con
CONFIG_MBEDTLS_DYNAMIC_BUFFER già attiva, libera le strutture di
certificato e chiave già analizzate appena l’handshake è concluso, invece di
tenerle per tutta la durata della sessione: sono alcuni kilobyte recuperati su
una scheda il cui pool principale gira con un margine di poche unità di
kilobyte. Seleziona inoltre CONFIG_MBEDTLS_DYNAMIC_FREE_CA_CERT di default,
ed è lì che sta l’insidia: un oggetto sessione che debba rifare l’handshake
deve prima farsi registrare di nuovo la CA. Questo firmware costruisce una
connessione TLS nuova per ognuna, quindi qui nulla riusa una sessione tra
handshake, ma il sintomo da sorvegliare è una prima richiesta che riesce
seguita da altre che falliscono nell’handshake anziché in modo casuale. Se
compare, disattiva l’opzione prima di guardare altrove.
«Stamattina l’heap libero è più basso di ieri sera e il log non dice nulla.»#
Ogni altra cifra di heap che questo firmware registra viene stampata dopo un guasto: le due righe del trasporto Telegram, il dettaglio di diagnosi allegato a un avvio fallito, il percorso di errore di scrittura degli archivi JSON. Descrivono l’heap a danno già fatto e non dicono nulla su come ci si è arrivati. Un minimo storico caduto alle tre del mattino non lascia quindi alcuna traccia di ciò che era in esecuzione.
La strumentazione permanente che risponde a quella domanda sta in
main/heap_monitor.c, è azionata dal tick a 1 Hz del servizio APRS e si
configura sotto APRS heap instrumentation in idf.py menuconfig.
Una riga per periodo. CONFIG_APRS_HEAP_REPORT è attivo di default ed
emette una riga ogni CONFIG_APRS_HEAP_REPORT_PERIOD_S secondi:
I (3600123) heap_monitor: free=104512 largest=45056 min_sum=41216
Tre cifre da leggere insieme. La dimensione libera è quanta memoria esiste; il blocco libero più grande è la maggiore allocazione singola ancora possibile, e le due che si allontanano indicano un heap che si frammenta anziché consumarsi; il minimo è il livello più basso dall’avvio, quindi un calo rientrato prima della riga successiva compare comunque lì. Tutte e tre sono lette per la memoria interna a 8 bit, la stessa classe che il trasporto stampa in caso di errore, così i due tipi di riga si possono confrontare. La riga costa tre interrogazioni all’allocatore per periodo e nessuna memoria.
Il default di Kconfig è dieci secondi — l”sdkconfig distribuito lo imposta a
60, ed è il valore da abbassare per primo quando si insegue un picco — perché il
periodo deve essere più breve degli eventi che intende cogliere, e su questo firmware sono brevi: un handshake TLS,
una riconnessione ad APRS-IS o un salvataggio di impostazioni raggiungono il
picco e rientrano in pochi secondi. Un campionatore più lento è
strutturalmente cieco a tutti loro — registra l’heap prima e dopo, mai durante,
e ogni riga che stampa è compatibile con un picco mai avvenuto. Il refresh a
1 Hz del cruscotto è ancora più fine, ma esiste solo finché un browser è aperto
sulla pagina, che non è quando avvengono l’avvio o una riconnessione non
presidiata.
Avvertimento
min_sum è una somma di minimi, non il minimo della somma. L’allocatore
tiene un livello minimo per ogni heap registrato e questa cifra li somma,
prendendo ogni termine nell’istante peggiore di quello specifico heap. Un ESP32 senza
PSRAM non ha un unico heap DRAM: le riserve della ROM e del PHY dividono la
DRAM interna in tre o quattro regioni non contigue, ciascuna registrata
separatamente.
Poiché i termini sono indipendenti, la somma è un limite inferiore del
minimo che il totale di heap libero ha davvero raggiunto, e il limite si
allenta al crescere del numero di heap. Un min_sum minuscolo ammette
quindi due letture che questa riga da sola non distingue: che tutte le regioni
fossero quasi vuote nello stesso momento, oppure che ciascuna abbia toccato il
fondo separatamente in un momento proprio e il totale non sia mai stato in
pericolo. Contano entrambe — una regione ferma vicino allo zero è un vero
guasto di frammentazione, perché heap_caps_malloc() da quel momento la
salta e l’allocatore si comporta come se non esistesse — ma richiedono lavori
diversi. Stabilisci quale delle due hai prima di modificare qualsiasi
allocazione, con il dettaglio qui sotto.
Quale heap si è davvero esaurito. CONFIG_APRS_HEAP_REPORT_PER_HEAP
accompagna ogni riga con la tabella che stampa il componente heap stesso: una
riga per ogni heap registrato, con indirizzo iniziale, dimensione, libero
attuale, blocco libero più grande e minimo libero storico. È questo che separa
le due letture precedenti. Se il minimo del pool principale è nell’ordine delle
decine di kilobyte e solo le piccole regioni D/IRAM segnano quasi zero, il
riepilogo era un artefatto della somma di minimi; se anche il minimo del pool
principale è vicino allo zero, la stazione è davvero arrivata a un passo
dall’esaurire la memoria. Disattivato di default perché sono più righe per
periodo: attivalo per l’esecuzione che risponde alla domanda, poi disattivalo.
Parentesi attorno ai percorsi pesanti. CONFIG_APRS_HEAP_BRACKET registra
l’heap libero e il blocco libero più grande subito prima e subito dopo ogni
passaggio noto per prendersi un morso grande o duraturo: l’avvio di Telegram, la
connessione ad APRS-IS, il caricamento della configurazione, l’anello del mirror
di console, la scansione WiFi, il caricamento OTA e ogni buffer di modulo web.
CONFIG_TELEGRAM_BOT_HEAP_BRACKET, sotto Telegram bot transport, fa lo
stesso per ogni richiesta del bot — ogni tentativo di una chiamata JSON, più il
caricamento multipart e il download del file, che aprono connessioni proprie. Le
due si annidano, portano le stesse cifre nella stessa classe di memoria e sono
pensate per essere attivate insieme.
La riga periodica dice quando l’heap si è mosso; una parentesi dice cosa lo ha mosso, perché le sue cifre sono prese ai due lati di un passaggio con un nome e non nell’istante in cui scadeva il periodo. Se le tacche della traccia periodica cadono dentro una parentesi, è quel passaggio a muovere l’heap; se cadono tra le parentesi, lo muove altro. Entrambe sono disattivate di default: sono due righe per evento e il percorso di polling ne genera uno ogni pochi secondi.
Attribuire la memoria a un task. Attiva CONFIG_HEAP_TASK_TRACKING
(Component config → Heap memory debugging) e comparirà
CONFIG_APRS_HEAP_REPORT_TASKS, che aggiunge la tabella riepilogativa per
task sotto ogni riga di heap, così una perdita reale nomina il suo proprietario
in un solo dump invece che in una settimana di bisezione. Il tracciamento costa
RAM per ogni allocazione viva e rallenta ogni allocazione e liberazione, quindi
appartiene a una build diagnostica: disattivalo dopo.
Margine di stack. CONFIG_APRS_STACK_REPORT è attivo di default ed emette
una riga per ogni task ogni CONFIG_APRS_STACK_REPORT_PERIOD_S secondi
(un’ora) con il livello minimo di stack di quel task:
I (3600130) heap_monitor: stack igate_task: 2712 bytes free at its worst
I (3600131) heap_monitor: stack wifi: 1544 bytes free at its worst
Gli stack dei task sono il maggiore blocco singolo di RAM che questo firmware riserva e quello che nessuno misura: ogni dimensione di stack del progetto è un budget fissato con margine deliberato, non una cifra ritagliata su ciò di cui il task si è rivelato aver bisogno. Senza questa riga, l’unico modo in cui uno stack segnala di essere piccolo è traboccando, e non c’è alcun modo di scoprire che un altro è enormemente grande. Un livello minimo può solo scendere, quindi tra una riga e l’altra non si perde nulla: ognuna riporta il peggio che quel task ha visto da quando è partito. Compaiono solo i task vivi: uno che l’operatore ha spento semplicemente non ha una riga in quell’ora.
Escludere la corruzione. CONFIG_APRS_HEAP_INTEGRITY_CHECK scansiona ogni
heap ogni CONFIG_APRS_HEAP_INTEGRITY_PERIOD_S secondi e registra un errore,
dopo gli indirizzi che stampa il verificatore stesso, se qualcosa non va. Le
strutture corrotte dell’allocatore si presentano come comportamento
inspiegabile dell’heap e altrimenti vengono inseguite come una perdita. La
scansione mantiene il lock di ogni heap mentre lo percorre, quindi gli altri task
si bloccano se allocano nel frattempo: per questo è disattivata di default e, se
attiva, su un timer lento. Ciò che riesce a vedere dipende dal livello di
rilevamento della corruzione: senza poisoning si verificano solo le strutture
dell’allocatore, quindi scegli «Light impact» o «Comprehensive» sotto
Heap memory debugging per verificare anche i byte canarino attorno a ogni
blocco allocato. Senza questo, un livello minimo inverosimile non si può
distinguere da uno corrotto.
Individuare un overflow di stack dove avviene.
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK (Component config →
FreeRTOS → Port) punta l’ultimo watchpoint hardware ai 32 byte finali
dello stack del task in esecuzione, così un overflow provoca un panic
sull’istruzione che lo ha causato. Il controllo del canarino attivo di default
viene eseguito solo a un cambio di contesto, quindi segnala il danno molto dopo
che il codice responsabile è già ritornato; e se l’overflow è una variabile
locale grande che scavalca la zona del canarino non viene segnalato affatto, e
riemerge più tardi come corruzione senza relazione. Questo conta quando un
crash cade in un punto impossibile, come lo scheduler che non trova alcun task
eseguibile. Il costo è un watchpoint in meno sotto gdb e fino a 60 byte in meno
su ogni stack di task, quindi appartiene a una build diagnostica insieme alle
opzioni sopra. Cattura solo le scritture che ricadono in quegli ultimi 32 byte.
Serializzare le due operazioni di rete più pesanti. Lo stesso modulo
possiede anche un piccolo lock non bloccante, indipendente dal campionamento
sopra descritto e sempre presente indipendentemente da quali opzioni
CONFIG_APRS_HEAP_* siano attive. L’handshake TLS del bot Telegram
(Bot Telegram) e la connect() TCP dell’uplink APRS-IS
(IGate — gateway APRS-IS) lo prendono ciascuno attorno alla propria verifica della
soglia di heap e lo rilasciano non appena quel lavoro di avvio è concluso,
cosicché non vengono mai eseguiti nello stesso istante competendo per la
stessa memoria contigua. Chi trova il lock già occupato riprova semplicemente
al proprio ciclo successivo: nulla qui resta bloccato in attesa dell’altra
parte.
«Le richieste falliscono a caso con “mbedtls_ssl_handshake returned -0x2700”, con heap in abbondanza.»#
-0x2700 è MBEDTLS_ERR_X509_CERT_VERIFY_FAILED: l’handshake TLS ha
raggiunto il server, ha scambiato messaggi e poi ha rifiutato il certificato
che gli è stato mostrato. Non c’era nulla di sbagliato nel collegamento né
nell’heap, ed è per questo che le cifre stampate accanto al fallimento
sembrano sane.
api.telegram.org è servito da più di un front-end e non tutti si
agganciano alla stessa autorità di certificazione. Quando il trasporto valida
contro un file PEM invece che contro il bundle dell’ESP-IDF, quel file si
fida solo delle autorità che porta davvero, quindi un file con una sola
radice valida le connessioni che finiscono su un front-end compatibile e
fallisce le altre. Quale front-end restituisca il DNS varia tra un tentativo
e l’altro, ed è esattamente per questo che il guasto sembra casuale e che un
nuovo tentativo di solito riesce.
Il trasporto lo segnala esplicitamente. Una catena rifiutata viene registrata
come Peer certificate refused, verification flags 0x…, validating against
<percorso>, e l’avvio registra quanti ancoraggi di fiducia ha prodotto il
file (Loaded N trust anchors from …). Un solo ancoraggio con -0x2700
intermittente è la firma di questo problema.
Le soluzioni sono due. Concatenare le radici mancanti nel file PEM: ogni
certificato che contiene diventa un ancoraggio di fiducia, e il file è
sostituibile dalla pagina File Storage dell’admin web senza ricompilare.
Oppure selezionare TELEGRAM_BOT_CERT_BUNDLE in menuconfig e validare
contro il bundle di certificati fornito con ESP-IDF, che copre le autorità
pubbliche e continua a funzionare quando Telegram ruota la propria catena, al
prezzo di portare il bundle nell’immagine.
Si noti che CONFIG_MBEDTLS_HAVE_TIME_DATE non è abilitato in questo
firmware, quindi le date di validità dei certificati non vengono
controllate. Un orologio non sincronizzato non è mai qui la causa di
-0x2700.