""" Contents server — serves alarm audio and status images over WebSocket. Streams WAV PCM chunks and pushes 1-bit monochrome status images to the connected ESP32 dashboard client on port 8766. Protocol: Status image: 1. Text frame: {"type":"status_image","width":120,"height":120} 2. Binary frame: 1-bit monochrome bitmap Alarm audio: 1. Text frame: {"type":"alarm_start","sample_rate":N,"channels":N,"bits":N} 2. Binary frames: raw PCM chunks (4096 bytes each, paced at ~90% real-time) 3. Text frame: {"type":"alarm_stop"} """ import asyncio import logging from random import randint import websockets from audio_handler import find_wav, read_wav, stream_alarm from image_handler import IMG_DIR, load_status_image, send_status_image logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("contents_server") PORT = 8766 async def handler(ws): """Handle a single WebSocket connection.""" remote = ws.remote_address log.info("Client connected: %s:%d", remote[0], remote[1]) wav_path = find_wav() pcm, sr, ch, bits = read_wav(wav_path) # Load status images img_idle = load_status_image(IMG_DIR / "idle.png") img_alarm = load_status_image(IMG_DIR / "on_alarm.png") try: # Send idle image on connect await send_status_image(ws, img_idle) while True: delay = randint(30, 60) log.info("Next alarm in %ds", delay) await asyncio.sleep(delay) # Switch to alarm image before audio await send_status_image(ws, img_alarm) await stream_alarm(ws, pcm, sr, ch, bits) # Switch back to idle after alarm await send_status_image(ws, img_idle) except websockets.exceptions.ConnectionClosed: log.info("Client disconnected: %s:%d", remote[0], remote[1]) async def main(): log.info("Contents server starting on port %d", PORT) async with websockets.serve(handler, "0.0.0.0", PORT): await asyncio.Future() # run forever if __name__ == "__main__": asyncio.run(main())