From 0ff56285bc61892ff9aedef1ab536e4c881c1f4e Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Sun, 25 Oct 2026 21:53:15 +0100 Subject: [PATCH] EVHardware: translate the French comments, log texts and test names The vendored library's French and half-translated comments, its French log messages and the French test names and assertion messages are now English. Comments, log texts and test names only; no code changed. The French resources, place names in test data and two quoted phrases stay; "Katman" (Turkish for layer) stays as the layers' name and is explained. SPECS: the Grafana host is confirmed; both items ticked. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- .../java/com/evsuite/hardware/EVHardware.kt | 368 +++++++++--------- .../evsuite/hardware/FirmwareSupportTest.kt | 18 +- .../com/evsuite/hardware/GlassEvidenceTest.kt | 30 +- .../com/evsuite/hardware/GlassProbeTest.kt | 44 +-- .../PhysicalButtonEventDecoderTest.kt | 2 +- .../catalog/CatalogConsistencyTest.kt | 95 +++-- SPECS.md | 16 +- 7 files changed, 290 insertions(+), 283 deletions(-) diff --git a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt index 0b9e3a8..38d9fea 100644 --- a/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt +++ b/EVHardware/lib/src/main/java/com/evsuite/hardware/EVHardware.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.launch /** * Hardware abstraction layer for MG4 vehicle control. * - * Three runtime communication layers supported by this project: + * Runtime communication layers ("Katman", Turkish for layer, as the names in this file call them): * Katman1 — android.car.Car → CarPropertyManager / CarHvacManager (async ServiceConnection) * Katman2 — ServiceManager.getService("vehiclesetting") → raw IBinder (often SELinux-blocked) * Katman3 — bindService(VehicleService) → IHubService → sub-services (not needed for our use case) @@ -61,13 +61,13 @@ object EVHardware { private const val PROP_SPEED_LIMIT_TONE = 0x503004f private const val PROP_MIX_INTELLIGENT_DRIVE = 0x32 - // ELK — Assistant de sortie de voie (SWI133) + // ELK — emergency lane keeping (SWI133) // Access via the runtime IVehicleSettingService binder (sVehicleBinder). // getLaneKeepingAsstMode() → TX 0x53 (synchrone, reply: readException + readInt) // setLaneKeepingAsstMode(I) → TX 0x54 (ONEWAY, data: writeInt(value)) // getLaneKeepingAsstSen() → TX 0x55 (synchrone) // setLaneKeepingAsstSen(I) → TX 0x56 (ONEWAY) - // Mode : 1=OFF, 2=Alert(LDW), 3=Aider(LDP), 5=Maintien d'urgence(ELK) + // Mode : 1=OFF, 2=Alert(LDW), 3=Assist(LDP), 5=Emergency keep(ELK) // Sen : 1=Low, 2=Standard, 3=High private const val TX_ELK_GET_MODE = 0x53 private const val TX_ELK_SET_MODE = 0x54 @@ -84,15 +84,15 @@ object EVHardware { private const val VSM132_TX_GET_OVERSPEED = 0x129 private const val VSM132_TX_GET_SPEED_LIMIT = 0x12b - /** Values du mode ELK (LaneKeepingAsstMode). */ + /** ELK mode values (LaneKeepingAsstMode). */ object ElkMode { const val OFF = 1 // Disabled const val ALERT = 2 // Alert (LDW) - const val ASSIST = 3 // Aider (LDP) - const val EMERGENCY = 5 // Maintien d'urgence (ELK) + const val ASSIST = 3 // Assist (LDP) + const val EMERGENCY = 5 // Emergency lane keeping (ELK) } - /** Values de sensitivity ELK. */ + /** ELK sensitivity values. */ object ElkSensitivity { const val LOW = 1 // Low const val STANDARD = 2 // Standard @@ -102,15 +102,15 @@ object EVHardware { // AEB — Front collision avoidance system (SWI133) // PROP_AEB_SWITCH : CarPropertyManager, AREA_GLOBAL, 1=OFF / 2=ON private const val PROP_AEB_SWITCH = 0x2140a108 // AAD_FRONT_COLLISION_ASST_SYS (CPM) - // PROP_AEB_SYS_MODE : VPM, ID_AAD_FRONT_COLLISION_ASST_SYS, 1=Alert / 2=Alert+Freinage - // PROP_AEB_MODE : VPM, ID_AAD_AUTO_EME_BREAK, 1=Alert / 2=Alert+Freinage + // PROP_AEB_SYS_MODE : VPM, ID_AAD_FRONT_COLLISION_ASST_SYS, 1=Alert / 2=Alert+Braking + // PROP_AEB_MODE : VPM, ID_AAD_AUTO_EME_BREAK, 1=Alert / 2=Alert+Braking // Both properties are written together to preserve the observed vehicle behaviour. private const val PROP_AEB_SYS_MODE = 0x302000a // ID_AAD_FRONT_COLLISION_ASST_SYS (VPM) private const val PROP_AEB_MODE = 0x302000b // ID_AAD_AUTO_EME_BREAK (VPM) // PROP_AEB_SENSITIVITY : VPM, ForwardCollisionAsstSentItem, 1=Low / 2=Standard / 3=High private const val PROP_AEB_SENSITIVITY = 0x302000e // ID_AAD_FRONT_COLLISION_ASST_SEN (VPM) - // TSR — Reconnaissance des panneaux de speed (SLIF Warning) + // TSR — traffic sign recognition (SLIF Warning) // SWI133 : VPM toggle 0/1 ; SWI68/SWI165 : VSM setSpeedAsstSlifWarning ; SWI69/SWI131 : VSM setSLIFWarningState (inverted) private const val PROP_TSR_MODE = 0x5030049 // ID_AAD_SLIF_WARNING @@ -122,7 +122,7 @@ object EVHardware { private const val VSM_CLASS = "com.saicmotor.sdk.vehiclesettings.manager.VehicleSettingManager" private const val LAUNCHER68_PKG = "com.saicmotor.hmi.launcher" - // Brightness screen — ancien SDK (SWI133/68/165) : GeneralManager.setBrightness(Int)/getBrightness(). + // Screen brightness — old SDK (SWI133/68/165): GeneralManager.setBrightness(Int)/getBrightness(). // Runtime range 0..255 on the supported generations. // A9 (SWI132/131/69) = phase 2 (setScreenBrightness(III), params not decodable without SystemUI). private const val GENERAL_MANAGER_CLASS = "com.saicmotor.sdk.systemsettings.GeneralManager" @@ -131,20 +131,20 @@ object EVHardware { private const val VCONTROL_CLASS = "com.saicmotor.sdk.vehiclesettings.manager.VehicleControlManager" private const val BRIGHTNESS_NATIVE_MAX = 255 - // Media volume — ancien SDK (SWI133/68/165) : SmartSoundManager.getVolume/setVolume/getMaxVolume(type). + // Media volume — old SDK (SWI133/68/165): SmartSoundManager.getVolume/setVolume/getMaxVolume(type). // Same systemsettings/BaseManager SDK as GeneralManager (singleton sInstance + init(Context, listener)). private const val SMART_SOUND_MANAGER_CLASS = "com.saicmotor.sdk.systemsettings.SmartSoundManager" - private const val BRIGHTNESS_MIN_PERCENT = 5 // floor de safety : ne jamais turn off l'screen + private const val BRIGHTNESS_MIN_PERCENT = 5 // safety floor: never turn the screen off // SWI69/SWI131 : access via CarAdapterClient → queryClient(0x8) → CarVehicleSettingClient // Actual architecture : CarAdapterClient connects to com.saicmotor.caradapter.CarAdapterService, - // puis queryClient(code) returns l'IBinder pour chaque service. + // then queryClient(code) returns the IBinder for each service. // Runtime service code 0x8 = CarVehicleSettingClient. private const val LAUNCHER69_PKG = "com.saicmotor.launcher" private const val CAR_ADAPTER_CLASS = "com.saicmotor.carapi.CarAdapterClient" private const val VSM69_CLIENT_CLASS = "com.saicmotor.carapi.client.CarVehicleSettingClient" private const val VSM_SERVICE_CODE = 0x8 // queryClient(0x8) → ICarVehicleSettingService - private const val VEHICLE_SETTING_PKG = "com.saicmotor.vehiclesetting" // SWI131 : carapi dans VS + private const val VEHICLE_SETTING_PKG = "com.saicmotor.vehiclesetting" // SWI131: carapi inside VS // Katman5 — VehicleConditionManager (IVehicleConditionService via IHubService "vehiclecondition") private const val VCM_CLASS = "com.saicmotor.sdk.vehiclesettings.manager.VehicleConditionManager" @@ -154,19 +154,19 @@ object EVHardware { private const val CAR_GENERAL_CLIENT_CLASS = "com.saicmotor.carapi.client.CarGeneralClient" private const val BIND_CODE_CAR_GENERAL = 0x1 // ICarAdapterService.queryClient(0x1) - // Standard AAOS — VehicleProperty.IGNITION_STATE (compatible tous firmwares via CarPropertyManager) + // Standard AAOS — VehicleProperty.IGNITION_STATE (all firmwares, through CarPropertyManager) private const val PROP_IGNITION_STATE = 0x11400409 - // Standard AAOS — VehicleProperty.PERF_VEHICLE_SPEED (float, m/s). Base du lock - // of writes at 0 km/h (see VehicleWriteGate). + // Standard AAOS — VehicleProperty.PERF_VEHICLE_SPEED (float, m/s). The basis of the lock + // on writes at 0 km/h (see VehicleWriteGate). private const val PROP_VEHICLE_SPEED = 0x11600207 // Standard AAOS — VehicleProperty.ENV_OUTSIDE_TEMPERATURE (float, °C). - // NON VERIFIED sur vehicle : tous les firmwares MG4 n'exposent pas cette property. - // Les callers doivent traiter null comme « data unavailable », pas comme 0 °C. + // NOT VERIFIED on a vehicle: not every MG4 firmware exposes this property. + // Callers must treat null as "data unavailable", not as 0 °C. private const val PROP_OUTSIDE_TEMP = 0x11600703 - /** Values de IGNITION_STATE (VehicleIgnitionState) — property AAOS standard. */ + /** IGNITION_STATE values (VehicleIgnitionState) — a standard AAOS property. */ object IgnitionState { const val UNDEFINED = 0 const val LOCK = 1 @@ -199,21 +199,21 @@ object EVHardware { const val INTELLIGENT = 3 } - /** Values de mode ADAS pour firmware SWI68/SWI132 (CarAccTja constants). */ + /** ADAS mode values for SWI68/SWI132 firmware (CarAccTja constants). */ object Swi68Mode { const val OFF = 0x4 // Disable const val SHWA = 0x3 // Speed Limit Mode (Limiter) — SWI132 only const val ACC = 0x1 // ACC - const val TJA = 0x2 // TJA (Traffic Jam Assist) = ICA dans l'UI SWI132 + const val TJA = 0x2 // TJA (Traffic Jam Assist) = ICA in the SWI132 UI } - /** Values du mode AEB (communes SWI133 + SWI68). */ + /** AEB mode values (shared by SWI133 and SWI68). */ object AebMode { const val ALARM = 1 // Alert seule (FCW) - const val ALARM_BRAKE = 2 // Alert + Freinage automatique d'urgence + const val ALARM_BRAKE = 2 // Alert + automatic emergency braking } - /** Values de sensitivity AEB — SWI133 only (PROP_AEB_SENSITIVITY = 0x302000e). */ + /** AEB sensitivity values — SWI133 only (PROP_AEB_SENSITIVITY = 0x302000e). */ object AebSensitivity { const val LOW = 1 // Low const val STANDARD = 2 // Standard @@ -240,7 +240,7 @@ object EVHardware { @Volatile private var sCarAdapter: Any? = null @Volatile private var sCarAdapterClass: Class<*>? = null @Volatile private var sVsmService: Any? = null // mVehicleSettingService field value (SWI68) - @Volatile private var sVsm133: Any? = null // VehicleSettingManager instance (SWI133, pour ELK) + @Volatile private var sVsm133: Any? = null // VehicleSettingManager instance (SWI133, for ELK) @Volatile private var sGeneral: Any? = null // GeneralManager instance (SWI133/68/165, brightness) @Volatile private var sVcontrol: Any? = null // VehicleControlManager instance (SWI68/165, ESC) @Volatile private var sSmartSound: Any? = null // SmartSoundManager instance (SWI133/68/165, loudness) @@ -273,7 +273,7 @@ object EVHardware { private val katman4ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>() /** - * Runs [action] as soon as CarPropertyManager et CarHvacManager are available. + * Runs [action] as soon as CarPropertyManager and CarHvacManager are available. * If already ready, runs immediately. Otherwise queued and triggered on connection. */ fun whenKatman1Ready(action: () -> Unit) { @@ -307,7 +307,7 @@ object EVHardware { vehicleConditionCallbacks.remove(callback) } - /** Enregistre un callback sur IGNITION_STATE via CarPropertyManager (standard AAOS). */ + /** Registers a callback on IGNITION_STATE through CarPropertyManager (standard AAOS). */ fun registerIgnitionCallback(callback: (Int) -> Unit) { ignitionCallbacks.add(callback) if (sCarPropertyManager != null) registerIgnitionPropertyCallback() @@ -339,7 +339,7 @@ object EVHardware { * * CarPropertyManager first. But SWI68 does not expose IGNITION_STATE through CPM: the * property comes back empty, and the ignition state looked unreadable although Katman5 - * reports it correctly — the log shows it (`Katman5: state initial = 2 (RUN/READY)`) while + * reports it correctly — the log shows it (`Katman5: initial state = 2 (RUN/READY)`) while * the diagnostics classed IGNITION_STATE as NOT_READABLE. * * Hence the fallback to the last state delivered by IVehicleConditionService, converted: @@ -367,7 +367,7 @@ object EVHardware { /** * Vehicle speed in km/h, or null if it cannot be read (CPM not ready, - * property non supportsde, exception). [VehicleWriteGate] traite null comme un refus. + * property not supported, exception). [VehicleWriteGate] treats null as a refusal. * * The property's unit varies by generation — see [VehicleSpeedScale]. */ @@ -884,7 +884,7 @@ object EVHardware { private fun readWindowArea(area: Int): Int? = getIntPropertyCPM(PROP_WINDOW_POS, area).takeIf { it >= 0 } - /** Contexte applicatif, pour les messages utilisateur du lock d'write. */ + /** The application context, for the write lock's messages to the user. */ internal fun appContext(): Context? = sAppContext interface DriveModeListener { fun onDriveModeChanged(mode: DriveMode) } @@ -910,12 +910,12 @@ object EVHardware { sVehicleBinder = getBinderService("vehiclesetting") when { FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 -> initKatman4Swi68(context) - FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI165 -> initKatman4Swi68(context) // same SDK que SWI68 - FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> initKatman4Swi69(context) // CarVehicleSettingClient, same path que SWI69 + FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI165 -> initKatman4Swi68(context) // same SDK as SWI68 + FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> initKatman4Swi69(context) // CarVehicleSettingClient, same path as SWI69 FirmwareInfo.isNewGenVsm() -> initKatman4Swi69(context) // SWI69 + SWI131 else -> initKatman4(context) } - // Katman5 — detection IGNITION_STATE push (VehicleConditionManager ou ICarGeneralService) + // Katman5 — IGNITION_STATE push detection (VehicleConditionManager or ICarGeneralService) // SWI132 uses the same observed ICarGeneralService path as SWI69/SWI131. if (FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132) initKatman5Swi69(context) @@ -926,7 +926,7 @@ object EVHardware { else AppLogger.w(TAG, " ✗ Katman2: vehiclesetting null (SELinux — expected)") - // Cranking auto du watcher door au boot si la feature est enabled (tous firmwares). + // Start the door watcher at boot if the feature is enabled (all firmwares). startDoorWatcherIfEnabled() // Car connection established even if the feature is OFF → the Diagnostic probe can read the doors. if (hasDoorVolumeFeature()) connectCarProperty() @@ -1111,7 +1111,7 @@ object EVHardware { Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } } - // Tentative d'registersment du callback IGNITION_STATE (best-effort) + // Try to register the IGNITION_STATE callback (best effort) if (sCarPropertyManager != null) registerIgnitionPropertyCallback() } catch (e: Exception) { AppLogger.e(TAG, " Katman1: tryGetManagersFromCar error: ${e.message}") @@ -1191,20 +1191,20 @@ object EVHardware { sVpm = vpm - // 1) bindService() — connecte au service vehicle (async) + // 1) bindService() — connects to the vehicle service (async) tryInvoke("vpm.bindService()") { vpm!!.javaClass.getMethod("bindService").invoke(vpm) } // 2) init(Context, IVehicleServiceListener) via dynamic proxy — receives onServiceConnected initWithServiceListener(vpm!!, context, launcherCtx) - // 3) VehicleSettingManager pour SWI133 (ELK) — same singleton que SWI68 + // 3) VehicleSettingManager for SWI133 (ELK) — the same singleton as SWI68 tryInitVsm133(launcherCtx, context) - // 3b) GeneralManager pour SWI133 (brightness screen) + // 3b) GeneralManager for SWI133 (screen brightness) tryInitGeneralManager(launcherCtx, context) tryInitVehicleControlManager(launcherCtx, context) - // 3c) SmartSoundManager pour SWI133 (loudness audio) + // 3c) SmartSoundManager for SWI133 (audio loudness) tryInitSmartSoundManager(launcherCtx, context) // 4) Retries to obtain mIVehiclePropertyService and VSM133 once the service is connected @@ -1300,7 +1300,7 @@ object EVHardware { AppLogger.d(TAG, " Katman4: init() failed: ${e.message}") } - AppLogger.w(TAG, " Katman4: aucun init() fonctionnel — mIVehiclePropertyService restera null") + AppLogger.w(TAG, " Katman4: no working init() — mIVehiclePropertyService stays null") } /** Runs [block], returns the result or null, logs the result/error. */ @@ -1344,7 +1344,7 @@ object EVHardware { try { val vsmClass = launcherCtx.classLoader.loadClass(VSM_CLASS) - // Tentative 1 : lire le singleton already initialisesd par le launcher + // Attempt 1: read the singleton the launcher has already initialised val f = vsmClass.getDeclaredField("sVehicleSettingManager") f.isAccessible = true val singleton = f.get(null) @@ -1354,7 +1354,7 @@ object EVHardware { return } - // Tentative 2 : appeler init() nous-same (comme SWI68) + // Attempt 2: call init() ourselves (as on SWI68) val initMethod = vsmClass.methods.firstOrNull { m -> m.name == "init" && m.parameterCount == 2 && Context::class.java.isAssignableFrom(m.parameterTypes[0]) @@ -1385,7 +1385,7 @@ object EVHardware { } // ------------------------------------------------------------------------- - // Brightness screen — GeneralManager (ancien SDK SWI133/68/165) + // Screen brightness — GeneralManager (old SDK SWI133/68/165) // GeneralManager.init(Context, ISettingsServiceListener) — singleton sInstance. // Same pattern as tryInitVsm133; loaded from the launcher com.saicmotor.hmi.launcher. // ------------------------------------------------------------------------- @@ -1469,7 +1469,7 @@ object EVHardware { try { val cls = launcherCtx.classLoader.loadClass(GENERAL_MANAGER_CLASS) - // Tentative 1 : singleton already initialisesd + // Attempt 1: the singleton already initialised val f = cls.getDeclaredField("sInstance") f.isAccessible = true f.get(null)?.let { @@ -1478,7 +1478,7 @@ object EVHardware { return } - // Tentative 2 : init(Context, ISettingsServiceListener) — proxy dynamique + // Attempt 2: init(Context, ISettingsServiceListener) — a dynamic proxy val initMethod = cls.methods.firstOrNull { m -> m.name == "init" && m.parameterCount == 2 && Context::class.java.isAssignableFrom(m.parameterTypes[0]) @@ -1508,7 +1508,7 @@ object EVHardware { } // ------------------------------------------------------------------------- - // Loudness — SmartSoundManager (ancien SDK SWI133/68/165) + // Loudness — SmartSoundManager (old SDK SWI133/68/165) // Same SDK/pattern as GeneralManager : singleton sInstance + init(Context, ISettingsServiceListener). // ------------------------------------------------------------------------- @@ -1517,7 +1517,7 @@ object EVHardware { try { val cls = launcherCtx.classLoader.loadClass(SMART_SOUND_MANAGER_CLASS) - // Tentative 1 : singleton already initialisesd + // Attempt 1: the singleton already initialised val f = cls.getDeclaredField("sInstance") f.isAccessible = true f.get(null)?.let { @@ -1526,7 +1526,7 @@ object EVHardware { return } - // Tentative 2 : init(Context, ISettingsServiceListener) — proxy dynamique + // Attempt 2: init(Context, ISettingsServiceListener) — a dynamic proxy val initMethod = cls.methods.firstOrNull { m -> m.name == "init" && m.parameterCount == 2 && Context::class.java.isAssignableFrom(m.parameterTypes[0]) @@ -1558,23 +1558,23 @@ object EVHardware { private fun isA9Brightness(): Boolean = FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 - /** Brightness screen disponible : ancien SDK (133/68/165) ou A9 (132/131/69). */ + /** Screen brightness available: old SDK (133/68/165) or A9 (132/131/69). */ fun hasBrightnessControl(): Boolean = FirmwareInfo.getGeneration() != FirmwareInfo.Gen.UNKNOWN - /** Lit la brightness screen en % (0–100), ou -1 si unavailable. */ + /** Reads the screen brightness in % (0–100), or -1 if unavailable. */ fun getScreenBrightnessPercent(): Int = if (isA9Brightness()) getBrightnessA9() else getBrightnessOldSdk() /** * Sets screen brightness in % (0–100). Safety floor at BRIGHTNESS_MIN_PERCENT - * pour ne jamais turn off l'screen. + * so the screen is never turned off. */ fun setScreenBrightnessPercent(pct: Int): Boolean { val clamped = pct.coerceIn(BRIGHTNESS_MIN_PERCENT, 100) return if (isA9Brightness()) setBrightnessA9(clamped) else setBrightnessOldSdk(clamped) } - // ── Ancien SDK (SWI133/68/165) — GeneralManager.setBrightness(Int), plage native 0..255 ── + // ── Old SDK (SWI133/68/165) — GeneralManager.setBrightness(Int), native range 0..255 ── private fun getBrightnessOldSdk(): Int { val g = sGeneral ?: return -1 @@ -1604,10 +1604,10 @@ object EVHardware { } // ── A9 (SWI132/131/69) — brightness via Settings.System.SCREEN_BRIGHTNESS ── - // L'app Settings A9 (GeneralModel.setBrightness) pilote realment la dalle par - // Settings.System.putInt("screen_brightness", 0..255) + passage en mode manuel. - // CarGeneralClient.setScreenBrightness(mode,day,night) ne stocke que le jour/nuit - // et n'a AUCUN effet sur la dalle (confirmed par les logs SWI132 : value lue=0, + // The A9 Settings app (GeneralModel.setBrightness) really drives the panel through + // Settings.System.putInt("screen_brightness", 0..255) plus a switch to manual mode. + // CarGeneralClient.setScreenBrightness(mode,day,night) only stores day/night + // and has NO effect on the panel (confirmed by the SWI132 logs: value read = 0, // no visual change). Being uid.system, the app can write Settings.System. private const val A9_BRIGHTNESS_NATIVE_MAX = 255 @@ -1641,8 +1641,8 @@ object EVHardware { } /** - * Appelle une method sur sVsm133 par reflection. - * Returns la value (Int pour getters, null pour setters void) ou null si erreur. + * Calls a method on sVsm133 by reflection. + * Returns the value (Int for getters, null for void setters), or null on error. */ private fun callVsm133(methodName: String, vararg args: Any?): Any? { val vsm = sVsm133 ?: return null @@ -1719,11 +1719,11 @@ object EVHardware { sVsm?.let { tryGetVsmService(it, vsmClass) } - // GeneralManager pour SWI68/SWI165 (brightness screen) — same launcher context + // GeneralManager for SWI68/SWI165 (screen brightness) — same launcher context tryInitGeneralManager(launcherCtx, context) tryInitVehicleControlManager(launcherCtx, context) - // SmartSoundManager pour SWI68/SWI165 (loudness audio) — same launcher context + // SmartSoundManager for SWI68/SWI165 (audio loudness) — same launcher context tryInitSmartSoundManager(launcherCtx, context) // Retries to obtain mVehicleSettingService and the singleton if not ready yet @@ -1775,7 +1775,7 @@ object EVHardware { // → onResult(0=OK) : queryClient(0x8) → IBinder (ICarVehicleSettingService) // → new CarVehicleSettingClient(ibinder) // - // CarVehicleSettingClient expose exactement les same methods que VehicleSettingManager + // CarVehicleSettingClient exposes exactly the same methods as VehicleSettingManager // (getAccTjaState, setAccTjaState, getLasWarningSound, getFcwState, etc.) // ------------------------------------------------------------------------- @@ -1799,7 +1799,7 @@ object EVHardware { return } - // Obtenir le singleton CarAdapterClient + // Get the CarAdapterClient singleton val adapter = tryInvoke("SWI69 CarAdapterClient.getInstance(appCtx)") { adapterClass.getMethod("getInstance", Context::class.java).invoke(null, context.applicationContext) } ?: tryInvoke("SWI69 CarAdapterClient.getInstance(launcherCtx)") { @@ -1815,7 +1815,7 @@ object EVHardware { sCarAdapter = adapter sCarAdapterClass = adapterClass - // Enregistrer le ServiceConnListener (onResult(0) = connected) + // Register the ServiceConnListener (onResult(0) = connected) val listenerType = adapterClass.declaredClasses .firstOrNull { it.simpleName == "ServiceConnListener" } if (listenerType != null && listenerType.isInterface) { @@ -1855,7 +1855,7 @@ object EVHardware { } /** - * Tente d'obtenir un CarVehicleSettingClient via queryClient(0x8). + * Tries to get a CarVehicleSettingClient through queryClient(0x8). * Called on connection (onResult=0) and on retries. */ private fun tryInitClientFromAdapter(adapter: Any, adapterClass: Class<*>, clientClass: Class<*>) { @@ -1866,7 +1866,7 @@ object EVHardware { .invoke(adapter, VSM_SERVICE_CODE) as? IBinder if (ibinder == null) { - AppLogger.d(TAG, " SWI69: queryClient(0x${VSM_SERVICE_CODE.toString(16)}) → null (pas encore connected)") + AppLogger.d(TAG, " SWI69: queryClient(0x${VSM_SERVICE_CODE.toString(16)}) → null (not connected yet)") return } @@ -1967,9 +1967,9 @@ object EVHardware { } /** - * Appelle une method void sur sVsm par reflection. + * Calls a void method on sVsm by reflection. * Unlike callVsm(), returns true if the method exists and runs without exception, - * same si invoke() returns null (behaviour normal pour les methods void). + * even if invoke() returns null (normal behaviour for void methods). * Returns false if sVsm is null or an exception is thrown (method not found, etc.). */ private fun callVsmVoid(methodName: String, vararg args: Any?): Boolean { @@ -2016,19 +2016,19 @@ object EVHardware { if (logEnabled) AppLogger.i(TAG, " VPM setIntRecovery 0x${propId.toString(16)} value=$value ✓") true } catch (e: Exception) { - // Fallback sur setIntProperty si setIntPropertyRecovery absent + // Fall back to setIntProperty if setIntPropertyRecovery is missing AppLogger.d(TAG, " VPM setIntRecovery fallback for 0x${propId.toString(16)}: ${e.message}") setIntPropertyVpm(propId, value) } } - // ── Alimentation vehicle (mise hors tension, infodivertissement maintenu) ─ - // Reproduit le bouton "Vehicle Power → Off" du launcher MG (onglet Safety). + // ── Vehicle power (switched off, infotainment kept on) ───────────────── + // Reproduces the MG launcher's "Vehicle Power → Off" button (Safety tab). // Value 2 on all 6 firmwares; only the access path differs : // • SWI133 : VehiclePropertyManager.setIntPropertyRecovery(0x6030021, 2) // • SWI68/SWI165 : VehicleSettingManager.setPowerModeSwitch(2) // • A9 (132/131/69): CarAdapterClient.queryClient(0xf) → CarComfortabletClient.setPowerModeSwitch(2) - // Le vehicle n'runs la coupure qu'when stopped en position P (garde side firmware). + // The vehicle only cuts power when stopped in P (a firmware-side guard). private const val PROP_POWER_MODE_SWITCH = 0x6030021 // ID_POWER_MODE_SWITCH (SWI133) private const val POWER_MODE_OFF = 2 private const val COMFORTABLET_CLIENT_CLASS = "com.saicmotor.carapi.client.CarComfortabletClient" @@ -2037,8 +2037,8 @@ object EVHardware { /** * Available on all 6 firmwares: each implements a "P position" check (guard for - * safety — le firmware ne garde PAS la commande, donc sans check P une extinction en roulant - * serait possible). Value P = 1 partout (cf. isVehicleInPark) : + * safety — the firmware does NOT guard the command, so without the P check switching off + * while driving would be possible). P is 1 everywhere (see isVehicleInPark): * • SWI133 : gear VPM 0x5030043 ✓ * • A9 (132/131/69) : CarStateClient.getGearState() ✓ (confirmed SWI132) * • SWI68/165 : VehicleConditionManager.getCarGear() poll ✓ (confirmed SWI68, same SDK 165) @@ -2046,7 +2046,7 @@ object EVHardware { fun hasVehiclePowerOff(): Boolean = FirmwareInfo.getGeneration() != FirmwareInfo.Gen.UNKNOWN - /** Coupe l'alimentation du vehicle tout en gardant l'screen/infodivertissement actif. */ + /** Cuts the vehicle's power while keeping the screen and infotainment on. */ fun vehiclePowerOff(): Boolean { // Safety guard: never power off outside P (re-verified on send). if (isVehicleInPark() != true) { @@ -2067,7 +2067,7 @@ object EVHardware { } } - /** A9 (SWI132/131/69) : obtient CarComfortabletClient via l'adaptateur carapi et coupe l'alim. */ + /** A9 (SWI132/131/69): gets CarComfortabletClient through the carapi adapter and cuts the power. */ private fun vehiclePowerOffA9(): Boolean { val cl = sVsm?.javaClass?.classLoader ?: run { AppLogger.w(TAG, " A9 power-off: sVsm/classloader null"); return false @@ -2089,9 +2089,9 @@ object EVHardware { } } - // ── Garde de safety : levier en position P ? ─────────────────────────── - // L'OEM n'autorise le power-off qu'en P (gear == 1), mais NE garde PAS la commande - // side firmware → c'est l'app qui doit verify (sinon extinction possible en roulant). + // ── Safety guard: is the lever in P? ────────────────────────────────── + // The OEM only allows power-off in P (gear == 1) but does NOT guard the command in the + // firmware → the app must check (otherwise switching off while driving is possible). // Supported gear paths by firmware: // • SWI133 : VPM getIntProperty(0x5030043) ; PARK = 1 (CAR_GEAR_PARK_RANGE) // • SWI68/SWI165 : VehicleConditionBean.getCarGear() (signal condition vehicle) @@ -2555,7 +2555,7 @@ object EVHardware { fun setOverspeedAlarm(on: Boolean): Boolean { if (logEnabled) AppLogger.i(TAG, "setOverspeedAlarm → $on") if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132) { - // Essai 1 : CarVehicleSettingClient — setOverSpeedSoundMode(I)V est une method void ; + // Attempt 1: CarVehicleSettingClient — setOverSpeedSoundMode(I)V is a void method; // callVsmVoid() detects success even when invoke() returns null (normal behaviour). if (callVsmVoid("setOverSpeedSoundMode", if (on) 1 else 0)) { AppLogger.i(TAG, " SWI132 overspeed → VSM OK") @@ -2616,7 +2616,7 @@ object EVHardware { // ── SWI68 / SWI69 ADAS API — VehicleSettingManager (different method names) ── /** - * Returns le mode ACC/TJA actuel (0x4=Off, 0x1=ACC, 0x2=TJA), ou -1 si pas ready. + * Returns the current ACC/TJA mode (0x4=Off, 0x1=ACC, 0x2=TJA), or -1 if not ready. * SWI68/SWI165 : getAccTjaMode() SWI69/SWI131/SWI132 : getAccTjaState() */ fun getAccTjaMode(): Int { @@ -2638,7 +2638,7 @@ object EVHardware { /** * Speed limiter — unified API for all VSM firmwares. - * Le limiteur est un setting INDEPENDENT of the ACC/TJA mode, avec les same values partout + * The limiter is a setting INDEPENDENT of the ACC/TJA mode, with the same values everywhere * (0=Disabled, 2=Manual, 3=Intelligent on supported firmware), * only the binder method name differs : * SWI132/SWI131/SWI69 : getSasMode/setSasMode (CarVehicleSettingClient) @@ -2647,7 +2647,7 @@ object EVHardware { private fun useSasApi(): Boolean = FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 - /** Lit le mode du limiteur de speed. Returns 0/2/3, ou -1 si unavailable. */ + /** Reads the speed limiter mode. Returns 0/2/3, or -1 if unavailable. */ fun getSpeedLimiterMode(): Int { val method = if (useSasApi()) "getSasMode" else "getSpeedAsstMode" val value = (callVsm(method) as? Int) ?: -1 @@ -2655,7 +2655,7 @@ object EVHardware { return value } - /** Configure le mode du limiteur de speed. 0=Disabled, 2=Manual, 3=Intelligent. */ + /** Sets the speed limiter mode. 0=Disabled, 2=Manual, 3=Intelligent. */ @RequiresStandstill fun setSpeedLimiterMode(mode: Int): Boolean { val method = if (useSasApi()) "setSasMode" else "setSpeedAsstMode" @@ -2711,13 +2711,13 @@ object EVHardware { if (logEnabled) AppLogger.i(TAG, "setAebEnabled → $on") return when { // SWI69 / SWI131 / SWI132 — CarVehicleSettingClient (same API) - // setFcwState(I)V et setFcwAutoBrakeMode(I)V sont des methods VOID → + // setFcwState(I)V and setFcwAutoBrakeMode(I)V are VOID methods → // callVsmVoid() is used to avoid the false negative of callVsm() != null. FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> { // OFF : setFcwState(1) + setFcwAutoBrakeMode(1) + setFcwSensitivity(0) // ON : setFcwState(2) + setFcwAutoBrakeMode(curMode) // The launcher gates its display on fcwState==1 AND autoBreakState==1 - // → sans setFcwAutoBrakeMode, son switch reste ON same quand l'AEB est disabled + // → without setFcwAutoBrakeMode, its switch stays ON even when AEB is disabled if (on) { val sOk = callVsmVoid("setFcwState", 2) val curMode = (callVsm("getFcwAutoBrakeMode") as? Int) ?: 1 @@ -2731,10 +2731,10 @@ object EVHardware { } FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 || FirmwareInfo.isSWI165() -> { - // SWI68 / SWI165 : setFcwAlarmMode(2=ON / 1=OFF) + setFcwAutoBrakeMode(1) si OFF + // SWI68 / SWI165: setFcwAlarmMode(2=ON / 1=OFF) + setFcwAutoBrakeMode(1) if OFF // Verified in SafeSettingsRepository SWI165 — same API as SWI68, // setAutoEmergencyBraking() is never used by the official app. - // setFcwAlarmMode(I)V et setFcwAutoBrakeMode(I)V sont void → callVsmVoid() + // setFcwAlarmMode(I)V and setFcwAutoBrakeMode(I)V are void → callVsmVoid() if (on) callVsmVoid("setFcwAlarmMode", 2) else { callVsmVoid("setFcwAlarmMode", 1) or callVsmVoid("setFcwAutoBrakeMode", 1) } } @@ -2743,9 +2743,9 @@ object EVHardware { } /** - * Returns le mode AEB courant (1=Alert, 2=Alert+Freinage), ou -1 si pas ready. + * Returns the current AEB mode (1=Alert, 2=Alert+Braking), or -1 if not ready. * SWI133 : PROP_AEB_MODE (0x302000b) via VehiclePropertyManager. - * SWI68/SWI69/SWI131 : getFcwAutoBrakeMode() (1=Alert, 2=Alert+Freinage). + * SWI68/SWI69/SWI131 : getFcwAutoBrakeMode() (1=Alert, 2=Alert+Braking). */ fun getAebMode(): Int { return if (FirmwareInfo.isVsmBased()) { @@ -2769,10 +2769,10 @@ object EVHardware { fun setAebMode(mode: Int): Boolean { if (logEnabled) AppLogger.i(TAG, "setAebMode → $mode") return when { - // SWI69 / SWI131 / SWI132 — CarVehicleSettingClient : fixer mode puis enable - // setFcwAutoBrakeMode(I)V et setFcwState(I)V sont void → callVsmVoid() + // SWI69 / SWI131 / SWI132 — CarVehicleSettingClient: set the mode, then enable + // setFcwAutoBrakeMode(I)V and setFcwState(I)V are void → callVsmVoid() FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> { - // L'ordre : 1) fixer le mode, 2) enable (commit le mode). + // The order: 1) set the mode, 2) enable (commits the mode). val modeVal = if (mode == AebMode.ALARM_BRAKE) 2 else 1 val mOk = callVsmVoid("setFcwAutoBrakeMode", modeVal) val sOk = callVsmVoid("setFcwState", 2) @@ -2780,7 +2780,7 @@ object EVHardware { } FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 || FirmwareInfo.isSWI165() -> { - // SWI68 / SWI165 : setFcwAutoBrakeMode only (1=Alert, 2=Alert+Freinage) + // SWI68 / SWI165 : setFcwAutoBrakeMode only (1=Alert, 2=Alert+Braking) callVsm("setFcwAutoBrakeMode", if (mode == AebMode.ALARM_BRAKE) 2 else 1) != null } else -> { @@ -2797,7 +2797,7 @@ object EVHardware { } /** - * Returns la sensitivity AEB courante (1=Low, 2=Standard, 3=High), ou -1 si pas ready. + * Returns the current AEB sensitivity (1=Low, 2=Standard, 3=High), or -1 if not ready. * SWI133 : PROP_AEB_SENSITIVITY (0x302000e, VPM) * SWI68/SWI165 : VehicleSettingManager.getFcwSensitivity() * SWI69/SWI131 : CarVehicleSettingClient.getFcwSensitivity() @@ -2832,13 +2832,13 @@ object EVHardware { } // ------------------------------------------------------------------------- - // ELK — Assistant de sortie de voie (SWI133 only pour l'instant) + // ELK — emergency lane keeping (SWI133 only for now) // Utilise IVehicleSettingService via sVehicleBinder (TX 0x53–0x56) // ------------------------------------------------------------------------- /** - * Returns le mode ELK courant (1=OFF, 2=Alert, 3=Aider, 5=Maintien d'urgence). - * Routage par firmware : + * Returns the current ELK mode (1=OFF, 2=Alert, 3=Assist, 5=Emergency keep). + * Routing per firmware: * SWI133 → VSM133 (getLaneKeepingAsstMode) → binder TX 0x53 * SWI68/SWI165 → VSM (getLaneKeepingAsstMode) * SWI69/SWI131 → VSM (getLasMode) @@ -2897,7 +2897,7 @@ object EVHardware { } /** - * Returns la sensitivity ELK courante (1=Low, 2=Standard, 3=High). + * Returns the current ELK sensitivity (1=Low, 2=Standard, 3=High). * Routing per firmware — identical to getElkMode(). */ fun getElkSensitivity(): Int = when { @@ -2953,7 +2953,7 @@ object EVHardware { } } - /** Returns true si l'ELK est enabled (mode ≠ OFF). */ + /** Returns true if ELK is enabled (mode ≠ OFF). */ fun isElkEnabled(): Boolean { val mode = getElkMode() return mode > 0 && mode != ElkMode.OFF @@ -2961,7 +2961,7 @@ object EVHardware { /** * SWI132 — Alert sound (LAS Warning Sound) : 0=OFF, 1=ON. - * Returns -1 si erreur ou firmware non SWI132. + * Returns -1 on error or on a firmware other than SWI132. */ fun getLasWarningSound(): Int { if (FirmwareInfo.getGeneration() != FirmwareInfo.Gen.SWI132) return -1 @@ -2978,8 +2978,8 @@ object EVHardware { } /** - * SWI132 — Rappel par vibration (LAS Warning Vibration) : 0=OFF, 1=ON. - * Returns -1 si erreur ou firmware non SWI132. + * SWI132 — Vibration reminder (LAS Warning Vibration) : 0=OFF, 1=ON. + * Returns -1 on error or on a firmware other than SWI132. */ fun getLasWarningVibration(): Int { if (FirmwareInfo.getGeneration() != FirmwareInfo.Gen.SWI132) return -1 @@ -3301,7 +3301,7 @@ object EVHardware { } // ------------------------------------------------------------------------- - // TSR — Reconnaissance des panneaux de speed + // TSR — traffic sign recognition // ------------------------------------------------------------------------- fun tsrOnOrNull(): Boolean? = when { @@ -3342,7 +3342,7 @@ object EVHardware { } FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 -> { // SWI133: the firmware resets OVERSPEED and SPEED_TONE to ON when SLIF is re-enabled - // → on sauvegarde avant et on restaure after. + // → save them before and restore them after. val prefs = sAppContext?.getSharedPreferences("ev_settings", 0) if (!enabled) { val overspeedOn = isOverspeedAlarmOn() @@ -3365,19 +3365,19 @@ object EVHardware { ok } FirmwareInfo.isNewGenVsm() -> { // SWI69 + SWI131 — convention invertede : 0=enable, 1=disablesr - // setSLIFWarningState(I)V est void → callVsmVoid() + // setSLIFWarningState(I)V is void → callVsmVoid() callVsmVoid("setSLIFWarningState", if (enabled) 0 else 1) } FirmwareInfo.isVsmBased() -> { // SWI68 + SWI165 // The warning sound may be reset to ON when re-enabling TSR - // → on sauvegarde avant et on restaure after. + // → save it before and restore it after. val prefs = sAppContext?.getSharedPreferences("ev_settings", 0) if (!enabled) { val soundOn = isSoundWarningOn() prefs?.edit()?.putBoolean("tsr_saved_sound_warning", soundOn)?.apply() AppLogger.i(TAG, " TSR OFF — sauvegarde soundWarning=$soundOn") } - // setSpeedAsstSlifWarning(I)V est void → callVsmVoid() + // setSpeedAsstSlifWarning(I)V is void → callVsmVoid() if (!callVsmVoid("setSpeedAsstSlifWarning", if (enabled) 1 else 0)) return false if (enabled) { Thread.sleep(400) @@ -3394,8 +3394,8 @@ object EVHardware { /** * SWI133: returns (overspeed, speedTone) as saved at the last TSR OFF. * Used by the UI to update the switches after re-enabling TSR, without - * relire le hardware (le VPM a une latence de propagation qui renverrait encore ON - * pendant ~500–1000ms after les writes internes de setTsrMode). + * reading the hardware again (the VPM has a propagation delay that would still report ON + * for ~500–1000 ms after setTsrMode's internal writes). */ fun savedTsrAlerts(): Pair { val prefs = sAppContext?.getSharedPreferences("ev_settings", 0) @@ -3428,11 +3428,11 @@ object EVHardware { FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 -> setIntPropertyVpmRecovery(PROP_ENERGY_SAVING, if (enabled) 1 else 0) FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> { // SWI69 + SWI131 + SWI132 - // setEnduranceMode(I)V est void → callVsmVoid() + // setEnduranceMode(I)V is void → callVsmVoid() callVsmVoid("setEnduranceMode", if (enabled) 1 else 0) } FirmwareInfo.isVsmBased() -> { // SWI68 + SWI165 - // setLongerEndurance(I)V est void → callVsmVoid() + // setLongerEndurance(I)V is void → callVsmVoid() callVsmVoid("setLongerEndurance", if (enabled) 1 else 0) } else -> false @@ -3447,13 +3447,13 @@ object EVHardware { fun isCarHvacManagerReady(): Boolean = sCarHvacManager != null // ------------------------------------------------------------------------- - // IGNITION_STATE — CarPropertyManager callback (standard AAOS, tous firmwares) + // IGNITION_STATE — CarPropertyManager callback (standard AAOS, all firmwares) // ------------------------------------------------------------------------- /** - * Enregistre un CarPropertyEventCallback sur PROP_IGNITION_STATE via reflection. + * Registers a CarPropertyEventCallback on PROP_IGNITION_STATE by reflection. * Guarded by [sIgnitionCallbackRegistered] — runs only once. - * Lit l'state courant immediatement after l'registersment : CPM ne notifie que sur changement, + * Reads the current state right after registering: CPM only notifies on change, * so if the car is already READY at bind time, no event would arrive without this read. */ private fun registerIgnitionPropertyCallback() { @@ -3509,10 +3509,10 @@ object EVHardware { sIgnitionCallbackRegistered = true AppLogger.i(TAG, " IGNITION_STATE callback registered ✓ (propId=0x${PROP_IGNITION_STATE.toString(16)})") - // Read immediate de l'state courant + // Read the current state at once Handler(Looper.getMainLooper()).postDelayed({ val currentState = getCurrentIgnitionState() - AppLogger.i(TAG, " IGNITION: state initial lu = $currentState (${ignitionStateName(currentState)})") + AppLogger.i(TAG, " IGNITION: initial state read = $currentState (${ignitionStateName(currentState)})") if (currentState > 0) { dispatchIgnitionState(currentState) } @@ -3570,7 +3570,7 @@ object EVHardware { if (sVcmCallbackRegistered) return val classes = findSwi69Classes(context) ?: run { - AppLogger.w(TAG, " Katman5 SWI69: CarAdapterClient/CarGeneralClient introuvable — retry in 10s") + AppLogger.w(TAG, " Katman5 SWI69: CarAdapterClient/CarGeneralClient not found — retry in 10s") Handler(Looper.getMainLooper()).postDelayed({ initKatman5Swi69(context.applicationContext) }, 10_000) return } @@ -3614,12 +3614,12 @@ object EVHardware { return false } - // Binder real required pour l'registersment cross-process via AIDL. - // ICarGeneralService est dans un processus distant (com.saicmotor.caradapter) : - // registListener() appelle writeStrongBinder(callback.asBinder()) — un proxy qui - // returns null pour asBinder() transmettrait un binder null au service, qui ne + // A real Binder is required to register across processes over AIDL. + // ICarGeneralService lives in a remote process (com.saicmotor.caradapter): + // registListener() calls writeStrongBinder(callback.asBinder()) — a proxy that + // returns null from asBinder() would hand the service a null binder, and it // would ever call back. So we create a concrete Binder implementing onTransact - // pour le code 0x7 (TRANSACTION_onIgnitionStateChange, identique sur SWI69 et SWI131). + // for code 0x7 (TRANSACTION_onIgnitionStateChange, the same on SWI69 and SWI131). val callbackBinder = object : android.os.Binder() { override fun onTransact( code: Int, data: android.os.Parcel, @@ -3645,7 +3645,7 @@ object EVHardware { ) { _, method, args -> when (method.name) { "onIgnitionStateChange" -> { - // Chemin in-process (rare) — le service appelle directement l'interface + // In-process path (rare) — the service calls the interface directly val ignition = args?.get(0) as? Int if (ignition != null) { AppLogger.i(TAG, " Katman5 SWI69: ignition=$ignition (${carIgnitionName(ignition)})") @@ -3675,7 +3675,7 @@ object EVHardware { try { val ignition = generalClientClass.getMethod("getIgnitionState").invoke(client) as? Int if (ignition != null) { - AppLogger.i(TAG, " Katman5 SWI69: state initial ignition=$ignition") + AppLogger.i(TAG, " Katman5 SWI69: initial state ignition=$ignition") dispatchVehicleConditionIgnition(ignition) } } catch (e: Exception) { @@ -3714,7 +3714,7 @@ object EVHardware { ) } catch (_: Exception) { null } } ?: run { - AppLogger.w(TAG, " Katman5: launcher package introuvable") + AppLogger.w(TAG, " Katman5: launcher package not found") return } @@ -3726,7 +3726,7 @@ object EVHardware { return } - // Tentative 1 : singleton already initialisesd par le launcher + // Attempt 1: the singleton the launcher has already initialised val existing = try { val f = vcmClass.getDeclaredField("sVehicleConditionManager") f.isAccessible = true @@ -3734,13 +3734,13 @@ object EVHardware { } catch (_: Exception) { null } if (existing != null) { - AppLogger.i(TAG, " Katman5: singleton VCM already existant ✓") + AppLogger.i(TAG, " Katman5: VCM singleton already exists ✓") sVcm = existing setupVcmCallback(existing, launcherCtx) return } - // Tentative 2 : appeler init(Context, IVehicleServiceListener) + // Attempt 2: call init(Context, IVehicleServiceListener) val initMethod = vcmClass.methods.firstOrNull { m -> m.name == "init" && m.parameterCount == 2 && Context::class.java.isAssignableFrom(m.parameterTypes[0]) @@ -3792,7 +3792,7 @@ object EVHardware { AppLogger.w(TAG, " Katman5: init(Context, listener) not found") } - // Retries — singleton disponible after connection asynchrone + // Retries — the singleton is available after the async connection val h = Handler(Looper.getMainLooper()) listOf(2_000L, 5_000L, 10_000L, 20_000L, 30_000L, 60_000L).forEach { delay -> h.postDelayed({ @@ -3877,7 +3877,7 @@ object EVHardware { vcm.javaClass.getMethod("getVehicleIgnition").invoke(vcm) as? Int } catch (_: Exception) { null } if (ignition != null) { - AppLogger.i(TAG, " Katman5: state initial = $ignition (${carIgnitionName(ignition)})") + AppLogger.i(TAG, " Katman5: initial state = $ignition (${carIgnitionName(ignition)})") dispatchVehicleConditionIgnition(ignition) } }, 500L) @@ -3890,7 +3890,7 @@ object EVHardware { private fun dispatchVehicleConditionIgnition(state: Int) { // Only dispatch if the state really changes — avoids repeated false RUN events // that VehicleConditionManager sends on every vehicle condition change - // (changement de rapport D/N/R, etc.) alors que la car est already en RUN. + // (a gear change D/N/R, etc.) while the car is already in RUN. if (state == sLastVcmIgnitionState) return sLastVcmIgnitionState = state val toNotify = vehicleConditionCallbacks.toList() @@ -3917,12 +3917,12 @@ object EVHardware { // Diagnostic // ------------------------------------------------------------------------- - /** Returns true si le binder IVehicleSettingService est disponible. */ + /** Returns true if the IVehicleSettingService binder is available. */ fun isVehicleBinderAvailable(): Boolean = sVehicleBinder != null /** * Generates a full diagnostic report : - * state des services, tests binder SWI132 en temps real, dump AppLogger. + * the services' state, live SWI132 binder tests, an AppLogger dump. * Call on Dispatchers.IO (binder TX are blocking). */ fun buildDiagnosticReport(appVersion: String): String { @@ -3968,7 +3968,7 @@ object EVHardware { // ── Alerts — read brute + interprstateion ────────────────── fun fmtRaw(raw: Int, onVal: Int = 1): String = when { - raw < 0 -> "$raw ← ERREUR (SELinux ? binder mort ?)" + raw < 0 -> "$raw ← ERROR (SELinux? binder dead?)" raw == onVal -> "$raw → ON ✓" raw == 0 -> "$raw → OFF" else -> "$raw → ?" @@ -3985,7 +3985,7 @@ object EVHardware { // ── Alerts — SET round-trip test (writes the current value) ─ // If the raw value is readable (≥ 0), rewrite the same value to test - // que le SET passe (sans modifier l'state real de la car). + // that the SET goes through (without changing the car's real state). sb.appendLine("── SWI132 Alerts (SET round-trip) ──") if (rawOverspeed >= 0) { val setOk = swi132BinderSet(VSM132_TX_OVERSPEED_SOUND, rawOverspeed) @@ -4018,7 +4018,7 @@ object EVHardware { fun vsmGet(method: String): Int = try { (callVsm(method) as? Int) ?: -1 } catch (_: Exception) { -1 } - fun fmtVsm(v: Int, ok: String) = if (v >= 0) "$v → $ok" else "-1 ← ERREUR (method absente ou sVsm null)" + fun fmtVsm(v: Int, ok: String) = if (v >= 0) "$v → $ok" else "-1 ← ERROR (method missing or sVsm null)" // ACC/TJA val accTjaRaw = getAccTjaMode() @@ -4026,29 +4026,29 @@ object EVHardware { Swi68Mode.OFF -> "4 → OFF" Swi68Mode.ACC -> "1 → ACC" Swi68Mode.TJA -> "2 → TJA" - -1 -> "-1 ← ERREUR" + -1 -> "-1 ← ERROR" else -> "$accTjaRaw → ?" }}") // AEB val fcwRaw = vsmGet("getFcwState") sb.appendLine("getFcwState : ${when (fcwRaw) { - 2 -> "2 → AEB ON" ; 1 -> "1 → AEB OFF" ; -1 -> "-1 ← ERREUR" ; else -> "$fcwRaw → ?" + 2 -> "2 → AEB ON" ; 1 -> "1 → AEB OFF" ; -1 -> "-1 ← ERROR" ; else -> "$fcwRaw → ?" }}") val fcwModeRaw = vsmGet("getFcwAutoBrakeMode") sb.appendLine("getFcwAutoBreak : ${when (fcwModeRaw) { - 1 -> "1 → Alert" ; 2 -> "2 → Al.+Frein" ; -1 -> "-1 ← ERREUR" ; else -> "$fcwModeRaw → ?" + 1 -> "1 → Alert" ; 2 -> "2 → Al.+Brake" ; -1 -> "-1 ← ERROR" ; else -> "$fcwModeRaw → ?" }}") val fcwSenRaw = vsmGet("getFcwSensitivity") sb.appendLine("getFcwSensitiv. : ${when (fcwSenRaw) { - 1 -> "1 → Low" ; 2 -> "2 → Standard" ; 3 -> "3 → High" ; -1 -> "-1 ← ERREUR" ; else -> "$fcwSenRaw → ?" + 1 -> "1 → Low" ; 2 -> "2 → Standard" ; 3 -> "3 → High" ; -1 -> "-1 ← ERROR" ; else -> "$fcwSenRaw → ?" }}") // ELK val lasRaw = vsmGet("getLasMode") sb.appendLine("getLasMode (ELK) : ${when (lasRaw) { 1 -> "1 → OFF" ; 2 -> "2 → Alert" ; 3 -> "3 → Assist" ; 5 -> "5 → Urgence" - -1 -> "-1 ← ERREUR" ; else -> "$lasRaw → ?" + -1 -> "-1 ← ERROR" ; else -> "$lasRaw → ?" }}") // ── Alerts via the SWI132 VSM runtime path ────────────────────── @@ -4056,13 +4056,13 @@ object EVHardware { sb.appendLine("── SWI132 Alerts via VSM (ICarVehicleSettingService) ──") val vsmOverspeed = vsmGet("getOverSpeedSoundMode") sb.appendLine("getOverSpeedSoundMode : ${when { - vsmOverspeed < 0 -> "-1 ← ERREUR (method absente ?)" + vsmOverspeed < 0 -> "-1 ← ERROR (method missing?)" vsmOverspeed == 0 -> "0 → OFF" else -> "$vsmOverspeed → ON" }}") val vsmSpeedLimit = vsmGet("getSpeedLimitSoundMode") sb.appendLine("getSpeedLimitSoundMode : ${when { - vsmSpeedLimit < 0 -> "-1 ← ERREUR (method absente ?)" + vsmSpeedLimit < 0 -> "-1 ← ERROR (method missing?)" vsmSpeedLimit == 0 -> "0 → OFF" else -> "$vsmSpeedLimit → ON" }}") @@ -4070,16 +4070,16 @@ object EVHardware { // TSR / SLIF val vsmSlif = vsmGet("getSLIFWarningState") sb.appendLine("getSLIFWarningState : ${when { - vsmSlif < 0 -> "-1 ← ERREUR (method absente ?)" + vsmSlif < 0 -> "-1 ← ERROR (method missing?)" vsmSlif == 0 -> "0 → TSR ON (convention SWI69)" vsmSlif == 1 -> "1 → TSR OFF (convention SWI69)" else -> "$vsmSlif → ?" }}") - // Son d'alert de voie (LAS) + // Lane alert sound (LAS) val vsmLasSound = vsmGet("getLasWarningSound") sb.appendLine("getLasWarningSound : ${when { - vsmLasSound < 0 -> "-1 ← ERREUR (method absente ?)" + vsmLasSound < 0 -> "-1 ← ERROR (method missing?)" vsmLasSound == 2 -> "2 → ON" vsmLasSound == 1 -> "1 → OFF" else -> "$vsmLasSound → ?" @@ -4117,7 +4117,7 @@ object EVHardware { val enduranceRaw = vsmGet("getEnduranceMode") sb.appendLine() sb.appendLine("getEnduranceMode : ${when (enduranceRaw) { - 1 -> "1 → Eco ON" ; 0 -> "0 → Eco OFF" ; -1 -> "-1 ← ERREUR" ; else -> "$enduranceRaw → ?" + 1 -> "1 → Eco ON" ; 0 -> "0 → Eco OFF" ; -1 -> "-1 ← ERROR" ; else -> "$enduranceRaw → ?" }}") sb.appendLine() } @@ -4130,18 +4130,18 @@ object EVHardware { return sb.toString() } - // ── Contrôle audio (CarAdapterService vendor SAIC) — A9 only ────────── + // ── Audio control (SAIC's vendor CarAdapterService) — A9 only ───────── // - // Le service vendor `com.saicmotor.caradapter` (descripteur ICarAudioService) - // n'existe QUE sur la famille A9 (SWI69/131/132). Sur old-SDK (SWI133/68/165) - // il est absent → on ne tente same pas le bind (cf. hasAudioControl / initAudio). - // Codes de transaction verifieds identiques sur les 3 A9 (ICarAudioService$Stub). + // The vendor service `com.saicmotor.caradapter` (descriptor ICarAudioService) + // exists ONLY on the A9 family (SWI69/131/132). On the old SDK (SWI133/68/165) + // it is absent → the bind is not even attempted (see hasAudioControl / initAudio). + // Transaction codes verified identical on all three A9 (ICarAudioService$Stub). - /** A9 (SWI69/131/132) : loudness via le service vendor caradapter (ICarAudioService). */ + /** A9 (SWI69/131/132): loudness through the vendor caradapter service (ICarAudioService). */ private fun isA9Sound(): Boolean = FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 - /** Ancien SDK (SWI133/68/165) : loudness via SmartSoundManager (SDK systemsettings). */ + /** Old SDK (SWI133/68/165): loudness through SmartSoundManager (systemsettings SDK). */ private fun isOldSdkSound(): Boolean { val gen = FirmwareInfo.getGeneration() return gen == FirmwareInfo.Gen.SWI133 || gen == FirmwareInfo.Gen.SWI68 || gen == FirmwareInfo.Gen.SWI165 @@ -4176,7 +4176,7 @@ object EVHardware { val isAudioAvailable: Boolean get() = sAudioHelper?.isBinderAlive == true fun initAudio(context: Context) { - // Le bind caradapter ne concerne que l'A9. Sur old-SDK, le loudness passe par + // The caradapter bind only concerns the A9. On the old SDK, loudness goes through // SmartSoundManager (initialised in the Katman4 flow), so nothing to bind here. if (!isA9Sound()) return if (sAudioHelper?.isBinderAlive == true) return @@ -4256,7 +4256,7 @@ object EVHardware { fun getAudioFader(): Int = audioGet(AUDIO_GET_FADER) fun setAudioFader(v: Int): Boolean = audioSet(AUDIO_SET_FADER_FRONT, v.coerceIn(AUDIO_LEVEL_MIN, AUDIO_LEVEL_MAX)) // ── Media volume — VOL_TYPE_MEDIA=0 ──────────────────────────────────────── - // Routage par firmware : old-SDK (133/68/165) → SmartSoundManager (reflection) ; + // Routing per firmware: old SDK (133/68/165) → SmartSoundManager (reflection); // A9 (69/131/132) → ICarAudioService via binder caradapter (tx getMax=0x5 / getVol=0x6 / setVol=0x7). private const val VOL_TYPE_MEDIA = 0 private const val AUDIO_GET_MAX_VOL = 0x5 @@ -4265,7 +4265,7 @@ object EVHardware { private const val VOL_TAG = "EV_VOL" - /** Max du volume media (borne le slider). -1 si unavailable. */ + /** The media volume's maximum (bounds the slider). -1 if unavailable. */ fun getMediaVolumeMax(): Int { val v = when { isOldSdkSound() -> smartSoundGetInt("getMaxVolume", VOL_TYPE_MEDIA) @@ -4277,7 +4277,7 @@ object EVHardware { return v } - /** Media volume courant. -1 si unavailable. */ + /** The current media volume. -1 if unavailable. */ fun getMediaVolume(): Int { val v = when { isOldSdkSound() -> smartSoundGetInt("getVolume", VOL_TYPE_MEDIA) @@ -4288,7 +4288,7 @@ object EVHardware { return v } - /** Fixe le volume media. setVolume(type, niveau, flags=0). */ + /** Sets the media volume. setVolume(type, level, flags=0). */ fun setMediaVolume(level: Int): Boolean { val ok = when { isOldSdkSound() -> smartSoundSetVolume(level) @@ -4378,8 +4378,8 @@ object EVHardware { } catch (_: Exception) { false } finally { data.recycle(); reply.recycle() } } - // Diagnostic A9 : le param de getVolume/getMaxVolume est-il un "type" (0=media) ou un - // "group id" (AAOS) ? On logge les deux pour comparer au max real de la car. + // A9 diagnostic: is getVolume/getMaxVolume's parameter a "type" (0=media) or a + // "group id" (AAOS)? Both are logged, to compare with the car's real maximum. private const val AUDIO_GET_GROUP_FOR_USAGE = 0xe // getVolumeGroupIdForUsage(usage) private const val USAGE_MEDIA_AAOS = 1 // AudioAttributes.USAGE_MEDIA fun logMediaVolumeDiag() { @@ -4395,13 +4395,13 @@ object EVHardware { // ── Volume drop when a front door opens (v1: SWI133) ────────── // Detection via the AOSP Car API **CarPropertyManager** (service "property") + permission // CAR_VENDOR_EXTENSION (already declared). FRONT door IDs confirmed by a user : - // FL/FR "ratio" (taux d'openure, >0 = opene) + "mode". Le SDK SAIC (getIntProperty) + // FL/FR "ratio" (how far open, >0 = open) + "mode". SAIC's SDK (getIntProperty) // did not read them. Poll ~700ms, change-only (log EV_DOOR), drop on opening. - // Pas de restauration. Connexion Car async (createCar + ServiceConnection). + // No restore. The Car connection is async (createCar + ServiceConnection). private const val DOORWATCH_TAG = "EV_DOOR" - // Signal d'openure confirmed sur SWI133 : DLOCK_DOOR_OPEN_STS (property de zone PORTE), - // areaId 0x1 = door AV-gauche, 0x4 = AV-droite ; value 1 = opene, 0 = closede. + // Opening signal confirmed on SWI133: DLOCK_DOOR_OPEN_STS (a DOOR-zone property), + // areaId 0x1 = front-left door, 0x4 = front-right; value 1 = open, 0 = closed. private const val DOOR_OPEN_PROP = 0x2640c623 private val DOOR_FRONT_AREAS = intArrayOf(0x1, 0x4) private val sDoorReadLast = HashMap() @@ -4431,7 +4431,7 @@ object EVHardware { private fun doorRestoreEnabled(): Boolean = sAppContext?.getSharedPreferences("ev_settings", 0)?.getBoolean("door_volume_restore", false) ?: false - /** areaIds des doors choisies par l'utilisateur (gauche=0x1, droite=0x4). */ + /** areaIds of the doors the user picked (left=0x1, right=0x4). */ private fun doorTriggerAreas(): IntArray { val p = sAppContext?.getSharedPreferences("ev_settings", 0) ?: return DOOR_FRONT_AREAS val list = ArrayList(2) @@ -4440,7 +4440,7 @@ object EVHardware { return list.toIntArray() } - /** Cranking auto au boot (called par init) : ne lance le watcher que si la feature est enabled. */ + /** Starts at boot (called by init): runs the watcher only if the feature is enabled. */ fun startDoorWatcherIfEnabled() { if (hasDoorVolumeFeature() && doorVolumeEnabled()) startDoorVolumeWatcher() } @@ -4505,14 +4505,14 @@ object EVHardware { AppLogger.i(VOL_TAG, "── DIAG (bouton Diagnostic) ──") getMediaVolumeMax() getMediaVolume() - connectCarProperty() // idempotent ; normalement already connected depuis l'init + connectCarProperty() // idempotent; normally already connected since init registerDoorCallback() // retries the subscription if not attached yet probeDoorSnapshot() } private fun probeDoorSnapshot() { if (sCarPropMgr == null && sCarDoorMgr == null) { - AppLogger.w(DOORWATCH_TAG, "DIAG door: aucun manager Car (createCar failure ?)") + AppLogger.w(DOORWATCH_TAG, "DIAG door: no Car manager (createCar failure?)") return } AppLogger.i(DOORWATCH_TAG, "DIAG door: property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null}") @@ -4542,7 +4542,7 @@ object EVHardware { sCarDoorMgr = try { getMgr.invoke(car, "doorlock") } catch (_: Exception) { null } AppLogger.i(DOORWATCH_TAG, "managers: property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null}") if (sCarPropMgr != null || sCarDoorMgr != null) startDoorPolling() - else AppLogger.w(DOORWATCH_TAG, "aucun manager door disponible") + else AppLogger.w(DOORWATCH_TAG, "no door manager available") } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "getCarManager failure: ${e.message}") } } override fun onServiceDisconnected(name: ComponentName?) { sCarPropMgr = null; sCarDoorMgr = null; sDoorSubProperty = false; sDoorSubDoorlock = false } @@ -4558,8 +4558,8 @@ object EVHardware { } } - // Lit DLOCK_DOOR_OPEN_STS(areaId) via CarPropertyManager.getIntProperty puis, en fallback, - // via CarDoorLockManager.getProperty(Integer, propId, areaId).getValue(). null si aucun. + // Reads DLOCK_DOOR_OPEN_STS(areaId) through CarPropertyManager.getIntProperty, then as a + // fallback through CarDoorLockManager.getProperty(Integer, propId, areaId).getValue(). null if neither. private fun readDoorOpen(area: Int): Int? { sCarPropMgr?.let { m -> try { @@ -4584,7 +4584,7 @@ object EVHardware { val poll = object : Runnable { override fun run() { if (sCarPropMgr == null && sCarDoorMgr == null) return - // DLOCK_DOOR_OPEN_STS sur les doors avant (0x1 gauche / 0x4 droite). + // DLOCK_DOOR_OPEN_STS on the front doors (0x1 left / 0x4 right). for (area in DOOR_FRONT_AREAS) { val v = readDoorOpen(area) ?: continue onDoorAreaValue(area, v) @@ -4608,7 +4608,7 @@ object EVHardware { } /** Recomputes "at least one selected door open" from the accumulated state and applies - * la baisse (front d'openure) / la restauration (front de fermeture). Idempotent. */ + * the drop (on opening) / the restore (on closing). Idempotent. */ private fun evaluateDoorTrigger() { if (!(sDoorWatcherOn && doorVolumeEnabled())) return val triggerAreas = doorTriggerAreas() @@ -4618,7 +4618,7 @@ object EVHardware { CoroutineScope(Dispatchers.IO).launch { sVolumeBeforeDrop = getMediaVolume() val ok = setMediaVolume(level) - AppLogger.i(DOORWATCH_TAG, "door opene → vol $sVolumeBeforeDrop→$level = $ok") + AppLogger.i(DOORWATCH_TAG, "door open → vol $sVolumeBeforeDrop→$level = $ok") } } else if (!anyOpen && sAnyFrontOpenPrev) { val restore = sVolumeBeforeDrop @@ -4626,14 +4626,14 @@ object EVHardware { if (doorRestoreEnabled() && restore >= 0) { CoroutineScope(Dispatchers.IO).launch { val ok = setMediaVolume(restore) - AppLogger.i(DOORWATCH_TAG, "door closede → restauration vol $restore = $ok") + AppLogger.i(DOORWATCH_TAG, "door closed → restoring vol $restore = $ok") } } } sAnyFrontOpenPrev = anyOpen } - /** InvocationHandler commun aux deux interfaces de callback (onChangeEvent/onErrorEvent). + /** InvocationHandler shared by both callback interfaces (onChangeEvent/onErrorEvent). * Filters DLOCK_DOOR_OPEN_STS and feeds onDoorAreaValue. Also handles Object methods. */ private fun doorEventHandler(src: String) = InvocationHandler { proxy, method, args -> when (method.name) { @@ -4657,7 +4657,7 @@ object EVHardware { } catch (e: Exception) { AppLogger.w(DOORWATCH_TAG, "onChangeEvent: ${e.message}") } null } - "onErrorEvent" -> { AppLogger.w(DOORWATCH_TAG, "EVENT erreur door"); null } + "onErrorEvent" -> { AppLogger.w(DOORWATCH_TAG, "EVENT door error"); null } "hashCode" -> System.identityHashCode(proxy) "equals" -> proxy === args?.getOrNull(0) "toString" -> "EVDoorListener@" + Integer.toHexString(System.identityHashCode(proxy)) @@ -4669,14 +4669,14 @@ object EVHardware { * Subscribes to DLOCK_DOOR_OPEN_STS changes. Needed on SWI69/131/68/165 where the prop * is NOT readable on demand (getProperty throws IllegalArgumentException "Failed to get value") * but pushed via ON_CHANGE. IMPORTANT: the Proxy must be defined by the APP classloader - * (android.car est en BootClassLoader → Proxy.newProxyInstance y fails). On tente DEUX voies : + * (android.car is in the BootClassLoader → Proxy.newProxyInstance fails there). TWO ways are tried: * A) CarPropertyManager.registerListener (service "property") - * B) CarDoorLockManager.registerCallback (service "doorlock", comme la SystemUI d'origine) + * B) CarDoorLockManager.registerCallback (service "doorlock", like the stock SystemUI) */ private fun registerDoorCallback() { val cl = sAppContext?.classLoader ?: return - // Voie A — CarPropertyManager.registerListener(CarPropertyEventListener, propId, rate). + // Way A — CarPropertyManager.registerListener(CarPropertyEventListener, propId, rate). // rate=5f (not 0f): if the VHAL declares the prop CONTINUOUS, rate 0 = no updates. if (!sDoorSubProperty) sCarPropMgr?.let { m -> try { @@ -4693,7 +4693,7 @@ object EVHardware { } } - // Voie B — CarDoorLockManager.registerCallback(CarDoorLockEventCallback) — voie de l'OEM + // Way B — CarDoorLockManager.registerCallback(CarDoorLockEventCallback) — the OEM's way if (!sDoorSubDoorlock) sCarDoorMgr?.let { m -> try { val iface = cl.loadClass("android.car.hardware.doorlock.CarDoorLockManager\$CarDoorLockEventCallback") diff --git a/EVHardware/lib/src/test/java/com/evsuite/hardware/FirmwareSupportTest.kt b/EVHardware/lib/src/test/java/com/evsuite/hardware/FirmwareSupportTest.kt index 4bca781..f4d1109 100644 --- a/EVHardware/lib/src/test/java/com/evsuite/hardware/FirmwareSupportTest.kt +++ b/EVHardware/lib/src/test/java/com/evsuite/hardware/FirmwareSupportTest.kt @@ -30,19 +30,19 @@ class FirmwareSupportTest { ConditionType.FIRMWARE_GEN, ConditionType.LOCATION_WITHIN, ConditionType.PHYSICAL_BUTTON, - // Lus dans Android, pas dans la voiture : rien à router par génération. + // Read from Android, not from the car: nothing to route per generation. ConditionType.MEDIA_PLAYING, ConditionType.WIFI_SSID, ConditionType.IN_CALL, ConditionType.DRIVE_DURATION, ConditionType.RANDOM_CHANCE, - // Services des applications du poste (adaptateur de navigation, météo) : ce ne sont - // pas des services du SDK véhicule, rien ne les route par génération. Ils répondent - // ou non, comme le SMS. + // Services of the head unit's apps (navigation adapter, weather): these are not + // vehicle-SDK services, nothing routes them per generation. They answer or they do + // not, like SMS. ConditionType.ODOMETER, ConditionType.WEATHER_NOW, - // La connexion du poste, comptée par Android : elle dépend d'une permission, pas - // d'une génération de firmware. + // The head unit's connection, as counted by Android: it depends on a permission, not + // on a firmware generation. ConditionType.DATA_USED_TODAY, ConditionType.DATA_USED_MONTH ) @@ -54,10 +54,10 @@ class FirmwareSupportTest { ActionType.WEBHOOK, ActionType.ASK_CONFIRM, ActionType.DELAY, - // Le SMS part par le profil Bluetooth MAP, pas par un service SAIC : rien à router - // par génération de firmware. Sa disponibilité se lit au bind, pas dans la matrice. + // SMS goes out over the Bluetooth MAP profile, not through a SAIC service: nothing to + // route per firmware generation. Whether it is available shows at bind, not in the matrix. ActionType.SEND_SMS, - // Règles, touche média et radios du poste : rien de tout cela ne passe par la voiture. + // Rules, the media key and the head unit's radios: none of this goes through the car. ActionType.ENABLE_RULE, ActionType.DISABLE_RULE, ActionType.MEDIA_CONTROL, diff --git a/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassEvidenceTest.kt b/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassEvidenceTest.kt index d09c6a5..4636950 100644 --- a/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassEvidenceTest.kt +++ b/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassEvidenceTest.kt @@ -7,11 +7,11 @@ import org.junit.Assert.assertTrue import org.junit.Test /** - * Ce que la preuve relevée sur une voiture change — et ce qu'elle ne change pas. + * What evidence recorded on a car changes — and what it does not. * - * L'enjeu : `writeProven` parle du projet, `effectProven` parle de la voiture. Confondre les - * deux rendrait soit les actions vitres définitivement inaccessibles, soit accessibles sur des - * voitures où rien n'a été constaté. + * What is at stake: `writeProven` speaks about the project, `effectProven` about the car. + * Mixing the two up would make the window actions either unreachable for good, or reachable on + * cars where nothing was observed. */ class GlassEvidenceTest { @@ -21,7 +21,7 @@ class GlassEvidenceTest { fun tearDown() = GlassEvidence.setForTest(null) @Test - fun `sans preuve, effectProven suit writeProven`() { + fun `without evidence, effectProven follows writeProven`() { GlassEvidence.setForTest(null) ActionType.entries.forEach { assertTrue("${it.name}", it.writeProven == it.effectProven) @@ -29,29 +29,29 @@ class GlassEvidenceTest { } @Test - fun `la preuve debloque les actions vitres, et elles seules`() { + fun `evidence unlocks the window actions, and only them`() { GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, generation, 0L)) GlassEvidence.ACTIONS.forEach { - assertFalse("${it.name} reste non prouvée dans le catalogue", it.writeProven) - assertTrue("${it.name} doit être débloquée par la preuve", it.effectProven) + assertFalse("${it.name} stays unproven in the catalogue", it.writeProven) + assertTrue("${it.name} must be unlocked by the evidence", it.effectProven) } - // Une preuve sur les vitres ne dit rien d'une autre action : le catalogue reste seul - // juge partout ailleurs. + // Evidence about the windows says nothing about another action: everywhere else the + // catalogue alone decides. ActionType.entries.filterNot { it in GlassEvidence.ACTIONS }.forEach { assertTrue("${it.name}", it.writeProven == it.effectProven) } } @Test - fun `une preuve d'un autre firmware ne vaut rien`() { - // Les commandes sont propres au SDK de la génération : ce qui ouvre sur l'une n'a - // aucune raison d'ouvrir sur l'autre, et une mise à jour ne doit pas laisser derrière - // elle une autorisation qui n'a plus de fondement. + fun `evidence from another firmware counts for nothing`() { + // The commands belong to each generation's SDK: what opens on one has no reason to + // open on another, and an update must not leave behind a permission that no longer + // has any grounds. GlassEvidence.setForTest(GlassEvidence.Proof(5, 2, "SOME_OTHER_GEN", 0L)) GlassEvidence.ACTIONS.forEach { - assertFalse("${it.name} ne doit pas hériter d'une preuve étrangère", it.effectProven) + assertFalse("${it.name} must not inherit evidence from elsewhere", it.effectProven) } } } diff --git a/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassProbeTest.kt b/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassProbeTest.kt index 35673a2..a391f03 100644 --- a/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassProbeTest.kt +++ b/EVHardware/lib/src/test/java/com/evsuite/hardware/GlassProbeTest.kt @@ -8,15 +8,15 @@ import org.junit.Assert.assertTrue import org.junit.Test /** - * Ce que la sonde doit établir sans voiture : l'enchaînement des deux passes et les refus. + * What the probe must establish without a car: the sequence of its two passes, and its refusals. * - * La vitre simulée reproduit le comportement qui a rendu la sonde nécessaire : le service - * accepte n'importe quelle commande et n'en applique que deux. Une passe unique ne peut donc - * pas voir les deux sens — c'est exactement ce que ces tests vérifient. + * The simulated window reproduces the behaviour that made the probe necessary: the service + * accepts any command and applies only two of them. A single pass therefore cannot see both + * directions — which is exactly what these tests check. */ class GlassProbeTest { - /** Vitre où [openCommand] ouvre en grand et [closeCommand] referme ; le reste est inerte. */ + /** A window where [openCommand] opens fully and [closeCommand] closes; the rest do nothing. */ private class FakeGlass( private val openCommand: Int, private val closeCommand: Int, @@ -48,9 +48,9 @@ class GlassProbeTest { ) @Test - fun `les deux sens sont trouves depuis une vitre fermee`() { - // Depuis 0 %, la commande qui ferme ne bouge rien : sans seconde passe elle passerait - // pour inerte, et l'action « fermer » resterait à jamais non prouvée. + fun `both directions are found from a closed window`() { + // From 0 %, the closing command moves nothing: without a second pass it would pass + // for inert, and the "close" action would stay unproven forever. val glass = FakeGlass(openCommand = 5, closeCommand = 2) val result = probe(glass) @@ -58,27 +58,27 @@ class GlassProbeTest { assertEquals(2, result.closeCommand) assertTrue(result.proven) assertNull(result.refusal) - // Repartie de 0 %, la vitre doit y revenir : une sonde qui laisse la vitre ouverte - // est un défaut, pas un résultat. + // Having started at 0 %, the window must end there: a probe that leaves the window + // open is a defect, not a result. assertTrue(result.restored) assertEquals(0, glass.percent) } @Test - fun `une commande deja classee n'est pas renvoyee`() { + fun `a command already classified is not sent again`() { val glass = FakeGlass(openCommand = 0, closeCommand = 1) val result = probe(glass) assertEquals(0, result.openCommand) assertEquals(1, result.closeCommand) - // 0 ouvre, 1 referme, les deux sens sont connus : la sonde s'arrête là, plus une - // remise en place. Rien au-delà de la commande 1 ne doit avoir été envoyé. + // 0 opens, 1 closes, both directions are known: the probe stops there, plus one + // restore. Nothing beyond command 1 may have been sent. assertEquals(listOf(0, 1), glass.sent.take(2)) assertTrue(glass.sent.none { it > 1 }) } @Test - fun `une vitre inerte ne prouve rien`() { + fun `an inert window proves nothing`() { val glass = FakeGlass(openCommand = 99, closeCommand = 98) val result = probe(glass) @@ -86,28 +86,28 @@ class GlassProbeTest { assertNull(result.closeCommand) assertEquals(GlassProbe.Refusal.NOTHING_MOVED, result.refusal) assertFalse(result.proven) - // Les huit commandes, deux fois : la seconde passe ne peut être sautée sur la foi de - // la première, qui ne voit pas les fermetures. + // All eight commands, twice: the second pass cannot be skipped on the strength of + // the first, which does not see closing. assertEquals(16, glass.sent.size) } @Test - fun `un seul sens trouve n'est pas une preuve`() { + fun `one direction found is not proof`() { val glass = FakeGlass(openCommand = 3, closeCommand = 99) val result = probe(glass) assertEquals(3, result.openCommand) assertNull(result.closeCommand) assertEquals(GlassProbe.Refusal.ONE_DIRECTION_ONLY, result.refusal) - // Rien ne peut refermer la vitre : il faut le dire, pas laisser croire au contraire. + // Nothing can close the window: that must be said, not left to look otherwise. assertFalse(result.restored) } @Test - fun `la voiture qui repart interrompt la sonde`() { + fun `the car moving off interrupts the probe`() { val glass = FakeGlass(openCommand = 5, closeCommand = 2) var checks = 0 - // Arrêtée pour la lecture initiale et la première commande, puis en mouvement. + // Stopped for the first reading and the first command, then moving. val result = probe(glass, stopped = { checks++ < 2 }) assertEquals(GlassProbe.Refusal.NOT_STOPPED, result.refusal) @@ -115,7 +115,7 @@ class GlassProbeTest { } @Test - fun `contact coupe, aucune commande n'est envoyee`() { + fun `ignition off, no command is sent`() { val glass = FakeGlass(openCommand = 5, closeCommand = 2) val result = probe(glass, ignitionRun = { false }) @@ -124,7 +124,7 @@ class GlassProbeTest { } @Test - fun `une position illisible arrete tout`() { + fun `an unreadable position stops everything`() { val glass = FakeGlass(openCommand = 5, closeCommand = 2).apply { readable = false } val result = probe(glass) diff --git a/EVHardware/lib/src/test/java/com/evsuite/hardware/PhysicalButtonEventDecoderTest.kt b/EVHardware/lib/src/test/java/com/evsuite/hardware/PhysicalButtonEventDecoderTest.kt index eb99df4..eb37376 100644 --- a/EVHardware/lib/src/test/java/com/evsuite/hardware/PhysicalButtonEventDecoderTest.kt +++ b/EVHardware/lib/src/test/java/com/evsuite/hardware/PhysicalButtonEventDecoderTest.kt @@ -109,7 +109,7 @@ class PhysicalButtonEventDecoderTest { } @Test fun `three presses give one double and then a single`() { - // Sinon un troisième appui rendrait un second DOUBLE, et la règle partirait deux fois. + // Otherwise a third press would make a second DOUBLE, and the rule would fire twice. val decoder = PhysicalButtonEventDecoder() val presses = listOf(0L, 100L, 200L).map { decoder.accept(17, true, false, atMillis = it) diff --git a/EVHardware/lib/src/test/java/com/evsuite/hardware/catalog/CatalogConsistencyTest.kt b/EVHardware/lib/src/test/java/com/evsuite/hardware/catalog/CatalogConsistencyTest.kt index a16038a..7c0c2f2 100644 --- a/EVHardware/lib/src/test/java/com/evsuite/hardware/catalog/CatalogConsistencyTest.kt +++ b/EVHardware/lib/src/test/java/com/evsuite/hardware/catalog/CatalogConsistencyTest.kt @@ -13,9 +13,8 @@ import org.junit.Assert.assertTrue import org.junit.Test /** - * Garde-fous sur le catalogue. Les erreurs visées ne cassent pas la compilation : elles - * produisent une entrée d'interface qui ne fait rien, ou pire, une écriture véhicule qui - * contourne le verrou de vitesse. + * Guard rails on the catalogue. The mistakes they catch do not break the build: they produce a + * UI entry that does nothing or, worse, a vehicle write that gets round the speed lock. */ class CatalogConsistencyTest { @@ -30,42 +29,42 @@ class CatalogConsistencyTest { } @Test - fun `la destination et le point utilisent des controles distincts`() { - // Un point a un rayon, une destination n'en a pas : deux ValueKind, sinon l'éditeur - // demanderait un rayon pour une adresse. + fun `the destination and the point use separate controls`() { + // A point has a radius, a destination does not: two ValueKinds, or the editor would + // ask for a radius for an address. assertEquals(ValueKind.DESTINATION, ActionType.NAVIGATE_TO.spec.kind) assertEquals(ValueKind.LOCATION, ConditionType.LOCATION_WITHIN.spec.kind) assertEquals(0, ActionType.NAVIGATE_TO.spec.max) } @Test - fun `le sms porte un destinataire et un message`() { - // Le destinataire est un numéro (ou un contact du répertoire PBAP) et le message est - // du texte libre : un seul contrôle ne pourrait pas porter les deux. + fun `an sms carries a recipient and a message`() { + // The recipient is a number (or a contact from the PBAP phone book) and the message is + // free text: a single control could not carry both. assertEquals(ValueKind.SMS, ActionType.SEND_SMS.spec.kind) assertTrue(ActionType.SEND_SMS.spec.hintRes != 0) - assertFalse("le SMS n'écrit rien dans le véhicule", ActionType.SEND_SMS.gated) + assertFalse("SMS writes nothing to the vehicle", ActionType.SEND_SMS.gated) } @Test - fun `la demande de confirmation porte sa question et son delai`() { - // Le texte EST la question posée au conducteur ; un exemple est indispensable, la - // formulation par défaut d'un champ vide n'apprend rien. Le nombre est l'attente en - // secondes, bornée pour qu'une question reste lisible sans bloquer le cycle. + fun `the confirmation request carries its question and its delay`() { + // The text IS the question put to the driver; an example is essential, the default + // wording of an empty field teaches nothing. The number is the wait in seconds, + // bounded so that a question stays readable without blocking the cycle. assertEquals(ValueKind.CONFIRM, ActionType.ASK_CONFIRM.spec.kind) assertTrue(ActionType.ASK_CONFIRM.spec.hintRes != 0) assertTrue(ActionType.ASK_CONFIRM.spec.min > 0) assertTrue(ActionType.ASK_CONFIRM.spec.max > ActionType.ASK_CONFIRM.spec.min) - // Le défaut des règles enregistrées sans délai doit rester dans les bornes offertes. + // The default for rules saved without a delay must stay within the offered bounds. assertTrue(ActionType.ASK_CONFIRM_DEFAULT_SECONDS in ActionType.ASK_CONFIRM.spec.min..ActionType.ASK_CONFIRM.spec.max) - assertFalse("la question ne touche pas le véhicule", ActionType.ASK_CONFIRM.gated) + assertFalse("the question does not touch the vehicle", ActionType.ASK_CONFIRM.gated) } @Test - fun `toute action vehicule cible une action du pont`() { - // bridgeAction=null est réservé aux actions locales (lancer une app, notifier). - // Une action véhicule sans cible serait silencieusement ignorée à l'exécution. + fun `every vehicle action targets a bridge action`() { + // bridgeAction=null is reserved for local actions (launch an app, notify). + // A vehicle action without a target would be silently ignored when run. val localOnly = setOf( ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT, ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM, @@ -75,12 +74,12 @@ class CatalogConsistencyTest { ) ActionType.entries.filterNot { it in localOnly }.forEach { type -> - assertNotNull("${type.name} n'a pas de bridgeAction", type.bridgeAction) + assertNotNull("${type.name} has no bridgeAction", type.bridgeAction) } } @Test - fun `les actions locales ne passent pas par le pont`() { + fun `local actions do not go through the bridge`() { listOf( ActionType.LAUNCH_APP, ActionType.SHOW_NOTIFICATION, ActionType.SPEAK_TEXT, ActionType.NAVIGATE_TO, ActionType.WEBHOOK, ActionType.ASK_CONFIRM, @@ -89,17 +88,17 @@ class CatalogConsistencyTest { ActionType.SET_BLUETOOTH, ActionType.SET_WIFI ).forEach { type -> assertTrue( - "${type.name} ne touche pas le véhicule et ne doit pas avoir de bridgeAction", + "${type.name} does not touch the vehicle and must not have a bridgeAction", type.bridgeAction == null ) } } @Test - fun `les ecritures de comportement routier sont marquees gated`() { - // Le marquage ne remplace PAS le verrou côté EVProfile, qui reste l'autorité. - // Il conditionne l'avertissement affiché à l'utilisateur au moment du choix : - // une action gated non marquée se présenterait comme applicable en roulant. + fun `writes that change driving behaviour are marked gated`() { + // The marking does NOT replace the lock on the EVProfile side, which stays the authority. + // It decides the warning shown to the user when choosing: + // an unmarked gated action would present itself as usable while driving. val mustBeGated = setOf( ActionType.APPLY_PROFILE, ActionType.SET_DRIVE_MODE, ActionType.SET_REGEN_LEVEL, ActionType.SET_ONE_PEDAL, @@ -113,16 +112,16 @@ class CatalogConsistencyTest { ) mustBeGated.forEach { type -> - assertTrue("${type.name} modifie le comportement routier et doit être gated", type.gated) + assertTrue("${type.name} changes driving behaviour and must be gated", type.gated) } } @Test - fun `couper le vehicule n est pas automatisable`() { - // Une règle ne doit jamais pouvoir éteindre la voiture : c'est irréversible pour - // le conducteur, et ça reste réservé à un geste humain dans EVProfile. + fun `switching the vehicle off cannot be automated`() { + // A rule must never be able to switch the car off: that is irreversible for the + // driver, and it stays reserved for a human gesture in EVProfile. assertFalse( - "aucune action du catalogue ne doit couper le véhicule", + "no catalogue action may switch the vehicle off", ActionType.entries.any { it.bridgeAction?.contains("POWER_OFF", ignoreCase = true) == true } @@ -130,9 +129,9 @@ class CatalogConsistencyTest { } @Test - fun `toute condition vehicule lit une cle de l instantane`() { - // Les conditions calculées localement (heure, jour, Bluetooth) n'ont pas de clé ; - // toutes les autres en ont besoin, sinon elles seraient toujours indisponibles. + fun `every vehicle condition reads a snapshot key`() { + // Conditions computed locally (time, day, Bluetooth) have no key; all the others need + // one, or they would always be unavailable. val localOnly = setOf( ConditionType.BT_DEVICE_CONNECTED, ConditionType.ANY_BT_CONNECTED, @@ -142,12 +141,12 @@ class CatalogConsistencyTest { ConditionType.DAY_OF_WEEK, ConditionType.DATE, ConditionType.LOCATION_WITHIN, - // Tiré au sort à l'évaluation : il n'y a rien à lire. + // Drawn at random when evaluated: there is nothing to read. ConditionType.RANDOM_CHANCE ) ConditionType.entries.filterNot { it in localOnly }.forEach { type -> - assertNotNull("${type.name} n'a pas de clé d'instantané", type.snapshotKey) + assertNotNull("${type.name} has no snapshot key", type.snapshotKey) } } @@ -363,32 +362,32 @@ class CatalogConsistencyTest { } @Test - fun `les enumerations ont des options`() { - // Une ENUM sans option produit une liste déroulante vide : l'utilisateur ne peut - // rien choisir et la règle reste inutilisable. + fun `enumerations have options`() { + // An ENUM without options gives an empty drop-down: the user cannot choose anything + // and the rule stays unusable. ConditionType.entries .filter { it.spec.kind == ValueKind.ENUM && it != ConditionType.FIRMWARE_GEN } - .forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) } + .forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) } - // RADIO dessine aussi une liste déroulante — celle des bandes — donc la même règle - // s'applique : sans option, l'action ne peut nommer aucune bande. + // RADIO draws a drop-down too — the bands — so the same rule applies: without + // options, the action cannot name any band. ActionType.entries .filter { it.spec.kind == ValueKind.ENUM || it.spec.kind == ValueKind.RADIO } - .forEach { assertTrue("${it.name} : ENUM sans option", it.spec.options.isNotEmpty()) } + .forEach { assertTrue("${it.name}: ENUM without options", it.spec.options.isNotEmpty()) } } @Test - fun `les bornes numeriques sont coherentes`() { + fun `numeric bounds are consistent`() { val specs = ConditionType.entries.map { it.name to it.spec } + ActionType.entries.map { it.name to it.spec } specs.filter { it.second.kind == ValueKind.NUMBER }.forEach { (name, spec) -> if (spec.max >= 0) { - assertTrue("$name : max doit dépasser min", spec.max > spec.min) + assertTrue("$name: max must exceed min", spec.max > spec.min) } else { - // max = -1 : borne résolue à l'exécution. Le repli doit rester utilisable - // même si le véhicule ne répond pas. - assertTrue("$name : fallbackMax doit dépasser min", spec.fallbackMax > spec.min) + // max = -1: the bound is resolved at run time. The fallback must stay usable + // even if the vehicle does not answer. + assertTrue("$name: fallbackMax must exceed min", spec.fallbackMax > spec.min) } } } diff --git a/SPECS.md b/SPECS.md index 2873d07..143da58 100644 --- a/SPECS.md +++ b/SPECS.md @@ -48,7 +48,8 @@ on the cluster since 2026-10-25). (telemetry, consumption, charging, pairing, settings from the phone) and Grafana 13.2.3 (all dashboard queries return data). - [x] Manifests validated offline (kubeconform, with Traefik CRDs). **Applied by Nils only.** -- [ ] Check Grafana's host in `k8s/ingress.yaml` (placeholder `car-grafana.k8s.stinnesbeck.com`). +- [x] Grafana's host in `k8s/ingress.yaml` is `car-grafana.k8s.stinnesbeck.com`, the one in use + (owner, 2026-10-25). - [x] Screen on the car: 1920×720 px at 160 dpi, app window 1778×720 px (Logs → SCREEN). - [x] Car-colour probe on the car: only `ro.build.mtk2712.vehicle_type = AS28`; the paint colour is not in the system properties. It may be in the VHAL (a debug-build VhalProbe listing @@ -88,9 +89,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * write the file. To run more than one replica, first move Grafana to PostgreSQL or MySQL (`GF_DATABASE_TYPE`, `GF_DATABASE_HOST`, …); the replicas then share that database instead of a volume. The car data itself is in InfluxDB and unaffected. -- [ ] **EVHardware's comments are partly French.** About 120 comment lines in the vendored - `EVHardware/` library (mostly `EVHardware.kt`, some tests), half-translated upstream. The - app, the docs and the build files are English. +- [x] **EVHardware's comments are partly French.** Translated 2026-10-25 (see the done log). - [ ] **Idea: climate energy on the Overview (motor vs climate).** Nothing the app reads splits energy by consumer: battery power (voltage × current) is the total of motor, climate, 12 V and the rest. SAIC's charging service, which EVHardware already binds, has counters that @@ -139,6 +138,15 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-10-25 — EVHardware in English: the French comments (and the half-translated ones like + "l'screen", "registersment", "initialisesd") in `EVHardware.kt` and five test files, the French + log messages ("aucun init() fonctionnel", "introuvable", "state initial", "ERREUR"), the French + test names and assertion messages (`CatalogConsistencyTest`, `GlassEvidenceTest`, + `GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the + diff). Kept on purpose: the French translation `values-fr/strings.xml`, place names in test + data, the quoted French sentence in `ChargeStopPlan` and the voice phrase in `SaicNavGuidance`, + and "Katman" (Turkish for layer), which names the layers in function names and logs — now + explained at the top of `EVHardware.kt`. EVHardware, app tests and lint pass. - 2026-10-25 — README.md brought up to date: the tab screenshots retaken with 2.35.2 on the emulator right after installing (`screenshots/nilsmg4-2.35.2-*.png`, the 2.18.1 ones removed), "read-only" now names the drive mode exception, the upload frequency matches the app (1 min by