Skip to content

Commit d7299d9

Browse files
committed
electric window feature
1 parent 72fa3f4 commit d7299d9

13 files changed

Lines changed: 137 additions & 66 deletions

File tree

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

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,14 @@ object PowerWindows {
7575
private var estimatorsLoaded = false
7676

7777
/**
78-
* Commande répétée en cours, par vitre : doigt posé ([finger]) ou ouverture auto émulée.
78+
* Commande répétée en cours, par vitre : doigt posé ([finger]) ou course auto émulée.
7979
* Lu et écrit uniquement sur [worker].
8080
*/
8181
private class Hold(val value: Int, val startMs: Long, val finger: Boolean, val label: String, val repeat: Runnable) {
8282
var sent = 0
8383
var failed = 0
84+
/** Arrêtée si l'utilisateur quitte l'onglet : doigt posé, ou fermeture émulée. */
85+
val stopOnLeave: Boolean get() = finger || WindowCommand.stopsWhenUnattended(value)
8486
}
8587
private val holds = HashMap<PowerWindow, Hold>()
8688

@@ -157,49 +159,63 @@ object PowerWindows {
157159

158160
// ── Commandes ───────────────────────────────────────────────────────────
159161

160-
/** Appui court : course automatique, ou stop si une course vient d'être lancée. */
162+
/**
163+
* Appui court. Vitre à courses natives (conducteur) : course auto, ou stop si elle vient d'être
164+
* lancée. Autres vitres : course émulée, ou stop si une course émulée est en cours — c'est la
165+
* répétition en cours qui fait foi, pas le temps écoulé depuis l'appui précédent.
166+
*/
161167
fun shortPress(window: PowerWindow, direction: Direction) {
162168
val now = SystemClock.elapsedRealtime()
163-
val value = WindowCommand.forShortPress(direction, lastAutoMs[window], now)
164-
if (value == WindowCommand.STOP) {
165-
lastAutoMs.remove(window)
166-
// Une ouverture émulée s'arrête comme une course native : d'un seul appui.
167-
worker.post { if (!endRepeat(window, "appui court")) send(window, WindowCommand.STOP, "appui court") }
168-
} else {
169-
lastAutoMs[window] = now
170-
worker.post { startAuto(window, direction, "appui court") }
169+
worker.post {
170+
if (!window.hasNativeAuto) {
171+
if (!endRepeat(window, "appui court")) startEmulatedCourse(window, direction, "appui court")
172+
return@post
173+
}
174+
val value = WindowCommand.forShortPress(direction, lastAutoMs[window], now)
175+
if (value == WindowCommand.STOP) {
176+
lastAutoMs.remove(window)
177+
if (!endRepeat(window, "appui court")) send(window, WindowCommand.STOP, "appui court")
178+
} else {
179+
startAuto(window, direction, "appui court")
180+
}
171181
}
172182
}
173183

174184
/** Tout ouvrir / tout fermer : toujours la course automatique, jamais le stop. */
175185
fun autoAll(direction: Direction) {
176-
val now = SystemClock.elapsedRealtime()
177-
PowerWindow.entries.forEach { lastAutoMs[it] = now }
178186
val origin = "tout ${if (direction == Direction.UP) "fermer" else "ouvrir"}"
179187
worker.post { PowerWindow.entries.forEach { startAuto(it, direction, origin) } }
180188
}
181189

182-
/** Étape 1 de la calibration : montée automatique, pour partir d'une vitre fermée. */
190+
/**
191+
* Étape 1 de la calibration : fermeture complète. Émulée, elle dure au moins la course par défaut,
192+
* pour atteindre la butée même si une ancienne calibration était trop courte.
193+
*/
183194
fun closeForCalibration(window: PowerWindow) {
184195
lastAutoMs.remove(window)
185-
worker.post { startAuto(window, Direction.UP, "calibration") }
196+
worker.post { startAuto(window, Direction.UP, "calibration", minEmulatedMs = WindowCommand.EMULATED_COURSE_MS) }
197+
}
198+
199+
/** Sur [worker]. Course automatique : native pour le conducteur, émulée pour les autres vitres. */
200+
private fun startAuto(window: PowerWindow, direction: Direction, origin: String, minEmulatedMs: Long = 0L) {
201+
if (!window.hasNativeAuto) {
202+
startEmulatedCourse(window, direction, origin, minEmulatedMs)
203+
return
204+
}
205+
lastAutoMs[window] = SystemClock.elapsedRealtime()
206+
endRepeat(window, "$origin demandé")
207+
send(window, WindowCommand.auto(direction), origin)
186208
}
187209

188210
/**
189-
* Sur [worker]. Course automatique ; sans descente auto native (toutes les vitres sauf le
190-
* conducteur, mesuré en voiture), l'ouverture est la descente manuelle répétée le temps d'une
191-
* course — la course calibrée si elle existe.
211+
* Sur [worker]. Course émulée : commande manuelle répétée le temps d'une course (la course
212+
* calibrée si elle existe), ce que ferait un doigt posé. L'estimation suit donc le vrai mouvement.
192213
*/
193-
private fun startAuto(window: PowerWindow, direction: Direction, origin: String) {
194-
if (direction == Direction.DOWN && !window.hasNativeAutoDown) {
195-
loadEstimators()
196-
startRepeat(window, WindowCommand.MANUAL_DOWN, WindowCommand.emulatedOpenMs(estimators[window]?.calibration),
197-
finger = false, label = "$origin, ouverture auto émulée")
198-
} else {
199-
// Une ouverture émulée en cours ne doit pas contrarier la montée demandée.
200-
endRepeat(window, "$origin demandé")
201-
send(window, WindowCommand.auto(direction), origin)
202-
}
214+
private fun startEmulatedCourse(window: PowerWindow, direction: Direction, origin: String, minMs: Long = 0L) {
215+
loadEstimators()
216+
val ms = maxOf(minMs, WindowCommand.emulatedCourseMs(direction, estimators[window]?.calibration))
217+
val what = if (direction == Direction.UP) "fermeture" else "ouverture"
218+
startRepeat(window, WindowCommand.manual(direction), ms, finger = false, label = "$origin, $what auto émulée")
203219
}
204220

205221
/** Appui long : commande manuelle répétée jusqu'à [stopHold], comme un doigt sur l'interrupteur. */
@@ -217,11 +233,12 @@ object PowerWindows {
217233
}
218234

219235
/**
220-
* Onglet quitté : les doigts posés s'arrêtent (le relâché ne viendra peut-être jamais) ;
221-
* une ouverture émulée, bornée dans le temps, va au bout comme une course native.
236+
* Onglet quitté : s'arrêtent les doigts posés (le relâché ne viendra peut-être jamais) et les
237+
* fermetures émulées (pas d'anti-pincement hors de la vue de l'utilisateur). Une ouverture
238+
* émulée, bornée dans le temps, va au bout comme une course native.
222239
*/
223-
fun stopFingerHolds() {
224-
worker.post { holds.filterValues { it.finger }.keys.toList().forEach { endRepeat(it, "onglet quitté") } }
240+
fun stopUnattendedMoves() {
241+
worker.post { holds.filterValues { it.stopOnLeave }.keys.toList().forEach { endRepeat(it, "onglet quitté") } }
225242
}
226243

227244
/** Test brut de l'onglet Diagnostic : n'importe quelle valeur de 0 à 7, pour décoder l'échelle. */

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ package com.mg4.control.model
1010
*
1111
* Mesuré en voiture le 2026-09-17 (peut dépendre de la finition, codes de configuration relevés
1212
* par la sonde) :
13-
* - [hasNativeAutoDown] : la descente auto n'ouvre que la vitre conducteur ; la montée auto (3) marche
14-
* sur les quatre. Ailleurs, l'ouverture auto est émulée par la descente manuelle répétée.
13+
* - [hasNativeAuto] : la descente auto (4) n'ouvre que la vitre conducteur, et la montée auto (3), qui
14+
* fermait les quatre au premier essai, a cessé de fermer les autres après la calibration (état interne
15+
* du module de vitre, invisible pour l'app). Ailleurs, les courses auto sont donc émulées dans les deux
16+
* sens par la commande manuelle répétée — sans l'anti-pincement de la course native.
1517
* - [hasPositionSensor] : seule la vitre conducteur remonte une position ; les autres restent figées
1618
* hors plage (127.5 / 255) et leur position est estimée après calibration ([WindowEstimator]).
1719
*
@@ -20,20 +22,20 @@ package com.mg4.control.model
2022
*/
2123
enum class PowerWindow(
2224
val propId: Int,
23-
val hasNativeAutoDown: Boolean,
25+
val hasNativeAuto: Boolean,
2426
val hasPositionSensor: Boolean,
2527
val shortName: String,
2628
) {
27-
FRONT_LEFT (0x11603801, hasNativeAutoDown = true, hasPositionSensor = true, shortName = "AVG"),
28-
FRONT_RIGHT(0x11603802, hasNativeAutoDown = false, hasPositionSensor = false, shortName = "AVD"),
29-
REAR_LEFT (0x11603803, hasNativeAutoDown = false, hasPositionSensor = false, shortName = "ARG"),
30-
REAR_RIGHT (0x11603804, hasNativeAutoDown = false, hasPositionSensor = false, shortName = "ARD"),
29+
FRONT_LEFT (0x11603801, hasNativeAuto = true, hasPositionSensor = true, shortName = "AVG"),
30+
FRONT_RIGHT(0x11603802, hasNativeAuto = false, hasPositionSensor = false, shortName = "AVD"),
31+
REAR_LEFT (0x11603803, hasNativeAuto = false, hasPositionSensor = false, shortName = "ARG"),
32+
REAR_RIGHT (0x11603804, hasNativeAuto = false, hasPositionSensor = false, shortName = "ARD"),
3133
}
3234

3335
/**
3436
* Valeurs de commande d'une vitre. 0, 1 et 3 viennent du projet winclose (SWI69) ; 2 et 4 ont été
35-
* essayées en voiture le 2026-09-17 via l'onglet Vitres : 4 n'ouvre que la vitre conducteur (voir
36-
* [PowerWindow.hasNativeAutoDown]). 5 à 7 restent inconnues (test brut). Le service SWI68 refuse
37+
* essayées en voiture le 2026-09-17 via l'onglet Vitres : 3 et 4 ne sont fiables que sur la vitre
38+
* conducteur (voir [PowerWindow.hasNativeAuto]). 5 à 7 restent inconnues (test brut). Le service SWI68 refuse
3739
* toute valeur hors de 0..7.
3840
*/
3941
object WindowCommand {
@@ -42,13 +44,13 @@ object WindowCommand {
4244
const val MANUAL_UP = 1
4345
const val MANUAL_DOWN = 2
4446
const val AUTO_UP = 3
45-
const val AUTO_DOWN = 4 // conducteur seulement
47+
const val AUTO_DOWN = 4
4648
const val MAX = 7
4749

48-
/** Au-delà, un appui court n'est plus pris pour un « stop » : la course auto est finie. */
50+
/** Au-delà, un appui court n'est plus pris pour un « stop » : la course auto native est finie. */
4951
const val AUTO_TRAVEL_MS = 6_000L
50-
/** Durée de l'ouverture auto émulée tant que la vitre n'est pas calibrée. */
51-
const val EMULATED_OPEN_MS = AUTO_TRAVEL_MS
52+
/** Durée d'une course auto émulée tant que la vitre n'est pas calibrée. */
53+
const val EMULATED_COURSE_MS = AUTO_TRAVEL_MS
5254
/**
5355
* Plus grande position valide. Le service véhicule SWI68 rejette toute lecture au-delà
5456
* (`max_vehicle_window_get` = 100) ; en voiture, 127.5 et 255 restent figés sur les vitres
@@ -80,8 +82,17 @@ object WindowCommand {
8082
/** Position utilisable, ou null (illisible, ou valeur de remplacement d'une vitre sans capteur). */
8183
fun position(raw: Float?): Float? = raw?.takeIf { it in 0f..POSITION_MAX }
8284

83-
/** Durée de l'ouverture émulée : la course calibrée si elle existe, sinon la valeur par défaut. */
84-
fun emulatedOpenMs(calibration: WindowCalibration?): Long = calibration?.emulatedOpenMs ?: EMULATED_OPEN_MS
85+
/** Durée d'une course émulée : la course calibrée dans ce sens si elle existe, sinon la valeur par défaut. */
86+
fun emulatedCourseMs(direction: Direction, calibration: WindowCalibration?): Long = when (direction) {
87+
Direction.DOWN -> calibration?.emulatedOpenMs
88+
Direction.UP -> calibration?.emulatedCloseMs
89+
} ?: EMULATED_COURSE_MS
90+
91+
/**
92+
* Une fermeture émulée n'a pas l'anti-pincement de la course native : elle ne continue que
93+
* sous les yeux de l'utilisateur (arrêtée si l'onglet est quitté). Une ouverture peut finir seule.
94+
*/
95+
fun stopsWhenUnattended(value: Int): Boolean = directionOf(value) == Direction.UP
8596

8697
/**
8798
* Appui court : lance la course automatique, sauf si une course vient d'être lancée sur cette

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import com.mg4.control.model.WindowCommand.Direction
88
*/
99
data class WindowCalibration(val downMs: Long, val upMs: Long) {
1010

11-
/** Ouverture auto émulée : la course mesurée plus une marge, pour finir en butée malgré l'imprécision. */
12-
val emulatedOpenMs: Long get() = downMs + OPEN_MARGIN_MS
11+
/** Courses auto émulées : la course mesurée plus une marge, pour finir en butée malgré l'imprécision. */
12+
val emulatedOpenMs: Long get() = downMs + COURSE_MARGIN_MS
13+
val emulatedCloseMs: Long get() = upMs + COURSE_MARGIN_MS
1314

1415
companion object {
1516
/** Plus court : appui manqué. Plus long : le garde-fou du maintien a coupé la vitre avant la butée. */
1617
const val MIN_MS = 800L
1718
const val MAX_MS = WindowCommand.HOLD_MAX_MS
18-
const val OPEN_MARGIN_MS = 500L
19+
const val COURSE_MARGIN_MS = 500L
1920

2021
fun isValidMeasure(ms: Long): Boolean = ms in MIN_MS..MAX_MS
2122
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class WindowsPanel {
5555
}
5656

5757
companion object {
58-
private const val POLL_MS = 1_000L
58+
/** Assez rapide pour que le % estimé suive la vitre en mouvement. */
59+
private const val POLL_MS = 500L
5960
private const val SUBSCRIBE_RETRY_MS = 10_000L
6061

6162
fun nameRes(window: PowerWindow): Int = when (window) {
@@ -134,13 +135,13 @@ class WindowsPanel {
134135
handler.postDelayed(poll, POLL_MS)
135136
}
136137

137-
/** Onglet quitté ou écran en pause : plus de sondage, et aucune vitre ne reste en mouvement manuel. */
138+
/** Onglet quitté ou écran en pause : plus de sondage, ni doigt posé ni fermeture émulée en cours. */
138139
fun onHidden() {
139140
if (!shown) return
140141
shown = false
141142
handler.removeCallbacksAndMessages(null)
142143
calibration.onHidden()
143-
PowerWindows.stopFingerHolds()
144+
PowerWindows.stopUnattendedMoves()
144145
}
145146

146147
// ── Appui court / long ──────────────────────────────────────────────────

‎app/src/main/res/layout/dash_page_windows.xml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@
5353
android:paddingEnd="16dp"
5454
android:paddingTop="8dp" />
5555

56+
<!-- Les courses émulées n'ont pas l'anti-pincement de la course native du véhicule -->
57+
<TextView
58+
android:layout_width="match_parent"
59+
android:layout_height="wrap_content"
60+
android:text="@string/win_close_warning"
61+
android:textColor="@color/dash_warn"
62+
android:textSize="13sp"
63+
android:paddingStart="16dp"
64+
android:paddingEnd="16dp"
65+
android:paddingTop="4dp" />
66+
5667
<!-- Rangée avant -->
5768
<LinearLayout
5869
android:layout_width="match_parent"

‎app/src/main/res/values-de/strings.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@
494494
<!-- Onglet Vitres (V0 de test) -->
495495
<string name="dash_cat_windows">Fenster</string>
496496
<string name="win_card_title">Fenster</string>
497-
<string name="win_hint">Kurz drücken: automatisch öffnen oder schließen · Lang drücken: das Fenster bewegt sich, solange der Finger bleibt</string>
497+
<string name="win_hint">Kurz drücken: vollständig öffnen oder schließen (erneut drücken stoppt) · Lang drücken: das Fenster bewegt sich, solange der Finger bleibt</string>
498+
<string name="win_close_warning">Automatisches Schließen durch die App ohne Einklemmschutz (außer Fahrerfenster): Fenster beobachten. Es stoppt, wenn Sie den Tab verlassen.</string>
498499
<string name="win_front_left">Vorne links</string>
499500
<string name="win_front_right">Vorne rechts</string>
500501
<string name="win_rear_left">Hinten links</string>

‎app/src/main/res/values-en/strings.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@
494494
<!-- Onglet Vitres (V0 de test) -->
495495
<string name="dash_cat_windows">Windows</string>
496496
<string name="win_card_title">Windows</string>
497-
<string name="win_hint">Short press: automatic open or close · Long press: the window moves while your finger stays down</string>
497+
<string name="win_hint">Short press: full open or close (press again to stop) · Long press: the window moves while your finger stays down</string>
498+
<string name="win_close_warning">Automatic closing by the app has no anti-pinch protection (except the driver window): watch the window. It stops if you leave the tab.</string>
498499
<string name="win_front_left">Front left</string>
499500
<string name="win_front_right">Front right</string>
500501
<string name="win_rear_left">Rear left</string>

‎app/src/main/res/values-es/strings.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@
494494
<!-- Onglet Vitres (V0 de test) -->
495495
<string name="dash_cat_windows">Ventanillas</string>
496496
<string name="win_card_title">Ventanillas</string>
497-
<string name="win_hint">Pulsación corta: apertura o cierre automático · Pulsación larga: la ventanilla se mueve mientras mantienes el dedo</string>
497+
<string name="win_hint">Pulsación corta: apertura o cierre completo (otra pulsación lo detiene) · Pulsación larga: la ventanilla se mueve mientras mantienes el dedo</string>
498+
<string name="win_close_warning">El cierre automático desde la app no tiene antipinzamiento (salvo la ventanilla del conductor): vigila la ventanilla. Se detiene si sales de la pestaña.</string>
498499
<string name="win_front_left">Delantera izquierda</string>
499500
<string name="win_front_right">Delantera derecha</string>
500501
<string name="win_rear_left">Trasera izquierda</string>

‎app/src/main/res/values-it/strings.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@
494494
<!-- Onglet Vitres (V0 de test) -->
495495
<string name="dash_cat_windows">Finestrini</string>
496496
<string name="win_card_title">Finestrini</string>
497-
<string name="win_hint">Pressione breve: apertura o chiusura automatica · Pressione lunga: il finestrino si muove finché tieni il dito</string>
497+
<string name="win_hint">Pressione breve: apertura o chiusura completa (una nuova pressione la ferma) · Pressione lunga: il finestrino si muove finché tieni il dito</string>
498+
<string name="win_close_warning">La chiusura automatica dall\'app non ha l\'antischiacciamento (tranne il finestrino del conducente): sorveglia il finestrino. Si ferma se esci dalla scheda.</string>
498499
<string name="win_front_left">Anteriore sinistro</string>
499500
<string name="win_front_right">Anteriore destro</string>
500501
<string name="win_rear_left">Posteriore sinistro</string>

‎app/src/main/res/values-pt/strings.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,8 @@
494494
<!-- Onglet Vitres (V0 de test) -->
495495
<string name="dash_cat_windows">Vidros</string>
496496
<string name="win_card_title">Vidros</string>
497-
<string name="win_hint">Toque curto: abertura ou fecho automático · Toque longo: o vidro move-se enquanto o dedo estiver pousado</string>
497+
<string name="win_hint">Toque curto: abertura ou fecho completo (novo toque para parar) · Toque longo: o vidro move-se enquanto o dedo estiver pousado</string>
498+
<string name="win_close_warning">O fecho automático pela app não tem antientalamento (exceto o vidro do condutor): vigie o vidro. Para se sair do separador.</string>
498499
<string name="win_front_left">Dianteiro esquerdo</string>
499500
<string name="win_front_right">Dianteiro direito</string>
500501
<string name="win_rear_left">Traseiro esquerdo</string>

0 commit comments

Comments
 (0)