Skip to content

Commit b1543df

Browse files
committed
Add 3 parameters for CUSTOM drive mode
1 parent 032d518 commit b1543df

14 files changed

Lines changed: 794 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ L'application communique avec le véhicule via le SDK propriétaire SAIC, en acc
5151
- **Mode de conduite** : ECO / NORMAL / SPORT / SNOW / CUSTOM
5252
- **Régénération** : Off / Faible / Moyen / Fort / Adaptatif / 1 Pédale
5353

54+
#### Mode de conduite Personnalisé
55+
Trois réglages que le véhicule n'expose que dans ce mode : **puissance** (Éco / Normal / Sport),
56+
**direction** et **force sur la pédale** (Confort / Normal / Sport). Disponibles sur les six
57+
firmwares, par deux voies distinctes — signaux `VehiclePropertyManager` en SWI133/68/165,
58+
méthodes `CarVehicleSettingClient` sur A9.
59+
60+
Présents à deux endroits, avec une règle d'apparition différente :
61+
62+
| Écran | La carte apparaît quand |
63+
|---|---|
64+
| Dashboard, onglet Conduite | la **voiture** est en mode Personnalisé, y compris si le mode a été mis depuis l'écran d'origine |
65+
| Éditeur de profil, onglet Conduite | le **profil** en cours d'édition a Personnalisé pour mode |
66+
67+
Sur le Dashboard, une ligne dont le véhicule ne rend pas l'état disparaît : la commande existe sur
68+
tous les firmwares, mais rien ne garantit que la finition porte l'équipement.
69+
70+
> [!WARNING]
71+
> Les trois réglages n'ont pas la même échelle côté véhicule, et la pédale est un piège :
72+
> **Normal y vaut 0**, pas la valeur du milieu. La puissance vaut 1/2/3 en SWI133/68/165 mais
73+
> 2/3/4 sur A9, où la voiture réutilise l'échelle des modes de conduite. L'application ne
74+
> manipule donc jamais ces nombres — elle passe un index 0/1/2 et la conversion vit dans
75+
> `MG4Hardware`.
76+
77+
Dans un profil, ils ne sont écrits que si son mode de conduite est Personnalisé, et un profil
78+
enregistré avant cette fonctionnalité n'écrit rien tant qu'il n'a pas été rouvert et sauvegardé.
5479
### Sécurité
5580
- **ESC** : ON / OFF
5681
- **Avertissement de somnolence** : ON / OFF, avec sensibilité Faible / Standard / Élevé
@@ -899,6 +924,30 @@ The app communicates with the vehicle through the proprietary SAIC SDK, accessin
899924
- **Drive mode**: ECO / NORMAL / SPORT / SNOW / CUSTOM
900925
- **Regenerative braking**: Off / Low / Medium / High / Adaptive / One Pedal
901926

927+
#### Custom drive mode
928+
Three settings the vehicle only exposes in that mode: **power** (Eco / Normal / Sport),
929+
**steering** and **pedal force** (Comfort / Normal / Sport). Available on all six firmwares
930+
through two distinct routes — `VehiclePropertyManager` signals on SWI133/68/165, and
931+
`CarVehicleSettingClient` methods on A9.
932+
933+
Present in two places, with a different reveal rule:
934+
935+
| Screen | The card appears when |
936+
|---|---|
937+
| Dashboard, Driving tab | the **car** is in Custom mode, including when it was set from the stock screen |
938+
| Profile editor, Driving tab | the **profile** being edited has Custom as its mode |
939+
940+
On the Dashboard, a row whose state the vehicle does not report disappears: the command exists on
941+
every firmware, but nothing guarantees the trim carries the hardware.
942+
943+
> [!WARNING]
944+
> The three settings do not share one scale, and the pedal is a trap: **Normal is 0 there**, not
945+
> the middle value. Power is 1/2/3 on SWI133/68/165 but 2/3/4 on A9, where the car reuses the
946+
> drive-mode scale. The app therefore never handles those numbers — it passes a 0/1/2 index and
947+
> the conversion lives in `MG4Hardware`.
948+
949+
In a profile they are only written when its drive mode is Custom, and a profile saved before this
950+
feature writes nothing until it has been reopened and saved.
902951
### Safety
903952
- **ESC**: ON / OFF
904953
- **Drowsiness warning**: ON / OFF, with Low / Standard / High sensitivity

‎app/src/main/java/com/mg4/control/hardware/MG4Hardware.kt‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,6 +1686,115 @@ object MG4Hardware {
16861686
return ok
16871687
}
16881688

