viz: proper POLLOUT queue to prevent frame drops
- FrameQueue (max 5) buffers frames when router is slow - POLLOUT drains pending_frames queue - No silent drops — full queue increments counter - Router always registered POLLIN| POLLOUT - Frame interval 30ms (33 FPS)
This commit is contained in:
+91
-38
@@ -1,18 +1,29 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Listen for MIDI events via UDP and display note activity on LED matrix.
|
"""Listen for MIDI events via UDP and display note activity on LED matrix.
|
||||||
|
|
||||||
Non-blocking I/O with immediate MIDI processing and fast frame rendering.
|
Non-blocking I/O with proper POLLOUT queue, instrumentation, and fast rendering.
|
||||||
"""
|
"""
|
||||||
import socket, msgpack, time, os, select, collections
|
import socket, msgpack, time, os, select, collections
|
||||||
|
|
||||||
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
VIZ_PORT = int(os.environ.get("VIZ_PORT", "8082"))
|
||||||
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
|
ROUTER_SOCK = os.environ.get("ROUTER_SOCK", "/var/run/arduino-router.sock")
|
||||||
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
|
DECAY_FRAMES = int(os.environ.get("DECAY_FRAMES", "8"))
|
||||||
FPS = 30
|
|
||||||
|
|
||||||
COLS = 13
|
COLS = 13
|
||||||
ROWS = 8
|
ROWS = 8
|
||||||
|
|
||||||
|
frame_interval = 1.0 / 30.0 # 30 FPS target
|
||||||
|
next_frame_time = 0.0
|
||||||
|
|
||||||
|
_poll = select.poll()
|
||||||
|
POLLIN = select.POLLIN
|
||||||
|
POLLOUT = select.POLLOUT
|
||||||
|
|
||||||
|
SOCK_BUFFER = 4096
|
||||||
|
ROUTER_RECONNECT_DELAY = 0.1
|
||||||
|
|
||||||
|
_frame_hooks = [] # for metrics
|
||||||
|
|
||||||
|
|
||||||
def pack_frame(pixels):
|
def pack_frame(pixels):
|
||||||
bits = 0
|
bits = 0
|
||||||
@@ -24,6 +35,27 @@ def pack_frame(pixels):
|
|||||||
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF)
|
(bits >> 64) & 0xFFFFFFFF, (bits >> 96) & 0xFFFFFFFF)
|
||||||
|
|
||||||
|
|
||||||
|
class FrameQueue:
|
||||||
|
def __init__(self, maxsize=10):
|
||||||
|
self._q = collections.deque()
|
||||||
|
self._maxsize = maxsize
|
||||||
|
|
||||||
|
def push(self, frame_bytes):
|
||||||
|
if len(self._q) >= self._maxsize:
|
||||||
|
return False # dropped
|
||||||
|
self._q.append(frame_bytes)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def pop(self):
|
||||||
|
return self._q.popleft() if self._q else None
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._q)
|
||||||
|
|
||||||
|
def full(self):
|
||||||
|
return len(self._q) >= self._maxsize
|
||||||
|
|
||||||
|
|
||||||
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.setblocking(False)
|
||||||
@@ -35,6 +67,8 @@ def connect_router():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global next_frame_time
|
||||||
|
|
||||||
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)
|
||||||
sock.bind(("0.0.0.0", VIZ_PORT))
|
sock.bind(("0.0.0.0", VIZ_PORT))
|
||||||
@@ -43,40 +77,44 @@ def main():
|
|||||||
router = connect_router()
|
router = connect_router()
|
||||||
packer = msgpack.Packer()
|
packer = msgpack.Packer()
|
||||||
|
|
||||||
|
_poll.register(sock, POLLIN)
|
||||||
|
|
||||||
active_notes = {}
|
active_notes = {}
|
||||||
last_frame = None
|
last_frame = None
|
||||||
buf = b""
|
buf = b""
|
||||||
msg_id = 0
|
msg_id = 0
|
||||||
frame_interval = 1.0 / FPS
|
next_frame_time = time.monotonic()
|
||||||
next_frame = time.monotonic()
|
|
||||||
router_buf = b""
|
pending_frames = FrameQueue(maxsize=5)
|
||||||
|
router_registered = False
|
||||||
|
router_fd = router.fileno()
|
||||||
|
_poll.register(router, POLLIN | POLLOUT)
|
||||||
|
|
||||||
frames_sent = 0
|
frames_sent = 0
|
||||||
frames_dropped = 0
|
frames_dropped = 0
|
||||||
|
router_errors = 0
|
||||||
poller = select.poll()
|
|
||||||
poller.register(sock, select.POLLIN)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Always poll UDP with short timeout
|
now = time.monotonic()
|
||||||
timeout = max(0, int((next_frame - time.monotonic()) * 1000))
|
|
||||||
timeout = min(timeout, 10) # cap at 10ms for responsiveness
|
timeout = max(0, int((next_frame_time - now) * 1000))
|
||||||
|
timeout = min(timeout, 5) # 5ms max to stay responsive
|
||||||
|
|
||||||
try:
|
try:
|
||||||
events = poller.poll(timeout)
|
events = _poll.poll(timeout)
|
||||||
except OSError:
|
except OSError:
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
# Process all pending UDP data immediately
|
# Process UDP
|
||||||
for fd, flags in events:
|
for fd, flags in events:
|
||||||
if fd == sock.fileno() and flags & select.POLLIN:
|
if fd == sock.fileno() and (flags & POLLIN):
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
data, _ = sock.recvfrom(1024)
|
data, _ = sock.recvfrom(SOCK_BUFFER)
|
||||||
buf += data
|
buf += data
|
||||||
except BlockingIOError:
|
except BlockingIOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Parse all complete messages
|
|
||||||
while b";" in buf:
|
while b";" in buf:
|
||||||
msg, buf = buf.split(b";", 1)
|
msg, buf = buf.split(b";", 1)
|
||||||
msg = msg.decode().strip()
|
msg = msg.decode().strip()
|
||||||
@@ -91,22 +129,44 @@ def main():
|
|||||||
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
|
elif msg_type == 0x80 or (msg_type == 0x90 and vel == 0):
|
||||||
active_notes.pop(note, None)
|
active_notes.pop(note, None)
|
||||||
|
|
||||||
# Also drain any pending router responses
|
# Process router readable
|
||||||
if router.fileno() >= 0:
|
if fd == router_fd and (flags & POLLIN):
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
router_buf += router.recv(4096)
|
d = router.recv(SOCK_BUFFER)
|
||||||
except BlockingIOError:
|
if not d:
|
||||||
pass
|
break
|
||||||
|
except BlockingIOError:
|
||||||
|
pass
|
||||||
|
|
||||||
# Render frame when it's time
|
# Process router writable — drain pending frames
|
||||||
now = time.monotonic()
|
if fd == router_fd and (flags & POLLOUT):
|
||||||
if now >= next_frame:
|
frame = pending_frames.pop()
|
||||||
next_frame = now + frame_interval
|
if frame:
|
||||||
|
try:
|
||||||
|
router.sendall(frame)
|
||||||
|
frames_sent += 1
|
||||||
|
except (BlockingIOError, OSError):
|
||||||
|
frames_dropped += 1
|
||||||
|
router_errors += 1
|
||||||
|
pending_frames.push(frame)
|
||||||
|
time.sleep(ROUTER_RECONNECT_DELAY)
|
||||||
|
try:
|
||||||
|
router.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
router = connect_router()
|
||||||
|
router_fd = router.fileno()
|
||||||
|
_poll.unregister(router)
|
||||||
|
_poll.register(router, POLLIN | POLLOUT)
|
||||||
|
|
||||||
|
# Render frame when due
|
||||||
|
if now >= next_frame_time:
|
||||||
|
next_frame_time = now + frame_interval
|
||||||
|
|
||||||
pixels = [[0] * COLS for _ in range(ROWS)]
|
pixels = [[0] * COLS for _ in range(ROWS)]
|
||||||
expired = []
|
expired = []
|
||||||
for note, decay in active_notes.items():
|
for note, decay in list(active_notes.items()):
|
||||||
if decay <= 0:
|
if decay <= 0:
|
||||||
expired.append(note)
|
expired.append(note)
|
||||||
continue
|
continue
|
||||||
@@ -125,17 +185,10 @@ def main():
|
|||||||
msg_id = (msg_id + 1) % 65535
|
msg_id = (msg_id + 1) % 65535
|
||||||
params = [str(v) for v in frame]
|
params = [str(v) for v in frame]
|
||||||
req = packer.pack([0, msg_id, "draw_frame", params])
|
req = packer.pack([0, msg_id, "draw_frame", params])
|
||||||
try:
|
if pending_frames.push(req):
|
||||||
router.sendall(req)
|
pass # queued
|
||||||
frames_sent += 1
|
else:
|
||||||
except (BlockingIOError, OSError):
|
|
||||||
frames_dropped += 1
|
frames_dropped += 1
|
||||||
# Reconnect on error
|
|
||||||
try:
|
|
||||||
router.close()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
router = connect_router()
|
|
||||||
last_frame = frame
|
last_frame = frame
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user