feat(07-03): implement frame mode in firmware/main.py
- Import parse_drgb, parse_drgbw, parse_dnrgb, FrameAssembler - Wire DRGB/DRGBW/DNRGB parsing in udp_listener_task - Add frame_is_rgbw flag to route to write_rgb or write_rgbw - Add 2s frame timeout in render_task — reverts to last animation - Initialize FrameAssembler with led_count in main()
This commit is contained in:
@@ -1,10 +1,26 @@
|
|||||||
|
"""LightSync firmware — main entry point.
|
||||||
|
|
||||||
|
Two asyncio tasks:
|
||||||
|
1. udp_listener_task: non-blocking UDP socket, parses packets, updates state
|
||||||
|
2. render_task: 60fps loop, renders animations or writes frame data to strip
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
- 'idle': strip is off, waiting for first packet
|
||||||
|
- 'animation': running a locally-rendered animation (from 0xAC command)
|
||||||
|
- 'frame': displaying raw pixel data (from DRGB/DRGBW/DNRGB packets)
|
||||||
|
|
||||||
|
Frame timeout: if no frame packet arrives for 2 seconds, reverts to last animation.
|
||||||
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import socket
|
import socket
|
||||||
import select
|
import select
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from protocol import classify_packet, parse_animation_cmd
|
from protocol import (
|
||||||
|
classify_packet, parse_animation_cmd,
|
||||||
|
parse_drgb, parse_drgbw, parse_dnrgb, FrameAssembler
|
||||||
|
)
|
||||||
from led_driver import create_strip
|
from led_driver import create_strip
|
||||||
from animations import create_animation
|
from animations import create_animation
|
||||||
|
|
||||||
@@ -15,7 +31,11 @@ current_mode = 'idle' # 'idle', 'animation', 'frame'
|
|||||||
current_animation = None # dict from parse_animation_cmd
|
current_animation = None # dict from parse_animation_cmd
|
||||||
current_anim_instance = None # animation object with render()
|
current_anim_instance = None # animation object with render()
|
||||||
animation_start_ms = 0
|
animation_start_ms = 0
|
||||||
frame_pixels = None # set by frame packets
|
frame_pixels = None # list of (R,G,B) or (R,G,B,W) tuples
|
||||||
|
frame_is_rgbw = False # True if last frame was DRGBW
|
||||||
|
last_frame_ms = 0 # ticks_ms of last frame packet
|
||||||
|
frame_assembler = None # FrameAssembler for DNRGB
|
||||||
|
FRAME_TIMEOUT_MS = 2000 # revert to animation after 2s without frames
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
@@ -30,7 +50,8 @@ def load_config():
|
|||||||
async def udp_listener_task(port):
|
async def udp_listener_task(port):
|
||||||
"""Non-blocking UDP listener using select.poll(0) inside asyncio."""
|
"""Non-blocking UDP listener using select.poll(0) inside asyncio."""
|
||||||
global current_mode, current_animation, current_anim_instance
|
global current_mode, current_animation, current_anim_instance
|
||||||
global animation_start_ms, frame_pixels
|
global animation_start_ms, frame_pixels, frame_is_rgbw
|
||||||
|
global last_frame_ms, frame_assembler
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.bind(('0.0.0.0', port))
|
sock.bind(('0.0.0.0', port))
|
||||||
@@ -56,10 +77,29 @@ async def udp_listener_task(port):
|
|||||||
animation_start_ms = time.ticks_ms()
|
animation_start_ms = time.ticks_ms()
|
||||||
print('[udp] Animation:', cmd['animation'])
|
print('[udp] Animation:', cmd['animation'])
|
||||||
|
|
||||||
elif ptype in ('drgb', 'drgbw', 'dnrgb'):
|
elif ptype == 'drgb':
|
||||||
# Frame handling added by Plan 07-03
|
pixels = parse_drgb(data)
|
||||||
|
frame_pixels = pixels
|
||||||
|
frame_is_rgbw = False
|
||||||
current_mode = 'frame'
|
current_mode = 'frame'
|
||||||
print('[udp] Frame packet:', ptype, len(data), 'bytes')
|
last_frame_ms = time.ticks_ms()
|
||||||
|
|
||||||
|
elif ptype == 'drgbw':
|
||||||
|
pixels = parse_drgbw(data)
|
||||||
|
frame_pixels = pixels
|
||||||
|
frame_is_rgbw = True
|
||||||
|
current_mode = 'frame'
|
||||||
|
last_frame_ms = time.ticks_ms()
|
||||||
|
|
||||||
|
elif ptype == 'dnrgb':
|
||||||
|
start_idx, pixels = parse_dnrgb(data)
|
||||||
|
now = time.ticks_ms()
|
||||||
|
complete = frame_assembler.feed(start_idx, pixels, now)
|
||||||
|
if complete:
|
||||||
|
frame_pixels = complete
|
||||||
|
frame_is_rgbw = False # DNRGB is always RGB
|
||||||
|
current_mode = 'frame'
|
||||||
|
last_frame_ms = now
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print('[udp] Unknown packet type:', data[0] if data else '?')
|
print('[udp] Unknown packet type:', data[0] if data else '?')
|
||||||
@@ -70,30 +110,46 @@ async def udp_listener_task(port):
|
|||||||
|
|
||||||
|
|
||||||
async def render_task():
|
async def render_task():
|
||||||
"""Animation render loop at ~60fps."""
|
"""Animation/frame render loop at ~60fps."""
|
||||||
global current_mode, frame_pixels
|
global current_mode, frame_pixels
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if current_mode == 'animation' and current_anim_instance:
|
if current_mode == 'frame' and frame_pixels:
|
||||||
|
# Check frame timeout — revert to animation if no packets for 2s
|
||||||
|
if time.ticks_diff(time.ticks_ms(), last_frame_ms) > FRAME_TIMEOUT_MS:
|
||||||
|
if current_anim_instance:
|
||||||
|
current_mode = 'animation'
|
||||||
|
animation_start_ms = time.ticks_ms()
|
||||||
|
print('[render] Frame timeout, reverting to animation')
|
||||||
|
else:
|
||||||
|
current_mode = 'idle'
|
||||||
|
strip.clear()
|
||||||
|
else:
|
||||||
|
# Write frame to strip
|
||||||
|
if frame_is_rgbw:
|
||||||
|
strip.write_rgbw(frame_pixels)
|
||||||
|
else:
|
||||||
|
strip.write_rgb(frame_pixels)
|
||||||
|
|
||||||
|
elif current_mode == 'animation' and current_anim_instance:
|
||||||
t = time.ticks_diff(time.ticks_ms(), animation_start_ms) / 1000.0
|
t = time.ticks_diff(time.ticks_ms(), animation_start_ms) / 1000.0
|
||||||
pixels = current_anim_instance.render(t, config['led_count'])
|
pixels = current_anim_instance.render(t, config['led_count'])
|
||||||
strip.write_rgb(pixels)
|
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
|
await asyncio.sleep_ms(16) # ~60 fps
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
global config, strip
|
global config, strip, frame_assembler
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
strip = create_strip(config)
|
strip = create_strip(config)
|
||||||
strip.clear()
|
strip.clear()
|
||||||
|
frame_assembler = FrameAssembler(config['led_count'])
|
||||||
|
|
||||||
print('[main] LightSync firmware ready')
|
print('[main] LightSync firmware ready')
|
||||||
print('[main] Strip:', config['strip_type'], '/', config['led_count'], 'LEDs')
|
print('[main] Strip:', config['strip_type'], '/', config['led_count'], 'LEDs')
|
||||||
|
print('[main] UDP port:', config['udp_port'])
|
||||||
|
|
||||||
asyncio.create_task(udp_listener_task(config['udp_port']))
|
asyncio.create_task(udp_listener_task(config['udp_port']))
|
||||||
asyncio.create_task(render_task())
|
asyncio.create_task(render_task())
|
||||||
@@ -101,5 +157,4 @@ async def main():
|
|||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user