Skip to content

Latest commit

 

History

History
253 lines (156 loc) · 35.1 KB

REPORT.it.md

File metadata and controls

253 lines (156 loc) · 35.1 KB

Un modem ottico schermo-fotocamera con bit loading adattivo guidato dal sounding

Traduzione italiana di REPORT.md. In caso di discrepanza fa fede l'originale inglese.

Rapporto tecnico — 3–5 agosto 2026

Documenti complementari: PROTOCOL.md (specifica del formato di trasmissione) · FACTS.md (registro canonico dei fatti) · sounding/README.md (manuale di riproduzione passo passo).


1. Abstract

Abbiamo costruito, in tre giorni, un collegamento dati ottico in tempo reale dallo schermo di un laptop alla fotocamera di un telefono, e lo abbiamo spinto a 33,20 KB/s wall-clock (tempo effettivo trascorso, misurato a orologio) nella consegna di un file — 206.310 byte, verificati con SHA-256, in 6,1 secondi, dal primo frame della fotocamera al file verificato su disco. Il ricevitore è zero-install: un iPhone non modificato (iOS 18.7, Safari) apre un URL da QR code, tocca "start" e trasmette in streaming i frame della fotocamera sulla propria Wi-Fi al laptop che decodifica.

Il sistema in tempo reale più vicino in letteratura, FOCUS (MobiSys 2016), riportava ~30 KB/s di goodput sostenuto. I nostri 33,20 KB/s superano quel valore del 10,7% su una metrica più severa: il tempo di consegna wall-clock di un file completo e verificato con hash, inclusi l'acquisizione, tutto l'overhead di protocollo e ogni frame perso.

Nell'arco dell'intero progetto abbiamo completato 22 trasferimenti di file reali su 22, ciascuno verificato end-to-end con SHA-256 — zero consegne corrotte.

L'idea centrale del sistema è presa in prestito da DSL e Wi-Fi anziché dai codici a barre: misurare prima il canale schermo-fotocamera (SNR per frequenza spaziale tramite una sonda di sounding), poi caricare ogni portante OFDM con la costellazione più densa che riesce a sostenere, e continuare ad adattarsi in base al feedback dal ricevitore in tempo reale. Lungo il percorso abbiamo caratterizzato diversi effetti che non abbiamo visto documentati per questo canale — il codec video come collo di bottiglia dominante delle pipeline basate su video registrato, un vincolo di diluizione della potenza totale sul bit loading, l'ostilità attiva delle ottimizzazioni psicovisive di x264 verso i segnali di modem, e la codifica JPEG con fallback software nei worker di iOS Safari — e li riportiamo come risultati di prima classe nella Sezione 6.


2. Contesto e lavori correlati

I sistemi schermo-fotocamera ("optical camera communication") si dividono in due famiglie tecniche.

I codici a barre spaziali estendono i codici QR: lo schermo mostra una matrice di celle discrete, il ricevitore applica sharpening, sogliatura e decide in modo rigido (hard decision) ogni cella. La densità viene da celle più piccole e più livelli di colore. Questa famiglia include gli strumenti di trasferimento file a QR animati (TXQR, lo standard BC-UR usato dai wallet crypto air-gapped), libcimbar e Decimen. È semplice e robusta, ma ogni cella viene decisa indipendentemente — non c'è equalizzazione, non c'è adattamento di rate per regione, e le prestazioni crollano di colpo quando le celle si fondono per il blur.

I sistemi nel dominio della frequenza (OFDM) trattano lo schermo come una banda radio: i dati viaggiano su portanti a frequenza spaziale 2D, il ricevitore esegue una FFT, equalizza con i piloti e decide in modo soft i simboli di costellazione. Questa famiglia comprende PixNet e FOCUS nella letteratura accademica — e il nostro sistema. L'OFDM degrada in modo graduale (il blur attenua le portanti ad alta frequenza invece di distruggere celle casuali) e, soprattutto, espone un SNR per portante che può essere misurato e sfruttato.

Lo stato dell'arte, onestamente, come verificato dalla nostra ricerca sul web il 3 agosto 2026:

Sistema Anno / sede Famiglia Rate Condizioni
BC-UR (QR per wallet) in corso, standard aperto QR animato ~0,3–0,5 KB/s telefono-telefono, tempo reale
TXQR open source QR animato + fountain ~3 KB/s telefono-telefono, tempo reale
FOCUS MobiSys 2016 OFDM, QPSK a sola fase + Raptor ~30 KB/s monitor → Galaxy S6, tempo reale; la sola FFT occupava il 37% del budget di 54 ms/frame su una CPU del 2015
PixNet SIGCOMM 2010 OFDM spaziale 2D, prefisso ciclico 2D, 4-QAM fisso 12 Mbit/s monitor → DSLR, decodifica offline
libcimbar open source barcode a matrice di colori ~106 KB/s (852 kbit/s) metodologia riproducibile (PERFORMANCE.md)
Decimen 2026, open source multi-QR + colore, browser ~129 KB/s auto-dichiarato
Questo lavoro ago 2026 OFDM adattivo 33,20 KB/s wall-clock schermo laptop → iPhone Safari, tempo reale, zero install

