App 2.19.0: EVHardware updated from upstream (malys/EVHardware e4e1158)
Three-way merge from b595ccb, the commit Fatih copied, to upstream's current state; Fatih's vendor reads (tyres, mileage, charge status, SWI69) stay. LICENSE.md records the base for the next update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
@@ -418,12 +418,15 @@ object EVHardware {
|
||||
/** Total mileage, the vendor's own integer — `PERF_ODOMETER` never publishes here. */
|
||||
private const val PROP_VENDOR_TOTAL_MILEAGE = 0x21401566
|
||||
|
||||
/**
|
||||
* Charging session state. The vehicle's own values, from the stock charging screen:
|
||||
* 1 = AC charging, 10 = DC charging, 5 = connecting, 7 = connected but not charging,
|
||||
* 8 = stopped. Anything else is not a session.
|
||||
*/
|
||||
private const val PROP_VENDOR_CHARGE_STATUS = 0x2140F409
|
||||
// Ids from MG4Control's SWI133 readings; unproven on SWI68. The plug and energy ids are
|
||||
// probe-only. PROP_BMS_CHRG_STS is also read by getVendorChargeStatus() — the charging session
|
||||
// state, the vehicle's own values from the stock charging screen: 1 = AC charging,
|
||||
// 10 = DC charging, 5 = connecting, 7 = connected but not charging, 8 = stopped. Anything
|
||||
// else is not a session.
|
||||
private const val PROP_BMS_CHRG_STS = 0x2140F409 // vendor int
|
||||
private const val PROP_CCU_ONBD_PLUG = 0x2140F43E // vendor int, AC socket
|
||||
private const val PROP_CCU_OFFBD_PLUG = 0x2140F43F // vendor int, DC socket
|
||||
private const val PROP_ENERGY_SINCE_CHARGE = 0x2160A1BB // vendor float kWh
|
||||
|
||||
/**
|
||||
* `HVAC_AMBIENT_TEMPERATURE`, the one in-car candidate left on this firmware.
|
||||
@@ -479,6 +482,24 @@ object EVHardware {
|
||||
?.takeIf { it.isFinite() && it >= 0f }
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe-only vendor charge properties, read raw. MG4Control (a system-signed app) reads them
|
||||
* on SWI133: `BMS_CHRG_STS` 1 while charging, the AC and DC plug states 0 off a charge and
|
||||
* non-zero on the active socket, and a kWh counter that resets at plug-in. Whether this app
|
||||
* may read them on SWI68, and what they say there, is what the capture is for.
|
||||
*/
|
||||
internal fun probeVendorChargeStatus(): Int? = probeVendorInt(PROP_BMS_CHRG_STS)
|
||||
internal fun probeVendorPlugAc(): Int? = probeVendorInt(PROP_CCU_ONBD_PLUG)
|
||||
internal fun probeVendorPlugDc(): Int? = probeVendorInt(PROP_CCU_OFFBD_PLUG)
|
||||
internal fun probeVendorEnergySinceChargeKwh(): Float? = supportedTelemetryRead {
|
||||
getFloatPropertyCPM(PROP_ENERGY_SINCE_CHARGE, AREA_GLOBAL)?.takeIf { it.isFinite() }
|
||||
}
|
||||
|
||||
/** Null when unreadable, never the -1 [getIntPropertyCPM] stands in for a failure with. */
|
||||
private fun probeVendorInt(propId: Int): Int? = supportedTelemetryRead {
|
||||
readPropertyCPM(propId, AREA_GLOBAL, Int::class.javaObjectType) { sCpmGetIntMethod } as? Int
|
||||
}
|
||||
|
||||
fun getStandardRangeKm(): Float? = supportedTelemetryRead {
|
||||
getFloatPropertyCPM(PROP_RANGE_REMAINING, AREA_GLOBAL)
|
||||
?.takeIf { it.isFinite() && it >= 0f }
|
||||
@@ -509,10 +530,10 @@ object EVHardware {
|
||||
*
|
||||
* The vendor property, read second: the charging service is the better source when it
|
||||
* answers, and this is what is left on a unit where it does not.
|
||||
* See [PROP_VENDOR_CHARGE_STATUS] for the values.
|
||||
* See [PROP_BMS_CHRG_STS] for the values.
|
||||
*/
|
||||
fun getVendorChargeStatus(): Int? =
|
||||
getIntPropertyCPM(PROP_VENDOR_CHARGE_STATUS, AREA_GLOBAL).takeIf { it >= 0 }
|
||||
getIntPropertyCPM(PROP_BMS_CHRG_STS, AREA_GLOBAL).takeIf { it >= 0 }
|
||||
|
||||
fun getCabinTemperatureCelsius(): Float? = supportedTelemetryRead {
|
||||
getFloatPropertyCPM(PROP_CABIN_TEMP, AREA_HVAC)
|
||||
@@ -642,6 +663,15 @@ object EVHardware {
|
||||
probeProperty("HVAC_AMBIENT_TEMPERATURE", PROP_HVAC_AMBIENT_TEMP_SWI68, AREA_GLOBAL, Float::class.javaObjectType),
|
||||
)
|
||||
|
||||
/**
|
||||
* The six exterior-light properties (CR-046), read raw whatever [ExteriorLights.PROVEN] says:
|
||||
* this is the evidence that fills it. EVTasker's Diagnostic shows and exports it — read once
|
||||
* with a light off and once with it on; a generation is proven when the state follows.
|
||||
*/
|
||||
fun probeExteriorLights(): List<PropertyReport> = ExteriorLights.NAMES.map { (id, name) ->
|
||||
probeProperty(name, id, AREA_GLOBAL, Int::class.javaObjectType)
|
||||
}
|
||||
|
||||
private fun probeProperty(name: String, propId: Int, areaId: Int, boxed: Class<*>): PropertyReport {
|
||||
val cpm = sCarPropertyManager
|
||||
?: return PropertyReport(name, propId, areaId, "CarPropertyManager not connected")
|
||||
@@ -661,6 +691,118 @@ object EVHardware {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The vendor setting objects this library already holds, by the name a report prints.
|
||||
* Probe use only ([com.evsuite.hardware.probe.VendorSurfaceProbe]); no production path
|
||||
* reads this, and the probe calls nothing on them but value getters.
|
||||
*/
|
||||
internal fun vendorSettingHandles(): Map<String, Any> = listOfNotNull(
|
||||
sVsm?.let { "VehicleSetting" to it }, // SWI68/165 manager, A9 CarVehicleSettingClient
|
||||
sVsm133?.let { "VehicleSetting133" to it },
|
||||
sVcontrol?.let { "VehicleControl" to it },
|
||||
sGeneral?.let { "General" to it },
|
||||
sCarGeneral?.let { "CarGeneral" to it },
|
||||
sCarState?.let { "CarState" to it },
|
||||
).toMap()
|
||||
|
||||
/**
|
||||
* Every property the CarPropertyManager lists, read once per area, keyed `CPM 0x…/area`.
|
||||
* Probe use only. Stops at [deadlineMs] and says so rather than blocking a diagnostic on a
|
||||
* VHAL that answers slowly.
|
||||
*/
|
||||
internal fun readAllCarProperties(deadlineMs: Long): Map<String, String> {
|
||||
val cpm = sCarPropertyManager ?: return mapOf("CPM" to "not connected")
|
||||
cacheCarPropertyMethods(cpm)
|
||||
val getProperty = sCpmGetPropertyMethod ?: return mapOf("CPM" to "no getProperty")
|
||||
val configs = try {
|
||||
cpm.javaClass.getMethod("getPropertyList").invoke(cpm) as? List<*>
|
||||
} catch (e: Exception) {
|
||||
return mapOf("CPM" to "getPropertyList: ${e.readFailureReason()}")
|
||||
} ?: return mapOf("CPM" to "getPropertyList: null")
|
||||
val out = sortedMapOf<String, String>()
|
||||
for (config in configs) {
|
||||
if (config == null) continue
|
||||
if (System.currentTimeMillis() > deadlineMs) {
|
||||
out["CPM ~"] = "time budget spent after ${out.size} values"
|
||||
break
|
||||
}
|
||||
val type = config.javaClass
|
||||
val id = runCatching { type.getMethod("getPropertyId").invoke(config) as Int }.getOrNull() ?: continue
|
||||
val valueType = runCatching { type.getMethod("getPropertyType").invoke(config) as Class<*> }
|
||||
.getOrNull() ?: Any::class.java
|
||||
val areas = runCatching { type.getMethod("getAreaIds").invoke(config) as IntArray }
|
||||
.getOrNull()?.takeIf { it.isNotEmpty() } ?: intArrayOf(AREA_GLOBAL)
|
||||
for (area in areas) {
|
||||
val key = "CPM 0x%08X/%d".format(id, area)
|
||||
out[key] = try {
|
||||
val holder = getProperty.invoke(cpm, valueType, id, area)
|
||||
if (holder == null) "no value" else {
|
||||
val status = (holder.javaClass.getMethod("getStatus").invoke(holder) as? Int)
|
||||
?.let(CarPropertyEvidence::describe) ?: "no status"
|
||||
"$status ${com.evsuite.hardware.probe.VendorSurfaceRules.render(
|
||||
holder.javaClass.getMethod("getValue").invoke(holder))}"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.readFailureReason()
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Exterior lights (CR-046) — standard AAOS ids and policy in [ExteriorLights]
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** Status-checked read, only on a proven generation: an unpublished 0 would read as OFF. */
|
||||
private fun lightRead(propId: Int): Int? =
|
||||
if (ExteriorLights.isProven(FirmwareInfo.getGeneration()))
|
||||
readPropertyCPM(propId, AREA_GLOBAL, Int::class.javaObjectType) { sCpmGetIntMethod } as? Int
|
||||
else null
|
||||
|
||||
/** Low beams as VehicleLightState (0 off, 1 on, 2 daytime running); null when unreadable. */
|
||||
fun headlightsStateOrNull(): Int? = lightRead(ExteriorLights.PROP_HEADLIGHTS_STATE)
|
||||
|
||||
fun highBeamOnOrNull(): Boolean? =
|
||||
ExteriorLights.isOn(lightRead(ExteriorLights.PROP_HIGH_BEAM_LIGHTS_STATE))
|
||||
|
||||
fun fogLightsOnOrNull(): Boolean? =
|
||||
ExteriorLights.isOn(lightRead(ExteriorLights.PROP_FOG_LIGHTS_STATE))
|
||||
|
||||
/** Switch positions, for the action editor's current value. */
|
||||
fun headlightsSwitchOrNull(): Int? = lightRead(ExteriorLights.PROP_HEADLIGHTS_SWITCH)
|
||||
|
||||
fun highBeamSwitchOrNull(): Int? = lightRead(ExteriorLights.PROP_HIGH_BEAM_LIGHTS_SWITCH)
|
||||
|
||||
fun fogLightsSwitchOnOrNull(): Boolean? =
|
||||
lightRead(ExteriorLights.PROP_FOG_LIGHTS_SWITCH)?.let { it == ExteriorLights.SWITCH_ON }
|
||||
|
||||
/** Low beams ON or AUTOMATIC — never OFF ([ExteriorLights.HEADLIGHT_SWITCH_ALLOWED]). */
|
||||
@RequiresStandstill
|
||||
fun setHeadlightsSwitch(mode: Int): Boolean =
|
||||
lightWrite(ExteriorLights.PROP_HEADLIGHTS_SWITCH, mode, ExteriorLights.HEADLIGHT_SWITCH_ALLOWED)
|
||||
|
||||
/** High beam OFF or AUTOMATIC — never forced ON. */
|
||||
@RequiresStandstill
|
||||
fun setHighBeamSwitch(mode: Int): Boolean =
|
||||
lightWrite(ExteriorLights.PROP_HIGH_BEAM_LIGHTS_SWITCH, mode, ExteriorLights.HIGH_BEAM_SWITCH_ALLOWED)
|
||||
|
||||
@RequiresStandstill
|
||||
fun setFogLights(on: Boolean): Boolean = lightWrite(
|
||||
ExteriorLights.PROP_FOG_LIGHTS_SWITCH,
|
||||
if (on) ExteriorLights.SWITCH_ON else ExteriorLights.SWITCH_OFF,
|
||||
ExteriorLights.FOG_SWITCH_ALLOWED,
|
||||
)
|
||||
|
||||
/** Policy, then proven generation, then the 0 km/h gate inside [setIntPropertyCPM]. */
|
||||
private fun lightWrite(propId: Int, value: Int, allowed: Set<Int>): Boolean {
|
||||
ExteriorLights.writeRefusal(FirmwareInfo.getGeneration(), allowed, value)?.let {
|
||||
AppLogger.w(TAG, " light 0x${Integer.toHexString(propId)} refused: $it")
|
||||
return false
|
||||
}
|
||||
return setIntPropertyCPM(propId, AREA_GLOBAL, value)
|
||||
}
|
||||
|
||||
private inline fun <T> supportedTelemetryRead(read: () -> T?): T? =
|
||||
when (FirmwareInfo.getGeneration()) {
|
||||
FirmwareInfo.Gen.SWI68,
|
||||
@@ -1439,7 +1581,7 @@ object EVHardware {
|
||||
return try {
|
||||
val native = (g.javaClass.getMethod("getBrightness").invoke(g) as? Int) ?: return -1
|
||||
if (native < 0) return -1
|
||||
val pct = (native * 100 / BRIGHTNESS_NATIVE_MAX).coerceIn(0, 100)
|
||||
val pct = ((native * 100 + BRIGHTNESS_NATIVE_MAX / 2) / BRIGHTNESS_NATIVE_MAX).coerceIn(0, 100)
|
||||
AppLogger.d(TAG, " getBrightness native=$native → $pct%")
|
||||
pct
|
||||
} catch (e: Exception) {
|
||||
@@ -1450,7 +1592,7 @@ object EVHardware {
|
||||
|
||||
private fun setBrightnessOldSdk(clampedPct: Int): Boolean {
|
||||
val g = sGeneral ?: return false
|
||||
val native = (clampedPct * BRIGHTNESS_NATIVE_MAX / 100).coerceIn(0, BRIGHTNESS_NATIVE_MAX)
|
||||
val native = ((clampedPct * BRIGHTNESS_NATIVE_MAX + 50) / 100).coerceIn(0, BRIGHTNESS_NATIVE_MAX)
|
||||
if (logEnabled) AppLogger.i(TAG, "setBrightness → $clampedPct% (native=$native/255)")
|
||||
return try {
|
||||
g.javaClass.getMethod("setBrightness", Int::class.javaPrimitiveType).invoke(g, native)
|
||||
@@ -1477,12 +1619,12 @@ object EVHardware {
|
||||
AppLogger.w(TAG, " A9 getBrightness Settings.System exc: ${e.message}"); return -1
|
||||
}
|
||||
if (native < 0) return -1
|
||||
return (native * 100 / A9_BRIGHTNESS_NATIVE_MAX).coerceIn(0, 100)
|
||||
return ((native * 100 + A9_BRIGHTNESS_NATIVE_MAX / 2) / A9_BRIGHTNESS_NATIVE_MAX).coerceIn(0, 100)
|
||||
}
|
||||
|
||||
private fun setBrightnessA9(clampedPct: Int): Boolean {
|
||||
val resolver = sAppContext?.contentResolver ?: return false
|
||||
val native = (clampedPct.coerceIn(0, 100) * A9_BRIGHTNESS_NATIVE_MAX / 100).coerceIn(1, A9_BRIGHTNESS_NATIVE_MAX)
|
||||
val native = ((clampedPct.coerceIn(0, 100) * A9_BRIGHTNESS_NATIVE_MAX + 50) / 100).coerceIn(1, A9_BRIGHTNESS_NATIVE_MAX)
|
||||
return try {
|
||||
// Manual mode, otherwise auto-brightness overwrites the value at once
|
||||
android.provider.Settings.System.putInt(resolver,
|
||||
@@ -2329,23 +2471,22 @@ object EVHardware {
|
||||
fun getSeatHeatRight(): Int = seatHeatRightOrNull() ?: 0
|
||||
fun isSteeringHeatOn(): Boolean = steeringHeatOnOrNull() ?: false
|
||||
|
||||
fun getDriveMode(): DriveMode? {
|
||||
val cpm = sCarPropertyManager ?: return null
|
||||
return try {
|
||||
val raw = (cpm.javaClass
|
||||
.getMethod("getIntProperty", Int::class.java, Int::class.java)
|
||||
.invoke(cpm, PROP_DRIVE_MODE, AREA_GLOBAL) as? Int) ?: return null
|
||||
DriveMode.fromValue(raw)
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
fun getDriveMode(): DriveMode? = getDriveModeRaw()?.let(DriveMode::fromValue)
|
||||
|
||||
fun getRegenLevel(): RegenLevel? {
|
||||
/** The drive mode code as the car sends it; null when it could not be read. */
|
||||
fun getDriveModeRaw(): Int? = readIntGlobal(PROP_DRIVE_MODE)
|
||||
|
||||
fun getRegenLevel(): RegenLevel? = getRegenLevelRaw()?.let(RegenLevel::fromValue)
|
||||
|
||||
/** The regen code as the car sends it: 6 is one-pedal on SWI68 (EVTasker CR-045 diff). */
|
||||
fun getRegenLevelRaw(): Int? = readIntGlobal(PROP_REGEN_LEVEL)
|
||||
|
||||
private fun readIntGlobal(prop: Int): Int? {
|
||||
val cpm = sCarPropertyManager ?: return null
|
||||
return try {
|
||||
val raw = (cpm.javaClass
|
||||
cpm.javaClass
|
||||
.getMethod("getIntProperty", Int::class.java, Int::class.java)
|
||||
.invoke(cpm, PROP_REGEN_LEVEL, AREA_GLOBAL) as? Int) ?: return null
|
||||
RegenLevel.fromValue(raw)
|
||||
.invoke(cpm, prop, AREA_GLOBAL) as? Int
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
@@ -2464,8 +2605,9 @@ object EVHardware {
|
||||
fun getAccTjaMode(): Int {
|
||||
val useNewApi = FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132
|
||||
val method = if (useNewApi) "getAccTjaState" else "getAccTjaMode"
|
||||
if (logEnabled) AppLogger.i(TAG, "$method →")
|
||||
return (callVsm(method) as? Int) ?: -1
|
||||
val value = (callVsm(method) as? Int) ?: -1
|
||||
if (logEnabled) AppLogger.i(TAG, "$method → $value")
|
||||
return value
|
||||
}
|
||||
|
||||
/** SWI68/SWI165 : setAccTjaMode(I)V SWI69/SWI131/SWI132 : setAccTjaState(I)V — void */
|
||||
@@ -2491,8 +2633,9 @@ object EVHardware {
|
||||
/** Lit le mode du limiteur de speed. Returns 0/2/3, ou -1 si unavailable. */
|
||||
fun getSpeedLimiterMode(): Int {
|
||||
val method = if (useSasApi()) "getSasMode" else "getSpeedAsstMode"
|
||||
if (logEnabled) AppLogger.i(TAG, "$method →")
|
||||
return (callVsm(method) as? Int) ?: -1
|
||||
val value = (callVsm(method) as? Int) ?: -1
|
||||
if (logEnabled) AppLogger.i(TAG, "$method → $value")
|
||||
return value
|
||||
}
|
||||
|
||||
/** Configure le mode du limiteur de speed. 0=Disabled, 2=Manual, 3=Intelligent. */
|
||||
@@ -4316,6 +4459,30 @@ object EVHardware {
|
||||
return if (answered) open else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-door evidence for a firmware the door feature does not claim yet, for EVTasker's
|
||||
* Diagnostic.
|
||||
*
|
||||
* MG4Control reads DLOCK_DOOR_OPEN_STS on SWI68/69/131/165 through the change
|
||||
* subscription, because the on-demand read throws there. Nothing here has seen a car
|
||||
* confirm that, so [frontDoorOpenOrNull] stays gated. This connects and subscribes
|
||||
* regardless of [hasDoorVolumeFeature] — the first call starts the connection, which is
|
||||
* asynchronous — and reports what came back: export with the front doors shut, open
|
||||
* one, export again. Proven when the event value follows the door.
|
||||
*/
|
||||
fun probeFrontDoors(): String {
|
||||
connectCarProperty()
|
||||
registerDoorCallback()
|
||||
val reads = DOOR_FRONT_AREAS.joinToString(" ") { area ->
|
||||
"read[0x${area.toString(16)}]=${readDoorOpen(area) ?: "?"}"
|
||||
}
|
||||
val events = synchronized(this) {
|
||||
DOOR_FRONT_AREAS.joinToString(" ") { area -> "event[0x${area.toString(16)}]=${sDoorReadLast[area] ?: "?"}" }
|
||||
}
|
||||
return "managers property=${sCarPropMgr != null} doorlock=${sCarDoorMgr != null} " +
|
||||
"subscribed property=$sDoorSubProperty doorlock=$sDoorSubDoorlock $reads $events"
|
||||
}
|
||||
|
||||
/** Diagnostic button probe: logs the volume + door states at click time. */
|
||||
fun runDoorVolumeDiag() {
|
||||
AppLogger.i(VOL_TAG, "── DIAG (bouton Diagnostic) ──")
|
||||
|
||||
Reference in New Issue
Block a user