Skip to content

Commit 7e3add2

Browse files
committed
Change how heated seats are managed
1 parent aad6e16 commit 7e3add2

9 files changed

Lines changed: 146 additions & 95 deletions

File tree

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

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,14 +1682,15 @@ object MG4Hardware {
16821682
/**
16831683
* Niveau d'un chauffage à crans (sièges chauffants).
16841684
*
1685-
* La voiture ne sait qu'AVANCER d'un cran : écrire 1 dans la propriété est un appui, comme
1686-
* sur l'écran d'origine. Pour un niveau précis, [SeatHeat.reach] lit UNE fois, envoie le
1687-
* nombre de crans qui convient, puis vérifie sans jamais recliquer.
1685+
* La voiture ne connaît qu'une commande, « un cran » : écrire 1 dans la propriété est un
1686+
* appui, comme sur l'écran d'origine, et chaque cran fait DESCENDRE — éteint → 3 → 2 → 1 →
1687+
* éteint (établi sur SWI133 le 2026-10-09, voir [SeatHeat]). Pour un niveau précis,
1688+
* [SeatHeat.reach] lit UNE fois, envoie le nombre de crans qui convient, puis vérifie sans
1689+
* jamais recliquer. Monter d'un niveau coûte donc trois crans, en passant par « éteint ».
16881690
*
1689-
* L'ancienne boucle (lire, cliquer, relire dans les 0,7 s, recliquer) envoyait un cran de
1690-
* trop dès que la voiture tardait à annoncer le nouveau niveau, puis refaisait tout le tour :
1691-
* « 1 → 2 » donnait 2, 3, éteint, 1, 2 (constaté sur SWI133 le 2026-10-09). Et sans niveau
1692-
* lisible, elle cliquait à l'aveugle pendant 7 secondes.
1691+
* L'ancienne boucle (lire, cliquer, relire, recliquer jusqu'à lire la cible) arrivait au même
1692+
* résultat sans connaître le sens, mais cliquait à l'aveugle pendant 7 secondes quand le
1693+
* niveau était illisible, et pouvait recliquer sur une lecture en retard.
16931694
*
16941695
* ⚠️ Bloquant (0,6 s par cran, puis jusqu'à 3 s de vérification) → hors du thread principal.
16951696
*/
@@ -1714,8 +1715,9 @@ object MG4Hardware {
17141715
}
17151716

17161717
/**
1717-
* UN cran de plus, sans viser de niveau : c'est la commande même de la voiture, et tout ce
1718-
* qu'il faut pour « niveau suivant » (pop-up HVAC, raccourci siège chauffant).
1718+
* UN cran, sans viser de niveau : c'est la commande même de la voiture, et tout ce qu'il faut
1719+
* pour « niveau suivant » (pop-up HVAC, raccourci siège chauffant). Le niveau obtenu est
1720+
* [SeatHeat.next] : éteint → 3 → 2 → 1 → éteint.
17191721
*/
17201722
private fun stepHvacLevel(propId: Int, areaId: Int): Boolean =
17211723
synchronized(hvacClickLock(propId)) { clickHvac(propId, areaId) }
@@ -2078,7 +2080,7 @@ object MG4Hardware {
20782080
return setHvacLevelWithToggle(PROP_SEAT_HEAT_R, AREA_HVAC, level)
20792081
}
20802082

2081-
/** Siège chauffant gauche : niveau suivant (éteint → 1 → 2 → 3 → éteint), en UN cran. */
2083+
/** Siège chauffant gauche : niveau suivant (éteint → 3 → 2 → 1 → éteint), en UN cran. */
20822084
fun stepSeatHeatLeft(): Boolean {
20832085
if (logEnabled) AppLogger.i(TAG, "stepSeatHeatLeft → un cran")
20842086
return stepHvacLevel(PROP_SEAT_HEAT_L, AREA_HVAC)

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,12 @@ object HvacPopup {
9696
.coerceIn(reglages.fanMin, reglages.fanMax)
9797

9898
// ── Sièges chauffants (firmwares équipés seulement) ──────────────────────
99-
private const val NIVEAU_SIEGE_MAX = 3
100-
101-
/**
102-
* Niveau d'un siège chauffant après un appui : éteint → 1 → 2 → 3 → éteint, comme le
103-
* raccourci. null si le niveau est illisible — partir d'un 0 supposé alors que le siège est
104-
* à 3 ferait DESCENDRE le chauffage.
105-
*/
106-
fun niveauSiegeSuivant(actuel: Int?): Int? =
107-
actuel?.let { (it.coerceIn(0, NIVEAU_SIEGE_MAX) + 1) % (NIVEAU_SIEGE_MAX + 1) }
99+
// L'ordre des niveaux et le compte des crans vivent dans [SeatHeat].
108100

109101
/** Niveau d'un siège en pastilles pleines et vides : « ● ● ○ » pour 2. */
110102
fun pastilles(niveau: Int): String {
111-
val plein = niveau.coerceIn(0, NIVEAU_SIEGE_MAX)
112-
return (1..NIVEAU_SIEGE_MAX).joinToString(" ") { if (it <= plein) "●" else "○" }
103+
val plein = niveau.coerceIn(0, SeatHeat.MAX_LEVEL)
104+
return (1..SeatHeat.MAX_LEVEL).joinToString(" ") { if (it <= plein) "●" else "○" }
113105
}
114106

115107
// ── Taille de la fenêtre ─────────────────────────────────────────────────

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,54 @@
11
package com.mg4.control.model
22

33
/**
4-
* Sièges chauffants — atteindre un niveau sur une voiture qui ne sait qu'AVANCER d'un cran.
4+
* Sièges chauffants — atteindre un niveau sur une voiture qui ne connaît qu'une commande : « un
5+
* cran ».
56
*
6-
* La commande du véhicule est un appui : éteint → 1 → 2 → 3 → éteint, toujours dans ce sens
7-
* (l'écran clim d'origine envoie 1 à chaque clic). Et la voiture annonce son nouveau niveau avec
8-
* retard. Lire, cliquer, relire, recliquer ne marche donc pas : un cran part en trop dès que la
9-
* lecture tarde, et il faut refaire tout le tour — « 1 → 2 » donnait 2, 3, éteint, 1, 2
10-
* (constaté sur SWI133 le 2026-10-09).
7+
* L'écran clim d'origine envoie 1 à chaque clic et c'est la voiture qui choisit le niveau
8+
* suivant ; le sens n'est écrit nulle part dans le firmware. Il a été établi sur véhicule
9+
* (SWI133, 2026-10-09) : **chaque cran fait DESCENDRE** — éteint → 3 → 2 → 1 → éteint. Sept
10+
* essais sur les boutons de niveau, faits alors que le compte supposait à tort que le cran
11+
* montait, l'ont montré sans ambiguïté (demander 1 depuis éteint donnait 3, demander 2 depuis 3
12+
* éteignait le siège…) ; ils sont rejoués dans les tests.
1113
*
12-
* D'où la règle : on lit UNE fois, on compte, on envoie, et on ne reclique jamais d'après une
13-
* relecture. Sans vue ni véhicule : la lecture, le cran et l'attente sont fournis par l'appelant.
14+
* Conséquence : descendre d'un niveau coûte un cran, MONTER d'un niveau en coûte trois, en
15+
* passant par « éteint ». Aucun compte ne peut y échapper.
16+
*
17+
* La voiture annonce aussi son nouveau niveau avec retard : on lit donc UNE fois, on compte, on
18+
* envoie, puis on vérifie sans jamais recliquer. Sans vue ni véhicule : la lecture, le cran et
19+
* l'attente sont fournis par l'appelant.
1420
*/
1521
object SeatHeat {
1622

1723
const val MAX_LEVEL = 3
24+
private const val CYCLE = MAX_LEVEL + 1
1825

1926
/** Écart entre deux crans : trop rapprochés, la voiture pourrait en ignorer un. */
2027
const val CLICK_INTERVAL_MS = 600L
2128

22-
/** Délai après lequel le niveau LU reflète un cran qui vient de partir. */
23-
const val READBACK_MS = 1_200L
29+
/**
30+
* Délai après lequel le niveau LU reflète un cran qui vient de partir. Valeur prudente, non
31+
* mesurée : compter d'après une lecture en retard enverrait un cran de trop ou de moins.
32+
*/
33+
const val READBACK_MS = 2_000L
2434

2535
private const val VERIFY_POLL_MS = 250L
2636
private const val VERIFY_TIMEOUT_MS = 3_000L
2737

38+
/**
39+
* Niveau où un cran amène la voiture : éteint → 3 → 2 → 1 → éteint. C'est ce qu'il faut
40+
* annoncer après un appui (pop-up HVAC, raccourci) ; null si le niveau de départ est illisible.
41+
*/
42+
fun next(actuel: Int?): Int? =
43+
actuel?.let { (it.coerceIn(0, MAX_LEVEL) + CYCLE - 1) % CYCLE }
44+
2845
/**
2946
* Nombre de crans à envoyer pour passer de [actuel] à [cible] ; null si l'un des deux n'est
30-
* pas un niveau (lecture en échec, cible inconnue). Descendre d'un niveau en coûte trois :
31-
* la voiture ne recule pas.
47+
* pas un niveau (lecture en échec, cible inconnue).
3248
*/
3349
fun steps(actuel: Int, cible: Int): Int? {
3450
if (actuel !in 0..MAX_LEVEL || cible !in 0..MAX_LEVEL) return null
35-
val cycle = MAX_LEVEL + 1
36-
return ((cible - actuel) % cycle + cycle) % cycle
51+
return ((actuel - cible) % CYCLE + CYCLE) % CYCLE
3752
}
3853

3954
/**
@@ -43,7 +58,10 @@ object SeatHeat {
4358
* Rend false sans rien envoyer si le niveau est illisible — envoyer des crans à l'aveugle
4459
* ferait tourner le siège sans fin — et false aussi si la voiture n'a pas annoncé [cible] au
4560
* bout de la vérification. Dans ce cas on n'insiste pas : avec une lecture en retard, un cran
46-
* de rattrapage serait justement le cran de trop.
61+
* de rattrapage partirait d'un niveau faux.
62+
*
63+
* La vérification ne peut pas être trompée par un niveau traversé en route : une série compte
64+
* au plus trois crans, donc quatre niveaux distincts, et [cible] est le dernier.
4765
*
4866
* @param lire niveau annoncé par la voiture (négatif = illisible)
4967
* @param cran envoie UN cran

‎app/src/main/java/com/mg4/control/service/HvacPopupOverlay.kt‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.mg4.control.debug.AppLogger
2525
import com.mg4.control.hardware.MG4Hardware
2626
import com.mg4.control.model.AirFlow
2727
import com.mg4.control.model.HvacPopup
28+
import com.mg4.control.model.SeatHeat
2829
import com.mg4.control.util.FirmwareInfo
2930
import com.mg4.control.util.LocaleHelper
3031
import java.util.Locale
@@ -336,16 +337,16 @@ object HvacPopupOverlay {
336337
}
337338

338339
/**
339-
* Siège chauffant : un appui = niveau suivant (éteint → 1 → 2 → 3 → éteint), comme le
340-
* raccourci — et donc UN cran envoyé, puisque c'est la commande même de la voiture. Viser le
341-
* niveau par la boucle de recherche faisait faire un tour complet au siège (2, 3, éteint, 1,
342-
* 2 pour passer de 1 à 2). Deux appuis rapprochés envoient deux crans, espacés par le matériel.
340+
* Siège chauffant : un appui = UN cran envoyé, la commande même de la voiture. Le niveau
341+
* suivant est donc le sien — éteint → 3 → 2 → 1 → éteint ([SeatHeat.next]) — et c'est lui
342+
* qu'on affiche aussitôt, sans attendre la relecture. Deux appuis rapprochés envoient deux
343+
* crans, espacés par le matériel.
343344
*/
344345
private fun cyclerSiege(gauche: Boolean) {
345346
val c = chauffages ?: return
346347
relancerDelai()
347348
val actuel = if (gauche) c.siegeGauche else c.siegeDroit
348-
val suivant = HvacPopup.niveauSiegeSuivant(actuel) ?: return
349+
val suivant = SeatHeat.next(actuel) ?: return
349350
val cote = if (gauche) "gauche" else "droit"
350351
AppLogger.i(TAG, "Pop-up HVAC — siège chauffant $cote : $actuel → $suivant")
351352
chauffages = if (gauche) c.copy(siegeGauche = suivant) else c.copy(siegeDroit = suivant)

‎app/src/main/java/com/mg4/control/service/MG4ControlService.kt‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import com.mg4.control.hardware.MG4Hardware.AebMode
3939
import com.mg4.control.hardware.MG4Hardware.Swi68Mode
4040
import com.mg4.control.model.HvacPopup
4141
import com.mg4.control.model.RegenLevel
42+
import com.mg4.control.model.SeatHeat
4243
import com.mg4.control.model.WindowCommand
4344
import com.mg4.control.profile.ActiveProfile
4445
import com.mg4.control.profile.ProfileApplier
@@ -824,11 +825,10 @@ class MG4ControlService : Service() {
824825
AppLogger.w(TAG, "SHORTCUT siège chauffant — niveau illisible, aucune action")
825826
return
826827
}
827-
val suivant = ExternalApi.cycleStep(actuel, 0, 3, 1)
828+
// Un appui = UN cran, la commande même de la voiture. Le niveau suivant est le
829+
// sien : éteint → 3 → 2 → 1 → éteint (établi sur véhicule, voir SeatHeat).
828830
AppLogger.i(TAG, "SHORTCUT siège chauffant ${if (gauche) "gauche" else "droit"} " +
829-
": $actuel → $suivant (un cran)")
830-
// « Niveau suivant » est la commande même de la voiture : UN cran suffit. Viser
831-
// `suivant` par la recherche de niveau faisait faire un tour complet au siège.
831+
": $actuel → ${SeatHeat.next(actuel)} (un cran)")
832832
if (gauche) MG4Hardware.stepSeatHeatLeft()
833833
else MG4Hardware.stepSeatHeatRight()
834834
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,20 @@ class DashboardFragment : Fragment() {
477477
if (!isRefreshing)
478478
CoroutineScope(Dispatchers.IO).launch { MG4Hardware.setSteeringHeat(checked) }
479479
}
480+
// Au clic la rangée montre le niveau DEMANDÉ ; une fois la commande terminée on la
481+
// repeint d'après la voiture. Sans ça elle n'était relue qu'à l'ouverture de l'onglet,
482+
// et restait fausse dès que le siège n'était pas là où on l'attendait.
480483
seatLeftButtons?.let { setupSeatButtons(it) { level ->
481-
CoroutineScope(Dispatchers.IO).launch { MG4Hardware.setSeatHeatLeft(level) }
484+
CoroutineScope(Dispatchers.IO).launch {
485+
MG4Hardware.setSeatHeatLeft(level)
486+
repeindreSieges()
487+
}
482488
} }
483489
seatRightButtons?.let { setupSeatButtons(it) { level ->
484-
CoroutineScope(Dispatchers.IO).launch { MG4Hardware.setSeatHeatRight(level) }
490+
CoroutineScope(Dispatchers.IO).launch {
491+
MG4Hardware.setSeatHeatRight(level)
492+
repeindreSieges()
493+
}
485494
} }
486495
}
487496

@@ -1096,6 +1105,22 @@ class DashboardFragment : Fragment() {
10961105
}
10971106
}
10981107

1108+
/**
1109+
* Repeint les deux rangées de sièges d'après la voiture. À appeler hors du thread principal,
1110+
* une fois la commande terminée : [MG4Hardware.setSeatHeatLeft] ne rend la main qu'après
1111+
* avoir vu la voiture annoncer le niveau (ou y avoir renoncé), la lecture est donc à jour.
1112+
* Un niveau illisible laisse la rangée telle quelle.
1113+
*/
1114+
private suspend fun repeindreSieges() {
1115+
val gauche = MG4Hardware.getSeatHeatLeftOrNull()
1116+
val droit = MG4Hardware.getSeatHeatRightOrNull()
1117+
withContext(Dispatchers.Main) {
1118+
if (!isAdded) return@withContext
1119+
gauche?.let { niveau -> seatLeftButtons?.let { applySeatUI(it, niveau) } }
1120+
droit?.let { niveau -> seatRightButtons?.let { applySeatUI(it, niveau) } }
1121+
}
1122+
}
1123+
10991124
private fun setupSeatButtons(buttons: List<Button>, onLevel: (Int) -> Unit) {
11001125
buttons.forEachIndexed { index, btn ->
11011126
btn.setOnClickListener {

‎app/src/test/java/com/mg4/control/model/HvacPopupTest.kt‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -148,27 +148,6 @@ class HvacPopupTest {
148148

149149
// ── Sièges chauffants (firmwares équipés seulement) ──────────────────────
150150

151-
@Test
152-
fun `un appui sur un siege passe au niveau suivant puis l eteint`() {
153-
// Éteint → 1 → 2 → 3 → éteint : comme le raccourci « siège chauffant ».
154-
assertEquals(1, HvacPopup.niveauSiegeSuivant(0))
155-
assertEquals(2, HvacPopup.niveauSiegeSuivant(1))
156-
assertEquals(3, HvacPopup.niveauSiegeSuivant(2))
157-
assertEquals(0, HvacPopup.niveauSiegeSuivant(3))
158-
}
159-
160-
@Test
161-
fun `un niveau de siege illisible ne se regle pas`() {
162-
// Partir d'un 0 supposé alors que le siège est à 3 ferait DESCENDRE le chauffage.
163-
assertNull(HvacPopup.niveauSiegeSuivant(null))
164-
}
165-
166-
@Test
167-
fun `un niveau de siege hors echelle repart de l echelle`() {
168-
assertEquals(0, HvacPopup.niveauSiegeSuivant(7))
169-
assertEquals(1, HvacPopup.niveauSiegeSuivant(-1))
170-
}
171-
172151
@Test
173152
fun `les pastilles montrent le niveau du siege`() {
174153
assertEquals("○ ○ ○", HvacPopup.pastilles(0))

0 commit comments

Comments
 (0)