Due cose spiccano in questa tabella. Primo, il record in tempo reale della famiglia OFDM (FOCUS) è molto al di sotto di quello della famiglia dei barcode — perché la decodifica OFDM è costosa ed entrambi i sistemi accademici usavano costellazioni fisse scelte offline. Secondo, nessuno in nessuna delle due famiglie fa ciò che ogni modem DSL fa dagli anni '90: misurare il canale portante per portante e caricare i bit di conseguenza. Le nostre ricerche (agosto 2026) non hanno trovato alcuna combinazione pubblicata di bit loading adattivo per portante con il canale schermo-fotocamera, nessuna demodulazione OFDM offloaded su GPU/NPU per questo canale, e nessun OFDM spaziale telefono-telefono in tempo reale. Quel vuoto è il progetto.

Abbiamo anche valutato e scartato un'idea esotica — usare il moiré della fotocamera come portante — dopo aver trovato prior art e un risultato fatale sulla stabilità; si veda la Sezione 6.7.


3. Progettazione del sistema

3.1 Bit loading adattivo guidato dal sounding

Tutto parte da un channel sounder (sounding/, passo 1). Lo schermo mostra frame di sonda il cui spettro 2D è noto esattamente: una griglia di 256×256 campioni resa a 768×768 px, simboli QPSK su ~59k bin spettrali con il 10% dei bin lasciato deliberatamente nullo, incorniciata da 4 marker ArUco e una striscia di ID. L'analizzatore rettifica ogni frame catturato dalla fotocamera tramite i marker, torna nel dominio della frequenza e misura l'SNR per anello di frequenza radiale (24 anelli): potenza dei bin attivi su potenza dei bin nulli. I bin nulli fungono anche da misuratore di leakage — il residuo di registrazione finisce lì, quindi la stima è conservativa per costruzione.

Dalla curva di SNR per anello, make_allocation.py assegna una costellazione a ogni anello (QAM16 / QPSK / BPSK / spento, per soglie di SNR meno un margine; il waterfilling con gap vero e proprio è usato nella stima di capacità del sounder), scrivendo allocation.json. Questo è il bit loading adattivo: gli anelli che sopravvivono al blur ricevono costellazioni dense; gli anelli oltre il taglio ottico vengono spenti invece di trascinare in basso il frame error rate. I passi successivi chiudono il ciclo in modo più stretto: l'allocazione v2 sostituisce la stima di sounding con l'EVM che il ricevitore reale ha misurato su payload reale (SNR = 1/EVM²), e allocation_live è derivata dall'EVM di sessione live con una correzione esplicita di diluizione della potenza (Sezione 6.2).

3.2 PHY: OFDM 2D su spettri hermitiani

Ogni frame visualizzato è un simbolo 2D-OFDM. I dati sono collocati sui bin canonici di una griglia FFT 256×256 con simmetria hermitiana imposta, così che la trasformata inversa sia un'immagine a valori reali; il risultato viene reso nella geometria di sonda (marker + striscia di ID, con un valore riservato della striscia che distingue i frame dati dalle sonde). La catena del ricevitore, per ogni frame: rettifica ArUco → verifica della striscia → FFT → equalizzazione basata sui piloti (shift residuo sub-campione più un guadagno complesso per anello) → decodifica dell'header → demapping per anello → Reed-Solomon → un simbolo fountain.

