Sending updates to external screen + RGB LCD
This commit is contained in:
@@ -17,7 +17,7 @@ struct MicrowaveData {
|
|||||||
|
|
||||||
// Global states parsed from the API
|
// Global states parsed from the API
|
||||||
static std::vector<MicrowaveData> g_microwaves;
|
static std::vector<MicrowaveData> g_microwaves;
|
||||||
static bool g_cloud_alert = false;
|
static bool g_cloud_alert = true;
|
||||||
static std::string g_update_time = "--:--";
|
static std::string g_update_time = "--:--";
|
||||||
static bool g_received_update = false;
|
static bool g_received_update = false;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ except Exception:
|
|||||||
|
|
||||||
# --- GLOBAL VARIABLES ---
|
# --- GLOBAL VARIABLES ---
|
||||||
cooking_state = None
|
cooking_state = None
|
||||||
|
cooking_start_time = None # Track start timestamp
|
||||||
data_queue = SafeQueue()
|
data_queue = SafeQueue()
|
||||||
lora = None
|
lora = None
|
||||||
uart_device = None
|
uart_device = None
|
||||||
@@ -46,6 +47,31 @@ PING_PAYLOAD = {
|
|||||||
"type": deviceTypes.DEVICE_TYPES["MICROWAVE"]
|
"type": deviceTypes.DEVICE_TYPES["MICROWAVE"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_cooking_update(state=None, start_time=None, estimated_remaining_time=None, paused=None):
|
||||||
|
"""Sends a flexible COOKING_UPDATE payload containing only populated fields."""
|
||||||
|
if not lora:
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"id": DEVICE_ID,
|
||||||
|
"action": LoraCommands.COOKING_UPDATE
|
||||||
|
}
|
||||||
|
|
||||||
|
if state is not None:
|
||||||
|
payload["cooking_state"] = state
|
||||||
|
if estimated_remaining_time is not None:
|
||||||
|
payload["estimated_remaining_time"] = estimated_remaining_time
|
||||||
|
if paused is not None:
|
||||||
|
payload["paused"] = paused
|
||||||
|
|
||||||
|
# Only transmit if at least one field beyond id and action was provided
|
||||||
|
if len(payload) > 2:
|
||||||
|
log(f"[LoRa] Sending COOKING_UPDATE payload: {payload}")
|
||||||
|
|
||||||
|
lora.send(payload)
|
||||||
|
|
||||||
|
|
||||||
def on_new_alert(alert: Alert):
|
def on_new_alert(alert: Alert):
|
||||||
global microwave_screen
|
global microwave_screen
|
||||||
print(f"[Alert Manager] New alert received: {alert.to_dict()}")
|
print(f"[Alert Manager] New alert received: {alert.to_dict()}")
|
||||||
@@ -138,9 +164,7 @@ def cooking_state_temperature_provider():
|
|||||||
# 3. Handle fresh incoming reading
|
# 3. Handle fresh incoming reading
|
||||||
if not temp_is_invalid(current_temp):
|
if not temp_is_invalid(current_temp):
|
||||||
temps = [float(current_temp[0]), float(current_temp[1])]
|
temps = [float(current_temp[0]), float(current_temp[1])]
|
||||||
last_temp = [temps[0], temps[1]] # Keep a safe reference copy
|
last_temp = [temps[0], temps[1]]
|
||||||
|
|
||||||
# Reset current_temp buffer to consume the value
|
|
||||||
current_temp = [None, None]
|
current_temp = [None, None]
|
||||||
return temps
|
return temps
|
||||||
|
|
||||||
@@ -152,6 +176,7 @@ def cooking_state_temperature_provider():
|
|||||||
return (0.0, 0.0)
|
return (0.0, 0.0)
|
||||||
|
|
||||||
def cooking_state_on_state_change(state):
|
def cooking_state_on_state_change(state):
|
||||||
|
global cooking_start_time
|
||||||
print(f"[CookingState] State changed to: {state.state}")
|
print(f"[CookingState] State changed to: {state.state}")
|
||||||
|
|
||||||
if state.paused or state.state == cookingState.CookingStates.DONE or state.state == cookingState.CookingStates.IDLE:
|
if state.paused or state.state == cookingState.CookingStates.DONE or state.state == cookingState.CookingStates.IDLE:
|
||||||
@@ -162,8 +187,14 @@ def cooking_state_on_state_change(state):
|
|||||||
# Send state updates to WiFi board and Orchestrator
|
# Send state updates to WiFi board and Orchestrator
|
||||||
if uart_device:
|
if uart_device:
|
||||||
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state}))
|
uart_device.send_as_command(UARTCommand(UARTCommandType.COOKING_STATE_UPDATE, {"state": state.state}))
|
||||||
if lora:
|
|
||||||
lora.send_reliable({"id": DEVICE_ID, "new_cooking_state": state.state})
|
# Send LoRa update with start time and remaining time if beginning cooking process
|
||||||
|
send_cooking_update(
|
||||||
|
state=state.state,
|
||||||
|
start_time=cooking_start_time if state.state == cookingState.CookingStates.COOKING else None,
|
||||||
|
estimated_remaining_time=state.get_remaining_time() if hasattr(state, "get_remaining_time") else None,
|
||||||
|
paused=state.paused
|
||||||
|
)
|
||||||
|
|
||||||
microwave_screen.update(cooking_state, defrost_mode)
|
microwave_screen.update(cooking_state, defrost_mode)
|
||||||
|
|
||||||
@@ -176,14 +207,20 @@ def cooking_state_on_pause(state):
|
|||||||
# If the cooking is unpaused and was in STIRRING_REQUIRED or ALERT state, we set the state back to COOKING.
|
# If the cooking is unpaused and was in STIRRING_REQUIRED or ALERT state, we set the state back to COOKING.
|
||||||
if not state.paused and (state.state == cookingState.CookingStates.STIRRING_REQUIRED or state.state == cookingState.CookingStates.ALERT):
|
if not state.paused and (state.state == cookingState.CookingStates.STIRRING_REQUIRED or state.state == cookingState.CookingStates.ALERT):
|
||||||
state.set_state(cookingState.CookingStates.COOKING)
|
state.set_state(cookingState.CookingStates.COOKING)
|
||||||
# TODO: send_reliable lora message to orchestrator about pause/resume state
|
|
||||||
|
# Send update on pause/resume toggle
|
||||||
|
send_cooking_update(
|
||||||
|
state=state.state,
|
||||||
|
paused=state.paused,
|
||||||
|
estimated_remaining_time=state.get_remaining_time()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --- ASYNC TASKS ---
|
# --- ASYNC TASKS ---
|
||||||
|
|
||||||
async def uart_polling_task():
|
async def uart_polling_task():
|
||||||
"""Polls UART for incoming messages from the WiFi board."""
|
"""Polls UART for incoming messages from the WiFi board."""
|
||||||
global cooking_state, temperature_asked
|
global cooking_state, cooking_start_time, temperature_asked
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if uart_device and uart_device.any():
|
if uart_device and uart_device.any():
|
||||||
@@ -196,6 +233,9 @@ async def uart_polling_task():
|
|||||||
|
|
||||||
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
|
uart_device.send_as_command(UARTCommand(UARTCommandType.TEMPERATURE_REQUEST, {}))
|
||||||
|
|
||||||
|
# Track start time timestamp
|
||||||
|
cooking_start_time = time.time()
|
||||||
|
|
||||||
cooking_state = cookingState.CookingState(
|
cooking_state = cookingState.CookingState(
|
||||||
cook_time=params["cook_time"],
|
cook_time=params["cook_time"],
|
||||||
power_level=params["power_level"],
|
power_level=params["power_level"],
|
||||||
@@ -271,10 +311,16 @@ async def cooking_loop_task():
|
|||||||
cooking_state.update_tick()
|
cooking_state.update_tick()
|
||||||
log_counter += 1
|
log_counter += 1
|
||||||
|
|
||||||
# Print log output every 5 seconds (10 ticks x 500ms)
|
# Print log output and send periodic updates every 5 seconds (10 ticks x 500ms)
|
||||||
if log_counter % 10 == 0:
|
if log_counter % 10 == 0:
|
||||||
print(f"[Cooking Task] State: {cooking_state.state}, Temp: {cooking_state.current_dish_temp}, "
|
print(f"[Cooking Task] State: {cooking_state.state}, Temp: {cooking_state.current_dish_temp}, "
|
||||||
f"Paused: {cooking_state.paused}, Remaining: {cooking_state.get_remaining_time():.2f}s")
|
f"Paused: {cooking_state.paused}, Remaining: {cooking_state.get_remaining_time():.2f}s")
|
||||||
|
|
||||||
|
# Send periodic time remaining update while actively cooking
|
||||||
|
if not cooking_state.paused and cooking_state.state == cookingState.CookingStates.COOKING:
|
||||||
|
send_cooking_update(
|
||||||
|
estimated_remaining_time=cooking_state.get_remaining_time()
|
||||||
|
)
|
||||||
|
|
||||||
await asyncio.sleep_ms(500)
|
await asyncio.sleep_ms(500)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ services:
|
|||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
environment:
|
environment:
|
||||||
- OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800
|
- OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800
|
||||||
- OT_INFRA_IF=wlan0
|
- OT_INFRA_IF=wlx6815790f3204
|
||||||
- OT_THREAD_IF=wpan0
|
- OT_THREAD_IF=wpan0
|
||||||
- OT_LOG_LEVEL=6
|
- OT_LOG_LEVEL=6
|
||||||
- FIREWALL=0
|
- FIREWALL=0
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class DisplayManager:
|
|||||||
self.screens: Dict[str, str] = {} # Map: service_name -> URL
|
self.screens: Dict[str, str] = {} # Map: service_name -> URL
|
||||||
self.aiozc: AsyncZeroconf | None = None
|
self.aiozc: AsyncZeroconf | None = None
|
||||||
self.browser: AsyncServiceBrowser | None = None
|
self.browser: AsyncServiceBrowser | None = None
|
||||||
|
self._on_screens_change_callback = None # Callback for screen changes
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Starts dynamic mDNS discovery for e-Paper screens."""
|
"""Starts dynamic mDNS discovery for e-Paper screens."""
|
||||||
@@ -48,12 +49,20 @@ class DisplayManager:
|
|||||||
url = f"http://{ip_str}:{port}{self.endpoint_path}"
|
url = f"http://{ip_str}:{port}{self.endpoint_path}"
|
||||||
|
|
||||||
self.screens[name] = url
|
self.screens[name] = url
|
||||||
|
if self._on_screens_change_callback:
|
||||||
|
self._on_screens_change_callback(self.screens)
|
||||||
print(f"[DisplayManager] 📺 Screen registered: {name} -> {url}")
|
print(f"[DisplayManager] 📺 Screen registered: {name} -> {url}")
|
||||||
|
|
||||||
elif state_change == ServiceStateChange.Removed:
|
elif state_change == ServiceStateChange.Removed:
|
||||||
if name in self.screens:
|
if name in self.screens:
|
||||||
print(f"[DisplayManager] ❌ Screen disconnected: {name}")
|
print(f"[DisplayManager] ❌ Screen disconnected: {name}")
|
||||||
self.screens.pop(name, None)
|
self.screens.pop(name, None)
|
||||||
|
if self._on_screens_change_callback:
|
||||||
|
self._on_screens_change_callback(self.screens)
|
||||||
|
|
||||||
|
def set_on_screens_change_callback(self, callback):
|
||||||
|
"""Set a callback function to be called when screens are added or removed."""
|
||||||
|
self._on_screens_change_callback = callback
|
||||||
|
|
||||||
async def broadcast_state(self, microwave_states: dict, 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."""
|
||||||
@@ -113,9 +122,9 @@ class DisplayManager:
|
|||||||
timeout=3.0
|
timeout=3.0
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
print(f"[DisplayManager] ✅ Updated {name}")
|
print(f"[DisplayManager] Updated {name}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DisplayManager] ⚠️ Error pushing state to {name}: {e}")
|
print(f"[DisplayManager] Error pushing state to {name}: {e}")
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
"""Clean up mDNS browser and Zeroconf instance."""
|
"""Clean up mDNS browser and Zeroconf instance."""
|
||||||
|
|||||||
+54
-8
@@ -11,9 +11,13 @@ from shared.logging import log
|
|||||||
from shared.cookingState import CookingStates
|
from shared.cookingState import CookingStates
|
||||||
from shared.lora_device import LoraCommands
|
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, rgb_lcd
|
||||||
from lib.systemd_logs import get_systemd_logs
|
from lib.systemd_logs import get_systemd_logs
|
||||||
from external_display_manager import DisplayManager
|
from external_display_manager import DisplayManager
|
||||||
|
from rgb_lcd_manager import RGBLCDManager
|
||||||
|
|
||||||
|
rgb_lcd.setRGB(255, 255, 255, brightness=0.8)
|
||||||
|
rgb_lcd.setText("SmartWave\nOrchestrateur")
|
||||||
|
|
||||||
# --- CONFIGURATION CONSTANTS ---
|
# --- CONFIGURATION CONSTANTS ---
|
||||||
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
|
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
|
||||||
@@ -69,6 +73,7 @@ display_update_queue = None
|
|||||||
|
|
||||||
def notify_display_update():
|
def notify_display_update():
|
||||||
"""Safely adds an update event timestamp to the display update queue."""
|
"""Safely adds an update event timestamp to the display update queue."""
|
||||||
|
|
||||||
if display_update_queue is not None:
|
if display_update_queue is not None:
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
@@ -79,18 +84,21 @@ def notify_display_update():
|
|||||||
def set_cloud_alert(state: bool):
|
def set_cloud_alert(state: bool):
|
||||||
"""Setter for cloud_alert that triggers a display update when changed."""
|
"""Setter for cloud_alert that triggers a display update when changed."""
|
||||||
global cloud_alert
|
global cloud_alert
|
||||||
|
|
||||||
if cloud_alert != state:
|
if cloud_alert != state:
|
||||||
cloud_alert = state
|
cloud_alert = state
|
||||||
notify_display_update()
|
notify_display_update()
|
||||||
|
update_lcd_cloud_alert()
|
||||||
|
|
||||||
# Global state trackers
|
# Global state trackers
|
||||||
microwave_states = {"2": MicrowaveState("2", on_change_callback=notify_display_update)}
|
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 = None # Global status flag for screen / UI display
|
||||||
async_event_queue = None
|
async_event_queue = None
|
||||||
|
|
||||||
# Display Manager instance
|
# Display Manager instance
|
||||||
display_manager = DisplayManager(endpoint_path="/api/display")
|
display_manager = DisplayManager(endpoint_path="/api/display")
|
||||||
|
rgb_lcd_manager = RGBLCDManager(rgb_lcd, brightness=0.8, interval=5.0)
|
||||||
|
|
||||||
# Async synchronization trackers for MQTT IR sensors responses
|
# Async synchronization trackers for MQTT IR sensors responses
|
||||||
ir_data_cache = {} # mw_id -> dict of IR readings
|
ir_data_cache = {} # mw_id -> dict of IR readings
|
||||||
@@ -112,6 +120,10 @@ mqtt_client.connect()
|
|||||||
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
|
mqtt_client.subscribe(config.MQTT_TOPIC_SENSOR, qos=config.MQTT_QOS)
|
||||||
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
mqtt_client.subscribe(config.MQTT_TOPIC_HELLO, qos=config.MQTT_QOS)
|
||||||
|
|
||||||
|
# DHT sensor warmup
|
||||||
|
time.sleep(2) # Allow GrovePi MCU and DHT sensor circuits to settle post-service start
|
||||||
|
temp_hum.get_temperature_and_humidity() # Dummy read to flush initial NaN state
|
||||||
|
|
||||||
if hasattr(mqtt_client._client, "loop_start"):
|
if hasattr(mqtt_client._client, "loop_start"):
|
||||||
mqtt_client._client.loop_start()
|
mqtt_client._client.loop_start()
|
||||||
print("[MQTT Local] Paho background loop started.")
|
print("[MQTT Local] Paho background loop started.")
|
||||||
@@ -192,6 +204,23 @@ async def cloud_mqtt_listener_task():
|
|||||||
"data": payload
|
"data": payload
|
||||||
})
|
})
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
# === LCD DISPLAY ===
|
||||||
|
def on_screens_change(screens):
|
||||||
|
"""Callback for when screens are added or removed."""
|
||||||
|
log(f"[DisplayManager] Screens changed. Current screens: {list(screens.keys())}")
|
||||||
|
rgb_lcd_manager.set_external_screen_count(len(screens))
|
||||||
|
display_manager.set_on_screens_change_callback(on_screens_change)
|
||||||
|
|
||||||
|
def update_lcd_microwave_count():
|
||||||
|
"""Updates the LCD with the current number of connected microwaves."""
|
||||||
|
global microwave_states
|
||||||
|
count = len(microwave_states)
|
||||||
|
rgb_lcd_manager.set_microwave_count(count)
|
||||||
|
|
||||||
|
def update_lcd_cloud_alert():
|
||||||
|
"""Updates the LCD with the current cloud connectivity status."""
|
||||||
|
global cloud_alert
|
||||||
|
rgb_lcd_manager.set_cloud_alert(cloud_alert)
|
||||||
|
|
||||||
async def display_broadcast_worker_task():
|
async def display_broadcast_worker_task():
|
||||||
"""
|
"""
|
||||||
@@ -199,6 +228,8 @@ async def display_broadcast_worker_task():
|
|||||||
Waits until DISPLAY_DEBOUNCE_SECONDS have passed since the first queued update
|
Waits until DISPLAY_DEBOUNCE_SECONDS have passed since the first queued update
|
||||||
before sending state to all connected screens.
|
before sending state to all connected screens.
|
||||||
"""
|
"""
|
||||||
|
global display_update_queue, microwave_states, cloud_alert
|
||||||
|
|
||||||
print("[Display Worker] Started debounced display broadcast worker.")
|
print("[Display Worker] Started debounced display broadcast worker.")
|
||||||
while True:
|
while True:
|
||||||
# Block until the FIRST update reminder arrives in the queue
|
# Block until the FIRST update reminder arrives in the queue
|
||||||
@@ -331,7 +362,7 @@ async def handle_new_dish(microwave_id, detected_height):
|
|||||||
# 6. Dispatch cloud request task
|
# 6. Dispatch cloud request task
|
||||||
asyncio.create_task(request_cloud_cooking_plan(microwave_id, sensors_data))
|
asyncio.create_task(request_cloud_cooking_plan(microwave_id, sensors_data))
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
print(f"[{microwave_id}] ⚠️ Timeout waiting for MQTT IR data from ESP32.")
|
print(f"[{microwave_id}] Timeout waiting for MQTT IR data from ESP32.")
|
||||||
sensors_data["ir_initial_temp"] = None
|
sensors_data["ir_initial_temp"] = None
|
||||||
sensors_data["ir_ambient_temp"] = None
|
sensors_data["ir_ambient_temp"] = None
|
||||||
finally:
|
finally:
|
||||||
@@ -376,6 +407,8 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
|||||||
return
|
return
|
||||||
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
set_cloud_alert(False) # Clear any previous cloud alert
|
||||||
|
|
||||||
# JSON extraction
|
# JSON extraction
|
||||||
res_json = response.json()
|
res_json = response.json()
|
||||||
@@ -400,12 +433,11 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
|||||||
c_temp = plan.get("target_temp")
|
c_temp = plan.get("target_temp")
|
||||||
|
|
||||||
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].set_state(MicrowaveState.IDLE)
|
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
|
|
||||||
microwave_states[microwave_id].set_state(MicrowaveState.COOKING)
|
microwave_states[microwave_id].set_state(MicrowaveState.COOKING)
|
||||||
|
|
||||||
if config.DEBUG:
|
if config.DEBUG:
|
||||||
@@ -417,6 +449,8 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
|||||||
payloads.mqtt_cooking_config(microwave_id, c_time, c_power, c_temp),
|
payloads.mqtt_cooking_config(microwave_id, c_time, c_power, c_temp),
|
||||||
qos=config.MQTT_QOS
|
qos=config.MQTT_QOS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
microwave_states[microwave_id].set_cooking_start_time(time.time())
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -436,7 +470,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
|
set_cloud_alert(True)
|
||||||
microwave_states[microwave_id].set_state(MicrowaveState.DONE)
|
microwave_states[microwave_id].set_state(MicrowaveState.DONE)
|
||||||
|
|
||||||
# --- MAIN LOGIC TASKS ---
|
# --- MAIN LOGIC TASKS ---
|
||||||
@@ -458,6 +492,7 @@ async def process_messages_task():
|
|||||||
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)
|
microwave_states[mw_id].set_cooking_state(n_state)
|
||||||
|
update_lcd_microwave_count() # Update the LCD with new microwave count
|
||||||
|
|
||||||
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)
|
||||||
@@ -465,7 +500,17 @@ async def process_messages_task():
|
|||||||
if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
|
if n_state == CookingStates.IDLE and microwave_states.get(mw_id).state == MicrowaveState.COOKING:
|
||||||
microwave_states[mw_id].set_state(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.")
|
||||||
|
if "action" in data.get("data", {}):
|
||||||
|
action = data["data"]["action"]
|
||||||
|
if action == LoraCommands.COOKING_UPDATE:
|
||||||
|
mw_id = data["data"].get("id")
|
||||||
|
if mw_id in microwave_states:
|
||||||
|
state_info = data["data"]
|
||||||
|
microwave_states[mw_id].set_cooking_state(state_info.get("cooking_state", microwave_states[mw_id].cooking_state))
|
||||||
|
microwave_states[mw_id].set_cooking_estimated_remaining_time(state_info.get("estimated_remaining_time", microwave_states[mw_id].cooking_estimated_remaining_time))
|
||||||
|
microwave_states[mw_id].set_paused(state_info.get("paused", microwave_states[mw_id].paused))
|
||||||
|
else:
|
||||||
|
print(f"[LoRa] Unhandled action received: {action} with data: {data['data']}")
|
||||||
elif source == "MQTT":
|
elif source == "MQTT":
|
||||||
topic = msg["topic"]
|
topic = msg["topic"]
|
||||||
|
|
||||||
@@ -487,7 +532,8 @@ async def process_messages_task():
|
|||||||
component_id,
|
component_id,
|
||||||
on_change_callback=notify_display_update
|
on_change_callback=notify_display_update
|
||||||
)
|
)
|
||||||
notify_display_update()
|
notify_display_update() # Update the external screens
|
||||||
|
update_lcd_microwave_count() # Update the LCD with new microwave count
|
||||||
|
|
||||||
mqtt_client.publish(
|
mqtt_client.publish(
|
||||||
config.MQTT_TOPIC_HELLO,
|
config.MQTT_TOPIC_HELLO,
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class MicrowaveState:
|
|||||||
|
|
||||||
if self.state != new_state:
|
if self.state != new_state:
|
||||||
self.state = new_state
|
self.state = new_state
|
||||||
self._notify_change()
|
# self._notify_change()
|
||||||
|
|
||||||
def set_paused(self, is_paused):
|
def set_paused(self, is_paused):
|
||||||
if self.paused != is_paused:
|
if self.paused != is_paused:
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
"""
|
||||||
|
Manages the RGB LCD display with the current state of the microwaves and cloud connectivity.
|
||||||
|
|
||||||
|
The display switches content every x seconds between microwaves, external screens,
|
||||||
|
and cloud connectivity state using a background thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
SCREEN_SWITCH_INTERVAL = 5 # seconds
|
||||||
|
DEFAULT_BRIGHTNESS = 0.3 # Constant brightness (range 0.0 to 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
class RGBLCDManager:
|
||||||
|
def __init__(self, rgb_lcd, interval=SCREEN_SWITCH_INTERVAL, brightness=DEFAULT_BRIGHTNESS, auto_start=True):
|
||||||
|
self.rgb_lcd = rgb_lcd
|
||||||
|
self.interval = interval
|
||||||
|
self.brightness = max(0.0, min(1.0, float(brightness)))
|
||||||
|
|
||||||
|
# Internal state store
|
||||||
|
self._microwave_count = "N/A"
|
||||||
|
self._external_screen_count = "N/A"
|
||||||
|
self._cloud_alert = None
|
||||||
|
|
||||||
|
# Screen rotation sequence
|
||||||
|
self.screens = [
|
||||||
|
self.display_microwave_state,
|
||||||
|
self.display_external_screens,
|
||||||
|
self.display_cloud_connectivity,
|
||||||
|
]
|
||||||
|
self.current_screen_idx = 0
|
||||||
|
|
||||||
|
# Threading controls
|
||||||
|
self._thread = None
|
||||||
|
self._running = False
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
if auto_start:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
# --- SETTER METHODS ---
|
||||||
|
|
||||||
|
def set_brightness(self, brightness):
|
||||||
|
"""Dynamically adjust backlight brightness (0.0 to 1.0)."""
|
||||||
|
self.brightness = max(0.0, min(1.0, float(brightness)))
|
||||||
|
|
||||||
|
def set_microwave_count(self, count):
|
||||||
|
"""Set the number of connected microwaves."""
|
||||||
|
self._microwave_count = count
|
||||||
|
|
||||||
|
def set_external_screen_count(self, count):
|
||||||
|
"""Set the number of connected external screens."""
|
||||||
|
self._external_screen_count = count
|
||||||
|
|
||||||
|
def set_cloud_alert(self, status: bool):
|
||||||
|
"""Set cloud connectivity status."""
|
||||||
|
self._cloud_alert = status
|
||||||
|
|
||||||
|
def set_data(self, microwave_count=None, external_screen_count=None, cloud_alert=None):
|
||||||
|
"""Convenience method to update all state variables at once."""
|
||||||
|
if microwave_count is not None:
|
||||||
|
self.set_microwave_count(microwave_count)
|
||||||
|
if external_screen_count is not None:
|
||||||
|
self.set_external_screen_count(external_screen_count)
|
||||||
|
if cloud_alert is not None:
|
||||||
|
self.set_cloud_alert(cloud_alert)
|
||||||
|
|
||||||
|
# --- DISPLAY RENDERERS ---
|
||||||
|
|
||||||
|
def display_microwave_state(self):
|
||||||
|
"""Display the number of connected microwaves."""
|
||||||
|
text = f"Microwaves:\nConnected: {self._microwave_count}"
|
||||||
|
self.rgb_lcd.setText(text)
|
||||||
|
self.rgb_lcd.setRGB(255, 255, 255, brightness=self.brightness)
|
||||||
|
|
||||||
|
def display_external_screens(self):
|
||||||
|
"""Display the number of connected external screens."""
|
||||||
|
text = f"Screens:\nConnected: {self._external_screen_count}"
|
||||||
|
self.rgb_lcd.setText(text)
|
||||||
|
self.rgb_lcd.setRGB(255, 255, 255, brightness=self.brightness)
|
||||||
|
|
||||||
|
def display_cloud_connectivity(self):
|
||||||
|
"""Display cloud connectivity status with brightness-scaled backlight color."""
|
||||||
|
status_str = None
|
||||||
|
|
||||||
|
# Base RGB colors
|
||||||
|
if self._cloud_alert is True:
|
||||||
|
rgb = (255, 0, 0) # Red
|
||||||
|
status_str = "Disconnected"
|
||||||
|
elif self._cloud_alert is False:
|
||||||
|
rgb = (0, 255, 0) # Green
|
||||||
|
status_str = "Connected"
|
||||||
|
else:
|
||||||
|
rgb = (255, 165, 0) # Orange
|
||||||
|
status_str = "N/A"
|
||||||
|
|
||||||
|
self.rgb_lcd.setRGB(rgb[0], rgb[1], rgb[2], brightness=self.brightness)
|
||||||
|
self.rgb_lcd.setText(f"Cloud Status:\n{status_str}")
|
||||||
|
|
||||||
|
# --- BACKGROUND THREAD LOGIC ---
|
||||||
|
|
||||||
|
def _display_loop(self):
|
||||||
|
"""Background loop that switches screens at regular intervals."""
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
try:
|
||||||
|
self.screens[self.current_screen_idx]()
|
||||||
|
self.current_screen_idx = (self.current_screen_idx + 1) % len(self.screens)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[RGBLCDManager] Error updating LCD: {e}")
|
||||||
|
|
||||||
|
sleep_ticks = int(self.interval * 10)
|
||||||
|
for _ in range(sleep_ticks):
|
||||||
|
if self._stop_event.is_set():
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Start the display background thread."""
|
||||||
|
if not self._running:
|
||||||
|
self._running = True
|
||||||
|
self._stop_event.clear()
|
||||||
|
self._thread = threading.Thread(target=self._display_loop, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop the display loop and turn off the LCD backlight."""
|
||||||
|
if self._running:
|
||||||
|
self._stop_event.set()
|
||||||
|
if self._thread and self._thread.is_alive():
|
||||||
|
self._thread.join(timeout=2.0)
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.rgb_lcd.setText("Bye !")
|
||||||
|
self.rgb_lcd.setRGB(0, 0, 0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[RGBLCDManager] Error clearing LCD: {e}")
|
||||||
@@ -5,4 +5,5 @@ import sensors.temp_hum as temp_hum
|
|||||||
import sensors.button as button
|
import sensors.button as button
|
||||||
import sensors.gps as gps
|
import sensors.gps as gps
|
||||||
import sensors.camera as camera
|
import sensors.camera as camera
|
||||||
import sensors.buzzer as buzzer
|
import sensors.buzzer as buzzer
|
||||||
|
import sensors.rgb_lcd as rgb_lcd
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from sensors.lock import grove_lock
|
||||||
|
|
||||||
|
if sys.platform == 'uwp':
|
||||||
|
import winrt_smbus as smbus
|
||||||
|
bus = smbus.SMBus(1)
|
||||||
|
else:
|
||||||
|
import smbus
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
rev = GPIO.RPI_REVISION
|
||||||
|
if rev == 2 or rev == 3:
|
||||||
|
bus = smbus.SMBus(1)
|
||||||
|
else:
|
||||||
|
bus = smbus.SMBus(0)
|
||||||
|
|
||||||
|
# Device I2C addresses
|
||||||
|
DISPLAY_RGB_ADDR = 0x62
|
||||||
|
DISPLAY_TEXT_ADDR = 0x3e
|
||||||
|
|
||||||
|
|
||||||
|
def setRGB(r, g, b, brightness=1.0):
|
||||||
|
"""Set backlight to (R,G,B) with optional brightness level (0.0 to 1.0)."""
|
||||||
|
brightness = max(0.0, min(1.0, float(brightness)))
|
||||||
|
|
||||||
|
r_scaled = int(max(0, min(255, r * brightness)))
|
||||||
|
g_scaled = int(max(0, min(255, g * brightness)))
|
||||||
|
b_scaled = int(max(0, min(255, b * brightness)))
|
||||||
|
|
||||||
|
with grove_lock:
|
||||||
|
try:
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 0, 0)
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 1, 0)
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 0x08, 0xaa)
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 4, r_scaled)
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 3, g_scaled)
|
||||||
|
bus.write_byte_data(DISPLAY_RGB_ADDR, 2, b_scaled)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def textCommand(cmd):
|
||||||
|
"""Send command to display (internal use)."""
|
||||||
|
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x80, cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def setText(text):
|
||||||
|
"""Set display text (\n for second line or auto wrap)."""
|
||||||
|
with grove_lock:
|
||||||
|
try:
|
||||||
|
textCommand(0x01) # Clear display
|
||||||
|
time.sleep(0.05)
|
||||||
|
textCommand(0x08 | 0x04) # Display on, no cursor
|
||||||
|
textCommand(0x28) # 2 lines
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
row = 0
|
||||||
|
for c in text:
|
||||||
|
if c == '\n' or count == 16:
|
||||||
|
count = 0
|
||||||
|
row += 1
|
||||||
|
if row == 2:
|
||||||
|
break
|
||||||
|
textCommand(0xc0)
|
||||||
|
if c == '\n':
|
||||||
|
continue
|
||||||
|
count += 1
|
||||||
|
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c))
|
||||||
|
time.sleep(0.001) # Small pacing delay to prevent LCD buffer overflow
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def setText_norefresh(text):
|
||||||
|
"""Update display text without full screen erase."""
|
||||||
|
with grove_lock:
|
||||||
|
try:
|
||||||
|
textCommand(0x02) # Return home
|
||||||
|
time.sleep(0.05)
|
||||||
|
textCommand(0x08 | 0x04) # Display on, no cursor
|
||||||
|
textCommand(0x28) # 2 lines
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
row = 0
|
||||||
|
while len(text) < 32: # Clear rest of screen space
|
||||||
|
text += ' '
|
||||||
|
for c in text:
|
||||||
|
if c == '\n' or count == 16:
|
||||||
|
count = 0
|
||||||
|
row += 1
|
||||||
|
if row == 2:
|
||||||
|
break
|
||||||
|
textCommand(0xc0)
|
||||||
|
if c == '\n':
|
||||||
|
continue
|
||||||
|
count += 1
|
||||||
|
bus.write_byte_data(DISPLAY_TEXT_ADDR, 0x40, ord(c))
|
||||||
|
time.sleep(0.001) # Small pacing delay to prevent LCD buffer overflow
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
+2
-2
@@ -9,11 +9,11 @@ import shared.safeQueue as safeQueue
|
|||||||
import shared.alerts as alerts
|
import shared.alerts as alerts
|
||||||
try:
|
try:
|
||||||
import shared.lora_device as lora_device
|
import shared.lora_device as lora_device
|
||||||
except ImportError:
|
except (ImportError, SyntaxError):
|
||||||
pass # No need
|
pass # No need
|
||||||
try:
|
try:
|
||||||
import shared.uart_comm as uart_comm
|
import shared.uart_comm as uart_comm
|
||||||
except ImportError:
|
except (ImportError, SyntaxError):
|
||||||
pass # No need as we are on the RPI
|
pass # No need as we are on the RPI
|
||||||
import shared.sensors
|
import shared.sensors
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ def get_lora_device(port_or_pins=None):
|
|||||||
class LoraCommands:
|
class LoraCommands:
|
||||||
PING = "ping"
|
PING = "ping"
|
||||||
COOKING_STATE_UPDATE = "cooking_state_update"
|
COOKING_STATE_UPDATE = "cooking_state_update"
|
||||||
|
COOKING_UPDATE = "cooking_update"
|
||||||
TOGGLE_PAUSE = "toggle_pause"
|
TOGGLE_PAUSE = "toggle_pause"
|
||||||
TOGGLE_DEFROST = "toggle_defrost"
|
TOGGLE_DEFROST = "toggle_defrost"
|
||||||
NEW_ALERT = "new_alert"
|
NEW_ALERT = "new_alert"
|
||||||
Reference in New Issue
Block a user