Skip to content

Commit b9d2152

Browse files
malysclaude
andcommitted
fix: stop reporting unreadable vehicle signals as "off"
Several readers collapsed "the firmware never answered" into false or 0: isAebEnabled, isTsrOn, isEnergySavingOn, isSoundWarningOn, isOverspeedAlarmOn, isSpeedLimitToneOn, isSteeringHeatOn and getSeatHeat*. For a UI switch that is a reasonable default. For a rule engine it is not: a car whose VSM does not expose a signal looks exactly like a car with the feature turned off, and the rule then writes on that assumption — the one thing the design says must never happen. Each routing now lives in a nullable `…OrNull` reader; the historical getter is a thin `?: false` / `?: 0` over it, so every existing caller behaves exactly as before. aebModeOrNull is added alongside getAebMode, which defaults to ALARM on VSM firmware. Also retarget the vendor-audio actions. Balance, fader, tone, Bose, 3D and speed volume go through the SAIC caradapter helper, bound only on A9 (SWI69/SWI131/SWI132). They were annotated SWI133 + SWI132, read off hasAudioControl() — which describes the door-volume feature, a different thing. The effect was an action offered on SWI133 where the write silently fails, and hidden on SWI69/SWI131 where it works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e400586 commit b9d2152

3 files changed

Lines changed: 81 additions & 38 deletions

File tree

‎docs/firmware-matrix.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@
5757
| SET_STEERING_HEAT | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
5858
| SET_SCREEN_BRIGHTNESS | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
5959
| SET_MEDIA_VOLUME | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
60-
| SET_AUDIO_BALANCE | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
61-
| SET_AUDIO_FADER | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
62-
| SET_TONE_CONTROL | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
63-
| SET_BOSE_SOUND_TYPE | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
64-
| SET_3D_EFFECT | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
65-
| SET_SPEED_VOLUME | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
60+
| SET_AUDIO_BALANCE | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
61+
| SET_AUDIO_FADER | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
62+
| SET_TONE_CONTROL | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
63+
| SET_BOSE_SOUND_TYPE | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
64+
| SET_3D_EFFECT | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
65+
| SET_SPEED_VOLUME | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
6666
| SET_AEB_ENABLED | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
6767
| SET_AEB_MODE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
6868
| SET_AEB_SENSITIVITY | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

‎lib/src/main/java/com/mg4/hardware/MG4Hardware.kt‎

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,9 +1695,24 @@ object MG4Hardware {
16951695
return ok
16961696
}
16971697

1698-
fun getSeatHeatLeft(): Int = getIntPropertyHvac(PROP_SEAT_HEAT_L, AREA_HVAC).coerceAtLeast(0)
1699-
fun getSeatHeatRight(): Int = getIntPropertyHvac(PROP_SEAT_HEAT_R, AREA_HVAC).coerceAtLeast(0)
1700-
fun isSteeringHeatOn(): Boolean = getIntPropertyHvac(PROP_STEERING_HEAT, AREA_HVAC) > 0
1698+
/*
1699+
* `…OrNull` readers: null means "could not be read", which is not the same as off/zero.
1700+
*
1701+
* A consumer that drives a switch (MG4Control) wants a Boolean and treats unreadable as
1702+
* off; a consumer that evaluates a rule (MG4Tasker) must not, or a car whose firmware
1703+
* never answers looks exactly like a car with the feature turned off — and the rule
1704+
* writes on that assumption. So the routing lives in the nullable reader and the
1705+
* historical non-null getter is a thin `?: false` / `?: 0` over it, unchanged for its
1706+
* callers.
1707+
*/
1708+
fun seatHeatLeftOrNull(): Int? = getIntPropertyHvac(PROP_SEAT_HEAT_L, AREA_HVAC).takeIf { it >= 0 }
1709+
fun seatHeatRightOrNull(): Int? = getIntPropertyHvac(PROP_SEAT_HEAT_R, AREA_HVAC).takeIf { it >= 0 }
1710+
fun steeringHeatOnOrNull(): Boolean? =
1711+
getIntPropertyHvac(PROP_STEERING_HEAT, AREA_HVAC).takeIf { it >= 0 }?.let { it > 0 }
1712+
1713+
fun getSeatHeatLeft(): Int = seatHeatLeftOrNull() ?: 0
1714+
fun getSeatHeatRight(): Int = seatHeatRightOrNull() ?: 0
1715+
fun isSteeringHeatOn(): Boolean = steeringHeatOnOrNull() ?: false
17011716