3.3 Framing: piloti, header, RS, fountain LT

  • Piloti: 1 bin su 20, riservati in esclusiva (nient'altro trasmette lì), che portano QPSK nota. Finanziano l'equalizzazione e, più avanti, una stima gratuita della qualità del canale (EVM dei piloti) usata per il gating fail-fast.
  • Header: 56 bit — ID di sessione, contatore di frame, K, CRC8 — in BPSK con ripetizione 4×. L'header deliberatamente non dipende dall'allocazione del payload, il che in seguito abilita la demodulazione multi-profilo (il ricevitore prima equalizza, legge l'ID di sessione, poi effettua il demapping con l'allocazione di quella sessione).
  • FEC: Reed-Solomon RS(255, 255−nsym) sui byte di payload del frame (nsym 32 nella v1, portato a 48 dopo un rischio scampato — Sezione 6.8).
  • Fountain: un frame = un simbolo LT (distribuzione robust soliton, seed = contatore di frame). È questo che rende il collegamento genuinamente unidirezionale e tollerante alle perdite: qualunque sottoinsieme sufficientemente grande di frame decodificati ricostruisce il file; i frame persi o corrotti sono semplicemente simboli mancanti, recuperati al giro successivo del ciclo di trasmissione. Il file (nome, SHA-256, contenuto compresso con zlib) è suddiviso in K blocchi; la decodifica si completa quando il decoder LT risolve tutti i K.

Il primo trasferimento reale (video4) ha consegnato il suo file con SHA OK a partire da 1,1 s di video — 68 frame catturati, 29 simboli LT recuperati contro K = 15. Ha anche fatto emergere due trappole — l'avvelenamento dei retry e un margine RS di un byte — registrate nella Sezione 6.8.

3.4 Colore: inversione del crosstalk 3×3 per pilota

Un sounding dedicato al colore (sonde che pilotano una primaria del display alla volta) ha misurato la matrice di crosstalk display→fotocamera a 3×3 e l'ha trovata quasi diagonale — 0,99 sulla diagonale contro 0,19–0,25 fuori diagonale — con SNR di crominanza per canale approssimativamente pari a quello di luminanza. Il waterfilling su tre canali indipendenti proiettava 40.598 bit/frame, ~2,6× un singolo canale equivalente.

Il modem a colori pilota R, G, B con allocazioni indipendenti per canale. I piloti globali sono partizionati per canale (i % 3) e mantenuti esclusivi — sui bin pilota di un canale gli altri due trasmettono zero — così il ricevitore ottiene una stima pulita per anello della matrice di crosstalk 3×3 completa H, invertita bin per bin (x = H⁻¹y). L'header vive solo in G, anch'esso in esclusiva. Empiricamente la penalità di crosstalk è risultata ≈ 0 dopo l'inversione: l'allocazione color-v2, costruita a partire dal feedback EVM per canale, è salita a 676 B netti/frame da 263.

3.5 Canvas panoramico: due tile

Il passo 4 scala la capacità con lo spazio invece che con il frame rate: un canvas panoramico 1880×994 (che entra 1:1 in un 1080p) porta due tile da 768×768, 6 marker ArUco (4 agli angoli + 2 a metà bordo), una striscia di ID a livello di canvas. Un frame resta UN simbolo LT; l'RS copre la concatenazione dei payload di entrambe le tile; l'header è duplicato per tile (vince la prima che passa il CRC); ogni tile è equalizzata indipendentemente, inclusa la propria matrice H per anello. Capacità in loopback: 1400 B netti/frame — 41,0 KB/s a 30 simboli/s.

3.6 Ricevitore live zero-install

Il passo 5 ha rimosso del tutto il codec video dal ciclo. Il laptop esegue rx_live.py — un server aiohttp HTTPS/WSS su singola porta che genera automaticamente un certificato TLS autofirmato e stampa un QR code nel terminale. Il telefono apre l'URL nel suo browser di serie: la pagina servita cattura la fotocamera con getUserMedia (i frame non passano mai attraverso un codec video), codifica ogni frame in modalità intra-only — JPEG q0.9 all'inizio, H.264 hardware all-intra via WebCodecs dal passo 6b — e lo spedisce su un WebSocket binario al laptop, che demodula in tempo reale.

La decodifica è parallelizzata su un pool di processi basato su spawn (di default cpu_count()−2 worker) con un dispatcher freshest-first: al più un job in volo per worker, mailbox di profondità 1, frame stantii scartati — il fountain assorbe le perdite, quindi decodificare il frame più recente batte sempre l'accodarne uno vecchio. Il lavoro sul percorso critico per frame è passato da 187 a 146 ms su singolo thread (rilevamento ArUco su un'immagine a metà scala con raffinamento degli angoli a piena risoluzione, 2,66×; warp diretti per tile invece di warp sull'intero canvas, 1,59×), e il throughput di decodifica da 6,5 a 15–17 fps con 6 worker. Il fast_rs.py del passo 6c (Sezione 4.3) ha rimosso l'ultimo collo di bottiglia.

3.7 Adattamento di rate ad anello chiuso

Il passo 5c chiude il ciclo: poiché il demodulatore è co-locato con il trasmettitore (entrambi sul laptop), il ricevitore può comandare la pagina di trasmissione direttamente su un WebSocket di controllo — non serve alcun canale ottico di ritorno. Due insiemi di frame sono pre-codificati dallo stesso payload: "hot" (allocazione aggressiva, 1041 B/frame) e "safe" (conservativa, 621 B/frame). Il server osserva il tasso di successo RS su una finestra scorrevole di ~3 s: sotto il 40% → passa a safe; sopra l'85% per 5 s → riprova hot; permanenza minima di 5 s contro il flapping. Il demodulatore è multi-profilo (l'ID di sessione nell'header seleziona l'allocazione) e fail-fast (un EVM dei piloti sopra la soglia di 0,55 salta demap+RS, lo stadio più costoso per i frame condannati — con la soglia mascherata sugli anelli di payload del profilo). Un decoder LT per sessione gira in parallelo; il trasferimento si completa quando uno dei due termina.