1689+
// ── Mode de conduite Personnalisé : puissance, direction, pédale ─────────
1690+
//
1691+
// Ces trois réglages n'existent que dans le mode Personnalisé du véhicule. Ils sont présents
1692+
// sur les six firmwares, mais par deux voies différentes.
1693+
1694+
/**
1695+
* Signaux du SDK vehiclesettings (SWI133/68/165).
1696+
*
1697+
* ⚠️ Ce ne sont PAS des identifiants de propriété VHAL : l'app d'origine passe par
1698+
* `VehiclePropertyManager` avec sa propre numérotation. Les équivalents VHAL existent
1699+
* (0x2140a18c / 0x2140a18d / 0x2140a18e) si cette voie devenait un jour nécessaire.
1700+
*/
1701+
private const val SIG_CUSTOM_POWER = 0x2040002
1702+
private const val SIG_CUSTOM_STEERING = 0x2040004
1703+
private const val SIG_CUSTOM_PEDAL = 0x2040005
1704+
1705+
/** Index manipulés par l'application — jamais les valeurs véhicule, voir [valeurPuissance]. */
1706+
object CustomDrive {
1707+
const val ECO_COMFORT = 0
1708+
const val NORMAL = 1
1709+
const val SPORT = 2
1710+
}
1711+
1712+
/**
1713+
* A9 : SWI69, SWI131, SWI132. Même ensemble que la clim, mais fonction distincte à dessein —
1714+
* changer la règle d'un domaine ne doit pas déplacer l'autre en silence.
1715+
*/
1716+
private fun isDriveCustomA9(): Boolean {
1717+
val gen = FirmwareInfo.getGeneration()
1718+
return gen == FirmwareInfo.Gen.SWI69 || gen == FirmwareInfo.Gen.SWI131 ||
1719+
gen == FirmwareInfo.Gen.SWI132
1720+
}
1721+
1722+
// ⚠️ TROIS ÉCHELLES DIFFÉRENTES, relevées dans le code d'origine des deux familles :
1723+
// • direction : 1/2/3 partout ;
1724+
// • pédale : 1/0/2 partout — Normal vaut ZÉRO, et non la valeur du milieu. Le smali SWI133
1725+
// réassigne le registre entre deux branches, ce qui se lit 1/2/3 si on va trop vite ;
1726+
// le dispatch A9 sur les libellés comfort/normal/sport donne le même résultat ;
1727+
// • puissance : 1/2/3 en old-SDK mais 2/3/4 sur A9, où la voiture réutilise l'échelle des
1728+
// modes de conduite (ÉCO=2, NORMAL=3, SPORT=4).
1729+
//
1730+
// D'où l'index 0/1/2 exposé au reste de l'application : personne d'autre ne manipule ces
1731+
// nombres, et une échelle qui changerait ne se corrige qu'ici.
1732+
private fun valeurPuissance(index: Int): Int = index + (if (isDriveCustomA9()) 2 else 1)
1733+
private fun valeurDirection(index: Int): Int = index + 1
1734+
private fun valeurPedale(index: Int): Int = when (index) {
1735+
CustomDrive.ECO_COMFORT -> 1
1736+
CustomDrive.SPORT -> 2
1737+
else -> 0
1738+
}
1739+
1740+
private fun indexPuissance(v: Int): Int? =
1741+
(v - (if (isDriveCustomA9()) 2 else 1)).takeIf { it in 0..2 }
1742+
private fun indexDirection(v: Int): Int? = (v - 1).takeIf { it in 0..2 }
1743+
private fun indexPedale(v: Int): Int? = when (v) {
1744+
1 -> CustomDrive.ECO_COMFORT
1745+
0 -> CustomDrive.NORMAL
1746+
2 -> CustomDrive.SPORT
1747+
else -> null
1748+
}
1749+
1750+
/** Puissance en chevaux du mode Personnalisé. [index] : 0=Éco, 1=Normal, 2=Sport. */
1751+
fun setCustomPower(index: Int): Boolean {
1752+
if (index !in 0..2) return false
1753+
val v = valeurPuissance(index)
1754+
if (logEnabled) AppLogger.i(TAG, "setCustomPower → index=$index valeur=$v")
1755+
return if (isDriveCustomA9()) callVsmVoid("setDrivingPowerTrainMode", v)
1756+
else setIntPropertyVpmRecovery(SIG_CUSTOM_POWER, v)
1757+
}
1758+
1759+
/** Direction du mode Personnalisé. [index] : 0=Confort, 1=Normal, 2=Sport. */
1760+
fun setCustomSteering(index: Int): Boolean {
1761+
if (index !in 0..2) return false
1762+
val v = valeurDirection(index)
1763+
if (logEnabled) AppLogger.i(TAG, "setCustomSteering → index=$index valeur=$v")
1764+
// A9 : setSteeringMode, et NON setDrivingEpsMode — les deux existent, seule la première
1765+
// est appelée par l'app d'origine. Vérifié en traçant fragment → presenter → model.
1766+
return if (isDriveCustomA9()) callVsmVoid("setSteeringMode", v)
1767+
else setIntPropertyVpmRecovery(SIG_CUSTOM_STEERING, v)
1768+
}
1769+
1770+
/** Force exercée sur la pédale. [index] : 0=Confort, 1=Normal, 2=Sport. */
1771+
fun setCustomPedal(index: Int): Boolean {
1772+
if (index !in 0..2) return false
1773+
val v = valeurPedale(index)
1774+
if (logEnabled) AppLogger.i(TAG, "setCustomPedal → index=$index valeur=$v")
1775+
return if (isDriveCustomA9()) callVsmVoid("setBrakePedalMode", v)
1776+
else setIntPropertyVpmRecovery(SIG_CUSTOM_PEDAL, v)
1777+
}
1778+
1779+
/**
1780+
* Lectures — `null` si le véhicule ne répond pas OU rend une valeur hors échelle.
1781+
*
1782+
* C'est le seul garde-fou honnête pour savoir si la voiture porte réellement ces réglages :
1783+
* ils existent sur les six firmwares, mais rien ne dit qu'ils sont montés sur toutes les
1784+
* finitions. Un `null` fait masquer la ligne plutôt que d'offrir un bouton sans effet.
1785+
*/
1786+
fun getCustomPower(): Int? = indexPuissance(
1787+
if (isDriveCustomA9()) (callVsm("getDrivingPowerTrainMode") as? Int) ?: -1
1788+
else getIntPropertyVpm(SIG_CUSTOM_POWER))
1789+
1790+
fun getCustomSteering(): Int? = indexDirection(
1791+
if (isDriveCustomA9()) (callVsm("getSteeringMode") as? Int) ?: -1
1792+
else getIntPropertyVpm(SIG_CUSTOM_STEERING))
1793+
1794+
fun getCustomPedal(): Int? = indexPedale(
1795+
if (isDriveCustomA9()) (callVsm("getBrakePedalMode") as? Int) ?: -1
1796+
else getIntPropertyVpm(SIG_CUSTOM_PEDAL))
1797+
16891798
fun setRegenLevel(level: RegenLevel): Boolean {
16901799
if (logEnabled) AppLogger.i(TAG, "setRegenLevel → ${level.label} (${level.value})")
16911800
return if (level == RegenLevel.ONE_PEDAL) {

‎app/src/main/java/com/mg4/control/model/DrivingProfile.kt‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ data class DrivingProfile(
5454
val escEnabled: Boolean? = null,
5555
val drowsinessEnabled: Boolean? = null,
5656
val drowsinessSensitivity: Int? = null, // 1=Faible, 2=Standard, 3=Élevé
57+
// ── Mode de conduite Personnalisé ────────────────────────────────────────
58+
//
59+
// Index 0/1/2 (Éco ou Confort, Normal, Sport) — jamais les valeurs véhicule, dont l'échelle
60+
// varie d'un firmware à l'autre. La conversion vit dans MG4Hardware.
61+
//
62+
// ⚠️ NULLABLES À DESSEIN, comme les chauffages : Gson n'appelle pas le constructeur Kotlin,
63+
// donc un profil enregistré avant cette fonctionnalité rend `null` et n'écrit RIEN. Un
64+
// `Int = 1` aurait rendu 0 (défaut de la JVM) et se serait appliqué comme « Éco/Confort »
65+
// sur tous les profils Personnalisé existants.
66+
//
67+
// Ils ne sont appliqués que si [driveMode] vaut CUSTOM : le véhicule les ignore ailleurs.
68+
val customPower: Int? = null,
69+
val customSteering: Int? = null,
70+
val customPedal: Int? = null,
5771
// ── Climatisation ────────────────────────────────────────────────────────────
5872
/**
5973
* Interrupteur du bloc clim. FAUX par défaut, et c'est ce que Gson donne aux profils

‎app/src/main/java/com/mg4/control/profile/ProfileApplier.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.mg4.control.profile
33
import com.mg4.control.debug.AppLogger
44
import com.mg4.control.hardware.MG4Hardware
55
import com.mg4.control.hardware.MG4Hardware.Swi68Mode
6+
import com.mg4.control.model.DriveMode
67
import com.mg4.control.model.DrivingProfile
78
import com.mg4.control.util.FirmwareInfo
89
import kotlinx.coroutines.CoroutineScope
@@ -117,6 +118,20 @@ object ProfileApplier {
117118
}
118119
}
119120

121+
// Mode Personnalisé — les trois réglages n'ont d'effet que dans ce mode, les
122+
// écrire ailleurs serait au mieux sans effet, au pire refusé par le véhicule.
123+
if (profile.driveMode == DriveMode.CUSTOM) {
124+
profile.customPower?.let {
125+
AppLogger.i(TAG, " Puissance personnalisée=$it → ${MG4Hardware.setCustomPower(it)}")
126+
}
127+
profile.customSteering?.let {
128+
AppLogger.i(TAG, " Direction personnalisée=$it → ${MG4Hardware.setCustomSteering(it)}")
129+
}
130+
profile.customPedal?.let {
131+
AppLogger.i(TAG, " Pédale personnalisée=$it → ${MG4Hardware.setCustomPedal(it)}")
132+
}
133+
}
134+
120135
// Climatisation — facultative, décochée par défaut. Enchaîne des bascules qui
121136
// prennent plusieurs secondes, mais on est déjà sur le dispatcher IO sous verrou :
122137
// rien d'autre n'écrit sur le véhicule pendant ce temps.

‎app/src/main/java/com/mg4/control/ui/DashboardFragment.kt‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.fragment.app.Fragment
1717
import com.google.android.material.button.MaterialButton
1818
import com.google.android.material.slider.Slider
1919
import com.mg4.control.R
20+
import com.mg4.control.debug.AppLogger
2021
import com.mg4.control.hardware.MG4Hardware
2122
import com.mg4.control.hardware.MG4Hardware.AebMode
2223
import com.mg4.control.hardware.MG4Hardware.AebSensitivity
@@ -86,6 +87,12 @@ class DashboardFragment : Fragment() {
8687
/** Dernier mode de conduite connu — nécessaire pour arbitrer les exclusions SNOW / Éco énergie. */
8788
private var currentDriveMode: DriveMode? = null
8889

90+
// ── Mode Personnalisé — puissance / direction / pédale ───────────────────
91+
// Une entrée par ligne : le bouton, et l'index qu'il représente (0/1/2).
92+
private val customPowerButtons = mutableMapOf<Int, Button>()
93+
private val customSteerButtons = mutableMapOf<Int, Button>()
94+
private val customPedalButtons = mutableMapOf<Int, Button>()
95+
8996
// ── AEB : page 0 pour VSM-based, page 1 (SWI133) pour les autres ───────────
9097
private var switchAeb: Switch? = null
9198
private var btnAebAlarm: Button? = null
@@ -320,6 +327,17 @@ class DashboardFragment : Fragment() {
320327
switchSoundWarning = view.findViewById(R.id.switch_sound_warning)
321328
alertsGroupSwi133 = view.findViewById(R.id.alerts_group_swi133)
322329

330+
// Mode Personnalisé — trois lignes de trois boutons
331+
customPowerButtons[0] = view.findViewById(R.id.btn_cd_power_eco)
332+
customPowerButtons[1] = view.findViewById(R.id.btn_cd_power_normal)
333+
customPowerButtons[2] = view.findViewById(R.id.btn_cd_power_sport)
334+
customSteerButtons[0] = view.findViewById(R.id.btn_cd_steer_comfort)
335+
customSteerButtons[1] = view.findViewById(R.id.btn_cd_steer_normal)
336+
customSteerButtons[2] = view.findViewById(R.id.btn_cd_steer_sport)
337+
customPedalButtons[0] = view.findViewById(R.id.btn_cd_pedal_comfort)
338+
customPedalButtons[1] = view.findViewById(R.id.btn_cd_pedal_normal)
339+
customPedalButtons[2] = view.findViewById(R.id.btn_cd_pedal_sport)
340+
323341
// TSR + Économie d'énergie
324342
switchTsr = view.findViewById(R.id.switch_tsr)
325343
btnEnergySaving = view.findViewById(R.id.btn_energy_saving)
@@ -363,6 +381,22 @@ class DashboardFragment : Fragment() {
363381
}
364382
}
365383

384+
// Mode Personnalisé — chaque ligne écrit son réglage et rien d'autre.
385+
listOf(
386+
Triple(customPowerButtons, MG4Hardware::setCustomPower, "puissance"),
387+
Triple(customSteerButtons, MG4Hardware::setCustomSteering, "direction"),
388+
Triple(customPedalButtons, MG4Hardware::setCustomPedal, "pédale")
389+
).forEach { (boutons, ecrire, nom) ->
390+
boutons.forEach { (index, btn) ->
391+
btn.setOnClickListener {
392+
highlightCustomRow(boutons, index)
393+
CoroutineScope(Dispatchers.IO).launch {
394+
AppLogger.i("MG4_DASH", "mode personnalisé — $nom = $index → ${ecrire(index)}")
395+
}
396+
}
397+
}
398+
}
399+
366400
// Regen
367401
regenButtons.forEach { (level, btn) ->
368402
btn.setOnClickListener {
@@ -906,8 +940,68 @@ class DashboardFragment : Fragment() {
906940
setRegenEnabled(!active && currentDriveMode != DriveMode.SNOW)
907941
}
908942

943+
/** Surligne l'option retenue d'une ligne du mode Personnalisé. */
944+
private fun highlightCustomRow(boutons: Map<Int, Button>, actif: Int?) {
945+
boutons.forEach { (index, btn) ->
946+
val on = index == actif
947+
btn.backgroundTintList = ColorStateList.valueOf(if (on) colorActive else colorInactive)
948+
btn.setTextColor(if (on) colorTextActive else colorTextInactive)
949+
}
950+
}
951+
952+
/**
953+
* Révèle la carte du mode Personnalisé et y reporte l'état du véhicule.
954+
*
955+
* Deux niveaux de masquage, et ils ne disent pas la même chose :
956+
* • la CARTE n'apparaît que si la voiture est réellement en mode Personnalisé — ces trois
957+
* réglages n'ont aucun effet ailleurs ;
958+
* • une LIGNE disparaît si le véhicule ne rend pas son état. Les six firmwares exposent la
959+
* commande, mais rien ne garantit que la finition porte l'équipement — une direction à
960+
* assistance variable, par exemple. Un bouton qui n'écrirait nulle part vaut moins que
961+
* pas de bouton du tout.
962+
*/
963+
private fun refreshCustomDrive(mode: DriveMode?) {
964+
val vue = view ?: return
965+
val carte = vue.findViewById<View>(R.id.section_custom_drive) ?: return
966+
if (mode != DriveMode.CUSTOM) {
967+
carte.visibility = View.GONE
968+
return
969+
}
970+
CoroutineScope(Dispatchers.IO).launch {
971+
val puissance = MG4Hardware.getCustomPower()
972+
val direction = MG4Hardware.getCustomSteering()
973+
val pedale = MG4Hardware.getCustomPedal()
974+
withContext(Dispatchers.Main) {
975+
if (!isAdded) return@withContext
976+
// L'état a pu changer pendant la lecture (l'utilisateur quitte le mode) : on
977+
// revérifie avant d'afficher, sinon la carte réapparaîtrait toute seule.
978+
if (currentDriveMode != DriveMode.CUSTOM) { carte.visibility = View.GONE; return@withContext }
979+
val lignes = listOf(
980+
Triple(customPowerButtons, puissance, R.id.row_cd_power),
981+
Triple(customSteerButtons, direction, R.id.row_cd_steer),
982+
Triple(customPedalButtons, pedale, R.id.row_cd_pedal)
983+
)
984+
lignes.forEach { (boutons, valeur, rowId) ->
985+
vue.findViewById<View>(rowId)?.visibility =
986+
if (valeur == null) View.GONE else View.VISIBLE
987+
highlightCustomRow(boutons, valeur)
988+
}
989+
// Aucune ligne lisible : la carte n'aurait qu'un titre à montrer.
990+
val lisible = lignes.any { it.second != null }
991+
carte.visibility = if (lisible) View.VISIBLE else View.GONE
992+
// Trois lectures nulles d'un coup, c'est plus vraisemblablement une couche
993+
// véhicule pas encore prête qu'une voiture dépourvue des trois équipements.
994+
// Même repli que refreshDriveRegen, qui réessaie pour la même raison.
995+
if (!lisible) view?.postDelayed({
996+
if (isAdded && currentDriveMode == DriveMode.CUSTOM) refreshCustomDrive(currentDriveMode)
997+
}, 3_000)
998+
}
999+
}
1000+
}
1001+
9091002
private fun applyDriveModeUI(mode: DriveMode) {
9101003
currentDriveMode = mode
1004+
refreshCustomDrive(mode)
9111005
driveModeButtons.forEach { (m, btn) ->
9121006
val (bg, text) = when {
9131007
m != mode -> colorInactive to colorTextInactive

0 commit comments

Comments
 (0)