"""
NEKO — Distributeur de croquettes
ESP32 + OLED SSD1306 128×64 I2C + DS3231 RTC
Broches I2C : SCL=22 SDA=21
Moteur : EN=5 STEP=18 DIR=19
Boutons : +/GPIO13 -/GPIO14 OK/GPIO12
DS3231 sur le même bus I2C (adresse 0x68)
🔧 CORRIGÉ : Suppression duplication repas + flags présents
"""
from machine import Pin, I2C
import uasyncio as asyncio
import ssd1306
import framebuf
# ─────────────────────────────────────────────────────────────────────────────
# MATÉRIEL
# ─────────────────────────────────────────────────────────────────────────────
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400_000)
oled = ssd1306.SSD1306_I2C(128, 64, i2c)
STEP_PIN = Pin(18, Pin.OUT)
DIR_PIN = Pin(19, Pin.OUT)
EN_PIN = Pin(5, Pin.OUT)
BTN_PLUS = Pin(13, Pin.IN, Pin.PULL_UP)
BTN_MINUS = Pin(14, Pin.IN, Pin.PULL_UP)
BTN_OK = Pin(12, Pin.IN, Pin.PULL_UP)
EN_PIN.value(1)
STEP_PIN.value(0)
DIR_PIN.value(1)
# ─────────────────────────────────────────────────────────────────────────────
# DS3231 — lecture / écriture BCD
# ─────────────────────────────────────────────────────────────────────────────
DS3231_ADDR = 0x68
def _bcd2dec(v): return (v >> 4) * 10 + (v & 0x0F)
def _dec2bcd(v): return ((v // 10) << 4) | (v % 10)
def ds3231_get():
"""Retourne (heure, minute, seconde) depuis le DS3231."""
try:
data = i2c.readfrom_mem(DS3231_ADDR, 0x00, 3)
s = _bcd2dec(data[0] & 0x7F)
mn = _bcd2dec(data[1])
h = _bcd2dec(data[2] & 0x3F)
return (h, mn, s)
except Exception:
return (0, 0, 0)
def ds3231_set_time(h, mn, s=0):
"""Ecrit l'heure dans le DS3231."""
try:
i2c.writeto_mem(DS3231_ADDR, 0x00, bytes([
_dec2bcd(s),
_dec2bcd(mn),
_dec2bcd(h),
]))
except Exception as e:
print("DS3231 write error:", e)
def get_heure_str():
h, mn, s = ds3231_get()
return "{:02d}:{:02d}:{:02d}".format(h, mn, s)
def get_heure_courte():
h, mn, s = ds3231_get()
return "{:02d}:{:02d}".format(h, mn)
# ─────────────────────────────────────────────────────────────────────────────
# CONSTANTES
# ─────────────────────────────────────────────────────────────────────────────
GRAMS_PER_ROTATION = 5
STEPS_PER_ROTATION = 533
MIN_DOSE = 5
MAX_DOSE = 100
MIN_PLUS = 5 # pas de réglage des minutes
# ─────────────────────────────────────────────────────────────────────────────
# ÉTAT GLOBAL (UNE SEULE DÉCLARATION!)
# ─────────────────────────────────────────────────────────────────────────────
motor_busy = False
veille_mode = False
# ✅ SEULE déclaration de repas avec flags INCLUS
repas = {
"heure": 8,
"minute": 0,
"grammage": 20,
"actif": False,
"last_execution_hour": -1, # ← FLAG 1 : Heure dernière exécution
"last_execution_minute": -1, # ← FLAG 2 : Minute dernière exécution
}
# ─────────────────────────────────────────────────────────────────────────────
# SPRITES (MONO_HLSB)
# ─────────────────────────────────────────────────────────────────────────────
NEKO_LOGO = bytearray([
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0xff,0xff,0xff,0xff,0xe0,0x03,0xff,0xff,0xff,0xff,0xf0,
0x07,0xff,0xff,0xf0,0x0f,0xf8,0x07,0xff,0xff,0xe0,0x03,0xf8,
0x07,0xff,0xff,0xe0,0x03,0xf8,0x07,0xf8,0xff,0xe0,0x03,0xf8,
0x07,0xf0,0x78,0x60,0x03,0xf8,0x07,0xe0,0x38,0x60,0x7f,0xf8,
0x07,0xe0,0x30,0x60,0xff,0xf8,0x07,0xe0,0x10,0x60,0x07,0xf8,
0x07,0xe0,0x10,0x20,0x03,0xf8,0x07,0xe0,0x00,0x20,0x03,0xf8,
0x07,0xe0,0x00,0x20,0x03,0xf8,0x07,0xe0,0x00,0x20,0x03,0xf8,
0x07,0xe0,0x00,0x20,0x7f,0xf8,0x07,0xe0,0x80,0x20,0xff,0xf8,
0x07,0xe1,0x80,0x60,0x7f,0xf8,0x07,0xe1,0xc0,0x60,0x01,0xf8,
0x07,0xe1,0xc0,0x60,0x00,0xf8,0x07,0xe1,0xe0,0x40,0x00,0xf8,
0x07,0xe3,0xf0,0xc0,0x01,0xf8,0x07,0xe3,0xf8,0xe0,0x01,0xf8,
0x07,0xf7,0xff,0xf0,0x03,0xf8,0x07,0xe3,0xff,0xff,0xff,0xf8,
0x07,0xe1,0xfd,0xfc,0x3f,0xf8,0x07,0xc1,0xf0,0xf0,0x0f,0xf8,
0x07,0xc1,0xc0,0xe0,0x07,0xf8,0x07,0xc1,0x81,0xc0,0x03,0xf8,
0x07,0xc1,0x01,0x80,0x03,0xf8,0x07,0xc2,0x07,0x80,0x03,0xf8,
0x07,0xc0,0x0f,0x81,0xc3,0xf8,0x07,0xc0,0x3f,0x83,0xc3,0xf8,
0x07,0xc0,0x1f,0x83,0xc3,0xf8,0x07,0xc0,0x0f,0x83,0xc3,0xf8,
0x07,0xc2,0x07,0x83,0xc3,0xf8,0x07,0xc2,0x07,0x83,0xc3,0xf8,
0x07,0xc2,0x03,0xc3,0xc3,0xf8,0x07,0xc3,0x03,0xc1,0x83,0xf8,
0x07,0xc3,0x01,0xc0,0x07,0xf8,0x07,0xc3,0x81,0xe0,0x07,0xf8,
0x07,0xe3,0xc3,0xf0,0x0f,0xf8,0x03,0xff,0xe7,0xf8,0x3f,0xf8,
0x01,0xff,0xff,0xff,0xff,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
])
CHECK = bytearray([
0x00,0x00,0x07,0xe0,0x18,0x10,0x20,0x08,
0x00,0x14,0x40,0x78,0x00,0x70,0x0c,0xe2,
0x1f,0xc2,0x47,0x80,0x43,0x00,0x02,0x04,
0x20,0x08,0x18,0x10,0x03,0xc0,0x00,0x00,
])
CAT = bytearray([
0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x3c,
0x11,0xb6,0x3f,0xe2,0x7f,0xfa,0x7f,0xfe,
0x7f,0xfc,0x7d,0xfc,0x7d,0x7c,0x7f,0x7c,
0x3f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,
])
HEART = bytearray([
0b01101100,
0b11111110,
0b11111110,
0b11111110,
0b01111100,
0b00111000,
0b00010000,
0b00000000,
])
def blit(data, x, y, w, h):
fb = framebuf.FrameBuffer(bytearray(data), w, h, framebuf.MONO_HLSB)
oled.blit(fb, x, y)
# ═════════════════════════════════════════════════════════════════════════════
# ÉCRANS
# ═════════════════════════════════════════════════════════════════════════════
def cls(): oled.fill(0)
def show(): oled.show()
# ── Boot ─────────────────────────────────────────────────────────────────────
def screen_boot():
cls()
blit(NEKO_LOGO, 40, 8, 48, 48)
show()
# ── Bienvenue (CENTRALISÉ ET AGRANDI) ────────────────────────────────────────
async def screen_welcome():
for _ in range(6):
cls()
oled.hline(0, 10, 128, 1)
oled.text("Bienvenue !", 18, 20)
oled.text("sur NEKO", 20, 34)
oled.hline(0, 54, 128, 1)
show()
await asyncio.sleep_ms(200)
await asyncio.sleep_ms(400)
# ── Menu principal (6 items) ─────────────────────────────────────────────────
MAIN_ITEMS = ["Mode", "Programme", "Regl. Heure", "Parametres", "Code QR", "WiFi"]
def screen_main_menu(sel):
cls()
oled.text("NEKO", 0, 0)
oled.text(get_heure_courte(), 80, 0)
oled.hline(0, 10, 128, 1)
visible = 4
start = max(0, min(sel - 1, len(MAIN_ITEMS) - visible))
for idx in range(visible):
i = start + idx
if i >= len(MAIN_ITEMS): break
y = 13 + idx * 12
if i == sel:
oled.fill_rect(0, y - 1, 128, 11, 1)
oled.text("> " + MAIN_ITEMS[i], 2, y, 0)
else:
oled.text(" " + MAIN_ITEMS[i], 2, y)
oled.hline(0, 58, 128, 1)
oled.text("+ - OK", 32, 59, 1)
show()
# ── Mode veille ──────────────────────────────────────────────────────────────
async def screen_standby():
global veille_mode
veille_mode = True
blink_count = 0
while veille_mode:
cls()
oled.text("NEKO", 30, 8)
oled.hline(0, 18, 128, 1)
oled.text(get_heure_str(), 18, 28)
if blink_count % 4 < 2:
blit(CAT, 56, 44, 16, 16)
oled.hline(0, 62, 128, 1)
show()
await asyncio.sleep_ms(500)
blink_count += 1
# ── Sous-menu modes ───────────────────────────────────────────────────────────
MODE_ITEMS = ["Manuel", "Programme", "Distance"]
def screen_mode_menu(sel):
cls()
oled.text("-- Modes --", 18, 0)
oled.hline(0, 10, 128, 1)
for i, item in enumerate(MODE_ITEMS):
y = 14 + i * 14
if i == sel:
oled.fill_rect(0, y - 1, 128, 12, 1)
oled.text("> " + item, 2, y, 0)
else:
oled.text(" " + item, 2, y)
oled.hline(0, 58, 128, 1)
oled.text("+ - OK", 32, 59, 1)
show()
# ── Dose manuelle ─────────────────────────────────────────────────────────────
def screen_set_dose(grams):
cls()
oled.text("Mode Manuel", 14, 0)
oled.hline(0, 10, 128, 1)
oled.text("Dose:", 2, 15)
oled.text("{:3d} g".format(grams), 50, 15)
bar_w = 110
filled = int((grams - MIN_DOSE) / (MAX_DOSE - MIN_DOSE) * bar_w)
oled.rect(4, 28, bar_w + 4, 8, 1)
oled.fill_rect(6, 30, max(1, filled), 4, 1)
oled.text("{}g".format(MIN_DOSE), 2, 38)
oled.text("{}g".format(MAX_DOSE), 98, 38)
oled.hline(0, 50, 128, 1)
oled.text("[+]", 0, 53)
oled.text("[-]", 44, 53)
oled.text("[OK]=GO", 78, 53)
show()
# ── Distribution en cours ─────────────────────────────────────────────────────
def screen_distributing(grams, cur, total):
cls()
oled.text("Distribution...", 0, 0)
oled.hline(0, 10, 128, 1)
oled.text("{} g".format(grams), 44, 14)
bar_w = 110
filled = int(cur / total * bar_w)
oled.rect(4, 26, bar_w + 4, 10, 1)
oled.fill_rect(6, 28, max(1, filled), 6, 1)
oled.text("{} / {}".format(cur, total), 36, 42)
oled.text("rotations", 22, 54)
show()
# ── Distribution terminée ─────────────────────────────────────────────────────
async def screen_done(grams):
for blink in range(5):
cls()
oled.text("Repas distribue!", 0, 2)
oled.hline(0, 12, 128, 1)
oled.text("{} g".format(grams), 48, 16)
if blink % 2 == 0:
blit(CHECK, 56, 28, 16, 16)
show()
await asyncio.sleep_ms(350)
# ── Distribution automatique (programmée) ────────────────────────────────────
async def screen_auto_distribution(grams):
"""Affiche la distribution automatique au temps programmé."""
for blink in range(4):
cls()
oled.text("Distribution", 10, 4)
oled.text("AUTOMATIQUE", 14, 14)
oled.hline(0, 24, 128, 1)
oled.text("{} g".format(grams), 48, 32)
if blink % 2 == 0:
blit(CAT, 56, 44, 16, 16)
show()
await asyncio.sleep_ms(300)
# ── Merci ─────────────────────────────────────────────────────────────────────
def screen_merci():
cls()
oled.text("Merci", 38, 8)
blit(HEART, 90, 8, 8, 8)
blit(CAT, 56, 28, 16, 16)
show()
# ── Réglage générique (heure/min/dose) ───────────────────────────────────────
def screen_reglage(titre, valeur, unite):
cls()
oled.text(get_heure_str(), 20, 0)
oled.hline(0, 10, 128, 1)
oled.text(titre, 0, 16)
oled.text("> {} {}".format(valeur, unite), 4, 30)
oled.hline(0, 54, 128, 1)
oled.text("[+/-] [OK]", 0, 56)
show()
# ── Repas planifié confirmé ───────────────────────────────────────────────────
def screen_planifie():
cls()
oled.text("PLANIFIE !", 20, 10)
oled.text("{:02d}:{:02d} {}g".format(
repas["heure"], repas["minute"], repas["grammage"]), 14, 28)
blit(CHECK, 56, 44, 16, 16)
show()
# ── Paramètres ────────────────────────────────────────────────────────────────
def screen_params():
cls()
oled.text("Parametres", 16, 0)
oled.hline(0, 10, 128, 1)
oled.text("Min: {}g".format(MIN_DOSE), 4, 16)
oled.text("Max: {}g".format(MAX_DOSE), 4, 28)
oled.text("Pas: {}g".format(GRAMS_PER_ROTATION), 4, 40)
oled.hline(0, 54, 128, 1)
oled.text("OK = Retour", 14, 56)
show()
# ── QR Code (placeholder) ─────────────────────────────────────────────────────
def screen_qr():
cls()
oled.text("Code QR", 28, 0)
oled.hline(0, 10, 128, 1)
oled.rect(30,14,68,38,1)
oled.rect(32,16,14,14,1); oled.fill_rect(34,18,10,10,1)
oled.rect(82,16,14,14,1); oled.fill_rect(84,18,10,10,1)
oled.rect(32,37,14,14,1); oled.fill_rect(34,39,10,10,1)
oled.fill_rect(50,16,8,8,1); oled.fill_rect(62,22,6,6,1)
oled.fill_rect(50,30,4,4,1); oled.fill_rect(58,36,8,8,1)
oled.hline(0,54,128,1)
oled.text("OK = Retour",14,56)
show()
# ── WiFi ──────────────────────────────────────────────────────────────────────
def screen_wifi():
cls()
oled.text("WiFi", 44, 0)
oled.hline(0, 10, 128, 1)
oled.text("Non connecte", 10, 20)
oled.rect(52, 36, 24, 16, 1)
oled.text("WiFi", 56, 38, 0)
oled.hline(0, 54, 128, 1)
oled.text("OK = Retour", 14, 56)
show()
# ── Erreur ────────────────────────────────────────────────────────────────────
async def screen_error(l1, l2=""):
cls()
oled.rect(2, 2, 124, 60, 1)
oled.text("!", 60, 10)
oled.text(l1, max(0, (128 - len(l1)*8)//2), 26)
if l2:
oled.text(l2, max(0, (128 - len(l2)*8)//2), 40)
show()
await asyncio.sleep_ms(900)
# ═════════════════════════════════════════════════════════════════════════════
# MOTEUR
# ═════════════════════════════════════════════════════════════════════════════
async def rotate_one_portion(direction=1):
start_delay_ms = 8
min_delay_ms = 1 #pour la vitess rapide
ramp_steps = 80
steps = STEPS_PER_ROTATION
EN_PIN.value(0)
DIR_PIN.value(direction)
await asyncio.sleep_ms(10)
for i in range(steps):
if i < ramp_steps: ratio = i / ramp_steps
elif i > steps - ramp_steps: ratio = (steps - i) / ramp_steps
else: ratio = 1.0
delay_ms = int(start_delay_ms - (start_delay_ms - min_delay_ms) * ratio)
half = max(1, delay_ms // 2)
STEP_PIN.value(1)
await asyncio.sleep_ms(half)
STEP_PIN.value(0)
await asyncio.sleep_ms(half)
EN_PIN.value(1)
await asyncio.sleep_ms(200)
async def distribuer(grams):
global motor_busy
motor_busy = True
rotations = grams // GRAMS_PER_ROTATION
for i in range(rotations):
screen_distributing(grams, i + 1, rotations)
await rotate_one_portion(direction=1)
await screen_done(grams)
motor_busy = False
# ═════════════════════════════════════════════════════════════════════════════
# BOUTON
# ═════════════════════════════════════════════════════════════════════════════
async def read_btn():
for pin, name in [(BTN_PLUS,'plus'),(BTN_MINUS,'minus'),(BTN_OK,'ok')]:
if pin.value() == 0:
await asyncio.sleep_ms(30)
if pin.value() == 0:
while pin.value() == 0:
await asyncio.sleep_ms(5)
await asyncio.sleep_ms(20)
return name
return None
# ═════════════════════════════════════════════════════════════════════════════
# NAVIGATION GÉNÉRIQUE
# ═════════════════════════════════════════════════════════════════════════════
async def navigate_menu_with_timeout(draw_fn, items, timeout_ms):
"""Navigation avec timeout pour activer mode veille."""
global veille_mode
sel = 0
draw_fn(sel)
start_time = None
last_time = ""
while True:
current_time = get_heure_courte()
if current_time != last_time:
last_time = current_time
draw_fn(sel)
btn = await read_btn()
if btn:
start_time = None
if btn == 'plus':
sel = (sel - 1) % len(items)
draw_fn(sel)
elif btn == 'minus':
sel = (sel + 1) % len(items)
draw_fn(sel)
elif btn == 'ok':
return sel
else:
if start_time is None:
start_time = 0
start_time += 50
if start_time >= timeout_ms:
veille_mode = True
await screen_standby()
veille_mode = False
start_time = None
draw_fn(sel)
await asyncio.sleep_ms(50)
async def navigate_menu(draw_fn, items):
sel = 0
draw_fn(sel)
last_time = ""
while True:
current_time = get_heure_courte()
if current_time != last_time:
last_time = current_time
draw_fn(sel)
btn = await read_btn()
if btn == 'plus':
sel = (sel - 1) % len(items)
draw_fn(sel)
elif btn == 'minus':
sel = (sel + 1) % len(items)
draw_fn(sel)
elif btn == 'ok':
return sel
await asyncio.sleep_ms(100)
# ═════════════════════════════════════════════════════════════════════════════
# RÉGLAGE DE L'HEURE DS3231
# ═════════════════════════════════════════════════════════════════════════════
async def menu_reglage_heure():
"""Permet de régler l'heure et les minutes du DS3231."""
h, mn, s = ds3231_get()
# ── Étape 1 : heure ──────────────────────────────────────────────────────
screen_reglage("Regler Heure", "{:02d}".format(h), "h")
while True:
btn = await read_btn()
if btn == 'plus':
h = (h + 3) % 24
elif btn == 'minus':
h = (h - 1) % 24
elif btn == 'ok':
break
screen_reglage("Regler Heure", "{:02d}".format(h), "h")
await asyncio.sleep_ms(10)
# ── Étape 2 : minutes ────────────────────────────────────────────────────
screen_reglage("Regler Minutes", "{:02d}".format(mn), "min")
while True:
btn = await read_btn()
if btn == 'plus':
mn = (mn + 5) % 60
elif btn == 'minus':
mn = (mn - 1) % 60
elif btn == 'ok':
break
screen_reglage("Regler Minutes", "{:02d}".format(mn), "min")
await asyncio.sleep_ms(10)
# ── Sauvegarde dans le DS3231 ────────────────────────────────────────────
ds3231_set_time(h, mn, 0)
cls()
oled.text("Heure sauvee !", 8, 20)
oled.text("{:02d}:{:02d}".format(h, mn), 44, 36)
show()
await asyncio.sleep_ms(1500)
# ═════════════════════════════════════════════════════════════════════════════
# MODE MANUEL
# ═════════════════════════════════════════════════════════════════════════════
async def run_manual_mode():
grams = MIN_DOSE
screen_set_dose(grams)
while True:
btn = await read_btn()
if btn == 'plus':
if grams < MAX_DOSE:
grams += GRAMS_PER_ROTATION
else:
await screen_error("Maximum {}g".format(MAX_DOSE))
screen_set_dose(grams)
elif btn == 'minus':
if grams > MIN_DOSE:
grams -= GRAMS_PER_ROTATION
else:
await screen_error("Minimum {}g".format(MIN_DOSE))
screen_set_dose(grams)
elif btn == 'ok':
await distribuer(grams)
screen_merci()
await asyncio.sleep_ms(1800)
return
await asyncio.sleep_ms(10)
# ═════════════════════════════════════════════════════════════════════════════
# MODE PROGRAMMÉ avec SYSTÈME DE FLAG (CORRIGÉ)
# ═════════════════════════════════════════════════════════════════════════════
async def menu_programme():
"""Configure et active un repas planifié via DS3231."""
global repas
# Étape 1 — heure du repas
screen_reglage("Heure repas", "{:02d}".format(repas["heure"]), "h")
while True:
btn = await read_btn()
if btn == 'plus':
repas["heure"] = (repas["heure"] + 3) % 24
elif btn == 'minus':
repas["heure"] = (repas["heure"] - 1) % 24
elif btn == 'ok':
break
screen_reglage("Heure repas", "{:02d}".format(repas["heure"]), "h")
await asyncio.sleep_ms(10)
# Étape 2 — minutes du repas
screen_reglage("Minute repas", "{:02d}".format(repas["minute"]), "min")
while True:
btn = await read_btn()
if btn == 'plus':
repas["minute"] = (repas["minute"] + 5) % 60
elif btn == 'minus':
repas["minute"] = (repas["minute"] - 1) % 60
elif btn == 'ok':
break
screen_reglage("Minute repas", "{:02d}".format(repas["minute"]), "min")
await asyncio.sleep_ms(10)
# Étape 3 — grammage
screen_reglage("Dose repas", repas["grammage"], "g")
while True:
btn = await read_btn()
if btn == 'plus':
if repas["grammage"] < MAX_DOSE:
repas["grammage"] += GRAMS_PER_ROTATION
elif btn == 'minus':
if repas["grammage"] > MIN_DOSE:
repas["grammage"] -= GRAMS_PER_ROTATION
elif btn == 'ok':
break
screen_reglage("Dose repas", repas["grammage"], "g")
await asyncio.sleep_ms(10)
# ACTIVATION + RÉINITIALISATION FLAGS
repas["actif"] = True
repas["last_execution_hour"] = -1
repas["last_execution_minute"] = -1
print(f"✓ Programme activé: {repas['heure']:02d}:{repas['minute']:02d}, {repas['grammage']}g")
print(f" Flags: h={repas['last_execution_hour']}, m={repas['last_execution_minute']}")
screen_planifie()
await asyncio.sleep_ms(2000)
async def check_programme():
"""
VERSION CORRIGÉE avec système de FLAG
Évite race condition sur la seconde exacte
"""
global motor_busy, veille_mode, repas
h, mn, s = ds3231_get()
# Condition 1 : Repas doit être actif
if not repas["actif"]:
return
# Condition 2 : Moteur pas en cours
if motor_busy:
return
# Condition 3 : L'heure/minute correspondent
if h != repas["heure"] or mn != repas["minute"]:
return
# Condition 4 : C'est la PREMIÈRE FOIS cette minute (flag check)
if (h == repas["last_execution_hour"] and
mn == repas["last_execution_minute"]):
# Déjà exécuté cette minute
return
# ✅ TOUTES CONDITIONS REMPLIES : DISTRIBUTION !
print(f"[DISTRIBUTION] {h:02d}:{mn:02d}:{s:02d} → {repas['grammage']}g")
repas["last_execution_hour"] = h
repas["last_execution_minute"] = mn
veille_mode = False
await asyncio.sleep_ms(100)
await screen_auto_distribution(repas["grammage"])
await distribuer(repas["grammage"])
screen_merci()
await asyncio.sleep_ms(1800)
repas["actif"] = False
# ═════════════════════════════════════════════════════════════════════════════
# BONUS : Réactivation quotidienne à minuit
# ═════════════════════════════════════════════════════════════════════════════
async def check_midnight_reset():
"""
Réactive le programme tous les jours à minuit
pour distribution quotidienne fiable
"""
global repas
h, mn, s = ds3231_get()
# À minuit exactement, réactiver le programme
if h == 0 and mn == 0 and not repas["actif"]:
repas["actif"] = True
repas["last_execution_hour"] = -1
repas["last_execution_minute"] = -1
print("🌙 Minuit : Programme réactivé pour demain")
# ═════════════════════════════════════════════════════════════════════════════
# TÂCHE PRINCIPALE
# ═════════════════════════════════════════════════════════════════════════════
async def main_task():
global veille_mode, motor_busy
screen_boot()
await asyncio.sleep_ms(1200)
await screen_welcome()
STANDBY_TIMEOUT = 120000 # 2 minutes
while True:
# ✅ Vérifications PRIORITAIRES
await check_programme()
await check_midnight_reset()
# Menu avec timeout
if not veille_mode:
sel_main = await navigate_menu_with_timeout(screen_main_menu, MAIN_ITEMS, STANDBY_TIMEOUT)
if veille_mode:
continue
if sel_main == 0: # Mode
sel_mode = await navigate_menu(screen_mode_menu, MODE_ITEMS)
if sel_mode == 0:
await run_manual_mode()
elif sel_mode == 1:
await menu_programme()
elif sel_mode == 2:
await screen_error("Bientot dispo")
elif sel_main == 1: # Programme (raccourci)
await menu_programme()
elif sel_main == 2: # Réglage Heure
await menu_reglage_heure()
elif sel_main == 3: # Paramètres
screen_params()
while True:
btn = await read_btn()
if btn == 'ok': break
await asyncio.sleep_ms(10)
elif sel_main == 4: # QR Code
screen_qr()
while True:
btn = await read_btn()
if btn == 'ok': break
await asyncio.sleep_ms(10)
elif sel_main == 5: # WiFi
screen_wifi()
while True:
btn = await read_btn()
if btn == 'ok': break
await asyncio.sleep_ms(10)
await asyncio.sleep_ms(10)
# ═════════════════════════════════════════════════════════════════════════════
# POINT D'ENTRÉE
# ═════════════════════════════════════════════════════════════════════════════
try:
asyncio.run(main_task())
except KeyboardInterrupt:
EN_PIN.value(1)
cls()
oled.text("Arret.", 30, 28)
show()
print("Interrompu — driver desactive")