17021717
fun getDriveMode(): DriveMode? {
17031718
val cpm = sCarPropertyManager ?: return null
@@ -1747,7 +1762,7 @@ object MG4Hardware {
17471762
// ADAS API (Katman4)
17481763
// -------------------------------------------------------------------------
17491764

1750-
fun isOverspeedAlarmOn(): Boolean {
1765+
fun overspeedAlarmOnOrNull(): Boolean? {
17511766
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132) {
17521767
// VSM priority (CarVehicleSettingClient) — confirmed in SWI132 smali
17531768
// getOverSpeedSoundMode() : 0=OFF, 1/2/3=ON
@@ -1756,11 +1771,13 @@ object MG4Hardware {
17561771
AppLogger.d(TAG, " SWI132 overspeed GET via VSM → $vsm")
17571772
return vsm > 0
17581773
}
1759-
return swi132BinderGet(VSM132_TX_GET_OVERSPEED) == 1
1774+
return swi132BinderGet(VSM132_TX_GET_OVERSPEED).takeIf { it >= 0 }?.let { it == 1 }
17601775
}
1761-
return getIntPropertyVpm(PROP_OVERSPEED_ALARM) > 0
1776+
return getIntPropertyVpm(PROP_OVERSPEED_ALARM).takeIf { it >= 0 }?.let { it > 0 }
17621777
}
17631778

1779+
fun isOverspeedAlarmOn(): Boolean = overspeedAlarmOnOrNull() ?: false
1780+
17641781
@RequiresStandstill
17651782
fun setOverspeedAlarm(on: Boolean): Boolean {
17661783
if (logEnabled) AppLogger.i(TAG, "setOverspeedAlarm → $on")
@@ -1780,19 +1797,21 @@ object MG4Hardware {
17801797
return setIntPropertyVpm(PROP_OVERSPEED_ALARM, if (on) 1 else 0)
17811798
}
17821799

1783-
fun isSpeedLimitToneOn(): Boolean {
1800+
fun speedLimitToneOnOrNull(): Boolean? {
17841801
if (FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132) {
17851802
// getSpeedLimitSoundMode() : 0=OFF, value positive=ON
17861803
val vsm = callVsm("getSpeedLimitSoundMode") as? Int
17871804
if (vsm != null) {
17881805
AppLogger.d(TAG, " SWI132 speedLimit GET via VSM → $vsm")
17891806
return vsm > 0
17901807
}
1791-
return swi132BinderGet(VSM132_TX_GET_SPEED_LIMIT) == 1
1808+
return swi132BinderGet(VSM132_TX_GET_SPEED_LIMIT).takeIf { it >= 0 }?.let { it == 1 }
17921809
}
1793-
return getIntPropertyVpm(PROP_SPEED_LIMIT_TONE) > 0
1810+
return getIntPropertyVpm(PROP_SPEED_LIMIT_TONE).takeIf { it >= 0 }?.let { it > 0 }
17941811
}
17951812

1813+
fun isSpeedLimitToneOn(): Boolean = speedLimitToneOnOrNull() ?: false
1814+
17961815
@RequiresStandstill
17971816
fun setSpeedLimitTone(on: Boolean): Boolean {
17981817
if (logEnabled) AppLogger.i(TAG, "setSpeedLimitTone → $on")
@@ -1874,12 +1893,14 @@ object MG4Hardware {
18741893
* SWI69/SWI131/SWI132 : getLasWarningSound() (confirmed dans smali SWI132)
18751894
* Values : 2=ON / 1=OFF
18761895
*/
1877-
fun isSoundWarningOn(): Boolean {
1896+
fun soundWarningOnOrNull(): Boolean? {
18781897
val method = if (FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132)
18791898
"getLasWarningSound" else "getLaneKeepingWarningSound"
1880-
return ((callVsm(method) as? Int) ?: 1) == 2
1899+
return (callVsm(method) as? Int)?.let { it == 2 }
18811900
}
18821901

1902+
fun isSoundWarningOn(): Boolean = soundWarningOnOrNull() ?: false
1903+
18831904
/** SWI68 : setLaneKeepingWarningSound(I) SWI69/SWI131/SWI132 : setLasWarningSound(I) — void */
18841905
@RequiresStandstill
18851906
fun setSoundWarning(on: Boolean): Boolean {
@@ -1898,16 +1919,18 @@ object MG4Hardware {
18981919
* Verified in SafeSettingsRepository SWI165 — same API as SWI68.
18991920
* SWI69 / SWI131 : getFcwState() — 1=DISABLED, 2=ENABLED
19001921
*/
1901-
fun isAebEnabled(): Boolean {
1922+
fun aebEnabledOrNull(): Boolean? {
19021923
return when {
19031924
// SWI69 / SWI131 / SWI132 — CarVehicleSettingClient : getFcwState() (1=OFF, 2=ON)
19041925
FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 ->
1905-
(callVsm("getFcwState") as? Int) == 2
1906-
FirmwareInfo.isVsmBased() -> (callVsm("getFcwAlarmMode") as? Int) == 2 // SWI68 / SWI165
1907-
else -> getIntPropertyCPM(PROP_AEB_SWITCH, AREA_GLOBAL) == 0x2 // SWI133
1926+
(callVsm("getFcwState") as? Int)?.let { it == 2 }
1927+
FirmwareInfo.isVsmBased() -> (callVsm("getFcwAlarmMode") as? Int)?.let { it == 2 } // SWI68 / SWI165
1928+
else -> getIntPropertyCPM(PROP_AEB_SWITCH, AREA_GLOBAL).takeIf { it >= 0 }?.let { it == 0x2 } // SWI133
19081929
}
19091930
}
19101931

1932+
fun isAebEnabled(): Boolean = aebEnabledOrNull() ?: false
1933+
19111934
@RequiresStandstill
19121935
fun setAebEnabled(on: Boolean): Boolean {
19131936
if (logEnabled) AppLogger.i(TAG, "setAebEnabled → $on")
@@ -1958,6 +1981,15 @@ object MG4Hardware {
19581981
}
19591982
}
19601983

1984+
/**
1985+
* Same routing as [getAebMode], without the ALARM default the VSM branch falls back to.
1986+
* That default is a sane switch position for a UI, and a lie for a rule: it would make
1987+
* "AEB mode is Alert" true on a car that never answered.
1988+
*/
1989+
fun aebModeOrNull(): Int? =
1990+
if (FirmwareInfo.isVsmBased()) (callVsm("getFcwAutoBrakeMode") as? Int)?.takeIf { it > 0 }
1991+
else getIntPropertyVpm(PROP_AEB_MODE).takeIf { it >= 1 }
1992+
19611993
@RequiresStandstill
19621994
fun setAebMode(mode: Int): Boolean {
19631995
if (logEnabled) AppLogger.i(TAG, "setAebMode → $mode")
@@ -2308,23 +2340,26 @@ object MG4Hardware {
23082340
// TSR — Reconnaissance des panneaux de speed
23092341
// -------------------------------------------------------------------------
23102342

2311-
fun isTsrOn(): Boolean = when {
2343+
fun tsrOnOrNull(): Boolean? = when {
23122344
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 ->
23132345
// CarVehicleSettingClient priority (vehiclesetting binder SELinux-blocked)
23142346
// Convention identique SWI69/SWI131 : 0=ON, 1=OFF
23152347
(callVsm("getSLIFWarningState") as? Int)?.let { raw ->
23162348
AppLogger.d(TAG, " SWI132 TSR GET via VSM → $raw")
23172349
raw == 0
2318-
} ?: (swi132BinderGet(VSM132_TX_GET_SLIF) == 1) // fallback binder (rarement accessible)
2350+
// fallback binder (rarement accessible)
2351+
} ?: swi132BinderGet(VSM132_TX_GET_SLIF).takeIf { it >= 0 }?.let { it == 1 }
23192352
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 ->
2320-
getIntPropertyVpm(PROP_TSR_MODE) > 0
2353+
getIntPropertyVpm(PROP_TSR_MODE).takeIf { it >= 0 }?.let { it > 0 }
23212354
FirmwareInfo.isNewGenVsm() -> // SWI69 + SWI131 — convention invertede : 0=ON, 1=OFF
2322-
(callVsm("getSLIFWarningState") as? Int) == 0
2355+
(callVsm("getSLIFWarningState") as? Int)?.let { it == 0 }
23232356
FirmwareInfo.isVsmBased() -> // SWI68 + SWI165
2324-
(callVsm("getSpeedAsstSlifWarning") as? Int) == 1
2325-
else -> false
2357+
(callVsm("getSpeedAsstSlifWarning") as? Int)?.let { it == 1 }
2358+
else -> null
23262359
}
23272360

2361+
fun isTsrOn(): Boolean = tsrOnOrNull() ?: false
2362+
23282363
@RequiresStandstill
23292364
fun setTsrMode(enabled: Boolean): Boolean {
23302365
AppLogger.i(TAG, "setTsrMode → $enabled")
@@ -2410,16 +2445,18 @@ object MG4Hardware {
24102445
// Energy saving (Endurance Mode)
24112446
// -------------------------------------------------------------------------
24122447

2413-
fun isEnergySavingOn(): Boolean = when {
2448+
fun energySavingOnOrNull(): Boolean? = when {
24142449
FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI133 ->
2415-
getIntPropertyVpm(PROP_ENERGY_SAVING) == 1
2450+
getIntPropertyVpm(PROP_ENERGY_SAVING).takeIf { it >= 0 }?.let { it == 1 }
24162451
FirmwareInfo.isNewGenVsm() || FirmwareInfo.getGeneration() == FirmwareInfo.Gen.SWI132 -> // SWI69 + SWI131 + SWI132
2417-
(callVsm("getEnduranceMode") as? Int) == 1
2452+
(callVsm("getEnduranceMode") as? Int)?.let { it == 1 }
24182453
FirmwareInfo.isVsmBased() -> // SWI68 + SWI165
2419-
(callVsm("getLongerEndurance") as? Int) == 1
2420-
else -> false
2454+
(callVsm("getLongerEndurance") as? Int)?.let { it == 1 }
2455+
else -> null
24212456
}
24222457

2458+
fun isEnergySavingOn(): Boolean = energySavingOnOrNull() ?: false
2459+
24232460
@RequiresStandstill
24242461
fun setEnergySavingMode(enabled: Boolean): Boolean {
24252462
AppLogger.i(TAG, "setEnergySavingMode → $enabled")

‎lib/src/main/java/com/mg4/hardware/catalog/ActionType.kt‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,43 +111,49 @@ enum class ActionType(
111111
),
112112

113113
// ── Audio ────────────────────────────────────────────────────────────────
114+
// Everything below the media volume goes through the SAIC `caradapter` audio helper,
115+
// which MG4Hardware binds only on the A9 platform (SWI69 / SWI131 / SWI132). These
116+
// entries used to be annotated SWI133 + SWI132, read off `hasAudioControl()` — but that
117+
// predicate describes the door-volume feature, a different thing. The effect was an
118+
// action offered on SWI133, where the helper is never bound and the write silently
119+
// returns false, and hidden on SWI69 / SWI131, where it works.
114120
@SupportedOn(SWI133, SWI132, SWI68, SWI69, SWI131, SWI165)
115121
SET_MEDIA_VOLUME(
116122
R.string.act_media_volume, ActionGroup.AUDIO,
117123
ValueSpec.dynamicNumber(0, VehicleEnums.MEDIA_VOLUME_FALLBACK_MAX),
118124
"SET_MEDIA_VOLUME"
119125
),
120-
@SupportedOn(SWI133, SWI132)
126+
@SupportedOn(SWI69, SWI131, SWI132)
121127
SET_AUDIO_BALANCE(
122128
R.string.act_balance, ActionGroup.AUDIO,
123129
number(VehicleEnums.AUDIO_LEVEL_MIN, VehicleEnums.AUDIO_LEVEL_MAX),
124130
"SET_AUDIO_BALANCE"
125131
),
126-
@SupportedOn(SWI133, SWI132)
132+
@SupportedOn(SWI69, SWI131, SWI132)
127133
SET_AUDIO_FADER(
128134
R.string.act_fader, ActionGroup.AUDIO,
129135
number(VehicleEnums.AUDIO_LEVEL_MIN, VehicleEnums.AUDIO_LEVEL_MAX),
130136
"SET_AUDIO_FADER"
131137
),
132-
@SupportedOn(SWI133, SWI132)
138+
@SupportedOn(SWI69, SWI131, SWI132)
133139
SET_TONE_CONTROL(
134140
R.string.act_tone, ActionGroup.AUDIO,
135141
number(VehicleEnums.AUDIO_LEVEL_MIN, VehicleEnums.AUDIO_LEVEL_MAX),
136142
"SET_TONE_CONTROL"
137143
),
138-
@SupportedOn(SWI133, SWI132)
144+
@SupportedOn(SWI69, SWI131, SWI132)
139145
SET_BOSE_SOUND_TYPE(
140146
R.string.act_bose, ActionGroup.AUDIO,
141147
number(VehicleEnums.AUDIO_TYPE_MIN, VehicleEnums.AUDIO_TYPE_MAX),
142148
"SET_BOSE_SOUND_TYPE"
143149
),
144-
@SupportedOn(SWI133, SWI132)
150+
@SupportedOn(SWI69, SWI131, SWI132)
145151
SET_3D_EFFECT(
146152
R.string.act_3d, ActionGroup.AUDIO,
147153
number(VehicleEnums.AUDIO_TYPE_MIN, VehicleEnums.AUDIO_TYPE_MAX),
148154
"SET_3D_EFFECT"
149155
),
150-
@SupportedOn(SWI133, SWI132)
156+
@SupportedOn(SWI69, SWI131, SWI132)
151157
SET_SPEED_VOLUME(
152158
R.string.act_speed_volume, ActionGroup.AUDIO,
153159
number(VehicleEnums.AUDIO_TYPE_MIN, VehicleEnums.AUDIO_TYPE_MAX),

0 commit comments

Comments
 (0)