Far funzionare su Linux un dispositivo senza documentazione
Un controller USB senza supporto ufficiale, un analizzatore di pacchetti e un workspace Rust. Come si affronta un protocollo che nessuno ha pubblicato.
Ho comprato un controller a pulsanti pensato per lo streaming. Su Windows funzionava con il software del produttore. Su Linux, niente: il sistema lo vedeva come un dispositivo HID generico e i pulsanti non facevano assolutamente nulla di utile.
Non c’era documentazione del protocollo. Non c’era SDK. I produttori a queste domande non rispondono.
Ho finito per scrivere il driver. Lo racconto perché il metodo vale per qualunque integrazione in cui dall’altra parte c’è una scatola nera, e succede molto più spesso di quanto sembri: una vecchia centrale telefonica, un lettore di tessere, una bilancia industriale, un ERP senza API.
Prima guardare, poi scrivere
La tentazione è aprire l’editor. È l’errore.
La prima cosa è catturare traffico vero. Su Linux, usbmon mostra tutto quello che entra ed esce da una porta USB, e Wireshark lo legge direttamente. Con il dispositivo in funzione su una macchina virtuale Windows, premi i pulsanti e osservi quali pacchetti escono.
Quello che cerchi in quella montagna di byte sono schemi ricorrenti:
- Premere il pulsante 1 produce sempre lo stesso byte nella stessa posizione.
- Rilasciarlo ne produce un altro.
- Premere due pulsanti insieme produce la somma, e questo ti dice che è una mappa di bit e non un indice.
Quest’ultimo è il tipo di scoperta che cambia tutto il disegno del codice. E non si deduce leggendo: si deduce premendo pulsanti e prendendo appunti.
Annota tutto, anche quello che non capisci
Nella mia cattura c’era un byte che cambiava senza che io toccassi nulla. Ho perso un paio d’ore a cercare di dargli un senso finché ho capito che era un contatore di sequenza: saliva da solo, ripartiva a 255 e non significava altro.
L’ho lasciato scritto nel repository, in testo semplice, accanto al codice. Non perché sia elegante, ma perché fra sei mesi io stesso non me lo ricorderò, e perché qualcun altro vorrà capire perché quel byte viene ignorato.
Un protocollo ricostruito senza appunti è un protocollo da ricostruire di nuovo.
Perché Rust, e perché tre crate
Ho scelto Rust per un motivo concreto: in un driver gli errori sono di byte e di tempi, e il compilatore ti toglie di mezzo tutta una famiglia di errori di memoria mentre combatti con gli altri due.
L’ho diviso in tre parti:
- Il protocollo, senza alcun ingresso o uscita. Riceve byte, restituisce eventi. Si testa senza avere il dispositivo collegato.
- Il trasporto, che parla con l’USB vero.
- L’applicazione, che traduce gli eventi in azioni di sistema.
Questa separazione non è cerimonia. È ciò che permette di avere test veri: il 90% della logica si prova con byte salvati in un file, senza hardware. Se fosse tutto insieme, provare qualsiasi cosa richiederebbe avere l’apparecchio collegato, e i test non si scriverebbero mai.
Provalo dove non l’hai scritto
Il driver gira in CI su Fedora, Debian, Ubuntu e Arch. Non è eccesso di zelo: i permessi di accesso all’USB e le regole di udev cambiano fra le distribuzioni, ed è esattamente il tipo di cosa che funziona sulla tua macchina e fallisce su quella di chi lo installa.
Se pubblichi qualcosa che tocca il sistema, provalo su più di un sistema. Costa poco e ti risparmia metà delle segnalazioni.
Cosa ci si porta a casa
Quando un fornitore ti dice “non ha API”, quello che quasi sempre intende è “non ha API documentate”. Se esiste un client che funziona, esiste un protocollo, e un protocollo si può osservare.
Non sempre conviene: richiede tempo, e se il produttore cambia il firmware si ricomincia. Ma quando l’alternativa è sostituire l’intero parco apparati, un paio di settimane di reverse engineering costano parecchio meno.
Il codice è pubblicato con licenza MIT, nel caso il punto di partenza serva a qualcuno.