Nella dimostrazione live il ciclo si è comportato esattamente come progettato: quando l'operatore si è allontanato dal telefono il log mostra lo switch verso il basso (rs_ok 33% < 40%) e, dopo il recupero delle condizioni, lo switch verso l'alto (rs_ok > 85% for 5.3 s) — SHA OK alla fine. Per quanto ci risulta (ricerche delimitate ad agosto 2026) questo è il primo collegamento ottico schermo-fotocamera documentato con adattamento di rate guidato dal feedback.


4. Campagna di misura

4.1 Risultati del sounding (giorno 1)

Tutte misure reali: iPhone che riprende lo schermo di un laptop, AE/AF bloccati, pattern al 60–80% del frame.

Modalità di cattura Capacità (bit/frame) Nota
1080p30 13.296 riferimento
4K30 13.181 il 4K non sposta il taglio
1080p60 13.009 → ~663 kbit/s netti proiettati a 60 fps

Tre risultati strutturali:

  • Il taglio è dovuto all'ottica/al display, non al campionamento della fotocamera. L'energia utile muore a f ≈ 0,25 cicli/campione (punto a 3 dB ≈ 0,18); quadruplicare i pixel della fotocamera (4K) ha cambiato la capacità di ~1%. Il limite è la catena di blur lente/pannello.
  • L'anello 0 è avvelenato dall'illuminazione. La potenza di rumore nell'anello a frequenza più bassa ha misurato 130× quella dell'anello 1 — gradienti di luce ambiente attraverso il pannello, non rumore del sensore. Tutte le allocazioni successive derateano o disabilitano l'anello 0.
  • L'auto-esposizione non bloccata costa ~1 kbit/frame. Il blocco dell'AE è diventato disciplina standard.

Selftest dell'intera pipeline sintetica: 29/29 controlli.

4.2 Progressione della capacità: allocazioni misurate

Byte netti di payload per frame, in ordine di progetto — ogni passo guidato da una nuova misura:

Allocazione B netti/frame Cosa è cambiato
v1 luma (statica, dal sounding) 202 soglie sull'SNR misurato dal sounding
v2 luma (dinamica) 251 SNR dall'EVM del ricevitore reale; RS 32→48
color v1 263 3 canali, penalità di crosstalk di 1,5 dB assunta
color v2 676 feedback EVM per canale; penalità di crosstalk misurata ≈ 0
wide-4K ("hot") 1041 2 tile, aggressiva
wide safe 621 2 tile, conservativa
live 1728 EVM live + correzione auto-consistente di diluizione della potenza
live2 (record) 1634 anello B7 spento

Il salto singolo più grande — color v1 → v2, 263 → 676 — è venuto puramente dal sostituire una penalità assunta con una misurata. Misura → alloca → misura di nuovo è il metodo del progetto in miniatura. Due traguardi intermedi ancorano la tabella a sessioni reali: video7 (color v1, conservativa) ha decodificato con il 100% di successo sui frame e zero errori RS, e video12 (wide safe a 4K30) ha fatto lo stesso su 84/84 frame (Sezione 6.1).

4.3 Progressione del throughput live

Record wall-clock nel corso delle sessioni live, dalla prima decodifica live al record finale:

Record live (KB/s) Fase
2,7 prime sessioni live (passo 5, demodulazione a singolo worker, limitata dalla decodifica)
9,21 passo 5b: pool di 6 worker + JPEG q0.9 fisso
10,60 sessioni dei passi 6–6c
24,99 sessioni dei passi 6–6c
25,43 sessioni dei passi 6–6c
27,97 sessioni dei passi 6–6c
33,20 record finale, sera del 5 agosto

Deliberatamente non attribuiamo a posteriori ogni record intermedio a una singola modifica; le sessioni live variavano insieme allocazione, encoder e condizioni ambientali (Sezione 6.6), e ogni record è stato semplicemente registrato alla fine della sessione che lo ha prodotto. I tre moltiplicatori dei passi 6–6c sono però misurati a banco individualmente: più byte per frame (allocation_live, 1728 B, proiezione da bench di saturazione 41,3 KB/s), codifica H.264 hardware sul telefono (12–19 ms/frame costanti, proiezione da bench 50,6 KB/s), e decodifica RS veloce — fast_rs.py, un decoder RS(255) batch in numpy identico bit a bit a reedsolo, che porta lo stadio rs da 113,3 a 3,2–5 ms/frame (bench realistico su chunk da 280 KB con 7 worker: 29–30 fps decodificati, proiezione 46,5–48 KB/s). Il percorso su video registrato, per contro, si era saturato a ~16–17 KB/s con l'attuale demapper a decisione rigida (miglior risultato: 17 KB/s wall-clock su video8, color v2, 30 sym/s).

