|
| 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 | +} |
0 commit comments