fix: pack frame at render time, send without POLLOUT dependency
Validate and Test / validate-patches (push) Failing after 13s

- pending_bytes stores already-packed msgpack, sendable immediately
- Try sendall() directly every iteration — no POLLOUT gate
- Refresh path resends last_sent_params (not raw frame tuple)
- Router socket uses settimeout instead of setblocking for simpler I/O
This commit is contained in:
2026-06-24 03:56:50 -07:00
parent 68d38de9a5
commit a94ac07a5c
+30 -52
View File
@@ -12,15 +12,13 @@ ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
COLS = 13 COLS = 13
ROWS = 8 ROWS = 8
REFRESH_INTERVAL = 0.2 # 5 FPS background refresh (catches dropped frames) REFRESH_INTERVAL = 0.2
STATS_FILE = "/tmp/viz-stats.json" STATS_FILE = "/tmp/viz-stats.json"
_poll = select.poll() _poll = select.poll()
POLLIN = select.POLLIN POLLIN = select.POLLIN
POLLOUT = select.POLLOUT
SOCK_BUFFER = 4096 SOCK_BUFFER = 4096
ROUTER_RECONNECT_DELAY = 0.1
def pack_frame(pixels): def pack_frame(pixels):
@@ -29,8 +27,11 @@ def pack_frame(pixels):
for col in range(COLS): for col in range(COLS):
if pixels[row][col]: if pixels[row][col]:
bits |= 1 << (row * COLS + col) bits |= 1 << (row * COLS + col)
return (bits & 0xFFFFFFFF, (bits >> 32) & 0xFFFFFFFF, w0 = bits & 0xFFFFFFFF
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF) w1 = (bits >> 32) & 0xFFFFFFFF
w2 = (bits >> 64) & 0xFFFFFFFF
w3 = (bits >> 96) & 0xFFFFFFFF
return [str(w0), str(w1), str(w2), str(w3)]
def render(active_notes): def render(active_notes):
@@ -46,7 +47,7 @@ def render(active_notes):
def connect_router(): def connect_router():
router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) router = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
router.setblocking(False) router.settimeout(0.3)
try: try:
router.connect(ROUTER_SOCK) router.connect(ROUTER_SOCK)
except OSError: except OSError:
@@ -54,19 +55,6 @@ def connect_router():
return router return router
def send_frame(router, packer, msg_id, frame):
if frame is None:
return msg_id
msg_id = (msg_id + 1) % 65535
params = [str(v) for v in frame]
req = packer.pack([0, msg_id, "draw_frame", params])
try:
router.sendall(req)
except (BlockingIOError, OSError):
pass
return msg_id
def main(): def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -77,18 +65,15 @@ def main():
packer = msgpack.Packer() packer = msgpack.Packer()
_poll.register(sock, POLLIN) _poll.register(sock, POLLIN)
router_fd = router.fileno()
_poll.register(router, POLLIN | POLLOUT)
active_notes = set() active_notes = set()
last_sent_frame = None last_sent_params = None
pending_frame = None pending_bytes = None
buf = b"" buf = b""
msg_id = 0 msg_id = 0
frames_sent = 0 frames_sent = 0
frames_dropped = 0 frames_dropped = 0
router_errors = 0
last_refresh = time.monotonic() last_refresh = time.monotonic()
stats_last_write = time.monotonic() stats_last_write = time.monotonic()
@@ -100,14 +85,11 @@ def main():
now = time.monotonic() now = time.monotonic()
refresh_due = (now - last_refresh) >= REFRESH_INTERVAL refresh_due = (now - last_refresh) >= REFRESH_INTERVAL
timeout = 5 # ms
try: try:
events = _poll.poll(timeout) events = _poll.poll(5)
except OSError: except OSError:
events = [] events = []
# Process UDP
for fd, flags in events: for fd, flags in events:
if fd == sock.fileno() and (flags & POLLIN): if fd == sock.fileno() and (flags & POLLIN):
try: try:
@@ -137,38 +119,34 @@ def main():
changed = True changed = True
if changed: if changed:
events_processed += 1 events_processed += 1
frame = render(active_notes) params = render(active_notes)
if frame != last_sent_frame: if params != last_sent_params:
pending_frame = frame msg_id = (msg_id + 1) % 65535
pending_bytes = packer.pack([0, msg_id, "draw_frame", params])
last_sent_params = params
# Process router readable # Try to send pending frame — no POLLOUT dependency
if fd == router_fd and (flags & POLLIN): if pending_bytes is not None:
try: try:
while True: router.sendall(pending_bytes)
d = router.recv(SOCK_BUFFER) pending_bytes = None
if not d:
break
except BlockingIOError:
pass
# Router writable: send pending frame immediately
if pending_frame is not None:
if any(fd == router_fd and (flags & POLLOUT) for fd, flags in events):
msg_id = send_frame(router, packer, msg_id, pending_frame)
last_sent_frame = pending_frame
pending_frame = None
frames_sent += 1 frames_sent += 1
else: except (BlockingIOError, OSError):
frames_dropped += 1 frames_dropped += 1
# Background refresh: resend last frame to recover from router drops # Background refresh: resend last params to recover from router drops
if refresh_due: if refresh_due:
last_refresh = now last_refresh = now
fps_sample_frames += 1 fps_sample_frames += 1
if last_sent_frame is not None: if last_sent_params is not None and pending_bytes is None:
msg_id = send_frame(router, packer, msg_id, last_sent_frame) msg_id = (msg_id + 1) % 65535
refresh_bytes = packer.pack([0, msg_id, "draw_frame", last_sent_params])
try:
router.sendall(refresh_bytes)
frames_sent += 1
except (BlockingIOError, OSError):
pass
# Write stats every 2s
if now - stats_last_write >= 2.0: if now - stats_last_write >= 2.0:
fps = fps_sample_frames / (now - fps_sample_start) if (now - fps_sample_start) > 0 else 0 fps = fps_sample_frames / (now - fps_sample_start) if (now - fps_sample_start) > 0 else 0
_write_stats(frames_sent, frames_dropped, fps, len(active_notes), events_processed) _write_stats(frames_sent, frames_dropped, fps, len(active_notes), events_processed)