4.4 La sessione del record (5 agosto, sera)

  • Payload: demo_payload6.zip, 206.310 byte — i dati di canale del progetto stesso più tutti e 10 i build contract.
  • Risultato: consegnato e verificato con SHA-256 in 6,1 s dal primo frame della fotocamera → 33,20 KB/s wall-clock; rate sostenuto durante la sessione ~34,3 KB/s.
  • Qualità del collegamento: 143 frame processati, 15 fallimenti RS (89,5% di successo sui frame), 1 fallimento di rettifica, 0 errori di header, 0 duplicati; EVM di payload 23–36%.
  • Setup: iPhone (iOS 18.7, Safari di serie, zero install) semplicemente appoggiato a un oggetto, in orizzontale, parametro di pagina ?vbr=25000000; laptop con 8 core che esegue 7 worker di demodulazione + fast_rs; trasmettitore a 30 simboli/s sul canvas panoramico a 2 tile con allocation_live2 (1634 B netti/frame — un tetto di capacità grezza per frame di ~48 KB/s a 30 sym/s, di cui il rate wall-clock consegnato è il ~69%).

Su tutte le configurazioni e tutti e tre i giorni: 22/22 trasferimenti completati con verifica SHA-256. Lo stack fountain + RS + fail-fast non ha mai consegnato un file sbagliato — o finiva correttamente, o continuava a ciclare.


5. Contributi originali

Confine rispetto alla prior art: tutte e cinque le affermazioni seguenti sono delimitate dalle nostre ricerche in letteratura e sul web ad agosto 2026; "non pubblicato" significa che non siamo riusciti a trovarlo, non che sia dimostrabile che non esista.

  1. Bit loading adattivo per portante a partire dal channel sounding su una coppia schermo-fotocamera. PixNet e FOCUS usano costellazioni fisse scelte in fase di progetto; noi misuriamo ogni specifica catena schermo-lente-sensore e carichiamo le portanti di conseguenza.
  2. Allocazione per dispositivo tramite un ciclo misura → alloca → misura. L'EVM di payload del ricevitore alimenta l'allocazione successiva (SNR = 1/EVM²); questo ciclo è stato eseguito ripetutamente e ha prodotto la progressione 202 → 1634 B/frame della Sezione 4.2.
  3. Il vincolo di potenza totale sul bit loading ("diluizione della potenza"), osservato e corretto su questo canale. Accendere più bin riduce la potenza per bin perché il display normalizza il drive totale; il caricamento ingenuo a soglia è quindi controproducente. Il nostro allocatore live applica la diluizione che ciascun insieme candidato causerebbe esso stesso prima di testare le soglie (dettagli nella Sezione 6.2).
  4. Adattamento di rate ad anello chiuso su un collegamento ottico unidirezionale, con il percorso di feedback realizzato attraverso il ricevitore co-locato con il trasmettitore — comportamento di commutazione misurato, senza flapping, in una sessione live.
  5. Un insieme di caratterizzazioni di canale precedentemente non documentate: il codec video come collo di bottiglia dominante dei collegamenti schermo-fotocamera registrati (e il 4K come cura); psy-rd/AQ di x264 che danneggiano attivamente i segnali di modem; la codifica JPEG con fallback software nei worker di iOS Safari; la luce ambiente in funzione dell'ora del giorno come variabile di EVM a livello di sessione. Tutto nella Sezione 6.

6. Risultati negativi e trappole

Questa sezione è l'anima del rapporto. La maggior parte del throughput tra 2,7 e 33,20 KB/s non è venuta da elaborazione del segnale ingegnosa, ma dal trovare, una a una, le cose che stavano silenziosamente distruggendo il segnale.

6.1 Il codec video era il collo di bottiglia — e il 4K era la cura

Il canvas panoramico, registrato a 1080p60, era inspiegabilmente pessimo: l'EVM dei canali R e B degradava da ~30% a ~51%. Stesso canvas, stesso telefono, registrato a 4K30: 100% di successo sui frame — video12: 84/84 frame, zero fallimenti RS (EVM di payload 24,5–33,3% per anello). Il muro era il codec, non l'ottica: a 1080p l'encoder HEVC del telefono affama di bit una texture pseudo-rumore a schermo pieno, mentre a 4K il bitrate per area è abbastanza alto da preservarla. Questo si generalizza: qualunque sistema schermo-fotocamera valutato attraverso video registrato sta in parte misurando il codec di registrazione.

