EVHardware: rename "Katman" (Turkish for layer) to "Layer"

Function names (whenLayer1Ready, initLayer4Swi69, isLayer4Ready, …), log
lines, comments and the library's README and changelog. Nothing outside
EVHardware used the names. The header now lists all five layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-25 21:58:45 +01:00
co-authored by Claude Opus 5.5
parent 0ff56285bc
commit 8b2acc3e70
7 changed files with 174 additions and 170 deletions
+1 -1
View File
@@ -824,7 +824,7 @@ All notable changes to this project are documented here. Format follows
- **The glass probe refused to run on a car whose ignition was on.** It read - **The glass probe refused to run on a car whose ignition was on.** It read
`getCurrentIgnitionState()`, which answers on the AAOS `IgnitionState` scale, against `getCurrentIgnitionState()`, which answers on the AAOS `IgnitionState` scale, against
`CarIgnitionItem.RUN` — and the two scales do not line up: RUN is 2 for Katman5, where 2 is `CarIgnitionItem.RUN` — and the two scales do not line up: RUN is 2 for Layer5, where 2 is
OFF. Contact mis read as 4 and was refused; contact coupé read as 2 and was let through. OFF. Contact mis read as 4 and was refused; contact coupé read as 2 and was let through.
`setEsc` compared the same two scales and now refuses only a *known* non-RUN ignition, as its `setEsc` compared the same two scales and now refuses only a *known* non-RUN ignition, as its
own documentation always said it did. own documentation always said it did.
+1 -1
View File
@@ -60,7 +60,7 @@ Discover the rest of the suite:
## Overview ## Overview
| Area | Types | | Area | Types |
|---|---| |---|---|
| Vehicle access | `EVHardware` — Katman1/4/5 reflection over `android.car` + SAIC SDK, per-firmware routing | | Vehicle access | `EVHardware` — Layer1/4/5 reflection over `android.car` + SAIC SDK, per-firmware routing |
| Safety | `VehicleWriteGate` (0 km/h, fail-closed) + `@RequiresStandstill` on every gated setter | | Safety | `VehicleWriteGate` (0 km/h, fail-closed) + `@RequiresStandstill` on every gated setter |
| Vendor services | `saic.*` — the AIDL the head unit's own HVAC, charging, radio, hands-free and TTS apps use | | Vendor services | `saic.*` — the AIDL the head unit's own HVAC, charging, radio, hands-free and TTS apps use |
| Radio | `SaicRadio` — play, silence, fail-closed play/pause toggle, FM/AM tune, station stepping (DAB included), screen | | Radio | `SaicRadio` — play, silence, fail-closed play/pause toggle, FM/AM tune, station stepping (DAB included), screen |
@@ -22,10 +22,13 @@ import kotlinx.coroutines.launch
/** /**
* Hardware abstraction layer for MG4 vehicle control. * Hardware abstraction layer for MG4 vehicle control.
* *
* Runtime communication layers ("Katman", Turkish for layer, as the names in this file call them): * Runtime communication layers, numbered as the names in this file use them (Layer1 … Layer5):
* Katman1 — android.car.Car → CarPropertyManager / CarHvacManager (async ServiceConnection) * Layer1 — android.car.Car → CarPropertyManager / CarHvacManager (async ServiceConnection)
* Katman2 — ServiceManager.getService("vehiclesetting") → raw IBinder (often SELinux-blocked) * Layer2 — ServiceManager.getService("vehiclesetting") → raw IBinder (often SELinux-blocked)
* Katman3 — bindService(VehicleService) → IHubService → sub-services (not needed for our use case) * Layer3 — bindService(VehicleService) → IHubService → sub-services (not needed for our use case)
* Layer4 — SAIC's settings SDK in the launcher's context: VehiclePropertyManager (SWI133),
* VehicleSettingManager (SWI68/SWI165), CarVehicleSettingClient via CarAdapterClient (A9)
* Layer5 — ignition state: VehicleConditionManager, or ICarGeneralService via CarAdapterClient (A9)
*/ */
object EVHardware { object EVHardware {
@@ -146,11 +149,11 @@ object EVHardware {
private const val VSM_SERVICE_CODE = 0x8 // queryClient(0x8) → ICarVehicleSettingService private const val VSM_SERVICE_CODE = 0x8 // queryClient(0x8) → ICarVehicleSettingService
private const val VEHICLE_SETTING_PKG = "com.saicmotor.vehiclesetting" // SWI131: carapi inside VS private const val VEHICLE_SETTING_PKG = "com.saicmotor.vehiclesetting" // SWI131: carapi inside VS
// Katman5 — VehicleConditionManager (IVehicleConditionService via IHubService "vehiclecondition") // Layer5 — VehicleConditionManager (IVehicleConditionService via IHubService "vehiclecondition")
private const val VCM_CLASS = "com.saicmotor.sdk.vehiclesettings.manager.VehicleConditionManager" private const val VCM_CLASS = "com.saicmotor.sdk.vehiclesettings.manager.VehicleConditionManager"
private const val VCM_LISTENER_CLASS = "com.saicmotor.sdk.vehiclesettings.IVehicleConditionListener" private const val VCM_LISTENER_CLASS = "com.saicmotor.sdk.vehiclesettings.IVehicleConditionListener"
// Katman5 SWI69/SWI131 — ICarGeneralService via CarAdapterClient (queryClient(0x1)) // Layer5 SWI69/SWI131 — ICarGeneralService via CarAdapterClient (queryClient(0x1))
private const val CAR_GENERAL_CLIENT_CLASS = "com.saicmotor.carapi.client.CarGeneralClient" private const val CAR_GENERAL_CLIENT_CLASS = "com.saicmotor.carapi.client.CarGeneralClient"
private const val BIND_CODE_CAR_GENERAL = 0x1 // ICarAdapterService.queryClient(0x1) private const val BIND_CODE_CAR_GENERAL = 0x1 // ICarAdapterService.queryClient(0x1)
@@ -177,7 +180,7 @@ object EVHardware {
} }
/** /**
* Values returned by IVehicleConditionService.getVehicleIgnition() (Katman5). * Values returned by IVehicleConditionService.getVehicleIgnition() (Layer5).
* Values exposed by the supported SWI133 runtime interface. * Values exposed by the supported SWI133 runtime interface.
*/ */
object CarIgnitionItem { object CarIgnitionItem {
@@ -232,9 +235,9 @@ object EVHardware {
/** Set once the getters above have been looked up, present or absent. */ /** Set once the getters above have been looked up, present or absent. */
@Volatile private var sCpmMethodsResolved = false @Volatile private var sCpmMethodsResolved = false
@Volatile private var sVehicleBinder: IBinder? = null @Volatile private var sVehicleBinder: IBinder? = null
@Volatile private var sVpm: Any? = null // VehiclePropertyManager instance (SWI133, Katman4) @Volatile private var sVpm: Any? = null // VehiclePropertyManager instance (SWI133, Layer4)
@Volatile private var sVpmService: Any? = null // mIVehiclePropertyService field value (SWI133) @Volatile private var sVpmService: Any? = null // mIVehiclePropertyService field value (SWI133)
@Volatile private var sVsm: Any? = null // VehicleSettingManager instance (SWI68, Katman4) @Volatile private var sVsm: Any? = null // VehicleSettingManager instance (SWI68, Layer4)
// The A9 adapter, kept after the VSM client is built: it is the door to every other // The A9 adapter, kept after the VSM client is built: it is the door to every other
// carapi client too — the climate one among them — and nothing else can open it. // carapi client too — the climate one among them — and nothing else can open it.
@Volatile private var sCarAdapter: Any? = null @Volatile private var sCarAdapter: Any? = null
@@ -253,49 +256,49 @@ object EVHardware {
@Volatile private var sDriveModeListener: DriveModeListener? = null @Volatile private var sDriveModeListener: DriveModeListener? = null
@Volatile private var sHvacListener: HvacListener? = null @Volatile private var sHvacListener: HvacListener? = null
// ── Katman5 — IGNITION_STATE via CarPropertyManager (standard AAOS) ────── // ── Layer5 — IGNITION_STATE via CarPropertyManager (standard AAOS) ──────
@Volatile private var sIgnitionCallbackProxy: Any? = null @Volatile private var sIgnitionCallbackProxy: Any? = null
@Volatile private var sIgnitionCallbackRegistered = false @Volatile private var sIgnitionCallbackRegistered = false
private val ignitionCallbacks = java.util.concurrent.CopyOnWriteArrayList<(Int) -> Unit>() private val ignitionCallbacks = java.util.concurrent.CopyOnWriteArrayList<(Int) -> Unit>()
// ── Katman5 — VehicleConditionManager / ICarGeneralService ─────────────── // ── Layer5 — VehicleConditionManager / ICarGeneralService ───────────────
@Volatile private var sVcm: Any? = null @Volatile private var sVcm: Any? = null
@Volatile private var sVcmListener: Any? = null @Volatile private var sVcmListener: Any? = null
@Volatile private var sVcmCallbackRegistered = false @Volatile private var sVcmCallbackRegistered = false
@Volatile private var sLastVcmIgnitionState = -1 // filters repeated false RUN events @Volatile private var sLastVcmIgnitionState = -1 // filters repeated false RUN events
private val vehicleConditionCallbacks = java.util.concurrent.CopyOnWriteArrayList<(Int) -> Unit>() private val vehicleConditionCallbacks = java.util.concurrent.CopyOnWriteArrayList<(Int) -> Unit>()
private val katman5ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>() private val layer5ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>()
/** Listeners notified as soon as Katman1 (CPM + HVAC) is operational. */ /** Listeners notified as soon as Layer1 (CPM + HVAC) is operational. */
private val katman1ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>() private val layer1ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>()
/** Listeners notified as soon as Katman4 (mIVehiclePropertyService) is operational. */ /** Listeners notified as soon as Layer4 (mIVehiclePropertyService) is operational. */
private val katman4ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>() private val layer4ReadyListeners = java.util.concurrent.CopyOnWriteArrayList<() -> Unit>()
/** /**
* Runs [action] as soon as CarPropertyManager and CarHvacManager are available. * Runs [action] as soon as CarPropertyManager and CarHvacManager are available.
* If already ready, runs immediately. Otherwise queued and triggered on connection. * If already ready, runs immediately. Otherwise queued and triggered on connection.
*/ */
fun whenKatman1Ready(action: () -> Unit) { fun whenLayer1Ready(action: () -> Unit) {
if (sCarPropertyManager != null && sCarHvacManager != null) { if (sCarPropertyManager != null && sCarHvacManager != null) {
action() action()
} else { } else {
katman1ReadyListeners.add(action) layer1ReadyListeners.add(action)
} }
} }
/** /**
* Runs [action] as soon as the ADAS service (Katman4) is available. * Runs [action] as soon as the ADAS service (Layer4) is available.
* SWI133 → mIVehiclePropertyService ; SWI68/SWI69/SWI131 → mVehicleSettingService * SWI133 → mIVehiclePropertyService ; SWI68/SWI69/SWI131 → mVehicleSettingService
*/ */
fun whenKatman4Ready(action: () -> Unit) { fun whenLayer4Ready(action: () -> Unit) {
val ready = if (FirmwareInfo.isVsmBased()) sVsmService != null else sVpmService != null val ready = if (FirmwareInfo.isVsmBased()) sVsmService != null else sVpmService != null
if (ready) action() else katman4ReadyListeners.add(action) if (ready) action() else layer4ReadyListeners.add(action)
} }
/** Runs [action] as soon as Katman5 (IVehicleConditionService) is operational. */ /** Runs [action] as soon as Layer5 (IVehicleConditionService) is operational. */
fun whenKatman5Ready(action: () -> Unit) { fun whenLayer5Ready(action: () -> Unit) {
if (sVcmCallbackRegistered) action() else katman5ReadyListeners.add(action) if (sVcmCallbackRegistered) action() else layer5ReadyListeners.add(action)
} }
/** Registers a callback invoked on every ignition state change (CarIgnitionItem). */ /** Registers a callback invoked on every ignition state change (CarIgnitionItem). */
@@ -338,12 +341,12 @@ object EVHardware {
* Current ignition state, as [IgnitionState] (AAOS) values. * Current ignition state, as [IgnitionState] (AAOS) values.
* *
* CarPropertyManager first. But SWI68 does not expose IGNITION_STATE through CPM: the * CarPropertyManager first. But SWI68 does not expose IGNITION_STATE through CPM: the
* property comes back empty, and the ignition state looked unreadable although Katman5 * property comes back empty, and the ignition state looked unreadable although Layer5
* reports it correctly — the log shows it (`Katman5: initial state = 2 (RUN/READY)`) while * reports it correctly — the log shows it (`Layer5: initial state = 2 (RUN/READY)`) while
* the diagnostics classed IGNITION_STATE as NOT_READABLE. * the diagnostics classed IGNITION_STATE as NOT_READABLE.
* *
* Hence the fallback to the last state delivered by IVehicleConditionService, converted: * Hence the fallback to the last state delivered by IVehicleConditionService, converted:
* Katman5 speaks [CarIgnitionItem], this function returns [IgnitionState], and the two * Layer5 speaks [CarIgnitionItem], this function returns [IgnitionState], and the two
* scales do not line up (RUN is 2 in one, OFF is 2 in the other). * scales do not line up (RUN is 2 in one, OFF is 2 in the other).
* *
* Returns -1 if nothing is readable, 0 if the property exists but says nothing. * Returns -1 if nothing is readable, 0 if the property exists but says nothing.
@@ -356,7 +359,7 @@ object EVHardware {
return vcmIgnitionAsAaos() ?: global return vcmIgnitionAsAaos() ?: global
} }
/** The last Katman5 state translated into [IgnitionState], or null if there was none. */ /** The last Layer5 state translated into [IgnitionState], or null if there was none. */
private fun vcmIgnitionAsAaos(): Int? = when (sLastVcmIgnitionState) { private fun vcmIgnitionAsAaos(): Int? = when (sLastVcmIgnitionState) {
CarIgnitionItem.OFF -> IgnitionState.OFF CarIgnitionItem.OFF -> IgnitionState.OFF
CarIgnitionItem.ACCESSORY -> IgnitionState.ACC CarIgnitionItem.ACCESSORY -> IgnitionState.ACC
@@ -909,22 +912,22 @@ object EVHardware {
bindCarService(context) bindCarService(context)
sVehicleBinder = getBinderService("vehiclesetting") sVehicleBinder = getBinderService("vehiclesetting")
when { when {
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 -> initKatman4Swi68(context) FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI68 -> initLayer4Swi68(context)
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI165 -> initKatman4Swi68(context) // same SDK as SWI68 FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI165 -> initLayer4Swi68(context) // same SDK as SWI68
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> initKatman4Swi69(context) // CarVehicleSettingClient, same path as SWI69 FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> initLayer4Swi69(context) // CarVehicleSettingClient, same path as SWI69
FirmwareInfo.isNewGenVsm() -> initKatman4Swi69(context) // SWI69 + SWI131 FirmwareInfo.isNewGenVsm() -> initLayer4Swi69(context) // SWI69 + SWI131
else -> initKatman4(context) else -> initLayer4(context)
} }
// Katman5 — IGNITION_STATE push detection (VehicleConditionManager or ICarGeneralService) // Layer5 — IGNITION_STATE push detection (VehicleConditionManager or ICarGeneralService)
// SWI132 uses the same observed ICarGeneralService path as SWI69/SWI131. // SWI132 uses the same observed ICarGeneralService path as SWI69/SWI131.
if (FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132) if (FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132)
initKatman5Swi69(context) initLayer5Swi69(context)
else else
initKatman5(context) initLayer5(context)
if (sVehicleBinder != null) if (sVehicleBinder != null)
AppLogger.i(TAG, " ✓ Katman2: vehiclesetting binder OK") AppLogger.i(TAG, " ✓ Layer2: vehiclesetting binder OK")
else else
AppLogger.w(TAG, " ✗ Katman2: vehiclesetting null (SELinux — expected)") AppLogger.w(TAG, " ✗ Layer2: vehiclesetting null (SELinux — expected)")
// Start the door watcher at boot if the feature is enabled (all firmwares). // Start the door watcher at boot if the feature is enabled (all firmwares).
startDoorWatcherIfEnabled() startDoorWatcherIfEnabled()
@@ -935,7 +938,7 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman1 — android.car.Car (async, mirrors original bindCarService exactly) // Layer1 — android.car.Car (async, mirrors original bindCarService exactly)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun bindCarService(context: Context) { private fun bindCarService(context: Context) {
@@ -944,12 +947,12 @@ object EVHardware {
val carClass: Class<*> val carClass: Class<*>
try { try {
carClass = Class.forName("android.car.Car") carClass = Class.forName("android.car.Car")
AppLogger.i(TAG, " Katman1: android.car.Car class found ✓") AppLogger.i(TAG, " Layer1: android.car.Car class found ✓")
} catch (e: ClassNotFoundException) { } catch (e: ClassNotFoundException) {
AppLogger.w(TAG, " Katman1: android.car.Car not found — not Automotive?") AppLogger.w(TAG, " Layer1: android.car.Car not found — not Automotive?")
return return
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.e(TAG, " Katman1: forName error: ${e.message}") AppLogger.e(TAG, " Layer1: forName error: ${e.message}")
return return
} }
@@ -958,7 +961,7 @@ object EVHardware {
// Attempt 1: createCar(Context) // Attempt 1: createCar(Context)
try { try {
car = carClass.getMethod("createCar", Context::class.java).invoke(null, context) car = carClass.getMethod("createCar", Context::class.java).invoke(null, context)
if (car != null) AppLogger.i(TAG, " Katman1: createCar(Context) → success") if (car != null) AppLogger.i(TAG, " Layer1: createCar(Context) → success")
} catch (_: Exception) {} } catch (_: Exception) {}
// Attempt 2: createCar(Context, Handler) // Attempt 2: createCar(Context, Handler)
@@ -966,7 +969,7 @@ object EVHardware {
try { try {
car = carClass.getMethod("createCar", Context::class.java, Handler::class.java) car = carClass.getMethod("createCar", Context::class.java, Handler::class.java)
.invoke(null, context, null) .invoke(null, context, null)
if (car != null) AppLogger.i(TAG, " Katman1: createCar(Context, Handler) → success") if (car != null) AppLogger.i(TAG, " Layer1: createCar(Context, Handler) → success")
} catch (_: Exception) {} } catch (_: Exception) {}
} }
@@ -974,18 +977,18 @@ object EVHardware {
var scMethodFound: java.lang.reflect.Method? = null var scMethodFound: java.lang.reflect.Method? = null
try { try {
scMethodFound = carClass.getMethod("createCar", Context::class.java, ServiceConnection::class.java) scMethodFound = carClass.getMethod("createCar", Context::class.java, ServiceConnection::class.java)
AppLogger.i(TAG, " Katman1: createCar(Context, ServiceConnection) method found") AppLogger.i(TAG, " Layer1: createCar(Context, ServiceConnection) method found")
} catch (_: Exception) {} } catch (_: Exception) {}
if (car == null && scMethodFound != null) { if (car == null && scMethodFound != null) {
try { try {
val sc = object : ServiceConnection { val sc = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
AppLogger.i(TAG, " Katman1: ServiceConnection.onServiceConnected") AppLogger.i(TAG, " Layer1: ServiceConnection.onServiceConnected")
tryGetManagersFromCar(carClass) tryGetManagersFromCar(carClass)
} }
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
AppLogger.w(TAG, " Katman1: Car service disconnected") AppLogger.w(TAG, " Layer1: Car service disconnected")
sCarPropertyManager = null sCarPropertyManager = null
sCarHvacManager = null sCarHvacManager = null
clearCarPropertyMethods() clearCarPropertyMethods()
@@ -993,14 +996,14 @@ object EVHardware {
} }
} }
car = scMethodFound.invoke(null, context, sc) car = scMethodFound.invoke(null, context, sc)
if (car != null) AppLogger.i(TAG, " Katman1: createCar(Context, SC) → callback pending") if (car != null) AppLogger.i(TAG, " Layer1: createCar(Context, SC) → callback pending")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman1: createCar(Context, SC) error: ${e.message}") AppLogger.w(TAG, " Layer1: createCar(Context, SC) error: ${e.message}")
} }
} }
if (car == null) { if (car == null) {
AppLogger.e(TAG, " Katman1: all createCar methods failed") AppLogger.e(TAG, " Layer1: all createCar methods failed")
// Not necessarily fatal: on a cold head unit the Car service is simply not up // Not necessarily fatal: on a cold head unit the Car service is simply not up
// yet. Giving up here would leave the whole process without a vehicle forever. // yet. Giving up here would leave the whole process without a vehicle forever.
armCarServiceWatchdog(context, carClass) armCarServiceWatchdog(context, carClass)
@@ -1012,11 +1015,11 @@ object EVHardware {
// Call car.connect() if available (required on older builds) // Call car.connect() if available (required on older builds)
try { try {
carClass.getMethod("connect").invoke(car) carClass.getMethod("connect").invoke(car)
AppLogger.i(TAG, " Katman1: car.connect() called") AppLogger.i(TAG, " Layer1: car.connect() called")
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
// connect() not present on all builds, ignore // connect() not present on all builds, ignore
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman1: car.connect() error: ${e.message}") AppLogger.w(TAG, " Layer1: car.connect() error: ${e.message}")
} }
// Try sync managers immediately // Try sync managers immediately
@@ -1075,9 +1078,9 @@ object EVHardware {
(carClass.getMethod("isConnected").invoke(car) as? Boolean) ?: true (carClass.getMethod("isConnected").invoke(car) as? Boolean) ?: true
} catch (_: Exception) { true } } catch (_: Exception) { true }
AppLogger.i(TAG, " Katman1: isConnected() → $connected") AppLogger.i(TAG, " Layer1: isConnected() → $connected")
if (!connected) { if (!connected) {
AppLogger.w(TAG, " Katman1: car not yet connected") AppLogger.w(TAG, " Layer1: car not yet connected")
return return
} }
@@ -1088,9 +1091,9 @@ object EVHardware {
val svc = carClass.getField("PROPERTY_SERVICE").get(null) as String val svc = carClass.getField("PROPERTY_SERVICE").get(null) as String
sCarPropertyManager = getCarManager.invoke(car, svc) sCarPropertyManager = getCarManager.invoke(car, svc)
cacheCarPropertyMethods(sCarPropertyManager) cacheCarPropertyMethods(sCarPropertyManager)
AppLogger.i(TAG, " Katman1: CarPropertyManager READY ✓") AppLogger.i(TAG, " Layer1: CarPropertyManager READY ✓")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman1: CarPropertyManager unavailable: ${e.message}") AppLogger.w(TAG, " Layer1: CarPropertyManager unavailable: ${e.message}")
} }
} }
@@ -1098,28 +1101,28 @@ object EVHardware {
try { try {
val svc = carClass.getField("HVAC_SERVICE").get(null) as String val svc = carClass.getField("HVAC_SERVICE").get(null) as String
sCarHvacManager = getCarManager.invoke(car, svc) sCarHvacManager = getCarManager.invoke(car, svc)
AppLogger.i(TAG, " Katman1: CarHvacManager READY ✓") AppLogger.i(TAG, " Layer1: CarHvacManager READY ✓")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman1: CarHvacManager unavailable: ${e.message}") AppLogger.w(TAG, " Layer1: CarHvacManager unavailable: ${e.message}")
} }
} }
// Notify whenKatman1Ready subscribers as soon as both managers are ready // Notify whenLayer1Ready subscribers as soon as both managers are ready
if (sCarPropertyManager != null && sCarHvacManager != null && katman1ReadyListeners.isNotEmpty()) { if (sCarPropertyManager != null && sCarHvacManager != null && layer1ReadyListeners.isNotEmpty()) {
val toNotify = katman1ReadyListeners.toList() val toNotify = layer1ReadyListeners.toList()
katman1ReadyListeners.clear() layer1ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
} }
// Try to register the IGNITION_STATE callback (best effort) // Try to register the IGNITION_STATE callback (best effort)
if (sCarPropertyManager != null) registerIgnitionPropertyCallback() if (sCarPropertyManager != null) registerIgnitionPropertyCallback()
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.e(TAG, " Katman1: tryGetManagersFromCar error: ${e.message}") AppLogger.e(TAG, " Layer1: tryGetManagersFromCar error: ${e.message}")
} }
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman2 — ServiceManager raw binder // Layer2 — ServiceManager raw binder
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun getBinderService(serviceName: String): IBinder? { private fun getBinderService(serviceName: String): IBinder? {
@@ -1136,10 +1139,10 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman4 — VehiclePropertyManager via saicmotor.hmi.launcher context // Layer4 — VehiclePropertyManager via saicmotor.hmi.launcher context
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun initKatman4(context: Context) { private fun initLayer4(context: Context) {
if (sVpm != null) return if (sVpm != null) return
val launcherCtx: Context val launcherCtx: Context
val vpmClass: Class<*> val vpmClass: Class<*>
@@ -1151,8 +1154,8 @@ object EVHardware {
vpmClass = launcherCtx.classLoader vpmClass = launcherCtx.classLoader
.loadClass("com.saicmotor.sdk.vehiclesettings.manager.VehiclePropertyManager") .loadClass("com.saicmotor.sdk.vehiclesettings.manager.VehiclePropertyManager")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman4: package/class error: ${e.message} — will retry") AppLogger.w(TAG, " Layer4: package/class error: ${e.message} — will retry")
Handler(Looper.getMainLooper()).postDelayed({ initKatman4(context.applicationContext) }, 5_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer4(context.applicationContext) }, 5_000)
return return
} }
@@ -1184,8 +1187,8 @@ object EVHardware {
} }
if (vpm == null) { if (vpm == null) {
AppLogger.w(TAG, " Katman4: all attempts failed — will retry") AppLogger.w(TAG, " Layer4: all attempts failed — will retry")
Handler(Looper.getMainLooper()).postDelayed({ initKatman4(context.applicationContext) }, 10_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer4(context.applicationContext) }, 10_000)
return return
} }
@@ -1221,7 +1224,7 @@ object EVHardware {
} }
tryGetVpmService(vpm!!) tryGetVpmService(vpm!!)
AppLogger.i(TAG, " Katman4: VPM ready — mIVehiclePropertyService=${if (sVpmService != null) "OK ✓" else "null (en attente)"}") AppLogger.i(TAG, " Layer4: VPM ready — mIVehiclePropertyService=${if (sVpmService != null) "OK ✓" else "null (en attente)"}")
} }
private fun initWithServiceListener(vpm: Any, context: Context, launcherCtx: Context) { private fun initWithServiceListener(vpm: Any, context: Context, launcherCtx: Context) {
@@ -1231,7 +1234,7 @@ object EVHardware {
val methodSummary = vpmClass.methods.joinToString(", ") { m -> val methodSummary = vpmClass.methods.joinToString(", ") { m ->
"${m.name}(${m.parameterTypes.joinToString(",") { it.simpleName }})" "${m.name}(${m.parameterTypes.joinToString(",") { it.simpleName }})"
} }
AppLogger.d(TAG, " Katman4: VPM methods = $methodSummary") AppLogger.d(TAG, " Layer4: VPM methods = $methodSummary")
// Strategy 1: Inspect the actual init() signature to get the real listener type // Strategy 1: Inspect the actual init() signature to get the real listener type
val initMethod2 = vpmClass.methods.firstOrNull { m -> val initMethod2 = vpmClass.methods.firstOrNull { m ->
@@ -1241,7 +1244,7 @@ object EVHardware {
if (initMethod2 != null) { if (initMethod2 != null) {
val listenerType = initMethod2.parameterTypes[1] val listenerType = initMethod2.parameterTypes[1]
AppLogger.i(TAG, " Katman4: init() found, listener type = ${listenerType.name}") AppLogger.i(TAG, " Layer4: init() found, listener type = ${listenerType.name}")
// Try dynamic proxy with the actual listener interface type // Try dynamic proxy with the actual listener interface type
if (listenerType.isInterface) { if (listenerType.isInterface) {
@@ -1251,11 +1254,11 @@ object EVHardware {
) { _, method, _ -> ) { _, method, _ ->
when (method.name) { when (method.name) {
"onServiceConnected" -> { "onServiceConnected" -> {
AppLogger.i(TAG, " Katman4: onServiceConnected ✓") AppLogger.i(TAG, " Layer4: onServiceConnected ✓")
tryGetVpmService(vpm) tryGetVpmService(vpm)
} }
"onServiceDisconnected" -> { "onServiceDisconnected" -> {
AppLogger.w(TAG, " Katman4: onServiceDisconnected") AppLogger.w(TAG, " Layer4: onServiceDisconnected")
sVpmService = null sVpmService = null
} }
else -> {} else -> {}
@@ -1263,53 +1266,53 @@ object EVHardware {
null null
} }
initMethod2.invoke(vpm, context, proxy) initMethod2.invoke(vpm, context, proxy)
AppLogger.i(TAG, " Katman4: init(Context, proxy) ✓") AppLogger.i(TAG, " Layer4: init(Context, proxy) ✓")
return return
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: init(Context, proxy) failed: ${e.message}") AppLogger.d(TAG, " Layer4: init(Context, proxy) failed: ${e.message}")
} }
} }
// Fallback: try init(Context, null) — works if listener is nullable // Fallback: try init(Context, null) — works if listener is nullable
try { try {
initMethod2.invoke(vpm, context, null) initMethod2.invoke(vpm, context, null)
AppLogger.i(TAG, " Katman4: init(Context, null) ✓") AppLogger.i(TAG, " Layer4: init(Context, null) ✓")
return return
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: init(Context, null) failed: ${e.message}") AppLogger.d(TAG, " Layer4: init(Context, null) failed: ${e.message}")
} }
} }
// Strategy 2: init(Context) single-param // Strategy 2: init(Context) single-param
try { try {
vpmClass.getMethod("init", Context::class.java).invoke(vpm, context) vpmClass.getMethod("init", Context::class.java).invoke(vpm, context)
AppLogger.i(TAG, " Katman4: init(Context) ✓") AppLogger.i(TAG, " Layer4: init(Context) ✓")
return return
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: init(Context) failed: ${e.message}") AppLogger.d(TAG, " Layer4: init(Context) failed: ${e.message}")
} }
// Strategy 3: init() no-arg // Strategy 3: init() no-arg
try { try {
vpmClass.getMethod("init").invoke(vpm) vpmClass.getMethod("init").invoke(vpm)
AppLogger.i(TAG, " Katman4: init() ✓") AppLogger.i(TAG, " Layer4: init() ✓")
return return
} catch (_: NoSuchMethodException) { } catch (_: NoSuchMethodException) {
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: init() failed: ${e.message}") AppLogger.d(TAG, " Layer4: init() failed: ${e.message}")
} }
AppLogger.w(TAG, " Katman4: no working init() — mIVehiclePropertyService stays null") AppLogger.w(TAG, " Layer4: no working init() — mIVehiclePropertyService stays null")
} }
/** Runs [block], returns the result or null, logs the result/error. */ /** Runs [block], returns the result or null, logs the result/error. */
private fun tryInvoke(label: String, block: () -> Any?): Any? = try { private fun tryInvoke(label: String, block: () -> Any?): Any? = try {
val r = block() val r = block()
AppLogger.i(TAG, " Katman4: $label → ${if (r != null) "OK ($r)" else "null"}") AppLogger.i(TAG, " Layer4: $label → ${if (r != null) "OK ($r)" else "null"}")
r r
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: $label → ${e.javaClass.simpleName}: ${e.message}") AppLogger.d(TAG, " Layer4: $label → ${e.javaClass.simpleName}: ${e.message}")
null null
} }
@@ -1322,10 +1325,10 @@ object EVHardware {
val svc = f.get(vpm) val svc = f.get(vpm)
if (svc != null) { if (svc != null) {
sVpmService = svc sVpmService = svc
AppLogger.i(TAG, " Katman4: mIVehiclePropertyService READY ✓") AppLogger.i(TAG, " Layer4: mIVehiclePropertyService READY ✓")
// Notify any pending Katman4 listeners // Notify any pending Layer4 listeners
val toNotify = katman4ReadyListeners.toList() val toNotify = layer4ReadyListeners.toList()
katman4ReadyListeners.clear() layer4ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
} }
return return
@@ -1655,10 +1658,10 @@ object EVHardware {
} }
} }
// Katman4 SWI68 — VehicleSettingManager via saicmotor.hmi.launcher context // Layer4 SWI68 — VehicleSettingManager via saicmotor.hmi.launcher context
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun initKatman4Swi68(context: Context) { private fun initLayer4Swi68(context: Context) {
if (sVsm != null) return if (sVsm != null) return
val launcherCtx: Context val launcherCtx: Context
val vsmClass: Class<*> val vsmClass: Class<*>
@@ -1671,7 +1674,7 @@ object EVHardware {
AppLogger.i(TAG, " SWI68: VehicleSettingManager class found ✓") AppLogger.i(TAG, " SWI68: VehicleSettingManager class found ✓")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " SWI68: class load error: ${e.message} — retry in 5s") AppLogger.w(TAG, " SWI68: class load error: ${e.message} — retry in 5s")
Handler(Looper.getMainLooper()).postDelayed({ initKatman4Swi68(context.applicationContext) }, 5_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer4Swi68(context.applicationContext) }, 5_000)
return return
} }
@@ -1757,8 +1760,8 @@ object EVHardware {
if (svc != null) { if (svc != null) {
sVsmService = svc sVsmService = svc
AppLogger.i(TAG, " SWI68: mVehicleSettingService READY ✓") AppLogger.i(TAG, " SWI68: mVehicleSettingService READY ✓")
val toNotify = katman4ReadyListeners.toList() val toNotify = layer4ReadyListeners.toList()
katman4ReadyListeners.clear() layer4ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
} }
return return
@@ -1767,7 +1770,7 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman4 SWI69/SWI131 — CarVehicleSettingClient via CarAdapterClient // Layer4 SWI69/SWI131 — CarVehicleSettingClient via CarAdapterClient
// //
// Runtime integration contract: // Runtime integration contract:
// CarAdapterClient.getInstance(ctx).start() // CarAdapterClient.getInstance(ctx).start()
@@ -1779,7 +1782,7 @@ object EVHardware {
// (getAccTjaState, setAccTjaState, getLasWarningSound, getFcwState, etc.) // (getAccTjaState, setAccTjaState, getLasWarningSound, getFcwState, etc.)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun initKatman4Swi69(context: Context) { private fun initLayer4Swi69(context: Context) {
if (sVsm != null) return if (sVsm != null) return
val launcherCtx: Context val launcherCtx: Context
@@ -1795,7 +1798,7 @@ object EVHardware {
AppLogger.i(TAG, " SWI69: CarAdapterClient + CarVehicleSettingClient classes found ✓") AppLogger.i(TAG, " SWI69: CarAdapterClient + CarVehicleSettingClient classes found ✓")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " SWI69: class load error: ${e.message} — retry in 5s") AppLogger.w(TAG, " SWI69: class load error: ${e.message} — retry in 5s")
Handler(Looper.getMainLooper()).postDelayed({ initKatman4Swi69(context.applicationContext) }, 5_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer4Swi69(context.applicationContext) }, 5_000)
return return
} }
@@ -1808,7 +1811,7 @@ object EVHardware {
if (adapter == null) { if (adapter == null) {
AppLogger.w(TAG, " SWI69: CarAdapterClient.getInstance() failed — retry in 10s") AppLogger.w(TAG, " SWI69: CarAdapterClient.getInstance() failed — retry in 10s")
Handler(Looper.getMainLooper()).postDelayed({ initKatman4Swi69(context.applicationContext) }, 10_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer4Swi69(context.applicationContext) }, 10_000)
return return
} }
@@ -1878,8 +1881,8 @@ object EVHardware {
sVsmService = ibinder sVsmService = ibinder
AppLogger.i(TAG, " SWI69: CarVehicleSettingClient READY ✓") AppLogger.i(TAG, " SWI69: CarVehicleSettingClient READY ✓")
val toNotify = katman4ReadyListeners.toList() val toNotify = layer4ReadyListeners.toList()
katman4ReadyListeners.clear() layer4ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " SWI69: tryInitClientFromAdapter error: ${e.message}") AppLogger.d(TAG, " SWI69: tryInitClientFromAdapter error: ${e.message}")
@@ -1892,7 +1895,7 @@ object EVHardware {
* [a9ClientBinder] returns null for two entirely different reasons — the code is not served, * [a9ClientBinder] returns null for two entirely different reasons — the code is not served,
* or there is no adapter to ask — and a survey that cannot tell them apart records "absent" * or there is no adapter to ask — and a survey that cannot tell them apart records "absent"
* for a question that was never put to the vehicle. `sCarAdapter` is assigned only by * for a question that was never put to the vehicle. `sCarAdapter` is assigned only by
* `initKatman4Swi69`, so on SWI68 every `queryClient` in a probe fails before it reaches a * `initLayer4Swi69`, so on SWI68 every `queryClient` in a probe fails before it reaches a
* binder. RI-002 was nearly closed on that false negative; the probe now says so out loud. * binder. RI-002 was nearly closed on that false negative; the probe now says so out loud.
*/ */
internal fun hasCarAdapterClient(): Boolean = sCarAdapter != null && sCarAdapterClass != null internal fun hasCarAdapterClient(): Boolean = sCarAdapter != null && sCarAdapterClass != null
@@ -2185,11 +2188,11 @@ object EVHardware {
when (result) { when (result) {
is Int -> result is Int -> result
is Number -> result.toInt() is Number -> result.toInt()
null -> { AppLogger.d(TAG, " Katman4: getMixProperty(0x${propId.toString(16)}) = null"); -1 } null -> { AppLogger.d(TAG, " Layer4: getMixProperty(0x${propId.toString(16)}) = null"); -1 }
else -> { AppLogger.d(TAG, " Katman4: getMixProperty(0x${propId.toString(16)}) = $result (${result.javaClass.simpleName})"); -1 } else -> { AppLogger.d(TAG, " Layer4: getMixProperty(0x${propId.toString(16)}) = $result (${result.javaClass.simpleName})"); -1 }
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: getMixProperty(0x${propId.toString(16)}) exc: ${e.message}") AppLogger.d(TAG, " Layer4: getMixProperty(0x${propId.toString(16)}) exc: ${e.message}")
-1 -1
} }
} }
@@ -2201,10 +2204,10 @@ object EVHardware {
vpm.javaClass vpm.javaClass
.getMethod("setMixProperty", Class::class.java, Int::class.java, Any::class.java) .getMethod("setMixProperty", Class::class.java, Int::class.java, Any::class.java)
.invoke(vpm, Int::class.javaObjectType, propId, value) .invoke(vpm, Int::class.javaObjectType, propId, value)
AppLogger.i(TAG, " Katman4: setMixProperty(0x${propId.toString(16)}, $value) ✓") AppLogger.i(TAG, " Layer4: setMixProperty(0x${propId.toString(16)}, $value) ✓")
true true
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman4: setMixProperty(0x${propId.toString(16)}, $value) exc: ${e.message}") AppLogger.d(TAG, " Layer4: setMixProperty(0x${propId.toString(16)}, $value) exc: ${e.message}")
false false
} }
} }
@@ -2359,7 +2362,7 @@ object EVHardware {
} }
/** /**
* SAIC proprietary binder transact (Katman2 fallback). * SAIC proprietary binder transact (Layer2 fallback).
* Runtime parcel contract: [interfaceToken, AREA_GLOBAL, 1, value, float[], byte[]] * Runtime parcel contract: [interfaceToken, AREA_GLOBAL, 1, value, float[], byte[]]
*/ */
private fun binderTransact(binder: IBinder?, descriptor: String, txCode: Int, value: Int): Boolean { private fun binderTransact(binder: IBinder?, descriptor: String, txCode: Int, value: Int): Boolean {
@@ -2532,7 +2535,7 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// ADAS API (Katman4) // ADAS API (Layer4)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
fun overspeedAlarmOnOrNull(): Boolean? { fun overspeedAlarmOnOrNull(): Boolean? {
@@ -2603,7 +2606,7 @@ object EVHardware {
return setIntPropertyVpm(PROP_SPEED_LIMIT_TONE, if (on) 1 else 0) return setIntPropertyVpm(PROP_SPEED_LIMIT_TONE, if (on) 1 else 0)
} }
/** Returns 0–4 (Off/Limiter/Auto/ACC/ICA), or -1 if Katman4 not ready. */ /** Returns 0–4 (Off/Limiter/Auto/ACC/ICA), or -1 if Layer4 not ready. */
fun getMixedIntelligentDrive(): Int = getMixIntProperty(PROP_MIX_INTELLIGENT_DRIVE) fun getMixedIntelligentDrive(): Int = getMixIntProperty(PROP_MIX_INTELLIGENT_DRIVE)
@RequiresStandstill @RequiresStandstill
fun setMixedIntelligentDrive(value: Int): Boolean { fun setMixedIntelligentDrive(value: Int): Boolean {
@@ -3161,7 +3164,7 @@ object EVHardware {
if (!hasDrowsinessAndEsc()) return false if (!hasDrowsinessAndEsc()) return false
// [IgnitionState], not [CarIgnitionItem]: getCurrentIgnitionState() answers on the // [IgnitionState], not [CarIgnitionItem]: getCurrentIgnitionState() answers on the
// AAOS scale, where the two do not line up — RUN is 2 for Katman5 and OFF is 2 here. // AAOS scale, where the two do not line up — RUN is 2 for Layer5 and OFF is 2 here.
// Unreadable is 0 or -1 and is deliberately not a refusal, per the KDoc above. // Unreadable is 0 or -1 and is deliberately not a refusal, per the KDoc above.
val ignition = getCurrentIgnitionState() val ignition = getCurrentIgnitionState()
if (ignition > 0 && ignition != IgnitionState.ON) { if (ignition > 0 && ignition != IgnitionState.ON) {
@@ -3439,10 +3442,10 @@ object EVHardware {
} }
} }
fun isKatman4Ready(): Boolean = fun isLayer4Ready(): Boolean =
if (FirmwareInfo.isVsmBased()) sVsm != null && sVsmService != null if (FirmwareInfo.isVsmBased()) sVsm != null && sVsmService != null
else sVpm != null && sVpmService != null else sVpm != null && sVpmService != null
fun isKatman4VpmCreated(): Boolean = sVpm != null || sVsm != null fun isLayer4VpmCreated(): Boolean = sVpm != null || sVsm != null
fun isCarPropertyManagerReady(): Boolean = sCarPropertyManager != null fun isCarPropertyManagerReady(): Boolean = sCarPropertyManager != null
fun isCarHvacManagerReady(): Boolean = sCarHvacManager != null fun isCarHvacManagerReady(): Boolean = sCarHvacManager != null
@@ -3540,7 +3543,7 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman5 SWI69/SWI131 — ICarGeneralService via CarAdapterClient (queryClient(0x1)) // Layer5 SWI69/SWI131 — ICarGeneralService via CarAdapterClient (queryClient(0x1))
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private data class Swi69Ctx( private data class Swi69Ctx(
@@ -3566,16 +3569,16 @@ object EVHardware {
return null return null
} }
private fun initKatman5Swi69(context: Context) { private fun initLayer5Swi69(context: Context) {
if (sVcmCallbackRegistered) return if (sVcmCallbackRegistered) return
val classes = findSwi69Classes(context) ?: run { val classes = findSwi69Classes(context) ?: run {
AppLogger.w(TAG, " Katman5 SWI69: CarAdapterClient/CarGeneralClient not found — retry in 10s") AppLogger.w(TAG, " Layer5 SWI69: CarAdapterClient/CarGeneralClient not found — retry in 10s")
Handler(Looper.getMainLooper()).postDelayed({ initKatman5Swi69(context.applicationContext) }, 10_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer5Swi69(context.applicationContext) }, 10_000)
return return
} }
val (_, adapterClass, generalClientClass) = classes val (_, adapterClass, generalClientClass) = classes
AppLogger.i(TAG, " Katman5 SWI69: classes loaded ✓") AppLogger.i(TAG, " Layer5 SWI69: classes loaded ✓")
fun trySetupGeneralClient(): Boolean { fun trySetupGeneralClient(): Boolean {
if (sVcmCallbackRegistered) return true if (sVcmCallbackRegistered) return true
@@ -3589,14 +3592,14 @@ object EVHardware {
adapterClass.getMethod("queryClient", Int::class.javaPrimitiveType!!) adapterClass.getMethod("queryClient", Int::class.javaPrimitiveType!!)
.invoke(adapter, BIND_CODE_CAR_GENERAL) as? IBinder .invoke(adapter, BIND_CODE_CAR_GENERAL) as? IBinder
} catch (_: Exception) { null } ?: run { } catch (_: Exception) { null } ?: run {
AppLogger.d(TAG, " Katman5 SWI69: queryClient(0x${BIND_CODE_CAR_GENERAL.toString(16)}) → null") AppLogger.d(TAG, " Layer5 SWI69: queryClient(0x${BIND_CODE_CAR_GENERAL.toString(16)}) → null")
return false return false
} }
val client = try { val client = try {
generalClientClass.getConstructor(IBinder::class.java).newInstance(ibinder) generalClientClass.getConstructor(IBinder::class.java).newInstance(ibinder)
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5 SWI69: CarGeneralClient ctor error: ${e.message}") AppLogger.w(TAG, " Layer5 SWI69: CarGeneralClient ctor error: ${e.message}")
return false return false
} }
sCarGeneral = client // kept for A9 screen brightness (setScreenBrightness) sCarGeneral = client // kept for A9 screen brightness (setScreenBrightness)
@@ -3604,13 +3607,13 @@ object EVHardware {
val registMethod = generalClientClass.methods.firstOrNull { val registMethod = generalClientClass.methods.firstOrNull {
it.name == "registListener" && it.parameterCount == 1 it.name == "registListener" && it.parameterCount == 1
} ?: run { } ?: run {
AppLogger.w(TAG, " Katman5 SWI69: registListener not found") AppLogger.w(TAG, " Layer5 SWI69: registListener not found")
return false return false
} }
val callbackType = registMethod.parameterTypes[0] val callbackType = registMethod.parameterTypes[0]
if (!callbackType.isInterface) { if (!callbackType.isInterface) {
AppLogger.w(TAG, " Katman5 SWI69: callback non-interface — proxy impossible") AppLogger.w(TAG, " Layer5 SWI69: callback non-interface — proxy impossible")
return false return false
} }
@@ -3629,7 +3632,7 @@ object EVHardware {
0x7 -> { // TRANSACTION_onIgnitionStateChange 0x7 -> { // TRANSACTION_onIgnitionStateChange
data.enforceInterface("com.saicmotor.carapi.general.ICarGeneralCallback") data.enforceInterface("com.saicmotor.carapi.general.ICarGeneralCallback")
val ignition = data.readInt() val ignition = data.readInt()
AppLogger.i(TAG, " Katman5 SWI69: onTransact ignition=$ignition (${carIgnitionName(ignition)})") AppLogger.i(TAG, " Layer5 SWI69: onTransact ignition=$ignition (${carIgnitionName(ignition)})")
dispatchVehicleConditionIgnition(ignition) dispatchVehicleConditionIgnition(ignition)
reply?.writeNoException() reply?.writeNoException()
true true
@@ -3648,7 +3651,7 @@ object EVHardware {
// In-process path (rare) — the service calls the interface directly // In-process path (rare) — the service calls the interface directly
val ignition = args?.get(0) as? Int val ignition = args?.get(0) as? Int
if (ignition != null) { if (ignition != null) {
AppLogger.i(TAG, " Katman5 SWI69: ignition=$ignition (${carIgnitionName(ignition)})") AppLogger.i(TAG, " Layer5 SWI69: ignition=$ignition (${carIgnitionName(ignition)})")
dispatchVehicleConditionIgnition(ignition) dispatchVehicleConditionIgnition(ignition)
} }
} }
@@ -3657,7 +3660,7 @@ object EVHardware {
null null
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5 SWI69: proxy creation error: ${e.message}") AppLogger.w(TAG, " Layer5 SWI69: proxy creation error: ${e.message}")
return false return false
} }
@@ -3665,27 +3668,27 @@ object EVHardware {
registMethod.invoke(client, proxy) registMethod.invoke(client, proxy)
sVcmListener = callbackBinder // strong reference to the Binder to avoid GC sVcmListener = callbackBinder // strong reference to the Binder to avoid GC
sVcmCallbackRegistered = true sVcmCallbackRegistered = true
AppLogger.i(TAG, " Katman5 SWI69: ICarGeneralCallback registered ✓") AppLogger.i(TAG, " Layer5 SWI69: ICarGeneralCallback registered ✓")
val toNotify = katman5ReadyListeners.toList() val toNotify = layer5ReadyListeners.toList()
katman5ReadyListeners.clear() layer5ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
try { try {
val ignition = generalClientClass.getMethod("getIgnitionState").invoke(client) as? Int val ignition = generalClientClass.getMethod("getIgnitionState").invoke(client) as? Int
if (ignition != null) { if (ignition != null) {
AppLogger.i(TAG, " Katman5 SWI69: initial state ignition=$ignition") AppLogger.i(TAG, " Layer5 SWI69: initial state ignition=$ignition")
dispatchVehicleConditionIgnition(ignition) dispatchVehicleConditionIgnition(ignition)
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman5 SWI69: getIgnitionState error: ${e.message}") AppLogger.d(TAG, " Layer5 SWI69: getIgnitionState error: ${e.message}")
} }
}, 500L) }, 500L)
true true
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5 SWI69: registListener error: ${e.message}") AppLogger.w(TAG, " Layer5 SWI69: registListener error: ${e.message}")
false false
} }
} }
@@ -3699,11 +3702,11 @@ object EVHardware {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Katman5 — VehicleConditionManager (IVehicleConditionService via IHubService) // Layer5 — VehicleConditionManager (IVehicleConditionService via IHubService)
// SWI133 / SWI68 / SWI165 // SWI133 / SWI68 / SWI165
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
private fun initKatman5(context: Context) { private fun initLayer5(context: Context) {
if (sVcmCallbackRegistered) return if (sVcmCallbackRegistered) return
val launcherCtx = listOf(LAUNCHER68_PKG, LAUNCHER69_PKG).firstNotNullOfOrNull { pkg -> val launcherCtx = listOf(LAUNCHER68_PKG, LAUNCHER69_PKG).firstNotNullOfOrNull { pkg ->
@@ -3714,15 +3717,15 @@ object EVHardware {
) )
} catch (_: Exception) { null } } catch (_: Exception) { null }
} ?: run { } ?: run {
AppLogger.w(TAG, " Katman5: launcher package not found") AppLogger.w(TAG, " Layer5: launcher package not found")
return return
} }
val vcmClass = try { val vcmClass = try {
launcherCtx.classLoader.loadClass(VCM_CLASS) launcherCtx.classLoader.loadClass(VCM_CLASS)
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5: VCM class not found: ${e.message} — retry in 10s") AppLogger.w(TAG, " Layer5: VCM class not found: ${e.message} — retry in 10s")
Handler(Looper.getMainLooper()).postDelayed({ initKatman5(context.applicationContext) }, 10_000) Handler(Looper.getMainLooper()).postDelayed({ initLayer5(context.applicationContext) }, 10_000)
return return
} }
@@ -3734,7 +3737,7 @@ object EVHardware {
} catch (_: Exception) { null } } catch (_: Exception) { null }
if (existing != null) { if (existing != null) {
AppLogger.i(TAG, " Katman5: VCM singleton already exists ✓") AppLogger.i(TAG, " Layer5: VCM singleton already exists ✓")
sVcm = existing sVcm = existing
setupVcmCallback(existing, launcherCtx) setupVcmCallback(existing, launcherCtx)
return return
@@ -3755,7 +3758,7 @@ object EVHardware {
) { _, method, args -> ) { _, method, args ->
when (method.name) { when (method.name) {
"onServiceConnected" -> { "onServiceConnected" -> {
AppLogger.i(TAG, " Katman5: onServiceConnected ✓") AppLogger.i(TAG, " Layer5: onServiceConnected ✓")
val mgr = args?.getOrNull(0) val mgr = args?.getOrNull(0)
?.takeIf { it.javaClass.name.contains("VehicleConditionManager") } ?.takeIf { it.javaClass.name.contains("VehicleConditionManager") }
val instance = mgr ?: try { val instance = mgr ?: try {
@@ -3769,7 +3772,7 @@ object EVHardware {
} }
} }
"onServiceDisconnected" -> { "onServiceDisconnected" -> {
AppLogger.w(TAG, " Katman5: onServiceDisconnected") AppLogger.w(TAG, " Layer5: onServiceDisconnected")
sVcmCallbackRegistered = false sVcmCallbackRegistered = false
sVcmListener = null sVcmListener = null
} }
@@ -3777,19 +3780,19 @@ object EVHardware {
null null
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.d(TAG, " Katman5: proxy init error: ${e.message}") AppLogger.d(TAG, " Layer5: proxy init error: ${e.message}")
null null
} }
} else null } else null
try { try {
initMethod.invoke(null, context.applicationContext, listenerArg) initMethod.invoke(null, context.applicationContext, listenerArg)
AppLogger.i(TAG, " Katman5: VehicleConditionManager.init() called") AppLogger.i(TAG, " Layer5: VehicleConditionManager.init() called")
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5: init() error: ${e.message}") AppLogger.w(TAG, " Layer5: init() error: ${e.message}")
} }
} else { } else {
AppLogger.w(TAG, " Katman5: init(Context, listener) not found") AppLogger.w(TAG, " Layer5: init(Context, listener) not found")
} }
// Retries — the singleton is available after the async connection // Retries — the singleton is available after the async connection
@@ -3803,7 +3806,7 @@ object EVHardware {
f.get(null) f.get(null)
} catch (_: Exception) { null } } catch (_: Exception) { null }
if (mgr != null && sVcm == null) { if (mgr != null && sVcm == null) {
AppLogger.i(TAG, " Katman5: singleton obtained @${delay}ms") AppLogger.i(TAG, " Layer5: singleton obtained @${delay}ms")
sVcm = mgr sVcm = mgr
setupVcmCallback(mgr, launcherCtx) setupVcmCallback(mgr, launcherCtx)
} else if (sVcm != null && !sVcmCallbackRegistered) { } else if (sVcm != null && !sVcmCallbackRegistered) {
@@ -3824,14 +3827,14 @@ object EVHardware {
m.parameterTypes[0].isInterface && m.parameterTypes[0].isInterface &&
m.parameterTypes[0].methods.any { it.name.contains("ConditionChange", ignoreCase = true) } m.parameterTypes[0].methods.any { it.name.contains("ConditionChange", ignoreCase = true) }
} ?: run { } ?: run {
AppLogger.w(TAG, " Katman5: registerVehicleConditionCallback not found — methods: ${ AppLogger.w(TAG, " Layer5: registerVehicleConditionCallback not found — methods: ${
vcm.javaClass.methods.filter { it.name.startsWith("register") }.joinToString { it.name } vcm.javaClass.methods.filter { it.name.startsWith("register") }.joinToString { it.name }
}") }")
return return
} }
val callbackType = registerMethod.parameterTypes[0] val callbackType = registerMethod.parameterTypes[0]
AppLogger.i(TAG, " Katman5: callback type = ${callbackType.name}") AppLogger.i(TAG, " Layer5: callback type = ${callbackType.name}")
val proxy = try { val proxy = try {
java.lang.reflect.Proxy.newProxyInstance( java.lang.reflect.Proxy.newProxyInstance(
@@ -3846,18 +3849,18 @@ object EVHardware {
// moved. Only the transitions are logged; every event is still // moved. Only the transitions are logged; every event is still
// dispatched, because suppressing one would change behaviour. // dispatched, because suppressing one would change behaviour.
if (sIgnitionLog.accept(ignition)) { if (sIgnitionLog.accept(ignition)) {
AppLogger.i(TAG, " Katman5 event: ignition=$ignition (${carIgnitionName(ignition)})") AppLogger.i(TAG, " Layer5 event: ignition=$ignition (${carIgnitionName(ignition)})")
} }
dispatchVehicleConditionIgnition(ignition) dispatchVehicleConditionIgnition(ignition)
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5: onChangeEvent parse error: ${e.message}") AppLogger.w(TAG, " Layer5: onChangeEvent parse error: ${e.message}")
} }
} }
null null
} }
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5: proxy creation error: ${e.message}") AppLogger.w(TAG, " Layer5: proxy creation error: ${e.message}")
return return
} }
@@ -3865,10 +3868,10 @@ object EVHardware {
registerMethod.invoke(vcm, proxy) registerMethod.invoke(vcm, proxy)
sVcmListener = proxy sVcmListener = proxy
sVcmCallbackRegistered = true sVcmCallbackRegistered = true
AppLogger.i(TAG, " Katman5: callback registered ✓") AppLogger.i(TAG, " Layer5: callback registered ✓")
val toNotify = katman5ReadyListeners.toList() val toNotify = layer5ReadyListeners.toList()
katman5ReadyListeners.clear() layer5ReadyListeners.clear()
Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } } Handler(Looper.getMainLooper()).post { toNotify.forEach { it() } }
// Immediate read (the callback only fires on CHANGE) // Immediate read (the callback only fires on CHANGE)
@@ -3877,13 +3880,13 @@ object EVHardware {
vcm.javaClass.getMethod("getVehicleIgnition").invoke(vcm) as? Int vcm.javaClass.getMethod("getVehicleIgnition").invoke(vcm) as? Int
} catch (_: Exception) { null } } catch (_: Exception) { null }
if (ignition != null) { if (ignition != null) {
AppLogger.i(TAG, " Katman5: initial state = $ignition (${carIgnitionName(ignition)})") AppLogger.i(TAG, " Layer5: initial state = $ignition (${carIgnitionName(ignition)})")
dispatchVehicleConditionIgnition(ignition) dispatchVehicleConditionIgnition(ignition)
} }
}, 500L) }, 500L)
} catch (e: Exception) { } catch (e: Exception) {
AppLogger.w(TAG, " Katman5: registerVehicleConditionCallback error: ${e.message}") AppLogger.w(TAG, " Layer5: registerVehicleConditionCallback error: ${e.message}")
} }
} }
@@ -3938,15 +3941,15 @@ object EVHardware {
sb.appendLine() sb.appendLine()
sb.appendLine("── Services ──") sb.appendLine("── Services ──")
sb.appendLine("Katman1 CPM : ${if (sCarPropertyManager != null) "✓" else "✗"}") sb.appendLine("Layer1 CPM : ${if (sCarPropertyManager != null) "✓" else "✗"}")
sb.appendLine("Katman1 HVAC : ${if (sCarHvacManager != null) "✓" else "✗"}") sb.appendLine("Layer1 HVAC : ${if (sCarHvacManager != null) "✓" else "✗"}")
sb.appendLine("Katman4 created : ${if (isKatman4VpmCreated()) "✓" else "✗"}") sb.appendLine("Layer4 created : ${if (isLayer4VpmCreated()) "✓" else "✗"}")
sb.appendLine("Katman4 ready : ${if (isKatman4Ready()) "✓" else "✗"}") sb.appendLine("Layer4 ready : ${if (isLayer4Ready()) "✓" else "✗"}")
sb.appendLine("Binder vsett : ${if (sVehicleBinder != null) "✓ OK" else "✗ null"}") sb.appendLine("Binder vsett : ${if (sVehicleBinder != null) "✓ OK" else "✗ null"}")
val katman5Path = if (FirmwareInfo.isNewGenVsm() || gen == FirmwareInfo.Gen.SWI132) val layer5Path = if (FirmwareInfo.isNewGenVsm() || gen == FirmwareInfo.Gen.SWI132)
"ICarGeneralService" else "VehicleConditionMgr" "ICarGeneralService" else "VehicleConditionMgr"
sb.appendLine("Katman5 ready : ${if (sVcmCallbackRegistered) "✓ ($katman5Path)" else "✗ ($katman5Path)"}") sb.appendLine("Layer5 ready : ${if (sVcmCallbackRegistered) "✓ ($layer5Path)" else "✗ ($layer5Path)"}")
sb.appendLine("Katman5 ign : ${carIgnitionName(sLastVcmIgnitionState)} ($sLastVcmIgnitionState)") sb.appendLine("Layer5 ign : ${carIgnitionName(sLastVcmIgnitionState)} ($sLastVcmIgnitionState)")
sb.appendLine() sb.appendLine()
if (gen == FirmwareInfo.Gen.SWI132) { if (gen == FirmwareInfo.Gen.SWI132) {
@@ -4013,7 +4016,7 @@ object EVHardware {
} }
sb.appendLine() sb.appendLine()
// ── CarVehicleSettingClient (Katman4) ───────────────────────── // ── CarVehicleSettingClient (Layer4) ─────────────────────────
sb.appendLine("── SWI132 CarVehicleSettingClient (sVsm=${if (sVsm != null) "✓" else "✗ null"}) ──") sb.appendLine("── SWI132 CarVehicleSettingClient (sVsm=${if (sVsm != null) "✓" else "✗ null"}) ──")
fun vsmGet(method: String): Int = try { fun vsmGet(method: String): Int = try {
(callVsm(method) as? Int) ?: -1 (callVsm(method) as? Int) ?: -1
@@ -4177,7 +4180,7 @@ object EVHardware {
fun initAudio(context: Context) { fun initAudio(context: Context) {
// The caradapter bind only concerns the A9. On the old SDK, loudness goes through // 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. // SmartSoundManager (initialised in the Layer4 flow), so nothing to bind here.
if (!isA9Sound()) return if (!isA9Sound()) return
if (sAudioHelper?.isBinderAlive == true) return if (sAudioHelper?.isBinderAlive == true) return
if (sAudioServiceConn != null) return if (sAudioServiceConn != null) return
@@ -95,7 +95,7 @@ object GlassProbe {
glass: Glass = VehicleGlass, glass: Glass = VehicleGlass,
stopped: () -> Boolean = { VehicleWriteGate.decideNow() == VehicleWriteGate.Decision.ALLOWED }, stopped: () -> Boolean = { VehicleWriteGate.decideNow() == VehicleWriteGate.Decision.ALLOWED },
// [IgnitionState], not [CarIgnitionItem]: getCurrentIgnitionState() answers on the AAOS // [IgnitionState], not [CarIgnitionItem]: getCurrentIgnitionState() answers on the AAOS
// scale, and the two do not line up — RUN is 2 for Katman5, where 2 is OFF here. Read // scale, and the two do not line up — RUN is 2 for Layer5, where 2 is OFF here. Read
// against the wrong one, a car with the ignition on refused the probe as if it were off. // against the wrong one, a car with the ignition on refused the probe as if it were off.
ignitionRun: () -> Boolean = { EVHardware.getCurrentIgnitionState() == EVHardware.IgnitionState.ON }, ignitionRun: () -> Boolean = { EVHardware.getCurrentIgnitionState() == EVHardware.IgnitionState.ON },
sleep: (Long) -> Unit = { Thread.sleep(it) }, sleep: (Long) -> Unit = { Thread.sleep(it) },
@@ -10,7 +10,7 @@ data class DrivingProfile(
val steeringHeat: Boolean = false, val steeringHeat: Boolean = false,
val seatHeatLeft: Int = 0, // 0=off, 1, 2, 3 val seatHeatLeft: Int = 0, // 0=off, 1, 2, 3
val seatHeatRight: Int = 0, val seatHeatRight: Int = 0,
// ADAS SWI133 (Katman4) — default OFF for compatibility with existing profiles // ADAS SWI133 (Layer4) — default OFF for compatibility with existing profiles
val overspeedAlarm: Boolean = false, val overspeedAlarm: Boolean = false,
val speedLimitTone: Boolean = false, val speedLimitTone: Boolean = false,
val adasMode: Int = 0, // 0=Off, 1=Limiter, 2=Auto, 3=ACC, 4=ICA val adasMode: Int = 0, // 0=Off, 1=Limiter, 2=Auto, 3=ACC, 4=ICA
@@ -471,7 +471,7 @@ object RuntimeInterfaceProbe {
*/ */
internal const val NO_ADAPTER_CLIENT = internal const val NO_ADAPTER_CLIENT =
"no CarAdapterClient on this firmware — sCarAdapter is assigned only by " + "no CarAdapterClient on this firmware — sCarAdapter is assigned only by " +
"initKatman4Swi69, so this call failed before it reached a binder. ABSENT here is " + "initLayer4Swi69, so this call failed before it reached a binder. ABSENT here is " +
"not a reading of the vehicle." "not a reading of the vehicle."
/** /**
+3 -2
View File
@@ -145,8 +145,9 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
`GlassProbeTest`). Comments, log texts and test names only: no code changed (checked in the `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 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`, 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 and "Katman" (Turkish for layer), which named the layers in function names and logs —
explained at the top of `EVHardware.kt`. EVHardware, app tests and lint pass. renamed to "Layer" right after (`whenLayer1Ready`, `initLayer4Swi69`, `Layer5: …` in the log,
and so on; nothing outside EVHardware used the names). 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 - 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), 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 "read-only" now names the drive mode exception, the upload frequency matches the app (1 min by