Files
pti-ledy/rpi/low_level.py
T
cos 7d2fe96704 add mock mode, websocket display stream, and text editor
- mock_rpi_ws281x: stub for running without RPi hardware; auto-fallback
  on RuntimeError, forced via --mock flag or MOCK_LED env var
- mock mode runs web server on port 8080 instead of 80
- replace display polling with WebSocket stream (/ws/display); client
  sends start/stop messages, server pushes binary RGB frames
- force-button feedback: button shows waiting state and polls until
  the cycle clears the flag
- text editor UI: edit top and bottom texts with per-segment color
  pickers; saved to texts.json for persistence across restarts
- bottom and top texts read from server state instead of hardcoded
- bottom text switched to _add_multi_color_node to support multi-color
- fix bottom text invisible: split scroll_all_multi_color_texts into
  top (y<8) and bottom (y>=8) passes so fill_pixels does not wipe
  bottom nodes before they are drawn; this also fixes force-vehicles
  polling loop that stalled when the bottom node never expired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 21:15:39 +02:00

82 lines
2.4 KiB
Python

try:
from rpi_ws281x import Color
except (ImportError, RuntimeError):
from mock_rpi_ws281x import Color
from config import PANEL_PIXEL_COUNT, DISPLAY_MAX_X, DISPLAY_MAX_Y
from fonts import FONT_7X5
# Set by main.py after strip is created
strip = None
def _pixel_index(x, y):
if x % 2 == 1:
y = PANEL_PIXEL_COUNT - 1 - y
return y + (x * PANEL_PIXEL_COUNT)
def set_pixel(x, y, r, g, b):
if x < 0 or y < 0 or x > DISPLAY_MAX_X or y > DISPLAY_MAX_Y:
return
strip.setPixelColor(_pixel_index(x, y), Color(r, g, b))
def get_pixel_color(x, y):
"""Returns (r, g, b) tuple."""
if x < 0 or y < 0 or x > DISPLAY_MAX_X or y > DISPLAY_MAX_Y:
return (0, 0, 0)
c = strip.getPixelColor(_pixel_index(x, y))
return ((c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF)
def fill_pixels(x1, y1, x2, y2, r, g, b):
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
set_pixel(x, y, r, g, b)
def shift_rectangle_left(x1, y1, x2, y2, shift_by):
if not shift_by:
return
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
for y in range(y1, y2 + 1):
if y < 0 or y > DISPLAY_MAX_Y:
continue
for x in range(x1, x2 + 1):
src_x = x1 + (x - x1)
dest_x = src_x - shift_by
color = (0, 0, 0)
if 0 <= src_x <= DISPLAY_MAX_X:
color = get_pixel_color(src_x, y)
if 0 <= dest_x <= DISPLAY_MAX_X:
set_pixel(dest_x, y, *color)
for y in range(y1, y2 + 1):
if y < 0 or y > DISPLAY_MAX_Y:
continue
for x in range(x2 - shift_by + 1, x2 + 1):
if 0 <= x <= DISPLAY_MAX_X:
set_pixel(x, y, 0, 0, 0)
def draw_character_part(row_data, width, r, g, b, start_x, start_y):
for col in range(width):
if row_data[col]:
px = col + start_x
py = start_y
if 0 <= px <= DISPLAY_MAX_X and 0 <= py <= DISPLAY_MAX_Y:
set_pixel(px, py, r, g, b)
def draw_character(char_data, height, width, r, g, b, cursor):
"""Draws one character at cursor position, advances cursor.x by width+1."""
for row in range(height):
draw_character_part(char_data[row], width, r, g, b, cursor['x'], row + cursor['y'])
cursor['x'] += width + 1