Servizio unico che possiede il pannello LED (256×64, FM6373 + DP32020B) su un Raspberry Pi e lo condivide fra più sorgenti di contenuto, con interfaccia web di controllo in italiano e inglese.
Nasce come emulatore di ZeDMD-WiFi per un cabinato, ma il pannello resta acceso anche quando non si gioca: l'obiettivo è un oggetto da salotto che racconta qualcosa — l'ora, il traffico aereo che passa sopra casa, la musica in ascolto, gli appuntamenti di domani — e che, quando serve, torna a fare il DMD.
Si accendono e si spengono uno per uno dalla pagina Servizi, e ognuno ha un interruttore anche in Home Assistant.
- ZeDMD — ricevitore del protocollo ZeDMD-WiFi: Batocera,
dmdserver, dmd-extensions o VPX inviano frame a questo Raspberry credendo di parlare con un ZeDMD reale. - Clock — orologio e data, con colori indipendenti, formato 12/24 ore e nomi dei giorni in italiano, francese o inglese. È la sorgente di riserva: quando non ha da dire niente nessun altro, c'è lui.
- Media Player — foto e video a rotazione dalla libreria, con intervallo casuale. Libreria raggiungibile via SMB e via upload dalla web UI. Supporta anche il materiale Pixelcade, utilizzabile a prescindere da Batocera.
- Air Radar — gli aerei in transito entro un raggio da una coordinata GPS, tramite le API pubbliche ADS-B della comunità, con le sigle tradotte in nomi leggibili e un registro CSV dei passaggi.
- Now Playing — il brano in ascolto da AirPlay 2 (shairport-sync), dall'API di Spotify o da un topic MQTT libero.
- Rolling Banner — dieci testi scorrevoli a comparsa periodica, ciascuno con colore, dimensione, velocità e lampeggio propri.
- Compleanni — l'augurio compare da solo nel giorno giusto.
- Scadenze — un semaforo accanto all'orologio dice se c'è qualcosa in arrivo, e ogni tanto il pannello mostra che cosa. Si inseriscono dalla web UI, da un CSV o da Home Assistant.
- Google Calendar — gli appuntamenti dei prossimi tre giorni, a giro. Sola lettura, e senza semaforo: un appuntamento succede quando succede.
- Rifiuti — il calendario della raccolta nella colonna libera accanto all'orologio, calcolato da una cadenza fissa senza interrogare nessun portale.
- Satelliti — avvisa dieci minuti prima che la Stazione Spaziale passi sopra casa, lo ricorda a cinque, e durante il passaggio disegna l'arco del cielo con il puntino che ci scorre sopra: si sa quanto in alto guardare. Parla una o due volte al giorno per quello che si vede davvero a occhio nudo, e in grigio-azzurro — senza avvisi e senza lampeggio — anche per i passaggi che ci sono ma non si vedono. Quando la Stazione sta per entrare nell'ombra della Terra e sparire a metà cielo, il pannello lo sa in anticipo e lo segna sull'arco. Un registro CSV conserva tutti i passaggi con l'altezza del Sole accanto: è lì che si legge perché una sera è rimasto muto.
- Notifiche — i messaggi che Home Assistant manda al pannello: la
porta che si apre, la lavatrice che ha finito, l'allarme che scatta. Un solo
topic MQTT e tre livelli:
infoeavvisoaspettano il loro turno come ogni altra sorgente,allarmesi prende il pannello anche a metà partita. La politica — quando parlare, quando tacere, con che parole — sta in Home Assistant, dove stanno già le automazioni. - Meteo — al mattino il bollettino della giornata, poi un aggiornamento ogni poche ore. Massima, minima, umidità, probabilità di pioggia e un'icona disegnata pixel per pixel. Le previsioni arrivano da Open-Meteo, che non chiede nessuna chiave: non c'è nessun segreto da custodire sul Raspberry. E le allerte di MeteoAlarm, che per l'Italia raccoglie quelle della Protezione Civile: l'avviso prende tutto il pannello, compare quando arriva e poi resta come una tacca colorata nell'angolo. Senza una regione scelta non se ne mostra nessuna — l'allerta di un'altra regione non è un'approssimazione, è un allarme falso.
- Funcam — una webcam USB sul pannello, ridotta a quello che un computer di quarant'anni fa sapeva mostrare. Il servizio arma e basta: la ripresa parte da un pulsante fisico o dai comandi della pagina, mai dal solo interruttore. Le immagini non escono dal Raspberry.
Non sono servizi: si comincia e si finisce. Premuto «Gioca» i servizi si fermano, il pannello è del gioco finché non si esce, e poi torna al suo lavoro. Si comandano da tastiera, da pad o dalla pagina web.
- Doom —
doomgenericin un processo separato, con i propri WAD su una condivisione di rete. - Game Boy — l'emulatore PyBoy, con le ROM su una condivisione di rete. Lo schermo 160×144 sta al centro del pannello in proporzione, con overscan e spostamento verticale per allargarlo, gamma regolabile e sei tavolozze di colore.
- Breakout e Invaders — scritti per questo pannello, senza dipendenze.
Il tasto Start del pad scorre il giro dei giochi; Select e PS escono. I quattro tasti frontali sparano: stanno sotto le stesse dita mentre si gioca, e nessuno di loro porta via la partita.
Fasce orarie: Night mode abbassa la luminosità, Sleep mode spegne il display. Sleep ha la precedenza su Night. Entrambe si comandano anche da Home Assistant.
Audio — con una scheda audio USB (l'unica strada: la libreria della matrice si prende il blocco PWM, cioè lo stesso dell'uscita jack) ogni servizio può avere un suono di avviso al momento della notifica, Breakout e Invaders hanno i loro effetti, e Doom e il Game Boy si portano il proprio audio — la colonna sonora dai WAD, l'APU emulata da PyBoy. Un interruttore in Now Playing manda la musica AirPlay sulla scheda: il DMD diventa una cassa.
Rete wifi dalla pagina web: si scandiscono le reti, si sceglie e si cambia, senza attaccare monitor e tastiera.
Pulsante fisico sulla Adafruit RGB Matrix Bonnet (GPIO 25, accanto a un GND): accende la Funcam, scatta una foto, la spegne tenendolo premuto.
Aggiornamento via rete da questo repository, con verifica dell'archivio e ripristino automatico se il servizio non riparte.
Home Assistant via MQTT Discovery: il brano corrente, un interruttore per ogni servizio, la luminosità, il semaforo delle scadenze e il calendario dei rifiuti compaiono da soli, comandabili e non solo leggibili.
I manuali stanno in docs/, in Markdown e in PDF già impaginato. Sono nel
repository e dentro l'archivio scaricato: si aprono direttamente da GitHub,
oppure si scaricano con git clone o dal pulsante Code → Download ZIP.
| Documento | |
|---|---|
| Manuale completo: hardware, cablaggio, installazione, diagnostica | docs/DMD_manuale_completo.pdf |
| ZeDMD-WiFi: protocollo e collegamento dei client | docs/DMD_zedmd_wifi.pdf |
| Now Playing: AirPlay, Spotify, MQTT, Home Assistant | docs/DMD_now_playing.pdf |
| Doom sul pannello | docs/DMD_doom.pdf |
| Game Boy sul pannello | docs/DMD_gameboy.pdf |
| Google Calendar | docs/DMD_calendario.pdf |
| Taratura automatica del pannello | docs/DMD_taratura.pdf |
| Rete wifi dalla pagina web | docs/DMD_rete.pdf |
| Funcam: la webcam sul pannello, con pochi colori | docs/DMD_telecamera.pdf |
| Joypad: mappatura dei comandi | docs/DMD_joypad.pdf |
| Audio: scheda USB, avvisi dei servizi, effetti dei giochi | docs/DMD_audio.pdf |
| Satelliti: i passaggi della Stazione Spaziale, l'arco del cielo, il registro | docs/DMD_satelliti.pdf |
| Notifiche da Home Assistant: il topic, i tre livelli, lo script pronto | docs/DMD_notifiche.pdf |
I PDF non vengono installati in /opt/dmd: sul Raspberry non servono, e
l'aggiornamento via rete copia solo ciò che il servizio esegue.
Oltre agli interruttori dei servizi, alla luminosità, alle scadenze, ai rifiuti e al brano in ascolto, dalla 6.8 il DMD pubblica anche quello che vede in cielo:
| Entità | Contenuto |
|---|---|
sensor…aerei_oggi |
quanti aerei sono stati registrati oggi, azzerato a mezzanotte |
sensor…aerei_totale |
quanti in tutto, da quando esiste il registro |
sensor…aerei_raggio |
quanti ce ne sono nel raggio in questo istante |
sensor…ultimo_aereo |
l'ultimo passato, con rotta, quota e distanza negli attributi |
sensor…iss_prossimo |
l'orario del prossimo passaggio visibile della Stazione |
sensor…iss_quanti |
quanti passaggi ci sono nelle prossime 24 ore |
L'orario del passaggio è un istante vero, quindi Home Assistant scrive «fra due ore» da solo; negli attributi c'è l'elenco completo delle ventiquattro ore, con durata, direzione, elevazione massima e visibilità di ciascuno.
Dalla 7.0 c'è anche il meteo — che non è un doppione di una stazione in giardino: quella misura un punto, questo prevede le prossime ore, ed è su una previsione che si costruisce un'automazione come chiudere la tapparella prima del temporale:
| Entità | Contenuto |
|---|---|
sensor…meteo_temperatura |
la temperatura adesso |
sensor…meteo_umidita |
l'umidità adesso |
sensor…meteo_massima / …meteo_minima |
la massima e la minima previste per oggi |
sensor…meteo_condizione |
che tempo fa, a parole, con tutto il resto negli attributi |
sensor…meteo_allerta |
il livello dell'allerta — giallo, arancione, rosso — con zona e orari negli attributi |
Lo stato dell'allerta è il livello e non il testo, perché è la parola su cui si scrive una condizione in un'automazione ed è normalizzata.
Dalla 6.9 si comandano da lì anche il pannello e il Game Boy:
| Entità | Che cosa fa |
|---|---|
switch…display_acceso |
Display: spegne il vetro lasciando acceso tutto il resto. ON vuol dire acceso |
switch…gameboy |
avvia PyBoy con la cartuccia già configurata, e torna OFF da solo se la partita finisce per inattività |
E la luminosità si dichiara non disponibile per la durata del Night mode, perché lì la luminosità applicata è quella notturna e il cursore non avrebbe effetto. Un comando che arrivasse comunque viene rifiutato, non accettato e ignorato: un comando che mente è peggio di un comando assente.
Ogni volta che un Mac copia qualcosa sulle condivisioni SMB lascia dietro di
sé un .DS_Store e, per ogni singolo file copiato, un gemello invisibile
che comincia con ._ — dentro c'è il resource fork. Su una libreria di
migliaia di foto raddoppiano le voci da elencare a ogni giro del Media Player.
Un giro dentro il servizio, ogni dodici ore, le toglie da /srv/dmd/media,
/srv/dmd/rom e /srv/dmd/doom. Si regola nella pagina Media, dove c'è
anche un pulsante Guarda e basta che conta le briciole senza cancellarle.
Vengono tolti nomi conosciuti e soltanto quelli — .DS_Store, ._*,
.apdisk, e le cartelle .Spotlight-V100, .Trashes, .fseventsd e simili.
Non «tutti i file che cominciano con un punto»: una cartella condivisa è di
chi la usa, e una regola cieca si mangerebbe un .stfolder di Syncthing o un
.git senza che nessuno capisca perché quella cosa ha smesso di funzionare.
Un Raspberry Pi già preparato secondo la procedura del progetto:
- audio integrato disattivato (
dtparam=audio=off) isolcpus=3sui modelli quad-core- libreria
rpi-rgb-led-matrix_pwm_experimentclonata e compilata - pannelli cablati e verificati con la demo
Facoltativa ma consigliata: una scheda audio USB. L'audio interno del Raspberry non è utilizzabile — la libreria della matrice si prende il blocco PWM, che è lo stesso dell'uscita jack — quindi l'USB non è un ripiego, è l'unica strada.
Dal Mac, copia la cartella sul Raspberry:
scp -r dmd gillo@dmdpi.local:~/Poi via SSH:
cd ~/dmd
chmod +x install.sh
sudo ./install.shLo script installa le dipendenze, compila i binding Python della libreria
matrice, copia i file in /opt/dmd, crea /etc/dmd/config.json e registra il
servizio systemd.
Se la libreria matrice non è nella home dell'utente:
sudo MATRIX_DIR=/percorso/della/libreria ./install.shPrima del primo avvio controlla /etc/dmd/config.json, in particolare:
| Chiave | Valore |
|---|---|
panel.slowdown |
1 Zero W · 3 Zero 2 W e Pi 3 · 5 Pi 4 |
panel.chain |
numero di pannelli in cascata |
panel.profile_dir |
impostato automaticamente dall'installer |
web.port |
8080, porta dell'interfaccia web |
zedmd.http_port |
80, riservata all'handshake ZeDMD |
Avvio e log:
sudo systemctl start dmd
journalctl -u dmd -fL'interfaccia è su http://dmdpi.local:8080/. Aprendo l'indirizzo senza porta
si viene rediretti automaticamente.
cd ~/dmd && chmod +x update.sh && sudo ./update.sh| Porta | Chi la usa | Servita da |
|---|---|---|
| 80 | handshake ZeDMD (cablata nel client) | server HTTP dedicato |
| 3333 | frame DMD, TCP e UDP | ricevitore ZeDMD |
| 8080 | interfaccia web | Flask |
L'handshake non passa da Flask, e non è un dettaglio estetico: il client di
libzedmd legge la risposta con una sola recv() e si ferma appena riceve meno
di 1024 byte. Flask invia header e corpo con due scritture separate, che sulla
rete diventano due pacchetti: il client leggerebbe solo gli header, vedrebbe un
corpo vuoto, e con tutti i campi a zero non riconoscerebbe il trasporto TCP,
ripiegando su UDP. Il server dedicato invia tutto in una sola sendall().
Il servizio si presenta come un ZeDMD-WiFi. Nel client indica solo l'indirizzo IP del Raspberry: userà da sé la porta 80 per l'handshake e la 3333 per i frame.
Su Batocera si configura in /userdata/system/configs/dmdserver/config.ini:
[DMDServer]
AltColor = 1
[ZeDMD]
Enabled = 0
[ZeDMD-WiFi]
Enabled = 1
WiFiAddr = 192.168.0.XXXPoi va attivato il servizio dmd_real — dal menu di Batocera oppure:
batocera-services enable dmd_real
batocera-services start dmd_realIl config.ini da solo non avvia niente: senza quel servizio non c'è
nessun processo che lo legga, e il Raspberry resta in ascolto senza mai vedere
un client. Si controlla così, e deve comparire un dmdserver con l'argomento
-c /userdata/system/...:
ps aux | grep dmdserver | grep -v grepVerifica rapida dell'handshake:
curl http://dmdpi.local/handshakeDeve rispondere con 22 campi separati da |, i primi due sono larghezza e
altezza del display.
Dal lato Raspberry, journalctl -u dmd distingue i due guasti che da fuori si
somigliano: la riga [zedmd-http] <ip> /handshake dice che il client ha
raggiunto il Pi, la sua assenza che non ci ha mai provato — tipicamente un
WiFiAddr rimasto al vecchio indirizzo dopo aver cambiato scheda o Raspberry.
Un comportamento noto di EmulationStation, che non è un guasto: tenendo
premuto il tasto di scorrimento l'immagine non si aggiorna, e non si aggiorna
nemmeno al rilascio. ES apre una connessione verso dmdserver a ogni cambio di
selezione, ma durante la ripetizione automatica del tasto non ne apre nessuna.
Un tocco in più riallinea il pannello.
Su http://dmdpi.local:8080/. Aprendo l'indirizzo senza porta si viene
rediretti automaticamente.
Impostazioni — luminosità con applicazione immediata, lingua dell'interfaccia, regolazione fine del driver S-PWM, profili hardware del pannello e taratura automatica (un pulsante: misura, e aggiunge il profilo trovato al menu), audio (scheda di uscita, volume, effetti dei giochi, audio di Doom, prova immediata), esportazione e importazione della configurazione, indirizzo IP locale, riavvio del servizio.
Orologio — colori di ora e data, formato 12/24 ore, lingua dei giorni, lampeggio dei due punti, server NTP e fuso orario.
Media — libreria, caricamento, rilettura, anteprima immediata, durate e intervalli, adattamento al pannello, modalità pixel art.
Banner — i dieci testi scorrevoli, uno per riga.
Musica — copertura delle sorgenti, collegamento dell'account Spotify, aspetto del player, stato di MQTT e delle entità di Home Assistant, e l'uscita musicale: con questa accesa la musica AirPlay esce davvero dalla scheda audio invece di essere solo raccontata.
Rete — le reti wifi visibili con segnale e cifratura, il collegamento
attuale, gli indirizzi a cui il DMD risponde. Le password le custodisce
NetworkManager, non il config.json.
Funcam — scelta della telecamera fra quelle collegate, risoluzione,
aspetto, i due comandi per accendere e spegnere la ripresa, il pulsante
fisico e l'installazione di gpiozero.
Compleanni — l'elenco delle date, compleanni e anniversari.
Scadenze — le scadenze aperte con il loro semaforo, l'inserimento, il CSV, il registro di quelle completate e le soglie dei tre colori.
Calendario — il collegamento dell'account Google e nient'altro, più l'elenco in sola lettura degli appuntamenti che il pannello vede.
Rifiuti — le voci del calendario, i giorni della settimana e la cadenza di ciascuna, e le due tabelle delle eccezioni.
Radar — coordinate e raggio, provider ADS-B, scelta dei parametri di volo, registro CSV dei passaggi, tabelle di conversione dei codici, prova diagnostica di una rotta.
Giochi — i giochi scritti per il pannello, i comandi di tastiera e pad, e la scheda degli emulatori esterni che porta a Doom e al Game Boy.
Doom — preparazione, scelta del WAD, gamma, avvio e uscita.
Game Boy — installazione di PyBoy, condivisione delle ROM, scelta della cartuccia, pad su schermo, overscan, spostamento verticale, gamma e tavolozza.
Servizi — attivazione dei servizi, indicazione della sorgente attualmente a schermo, possibilità di forzarne una invece di lasciar decidere l'arbitro, il suono di avviso di ciascuno e l'interruttore generale di MQTT.
Aggiornamenti — controllo e installazione della nuova versione da questo repository, con verifica dell'archivio e ripristino automatico se il servizio non riparte.
L'esportazione della configurazione (Impostazioni → Esporta) porta con sé
tutto config.json, blocchi nuovi compresi: l'audio con la scheda scelta,
il volume e il file di avviso di ogni servizio, la Funcam con il piedino del
pulsante. Reimportando un backup vecchio non si perde niente: le chiavi
che allora non esistevano ricompaiono con i valori predefiniti, e quelle che
c'erano restano come le avevi lasciate.
Restano fuori tre categorie, e nessuna per dimenticanza:
| Cosa | Dove vive | Perché non è nel backup |
|---|---|---|
| Password del broker MQTT | config.json, tolta all'export |
Un file di configurazione gira: finisce in un allegato, in una segnalazione. Si riscrive una volta sola |
| Password wifi | NetworkManager | Le custodisce chi lo fa di mestiere; nel nostro file non c'è niente da perdere né da regalare |
| Token di Spotify e Google | /var/lib/dmd/*.json, permessi 0600 |
Sono credenziali, non impostazioni |
| Uscita musicale (5.3) | /etc/shairport-sync.conf |
È la configurazione di un altro programma. Tenerne una copia da questa parte vorrebbe dire due verità che prima o poi divergono |
| Coordinate del radar | config.json |
Escludibili con una spunta all'export |
Da ricordare per l'audio: il backup salva il nome del file di avviso, non il file. Ripristinando su una macchina senza la libreria media, i servizi restano semplicemente muti finché i file non tornano al loro posto.
Un solo processo può pilotare i GPIO, quindi tutte le sorgenti convivono nello stesso servizio e un arbitro sceglie chi vince:
| Priorità | Sorgente |
|---|---|
| 100 | ZeDMD |
| 90 | Anteprima (gestione media) |
| 61 | Satelliti |
| 60 | Air Radar |
| 59 | Google Calendar |
| 58 | Now Playing |
| 57 | Scadenze |
| 56 | Compleanni |
| 55 | Rolling Banner |
| 51 | Funcam |
| 50 | Media Player |
| 10 | Clock |
Nessuna coppia pareggia, e non è un caso. A parità l'arbitro tiene chi si è registrato per primo, quindi la seconda non andrebbe mai a schermo: il Calendario era nato a 58, che è di Now Playing, e l'avviso non sarebbe mai comparso mentre suona musica. Una prova rifiuta i pareggi.
Le partite — Doom, Game Boy, i giochi — non partecipano a questa gara: prendono il pannello e lo tengono finché non si esce, sopra chiunque altro, ZeDMD compreso. La stessa presa la usa la gestione della libreria media, con la differenza che quella scade da sola se la pagina web smette di dare segni di vita.
La presa si chiede prima di avviare il gioco, non dopo. Avviare PyBoy vuol dire caricare Python, numpy e PIL più la ROM: secondi in cui la partita precedente ha già mollato il pannello e la nuova non l'ha ancora preso, e in quel buco l'arbitro lo dava all'orologio. Era il difetto corretto nella 5.7.
Sopra a tutto agiscono le fasce orarie: durante lo Sleep il display resta spento qualunque sia la sorgente vincente (salvo il risveglio su frame ZeDMD, se abilitato).
ZeDMD prende il controllo immediatamente appena un client si connette o
arriva un frame, e lo mantiene per zedmd.grace_seconds secondi dopo l'ultimo
segnale. Il tempo di grazia evita che l'orologio si intrometta durante le pause
di Batocera (menu, caricamenti); alzalo se vedi passaggi indesiderati.
/opt/dmd/dmdd.py servizio principale, arbitro e ciclo di rendering
/opt/dmd/display.py proprietario esclusivo del pannello
/opt/dmd/dmdconf.py configurazione persistente
/opt/dmd/webui.py Flask: pagine + endpoint del protocollo ZeDMD
/opt/dmd/sources/ sorgenti di contenuto
/opt/dmd/mqttbus.py client MQTT condiviso (ingresso metadati, uscita HA)
/opt/dmd/nowplaying.py stato del brano corrente, indipendente dalla sorgente
/opt/dmd/spotifyapi.py API web di Spotify (OAuth con PKCE)
/opt/dmd/gcalendar.py API di Google Calendar (OAuth, sola lettura)
/opt/dmd/autotune.py taratura automatica: misura, sceglie, salva il profilo
/opt/dmd/scadenze.py scadenze, semaforo e registro
/opt/dmd/hass.py entità di Home Assistant via MQTT Discovery
/opt/dmd/rifiuti.py calendario della raccolta: cadenze ed eccezioni
/opt/dmd/suoni.py avvisi, effetti e audio di Doom: chi suona e quando
/opt/dmd/suoni/ i dodici effetti dei giochi, nostri e non della libreria
/opt/dmd/cassa.py l'uscita musicale: shairport-sync sulla scheda vera
/opt/dmd/rete.py reti wifi via nmcli, senza toccare wpa_supplicant
/opt/dmd/webcam.py cattura dalla webcam e riduzione a pochi colori
/opt/dmd/pulsante.py il pulsante fisico sulla Bonnet, via gpiozero
/opt/dmd/diagnostica/ strumenti di misura del pannello, per la taratura
/opt/dmd/doom/ preparazione e ponte verso doomgeneric
/opt/dmd/gb/ preparazione e ponte verso PyBoy
/srv/dmd/media libreria media, condivisa come \\<ip>\dmd-media
/srv/dmd/doom WAD di Doom, condivisi come \\<ip>\dmd-doom
/srv/dmd/rom ROM Game Boy, condivise come \\<ip>\dmd-rom
/var/lib/dmd/soppressioni.csv, straordinari.csv eccezioni del calendario
/etc/dmd/config.json configurazione
/var/lib/dmd/spotify.json token di Spotify, permessi 0600, fuori dall'export
/var/lib/dmd/google.json token di Google, permessi 0600, fuori dall'export
Aggiungere un servizio significa quattro cose, e vanno fatte tutte e quattro:
scrivere la sorgente in sources/, registrarla nel Runtime, aggiungere la
chiave a services con la sua riga nella pagina Servizi, e la voce in
hass.SWITCHES. Le ultime due si dimenticano — è successo con i Compleanni e
di nuovo con le Scadenze — e il sintomo è sempre lo stesso: una sorgente che
funziona e che non si accende mai. Ora una prova le pretende entrambe.
Ricostruito dal sorgente di PPUC/libzedmd.
HTTP porta 80 — GET /handshake risponde con 22 campi separati da |:
larghezza, altezza, versione firmware, flag S3, protocollo, porta, ritardo UDP,
write-at-once, luminosità, ordine RGB, parametri del pannello, SSID, id,
potenza, tipo dispositivo, line decoder. Esistono anche gli endpoint singoli di
fallback (/get_width, /get_height, …).
TCP porta 3333 — flusso di payload:
b"FRAME" + [ b"ZeDMD" + cmd(1) + size_hi(1) + size_lo(1) + compresso(1) + dati ]*
I dati compressi usano deflate. Comandi principali: 0x05 zone RGB565,
0x04 zone RGB888, 0x06 render, 0x08 frame intero RGB565,
0x07 frame intero RGB888, 0x0a clear, 0x0b keep-alive, 0x16 luminosità.
Le zone sono una griglia fissa 16 × 8 (128 zone): ogni zona è preceduta dal suo
indice, e un indice ≥ 128 significa "zona interamente nera" senza pixel a
seguire. Dichiarando TCP nell'handshake si evita la frammentazione UDP e il
flusso arriva ordinato.
| Sintomo | Causa probabile | Rimedio |
|---|---|---|
Il servizio non parte, errore su rgbmatrix |
binding Python non compilati | rilanciare install.sh, oppure pip install . --break-system-packages dalla cartella della libreria |
| Pannello nero ma servizio attivo | nessuna sorgente abilitata | attivare Media Player - Clock dalla pagina Servizi |
| L'handshake risponde 200 ma non arriva nessun frame, e il client riprova all'infinito | la porta 80 è servita da Flask invece che dal server dedicato | verificare zedmd.http_port = 80 e web.port = 8080, poi riavviare |
Address already in use sulla porta 80 |
altro web server attivo | sudo systemctl stop lighttpd (o simili) |
| Immagine a scatti | panel.slowdown errato per il modello |
correggere il valore e riavviare il servizio |
| L'orologio interrompe Batocera | tempo di grazia troppo corto | alzare zedmd.grace_seconds |
Cartella predefinita: /srv/dmd/media, condivisa in rete come \\<ip>\dmd-media.
Si possono caricare i principali formati di immagine (jpg, png, bmp, webp, tif) e di video o animazione (gif, mp4, mkv, avi, mov, webm, mpg). L'adattamento a 256×64 avviene al momento della riproduzione: le immagini con Pillow, i video con ffmpeg. Le animazioni brevi vengono ripetute fino a coprire la durata impostata, comportamento pensato per le GIF di Pixelcade.
Le sottocartelle sono esplorate ricorsivamente: una raccolta Pixelcade si può copiare così com'è.
Il dettaglio completo, versione per versione, sta in
CHANGELOG.md e nell'intestazione di version.py. Qui solo
le tappe.
| Versione | Contenuto |
|---|---|
| 1.0 | Ricevitore ZeDMD-WiFi, orologio, web UI |
| 1.1 | Media Player, Night e Sleep mode, condivisione SMB |
| 1.2 | Regolazione fine del driver S-PWM dalla web UI |
| 1.3 | Air Radar dalle API pubbliche ADS-B |
| 1.5 | Aggiornamento via rete da GitHub, con ripristino automatico |
| 1.7 | Interfaccia web in italiano e inglese |
| 1.7.2 | Impronte md5 di tutti i file, verificate prima e dopo la copia |
| 1.8 | Esportazione e importazione della configurazione |
| 1.9 | Rolling banner |
| 1.10 | Now Playing: AirPlay 2, Spotify, MQTT, entità in Home Assistant |
| 1.11 | Radar: tabelle CSV che traducono i codici in nomi leggibili |
| 2.0 | Compleanni, profili hardware del pannello, Night e Sleep da Home Assistant |
| 3.0 | Doom sul pannello, con doomgeneric in un processo separato |
| 3.2 | Doom non è un servizio ma una partita: prende il pannello e lo restituisce |
| 3.3 | Pad PS4 e da PC, avvio da Home Assistant |
| 3.4 | Calendario della raccolta rifiuti accanto all'orologio |
| 3.8 | Breakout e Invaders, scritti per il pannello; il giro del tasto Start |
| 4.0 | Scadenze: semaforo accanto all'orologio, avviso periodico, registro, MQTT |
| 4.1 | L'interruttore delle Scadenze mancava nella pagina Servizi: la chiave c'era, la riga no |
| 4.2 | Cablaggio Adafruit RGB Matrix Bonnet selezionabile dalla web UI |
| 4.3 | Registri del driver modificabili a mano, per la caccia al ghosting |
| 4.4 | Un fotogramma identico al precedente non viene riscritto: meno traffico sul bus di memoria, meno righe chiare |
| 4.5 | Game Boy: l'emulatore PyBoy, con ROM su condivisione SMB, overscan e gamma |
| 4.5.4 | Il servizio non partiva: cablaggio prima della costruzione. Aggiunta test_avvio.py, che costruisce il Runtime vero |
| 4.6 | Spostamento verticale dell'immagine Game Boy |
| 4.6.1 | Tavolozze dello schermo Game Boy |
| 4.7 | Google Calendar: gli appuntamenti dei prossimi tre giorni, senza semaforo. Token fuori dalla configurazione, revoca del permesso allo scollegamento |
| 4.8 | Profili del pannello con taratura automatica; il profilo scelto a mano non viene più riscritto al salvataggio |
| 4.9 | Rete wifi dalla pagina web: scansione, scelta, password custodite da NetworkManager |
| 4.10 | Funcam: la webcam sul pannello, con pochi colori |
| 5.0 | Pulsante fisico sulla Bonnet: accende la Funcam, scatta una foto, la spegne tenendolo premuto |
| 5.1 | Il servizio Funcam arma, non accende: la ripresa parte solo se qualcuno la chiama |
| 5.2 | Audio: un avviso per servizio al momento della notifica, dodici effetti per Invaders e Breakout, la colonna sonora di Doom |
| 5.3 | Il DMD diventa una cassa: la musica AirPlay esce dalla scheda audio. Corretta la scelta automatica dell'uscita, che poteva finire sulla scheda fittizia |
| 5.4 | Cerchio (B) spara invece di uscire: era l'ultimo tasto frontale che chiudeva una partita. Il pulsante di prova dell'audio dice se l'interruttore generale è spento |
| 5.5 | Doom suona davvero: il Makefile non compilava nessun modulo sonoro, e la levetta della 5.2 prometteva l'impossibile. Il Game Boy suona con la sua APU. Breakout ha una nota per fila di mattoni |
| 5.5.1 | La ricompilazione di Doom falliva su multiple definition: il Makefile non rifaceva gli oggetti quando cambiavano le opzioni, non i sorgenti |
| 5.6 | Audio del Game Boy in ritardo di 0,7 s (buffer fisso di ffmpeg) e effetti di Breakout scartati: aplay con buffer da 80 ms, e un mixer che somma gli effetti invece di buttarli |
| 5.6.1 | Il mixer della 5.6 non partiva (un import mancante nascosto da un except), e il Game Boy spariva dal giro del tasto Start se si toglieva Doom |
| 5.7 | L'orologio non compare più fra un gioco e l'altro: la presa del pannello si chiede prima di avviare l'emulatore, non dopo. Schermata di attesa del Game Boy. Documentazione riallineata |
| 5.8 | Satelliti: il pannello avvisa dieci minuti prima che la Stazione Spaziale passi sopra casa e mostra dove guardare, con l'arco del passaggio e l'ora che lampeggia |
| 5.8.1 | Il radar contava l'intervallo dalla fine della sfilata invece che dall'inizio dell'interrogazione, e più aerei trovava meno spesso guardava. La cadenza misurata ora si legge nella riga di stato |
| 5.8.2 | Registro CSV dei passaggi dei satelliti, con dentro anche quelli non visibili e l'altezza del Sole che spiega perché |
| 6.0 | I satelliti diventano un servizio completo: pagina dedicata, il terzo colore per i passaggi che non si vedono, e lo spegnimento in ombra segnato sull'arco |
| 6.0.1 | Un cane da guardia riavvia il ciclo del radar se si ferma, e conta quante volte è successo: non è una cura, è la misura che dirà se il blocco esiste davvero |
| 6.1 | Home Assistant può parlare al pannello: un topic, tre livelli, e solo allarme interrompe una partita. Corretti i sensori che mandavano una stringa vuota e facevano protestare HA cinquanta volte al giorno |
| 6.2 | Il preavviso dei satelliti scriveva l'ora di sorgere nel posto dell'orologio e veniva letto come l'ora corrente: adesso in grande c'è il conto alla rovescia, e accanto la durata visibile. La configurazione MQTT passa dalla pagina Musica alla pagina Rete |
| 6.3 | Un pulsante di prova per le notifiche nella pagina Servizi: passa dal broker, così prova la catena e non solo il disegno, e dice quale metà funziona |
| 6.4 | Una freccia al posto di «FRA» sul preavviso dei satelliti, e un giro periodico che toglie dalle condivisioni le briciole lasciate dai Mac |
| 6.5 | L'arco del passaggio non era mai stato disegnato: mancava l'oggetto orbitale nel record, e l'eccezione lasciava il pannello congelato sull'ultimo fotogramma. Adesso un disegno che fallisce molla il pannello |
| 6.6 | Il pannello compare in Home Assistant come tre entità notify, una per livello: le notifiche si scelgono da una tendina accanto al telefono, senza passare da uno script |
| 6.7 | Il raggio del radar era 1,85 km invece dei 3 dichiarati — il provider troncava le miglia nautiche. Trovato nei dati del registro, non nel codice. E un margine di cortesia che elenca gli aerei passati appena fuori |
| 6.8 | Aerei e satelliti escono dal pannello: cinque entità in Home Assistant, con l'elenco dei passaggi della Stazione delle prossime ventiquattro ore |
| 6.8.1 | La procedura di pubblicazione dice anche le due cose che erano costate tempo: i cinque minuti di cache prima che l'OTA veda la versione nuova, e come si pubblicano più versioni in un push solo |
| 6.9 | Tre comandi che mentivano, corretti: il cursore della luminosità durante il Night mode, lo spegnimento del pannello che non esisteva, il Game Boy che non compariva in Home Assistant. E il totale dei voli su MQTT |
| 7.0 | Il meteo: bollettino della giornata al mattino, aggiornamento ogni poche ore, icone disegnate pixel per pixel, e le allerte di MeteoAlarm. La posizione passa dal Radar alle Impostazioni, perché la usano in tre |
| 7.1 | La finestra del meteo ridisegnata guardandola accesa: i gradi di adesso grandi al centro con l'umidità, massima e minima in alto a destra con le frecce, e l'unità — che prima mancava del tutto |
Un semaforo a destra dell'orologio dice se c'è qualcosa in arrivo; un avviso periodico dice che cosa. Le soglie sono in giorni e si regolano: oltre la verde il semaforo resta spento, perché una scadenza fra un mese non è una notizia e un pannello che segnala sempre qualcosa non segnala più niente.
Le scadenze si inseriscono dalla web UI, si importano da un CSV, o arrivano da Home Assistant su un topic MQTT. Quelle ricorrenti si rigenerano da sole alla cadenza scelta; quelle completate finiscono in un registro scaricabile.
Gli appuntamenti dei prossimi tre giorni compaiono sul pannello a giro: in alto a destra quando, nel colore che l'orologio usa per la data, al centro che cosa, e sotto dove. Un appuntamento compare tre giorni prima e sparisce quando è passato.
Niente semaforo, di proposito. Il semaforo dice manca poco, e ha senso per una bolletta, che si può pagare prima; non ne ha per un appuntamento, che succede quando succede.
È una vetrina, non un'agenda: sola lettura (calendar.readonly), solo il
calendario principale, e le ricorrenze le espande Google — al pannello arrivano
occorrenze con una data ciascuna invece di regole da interpretare. Calendari
secondari, colori e promemoria sono ignorati.
L'autorizzazione si fa dal browser del proprio computer, perché il DMD non
ha tastiera: si apre il link, si accetta, e si incolla l'indirizzo su cui si è
finiti. I token stanno in /var/lib/dmd/google.json con permessi 0600, fuori
dalla configurazione; il client secret viene tolto dall'export come la
password del broker MQTT. Scollegando, il DMD chiede anche a Google di revocare
il permesso, così non resta un consenso in piedi dal loro lato.
Procedura completa su Google Cloud — compreso il passo che si dimentica,
pubblicare la schermata di consenso in produzione, senza il quale Google
scollega tutto dopo sette giorni — in docs/calendario.it.md.
Non sono servizi: sono partite. Si comincia dalla pagina o dal pad, i servizi si fermano, il pannello è del gioco finché non si esce.
Doom gira come processo separato (doomgeneric), con i WAD su
\\<ip>\dmd-doom. Game Boy usa PyBoy, con le ROM su \\<ip>\dmd-rom: lo
schermo 160×144 viene portato a 64 righe mantenendo la proporzione — 71 pixel
al centro, i lati spenti — e l'overscan toglie righe sopra e sotto per
allargarlo, con uno spostamento verticale che decide da quale parte
tagliare. Gamma e tavolozza sono regolabili. Le ROM e i WAD sono dell'utente:
in questo progetto non ce n'è nessuno e non ce ne saranno mai.
Breakout e Invaders sono scritti per il pannello e non hanno dipendenze. Il tasto Start del pad scorre il giro dei giochi disponibili; PS esce.
Entrambi gli emulatori girano in un processo separato e non dentro il servizio. Il motivo principale è il GIL: un emulatore nel nostro processo si contenderebbe l'interprete con il ciclo che disegna il pannello, e su questo progetto la moneta è il microsecondo — è misurato che basta la contesa sul bus di memoria per accendere una riga sbagliata.
Interroga a intervalli regolari le reti ADS-B comunitarie e mostra i voli entro il raggio impostato. Servizi supportati, tutti gratuiti e senza chiave:
| Servizio | Endpoint |
|---|---|
| adsb.fi | https://opendata.adsb.fi/api/v3/lat/{lat}/lon/{lon}/dist/{nm} |
| adsb.one | https://api.adsb.one/v2/point/{lat}/{lon}/{nm} |
| adsb.lol | https://api.adsb.lol/v2/point/{lat}/{lon}/{nm} |
Sono compatibili tra loro (formato ADSBexchange v2): se quello scelto non risponde, gli altri vengono usati automaticamente come riserva. Il raggio si indica in chilometri e viene convertito in miglia nautiche; la distanza di ogni aereo viene poi ricalcolata con l'emisenoverso perché il filtro dell'API è approssimativo.
La rotta origine → destinazione arriva dal servizio routeset di adsb.lol
(POST https://api.adsb.lol/api/0/routeset), che accetta fino a 100 voli per
richiesta: tutte le rotte di un giro si ottengono con una sola chiamata.
Vengono preferiti i codici IATA, più corti e leggibili su un pannello stretto,
con ricaduta sugli ICAO quando mancano — cosa che succede spesso, ed è il
motivo per cui la tabella di conversione conosce entrambe le grafie. Se il servizio non risponde si ripiega
su hexdb.io, volo per volo.
La ricerca avviene solo se il campo è selezionato o se la si vuole nel registro CSV. È disponibile per i voli di linea, molto meno per cargo, aviazione generale e voli di Stato: la riga di stato riporta quante rotte sono state trovate e quante no, e la pagina Radar ha una prova diagnostica per un singolo codice volo.
I parametri mostrati sul pannello si scelgono dalla pagina Radar: rotta, modello, immatricolazione, quota, velocità, direzione, transponder, distanza e codice Mode S. Il codice volo compare sempre in grande.
Ogni passaggio viene registrato in /var/lib/dmd/flights.csv, scaricabile
dalla web UI. Colonne: timestamp, hex, callsign, registration, type, altitude_ft, speed_kt, track_deg, squawk, distance_km, latitude, longitude, route. Lo stesso aereo non viene riscritto finché resta nel raggio, quindi c'è
una riga per passaggio.
Le coordinate impostate restano solo in /etc/dmd/config.json su questo
Raspberry: non fanno parte del software distribuito.
Non essendoci un'antenna locale la copertura dipende dai riceventi volontari della zona: il traffico commerciale compare quasi sempre, aviazione generale e voli militari spesso no.
Il servizio confronta la propria versione con il version.py del repository
GitHub configurato e, se ne trova una più recente, la può installare da solo
dalla pagina Impostazioni.
L'installazione procede in quest'ordine, e si ferma al primo intoppo:
- scarica l'archivio del ramo in una cartella temporanea
- rifiuta archivi con percorsi assoluti o risalite di cartella
- verifica la presenza dei file attesi e compila tutto il Python
- salva una copia dell'installazione corrente in
/var/lib/dmd/backup - sostituisce i file e riavvia il servizio
- interroga
/api/statusper verificare che sia davvero ripartito - se non risponde, ripristina la copia e riavvia di nuovo
L'ultimo passo è il motivo per cui l'aggiornamento gira in un processo staccato: deve sopravvivere al riavvio del servizio che lo ha avviato.
La configurazione in /etc/dmd/config.json non viene mai toccata, e il diario
delle operazioni resta in /var/lib/dmd/ota.log, consultabile dalla web UI.
Il pannello mostra che cosa stai ascoltando: titolo, artista, album, stato e avanzamento del brano. Il DMD non riproduce audio e non si mette fra la musica e le casse — si limita ad ascoltare i metadati.
AirPlay 2. shairport-sync gira sul Raspberry e si presenta in rete come
una cassa AirPlay. L'audio finisce nella scheda fittizia snd_dummy (che ha
un orologio vero, a differenza di /dev/null), i metadati escono su MQTT. Al
ricevitore AirPlay non importa quale applicazione stia suonando: Apple Music,
Spotify, Amazon Music e YouTube funzionano tutte allo stesso modo, senza
niente da configurare per ciascuna.
Spotify. Copre la musica che non passa da AirPlay: Spotify Connect
verso casse vere, il computer, un Echo. Autenticazione OAuth con PKCE, senza
segreto dell'applicazione; i token stanno in /var/lib/dmd/spotify.json con
permessi 0600 e non compaiono mai in una configurazione esportata.
Topic MQTT libero. Qualsiasi altra cosa può pubblicare un JSON con
title, artist, album, duration, position e playing — sono
accettati anche i nomi di Home Assistant (media_title, media_artist, …).
È il modo di coprire un HomePod avviato a voce o un Echo, che il DMD non
vedrebbe altrimenti.
Quando più sorgenti hanno qualcosa da dire comanda AirPlay: se sta arrivando un flusso audio qui, quello è ciò che si sta ascoltando. A parità, vince chi suona su chi è in pausa.
Il broker predefinito è 127.0.0.1, cioè un Mosquitto installato sul
Raspberry stesso. Home Assistant è una possibilità, non un requisito.
Con mqtt.discovery attivo il DMD si presenta da solo via MQTT Discovery. In
Home Assistant compare un dispositivo con il brano corrente (titolo come
stato, il resto come attributi), un interruttore per ogni servizio e la
luminosità come number. Sono comandabili, non solo leggibili. Le entità
sono legate al testamento MQTT: se il servizio si ferma diventano non
disponibili invece di restare congelate.
Quando Home Assistant riparte non serve sorvegliarlo, e il DMD non ha bisogno
di sapere dove sia: le dichiarazioni sono pubblicate con retain, quindi il
broker le riconsegna a chi si iscrive dopo, e in più Home Assistant pubblica
online su homeassistant/status all'avvio — il DMD è iscritto a quel topic
e si ridichiara, con un ritardo casuale per non sommare la propria risposta a
quella di tutti gli altri dispositivi della casa. La pagina Musica ha
comunque un pulsante per ridichiarare a mano e uno per rimuovere le entità.
AirPlay manda prgr — tre timestamp RTP a 44100 Hz — solo al cambio di
traccia e dopo un salto; Spotify risponde solo quando lo si interroga. Fra un
aggiornamento e l'altro il tempo lo conta il DMD, ripartendo dall'ultimo
valore certo. Il conteggio usa time.monotonic() e non l'orologio di
sistema: una correzione NTP non deve far saltare la barra.
A 64 pixel di lato sarebbe illeggibile, ma soprattutto è fatta quasi solo di mezzi toni — il contenuto peggiore possibile per un pannello S-PWM a refresh basso. Per la stessa ragione il testo si disegna senza antialiasing (le sfumature dei bordi sono anch'esse mezzi toni) e, con l'opzione Solo colori pieni, in otto soli colori saturi: la gerarchia fra le righe si ottiene cambiando tinta invece che luminosità.
sudo /opt/dmd/setup_nowplaying.shChiede il nome della cassa e dove sta il broker, poi fa tutto: Mosquitto,
dipendenze, nqptp, shairport-sync compilato con AirPlay 2 e metadati,
scheda audio fittizia, file di configurazione, confinamento ai core 0-2 e
sezione MQTT del DMD. Ripetibile: salta i passi già fatti, compresa la
compilazione. --verifica controlla lo stato senza toccare niente.
Viene installato insieme al resto ma non viene mai lanciato in automatico: è facoltativo, e la sola compilazione porta via un quarto d'ora. Non dipende da nessun altro file, quindi si può anche scaricare da solo.
python3-paho-mqtt. Se manca, la pagina Musica lo dice e Now Playing resta
spento; il resto del DMD non se ne accorge.
Guida completa, anche per farlo a mano: docs/now-playing.it.md.
La fascia bassa è larga 256 pixel: oltre i quattro o cinque campi qualcosa deve cedere. Con nove parametri selezionati la riga misura circa 400 pixel. La pagina Radar decide come comportarsi:
| Modalità | Che cosa fa |
|---|---|
| A pagine (predefinita) | I campi si dividono in gruppi che ci stanno per intero e si alternano ogni page_seconds secondi. Non se ne perde nessuno, e il testo resta fermo. |
| Scorrevole | La riga passa da destra a sinistra a scroll_speed pixel al secondo. Si legge senza attese, ma è l'unica parte del pannello in movimento continuo: su una matrice a 29 Hz lascia una scia leggera. Qui display_seconds diventa un minimo: una passata iniziata arriva in fondo, e si cambia aereo quando il testo è uscito del tutto da sinistra. |
| Accorcia la riga | Il comportamento fino alla 1.11.3: i campi in eccesso vengono scartati dal fondo. |
Identificativo e rotta non si muovono mai: cambia solo la fascia bassa, così l'aereo non salta mentre lo stai leggendo. Finché i campi ci stanno tutti le tre scelte si comportano allo stesso modo.
Il radar riceve sigle. Il modello arriva come designatore ICAO (B738).
Gli aeroporti delle rotte arrivano invece in due grafie: il servizio
routeset di adsb.lol è documentato per rispondere con i codici IATA di tre
lettere (MXP), ma quel campo spesso non c'è e sia routeset sia hexdb.io
ripiegano sui codici ICAO di quattro (LIMC). Le tabelle conoscono entrambe.
Due file CSV modificabili traducono le sigle in nomi leggibili:
/var/lib/dmd/aerei.csv 177 tipi di aeromobile
/var/lib/dmd/aeroporti.csv 326 aeroporti
/var/lib/dmd/compagnie.csv 129 compagnie aeree
La compagnia non arriva come campo a sé: sta nelle prime tre lettere del
nominativo di volo. In AFR1732 la compagnia è AFR, Air France — il
designatore ICAO, non la sigla IATA di due lettere del biglietto. Un
nominativo che non ha quella forma non ha una compagnia da mostrare:
l'aviazione generale usa l'immatricolazione (I-ABCD), e quel campo resta
vuoto invece di inventarsi una sigla.
Ogni riga ha tre campi — codice,forma breve,nome completo. Nella prima
colonna possono stare più codici separati da /, e la riga risponde a
tutti: è così che un aeroporto porta le due grafie senza doverne tenere
allineate due righe.
MXP/LIMC,Malpensa,Milano Malpensa
Servono due forme perché il pannello è largo 256 px e la riga del radar porta già rotta,
quota, velocità e distanza: 737-800 ci sta, Boeing 737-800 no. Il nome
completo va nella web UI e nelle due colonne nuove del registro
(type_name, route_name), dove lo spazio non manca.
Un codice che non è in tabella viene mostrato com'è. Non è un errore, è il comportamento previsto. Il sistema tiene il conto dei codici che incontra senza saper tradurre e li elenca nella pagina Radar, ordinati per frequenza: è la lista di cosa conviene aggiungere per primo, invece di doverlo indovinare. Un pulsante li aggiunge in coda al file come righe da completare.
I file si modificano dalla pagina Radar oppure a mano via SSH o SMB: una modifica esterna viene raccolta senza riavviare il servizio, perché la rilettura è legata a data e dimensione del file.
Non vengono mai sovrascritti dagli aggiornamenti. Vivono in
/var/lib/dmd proprio per questo: /opt/dmd viene riscritto a ogni
installazione, e le aggiunte fatte a mano sparirebbero. Al primo avvio i file
si creano da un modello contenuto nel pacchetto; da quel momento sono
dell'utente.
Dalla 1.11.1 la rotta ha una riga sua, al centro, fra l'identificativo e i
dettagli: quello spazio prima restava vuoto, e su una riga sola i nomi lunghi
facevano scartare modello e quota per far entrare tutto. Ora ci stanno
Orio al Serio→Stansted sopra e 737-800 34000ft 450kt 3.2km sotto.
Se la rotta tradotta è comunque troppo larga si tornano a mostrare i codici, che ci stanno sempre: meglio un'informazione completa e stringata che una tagliata a metà. Senza rotta il disegno resta a due fasce, come prima.
Il colore della rotta è regolabile a parte; lasciato vuoto segue quello dei dettagli, così chi non tocca nulla non vede cambiare niente.