Sending updates to external screen + RGB LCD
This commit is contained in:
@@ -9,7 +9,7 @@ services:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800
|
||||
- OT_INFRA_IF=wlan0
|
||||
- OT_INFRA_IF=wlx6815790f3204
|
||||
- OT_THREAD_IF=wpan0
|
||||
- OT_LOG_LEVEL=6
|
||||
- FIREWALL=0
|
||||
|
||||
@@ -14,6 +14,7 @@ class DisplayManager:
|
||||
self.screens: Dict[str, str] = {} # Map: service_name -> URL
|
||||
self.aiozc: AsyncZeroconf | None = None
|
||||
self.browser: AsyncServiceBrowser | None = None
|
||||
self._on_screens_change_callback = None # Callback for screen changes
|
||||
|
||||
async def start(self):
|
||||
"""Starts dynamic mDNS discovery for e-Paper screens."""
|
||||
@@ -48,12 +49,20 @@ class DisplayManager:
|
||||
url = f"http://{ip_str}:{port}{self.endpoint_path}"
|
||||
|
||||
self.screens[name] = url
|
||||
if self._on_screens_change_callback:
|
||||
self._on_screens_change_callback(self.screens)
|
||||
print(f"[DisplayManager] 📺 Screen registered: {name} -> {url}")
|
||||
|
||||
elif state_change == ServiceStateChange.Removed:
|
||||
if name in self.screens:
|
||||
print(f"[DisplayManager] ❌ Screen disconnected: {name}")
|
||||
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):
|
||||
"""Broadcasts the system state JSON to all discovered screens concurrently."""
|
||||
@@ -113,9 +122,9 @@ class DisplayManager:
|
||||
timeout=3.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"[DisplayManager] ✅ Updated {name}")
|
||||
print(f"[DisplayManager] Updated {name}")
|
||||
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):
|
||||
"""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.lora_device import LoraCommands
|
||||
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 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 ---
|
||||
DISPLAY_DEBOUNCE_SECONDS = 3 # Seconds to wait after first change before broadcasting
|
||||
@@ -69,6 +73,7 @@ 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()
|
||||
@@ -79,18 +84,21 @@ def notify_display_update():
|
||||
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()
|
||||
update_lcd_cloud_alert()
|
||||
|
||||
# Global state trackers
|
||||
microwave_states = {"2": MicrowaveState("2", on_change_callback=notify_display_update)}
|
||||
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
|
||||
|
||||
# Display Manager instance
|
||||
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
|
||||
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_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"):
|
||||
mqtt_client._client.loop_start()
|
||||
print("[MQTT Local] Paho background loop started.")
|
||||
@@ -192,6 +204,23 @@ async def cloud_mqtt_listener_task():
|
||||
"data": payload
|
||||
})
|
||||
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():
|
||||
"""
|
||||
@@ -199,6 +228,8 @@ async def display_broadcast_worker_task():
|
||||
Waits until DISPLAY_DEBOUNCE_SECONDS have passed since the first queued update
|
||||
before sending state to all connected screens.
|
||||
"""
|
||||
global display_update_queue, microwave_states, cloud_alert
|
||||
|
||||
print("[Display Worker] Started debounced display broadcast worker.")
|
||||
while True:
|
||||
# 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
|
||||
asyncio.create_task(request_cloud_cooking_plan(microwave_id, sensors_data))
|
||||
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_ambient_temp"] = None
|
||||
finally:
|
||||
@@ -376,6 +407,8 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
||||
return
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
set_cloud_alert(False) # Clear any previous cloud alert
|
||||
|
||||
# JSON extraction
|
||||
res_json = response.json()
|
||||
@@ -400,12 +433,11 @@ async def request_cloud_cooking_plan(microwave_id, sensors_data):
|
||||
c_temp = plan.get("target_temp")
|
||||
|
||||
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)
|
||||
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].set_state(MicrowaveState.COOKING)
|
||||
|
||||
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),
|
||||
qos=config.MQTT_QOS
|
||||
)
|
||||
|
||||
microwave_states[microwave_id].set_cooking_start_time(time.time())
|
||||
return
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
# --- MAIN LOGIC TASKS ---
|
||||
@@ -458,6 +492,7 @@ async def process_messages_task():
|
||||
print(f"[LoRa] Microwave {mw_id} state changed to: {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):
|
||||
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:
|
||||
microwave_states[mw_id].set_state(MicrowaveState.DONE)
|
||||
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":
|
||||
topic = msg["topic"]
|
||||
|
||||
@@ -487,7 +532,8 @@ async def process_messages_task():
|
||||
component_id,
|
||||
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(
|
||||
config.MQTT_TOPIC_HELLO,
|
||||
|
||||
@@ -34,7 +34,7 @@ class MicrowaveState:
|
||||
|
||||
if self.state != new_state:
|
||||
self.state = new_state
|
||||
self._notify_change()
|
||||
# self._notify_change()
|
||||
|
||||
def set_paused(self, 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.gps as gps
|
||||
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
|
||||
Reference in New Issue
Block a user