La prova definitiva è venuta dal rimuovere del tutto il codec. Il percorso live con getUserMedia (JPEG q0.9 intra-only, ~4× il bitrate per area del 4K HEVC registrato) ha portato la stessa allocazione hot in una finestra di successo RS del 90–95%, dove la registrazione 4K di quella stessa allocazione arrivava al 31% — il canale nudo vale +1,2–2 dB.

6.2 Diluizione della potenza: più portanti non significano più capacità

Confrontando le allocazioni panoramiche safe (621 B) e hot (1041 B) sullo stesso canale registrato in 4K (video13): attivare i bin aggiuntivi è costato −3 dB per bin — il display normalizza la potenza totale emessa, quindi l'energia viene diluita su tutto ciò che è acceso. L'EVM è passato da 31% a 44%, il successo sui frame da 100% a 31% (62 utilizzabili su 202 frame distinti, 140 fallimenti RS). La lezione è strutturale: il bit loading su questo canale è waterfilling sotto vincolo di potenza totale, non sogliatura indipendente per anello. make_allocation_live.py applica quindi la penalità di diluizione 10·log10(N_new/N_old) dB dentro la valutazione dei candidati — ogni insieme candidato deve superare le soglie di costellazione sotto la diluizione che esso stesso creerebbe. La soluzione auto-consistente: R e G mantengono i bin dell'insieme hot in QPSK pieno, B assorbe la diluizione per aggiungere gli anelli 6–7 in BPSK, e gli anelli di notch del display (R2/B2) restano spenti. Risultato: 1728 B/frame che si decodificano davvero, invece di 1041 che per lo più no.

6.3 Le ottimizzazioni psicovisive di x264 sono ostili ai segnali di modem

Quando abbiamo spostato l'uplink del telefono da JPEG a H.264 via WebCodecs (all-intra), la qualità era misteriosamente scarsa finché non abbiamo controllato le impostazioni dell'encoder: psy-rd e la quantizzazione adattiva danneggiano attivamente il segnale, spostando i bit lontano dalla texture statisticamente uniforme — che è esattamente l'aspetto di un canvas OFDM — verso le regioni che un essere umano noterebbe. Con psy-rd=0, aq-mode=0 (e chroma-qp-offset −2), H.264 a 30 Mbit/s misura il 7,0% di EVM sul canale sintetico di riferimento contro il 4,6% di JPEG q0.9 (20M: 9,5%; 40M: 5,5%) — entro la nostra soglia di accettazione di +3 punti, a circa metà dei byte per messaggio. Sul canale di banco calibrato sull'EVM del collegamento reale (~28%) la differenza sparisce nel rumore di canale. Una trappola collegata: il rate control di Safari è adattivo, quindi le dimensioni reali dei chunk variavano tra 220 e 460 KB a seconda del contenuto e della sessione; richiedere vbr=25M ha compensato nella pratica (chunk ~380–460 KB) ed è ciò che la sessione del record ha usato.

6.4 iOS Safari codifica il JPEG via software dentro i worker

Il piano del passo 6 di parallelizzare la codifica JPEG su Web Worker (OffscreenCanvas) funzionava splendidamente nei test su desktop (~31 fps di codifica) ed è fallito in modo assurdo sull'iPhone reale: dentro un worker, la codifica JPEG di Safari scende a ~650 ms/frame — via software — contro ~56 ms sul thread principale. La pagina ora esegue automaticamente un benchmark (10 frame, soglia di 150 ms) e ricade in modo permanente sulla codifica nel thread principale quando il percorso worker è software.

6.5 Risparmio energetico e throttling termico sono parametri di canale

La modalità a basso consumo e il throttling termico rallentano la CPU dell'iPhone di circa 6× (da 56 a 340 ms per codifica JPEG sul thread principale). Una pipeline sul telefono limitata dalla CPU ha quindi il throughput controllato dalla percentuale di batteria e dalla temperatura dello chassis — inaccettabile. È questo, più della velocità grezza, il motivo per cui il passo 6b è passato all'encoder H.264 hardware del telefono: 12–19 ms/frame, costanti, indipendentemente dallo stato della CPU. Dopo il 6b, mettere in carica il telefono ha smesso di far parte della procedura di record.

6.6 La regola qualità JPEG contro fps, e la varianza tra sessioni

