@@ -18,6 +18,7 @@ import java.util.concurrent.ConcurrentHashMap
1818import java.lang.reflect.InvocationHandler
1919import java.lang.reflect.Proxy
2020import com.mg4.control.debug.AppLogger
21+ import com.mg4.control.model.AirFlow
2122import com.mg4.control.model.DriveMode
2223import com.mg4.control.model.RegenLevel
2324import com.mg4.control.util.FirmwareInfo
@@ -5128,6 +5129,13 @@ object MG4Hardware {
51285129
51295130 /* * Mode de recirculation — seule source fiable (l'OEM getLoopMode n'est pas vérifié). */
51305131 private const val PROP_HVAC_LOOP_MODE = 0x15402507
5132+ /* *
5133+ * Sens de l'air — `HVAC_BLOWER_DIRECTION`, échelle 0–7 décodée des firmwares (voir [AirFlow]).
5134+ * Les deux familles écrivent ICI, valeur brute : ce n'est pas une bascule.
5135+ */
5136+ private const val PROP_HVAC_BLOWER_DIRECTION = 0x1540250e
5137+ /* * Propriété AOSP homonyme (masque de bits, autre échelle) — lue par la sonde seulement. */
5138+ private const val PROP_AOSP_FAN_DIRECTION = 0x15400501
51315139 /* * État A/C — encodage mesuré sur véhicule : 1 = allumé, 0 = éteint. */
51325140 private const val PROP_HVAC_AC_ON = 0x15402500
51335141
@@ -5179,7 +5187,9 @@ object MG4Hardware {
51795187 val autoOn : Boolean? ,
51805188 val loopMode : Int? ,
51815189 val defrostFront : Boolean? ,
5182- val defrostRear : Boolean?
5190+ val defrostRear : Boolean? ,
5191+ /* * Sens de l'air, valeur brute 0–7 ([AirFlow]) ; null si illisible. */
5192+ val airFlow : Int?
51835193 )
51845194
51855195 /* *
@@ -5208,8 +5218,11 @@ object MG4Hardware {
52085218 * n'aboutir qu'après la création de la page (bind asynchrone), auquel cas le prochain
52095219 * rafraîchissement périodique remplit l'écran tout seul.
52105220 */
5211- fun getClimateState (): ClimateState ? {
5212- if (isClimateA9()) return getClimateStateA9()
5221+ fun getClimateState (): ClimateState ? =
5222+ (if (isClimateA9()) getClimateStateA9() else getClimateStateOldSdk())
5223+ ?.also { traceAirFlowChange(it) }
5224+
5225+ private fun getClimateStateOldSdk (): ClimateState ? {
52135226 if (sAirCondition == null ) sAppContext?.let { initAirCondition(it) }
52145227 if (sAirCondition == null ) return null
52155228 // Bornes lues sur la voiture (jamais codées en dur) ; repli sur des valeurs sûres.
@@ -5230,7 +5243,11 @@ object MG4Hardware {
52305243 // Lu via la PROPRIÉTÉ (0/1/2 mesurés sur véhicule), pas via l'OEM getLoopMode.
52315244 loopMode = getIntPropertyHvac(PROP_HVAC_LOOP_MODE , AREA_HVAC ).takeIf { it >= 0 },
52325245 defrostFront = acInt(" getFrontWindowDefroster" )?.takeIf { it >= 0 }?.let { it == 1 },
5233- defrostRear = acInt(" getBackWindowDefroster" )?.takeIf { it >= 0 }?.let { it == 1 }
5246+ defrostRear = acInt(" getBackWindowDefroster" )?.takeIf { it >= 0 }?.let { it == 1 },
5247+ // Propriété d'abord (même voie que le recyclage) ; le getter OEM lit la même chose,
5248+ // en repli si le CarHvacManager n'est pas encore lié.
5249+ airFlow = getIntPropertyHvac(PROP_HVAC_BLOWER_DIRECTION , AREA_HVAC ).takeIf { it in 0 .. 7 }
5250+ ? : acInt(" getBlowerDirectionMode" )?.takeIf { it in 0 .. 7 }
52345251 )
52355252 }
52365253
@@ -5273,7 +5290,8 @@ object MG4Hardware {
52735290 autoOn = a9Get(" getAutoStatus" ) as ? Boolean ,
52745291 loopMode = (a9Get(" getAirCirculationStatus" ) as ? Int )?.takeIf { it >= 0 },
52755292 defrostFront = a9Get(" getFrontDefrostStatus" ) as ? Boolean ,
5276- defrostRear = a9Get(" getRearDefrostStatus" ) as ? Boolean
5293+ defrostRear = a9Get(" getRearDefrostStatus" ) as ? Boolean ,
5294+ airFlow = (a9Get(" getFanDirection" ) as ? Int )?.takeIf { it in 0 .. 7 }
52775295 )
52785296 }
52795297
@@ -5359,18 +5377,97 @@ object MG4Hardware {
53595377 }
53605378
53615379 fun setClimateDefrostFront (on : Boolean ): Boolean =
5362- if (isClimateA9())
5380+ ( if (isClimateA9())
53635381 a9CycleTo(" defrostFront" , " getFrontDefrostStatus" , if (on) 1 else 0 , 2 , " switchFrontDefrostStatus" )
53645382 else
53655383 acCall(if (on) " openFrontWindowDefroster" else " closeFrontWindowDefroster" )
5366- .also { climLog(" defrostFront=$on " , it) }
5384+ .also { climLog(" defrostFront=$on " , it) })
5385+ .also { scheduleAirFlowProbe(" après dégivrage AV=$on " ) }
53675386
53685387 fun setClimateDefrostRear (on : Boolean ): Boolean =
5369- if (isClimateA9())
5388+ ( if (isClimateA9())
53705389 a9CycleTo(" defrostRear" , " getRearDefrostStatus" , if (on) 1 else 0 , 2 , " switchRearDefrostStatus" )
53715390 else
53725391 acCall(if (on) " openBackWindowDefroster" else " closeBackWindowDefroster" )
5373- .also { climLog(" defrostRear=$on " , it) }
5392+ .also { climLog(" defrostRear=$on " , it) })
5393+ .also { scheduleAirFlowProbe(" après dégivrage AR=$on " ) }
5394+
5395+ /* *
5396+ * Sens de l'air — écriture DIRECTE de la valeur (0–6, voir [AirFlow]), pas une bascule.
5397+ *
5398+ * Vérifié dans le code des services d'origine : l'ancien SDK (`setBlowerDirectionMode`) et A9
5399+ * (`setFanDirection`) écrivent tous deux la valeur brute dans `HVAC_BLOWER_DIRECTION`. Le
5400+ * service ancien SDK refuse hors de 0–7 ; celui d'A9 ignore une 2e écriture dans les 300 ms
5401+ * (anti-rebond du `HvacDataStore`) — sans conséquence pour des appuis au doigt.
5402+ *
5403+ * ⚠️ Pas encore validé sur véhicule : ce que 3, 5 et 6 font du dégivrage avant, et si 5/6
5404+ * sont appliqués. Les sondes MG4_AIR encadrent chaque écriture pour le mesurer.
5405+ */
5406+ fun setClimateAirFlow (direction : Int ): Boolean {
5407+ if (direction !in 0 .. 6 ) return false
5408+ probeAirFlow(" avant écriture $direction " )
5409+ val ok = if (isClimateA9())
5410+ a9Set(" setFanDirection" , direction).also { climLog(" A9 airFlow=$direction " , it) }
5411+ else
5412+ acSet(" setBlowerDirectionMode" , direction).also { climLog(" airFlow=$direction " , it) }
5413+ scheduleAirFlowProbe(" après écriture $direction " )
5414+ return ok
5415+ }
5416+
5417+ // ── SONDE TEMPORAIRE sens de l'air ────────────────────────────────────────
5418+ // À retirer une fois validés sur véhicule : le lien dégivrage AV ⇔ valeur 4, l'effet de
5419+ // 3/5/6 sur le dégivrage, et l'application réelle de 5 et 6. Tag dédié pour filtrer.
5420+ private const val AIR_TAG = " MG4_AIR"
5421+
5422+ /* * Dernier instantané tracé : on ne journalise qu'un CHANGEMENT, pas chaque rafraîchissement. */
5423+ @Volatile private var sLastAirTrace: String? = null
5424+
5425+ /* *
5426+ * Relevé complet des sources liées au sens de l'air. Lit les DEUX voies (propriété et getter
5427+ * du SDK) et la propriété AOSP homonyme : c'est leur comparaison qui dira laquelle est fiable.
5428+ */
5429+ fun probeAirFlow (origin : String ) {
5430+ try {
5431+ val prop = getIntPropertyHvac(PROP_HVAC_BLOWER_DIRECTION , AREA_HVAC )
5432+ val aosp = getIntPropertyHvac(PROP_AOSP_FAN_DIRECTION , AREA_HVAC )
5433+ val msg = if (isClimateA9()) {
5434+ " A9 getFanDirection=${a9Get(" getFanDirection" )} prop0x1540250e=$prop aosp0x15400501=$aosp " +
5435+ " dégAV=${a9Get(" getFrontDefrostStatus" )} dégAR=${a9Get(" getRearDefrostStatus" )} " +
5436+ " clim=${a9Get(" getHvacPowerStatus" )} ventil=${a9Get(" getFanSpeed" )} "
5437+ } else {
5438+ " prop0x1540250e=$prop getBlowerDirectionMode=${acInt(" getBlowerDirectionMode" )} aosp0x15400501=$aosp " +
5439+ " dégAV=${acInt(" getFrontWindowDefroster" )} dégAR=${acInt(" getBackWindowDefroster" )} " +
5440+ " clim=${acInt(" getHvacPowerStatus" )} ventil=${acInt(" getAirVolumeLevel" )} A/C=${acInt(" getAcSwitch" )} "
5441+ }
5442+ AppLogger .i(AIR_TAG , " SONDE [$origin ] $msg " )
5443+ } catch (e: Exception ) {
5444+ AppLogger .w(AIR_TAG , " SONDE [$origin ] échec : ${e.message} " )
5445+ }
5446+ }
5447+
5448+ /* * Deux relevés différés : le véhicule met un instant à propager, et le dégivrage peut suivre. */
5449+ private fun scheduleAirFlowProbe (origin : String ) {
5450+ kotlin.concurrent.thread(name = " mg4-air-probe" , isDaemon = true ) {
5451+ try {
5452+ Thread .sleep(1_500 )
5453+ probeAirFlow(" $origin +1,5 s" )
5454+ Thread .sleep(3_000 )
5455+ probeAirFlow(" $origin +4,5 s" )
5456+ } catch (_: InterruptedException ) {}
5457+ }
5458+ }
5459+
5460+ /* *
5461+ * Trace tout changement du sens de l'air ou des dégivrages vu par les rafraîchissements du
5462+ * Dashboard — y compris ceux faits depuis l'écran d'ORIGINE, qui ne passent pas par nous.
5463+ */
5464+ private fun traceAirFlowChange (s : ClimateState ) {
5465+ val snap = " air=${s.airFlow} dégAV=${s.defrostFront} dégAR=${s.defrostRear} clim=${s.powerOn} "
5466+ if (snap == sLastAirTrace) return
5467+ val avant = sLastAirTrace
5468+ sLastAirTrace = snap
5469+ AppLogger .i(AIR_TAG , " changement : ${avant ? : " (premier relevé)" } → $snap " )
5470+ }
53745471
53755472 /* *
53765473 * Applique un préréglage complet de climatisation (automatisation température).
@@ -5404,7 +5501,8 @@ object MG4Hardware {
54045501 fanLevel : Int ,
54055502 defrostFront : Boolean? ,
54065503 defrostRear : Boolean? ,
5407- loopMode : Int?
5504+ loopMode : Int? ,
5505+ airFlow : Int? = null
54085506 ): Boolean {
54095507 val state = getClimateState() ? : run {
54105508 AppLogger .w(CLIM_TAG , " Profil : état clim illisible → abandon" )
@@ -5446,10 +5544,22 @@ object MG4Hardware {
54465544 ok = setClimateLoopMode(mode) && ok
54475545 }
54485546
5547+ // Ligne « Air » (boutons cumulables), APRÈS les anciennes lignes de dégivrage, qui
5548+ // coexistent le temps de valider le lien dégivrage ⇔ pare-brise : si les deux portent
5549+ // sur la lunette arrière, c'est la ligne Air qui l'emporte. Hors « Inchangé », le profil
5550+ // applique ce qui est affiché : lunette arrière non cochée = dégivrage arrière ÉTEINT.
5551+ if (airFlow != null ) {
5552+ AirFlow .directionForMask(airFlow)?.let { ok = setClimateAirFlow(it) && ok }
5553+ if (state.defrostRear != null ) {
5554+ ok = setClimateDefrostRear(airFlow and AirFlow .REAR_DEFROST != 0 ) && ok
5555+ }
5556+ }
5557+
54495558 AppLogger .i(CLIM_TAG , " Profil : clim=ON A/C=$ac consigne=$targetTemp " +
54505559 (if (autoMode) " ventilation=AUTO" else " vent=$fanLevel " ) +
54515560 " dégAV=${defrostFront ? : " inchangé" } dégAR=${defrostRear ? : " inchangé" } " +
5452- " recyclage=${loopMode ? : " inchangé" } → ok=$ok " )
5561+ " recyclage=${loopMode ? : " inchangé" } " +
5562+ " air=${airFlow?.let { " boutons=$it valeur=${AirFlow .directionForMask(it)} " } ? : " inchangé" } → ok=$ok " )
54535563 return ok
54545564 }
54555565
0 commit comments