Skip to content

Commit ef85034

Browse files
committed
Add pop-up HVAC V1
1 parent e1edeed commit ef85034

22 files changed

Lines changed: 457 additions & 133 deletions

File tree

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

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.mg4.control.model.AirFlow
2323
import com.mg4.control.model.BatteryHeating
2424
import com.mg4.control.model.CustomDriveScale
2525
import com.mg4.control.model.DriveMode
26+
import com.mg4.control.model.HvacPopup
2627
import com.mg4.control.model.RegenLevel
2728
import com.mg4.control.util.FirmwareInfo
2829
import com.mg4.control.util.GarageMode
@@ -5477,8 +5478,17 @@ object MG4Hardware {
54775478
val defrostFront: Boolean?,
54785479
val defrostRear: Boolean?,
54795480
/** Sens de l'air, valeur brute 0–7 ([AirFlow]) ; null si illisible. */
5480-
val airFlow: Int?
5481-
)
5481+
val airFlow: Int?,
5482+
/** Dernier niveau de ventilation réglé à la main ([lastManualFan]) ; null si jamais vu. */
5483+
val lastManualFan: Int? = null
5484+
) {
5485+
/**
5486+
* Température et ventilation vues par la logique commune au pop-up HVAC, à la page Clim
5487+
* et aux raccourcis — c'est elle qui sait qu'en AUTO le niveau annoncé (15) n'en est pas un.
5488+
*/
5489+
fun reglages() = HvacPopup.Reglages(
5490+
tempC, tempMin, tempMax, fanLevel, fanMin, fanMax, autoOn, lastManualFan)
5491+
}
54825492

54835493
/**
54845494
* Vrai si le firmware expose le SDK clim SAIC (old-SDK : SWI133/68/165 ; absent sur A9).
@@ -5508,8 +5518,36 @@ object MG4Hardware {
55085518
*/
55095519
fun getClimateState(): ClimateState? =
55105520
(if (isClimateA9()) getClimateStateA9() else getClimateStateOldSdk())
5521+
?.let { it.copy(lastManualFan = rememberManualFan(it)) }
55115522
?.also { traceAirFlowChange(it) }
55125523

