Pretty display

This commit is contained in:
2026-08-15 15:00:22 +02:00
parent 2d06293325
commit e132d05a85
6 changed files with 323 additions and 121 deletions
+42 -10
View File
@@ -4,16 +4,33 @@
#include "esphome/components/json/json_util.h" #include "esphome/components/json/json_util.h"
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector>
// Callback signature: void(device_id, cloud_alert, defrost_mode) struct MicrowaveData {
using DisplayUpdateCallback = std::function<void(const std::string &device_id, bool cloud_alert, bool defrost_mode)>; std::string id;
std::string name;
int state;
bool paused;
int remaining_time;
int progress;
};
// Global states parsed from the API
static std::vector<MicrowaveData> g_microwaves;
static bool g_cloud_alert = false;
static std::string g_update_time = "--:--";
static bool g_received_update = false;
// Callback signature for refreshing UI
using DisplayUpdateCallback = std::function<void()>;
static DisplayUpdateCallback g_display_callback = nullptr; static DisplayUpdateCallback g_display_callback = nullptr;
class DisplayApiHandler : public esphome::web_server_idf::AsyncWebHandler { class DisplayApiHandler : public esphome::web_server_idf::AsyncWebHandler {
public: public:
bool canHandle(esphome::web_server_idf::AsyncWebServerRequest *request) const override { bool canHandle(esphome::web_server_idf::AsyncWebServerRequest *request) const override {
return request->url_to() == "/api/display"; // Provide the 513-byte buffer expected by ESPHome's new memory-safe API
char url_buf[513];
return request->url_to(url_buf) == "/api/display";
} }
void handleRequest(esphome::web_server_idf::AsyncWebServerRequest *request) override { void handleRequest(esphome::web_server_idf::AsyncWebServerRequest *request) override {
@@ -24,16 +41,31 @@ class DisplayApiHandler : public esphome::web_server_idf::AsyncWebHandler {
std::string body((char *)data, len); std::string body((char *)data, len);
esphome::json::parse_json(body, [](JsonObject root) -> bool { esphome::json::parse_json(body, [](JsonObject root) -> bool {
std::string device_id = root["device_id"] | "unknown"; g_cloud_alert = root["cloud_alert"] | false;
bool cloud_alert = root["cloud_alert"] | false; g_update_time = root["update_time"] | "--:--";
bool defrost_mode = root["defrost_mode"] | false;
ESP_LOGI("custom_api", "Received update for %s (Alert: %d, Defrost: %d)", g_microwaves.clear();
device_id.c_str(), cloud_alert, defrost_mode); JsonArray mw_array = root["microwaves"];
for (JsonObject mw : mw_array) {
MicrowaveData md;
md.id = mw["id"] | "unknown";
md.name = mw["name"] | "Microwave";
md.state = mw["state"] | 4;
md.paused = mw["paused"] | false;
md.remaining_time = mw["remaining_time"] | 0;
md.progress = mw["progress"] | 0;
g_microwaves.push_back(md);
}
g_received_update = true;
ESP_LOGI("custom_api", "Received update. Cloud Alert: %d, Microwaves: %d",
g_cloud_alert, g_microwaves.size());
// Execute the callback passed from YAML
if (g_display_callback != nullptr) { if (g_display_callback != nullptr) {
g_display_callback(device_id, cloud_alert, defrost_mode); g_display_callback();
} }
return true; return true;
+77 -59
View File
@@ -10,30 +10,11 @@ esphome:
priority: -100.0 priority: -100.0
then: then:
- lambda: |- - lambda: |-
setup_display_api([](const std::string &dev_id, bool alert, bool defrost) { setup_display_api([]() {
// Update global state
id(cloud_alert) = alert;
id(defrost_mode) = defrost;
// Set status message text
std::string msg = "Microwave " + dev_id;
id(status_message).publish_state(msg.c_str());
// Force immediate e-Paper refresh! // Force immediate e-Paper refresh!
id(my_display).update(); id(my_display).update();
}); });
# -------------------------------------------------------------------
# State Globals
# -------------------------------------------------------------------
globals:
- id: cloud_alert
type: bool
initial_value: 'false'
- id: defrost_mode
type: bool
initial_value: 'false'
web_server: web_server:
port: 5000 port: 5000
@@ -102,22 +83,13 @@ openthread:
# Template Text Sensor # Template Text Sensor
# ------------------------------------------------------------------- # -------------------------------------------------------------------
text_sensor: text_sensor:
- platform: template
name: "Display Status Message"
id: status_message
- platform: openthread_info - platform: openthread_info
ip_address:
name: "Thread IP"
id: thread_ip
role: role:
name: "Thread Role" name: "Thread Role"
id: thread_role id: thread_role
channel: channel:
name: "Thread Channel" name: "Thread Channel"
id: thread_channel id: thread_channel
rloc16:
name: "Thread RLOC16"
id: thread_rloc16
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Hardware SPI Pin Mapping (ESP32-H2) # Hardware SPI Pin Mapping (ESP32-H2)
@@ -139,12 +111,18 @@ font:
- file: - file:
type: gfonts type: gfonts
family: Overpass family: Overpass
weight: 400 weight: 600
id: font_body id: font_body
size: 14 size: 14
- file:
type: gfonts
family: Overpass
weight: 500
id: font_small
size: 12
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# Waveshare 2.13-inch e-Paper Display (V4 panel = 2.13inv3 in ESPHome) # Waveshare 2.13-inch e-Paper Display (250x122 standard resolution)
# ------------------------------------------------------------------- # -------------------------------------------------------------------
display: display:
- platform: waveshare_epaper - platform: waveshare_epaper
@@ -157,35 +135,75 @@ display:
update_interval: 30min update_interval: 30min
id: my_display id: my_display
lambda: |- lambda: |-
if (id(status_message).has_state()) { // --- STARTUP SCREEN ---
// Header if (!g_received_update) {
it.print(5, 3, id(font_header), id(status_message).state.c_str());
it.line(0, 26, it.get_width(), 26);
// Mode Status
if (id(defrost_mode)) {
it.print(5, 32, id(font_header), "MODE: DEFROST");
} else {
it.print(5, 32, id(font_header), "MODE: NORMAL");
}
// Cloud Alert Status Box
if (id(cloud_alert)) {
it.filled_rectangle(5, 60, 240, 24, COLOR_OFF); // Inverse background if supported, or frame box
it.print(10, 62, id(font_body), "CLOUD ALERT ACTIVE");
} else {
it.print(10, 62, id(font_body), "System Normal");
}
// Footer: Thread Info
it.line(0, 95, it.get_width(), 95);
it.printf(5, 100, id(font_body), "Role: %s | Ch: %s",
id(thread_role).state.c_str(),
id(thread_channel).state.c_str());
} else {
int center_x = it.get_width() / 2; int center_x = it.get_width() / 2;
int center_y = it.get_height() / 2; int center_y = it.get_height() / 2;
it.printf(center_x, center_y - 10, id(font_header), TextAlign::CENTER, "SmartWave"); it.printf(center_x, center_y - 10, id(font_header), TextAlign::CENTER, "SmartWave");
it.printf(center_x, center_y + 15, id(font_body), TextAlign::CENTER, "Waiting for Pi updates..."); it.printf(center_x, center_y + 15, id(font_body), TextAlign::CENTER, "Waiting for updates...");
return;
} }
// --- MICROWAVE LAYOUT ---
int y_offset = 0;
for (const auto& mw : g_microwaves) {
// Name
it.print(0, y_offset, id(font_header), mw.name.c_str());
// Decode State Text
std::string state_str;
switch(mw.state) {
case 0: state_str = mw.paused ? "PAUSED" : "COOKING"; break;
case 1: state_str = "STIRRING REQ."; break;
case 2: state_str = "DONE"; break;
case 3: state_str = "ALERT"; break;
case 4: state_str = "IDLE"; break;
default: state_str = "UNKNOWN"; break;
}
// Print Status (Top Right Align)
it.printf(it.get_width(), y_offset + 5, id(font_body), TextAlign::TOP_RIGHT, "%s", state_str.c_str());
// Process active/cooking visualisations
if (mw.state == 0) {
int bar_y = y_offset + 28;
int bar_w = 175;
int bar_h = 16;
// Draw progress bar outline & fill
it.rectangle(0, bar_y, bar_w, bar_h);
if (mw.progress > 0) {
int fill_w = (mw.progress * bar_w) / 100;
it.filled_rectangle(0, bar_y, fill_w, bar_h);
}
// Draw time remaining (or paused state)
if (mw.paused) {
it.printf(185, bar_y + 1, id(font_body), "PAUSED");
} else {
int mins = mw.remaining_time / 60;
int secs = mw.remaining_time % 60;
it.printf(185, bar_y + 1, id(font_body), "%02d:%02d", mins, secs);
}
}
else if (mw.state == 1) { // Stirring Required
it.filled_rectangle(0, y_offset + 28, 250, 20);
it.printf(125, y_offset + 28, id(font_body), COLOR_OFF, TextAlign::TOP_CENTER, "ACTION REQUIRED: STIR");
}
// Move cursor down (if we have more than 1 microwave on future larger screens)
y_offset += 60;
}
// --- SYSTEM FOOTER ---
// Line separator right above footer
it.line(0, 102, it.get_width(), 102);
// Bottom Left: Cloud Reachability & Last Update Time
std::string cloud_status = g_cloud_alert ? "Cloud: OK" : "Cloud: ERR";
it.printf(0, 105, id(font_small), "%s | %s", cloud_status.c_str(), g_update_time.c_str());
// Bottom Right: OpenThread Diagnostics
std::string role = id(thread_role).state;
it.printf(it.get_width(), 105, id(font_small), TextAlign::TOP_RIGHT,
"Th: %s (Ch %s)", role.c_str(), id(thread_channel).state.c_str());
+34 -9
View File
@@ -1,7 +1,7 @@
import asyncio import asyncio
import time import time
import requests import requests
import ipaddress import datetime
from typing import Dict from typing import Dict
from zeroconf import ServiceStateChange from zeroconf import ServiceStateChange
from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser, AsyncServiceInfo from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser, AsyncServiceInfo
@@ -55,20 +55,45 @@ class DisplayManager:
print(f"[DisplayManager] ❌ Screen disconnected: {name}") print(f"[DisplayManager] ❌ Screen disconnected: {name}")
self.screens.pop(name, None) self.screens.pop(name, None)
async def broadcast_state(self, device_id: str, microwave_states: dict, button_state: bool, cloud_alert: bool): async def broadcast_state(self, microwave_states: dict, cloud_alert: bool):
"""Broadcasts the system state JSON to all discovered screens concurrently.""" """Broadcasts the system state JSON to all discovered screens concurrently."""
if not self.screens: if not self.screens:
return return
now_time = time.time()
update_time_str = datetime.datetime.now().strftime("%H:%M")
microwaves_list = []
for mw_id, state_info in microwave_states.items():
# Extract state properties safely
state_val = state_info.cooking_state
paused = state_info.paused
rem_time = state_info.cooking_estimated_remaining_time
start_t = state_info.cooking_start_time
name = state_info.friendly_name
# Calculate progress percentage (0 to 100)
progress = 0
if start_t > 0 and rem_time > 0:
elapsed = now_time - start_t
total = elapsed + rem_time
if total > 0:
progress = int((elapsed / total) * 100)
progress = max(0, min(100, progress))
microwaves_list.append({
"id": mw_id,
"name": name,
"state": state_val,
"paused": paused,
"remaining_time": rem_time,
"progress": progress
})
payload = { payload = {
"device_id": device_id,
"timestamp": int(time.time()),
"cloud_alert": cloud_alert, "cloud_alert": cloud_alert,
"defrost_mode": button_state, "update_time": update_time_str,
"microwaves": [ "microwaves": microwaves_list
{"id": mw_id, "state": state}
for mw_id, state in microwave_states.items()
]
} }
# Dispatch POST requests in parallel without blocking the main loop # Dispatch POST requests in parallel without blocking the main loop
+82 -42
View File
@@ -4,6 +4,7 @@ import time
import asyncio import asyncio
import requests import requests
from orchestrateur.microwave_state import MicrowaveState
from orchestrateur.sensors import gps from orchestrateur.sensors import gps
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db
from shared.logging import log from shared.logging import log
@@ -12,7 +13,10 @@ from shared.lora_device import LoraCommands
from shared.alerts import AlertManager, Alert, AlertType from shared.alerts import AlertManager, Alert, AlertType
from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer
from lib.systemd_logs import get_systemd_logs from lib.systemd_logs import get_systemd_logs
from external_display_manager import DisplayManager # <-- Import display manager from external_display_manager import DisplayManager
# --- CONFIGURATION CONSTANTS ---
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
# --- DB SETUP --- # --- DB SETUP ---
DB_PATH = "orchestrateur/db.sqlite" DB_PATH = "orchestrateur/db.sqlite"
@@ -60,16 +64,27 @@ def get_device_id():
DEVICE_ID = get_device_id() DEVICE_ID = get_device_id()
# --- STATE MACHINE DEFINITIONS --- # --- DISPLAY QUEUE & NOTIFICATION HELPERS ---
class MicrowaveState: display_update_queue = None
IDLE = "IDLE" # Microwave is empty
ANALYZING = "ANALYZING" # Reading sensors & waiting for IR def notify_display_update():
WAITING_FOR_CLOUD = "WAITING_FOR_CLOUD" # Waiting for API parameters """Safely adds an update event timestamp to the display update queue."""
COOKING = "COOKING" # Microwave is active if display_update_queue is not None:
DONE = "DONE" # Finished/Stopped, waiting for dish removal try:
loop = asyncio.get_running_loop()
loop.call_soon_threadsafe(display_update_queue.put_nowait, time.time())
except RuntimeError:
pass
def set_cloud_alert(state: bool):
"""Setter for cloud_alert that triggers a display update when changed."""
global cloud_alert
if cloud_alert != state:
cloud_alert = state
notify_display_update()
# Global state trackers # Global state trackers
microwave_states = {"2": MicrowaveState.IDLE} microwave_states = {"2": MicrowaveState("2", on_change_callback=notify_display_update)}
button_state = False button_state = False
cloud_alert = False # Global status flag for screen / UI display cloud_alert = False # Global status flag for screen / UI display
async_event_queue = None async_event_queue = None
@@ -178,25 +193,40 @@ async def cloud_mqtt_listener_task():
}) })
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
async def display_broadcast_task(): async def display_broadcast_worker_task():
"""Monitors state changes and broadcasts updates to all connected screens.""" """
last_state = None Debounced display broadcast task.
Waits until DISPLAY_DEBOUNCE_SECONDS have passed since the first queued update
before sending state to all connected screens.
"""
print("[Display Worker] Started debounced display broadcast worker.")
while True: while True:
current_state = (dict(microwave_states), button_state, cloud_alert) # Block until the FIRST update reminder arrives in the queue
if current_state != last_state: first_event_time = await display_update_queue.get()
last_state = current_state
await display_manager.broadcast_state( # Compute wait time relative to the first event's timestamp
device_id=DEVICE_ID, elapsed = time.time() - first_event_time
microwave_states=microwave_states, remaining_wait = DISPLAY_DEBOUNCE_SECONDS - elapsed
button_state=button_state, if remaining_wait > 0:
cloud_alert=cloud_alert await asyncio.sleep(remaining_wait)
)
await asyncio.sleep(3.0) # Clear any subsequent events that arrived during the wait period
while not display_update_queue.empty():
try:
display_update_queue.get_nowait()
except asyncio.QueueEmpty:
break
# Broadcast state ONCE
await display_manager.broadcast_state(
microwave_states=microwave_states,
cloud_alert=cloud_alert
)
def button_callback(): def button_callback():
"""Button physical interrupt callback.""" """Button physical interrupt callback."""
global button_state global button_state
if microwave_states.get("2") == MicrowaveState.COOKING or microwave_states.get("2") == MicrowaveState.DONE: if microwave_states.get("2").cooking_state == CookingStates.COOKING or microwave_states.get("2").cooking_state == CookingStates.DONE or microwave_states.get("2").cooking_state == CookingStates.STIRRING_REQUIRED:
print("[Button] Toggling pause/resume for microwave '2'.") print("[Button] Toggling pause/resume for microwave '2'.")
lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_PAUSE}) lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_PAUSE})
else: else:
@@ -227,7 +257,7 @@ def on_new_alert(alert):
# Send the alert to the lora device # Send the alert to the lora device
lora.send_reliable(payloads.lora_new_alert(alert)) lora.send_reliable(payloads.lora_new_alert(alert))
# TODO : envoi de l'alerte aux écrans externes microwave_states["2"].set_cooking_state(CookingStates.ALERT)
# --- HARDWARE CONTROLLERS --- # --- HARDWARE CONTROLLERS ---
def _stop_hardware(microwave_id: str): def _stop_hardware(microwave_id: str):
@@ -264,7 +294,7 @@ def read_local_sensors(microwave_id, initial_dish_height):
async def handle_new_dish(microwave_id, detected_height): async def handle_new_dish(microwave_id, detected_height):
"""Triggered when a new dish is placed inside.""" """Triggered when a new dish is placed inside."""
microwave_states[microwave_id] = MicrowaveState.ANALYZING microwave_states[microwave_id].set_state(MicrowaveState.ANALYZING)
print(f"\n[{microwave_id}] 🍽️ Dish detected at {detected_height:.1f} cm! Requesting IR from microwave...") print(f"\n[{microwave_id}] 🍽️ Dish detected at {detected_height:.1f} cm! Requesting IR from microwave...")
# 1. Setup synchronization event and clear previous cache for this microwave # 1. Setup synchronization event and clear previous cache for this microwave
@@ -286,7 +316,7 @@ async def handle_new_dish(microwave_id, detected_height):
sensors_data = await sensor_task sensors_data = await sensor_task
# Check if dish was removed while reading sensors # Check if dish was removed while reading sensors
if microwave_states.get(microwave_id) != MicrowaveState.ANALYZING: if microwave_states.get(microwave_id).state != MicrowaveState.ANALYZING:
print(f"[{microwave_id}] Dish removed during sensor read. Aborting.") print(f"[{microwave_id}] Dish removed during sensor read. Aborting.")
ir_data_events.pop(microwave_id, None) ir_data_events.pop(microwave_id, None)
return return
@@ -310,7 +340,7 @@ async def handle_new_dish(microwave_id, detected_height):
async def request_cloud_cooking_plan(microwave_id, sensors_data): async def request_cloud_cooking_plan(microwave_id, sensors_data):
"""Sends all data to the cloud with up to 3 retries (330s timeout for AI generation).""" """Sends all data to the cloud with up to 3 retries (330s timeout for AI generation)."""
global cloud_alert, alert_manager global cloud_alert, alert_manager
microwave_states[microwave_id] = MicrowaveState.WAITING_FOR_CLOUD microwave_states[microwave_id].set_state(MicrowaveState.WAITING_FOR_CLOUD)
URL = "https://smartwave.matthiasg.dev/cooking-params" URL = "https://smartwave.matthiasg.dev/cooking-params"
# Format image # Format image
@@ -326,7 +356,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
HTTP_TIMEOUT = (10, 330) HTTP_TIMEOUT = (10, 330)
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries + 1):
if microwave_states.get(microwave_id) != MicrowaveState.WAITING_FOR_CLOUD: if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD:
print(f"[{microwave_id}] Dish removed or state changed. Aborting API request.") print(f"[{microwave_id}] Dish removed or state changed. Aborting API request.")
return return
@@ -341,7 +371,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
) )
# Abort if state changed while waiting for cloud AI response # Abort if state changed while waiting for cloud AI response
if microwave_states.get(microwave_id) != MicrowaveState.WAITING_FOR_CLOUD: if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD:
print(f"[{microwave_id}] Dish removed during API request. Discarding API plan.") print(f"[{microwave_id}] Dish removed during API request. Discarding API plan.")
return return
@@ -353,7 +383,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
# Error handling # Error handling
if "error" in res_json: if "error" in res_json:
print(f"[{microwave_id}] Cloud API returned error: {res_json['error']}") print(f"[{microwave_id}] Cloud API returned error: {res_json['error']}")
microwave_states[microwave_id] = MicrowaveState.DONE microwave_states[microwave_id].set_state(MicrowaveState.DONE)
if not res_json['is_safe']: # The cooking area is not safe, raise an alert if not res_json['is_safe']: # The cooking area is not safe, raise an alert
alert = Alert(AlertType.COOKING_SAFETY, res_json['warning_message']) alert = Alert(AlertType.COOKING_SAFETY, res_json['warning_message'])
@@ -371,12 +401,12 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
if c_time is None or c_power is None or c_temp is None: if c_time is None or c_power is None or c_temp is None:
print(f"[{microwave_id}] ❌ Invalid plan received: {res_json}") print(f"[{microwave_id}] ❌ Invalid plan received: {res_json}")
microwave_states[microwave_id] = None microwave_states[microwave_id].set_state(MicrowaveState.IDLE)
return return
print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W") print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W")
cloud_alert = False # Reset alert flag on success cloud_alert = False # Reset alert flag on success
microwave_states[microwave_id] = MicrowaveState.COOKING microwave_states[microwave_id].set_state(MicrowaveState.COOKING)
if config.DEBUG: if config.DEBUG:
c_time = 20 # Set to 20s for debug c_time = 20 # Set to 20s for debug
@@ -399,7 +429,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
print(f"[{microwave_id}] Retrying in {retry_delay_seconds} seconds...") print(f"[{microwave_id}] Retrying in {retry_delay_seconds} seconds...")
# Interruptible wait loop in case state changes mid-wait # Interruptible wait loop in case state changes mid-wait
for _ in range(retry_delay_seconds): for _ in range(retry_delay_seconds):
if microwave_states.get(microwave_id) != MicrowaveState.WAITING_FOR_CLOUD: if microwave_states.get(microwave_id).state != MicrowaveState.WAITING_FOR_CLOUD:
print(f"[{microwave_id}] State changed during retry wait. Aborting retries.") print(f"[{microwave_id}] State changed during retry wait. Aborting retries.")
return return
await asyncio.sleep(1) await asyncio.sleep(1)
@@ -407,7 +437,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
# Executed only if all 3 retries failed # Executed only if all 3 retries failed
print(f"[{microwave_id}] All cloud retries failed. Setting global alert flag.") print(f"[{microwave_id}] All cloud retries failed. Setting global alert flag.")
cloud_alert = True cloud_alert = True
microwave_states[microwave_id] = MicrowaveState.DONE microwave_states[microwave_id].set_state(MicrowaveState.DONE)
# --- MAIN LOGIC TASKS --- # --- MAIN LOGIC TASKS ---
async def process_messages_task(): async def process_messages_task():
@@ -427,11 +457,13 @@ async def process_messages_task():
n_state = data["data"].get("new_cooking_state") n_state = data["data"].get("new_cooking_state")
print(f"[LoRa] Microwave {mw_id} state changed to: {n_state}") print(f"[LoRa] Microwave {mw_id} state changed to: {n_state}")
microwave_states[mw_id].set_cooking_state(n_state)
if n_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT): if n_state in (CookingStates.DONE, CookingStates.STIRRING_REQUIRED, CookingStates.ALERT):
beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3) beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3)
buzzer.buzzer_siren(beatsNb=beats) buzzer.buzzer_siren(beatsNb=beats)
if n_state == CookingStates.IDLE and microwave_states.get(mw_id) == MicrowaveState.COOKING: if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
microwave_states[mw_id] = MicrowaveState.DONE microwave_states[mw_id].set_state(MicrowaveState.DONE)
print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.") print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.")
elif source == "MQTT": elif source == "MQTT":
@@ -450,6 +482,13 @@ async def process_messages_task():
# Offload DB insertion to async thread execution pool # Offload DB insertion to async thread execution pool
await asyncio.to_thread(save_connected_component, component_id, component_type) await asyncio.to_thread(save_connected_component, component_id, component_type)
# Pass change callback to newly connected microwave
microwave_states[component_id] = MicrowaveState(
component_id,
on_change_callback=notify_display_update
)
notify_display_update()
mqtt_client.publish( mqtt_client.publish(
config.MQTT_TOPIC_HELLO, config.MQTT_TOPIC_HELLO,
payloads.mqtt_hello_ack(DEVICE_ID, component_id), payloads.mqtt_hello_ack(DEVICE_ID, component_id),
@@ -497,7 +536,7 @@ async def handle_telemetry_request(endpoint: str):
# 3. Build the telemetry payload (returns a JSON string) # 3. Build the telemetry payload (returns a JSON string)
telemetry_payload = payloads.telemetry_payload( telemetry_payload = payloads.telemetry_payload(
device_id=DEVICE_ID, device_id=DEVICE_ID,
microwave_states=microwave_states, microwave_states=microwave_states.to_dict(),
button_state=button_state, button_state=button_state,
cloud_alert=cloud_alert, cloud_alert=cloud_alert,
gps_data=gps.get_gps_data(), gps_data=gps.get_gps_data(),
@@ -556,7 +595,7 @@ async def monitor_dish_height_task():
while True: while True:
dist = await get_filtered_dish_height() dist = await get_filtered_dish_height()
current_state = microwave_states.get(mw_id, MicrowaveState.IDLE) current_state = microwave_states.get(mw_id, MicrowaveState.IDLE).state
if dist is not None: if dist is not None:
# Hysteresis Thresholds: # Hysteresis Thresholds:
@@ -582,8 +621,8 @@ async def monitor_dish_height_task():
elif consecutive_absent >= REQUIRED_STABLE_READS and current_state != MicrowaveState.IDLE: elif consecutive_absent >= REQUIRED_STABLE_READS and current_state != MicrowaveState.IDLE:
consecutive_absent = 0 consecutive_absent = 0
print(f"\n[{mw_id}] Dish Removed! Resetting state to IDLE.") print(f"\n[{mw_id}] Dish Removed! Resetting state to IDLE.")
microwave_states[mw_id] = MicrowaveState.IDLE microwave_states[mw_id].set_state(MicrowaveState.IDLE)
cloud_alert = False # Reset error alert on dish removal set_cloud_alert(False)
if current_state == MicrowaveState.COOKING: if current_state == MicrowaveState.COOKING:
_stop_hardware(mw_id) _stop_hardware(mw_id)
@@ -595,7 +634,7 @@ async def monitor_dish_height_task():
# --- BOOTSTRAP --- # --- BOOTSTRAP ---
async def main(): async def main():
global async_event_queue global async_event_queue, display_update_queue
print("Orchestrateur prêt. Lancement des tâches...") print("Orchestrateur prêt. Lancement des tâches...")
# Initialize the alert manager and set the callback # Initialize the alert manager and set the callback
@@ -610,6 +649,7 @@ async def main():
await display_manager.start() await display_manager.start()
async_event_queue = asyncio.Queue() async_event_queue = asyncio.Queue()
display_update_queue = asyncio.Queue()
await asyncio.gather( await asyncio.gather(
lora_listener_task(), lora_listener_task(),
@@ -617,7 +657,7 @@ async def main():
cloud_mqtt_listener_task(), cloud_mqtt_listener_task(),
process_messages_task(), process_messages_task(),
monitor_dish_height_task(), monitor_dish_height_task(),
display_broadcast_task() display_broadcast_worker_task()
) )
if __name__ == "__main__": if __name__ == "__main__":
+85
View File
@@ -0,0 +1,85 @@
from shared.cookingState import CookingStates
import shared.config as config
class MicrowaveState:
state = None
paused = False
cooking_state = None
cooking_estimated_remaining_time = 0
cooking_start_time = 0
def __init__(self, id, on_change_callback=None):
self.friendly_name = f"Microwave-{id}"
self.state = MicrowaveState.IDLE
self.cooking_state = CookingStates.IDLE
self.on_change_callback = on_change_callback
def _notify_change(self):
"""Invokes callback if registered on state attribute changes."""
if self.on_change_callback:
try:
self.on_change_callback()
except Exception as e:
print(f"[MicrowaveState] Error in change callback: {e}")
def set_state(self, new_state):
if config.DEBUG:
assert new_state in (
MicrowaveState.IDLE,
MicrowaveState.ANALYZING,
MicrowaveState.WAITING_FOR_CLOUD,
MicrowaveState.COOKING,
MicrowaveState.DONE
), f"Invalid state: {new_state}"
if self.state != new_state:
self.state = new_state
self._notify_change()
def set_paused(self, is_paused):
if self.paused != is_paused:
self.paused = is_paused
self._notify_change()
def toggle_pause(self):
self.paused = not self.paused
self._notify_change()
def set_cooking_state(self, new_cooking_state):
if config.DEBUG:
assert new_cooking_state in (
CookingStates.IDLE,
CookingStates.COOKING,
CookingStates.DONE,
CookingStates.STIRRING_REQUIRED,
CookingStates.ALERT
), "Invalid cooking state: " + str(new_cooking_state)
if self.cooking_state != new_cooking_state:
self.cooking_state = new_cooking_state
self._notify_change()
def set_cooking_estimated_remaining_time(self, time_seconds):
if self.cooking_estimated_remaining_time != time_seconds:
self.cooking_estimated_remaining_time = time_seconds
self._notify_change()
def set_cooking_start_time(self, start_time):
if self.cooking_start_time != start_time:
self.cooking_start_time = start_time
self._notify_change()
def to_dict(self):
return {
"friendly_name": self.friendly_name,
"state": self.state,
"paused": self.paused,
"cooking_estimated_remaining_time": self.cooking_estimated_remaining_time,
"cooking_start_time": self.cooking_start_time
}
IDLE = "IDLE" # Microwave is empty
ANALYZING = "ANALYZING" # Reading sensors & waiting for IR
WAITING_FOR_CLOUD = "WAITING_FOR_CLOUD" # Waiting for API parameters
COOKING = "COOKING" # Microwave is active
DONE = "DONE" # Finished/Stopped, waiting for dish removal
+2
View File
@@ -1,4 +1,6 @@
# Debugging and others
DEBUG=True DEBUG=True
DEBUG_DANGEROUS_AREA=False
# LoRa # LoRa
LORA_HEARTBEAT_INTERVAL = 30 LORA_HEARTBEAT_INTERVAL = 30