Pretty display
This commit is contained in:
+82
-42
@@ -4,6 +4,7 @@ import time
|
||||
import asyncio
|
||||
import requests
|
||||
|
||||
from orchestrateur.microwave_state import MicrowaveState
|
||||
from orchestrateur.sensors import gps
|
||||
from shared import get_lora, get_mqtt_client, deviceTypes, config, payloads, db
|
||||
from shared.logging import log
|
||||
@@ -12,7 +13,10 @@ from shared.lora_device import LoraCommands
|
||||
from shared.alerts import AlertManager, Alert, AlertType
|
||||
from sensors import ultrasonicRanger, temp_hum, button, camera, buzzer
|
||||
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_PATH = "orchestrateur/db.sqlite"
|
||||
@@ -60,16 +64,27 @@ def get_device_id():
|
||||
|
||||
DEVICE_ID = get_device_id()
|
||||
|
||||
# --- STATE MACHINE DEFINITIONS ---
|
||||
class MicrowaveState:
|
||||
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
|
||||
# --- DISPLAY QUEUE & NOTIFICATION HELPERS ---
|
||||
display_update_queue = None
|
||||
|
||||
def notify_display_update():
|
||||
"""Safely adds an update event timestamp to the display update queue."""
|
||||
if display_update_queue is not None:
|
||||
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
|
||||
microwave_states = {"2": MicrowaveState.IDLE}
|
||||
microwave_states = {"2": MicrowaveState("2", on_change_callback=notify_display_update)}
|
||||
button_state = False
|
||||
cloud_alert = False # Global status flag for screen / UI display
|
||||
async_event_queue = None
|
||||
@@ -178,25 +193,40 @@ async def cloud_mqtt_listener_task():
|
||||
})
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def display_broadcast_task():
|
||||
"""Monitors state changes and broadcasts updates to all connected screens."""
|
||||
last_state = None
|
||||
async def display_broadcast_worker_task():
|
||||
"""
|
||||
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:
|
||||
current_state = (dict(microwave_states), button_state, cloud_alert)
|
||||
if current_state != last_state:
|
||||
last_state = current_state
|
||||
await display_manager.broadcast_state(
|
||||
device_id=DEVICE_ID,
|
||||
microwave_states=microwave_states,
|
||||
button_state=button_state,
|
||||
cloud_alert=cloud_alert
|
||||
)
|
||||
await asyncio.sleep(3.0)
|
||||
# Block until the FIRST update reminder arrives in the queue
|
||||
first_event_time = await display_update_queue.get()
|
||||
|
||||
# Compute wait time relative to the first event's timestamp
|
||||
elapsed = time.time() - first_event_time
|
||||
remaining_wait = DISPLAY_DEBOUNCE_SECONDS - elapsed
|
||||
if remaining_wait > 0:
|
||||
await asyncio.sleep(remaining_wait)
|
||||
|
||||
# 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():
|
||||
"""Button physical interrupt callback."""
|
||||
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'.")
|
||||
lora.send_reliable({"id": DEVICE_ID, "microwave_id": "2", "action": LoraCommands.TOGGLE_PAUSE})
|
||||
else:
|
||||
@@ -227,7 +257,7 @@ def on_new_alert(alert):
|
||||
# Send the alert to the lora device
|
||||
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 ---
|
||||
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):
|
||||
"""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...")
|
||||
|
||||
# 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
|
||||
|
||||
# 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.")
|
||||
ir_data_events.pop(microwave_id, None)
|
||||
return
|
||||
@@ -310,7 +340,7 @@ async def handle_new_dish(microwave_id, detected_height):
|
||||
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)."""
|
||||
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"
|
||||
|
||||
# Format image
|
||||
@@ -326,7 +356,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
||||
HTTP_TIMEOUT = (10, 330)
|
||||
|
||||
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.")
|
||||
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
|
||||
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.")
|
||||
return
|
||||
|
||||
@@ -353,7 +383,7 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
||||
# Error handling
|
||||
if "error" in res_json:
|
||||
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
|
||||
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:
|
||||
print(f"[{microwave_id}] ❌ Invalid plan received: {res_json}")
|
||||
microwave_states[microwave_id] = None
|
||||
microwave_states[microwave_id].set_state(MicrowaveState.IDLE)
|
||||
return
|
||||
|
||||
print(f"[{microwave_id}] Cloud Plan Received! Starting microwave: {c_time}s @ {c_power}W")
|
||||
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:
|
||||
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...")
|
||||
# Interruptible wait loop in case state changes mid-wait
|
||||
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.")
|
||||
return
|
||||
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
|
||||
print(f"[{microwave_id}] All cloud retries failed. Setting global alert flag.")
|
||||
cloud_alert = True
|
||||
microwave_states[microwave_id] = MicrowaveState.DONE
|
||||
microwave_states[microwave_id].set_state(MicrowaveState.DONE)
|
||||
|
||||
# --- MAIN LOGIC TASKS ---
|
||||
async def process_messages_task():
|
||||
@@ -427,11 +457,13 @@ async def process_messages_task():
|
||||
n_state = data["data"].get("new_cooking_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):
|
||||
beats = 5 if n_state == CookingStates.ALERT else (1 if n_state == CookingStates.STIRRING_REQUIRED else 3)
|
||||
buzzer.buzzer_siren(beatsNb=beats)
|
||||
if n_state == CookingStates.IDLE and microwave_states.get(mw_id) == MicrowaveState.COOKING:
|
||||
microwave_states[mw_id] = MicrowaveState.DONE
|
||||
if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
|
||||
microwave_states[mw_id].set_state(MicrowaveState.DONE)
|
||||
print(f"[{mw_id}] Cooking finished. Waiting for user to remove dish.")
|
||||
|
||||
elif source == "MQTT":
|
||||
@@ -450,6 +482,13 @@ async def process_messages_task():
|
||||
# Offload DB insertion to async thread execution pool
|
||||
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(
|
||||
config.MQTT_TOPIC_HELLO,
|
||||
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)
|
||||
telemetry_payload = payloads.telemetry_payload(
|
||||
device_id=DEVICE_ID,
|
||||
microwave_states=microwave_states,
|
||||
microwave_states=microwave_states.to_dict(),
|
||||
button_state=button_state,
|
||||
cloud_alert=cloud_alert,
|
||||
gps_data=gps.get_gps_data(),
|
||||
@@ -556,7 +595,7 @@ async def monitor_dish_height_task():
|
||||
|
||||
while True:
|
||||
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:
|
||||
# Hysteresis Thresholds:
|
||||
@@ -582,8 +621,8 @@ async def monitor_dish_height_task():
|
||||
elif consecutive_absent >= REQUIRED_STABLE_READS and current_state != MicrowaveState.IDLE:
|
||||
consecutive_absent = 0
|
||||
print(f"\n[{mw_id}] Dish Removed! Resetting state to IDLE.")
|
||||
microwave_states[mw_id] = MicrowaveState.IDLE
|
||||
cloud_alert = False # Reset error alert on dish removal
|
||||
microwave_states[mw_id].set_state(MicrowaveState.IDLE)
|
||||
set_cloud_alert(False)
|
||||
|
||||
if current_state == MicrowaveState.COOKING:
|
||||
_stop_hardware(mw_id)
|
||||
@@ -595,7 +634,7 @@ async def monitor_dish_height_task():
|
||||
|
||||
# --- BOOTSTRAP ---
|
||||
async def main():
|
||||
global async_event_queue
|
||||
global async_event_queue, display_update_queue
|
||||
print("Orchestrateur prêt. Lancement des tâches...")
|
||||
|
||||
# Initialize the alert manager and set the callback
|
||||
@@ -610,6 +649,7 @@ async def main():
|
||||
await display_manager.start()
|
||||
|
||||
async_event_queue = asyncio.Queue()
|
||||
display_update_queue = asyncio.Queue()
|
||||
|
||||
await asyncio.gather(
|
||||
lora_listener_task(),
|
||||
@@ -617,7 +657,7 @@ async def main():
|
||||
cloud_mqtt_listener_task(),
|
||||
process_messages_task(),
|
||||
monitor_dish_height_task(),
|
||||
display_broadcast_task()
|
||||
display_broadcast_worker_task()
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user