5524+
// ── Dernier niveau de ventilation réglé à la main ─────────────────────────
5525+
// En AUTO la voiture n'annonce plus de cran : elle rend 15, que les écrans d'origine affichent
5526+
// « AUTO ». Pour en sortir, ces écrans écrivent le dernier niveau qu'ils ont vu, ± 1 ; ils le
5527+
// gardent en mémoire ET en préférences (`mLastVolume` / « air_volume » dans AcCardView du
5528+
// launcher SWI133 et HvacView de SystemUI SWI165). On fait de même.
5529+
private const val PREF_LAST_MANUAL_FAN = "clim_last_manual_fan"
5530+
@Volatile private var sLastManualFan: Int? = null
5531+
5532+
/** Retient le niveau lu s'il en est un vrai, et rend le dernier connu. */
5533+
private fun rememberManualFan(state: ClimateState): Int? {
5534+
state.reglages().ventilationReelle
5535+
?.takeIf { it in state.fanMin..state.fanMax }
5536+
?.let { noteManualFan(it) }
5537+
return lastManualFan()
5538+
}
5539+
5540+
private fun noteManualFan(level: Int) {
5541+
if (sLastManualFan == level) return
5542+
sLastManualFan = level
5543+
sAppContext?.getSharedPreferences("mg4_settings", 0)?.edit()
5544+
?.putInt(PREF_LAST_MANUAL_FAN, level)?.apply()
5545+
}
5546+
5547+
private fun lastManualFan(): Int? = sLastManualFan
5548+
?: sAppContext?.getSharedPreferences("mg4_settings", 0)
5549+
?.getInt(PREF_LAST_MANUAL_FAN, -1)?.takeIf { it >= 0 }?.also { sLastManualFan = it }
5550+
55135551
private fun getClimateStateOldSdk(): ClimateState? {
55145552
if (sAirCondition == null) sAppContext?.let { initAirCondition(it) }
55155553
if (sAirCondition == null) return null
@@ -5611,11 +5649,13 @@ object MG4Hardware {
56115649
else
56125650
acSet("setDrvTemp", celsius).also { climLog("temp=$celsius", it) }
56135651

5652+
/** Écrire un niveau fait aussi SORTIR du mode AUTO (voir [setClimateAuto]). */
56145653
fun setClimateFan(level: Int): Boolean =
5615-
if (isClimateA9())
5654+
(if (isClimateA9())
56165655
a9Set("setFanSpeed", level).also { climLog("A9 fan=$level", it) }
56175656
else
5618-
acSet("setAirVolumeLevel", level).also { climLog("fan=$level", it) }
5657+
acSet("setAirVolumeLevel", level).also { climLog("fan=$level", it) })
5658+
.also { if (it) noteManualFan(level) }
56195659

56205660
/**
56215661
* A/C — bascule, comme la recirculation. L'encodage de lecture (1=ON, 0=OFF) a été mesuré
@@ -5630,11 +5670,37 @@ object MG4Hardware {
56305670
acSet("setAcStatus", 1)
56315671
}
56325672

5633-
fun setClimateAuto(on: Boolean): Boolean =
5634-
if (isClimateA9())
5635-
a9CycleTo("auto", "getAutoStatus", if (on) 1 else 0, 2, "switchAutoStatus")
5636-
else
5637-
acSet("setAutoStatus", if (on) 1 else 0).also { climLog("auto=$on", it) }
5673+
/**
5674+
* Mode AUTO : la voiture règle elle-même la ventilation et le sens de l'air.
5675+
*
5676+
* ⚠️ Sur l'ancien SDK ce n'est PAS une affectation mais un APPUI, comme l'A/C : l'écran clim
5677+
* d'origine (HvacView, SystemUI SWI165) envoie `setAutoStatus(1)` à chaque clic, jamais 0.
5678+
* Un 0 reste sans effet — c'est pourquoi AUTO ne se coupait pas (constaté sur SWI133 le
5679+
* 2026-10-09). Et ces écrans ne « coupent » jamais AUTO : ils en SORTENT en écrivant un
5680+
* niveau de ventilation, le dernier réglé à la main. On fait pareil.
5681+
*
5682+
* On n'appuie pas si AUTO est déjà actif : rien ne dit qu'un second appui ne le couperait pas.
5683+
*/
5684+
fun setClimateAuto(on: Boolean): Boolean {
5685+
if (isClimateA9()) {
5686+
// Bascule relue, validée sur SWI131 ; si elle ne suffit pas à couper, même repli.
5687+
if (a9CycleTo("auto", "getAutoStatus", if (on) 1 else 0, 2, "switchAutoStatus")) return true
5688+
return !on && quitterAutoParLaVentilation()
5689+
}
5690+
val actif = acInt("getAutoStatus")?.takeIf { it >= 0 }?.let { it == 1 }
5691+
if (actif == on) {
5692+
climLog("auto=$on (déjà)", true)
5693+
return true
5694+
}
5695+
return if (on) acSet("setAutoStatus", 1).also { climLog("auto=true (appui)", it) }
5696+
else quitterAutoParLaVentilation()
5697+
}
5698+
5699+
private fun quitterAutoParLaVentilation(): Boolean {
5700+
val etat = getClimateState() ?: return false
5701+
val niveau = HvacPopup.niveauDeSortie(etat.reglages())
5702+
return setClimateFan(niveau).also { climLog("auto=false → ventilation manuelle $niveau", it) }
5703+
}
56385704

56395705
/**
56405706
* Recirculation — commande **CYCLIQUE**, pas une affectation.

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

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,102 @@ import com.mg4.control.accessibility.JoystickFocus
88
* Ici le joystick ne déplace aucun focus, contrairement aux popups de profils : chaque direction
99
* agit directement, comme dans la fenêtre clim d'origine de la voiture.
1010
*
11-
* | | Action |
12-
* |----------|------------------|
13-
* | Haut | température + 1 |
14-
* | Bas | température − 1 |
15-
* | Droite | ventilation + 1 |
16-
* | Gauche | ventilation − 1 |
17-
* | Clic | ventilation AUTO |
11+
* | | Action |
12+
* |----------|------------------------------------------|
13+
* | Haut | température + 1 |
14+
* | Bas | température − 1 |
15+
* | Droite | ventilation + 1 |
16+
* | Gauche | ventilation − 1 |
17+
* | Clic | mode AUTO, ou retour au réglage manuel |
1818
*
1919
* La fenêtre se ferme au doigt, par le raccourci qui l'a ouverte, ou seule après son délai.
20+
*
21+
* ## Le mode AUTO
22+
*
23+
* En AUTO la voiture règle elle-même la ventilation (et le sens de l'air), et n'annonce plus un
24+
* cran mais **15** : c'est à ce 15 que les écrans d'origine reconnaissent AUTO et l'affichent
25+
* (`HvacControlDialog` et `AcCardView` de SWI133, `HvacView` de SWI165). Ces mêmes écrans ne
26+
* « coupent » jamais AUTO : ils en SORTENT en écrivant un niveau de ventilation — le dernier
27+
* réglé à la main, plus ou moins un cran. C'est la règle reprise ici, et partagée avec la page
28+
* Clim et les raccourcis « ventilation ± ».
2029
*/
2130
object HvacPopup {
2231

2332
sealed class Action {
2433
data class Temperature(val cible: Int) : Action()
34+
35+
/** Un niveau de ventilation écrit à la main — c'est aussi ce qui fait sortir d'AUTO. */
2536
data class Ventilation(val cible: Int) : Action()
26-
/** Ventilation automatique : la case centrale de la croix. */
27-
data class Auto(val actif: Boolean) : Action()
37+
38+
/** Passer en mode AUTO : la case centrale de la croix. */
39+
object Auto : Action()
2840
}
2941

3042
/** Ce qui est lu sur la voiture ; les bornes en viennent aussi. null = illisible. */
3143
data class Reglages(
3244
val temp: Int?, val tempMin: Int, val tempMax: Int,
3345
val fan: Int?, val fanMin: Int, val fanMax: Int,
34-
/** Ventilation AUTO ; null = illisible. */
46+
/** Mode AUTO annoncé par la voiture ; null = illisible. */
3547
val auto: Boolean? = null,
48+
/** Dernier niveau de ventilation réglé à la main, retenu avant AUTO ; null = jamais vu. */
49+
val dernierNiveau: Int? = null,
3650
) {
3751
/**
38-
* Niveau de ventilation à montrer et à régler, ou null s'il n'y en a pas : en AUTO la
39-
* voiture annonce 15, qui n'est pas un cran (constaté sur SWI133 le 2026-10-09). Tout ce
40-
* qui dépasse le maximum est traité de même — ce 15 peut traîner un instant après AUTO.
52+
* AUTO actif : la voiture le dit, ou son niveau de ventilation le trahit — au-delà du
53+
* maximum ce n'est plus un cran (15), et c'est à cela que les écrans d'origine s'y fient.
4154
*/
42-
val ventilationReelle: Int? get() = fan?.takeIf { auto != true && it <= fanMax }
55+
val enAuto: Boolean get() = auto == true || (fan != null && fan > fanMax)
56+
57+
/** Niveau de ventilation à montrer et à régler ; null en AUTO ou s'il est illisible. */
58+
val ventilationReelle: Int? get() = fan?.takeIf { !enAuto }
4359
}
4460

4561
/** Action portée par [commande], ou null s'il n'y a rien à écrire (butée, valeur illisible). */
4662
fun action(commande: JoystickFocus.Commande, reglages: Reglages): Action? = when (commande) {
4763
JoystickFocus.Commande.HAUT -> pas(reglages.temp, reglages.tempMin, reglages.tempMax, +1)?.let { Action.Temperature(it) }
4864
JoystickFocus.Commande.BAS -> pas(reglages.temp, reglages.tempMin, reglages.tempMax, -1)?.let { Action.Temperature(it) }
49-
JoystickFocus.Commande.DROITE -> ventilation(reglages, +1)
50-
JoystickFocus.Commande.GAUCHE -> ventilation(reglages, -1)
51-
// Sans état lisible on ne bascule pas : c'est l'inverse de l'intention qui pourrait partir.
52-
JoystickFocus.Commande.VALIDER -> reglages.auto?.let { Action.Auto(!it) }
65+
JoystickFocus.Commande.DROITE -> pasVentilation(reglages, +1)?.let { Action.Ventilation(it) }
66+
JoystickFocus.Commande.GAUCHE -> pasVentilation(reglages, -1)?.let { Action.Ventilation(it) }
67+
JoystickFocus.Commande.VALIDER -> when {
68+
// Rendre la main : le dernier niveau, tel quel.
69+
reglages.enAuto -> Action.Ventilation(niveauDeSortie(reglages))
70+
reglages.auto == false -> Action.Auto
71+
// Sans état lisible on n'appuie pas : sur l'ancien SDK la commande est un APPUI, et
72+
// rien ne dit qu'un second ne couperait pas le mode qu'on voulait activer.
73+
else -> null
74+
}
5375
}
5476

5577
/**
56-
* En AUTO, un geste sur la ventilation en SORT sans écrire de niveau : il n'y a pas de cran
57-
* d'où partir, et partir du 15 annoncé écrirait le maximum même pour un « moins ». La voiture
58-
* annonce ensuite son vrai niveau, et c'est le geste suivant qui le règle.
78+
* Niveau de ventilation à écrire pour un cran de plus ([sens] = +1) ou de moins (−1) ; null
79+
* s'il n'y a rien à écrire (butée, niveau illisible).
80+
*
81+
* En AUTO on repart du dernier niveau réglé à la main — surtout pas du 15 annoncé, d'où un
82+
* « moins » écrirait le MAXIMUM — et on écrit MÊME en butée : c'est cette écriture qui rend
83+
* la main, et l'utilisateur vient de demander à régler lui-même.
84+
*/
85+
fun pasVentilation(reglages: Reglages, sens: Int): Int? =
86+
if (reglages.enAuto) (niveauDeSortie(reglages) + sens).coerceIn(reglages.fanMin, reglages.fanMax)
87+
else pas(reglages.fan, reglages.fanMin, reglages.fanMax, sens)
88+
89+
/**
90+
* Niveau à écrire pour quitter AUTO sans rien changer d'autre : le dernier réglé à la main.
91+
* Faute de l'avoir jamais vu (application neuve, voiture en AUTO depuis toujours), le milieu
92+
* de l'échelle — ni le minimum ni le maximum ne feraient un point de départ raisonnable.
5993
*/
60-
private fun ventilation(reglages: Reglages, sens: Int): Action? =
61-
if (reglages.auto == true) Action.Auto(false)
62-
else pas(reglages.ventilationReelle, reglages.fanMin, reglages.fanMax, sens)?.let { Action.Ventilation(it) }
94+
fun niveauDeSortie(reglages: Reglages): Int =
95+
(reglages.dernierNiveau ?: ((reglages.fanMin + reglages.fanMax) / 2))
96+
.coerceIn(reglages.fanMin, reglages.fanMax)
6397

6498
// ── Taille de la fenêtre ─────────────────────────────────────────────────
65-
private const val LARGEUR_DP = 780f // la carte de overlay_hvac_popup.xml
99+
private const val LARGEUR_DP = 900f // la carte de overlay_hvac_popup.xml
66100
private const val HAUTEUR_DP = 390f // sa hauteur en texte « Standard »
67-
private const val PART_LARGEUR = 0.56f
101+
private const val PART_LARGEUR = 0.63f
68102
private const val PART_HAUTEUR = 0.72f
69103

70104
/**
71105
* Agrandissement à appliquer à la fenêtre, dessinée pour [LARGEUR_DP] × [HAUTEUR_DP] dp, pour
72-
* qu'elle prenne la même part de l'ÉCRAN quelle que soit sa densité : au plus 56 % de la
106+
* qu'elle prenne la même part de l'ÉCRAN quelle que soit sa densité : au plus 63 % de la
73107
* largeur et 72 % de la hauteur. Le reste de la hauteur laisse passer la barre d'état, le
74108
* texte « Très grand » et la ligne d'avertissement sans déborder.
75109
*

0 commit comments

Comments
 (0)