Leggere da iPhone la batteria ibrida di una Lexus CT200h
Un'app iOS che legge la batteria ibrida di una Lexus CT200h tramite un adattatore diagnostico Bluetooth da pochi euro: come si interroga la centralina della batteria, come si decodificano tensioni, correnti e temperature, e come si comanda la ventola in sicurezza.
Indice
In breve
- La centralina della batteria ibrida conosce tensioni, corrente, carica e temperature, ma l'auto non le mostra. Con un adattatore da pochi euro nella presa diagnostica si possono leggere.
- L'app chiede i dati alla centralina con comandi diagnostici e riceve pacchetti di byte. Il lavoro vero è capire come trasformare quei byte in numeri corretti.
- Un numero sbagliato ma plausibile è l'errore peggiore: le temperature mi risultavano 10 °C più alte del reale, e niente lo segnalava.
- L'app può anche comandare la ventola di raffreddamento della batteria, ma nelle modalità normali non può mai raffreddare meno di quanto farebbe l'auto da sola.
- Se l'app smette di comunicare, entro un paio di secondi la ventola torna sotto il controllo dell'auto.
La Lexus CT200h è una compatta ibrida, parente stretta della Toyota Prius. La sua batteria di trazione è un pacco al nichel-metallo idruro (NiMH) sotto il pianale del bagagliaio, raffreddato da una ventola che aspira aria dall'abitacolo. La centralina della batteria misura continuamente tensioni, corrente, stato di carica e temperature, ma nessuna schermata di bordo le mostra. Su Android esistono app che le leggono; io volevo un'app iOS che mostrasse i dati della mia auto e mi permettesse anche di regolare la ventola.
L'hardware è minimo: un adattatore Bluetooth Low Energy basato su ELM327 (il chip su cui si basano quasi tutti gli adattatori diagnostici economici) inserito nella presa OBD-II, quella che usano le officine per la diagnosi. L'app è scritta in Swift e SwiftUI.
Perché guardare la batteria
Il pacco è composto da 28 moduli in serie, che la centralina misura a coppie: 14 blocchi da circa 14,4 V nominali, per un totale di circa 200 V. Una batteria NiMH invecchia soprattutto con il calore, e quando invecchia i blocchi smettono di comportarsi allo stesso modo: uno cala di tensione sotto carico più degli altri, oppure la sua resistenza interna cresce. Guardare i 14 blocchi uno accanto all'altro, e la temperatura del pacco mentre si guida, dice molto più della spia sul cruscotto, che si accende quando il problema è già serio.
Per chi non è del mestiere. Immaginate 14 pile collegate in fila. Finché sono tutte uguali la batteria lavora bene. Quando una comincia a cedere, tutto il pacco lavora male, e l'auto se ne accorge tardi. L'app serve a vedere le 14 pile una per una.
La catena di comunicazione
Tra il telefono e la batteria ci sono due tratti di comunicazione molto diversi.
Il primo tratto è Bluetooth Low Energy. L'ELM327 nasce come interprete di comandi su una porta seriale; in BLE la seriale diventa una coppia di caratteristiche GATT, una su cui scrivere e una da cui ricevere notifiche. Adattatori diversi usano identificativi di servizio diversi, quindi l'app cerca tra alcune coppie note quella che ha una caratteristica scrivibile e una che invia notifiche.
Il secondo tratto è il bus CAN, la rete con cui comunicano le centraline dell'auto. L'adattatore riceve dal telefono comandi di testo, li trasforma in frame CAN, li invia alla centralina e restituisce la risposta come testo esadecimale.
Preparare l'adattatore
All'avvio l'app configura l'adattatore con una sequenza fissa di comandi AT, che sono documentati nel datasheet dell'ELM327:
| Comando | A cosa serve |
|---|---|
ATWS | riavvio dell'adattatore |
ATE0 | niente eco dei comandi nella risposta |
ATSP6 | protocollo ISO 15765-4: CAN con identificativi a 11 bit, 500 kbit/s |
ATAT1 | tempi di attesa adattivi |
ATH1 | mostra l'indirizzo di chi risponde |
ATL0, ATS0 | niente a capo e niente spazi: risposte più corte da interpretare |
ATCAF1 | l'adattatore gestisce da solo la formattazione dei frame CAN |
Poi, prima di ogni richiesta alla centralina della batteria, l'app imposta l'indirizzo di destinazione (7E2) e lo stesso indirizzo per i messaggi di controllo del flusso, che servono quando la risposta è lunga.
Risposte lunghe: più frame per un messaggio
Un frame CAN trasporta al massimo 8 byte di dati. Una richiesta come «dammi le tensioni dei blocchi» sta comodamente in un frame, ma la risposta, con 14 tensioni da 2 byte ciascuna, no. Lo standard ISO 15765-2 (ISO-TP) spezza i messaggi lunghi in più frame: il primo dice quanto è lungo il messaggio, chi riceve risponde «manda pure il resto», e i frame successivi sono numerati.
Ogni richiesta è formata da un modo e da un PID, l'identificativo del dato richiesto. Lo schema è questo. I byte indicati con xx sono dati; lunghezze e contenuti sono illustrativi:
→ 7E2 02 21 81 richiesta: modo 21, PID 81
← 7EA 10 LL 61 81 xx xx xx xx primo frame: il messaggio è lungo LL byte
→ 7E2 30 00 00 controllo di flusso: invia il resto
← 7EA 21 xx xx xx xx xx xx xx frame successivo n. 1
← 7EA 22 xx xx xx ... frame successivo n. 2
Il 61 81 all'inizio della risposta è la conferma: la risposta positiva a una richiesta di modo 21 ha codice 61, seguito dal PID richiesto. L'ELM327 manda da solo il controllo di flusso e restituisce i frame uno per riga; l'app toglie i byte di servizio, controlla che la risposta inizi con la conferma giusta e ricompone il contenuto.
Tenere aperta la sessione
La centralina chiude la sessione diagnostica se per qualche secondo non riceve nulla. Per questo l'app invia ogni 2,5 secondi un messaggio tester present (3E 00), che nello standard diagnostico UDS significa semplicemente «sono ancora qui».
Un comando alla volta
L'adattatore gestisce un solo comando alla volta, ma nell'app le richieste arrivano da tre parti indipendenti: il ciclo di lettura dei dati, il tester present e il controllo della ventola.
L'accesso all'adattatore passa da un actor Swift, un costrutto che garantisce che il suo codice non venga eseguito da due flussi contemporaneamente. Non basta, però. Un actor si sospende a ogni punto di attesa, per esempio mentre aspetta la risposta dell'adattatore, e in quel momento può iniziare a eseguire un'altra richiesta. Una sequenza completa è fatta di più passi: imposto l'indirizzo, invio la richiesta, aspetto la risposta. Se un'altra sequenza si infila a metà, l'adattatore riceve comandi mescolati. Il sintomo era un errore di scrittura intermittente, «busy», difficile da riprodurre.
La soluzione è una coda esplicita: ogni sequenza completa prende un «turno» prima di iniziare e lo rilascia solo alla fine; chi arriva nel frattempo aspetta in fila. È lo stesso principio del numerino al banco dei salumi: il banco è uno solo, e chi prende il numero viene servito per intero prima del successivo.
Da byte a numeri
Questi sono i dati che l'app chiede alla centralina. Il modo 21 è un modo diagnostico specifico di Toyota, non standard OBD-II:
| Modo + PID | Contenuto |
|---|---|
2181 | tensione dei 14 blocchi, batteria ausiliaria a 12 V, tensione del pacco |
2187 | temperature: aria in ingresso e sonde nel pacco |
2195 | resistenza interna dei blocchi |
2198 | corrente, limiti di potenza in carica e scarica, stato di carica minimo e massimo |
2101 | stato di carica |
219B | stato della ventola |
Per le formule sono partito dalle tabelle di PID che la community usa per la Prius di terza generazione, che ha lo stesso sistema ibrido della CT200h. Le ho confrontate tra loro e, soprattutto, con le letture sulla mia auto.
La maggior parte dei valori sono parole a 16 bit big-endian: due byte, il primo è la parte alta. La tensione di un blocco, per esempio:
byte ricevuti 2E 14
valore grezzo 0x2E14 = 46 × 256 + 20 = 11.796
tensione 11.796 / 819,2 ≈ 14,40 V
Nelle fonti la stessa formula compare in forme diverse: grezzo × 79,99 / 65.535, oppure grezzo / 819,2, oppure grezzo × 0,122 / 100 arrotondato. Sono equivalenti a meno dello 0,1%: 65.535 / 80 ≈ 819,2, cioè il fondo scala è di circa 80 V. Tre fonti diverse che danno lo stesso risultato sono un buon segno.
Lo stato di carica è un solo byte, con fondo scala 255 = 100%. I limiti di potenza in 2198 sono byte con un passo di 0,5 kW e uno scostamento di −64 kW.
Ogni decodifica controlla la lunghezza della risposta e scarta i valori fuori da un intervallo plausibile invece di mostrarli: un blocco NiMH deve stare tra 5 e 25 V, la batteria ausiliaria a 12 V tra 0 e 20 V, il pacco tra 100 e 400 V. Un valore fuori intervallo indica quasi sempre un errore di decodifica, non una batteria anomala. L'app conserva anche i byte grezzi di ogni risposta, così la decodifica si può rifare in seguito.
Il segno della corrente
La corrente arriva come valore a 16 bit centrato su 0x8000: sottraendo 32.768 e dividendo per 100 si ottengono ampere, con risoluzione di un centesimo. Nella convenzione Toyota il valore è positivo quando la batteria si scarica.
L'app inverte il segno, perché per chi guarda è più naturale leggere «+» quando la batteria si ricarica. Una scelta del genere va verificata, non assunta. L'ho controllata su una registrazione reale: mentre il motore termico ricaricava il pacco e lo stato di carica saliva dal 38 al 50%, il valore grezzo restava sotto il centro, cioè corrente in ingresso. E il picco di corrente in ingresso coincideva con una frenata rigenerativa, come doveva essere.
Le temperature e i 10 gradi di scarto
Le temperature sono il punto in cui è più facile sbagliare. La lettura più ovvia prende un byte per sonda e sottrae 40, la convenzione più comune nell'OBD-II standard. Con questa lettura i numeri sono plausibili, 30, 35, 38 gradi, ma circa 10 °C più alti del reale. Su questo punto le fonti pubbliche non sono d'accordo tra loro, ed è per questo che l'ho verificato a fondo.
In realtà ogni temperatura è una parola a 16 bit con un'altra scala:
temperatura = grezzo × 255,9 / 65.535 − 50
Il motivo dei 10 gradi esatti si vede facendo i conti. Moltiplicare per 255,9 / 65.535 equivale quasi esattamente a dividere per 256, cioè a prendere solo il byte alto. La formula giusta è quindi, in pratica, «byte alto − 50». La convenzione standard legge lo stesso byte e sottrae 40. Un esempio:
byte ricevuti 4E 07
formula corretta 19.975 × 255,9 / 65.535 − 50 ≈ 28,0 °C
convenzione OBD-II 0x4E = 78, 78 − 40 = 38 °C
È il tipo di errore più insidioso: niente crash, niente valori assurdi, solo un numero sbagliato che sembra giusto. Per questo, per ogni grandezza, controllo due cose: che le fonti siano coerenti tra loro e che l'andamento abbia senso rispetto a quello che fa l'auto. La corrente deve cambiare segno tra carica e scarica, la temperatura deve salire sotto carico e scendere quando la ventola gira forte.
La ventola
Oltre a leggere, l'app può comandare la ventola di raffreddamento della batteria, con un comando diagnostico che chiede alla centralina di forzare una delle 6 velocità disponibili. È l'unica parte in cui un errore ha conseguenze sull'auto, e l'ho progettata partendo da quello che può andare storto.
La centralina riprende il controllo da sola
Dalle registrazioni sull'auto si vede che la centralina mantiene la velocità comandata solo per circa 2 secondi dopo l'ultimo comando, poi torna alla sua gestione automatica: nei log la velocità scendeva da 6 a 3 a 1. Per mantenere una velocità l'app ripete quindi il comando ogni 1,5 secondi.
Questo comportamento è anche la protezione più importante. Se il telefono si blocca, se il Bluetooth cade o se l'app viene chiusa, i comandi smettono di arrivare e dopo un paio di secondi la ventola torna sotto il controllo dell'auto. Non esiste uno stato in cui l'app lascia la ventola bloccata.
Un comando viene considerato applicato solo se la centralina risponde con una conferma positiva. Se risponde con un rifiuto, o non risponde, l'app non considera la velocità applicata.
Le modalità
| Modalità | Cosa fa | Può raffreddare meno dell'auto? |
|---|---|---|
| Automatica | la ventola la gestisce l'auto; l'app non invia comandi | no |
| Velocità minima | una velocità fissa scelta dall'utente, alzata dalla curva di sicurezza se serve | no |
| Curva protetta | una curva temperatura → velocità scelta dall'utente, con la stessa soglia minima | no |
| Curva libera | la curva dell'utente, senza soglia minima | sì, modalità esperta |
| Manuale | una velocità fissa, senza protezioni | sì, modalità esperta |
Le due modalità esperte si sbloccano solo dopo un avviso e una doppia conferma. Se le si blocca di nuovo mentre una di esse è attiva, l'app torna alla curva protetta.
La curva di sicurezza
Nelle modalità protette la velocità comandata non scende mai sotto quella di una curva di sicurezza, calcolata sulla temperatura più alta tra le sonde del pacco. La curva parte dalle soglie documentate dal costruttore ed è un po' più prudente nella parte alta: velocità 1 da 36 °C, 2 da 39, 3 da 42, 4 da 44, 5 da 46 e 6 da 48 °C.
La regola è una sola: la velocità effettiva è la più alta tra quella chiesta dall'utente e quella della curva di sicurezza. L'utente può quindi solo aggiungere raffreddamento, mai toglierne.
L'isteresi
Se la temperatura oscilla attorno a una soglia, per esempio tra 41,9 e 42,1 °C, una regola ingenua cambierebbe velocità a ogni lettura. Per evitarlo c'è un'isteresi di 2 °C: la velocità sale appena la temperatura supera la soglia, ma scende solo quando la temperatura è di nuovo 2 gradi sotto. Nell'esempio, la velocità 3 si attiva a 42 °C e resta attiva finché il pacco non scende sotto i 40 °C.
È lo stesso principio del termostato di casa, che non accende e spegne la caldaia ogni volta che la temperatura passa di un decimo sopra o sotto quella impostata.
Altre auto, una sola verificata
Visto che il protocollo della CT200h è lo stesso della Prius di terza generazione, ho aggiunto dei profili per altri ibridi Toyota e Lexus. L'app riconosce l'auto dal numero di telaio (VIN). Se non basta, interroga la centralina batteria di ogni profilo con un PID di prova e controlla la risposta: una risposta corretta conferma che si sta parlando davvero con quella centralina, anche se non sempre distingue due modelli della stessa famiglia. In quel caso l'interfaccia mostra un «?» e lascia scegliere a mano.
L'unico profilo provato su un'auto reale è la CT200h. Gli altri sono verificati sulle fonti, non sulle auto. Per questi l'app mostra un avviso e privilegia il monitoraggio; per le famiglie più recenti (piattaforma TNGA, come il SUV Lexus UX250h) le fonti non concordano nemmeno sul comando della ventola, che quindi resta disattivato. Per completare queste verifiche l'app può esportare tutti i frame OBD grezzi: basta un giro con l'auto giusta per confermare o correggere un profilo.
Limiti
- È un'app personale, non uno strumento diagnostico certificato.
- Alcune costanti di scala possono cambiare tra una calibrazione della centralina e l'altra: per questo conservo i byte grezzi.
- La conferma dei profili diversi dalla CT200h richiede prove sulle rispettive auto.
- Il codice non è pubblico.