forked from genewildish/Mainline
feat(integration): Complete feature rewrite with pipeline architecture, effects system, and display improvements
Major changes: - Pipeline architecture with capability-based dependency resolution - Effects plugin system with performance monitoring - Display abstraction with multiple backends (terminal, null, websocket) - Camera system for viewport scrolling - Sensor framework for real-time input - Command-and-control system via ntfy - WebSocket display backend for browser clients - Comprehensive test suite and documentation Issue #48: ADR for preset scripting language included This commit consolidates 110 individual commits into a single feature integration that can be reviewed and tested before further refinement.
This commit is contained in:
268
engine/display/streaming.py
Normal file
268
engine/display/streaming.py
Normal file
@@ -0,0 +1,268 @@
|
||||
"""
|
||||
Streaming protocol utilities for efficient frame transmission.
|
||||
|
||||
Provides:
|
||||
- Frame differencing: Only send changed lines
|
||||
- Run-length encoding: Compress repeated lines
|
||||
- Binary encoding: Compact message format
|
||||
"""
|
||||
|
||||
import json
|
||||
import zlib
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
|
||||
class MessageType(IntEnum):
|
||||
"""Message types for streaming protocol."""
|
||||
|
||||
FULL_FRAME = 1
|
||||
DIFF_FRAME = 2
|
||||
STATE = 3
|
||||
CLEAR = 4
|
||||
PING = 5
|
||||
PONG = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
class FrameDiff:
|
||||
"""Represents a diff between two frames."""
|
||||
|
||||
width: int
|
||||
height: int
|
||||
changed_lines: list[tuple[int, str]] # (line_index, content)
|
||||
|
||||
|
||||
def compute_diff(old_buffer: list[str], new_buffer: list[str]) -> FrameDiff:
|
||||
"""Compute differences between old and new buffer.
|
||||
|
||||
Args:
|
||||
old_buffer: Previous frame buffer
|
||||
new_buffer: Current frame buffer
|
||||
|
||||
Returns:
|
||||
FrameDiff with only changed lines
|
||||
"""
|
||||
height = len(new_buffer)
|
||||
changed_lines = []
|
||||
|
||||
for i, line in enumerate(new_buffer):
|
||||
if i >= len(old_buffer) or line != old_buffer[i]:
|
||||
changed_lines.append((i, line))
|
||||
|
||||
return FrameDiff(
|
||||
width=len(new_buffer[0]) if new_buffer else 0,
|
||||
height=height,
|
||||
changed_lines=changed_lines,
|
||||
)
|
||||
|
||||
|
||||
def encode_rle(lines: list[tuple[int, str]]) -> list[tuple[int, str, int]]:
|
||||
"""Run-length encode consecutive identical lines.
|
||||
|
||||
Args:
|
||||
lines: List of (index, content) tuples (must be sorted by index)
|
||||
|
||||
Returns:
|
||||
List of (start_index, content, run_length) tuples
|
||||
"""
|
||||
if not lines:
|
||||
return []
|
||||
|
||||
encoded = []
|
||||
start_idx = lines[0][0]
|
||||
current_line = lines[0][1]
|
||||
current_rle = 1
|
||||
|
||||
for idx, line in lines[1:]:
|
||||
if line == current_line:
|
||||
current_rle += 1
|
||||
else:
|
||||
encoded.append((start_idx, current_line, current_rle))
|
||||
start_idx = idx
|
||||
current_line = line
|
||||
current_rle = 1
|
||||
|
||||
encoded.append((start_idx, current_line, current_rle))
|
||||
return encoded
|
||||
|
||||
|
||||
def decode_rle(encoded: list[tuple[int, str, int]]) -> list[tuple[int, str]]:
|
||||
"""Decode run-length encoded lines.
|
||||
|
||||
Args:
|
||||
encoded: List of (start_index, content, run_length) tuples
|
||||
|
||||
Returns:
|
||||
List of (index, content) tuples
|
||||
"""
|
||||
result = []
|
||||
for start_idx, line, rle in encoded:
|
||||
for i in range(rle):
|
||||
result.append((start_idx + i, line))
|
||||
return result
|
||||
|
||||
|
||||
def compress_frame(buffer: list[str], level: int = 6) -> bytes:
|
||||
"""Compress a frame buffer using zlib.
|
||||
|
||||
Args:
|
||||
buffer: Frame buffer (list of lines)
|
||||
level: Compression level (0-9)
|
||||
|
||||
Returns:
|
||||
Compressed bytes
|
||||
"""
|
||||
content = "\n".join(buffer)
|
||||
return zlib.compress(content.encode("utf-8"), level)
|
||||
|
||||
|
||||
def decompress_frame(data: bytes, height: int) -> list[str]:
|
||||
"""Decompress a frame buffer.
|
||||
|
||||
Args:
|
||||
data: Compressed bytes
|
||||
height: Number of lines in original buffer
|
||||
|
||||
Returns:
|
||||
Frame buffer (list of lines)
|
||||
"""
|
||||
content = zlib.decompress(data).decode("utf-8")
|
||||
lines = content.split("\n")
|
||||
if len(lines) > height:
|
||||
lines = lines[:height]
|
||||
while len(lines) < height:
|
||||
lines.append("")
|
||||
return lines
|
||||
|
||||
|
||||
def encode_binary_message(
|
||||
msg_type: MessageType, width: int, height: int, payload: bytes
|
||||
) -> bytes:
|
||||
"""Encode a binary message.
|
||||
|
||||
Message format:
|
||||
- 1 byte: message type
|
||||
- 2 bytes: width (uint16)
|
||||
- 2 bytes: height (uint16)
|
||||
- 4 bytes: payload length (uint32)
|
||||
- N bytes: payload
|
||||
|
||||
Args:
|
||||
msg_type: Message type
|
||||
width: Frame width
|
||||
height: Frame height
|
||||
payload: Message payload
|
||||
|
||||
Returns:
|
||||
Encoded binary message
|
||||
"""
|
||||
import struct
|
||||
|
||||
header = struct.pack("!BHHI", msg_type.value, width, height, len(payload))
|
||||
return header + payload
|
||||
|
||||
|
||||
def decode_binary_message(data: bytes) -> tuple[MessageType, int, int, bytes]:
|
||||
"""Decode a binary message.
|
||||
|
||||
Args:
|
||||
data: Binary message data
|
||||
|
||||
Returns:
|
||||
Tuple of (msg_type, width, height, payload)
|
||||
"""
|
||||
import struct
|
||||
|
||||
msg_type_val, width, height, payload_len = struct.unpack("!BHHI", data[:9])
|
||||
payload = data[9 : 9 + payload_len]
|
||||
return MessageType(msg_type_val), width, height, payload
|
||||
|
||||
|
||||
def encode_diff_message(diff: FrameDiff, use_rle: bool = True) -> bytes:
|
||||
"""Encode a diff message for transmission.
|
||||
|
||||
Args:
|
||||
diff: Frame diff
|
||||
use_rle: Whether to use run-length encoding
|
||||
|
||||
Returns:
|
||||
Encoded diff payload
|
||||
"""
|
||||
|
||||
if use_rle:
|
||||
encoded_lines = encode_rle(diff.changed_lines)
|
||||
data = [[idx, line, rle] for idx, line, rle in encoded_lines]
|
||||
else:
|
||||
data = [[idx, line] for idx, line in diff.changed_lines]
|
||||
|
||||
payload = json.dumps(data).encode("utf-8")
|
||||
return payload
|
||||
|
||||
|
||||
def decode_diff_message(payload: bytes, use_rle: bool = True) -> list[tuple[int, str]]:
|
||||
"""Decode a diff message.
|
||||
|
||||
Args:
|
||||
payload: Encoded diff payload
|
||||
use_rle: Whether run-length encoding was used
|
||||
|
||||
Returns:
|
||||
List of (line_index, content) tuples
|
||||
"""
|
||||
|
||||
data = json.loads(payload.decode("utf-8"))
|
||||
|
||||
if use_rle:
|
||||
return decode_rle([(idx, line, rle) for idx, line, rle in data])
|
||||
else:
|
||||
return [(idx, line) for idx, line in data]
|
||||
|
||||
|
||||
def should_use_diff(
|
||||
old_buffer: list[str], new_buffer: list[str], threshold: float = 0.3
|
||||
) -> bool:
|
||||
"""Determine if diff or full frame is more efficient.
|
||||
|
||||
Args:
|
||||
old_buffer: Previous frame
|
||||
new_buffer: Current frame
|
||||
threshold: Max changed ratio to use diff (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
True if diff is more efficient
|
||||
"""
|
||||
if not old_buffer or not new_buffer:
|
||||
return False
|
||||
|
||||
diff = compute_diff(old_buffer, new_buffer)
|
||||
total_lines = len(new_buffer)
|
||||
changed_ratio = len(diff.changed_lines) / total_lines if total_lines > 0 else 1.0
|
||||
|
||||
return changed_ratio <= threshold
|
||||
|
||||
|
||||
def apply_diff(old_buffer: list[str], diff: FrameDiff) -> list[str]:
|
||||
"""Apply a diff to an old buffer to get the new buffer.
|
||||
|
||||
Args:
|
||||
old_buffer: Previous frame buffer
|
||||
diff: Frame diff to apply
|
||||
|
||||
Returns:
|
||||
New frame buffer
|
||||
"""
|
||||
new_buffer = list(old_buffer)
|
||||
|
||||
for line_idx, content in diff.changed_lines:
|
||||
if line_idx < len(new_buffer):
|
||||
new_buffer[line_idx] = content
|
||||
else:
|
||||
while len(new_buffer) < line_idx:
|
||||
new_buffer.append("")
|
||||
new_buffer.append(content)
|
||||
|
||||
while len(new_buffer) < diff.height:
|
||||
new_buffer.append("")
|
||||
|
||||
return new_buffer[: diff.height]
|
||||
Reference in New Issue
Block a user