From 8e521e5bfbed0f470387bd4ef44b826d63c58b7f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 15:40:43 +0000 Subject: [PATCH] feat(07-02): wire animation rendering into firmware/main.py - Add import for create_animation from animations module - Add current_anim_instance global for animation object - Create animation instance in udp_listener_task on anim_cmd receipt - Implement render loop in render_task using ticks_diff for timing --- firmware/main.py | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 firmware/main.py diff --git a/firmware/main.py b/firmware/main.py new file mode 100644 index 0000000..6e7d0fb --- /dev/null +++ b/firmware/main.py @@ -0,0 +1,105 @@ +import asyncio +import socket +import select +import time +import json + +from protocol import classify_packet, parse_animation_cmd +from led_driver import create_strip +from animations import create_animation + +# --- Global state --- +config = None +strip = None +current_mode = 'idle' # 'idle', 'animation', 'frame' +current_animation = None # dict from parse_animation_cmd +current_anim_instance = None # animation object with render() +animation_start_ms = 0 +frame_pixels = None # set by frame packets + + +def load_config(): + try: + with open('config.json') as f: + return json.load(f) + except OSError: + return {'led_count': 300, 'strip_type': 'sk6812', 'led_pin': 5, + 'spi_id': 1, 'udp_port': 21324} + + +async def udp_listener_task(port): + """Non-blocking UDP listener using select.poll(0) inside asyncio.""" + global current_mode, current_animation, current_anim_instance + global animation_start_ms, frame_pixels + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('0.0.0.0', port)) + sock.setblocking(False) + poller = select.poll() + poller.register(sock, select.POLLIN) + + print('[udp] Listening on port', port) + + while True: + events = poller.poll(0) + if events: + try: + data, addr = sock.recvfrom(1500) + ptype = classify_packet(data) + + if ptype == 'anim_cmd': + cmd = parse_animation_cmd(data) + if cmd: + current_animation = cmd + current_anim_instance = create_animation(cmd) + current_mode = 'animation' + animation_start_ms = time.ticks_ms() + print('[udp] Animation:', cmd['animation']) + + elif ptype in ('drgb', 'drgbw', 'dnrgb'): + # Frame handling added by Plan 07-03 + current_mode = 'frame' + print('[udp] Frame packet:', ptype, len(data), 'bytes') + + else: + print('[udp] Unknown packet type:', data[0] if data else '?') + except OSError: + pass + + await asyncio.sleep_ms(5) + + +async def render_task(): + """Animation render loop at ~60fps.""" + global current_mode, frame_pixels + + while True: + if current_mode == 'animation' and current_anim_instance: + t = time.ticks_diff(time.ticks_ms(), animation_start_ms) / 1000.0 + pixels = current_anim_instance.render(t, config['led_count']) + strip.write_rgb(pixels) + elif current_mode == 'frame' and frame_pixels: + # Plan 07-03 adds frame write logic here + pass + + await asyncio.sleep_ms(16) # ~60 fps + + +async def main(): + global config, strip + + config = load_config() + strip = create_strip(config) + strip.clear() + + print('[main] LightSync firmware ready') + print('[main] Strip:', config['strip_type'], '/', config['led_count'], 'LEDs') + + asyncio.create_task(udp_listener_task(config['udp_port'])) + asyncio.create_task(render_task()) + + while True: + await asyncio.sleep(1) + + +asyncio.run(main())