# =============================================================================
# main.py — Boucle principale Smart Home ESP32 [v2 — correctifs Wokwi]
# =============================================================================
# Correctifs v2 :
# 1. Anti-spam MQTT : les alertes warning/critical ne sont publiées que
# lors d'un CHANGEMENT de niveau (ex: normal→warning), pas à chaque cycle.
# 2. MQ-2 : lecture ADC réelle dans Wokwi via potentiomètre sur GPIO34.
# Brancher un potentiomètre dans le diagramme Wokwi sur GPIO34 pour
# simuler la concentration de gaz (0=air pur, 4095=gaz max).
# 3. Buzzer : coroutines digitales dédiées Wokwi (buzzer actif HIGH/LOW)
# car le PWM ne produit pas de son audible dans le simulateur Wokwi.
# =============================================================================
import uasyncio as asyncio
import network
import utime
import ujson
import machine
from machine import WDT, Pin
import config
from sensors import Sensors
from actuators import Actuators
from mqtt_handler import MQTTHandler
# =============================================================================
# VARIABLES GLOBALES D'ÉTAT
# =============================================================================
_boot_time = utime.time()
_wlan = network.WLAN(network.STA_IF)
_sensors = None
_actuators = None
_mqtt = None
_wdt = None
# Tâches alarmes continues
_task_blink_gaz = None
_task_blink_temp = None
_task_blink_hum = None
_task_buzzer_gaz = None
_task_buzzer_hum = None
# ── Anti-spam : mémorisation du dernier niveau publié ───────────────────────
# Les alertes MQTT ne sont envoyées qu'au CHANGEMENT de niveau,
# pas à chaque cycle de 2s (évite de saturer le broker).
_prev_temp_level = "normal"
_prev_hum_level = "normal"
_prev_gaz_level = "ok"
# =============================================================================
# WIFI
# =============================================================================
def wifi_connect() -> bool:
global _wlan
_wlan.active(True)
if _wlan.isconnected():
if config.DEBUG:
print(f"[WIFI] Déjà connecté — IP : {_wlan.ifconfig()[0]}")
return True
print(f"[WIFI] Connexion à '{config.WIFI_SSID}'...")
_wlan.connect(config.WIFI_SSID, config.WIFI_PASSWORD)
timeout = config.WIFI_TIMEOUT
while not _wlan.isconnected() and timeout > 0:
utime.sleep(1)
timeout -= 1
if _wlan.isconnected():
print(f"[WIFI] Connecté — IP={_wlan.ifconfig()[0]}")
return True
else:
print("[WIFI] Échec de connexion.")
return False
async def task_wifi_watchdog():
global _mqtt
while True:
await asyncio.sleep(10)
if not _wlan.isconnected():
print("[WIFI] Connexion perdue — reconnexion...")
if _mqtt:
_mqtt.connected = False
wifi_connect()
# =============================================================================
# BUZZER DIGITAL — compatibilité buzzer ACTIF Wokwi
# =============================================================================
# Dans Wokwi, le buzzer "actif" répond à HIGH/LOW simple.
# Le PWM (buzzer passif) ne produit pas de son dans le simulateur.
async def buzzer_bips_digital(pin: Pin, count: int,
on_ms: int, pause_ms: int):
"""Émet `count` bips digitaux de durée `on_ms` séparés de `pause_ms`."""
for _ in range(count):
pin.value(1)
await asyncio.sleep_ms(on_ms)
pin.value(0)
if pause_ms > 0:
await asyncio.sleep_ms(pause_ms)
async def _bip_gaz_wokwi(pin: Pin):
"""Bip continu gaz critique : 200ms ON / 100ms OFF tant qu'alarme active."""
while _actuators and _actuators.alarm_gaz_critique:
pin.value(1)
await asyncio.sleep_ms(200)
pin.value(0)
await asyncio.sleep_ms(100)
pin.value(0)
async def _bip_hum_wokwi(pin: Pin):
"""Bip intermittent humidité critique : 500ms ON / 500ms OFF."""
while _actuators and _actuators.alarm_hum_critique:
pin.value(1)
await asyncio.sleep_ms(500)
pin.value(0)
await asyncio.sleep_ms(500)
pin.value(0)
async def _bip_temp_wokwi(pin: Pin):
"""5 bips rapides pour température critique (one-shot)."""
for _ in range(5):
pin.value(1)
await asyncio.sleep_ms(100)
pin.value(0)
await asyncio.sleep_ms(100)
# =============================================================================
# INITIALISATION
# =============================================================================
async def init_system():
global _sensors, _actuators, _mqtt, _wdt
print("=" * 60)
print(" Smart Home ESP32 — Démarrage [v2]")
print("=" * 60)
# ── WiFi ────────────────────────────────────────────────────────────────
connected = False
for attempt in range(3):
connected = wifi_connect()
if connected:
break
print(f"[INIT] Tentative WiFi {attempt + 1}/3 échouée.")
utime.sleep(config.WIFI_RETRY_DELAY)
if not connected:
print("[INIT] WiFi impossible — reset dans 5s...")
utime.sleep(5)
machine.reset()
# ── Actionneurs ──────────────────────────────────────────────────────────
print("[INIT] Actionneurs...")
_actuators = Actuators()
# ── Capteurs ─────────────────────────────────────────────────────────────
print("[INIT] Capteurs...")
_sensors = Sensors()
# ── Signal de démarrage ──────────────────────────────────────────────────
print("[INIT] Signal démarrage...")
_buzzer_pin = Pin(config.PIN_BUZZER, Pin.OUT)
if getattr(config, 'WOKWI_MODE', False):
# Wokwi : 3 bips digitaux courts (buzzer actif)
await buzzer_bips_digital(_buzzer_pin, 3, 150, 100)
else:
# Physique : mélodie PWM
await _actuators.buzzer_melody_startup()
_actuators.led_verte_on()
# ── MQTT ─────────────────────────────────────────────────────────────────
print("[INIT] MQTT...")
_mqtt = MQTTHandler(_actuators)
if _mqtt.connect():
print("[INIT] MQTT OK.")
_mqtt.publish_alert("system", "Smart Home ESP32 v2 démarré",
"warning", utime.time())
else:
print("[INIT] MQTT hors ligne — reconnexion automatique.")
# ── Watchdog ─────────────────────────────────────────────────────────────
_wdt = WDT(timeout=config.WDT_TIMEOUT_MS)
print(f"[INIT] WDT activé ({config.WDT_TIMEOUT_MS}ms).")
print("[INIT] Système opérationnel.")
print("=" * 60)
# =============================================================================
# TÂCHE — LECTURE CAPTEURS (toutes les 2s)
# =============================================================================
async def task_sensors():
"""
Lit DHT, PIR et MQ-2 toutes les 2s.
Publie les alertes MQTT uniquement lors d'un CHANGEMENT de niveau
pour éviter de spammer le broker à chaque cycle.
"""
global _task_blink_gaz, _task_blink_temp, _task_blink_hum
global _task_buzzer_gaz, _task_buzzer_hum
global _prev_temp_level, _prev_hum_level, _prev_gaz_level
_buzzer_pin = Pin(config.PIN_BUZZER, Pin.OUT)
wokwi = getattr(config, 'WOKWI_MODE', False)
while True:
await asyncio.sleep(config.INTERVAL_SENSORS_S)
if _sensors is None or _mqtt is None:
continue
timestamp = utime.time()
# ══════════════════════════════════════════════════════════════════════
# DHT — Température + Humidité
# ══════════════════════════════════════════════════════════════════════
try:
dht_data = _sensors.read_dht()
if dht_data["error"] is None and dht_data["temp"] is not None:
temp = dht_data["temp"]
humidity = dht_data["humidity"]
temp_lvl = dht_data["temp_level"]
hum_lvl = dht_data["hum_level"]
# Toujours publier les données brutes
_mqtt.publish_dht(temp, humidity, timestamp)
# ── Température ───────────────────────────────────────────────
if temp_lvl != _prev_temp_level:
print(f"[TEMP] {_prev_temp_level} → {temp_lvl} ({temp}°C)")
_prev_temp_level = temp_lvl
if temp_lvl == "critical":
if not _actuators.alarm_temp_critique:
_actuators.start_alarme_temperature()
_mqtt.publish_alert(
"temperature",
f"Température critique : {temp}°C",
"critical", timestamp
)
# LED rouge très rapide
_task_blink_temp = asyncio.create_task(
_actuators.led_rouge_blink_continu(
config.LED_BLINK_VERY_FAST_MS
)
)
# Buzzer
if wokwi:
asyncio.create_task(
_bip_temp_wokwi(_buzzer_pin)
)
elif temp_lvl == "warning":
if _actuators.alarm_temp_critique:
_actuators.alarm_temp_critique = False
_actuators._blink_rouge_running = False
_actuators.relais_off("Temp revenue en warning")
_actuators._update_alarm_state()
_mqtt.publish_alert(
"temperature",
f"Température élevée : {temp}°C",
"warning", timestamp
)
if wokwi:
asyncio.create_task(
buzzer_bips_digital(_buzzer_pin, 1, 300, 0)
)
else: # normal
if _actuators.alarm_temp_critique:
_actuators.alarm_temp_critique = False
_actuators._blink_rouge_running = False
_actuators.relais_off("Température normalisée")
_actuators._update_alarm_state()
# ── Humidité ──────────────────────────────────────────────────
if hum_lvl != _prev_hum_level:
print(f"[HUM] {_prev_hum_level} → {hum_lvl} ({humidity}%)")
_prev_hum_level = hum_lvl
if hum_lvl == "critical":
if not _actuators.alarm_hum_critique:
_actuators.start_alarme_humidite()
_mqtt.publish_alert(
"humidity",
f"Humidité critique : {humidity}%",
"critical", timestamp
)
_task_blink_hum = asyncio.create_task(
_actuators.led_rouge_blink_continu(
config.LED_BLINK_SLOW_MS
)
)
if wokwi:
asyncio.create_task(
_bip_hum_wokwi(_buzzer_pin)
)
else:
_task_buzzer_hum = asyncio.create_task(
_actuators.buzzer_intermittent()
)
elif hum_lvl == "warning":
if _actuators.alarm_hum_critique:
_actuators.alarm_hum_critique = False
_actuators._blink_rouge_running = False
_actuators.relais_off("Hum revenue en warning")
_actuators._update_alarm_state()
_mqtt.publish_alert(
"humidity",
f"Humidité élevée : {humidity}%",
"warning", timestamp
)
if wokwi:
asyncio.create_task(
buzzer_bips_digital(_buzzer_pin, 2, 200, 150)
)
else: # normal / dry
if _actuators.alarm_hum_critique:
_actuators.alarm_hum_critique = False
_actuators._blink_rouge_running = False
_actuators.relais_off("Humidité normalisée")
_actuators._update_alarm_state()
except Exception as e:
print(f"[TASK SENSORS] Erreur DHT : {e}")
# ══════════════════════════════════════════════════════════════════════
# PIR
# ══════════════════════════════════════════════════════════════════════
try:
pir_data = _sensors.read_pir()
if pir_data["motion"]:
_mqtt.publish_pir(True, timestamp)
if _mqtt.mode_securite:
_mqtt.publish_alert(
"pir", "Mouvement détecté en mode sécurité",
"critical", timestamp
)
if wokwi:
await asyncio.gather(
_actuators.led_rouge_blink_n(3, 300),
buzzer_bips_digital(_buzzer_pin, 3, 300, 150)
)
else:
await _actuators.sequence_pir_alarme()
else:
if config.DEBUG:
print("[PIR] Mouvement (mode sécurité OFF — pas d'alarme locale)")
except Exception as e:
print(f"[TASK SENSORS] Erreur PIR : {e}")
# ══════════════════════════════════════════════════════════════════════
# MQ-2 — lecture ADC réelle (potentiomètre Wokwi sur GPIO34)
# ══════════════════════════════════════════════════════════════════════
try:
mq2_data = _sensors.read_mq2()
if mq2_data["error"] is None:
level = mq2_data["level"]
aout_raw = mq2_data["aout_raw"]
dout_triggered = mq2_data["dout"]
# Toujours publier la valeur brute (pour le dashboard)
_mqtt.publish_gaz(aout_raw, dout_triggered, level, timestamp)
# Alerte uniquement sur changement de niveau
if level != _prev_gaz_level:
print(f"[MQ2] {_prev_gaz_level} → {level} (ADC={aout_raw})")
_prev_gaz_level = level
if level == "crit":
if not _actuators.alarm_gaz_critique:
_actuators.start_alarme_gaz()
_mqtt.publish_alert(
"gaz",
f"GAZ CRITIQUE : ADC={aout_raw}",
"critical", timestamp
)
_task_blink_gaz = asyncio.create_task(
_actuators.led_rouge_blink_continu(
config.LED_BLINK_FAST_MS
)
)
if wokwi:
asyncio.create_task(
_bip_gaz_wokwi(_buzzer_pin)
)
else:
_task_buzzer_gaz = asyncio.create_task(
_actuators.buzzer_long_continu()
)
elif level == "warn":
if _actuators.alarm_gaz_critique:
_actuators.alarm_gaz_critique = False
_actuators._blink_rouge_running = False
_actuators._update_alarm_state()
_mqtt.publish_alert(
"gaz",
f"Gaz élevé : ADC={aout_raw}",
"warning", timestamp
)
if wokwi:
asyncio.create_task(
buzzer_bips_digital(_buzzer_pin, 2, 300, 200)
)
else: # ok
if _actuators.alarm_gaz_critique:
_actuators.alarm_gaz_critique = False
_actuators._blink_rouge_running = False
_actuators._update_alarm_state()
except Exception as e:
print(f"[TASK SENSORS] Erreur MQ-2 : {e}")
# ── Mise à jour LED verte ─────────────────────────────────────────────
if not _actuators.alarm_active:
_actuators.led_verte_on()
else:
_actuators.led_verte_off()
# =============================================================================
# TÂCHE — POLLING RFID (toutes les 500ms)
# =============================================================================
async def task_rfid():
_last_uid = None
_last_uid_time = 0
DEBOUNCE_MS = 3000
wokwi = getattr(config, 'WOKWI_MODE', False)
_buzzer_pin = Pin(config.PIN_BUZZER, Pin.OUT)
while True:
await asyncio.sleep(config.INTERVAL_RFID_S)
if _sensors is None or _mqtt is None:
continue
try:
rfid_data = _sensors.read_rfid()
if not rfid_data["present"] or rfid_data["uid"] is None:
continue
uid = rfid_data["uid"]
granted = rfid_data["granted"]
timestamp = rfid_data["timestamp"]
# Debounce : même badge ignoré pendant 3s
now = utime.ticks_ms()
if (uid == _last_uid and
utime.ticks_diff(now, _last_uid_time) < DEBOUNCE_MS):
continue
_last_uid = uid
_last_uid_time = now
status = "granted" if granted else "denied"
_mqtt.publish_rfid(uid, status, timestamp)
if granted:
print(f"[RFID] Accès ACCORDÉ — UID={uid}")
if wokwi:
await asyncio.gather(
_actuators.led_verte_pulse(1000),
buzzer_bips_digital(_buzzer_pin, 1, 100, 0)
)
await _actuators.relais_pulse(
config.RELAIS_RFID_DURATION_MS, "RFID accordé"
)
else:
await _actuators.sequence_rfid_granted()
else:
print(f"[RFID] Accès REFUSÉ — UID={uid}")
if wokwi:
await asyncio.gather(
_actuators.led_rouge_fixe(2000),
buzzer_bips_digital(_buzzer_pin, 2, 100, 150)
)
else:
await _actuators.sequence_rfid_denied()
except Exception as e:
print(f"[TASK RFID] Erreur : {e}")
# =============================================================================
# TÂCHE — MQTT CHECK (toutes les 100ms)
# =============================================================================
async def task_mqtt_loop():
while True:
await asyncio.sleep(config.INTERVAL_MQTT_CHECK_S)
if _mqtt is None:
continue
if not _mqtt.connected:
if _wlan.isconnected():
_mqtt.reconnect_if_needed()
continue
_mqtt.check_messages()
await _mqtt.process_pending_commands()
# =============================================================================
# TÂCHE — STATUT SYSTÈME (toutes les 60s)
# =============================================================================
async def task_publish_status():
while True:
await asyncio.sleep(config.INTERVAL_STATUS_S)
if _mqtt is None or not _mqtt.connected:
continue
try:
uptime = utime.time() - _boot_time
wifi_rssi = _wlan.status("rssi") if _wlan.isconnected() else 0
mode_sec = _mqtt.mode_securite
act_state = _actuators.get_state()
_mqtt.publish_status(uptime, wifi_rssi, mode_sec, act_state)
if config.DEBUG:
print(f"[STATUS] Uptime={uptime}s RSSI={wifi_rssi}dBm ")
print(f"Sécurité={'ON' if mode_sec else 'OFF'} ")
print(f"Alarme={'OUI' if _actuators.alarm_active else 'NON'}")
except Exception as e:
print(f"[TASK STATUS] Erreur : {e}")
# =============================================================================
# TÂCHE — WATCHDOG FEED (toutes les 2s)
# =============================================================================
async def task_watchdog():
while True:
await asyncio.sleep(2)
if _wdt is not None:
_wdt.feed()
# =============================================================================
# MAIN
# =============================================================================
async def main():
await init_system()
print("[MAIN] Lancement des tâches uasyncio...")
asyncio.create_task(task_sensors())
asyncio.create_task(task_rfid())
asyncio.create_task(task_mqtt_loop())
asyncio.create_task(task_publish_status())
asyncio.create_task(task_wifi_watchdog())
asyncio.create_task(task_watchdog())
print("[MAIN] Toutes les tâches actives. Système en fonctionnement.")
# Heartbeat de supervision toutes les 30s
while True:
await asyncio.sleep(30)
if config.DEBUG:
uptime = utime.time() - _boot_time
wifi = "OK" if _wlan.isconnected() else "DOWN"
mqtt = "OK" if (_mqtt and _mqtt.connected) else "DOWN"
alarm = "OUI" if (_actuators and _actuators.alarm_active) else "non"
print(f"[♥] Uptime={uptime}s WiFi={wifi} MQTT={mqtt} Alarme={alarm}")
# =============================================================================
# LANCEMENT
# =============================================================================
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n[MAIN] Arrêt manuel.")
if _actuators:
_actuators.stop_alarm()
_actuators.led_verte_off()
_actuators.led_rouge_off()
_actuators.relais_off("Arrêt système")
if _mqtt:
_mqtt.disconnect()
print("[MAIN] Arrêt propre.")
except Exception as e:
print(f"[MAIN] Erreur fatale : {e}")
utime.sleep(5)
machine.reset()Loading
mfrc522
mfrc522