La nostra logica di "qualità adattiva" del passo 5b (abbassare la qualità JPEG sotto backpressure del WebSocket) era esattamente al contrario: una sessione scivolata a q0.7 ha misurato il 40% di EVM e ha consegnato 2,9 KB/s, mentre q0.9 fissa ha consegnato EVM 25–36% e 9,21 KB/s — 3,1× meglio. La perdita di qualità corrompe ogni frame un po' (e l'RS paga per tutto); la perdita di fps distanzia soltanto i frame buoni (al fountain non importa). La regola, ora cablata nel codice: mai ridurre la qualità; ridurre il frame rate (a gradini del −20% fino a un minimo, con recupero graduale).

A parte questo, le sessioni hanno il loro meteo. Le sessioni del mattino, con la luce ambiente che dilava il pannello, hanno misurato +5–6 punti di EVM, facendo crollare il successo sui frame dell'allocazione hot dall'86–95% al 9–36%. Ogni record è stato stabilito di sera. E una lezione puramente operativa: dopo una sessione bloccata e convergente, non toccare niente — il trasferimento successivo parte già in crociera.

6.7 Il vicolo cieco del moiré

All'inizio del giorno 1 abbiamo considerato di sfruttare il moiré della fotocamera — l'aliasing tra la griglia di pixel del display e quella del sensore — come portante nascosta ad alta frequenza. Due risultati l'hanno ucciso. Primo, esiste prior art: mQRCode (MobiCom 2019, e brevetto US 10,970,607) e mID (USENIX Security 2021) usano già pattern di moiré, il secondo arrivando a ~10 bit per foto a 58–80 cm — ordini di grandezza sotto le nostre necessità. Secondo, e decisivo: l'effetto è geometricamente instabile — MoirePose dimostra che 5 mm di traslazione o 5° di rotazione ridisegnano completamente le frange. Una portante che un tremore della mano ri-randomizza non è una portante. Scartato prima di scrivere una riga di codice; le ore sono andate nel sounding.

