Skip to content

Commit 9e5125e

Browse files
committed
attempt to detect the key
1 parent c25185e commit 9e5125e

5 files changed

Lines changed: 337 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.mg4.control.hardware
2+
3+
import android.os.Handler
4+
import android.os.HandlerThread
5+
import android.os.SystemClock
6+
import com.mg4.control.debug.AppLogger
7+
import com.mg4.control.model.PropertySweep
8+
import com.mg4.control.util.FirmwareInfo
9+
import java.text.SimpleDateFormat
10+
import java.util.Date
11+
import java.util.Locale
12+
13+
/**
14+
* Sonde clé, en **lecture seule** : la voiture dit-elle à l'écran de bord si la clé sans fil est
15+
* là ? Elle n'écrit rien sur le véhicule. PROVISOIRE — à retirer une fois le relevé fait.
16+
*
17+
* La recherche dans les firmwares (2026-10-11, cinq tables de propriétés) n'a trouvé AUCUN signal
18+
* de présence ni de force de signal — seulement la disponibilité du calculateur PEPS, des codes
19+
* d'équipement et des réglages. Reste à le constater sur la voiture, de deux façons :
20+
*
21+
* 1. **Les signaux nommés** de [SIGNALS], relus à chaque ouverture du Diagnostic.
22+
* 2. **Un balayage à l'aveugle** de toutes les propriétés entières et booléennes du constructeur
23+
* ([PropertySweep.sweepable]), comparé au balayage précédent : c'est lui qui dirait si la clé
24+
* se cache sous un autre nom. Il tourne sur son propre fil, sans retarder le Diagnostic ; son
25+
* résultat apparaît donc à l'ouverture SUIVANTE.
26+
*
27+
* Mode d'emploi : voiture à l'arrêt, sans toucher à rien d'autre —
28+
* a. clé dans la voiture, ouvrir le Diagnostic, attendre 30 s, le fermer ;
29+
* b. éloigner la clé, ouvrir le Diagnostic, attendre 30 s, le fermer ;
30+
* c. rouvrir le Diagnostic : la section « Sonde clé : balayage » liste ce qui a changé entre a et b.
31+
*/
32+
object KeyProbe {
33+
34+
const val TAG = "MG4_KEY"
35+
36+
private const val AREA_GLOBAL = 0x1000000
37+
private const val MAX_RELEVES = 6
38+
/** Au-delà, le balayage n'a pas été fait à l'arrêt : inutile de noyer le rapport. */
39+
private const val MAX_CHANGEMENTS = 40
40+
41+
private data class Signal(val name: String, val id: Int)
42+
43+
private val SIGNALS = listOf(
44+
Signal("SENSOR_PEPSAVLBLY (calculateur PEPS joignable)", 0x21401582),
45+
Signal("SENSOR_BCMAVLBLY (calculateur de carrosserie, pour comparer)", 0x2140157f),
46+
Signal("SENSOR_REMOTELY_START_CAR_STATE", 0x21401562),
47+
Signal("COM_AUTO_IDENTIFICATION", 0x2140d70a),
48+
Signal("AUTO_KEY_IDEN_RECALL_OPT (mémoire siège/rétros par clé)", 0x2140c62d),
49+
Signal("DLOCK_UNLOCK_WITH_KEY", 0x2540c617),
50+
Signal("DLOCK_ENTRY_UNLOCK (déverrouillage à l'approche)", 0x2540c618),
51+
Signal("CAL_REMOTE_KEYLESS_ENTRY_SYSTEM (équipement)", 0x21704230),
52+
Signal("CAL_VEHICLE_START_TYPE (équipement)", 0x2170426d),
53+
Signal("SENSOR_EPTRDY (READY, contexte)", 0x2140157c),
54+
Signal("IGNITION_STATE (AOSP, contexte)", 0x11400409),
55+
)
56+
57+
/** Une propriété déclarée par le véhicule et les zones où elle se lit. */
58+
private class Config(val id: Int, val zones: IntArray)
59+
60+
private val historique = ArrayDeque<String>()
61+
private var dernierBalayage: String? = null
62+
63+
/** Relevés des signaux nommés, puis le résultat du dernier balayage. */
64+
val reports: List<String>
65+
@Synchronized get() = historique.toList() + listOfNotNull(dernierBalayage)
66+
67+
/** Fil dédié : un balayage fait un bon millier de lectures binder. */
68+
private val handler: Handler by lazy {
69+
Handler(HandlerThread("mg4-key-probe").also { it.start() }.looper)
70+
}
71+
72+
// Fil de la sonde uniquement.
73+
private var precedent: Map<String, String>? = null
74+
private var precedentA = 0L
75+
private var numero = 0
76+
77+
/** Relève les signaux nommés, lance un balayage en arrière-plan et renvoie le texte du relevé. */
78+
fun run(origin: String): String {
79+
val horloge = SimpleDateFormat("HH:mm:ss", Locale.ROOT).format(Date())
80+
val cpm = MG4Hardware.carPropertyManager()
81+
val configs = configs(cpm)
82+
val lignes = mutableListOf("══ Sonde clé [$origin] · ${FirmwareInfo.getGeneration()} · $horloge ══")
83+
SIGNALS.forEach { s ->
84+
val config = configs?.firstOrNull { it.id == s.id }
85+
val zones = config?.zones ?: intArrayOf(AREA_GLOBAL, 0)
86+
val declaree = if (configs == null) "?" else if (config != null) "oui" else "non"
87+
lignes += " ${s.name} 0x${Integer.toHexString(s.id)} : déclarée=$declaree · " +
88+
zones.joinToString(" · ") { z -> "zone 0x${Integer.toHexString(z)}=" + lire(cpm, s.id, z) }
89+
}
90+
lignes.forEach { AppLogger.i(TAG, it) }
91+
garde(lignes.joinToString("\n"))
92+
handler.post { balayer(origin) }
93+
return lignes.joinToString("\n")
94+
}
95+
96+
// ── Balayage ─────────────────────────────────────────────────────────────
97+
98+
private fun balayer(origin: String) {
99+
val cpm = MG4Hardware.carPropertyManager()
100+
val configs = configs(cpm)?.filter { PropertySweep.sweepable(it.id) }
101+
if (cpm == null || configs == null) {
102+
publier("── Sonde clé : balayage [$origin] impossible — liste des propriétés illisible ──")
103+
return
104+
}
105+
val debut = SystemClock.elapsedRealtime()
106+
val releve = HashMap<String, String>()
107+
var echecs = 0
108+
configs.forEach { c ->
109+
c.zones.forEach { zone ->
110+
// Zone 0 déclarée pour une propriété globale : c'est à la zone globale du HAL
111+
// qu'elle répond sur ces voitures, d'où l'essai des deux.
112+
val valeur = if (zone == 0) lireOuNull(cpm, c.id, AREA_GLOBAL) ?: lireOuNull(cpm, c.id, 0)
113+
else lireOuNull(cpm, c.id, zone)
114+
if (valeur == null) echecs++ else releve[PropertySweep.key(c.id, zone)] = valeur
115+
}
116+
}
117+
val duree = (SystemClock.elapsedRealtime() - debut) / 1000.0
118+
numero++
119+
val lignes = mutableListOf(
120+
"── Sonde clé : balayage n° $numero [$origin] — ${releve.size} valeurs lues en " +
121+
"%.1f s, $echecs illisibles ──".format(Locale.US, duree))
122+
val avant = precedent
123+
if (avant == null) {
124+
lignes += " premier balayage : rien à comparer. Éloigner la clé, puis rouvrir le Diagnostic."
125+
} else {
126+
val changements = PropertySweep.changes(avant, releve)
127+
val ecart = (SystemClock.elapsedRealtime() - precedentA) / 1000
128+
lignes += " depuis le balayage précédent (il y a $ecart s) : ${changements.size} changement(s)"
129+
changements.take(MAX_CHANGEMENTS).forEach { lignes += " $it" }
130+
if (changements.size > MAX_CHANGEMENTS) lignes += " … et ${changements.size - MAX_CHANGEMENTS} autres"
131+
}
132+
precedent = releve
133+
precedentA = SystemClock.elapsedRealtime()
134+
lignes.forEach { AppLogger.i(TAG, it) }
135+
publier(lignes.joinToString("\n"))
136+
}
137+
138+
@Synchronized
139+
private fun publier(texte: String) {
140+
dernierBalayage = texte
141+
}
142+
143+
@Synchronized
144+
private fun garde(texte: String) {
145+
historique.addLast(texte)
146+
while (historique.size > MAX_RELEVES) historique.removeFirst()
147+
}
148+
149+
// ── Lectures ─────────────────────────────────────────────────────────────
150+
151+
/** Propriétés déclarées par le véhicule avec leurs zones, ou null si la liste est illisible. */
152+
private fun configs(cpm: Any?): List<Config>? = runCatching {
153+
(cpm!!.javaClass.getMethod("getPropertyList").invoke(cpm) as? List<*>)
154+
?.filterNotNull()
155+
?.mapNotNull { c ->
156+
val id = runCatching { c.javaClass.getMethod("getPropertyId").invoke(c) as? Int }.getOrNull()
157+
?: return@mapNotNull null
158+
val zones = runCatching { c.javaClass.getMethod("getAreaIds").invoke(c) as? IntArray }.getOrNull()
159+
Config(id, zones?.takeIf { it.isNotEmpty() } ?: intArrayOf(0))
160+
}
161+
}.getOrNull()
162+
163+
/** Valeur lisible, ou le nom de l'exception : distinguer « absente » de « refusée » est une réponse. */
164+
private fun lire(cpm: Any?, id: Int, zone: Int): String {
165+
cpm ?: return "cpm absent"
166+
return try {
167+
valeur(cpm, id, zone)
168+
} catch (e: Exception) {
169+
val cause = e.cause ?: e
170+
cause.javaClass.simpleName + (cause.message?.let { " ($it)" } ?: "")
171+
}
172+
}
173+
174+
private fun lireOuNull(cpm: Any, id: Int, zone: Int): String? =
175+
runCatching { valeur(cpm, id, zone) }.getOrNull()
176+
177+
private fun valeur(cpm: Any, id: Int, zone: Int): String = when (id and 0x00ff0000) {
178+
0x00200000 -> typee(cpm, "getBooleanProperty", id, zone)
179+
0x00600000 -> typee(cpm, "getFloatProperty", id, zone)
180+
0x00700000 -> {
181+
// Bloc d'octets (codes d'équipement) : pas d'accesseur typé, on passe par getProperty.
182+
val v = cpm.javaClass
183+
.getMethod("getProperty", Class::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
184+
.invoke(cpm, ByteArray::class.java, id, zone)
185+
val octets = v?.javaClass?.getMethod("getValue")?.invoke(v) as? ByteArray
186+
octets?.let { PropertySweep.hex(it) } ?: "null"
187+
}
188+
else -> typee(cpm, "getIntProperty", id, zone)
189+
}
190+
191+
private fun typee(cpm: Any, accesseur: String, id: Int, zone: Int): String =
192+
cpm.javaClass.getMethod(accesseur, Int::class.java, Int::class.java).invoke(cpm, id, zone)?.toString() ?: "null"
193+
}

‎app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3767,6 +3767,12 @@ object MG4Hardware {
37673767
sb.appendLine()
37683768
}
37693769

3770+
// Sonde clé (provisoire) : signaux nommés des dernières ouvertures, puis le dernier balayage.
3771+
KeyProbe.reports.forEach {
3772+
sb.appendLine(it)
3773+
sb.appendLine()
3774+
}
3775+
37703776
if (gen == FirmwareInfo.Gen.SWI132) {
37713777

37723778
// ── Binder IVehicleSettingService ─────────────────────────────
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.mg4.control.model
2+
3+
/**
4+
* Balayage des propriétés du véhicule pour la sonde clé ([com.mg4.control.hardware.KeyProbe]) —
5+
* la partie sans véhicule : quoi relire, et quoi signaler entre deux relevés.
6+
*/
7+
object PropertySweep {
8+
9+
private const val GROUPE = 0xf0000000.toInt()
10+
private const val GROUPE_CONSTRUCTEUR = 0x20000000
11+
private const val TYPE = 0x00ff0000
12+
private const val TYPE_ENTIER = 0x00400000
13+
private const val TYPE_BOOLEEN = 0x00200000
14+
15+
/**
16+
* Vrai pour une propriété que le balayage relit : entier ou booléen du groupe constructeur.
17+
*
18+
* Le groupe « système » est laissé de côté — on y trouve les commandes d'usine et
19+
* d'alimentation, que personne ne relit en usage normal. Les flottants bougent tout seuls
20+
* (vitesse, énergie) et les blocs d'octets sont des codes d'équipement : ni les uns ni les
21+
* autres ne diraient si une clé est là.
22+
*/
23+
fun sweepable(id: Int): Boolean =
24+
(id and GROUPE) == GROUPE_CONSTRUCTEUR &&
25+
((id and TYPE) == TYPE_ENTIER || (id and TYPE) == TYPE_BOOLEEN)
26+
27+
/** Clé d'une valeur dans un relevé : identifiant et zone, en hexadécimal. */
28+
fun key(id: Int, area: Int): String = "0x" + Integer.toHexString(id) + "@0x" + Integer.toHexString(area)
29+
30+
/**
31+
* Valeurs qui diffèrent entre deux relevés, triées par clé. Une propriété lue une seule des
32+
* deux fois est ignorée : une lecture qui échoue par intermittence n'apprend rien.
33+
*/
34+
fun changes(before: Map<String, String>, after: Map<String, String>): List<String> =
35+
before.keys.sorted().mapNotNull { cle ->
36+
val avant = before[cle]
37+
val apres = after[cle] ?: return@mapNotNull null
38+
if (avant == apres) null else "$cle : $avant \u2192 $apres"
39+
}
40+
41+
/** Début d'un bloc d'octets en hexadécimal — assez pour comparer deux relevés. */
42+
fun hex(bytes: ByteArray, max: Int = 16): String {
43+
if (bytes.isEmpty()) return "(vide)"
44+
val debut = bytes.take(max).joinToString(" ") { "%02x".format(it.toInt() and 0xff) }
45+
return if (bytes.size > max) "$debut\u2026 (${bytes.size} octets)" else debut
46+
}
47+
}

‎app/src/main/java/com/mg4/control/ui/SettingsFragment.kt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,9 @@ class SettingsFragment : Fragment() {
706706
// Sonde lumière/position : un relevé instantané, à prendre en plein jour, au crépuscule
707707
// et dans un parking couvert. Ceux pris automatiquement au READY sont déjà gardés.
708708
com.mg4.control.hardware.LightProbe.run("diagnostic")
709+
// Sonde clé (provisoire) : signaux nommés tout de suite, balayage complet en
710+
// arrière-plan — son résultat est dans le rapport à l'ouverture suivante.
711+
com.mg4.control.hardware.KeyProbe.run("diagnostic")
709712
val report = MG4Hardware.buildDiagnosticReport(appVersion)
710713
withContext(Dispatchers.Main) {
711714
if (isAdded) tvReport.text = report
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.mg4.control.model
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Assert.assertFalse
5+
import org.junit.Assert.assertTrue
6+
import org.junit.Test
7+
8+
/**
9+
* Sonde clé — comparer deux relevés de TOUTES les propriétés du véhicule, clé dans la voiture
10+
* puis clé éloignée, pour voir si quoi que ce soit bouge.
11+
*
12+
* Aucune propriété ne porte le nom d'un signal de clé (recherche du 2026-10-11 sur cinq
13+
* firmwares) : seul un balayage à l'aveugle peut dire s'il en existe une sous un autre nom.
14+
*/
15+
class PropertySweepTest {
16+
17+
// ── Quelles propriétés relire ────────────────────────────────────────────
18+
19+
@Test
20+
fun `les entiers et les booleens du constructeur sont balayes`() {
21+
assertTrue(PropertySweep.sweepable(0x21401582)) // SENSOR_PEPSAVLBLY, entier
22+
assertTrue(PropertySweep.sweepable(0x2540c617)) // DLOCK_UNLOCK_WITH_KEY, entier par siège
23+
assertTrue(PropertySweep.sweepable(0x21209320)) // feux de croisement, booléen
24+
}
25+
26+
@Test
27+
fun `les proprietes du systeme ne sont pas balayees`() {
28+
// Groupe « système » : on y trouve les commandes d'usine (EOL) et d'alimentation, que
29+
// personne ne relit en usage normal. On s'en tient à ce que lisent les écrans d'origine.
30+
assertFalse(PropertySweep.sweepable(0x11408a07)) // PMS_SYSTEM_RESTART
31+
assertFalse(PropertySweep.sweepable(0x11400409)) // IGNITION_STATE
32+
}
33+
34+
@Test
35+
fun `les flottants et les blocs d octets ne sont pas balayes`() {
36+
// Un flottant (vitesse, énergie) bouge tout seul ; un bloc d'octets est un code d'équipement.
37+
assertFalse(PropertySweep.sweepable(0x2160f404)) // SoC, flottant
38+
assertFalse(PropertySweep.sweepable(0x21704230)) // CAL_REMOTE_KEYLESS_ENTRY_SYSTEM, octets
39+
}
40+
41+
// ── Ce qui a changé entre deux relevés ───────────────────────────────────
42+
43+
@Test
44+
fun `une valeur qui change est signalee avec ses deux valeurs`() {
45+
val avant = mapOf("0x21401582@0x1000000" to "1", "0x2140157f@0x1000000" to "1")
46+
val apres = mapOf("0x21401582@0x1000000" to "0", "0x2140157f@0x1000000" to "1")
47+
assertEquals(listOf("0x21401582@0x1000000 : 1 \u2192 0"), PropertySweep.changes(avant, apres))
48+
}
49+
50+
@Test
51+
fun `rien n est signale quand rien ne bouge`() {
52+
val releve = mapOf("0x21401582@0x1000000" to "1")
53+
assertEquals(emptyList<String>(), PropertySweep.changes(releve, releve))
54+
}
55+
56+
@Test
57+
fun `une propriete lue une seule des deux fois n est pas un changement`() {
58+
// Une lecture qui échoue une fois sur deux n'apprend rien sur la clé.
59+
val avant = mapOf("a" to "1")
60+
val apres = mapOf("b" to "1")
61+
assertEquals(emptyList<String>(), PropertySweep.changes(avant, apres))
62+
}
63+
64+
@Test
65+
fun `les changements sortent dans un ordre stable`() {
66+
val avant = mapOf("b" to "0", "a" to "0", "c" to "0")
67+
val apres = mapOf("c" to "1", "a" to "1", "b" to "1")
68+
assertEquals(
69+
listOf("a : 0 \u2192 1", "b : 0 \u2192 1", "c : 0 \u2192 1"),
70+
PropertySweep.changes(avant, apres))
71+
}
72+
73+
// ── Affichage ────────────────────────────────────────────────────────────
74+
75+
@Test
76+
fun `la cle d un releve porte l identifiant et la zone en hexadecimal`() {
77+
assertEquals("0x21401582@0x1000000", PropertySweep.key(0x21401582, 0x1000000))
78+
assertEquals("0x2540c617@0x1", PropertySweep.key(0x2540c617, 1))
79+
}
80+
81+
@Test
82+
fun `un bloc d octets s affiche en hexadecimal et reste court`() {
83+
assertEquals("01 ff 00", PropertySweep.hex(byteArrayOf(1, -1, 0)))
84+
assertEquals("(vide)", PropertySweep.hex(byteArrayOf()))
85+
// Un code d'équipement peut faire des dizaines d'octets : le début suffit à le comparer.
86+
assertEquals("00 01 02 03\u2026 (6 octets)", PropertySweep.hex(byteArrayOf(0, 1, 2, 3, 4, 5), max = 4))
87+
}
88+
}

0 commit comments

Comments
 (0)