Sending updates to external screen + RGB LCD

This commit is contained in:
2026-08-16 14:23:18 +02:00
parent 2276c1742a
commit fd00c9a89d
11 changed files with 367 additions and 24 deletions
+1 -1
View File
@@ -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;
+54 -8
View File
@@ -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)
+1 -1
View File
@@ -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
+11 -2
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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:
+138
View File
@@ -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}")
+2 -1
View File
@@ -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
+102
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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"