External Screen working
This commit is contained in:
@@ -1,26 +1,32 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
otbr:
|
||||
image: openthread/otbr:latest
|
||||
image: openthread/border-router:latest
|
||||
container_name: otbr
|
||||
restart: unless-stopped
|
||||
privileged: true
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
- OT_RCP_DEVICE=spinel+hdlc+uart:///dev/ttyUSB1?uart-baudrate=460800
|
||||
- OT_INFRA_IF=wlan0
|
||||
- OT_THREAD_IF=wpan0
|
||||
- OT_LOG_LEVEL=6
|
||||
- FIREWALL=0
|
||||
- NAT64=1
|
||||
devices:
|
||||
# Change /dev/ttyACM0 if your ESP32-H2 RCP enumerates as /dev/ttyUSB0
|
||||
- /dev/ttyACM0:/dev/ttyACM0
|
||||
command: --radio-url "spinel+hdlc+uart:///dev/ttyACM0?uart-baudrate=460800"
|
||||
- /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_ba78c1181566ee11b42a8d6293cd958c-if00-port0:/dev/ttyUSB1
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
volumes:
|
||||
- otbr-data:/data
|
||||
|
||||
mqtt-broker:
|
||||
image: eclipse-mosquitto:2.0
|
||||
container_name: mqtt-broker
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
environment:
|
||||
MQTT_TLS_ENABLED: ${MQTT_TLS_ENABLED:-true}
|
||||
ports:
|
||||
# Listens on all interfaces (IPv4 and IPv6) for Thread compatibility
|
||||
- "8884:8884"
|
||||
volumes:
|
||||
- ./mqtt/mosquitto-tls.conf:/mosquitto/config/mosquitto-tls.conf:ro
|
||||
- ./mqtt/mosquitto-plain.conf:/mosquitto/config/mosquitto-plain.conf:ro
|
||||
@@ -31,5 +37,6 @@ services:
|
||||
command: ["/bin/sh", "/scripts/start-broker.sh"]
|
||||
|
||||
volumes:
|
||||
otbr-data:
|
||||
mqtt-data:
|
||||
mqtt-log:
|
||||
@@ -0,0 +1,101 @@
|
||||
import asyncio
|
||||
import time
|
||||
import requests
|
||||
import ipaddress
|
||||
from typing import Dict
|
||||
from zeroconf import ServiceStateChange
|
||||
from zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser, AsyncServiceInfo
|
||||
|
||||
SERVICE_TYPE = "_displaytcp._tcp.local."
|
||||
|
||||
class DisplayManager:
|
||||
def __init__(self, endpoint_path: str = "/api/display"):
|
||||
self.endpoint_path = endpoint_path
|
||||
self.screens: Dict[str, str] = {} # Map: service_name -> URL
|
||||
self.aiozc: AsyncZeroconf | None = None
|
||||
self.browser: AsyncServiceBrowser | None = None
|
||||
|
||||
async def start(self):
|
||||
"""Starts dynamic mDNS discovery for e-Paper screens."""
|
||||
self.aiozc = AsyncZeroconf()
|
||||
self.browser = AsyncServiceBrowser(
|
||||
self.aiozc.zeroconf,
|
||||
SERVICE_TYPE,
|
||||
handlers=[self._on_service_state_change]
|
||||
)
|
||||
print(f"[DisplayManager] 🔍 Listening for mDNS screens ({SERVICE_TYPE})...")
|
||||
|
||||
def _on_service_state_change(self, zeroconf, service_type, name, state_change):
|
||||
asyncio.create_task(self._update_service(name, state_change))
|
||||
|
||||
async def _update_service(self, name: str, state_change: ServiceStateChange):
|
||||
if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
|
||||
info = AsyncServiceInfo(SERVICE_TYPE, name)
|
||||
if await info.async_request(self.aiozc.zeroconf, 3000):
|
||||
addresses = info.parsed_addresses()
|
||||
if addresses:
|
||||
# Prefer IPv4 if available, fallback to IPv6 (Thread)
|
||||
ipv4_addrs = [a for a in addresses if ":" not in a]
|
||||
|
||||
if ipv4_addrs:
|
||||
ip_str = ipv4_addrs[0]
|
||||
else:
|
||||
raw_ip = addresses[0]
|
||||
# Wrap IPv6 addresses in square brackets for valid HTTP URLs
|
||||
ip_str = f"[{raw_ip}]" if not raw_ip.startswith("[") else raw_ip
|
||||
|
||||
port = info.port or 5000
|
||||
url = f"http://{ip_str}:{port}{self.endpoint_path}"
|
||||
|
||||
self.screens[name] = url
|
||||
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)
|
||||
|
||||
async def broadcast_state(self, device_id: str, microwave_states: dict, button_state: bool, cloud_alert: bool):
|
||||
"""Broadcasts the system state JSON to all discovered screens concurrently."""
|
||||
if not self.screens:
|
||||
return
|
||||
|
||||
payload = {
|
||||
"device_id": device_id,
|
||||
"timestamp": int(time.time()),
|
||||
"cloud_alert": cloud_alert,
|
||||
"defrost_mode": button_state,
|
||||
"microwaves": [
|
||||
{"id": mw_id, "state": state}
|
||||
for mw_id, state in microwave_states.items()
|
||||
]
|
||||
}
|
||||
|
||||
# Dispatch POST requests in parallel without blocking the main loop
|
||||
tasks = [
|
||||
self._post_to_screen(name, url, payload)
|
||||
for name, url in list(self.screens.items())
|
||||
]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def _post_to_screen(self, name: str, url: str, payload: dict):
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
requests.post,
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=3.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(f"[DisplayManager] ✅ Updated {name}")
|
||||
except Exception as e:
|
||||
print(f"[DisplayManager] ⚠️ Error pushing state to {name}: {e}")
|
||||
|
||||
async def stop(self):
|
||||
"""Clean up mDNS browser and Zeroconf instance."""
|
||||
if self.browser:
|
||||
await self.browser.async_cancel()
|
||||
if self.aiozc:
|
||||
await self.aiozc.async_close()
|
||||
print("[DisplayManager] Stopped service browser.")
|
||||
+25
-1
@@ -12,6 +12,7 @@ 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
|
||||
|
||||
# --- DB SETUP ---
|
||||
DB_PATH = "orchestrateur/db.sqlite"
|
||||
@@ -73,6 +74,9 @@ button_state = False
|
||||
cloud_alert = False # Global status flag for screen / UI display
|
||||
async_event_queue = None
|
||||
|
||||
# Display Manager instance
|
||||
display_manager = DisplayManager(endpoint_path="/api/display")
|
||||
|
||||
# Async synchronization trackers for MQTT IR sensors responses
|
||||
ir_data_cache = {} # mw_id -> dict of IR readings
|
||||
ir_data_events = {} # mw_id -> asyncio.Event()
|
||||
@@ -174,6 +178,21 @@ 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
|
||||
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)
|
||||
|
||||
def button_callback():
|
||||
"""Button physical interrupt callback."""
|
||||
global button_state
|
||||
@@ -587,6 +606,9 @@ async def main():
|
||||
# Initialize SQLite database table
|
||||
init_db()
|
||||
|
||||
# Start mDNS Display discovery
|
||||
await display_manager.start()
|
||||
|
||||
async_event_queue = asyncio.Queue()
|
||||
|
||||
await asyncio.gather(
|
||||
@@ -594,7 +616,8 @@ async def main():
|
||||
mqtt_listener_task(),
|
||||
cloud_mqtt_listener_task(),
|
||||
process_messages_task(),
|
||||
monitor_dish_height_task()
|
||||
monitor_dish_height_task(),
|
||||
display_broadcast_task()
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -603,6 +626,7 @@ if __name__ == "__main__":
|
||||
except KeyboardInterrupt:
|
||||
print("\nArrêt manuel.")
|
||||
finally:
|
||||
asyncio.run(display_manager.stop())
|
||||
if hasattr(mqtt_client._client, "loop_stop"):
|
||||
mqtt_client._client.loop_stop()
|
||||
if hasattr(cloud_mqtt_client._client, "loop_stop"):
|
||||
|
||||
@@ -8,5 +8,11 @@ log_type notice
|
||||
log_type information
|
||||
allow_anonymous true
|
||||
|
||||
listener 8884 ::
|
||||
protocol mqtt
|
||||
listener 8884
|
||||
protocol mqtt
|
||||
|
||||
# Unencrypted listener for local Thread nodes
|
||||
listener 1883
|
||||
protocol mqtt
|
||||
socket_domain ipv6
|
||||
allow_anonymous true
|
||||
@@ -8,10 +8,17 @@ log_type notice
|
||||
log_type information
|
||||
allow_anonymous true
|
||||
|
||||
listener 8884 ::
|
||||
listener 8884
|
||||
protocol mqtt
|
||||
cafile /mosquitto/certs/ca.crt
|
||||
certfile /mosquitto/certs/server.crt
|
||||
keyfile /mosquitto/certs/server.key
|
||||
require_certificate false
|
||||
tls_version tlsv1.2
|
||||
tls_version tlsv1.2
|
||||
|
||||
# Unencrypted listener for local Thread nodes
|
||||
# Thread is already encrypted, so we don't need to use TLS for this listener
|
||||
listener 1883 ::
|
||||
protocol mqtt
|
||||
socket_domain ipv6
|
||||
allow_anonymous true
|
||||
@@ -3,4 +3,5 @@ pyserial>=3.5,<4
|
||||
# picamera2>=0.3.36,<4 # → Installed with apt install python3-picamera2
|
||||
# OpenCV
|
||||
# sudo apt install -y python3-opencv
|
||||
# sudo apt install -y opencv-data
|
||||
# sudo apt install -y opencv-data
|
||||
zeroconf>=0.131.0
|
||||
Reference in New Issue
Block a user