6.8 Piccole trappole che sono costate ore vere

  • Avvelenamento dei retry: i contatori di frame venivano marcati come "visti" prima che la loro decodifica RS riuscisse, quindi un primo avvistamento corrotto di un contatore rendeva inutile ogni sua ritrasmissione. Corretto: visto = decodificato.
  • Margine RS: la diagnosi del primo trasferimento (559/1053 frame decodificabili) mostrava una mediana di 15 byte errati per frame contro un limite di correzione di 16 (nsym 32) — un filo di rasoio che funzionava nelle demo e sarebbe fallito sul campo. La v2 è passata a nsym 48.
  • Byte di coda RS: quando i byte lordi del frame non sono un multiplo di 255, la coda residua ≤ nsym deve essere ignorata in modo identico da ogni percorso di decodifica — una discrepanza qui ha rotto silenziosamente il profilo safe ad anello chiuso (i worker sollevavano un'eccezione su ogni frame safe) finché non è stata corretta.
  • Encoder con stato nella calibrazione: il rate control adattivo di x264 rendeva la nostra bisezione di calibrazione del rumore dipendente dalla storia — ogni punto di sonda ereditava lo stato di rate control del punto precedente. Correzione: encoder fresco + 2 frame di warmup per ogni punto di calibrazione.
  • Più thread nel decoder lo rendevano più lento: abilitare il multi-threading nei decoder H.264 di ciascun worker peggiorava il bench saturato a 7 worker (contesa di CPU con 7 processi già occupati); resta disattivato di default. Misurato, non assunto.

7. Confronto onesto: cosa significa la metrica, e cosa non affermiamo

La nostra metrica è più severa di quella della letteratura. Riportiamo la consegna del file in wall-clock: il cronometro parte al primo frame ricevuto dalla fotocamera e si ferma quando l'SHA-256 del file ricostruito è verificato. Questo include l'acquisizione della fotocamera, ogni frame scartato e fallito, l'overhead del fountain, la parità RS e la verifica finale — tutto. PixNet, FOCUS e libcimbar riportano il goodput sostenuto su un collegamento già stabilito e sano, il che perdona l'avvio e media sul tratto buono. Il nostro rate sostenuto durante la sessione del record era ~34,3 KB/s; mettiamo in evidenza 33,20 perché è ciò che sperimenta un utente. Contro i ~30 KB/s sostenuti di FOCUS, i nostri 33,20 wall-clock sono +10,7% sulla metrica più difficile — e su hardware consumer del 2026 con un ricevitore zero-install, contro un prototipo di ricerca del 2016; il confronto è tra sistemi, non tra CPU.

Ciò che cimbar e Decimen mantengono. I ~106 KB/s di libcimbar (misurati in modo riproducibile) e i ~129 KB/s di Decimen (auto-dichiarati) restano ben al di sopra del nostro numero, e non affermiamo il contrario. Appartengono all'altra famiglia: i barcode a matrice spaziale decidono in modo rigido griglie dense di celle e raggiungono densità spaziali grezze che l'OFDM non può eguagliare quando il canale è a corto raggio, nitido e stabile. Lo scambio è simmetrico — il nostro sistema guadagna degradazione graduale (il blur costa gli anelli alti, non celle casuali), un quadro misurato del canale portante per portante, l'adattività a partire da quel quadro, e un ricevitore che dista una scansione QR su un telefono non modificato. All'interno della famiglia nel dominio della frequenza, e sui collegamenti in tempo reale, il nostro numero è — per quanto ci risulta ad agosto 2026 — il record.


8. Limitazioni e lavoro futuro

  • La cattura da browser è il tetto. Nelle nostre sessioni getUserMedia di Safari ha consegnato 30 fps indipendentemente da ciò che veniva richiesto (degrada silenziosamente da frameRate: exact 60). Un'app di cattura nativa — o un percorso di cattura a 60 fps dove disponibile — raddoppierebbe immediatamente il symbol rate; il pool di demodulazione tiene già il passo con i 30 fps.
  • FFT su GPU. La demodulazione è puro CPU (7 processi). Rettifica + FFT su GPU sono l'ovvio moltiplicatore successivo e — secondo la nostra ricerca di prior art — la demodulazione OFDM su GPU/NPU per questo canale resta non pubblicata.
  • QAM16 nelle allocazioni live. L'EVM del canale live (23–36%) tiene le allocazioni live su QPSK/BPSK; ottiche più nitide, controllo dell'esposizione o distanze minori dovrebbero aprire anelli in QAM16, che l'allocatore e il demapper già supportano.
  • Allineamento della dimensione dei blocchi LT tra allocazioni. Poiché la dimensione dei blocchi differisce per allocazione (1041 contro 621 B nell'anello chiuso), i simboli fountain di insiemi diversi non possono combinarsi in un unico decoder; il ciclo mantiene un decoder LT per sessione. Allineare le dimensioni dei blocchi — o uno strato rateless indipendente dalla dimensione dei simboli — permetterebbe a ogni frame di ogni profilo di contribuire a una singola decodifica.
  • 120 fps. Con la codifica hardware a 12–19 ms/frame e il fountain che assorbe le perdite, un percorso di cattura a 120 fps è un plausibile ulteriore moltiplicatore oltre i 60.

9. Riproducibilità

È tutto nel repository, e ogni numero di questo rapporto risale a un file al suo interno. La mappa:

Percorso Contenuto
sounding/README.md manuale operativo completo passo passo (sounding, modem, colore, panoramico, live, anello chiuso, bench) — si parte da qui
docs/PROTOCOL.md la specifica del formato di trasmissione: tutto ciò che serve a un'implementazione indipendente per interoperare
sounding/CONTRACT.md … CONTRACT10.md i 10 build contract, uno per passo, con i gate di accettazione
sounding/spec.json, spec_wide.json tutti i parametri geometrici/numerici (condivisi in modo bit a bit identico da generatore e analizzatore)
sounding/out/reale*/report.md report di sounding (1080p30 / 4K30 / 1080p60) con tabelle di SNR per anello
sounding/out_color/report_color.md sounding a colori: SNR per canale, matrice di crosstalk 3×3
sounding/rx_*/rx_report.md report di decodifica per sessione (contatori di frame, tabelle EVM, verdetto SHA)
docs/FACTS.md il registro canonico dei fatti da cui questo rapporto è costruito

La pipeline dall'inizio alla fine: generate_probes.py → filmare display/index.html → analyze.py video.mp4 → make_allocation.py → tx_encode.py FILE → filmare display/tx*.html → rx_decode.py video.mp4; live: rx_live.py (+ telefono sul QR code); anello chiuso: rx_live.py --closed-loop closedloop.json.

Non serve alcun telefono per validare il codice. I gate sintetici, tutti verdi sul codice finale: selftest.py 29/29, loopback_test.py 35/35, test_live_loopback.py 9/9, test_closed_loop.py 13/13, test_h264_path.py 10/10, bench_saturation.py 9/9 (sia JPEG sia --codec h264), più fast_rs.self_test() con 4000/4000 casi correggibili identici byte a byte a reedsolo e 800/800 fallimenti concordanti. Le dipendenze sono otto pacchetti pip su Python 3.13 di serie (sette per l'installazione base, più PyAV per il percorso H.264); il lato display è HTML standalone.

Per riprodurre il record, seguire la procedura di record in sounding/README.md (passo 6): luce serale, telefono appoggiato in orizzontale, ?vbr=25000000 e — come da Sezione 6.6 — non toccare niente una volta agganciato.