Skip to content

Commit 4f77631

Browse files
committed
electric window feature + audio feature for all firmware
1 parent 1031787 commit 4f77631

12 files changed

Lines changed: 203 additions & 47 deletions

File tree

‎app/src/main/java/com/mg4/control/MainActivity.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ class MainActivity : AppCompatActivity() {
239239
val btnProfiles = findViewById<MaterialButton>(R.id.btn_nav_profiles)
240240
val btnSettings = findViewById<MaterialButton>(R.id.btn_nav_settings)
241241

242-
// Bouton Audio : contrôle vendor caradapter dispo uniquement sur A9 → masqué ailleurs.
242+
// Bouton Audio : visible partout où le volume média est pilotable (ancien SDK et A9).
243243
if (MG4Hardware.hasAudioControl()) {
244244
btnAudio.setOnClickListener {
245245
when (navController.currentDestination?.id) {

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

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ object MG4Hardware {
365365
// Démarrage auto du watcher porte au boot si la feature est activée (tous firmwares).
366366
startDoorWatcherIfEnabled()
367367
// Connexion Car établie même si la feature est OFF → la sonde Diagnostic peut lire les portes.
368-
if (hasDoorVolumeFeature()) connectCarProperty()
368+
if (hasDoorDetection()) connectCarProperty()
369369

370370
AppLogger.i(TAG, "========================================")
371371
}
@@ -3565,7 +3565,7 @@ object MG4Hardware {
35653565
return gen == FirmwareInfo.Gen.SWI133 || gen == FirmwareInfo.Gen.SWI68 || gen == FirmwareInfo.Gen.SWI165
35663566
}
35673567

3568-
/** Onglet Audio (baisse volume à l'ouverture de porte) : uniquement là où c'est fonctionnel. */
3568+
/** Onglet Audio (baisse de volume en quittant la voiture) : partout où le volume est pilotable. */
35693569
fun hasAudioControl(): Boolean = hasDoorVolumeFeature()
35703570

35713571
private const val DESCRIPTOR_CARADAPTER = "com.saicmotor.carapi.ICarAdapterService"
@@ -4741,13 +4741,20 @@ object MG4Hardware {
47414741
}
47424742
}
47434743

4744-
/** Baisse du volume à l'ouverture de porte : détection DLOCK_DOOR_OPEN_STS via CarPropertyManager.
4745-
* Lisible/fonctionnel uniquement sur SWI132 et SWI133 ; ailleurs le prop n'est pas exposé à l'app. */
4746-
fun hasDoorVolumeFeature(): Boolean {
4744+
/** Détection réelle de l'ouverture des portes (DLOCK_DOOR_OPEN_STS via CarPropertyManager) :
4745+
* lisible uniquement sur SWI132 et SWI133 ; ailleurs le prop n'est pas exposé à l'app. */
4746+
fun hasDoorDetection(): Boolean {
47474747
val gen = FirmwareInfo.getGeneration()
47484748
return gen == FirmwareInfo.Gen.SWI132 || gen == FirmwareInfo.Gen.SWI133
47494749
}
47504750

4751+
/**
4752+
* Baisse du volume en quittant la voiture : partout où le volume média est pilotable (ancien SDK
4753+
* et A9). Déclencheur : la porte là où elle est lisible ([hasDoorDetection]), sinon la sortie du
4754+
* mode READY en P ([ReadyWatcher]), qui correspond sur MG4 à l'ouverture de la porte conducteur.
4755+
*/
4756+
fun hasDoorVolumeFeature(): Boolean = isOldSdkSound() || isA9Sound()
4757+
47514758
private fun doorVolumeEnabled(): Boolean {
47524759
// Mode Garage : une portière qui fait chuter le volume est exactement le genre de
47534760
// comportement inexpliqué qu'on ne veut pas laisser observer.
@@ -4779,19 +4786,79 @@ object MG4Hardware {
47794786
fun startDoorVolumeWatcher() {
47804787
if (!hasDoorVolumeFeature()) return
47814788
sDoorWatcherOn = true
4782-
connectCarProperty()
4789+
if (hasDoorDetection()) connectCarProperty() else ReadyWatcher.add(readyVolumeListener) // add idempotent
47834790
}
47844791

47854792
fun stopDoorVolumeWatcher() {
47864793
sDoorWatcherOn = false // poll conservé ; on ne déclenche plus la baisse
4794+
if (!hasDoorDetection()) ReadyWatcher.remove(readyVolumeListener)
47874795
AppLogger.i(TAG, " DoorVolumeWatcher: déclenchement désactivé")
47884796
}
47894797

4798+
/**
4799+
* Firmwares sans porte lisible : sortie de READY en P = départ du conducteur → baisse ; retour en
4800+
* READY → restauration si l'utilisateur l'a choisie. La sortie à l'extinction voiture occupée baisse
4801+
* aussi le volume (accepté). Rapport illisible : on baisse quand même (aucun risque, au contraire
4802+
* des vitres). Le premier état connu n'est pas une transition, sauf pour une restauration en
4803+
* attente (écran redémarré entre la baisse et le retour en READY).
4804+
*/
4805+
private val readyVolumeListener = ReadyWatcher.Listener { ready, firstRead ->
4806+
if (!sDoorWatcherOn) return@Listener
4807+
if (ready) {
4808+
restoreMediaVolumeAfterDrop(if (firstRead) "READY à l'activation" else "retour en READY")
4809+
return@Listener
4810+
}
4811+
if (firstRead) return@Listener
4812+
if (!doorVolumeEnabled()) return@Listener // inclut le Mode Garage
4813+
val inPark = isVehicleInPark()
4814+
if (inPark == false) {
4815+
AppLogger.i(DOORWATCH_TAG, "sortie de READY hors P → volume inchangé")
4816+
return@Listener
4817+
}
4818+
dropMediaVolumeForExit("sortie de READY (P=${inPark ?: "illisible"})")
4819+
}
4820+
4821+
/** Volume avant la baisse, gardé en préférences : la restauration survit à un redémarrage de l'écran. */
4822+
private const val KEY_VOLUME_BEFORE_DROP = "door_volume_before_drop"
4823+
4824+
private fun dropMediaVolumeForExit(origin: String) {
4825+
val level = doorVolumeLevel()
4826+
CoroutineScope(Dispatchers.IO).launch {
4827+
val before = getMediaVolume()
4828+
sVolumeBeforeDrop = before
4829+
if (before >= 0) sAppContext?.getSharedPreferences("mg4_settings", 0)?.edit()
4830+
?.putInt(KEY_VOLUME_BEFORE_DROP, before)?.apply()
4831+
val ok = setMediaVolume(level)
4832+
AppLogger.i(DOORWATCH_TAG, "$origin → vol $before→$level = $ok")
4833+
}
4834+
}
4835+
4836+
private fun restoreMediaVolumeAfterDrop(origin: String) {
4837+
val prefs = sAppContext?.getSharedPreferences("mg4_settings", 0)
4838+
val restore = sVolumeBeforeDrop.takeIf { it >= 0 } ?: prefs?.getInt(KEY_VOLUME_BEFORE_DROP, -1) ?: -1
4839+
sVolumeBeforeDrop = -1
4840+
prefs?.edit()?.remove(KEY_VOLUME_BEFORE_DROP)?.apply()
4841+
if (restore < 0) return
4842+
if (!doorRestoreEnabled()) {
4843+
AppLogger.i(DOORWATCH_TAG, "$origin → restauration désactivée (volume laissé tel quel)")
4844+
return
4845+
}
4846+
CoroutineScope(Dispatchers.IO).launch {
4847+
val ok = setMediaVolume(restore)
4848+
AppLogger.i(DOORWATCH_TAG, "$origin → restauration vol $restore = $ok")
4849+
}
4850+
}
4851+
47904852
/** Sonde du bouton Diagnostic : logge le volume + l'état des portes à l'instant du clic. */
47914853
fun runDoorVolumeDiag() {
47924854
AppLogger.i(VOL_TAG, "── DIAG (bouton Diagnostic) ──")
47934855
getMediaVolumeMax()
47944856
getMediaVolume()
4857+
if (!hasDoorDetection()) {
4858+
AppLogger.i(DOORWATCH_TAG, "DIAG pas de porte lisible sur ${FirmwareInfo.getGeneration()} → READY brut = " +
4859+
"${readEptReadyRaw() ?: "illisible"} (surveillance ${if (ReadyWatcher.ready != null) "active" else "inactive"})")
4860+
return
4861+
}
47954862
connectCarProperty() // idempotent ; normalement déjà connecté depuis l'init
47964863
registerDoorCallback() // re-tente la souscription si pas encore posée
47974864
probeDoorSnapshot()
@@ -5562,21 +5629,9 @@ object MG4Hardware {
55625629
val triggerAreas = doorTriggerAreas()
55635630
val anyOpen = triggerAreas.any { sDoorReadLast[it] == 1 }
55645631
if (anyOpen && !sAnyFrontOpenPrev) {
5565-
val level = doorVolumeLevel()
5566-
CoroutineScope(Dispatchers.IO).launch {
5567-
sVolumeBeforeDrop = getMediaVolume()
5568-
val ok = setMediaVolume(level)
5569-
AppLogger.i(DOORWATCH_TAG, "porte ouverte → vol $sVolumeBeforeDrop→$level = $ok")
5570-
}
5632+
dropMediaVolumeForExit("porte ouverte")
55715633
} else if (!anyOpen && sAnyFrontOpenPrev) {
5572-
val restore = sVolumeBeforeDrop
5573-
sVolumeBeforeDrop = -1
5574-
if (doorRestoreEnabled() && restore >= 0) {
5575-
CoroutineScope(Dispatchers.IO).launch {
5576-
val ok = setMediaVolume(restore)
5577-
AppLogger.i(DOORWATCH_TAG, "porte fermée → restauration vol $restore = $ok")
5578-
}
5579-
}
5634+
restoreMediaVolumeAfterDrop("porte fermée")
55805635
}
55815636
sAnyFrontOpenPrev = anyOpen
55825637
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.mg4.control.hardware
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import com.mg4.control.debug.AppLogger
6+
import com.mg4.control.model.WindowAutoCloseTrigger
7+
import com.mg4.control.util.FirmwareInfo
8+
9+
/**
10+
* Observateur partagé de l'état READY (`SENSOR_EPTRDY`), lisible sur les 6 firmwares
11+
* ([MG4Hardware.readEptReadyRaw]). Sur MG4, la voiture sort de READY quand la porte conducteur
12+
* s'ouvre en P (et à l'extinction) : c'est le signal de « départ du conducteur » là où la porte
13+
* elle-même n'est pas lisible.
14+
*
15+
* Une seule lecture par seconde, quel que soit le nombre d'abonnés (fermeture auto des vitres,
16+
* baisse de volume) ; la lecture s'arrête sans abonné. Tout se passe sur le fil principal.
17+
* Journal : tag [TAG], valeur brute à chaque changement.
18+
*/
19+
object ReadyWatcher {
20+
21+
const val TAG = "MG4_READY"
22+
private const val TICK_MS = 1_000L
23+
24+
fun interface Listener {
25+
/**
26+
* [firstRead] vrai pour le premier état connu (lecture initiale, ou abonnement alors que l'état
27+
* est déjà connu) : ce n'est pas une transition, l'abonné décide s'il en tient compte.
28+
*/
29+
fun onReadyChanged(ready: Boolean, firstRead: Boolean)
30+
}
31+
32+
private val main = Handler(Looper.getMainLooper())
33+
private val listeners = LinkedHashSet<Listener>()
34+
private var rawRead = false
35+
private var lastRaw: Int? = null
36+
37+
/** Dernier état connu ; null tant que READY n'a pas été lu (ou si la surveillance est arrêtée). */
38+
@Volatile var ready: Boolean? = null
39+
private set
40+
41+
fun add(listener: Listener) {
42+
main.post {
43+
if (!listeners.add(listener)) return@post
44+
ready?.let { listener.onReadyChanged(it, firstRead = true) }
45+
if (listeners.size == 1) {
46+
main.post(tick)
47+
AppLogger.i(TAG, "surveillance READY active (${FirmwareInfo.getGeneration()})")
48+
}
49+
}
50+
}
51+
52+
fun remove(listener: Listener) {
53+
main.post {
54+
if (!listeners.remove(listener) || listeners.isNotEmpty()) return@post
55+
main.removeCallbacks(tick)
56+
ready = null
57+
rawRead = false
58+
lastRaw = null
59+
AppLogger.i(TAG, "surveillance READY arrêtée (plus d'abonné)")
60+
}
61+
}
62+
63+
private val tick = object : Runnable {
64+
override fun run() {
65+
if (listeners.isEmpty()) return
66+
poll()
67+
main.postDelayed(this, TICK_MS)
68+
}
69+
}
70+
71+
private fun poll() {
72+
val raw = MG4Hardware.readEptReadyRaw()
73+
if (!rawRead || raw != lastRaw) {
74+
AppLogger.i(TAG, "READY brut ${if (rawRead) lastRaw ?: "illisible" else "?"} → ${raw ?: "illisible"}")
75+
rawRead = true
76+
lastRaw = raw
77+
}
78+
// Illisible ou hors 0/1 : on garde le dernier état connu, sans fausse transition.
79+
val newReady = WindowAutoCloseTrigger.readyFromRaw(raw) ?: return
80+
val previous = ready
81+
if (previous == newReady) return
82+
ready = newReady
83+
listeners.toList().forEach { it.onReadyChanged(newReady, firstRead = previous == null) }
84+
}
85+
}

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

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ import com.mg4.control.util.GarageMode
1616
* Option « Fermer les vitres en quittant la voiture » : branche [WindowAutoCloseTrigger] (logique
1717
* de winclose) sur les signaux du véhicule, et lance [PowerWindows.closeAllAutomatically].
1818
*
19-
* Signaux, lus chaque seconde sur le fil principal (l'état du déclencheur n'a donc qu'un fil) :
20-
* état READY ([MG4Hardware.readEptReadyRaw], disponible sur les 6 firmwares — la porte conducteur,
21-
* elle, ne l'est pas), vitesse (tant que non armé), rapport P (seulement à la sortie de READY et
22-
* pendant le délai : sa lecture journalise).
19+
* Signaux, sur le fil principal (l'état du déclencheur n'a donc qu'un fil) : état READY via
20+
* l'observateur partagé [ReadyWatcher] (6 firmwares — la porte conducteur, elle, ne l'est pas),
21+
* vitesse lue chaque seconde tant que non armé, rapport P (seulement à la sortie de READY et pendant
22+
* le délai : sa lecture journalise).
2323
*
2424
* Démarré par le service au boot si l'option est active, ou par l'onglet Vitres à l'activation.
2525
* Respecte le Mode Garage. Journal : tag [PowerWindows.TAG].
@@ -69,8 +69,6 @@ object WindowAutoClose {
6969
private val main = Handler(Looper.getMainLooper())
7070
private val trigger = WindowAutoCloseTrigger()
7171
private var running = false
72-
private var lastReadyRaw: Int? = null
73-
private var ready: Boolean? = null
7472
private var lastResult: Result? = null
7573
private var lastResultAt = 0L
7674

@@ -119,7 +117,7 @@ object WindowAutoClose {
119117
}
120118

121119
fun status(context: Context): Status =
122-
Status(isEnabled(context), trigger.isArmed, ready, lastResult, lastResultAt)
120+
Status(isEnabled(context), trigger.isArmed, ReadyWatcher.ready, lastResult, lastResultAt)
123121

124122
private fun apply(s: Settings) {
125123
trigger.arming = s.toArming()
@@ -152,9 +150,8 @@ object WindowAutoClose {
152150
if (running) return@post
153151
running = true
154152
apply(s)
155-
lastReadyRaw = null
156-
ready = null
157153
PowerWindows.prepare()
154+
ReadyWatcher.add(readyListener)
158155
main.post(tick)
159156
AppLogger.i(TAG, "fermeture auto : surveillance active sur ${FirmwareInfo.getGeneration()} (${describe(s)})")
160157
}
@@ -165,6 +162,7 @@ object WindowAutoClose {
165162
if (!running) return@post
166163
running = false
167164
main.removeCallbacks(tick)
165+
ReadyWatcher.remove(readyListener)
168166
if (trigger.cancelPending()) {
169167
setResult(Result.CANCELLED)
170168
AppLogger.i(TAG, "fermeture auto : attente abandonnée (option désactivée)")
@@ -174,7 +172,6 @@ object WindowAutoClose {
174172

175173
private fun onTick() {
176174
val now = SystemClock.elapsedRealtime()
177-
readReady(now)
178175
val wasArmed = trigger.isArmed
179176
// Vitesse inutile une fois armé ; rapport lu seulement pendant l'attente (sa lecture journalise).
180177
val speed = if (wasArmed) null else MG4Hardware.getVehicleSpeedKmh()
@@ -184,25 +181,18 @@ object WindowAutoClose {
184181
handle(outcome)
185182
}
186183

187-
/** Lit READY et transmet ses changements au déclencheur ; la valeur brute est journalisée à chaque changement. */
188-
private fun readReady(now: Long) {
189-
val raw = MG4Hardware.readEptReadyRaw()
190-
if (raw != lastReadyRaw) {
191-
AppLogger.i(TAG, "fermeture auto : READY brut ${lastReadyRaw ?: "?"} → ${raw ?: "illisible"}")
192-
lastReadyRaw = raw
193-
}
194-
val newReady = WindowAutoCloseTrigger.readyFromRaw(raw) ?: return // illisible / invalide : on garde l'état connu
195-
val previous = ready
196-
ready = newReady
197-
if (previous == newReady) return
184+
/** Changements de READY (valeur brute journalisée par [ReadyWatcher], tag MG4_READY). */
185+
private val readyListener = ReadyWatcher.Listener { ready, firstRead ->
186+
if (!running) return@Listener
187+
val now = SystemClock.elapsedRealtime()
198188
when {
199-
newReady -> {
189+
ready -> {
200190
val outcome = trigger.onReady(now)
201191
AppLogger.i(TAG, "fermeture auto : passage en READY → ${outcome.action} (${outcome.reason})")
202192
handle(outcome)
203193
}
204-
// Première lecture hors READY (option activée voiture éteinte) : pas une sortie de READY.
205-
previous == null -> AppLogger.i(TAG, "fermeture auto : voiture hors READY à l'activation")
194+
// Premier état connu hors READY (option activée voiture éteinte) : pas une sortie de READY.
195+
firstRead -> AppLogger.i(TAG, "fermeture auto : voiture hors READY à l'activation")
206196
else -> onReadyLost(now)
207197
}
208198
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.view.View
77
import android.view.ViewGroup
88
import android.widget.CheckBox
99
import android.widget.Switch
10+
import android.widget.TextView
1011
import androidx.fragment.app.Fragment
1112
import androidx.lifecycle.lifecycleScope
1213
import com.google.android.material.slider.Slider
@@ -25,7 +26,7 @@ class AudioFragment : Fragment() {
2526
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
2627
super.onViewCreated(view, savedInstanceState)
2728

28-
// ── Volume à l'ouverture d'une porte (SWI132/133) ───────────────────
29+
// ── Volume en quittant la voiture : porte (SWI132/133) ou sortie de READY (autres) ──
2930
setupDoorVolume(view)
3031
}
3132

@@ -43,6 +44,15 @@ class AudioFragment : Fragment() {
4344
val cbLeft = view.findViewById<CheckBox>(R.id.cb_door_left)
4445
val cbRight = view.findViewById<CheckBox>(R.id.cb_door_right)
4546

47+
// Sans porte lisible, le déclencheur est la sortie de READY : il ne concerne que le conducteur
48+
// (pas de choix de porte) et la restauration se fait au retour en READY.
49+
if (!MG4Hardware.hasDoorDetection()) {
50+
view.findViewById<TextView>(R.id.door_volume_desc_text).setText(R.string.door_volume_desc_ready)
51+
view.findViewById<TextView>(R.id.door_volume_restore_label).setText(R.string.door_volume_restore_ready_title)
52+
view.findViewById<View>(R.id.door_volume_doors_label).visibility = View.GONE
53+
view.findViewById<View>(R.id.door_volume_doors_row).visibility = View.GONE
54+
}
55+
4656
val enabled = prefs.getBoolean("door_volume_enabled", false)
4757
toggle.isChecked = enabled
4858
restore.isChecked = prefs.getBoolean("door_volume_restore", false)

0 commit comments

Comments
 (0)