This commit is contained in:
GitProtogen
2026-03-16 08:28:45 +01:00
parent ba55ced7cf
commit 1fda094fc9
6 changed files with 883 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
PIN = 18 # GPIO18 (hardware PWM) for WS2812B on RPi4
PANEL_PIXEL_COUNT = 16
PANEL_COUNT = 3
NUMPIXELS = PANEL_PIXEL_COUNT * PANEL_PIXEL_COUNT * PANEL_COUNT # 768
DISPLAY_MAX_X = PANEL_PIXEL_COUNT * PANEL_COUNT - 1 # 47
DISPLAY_MAX_Y = PANEL_PIXEL_COUNT - 1 # 15
TEXT_MAX_LENGTH = 64
MAX_TEXT_NODES_COUNT = 4
MAX_IMAGES_SAVED = 2
MAX_ANIMATION_FRAME_COUNT = 12
MAX_ANIMATIONS_COUNT = 2
# 0xFF111111 in NeoPixel = R=0x11, G=0x11, B=0x11 (W byte ignored on GRB strip)
VEHICLE_COLOR = (17, 17, 17)
SMALL_TEXT_HEIGHT = 7
SMALL_TEXT_WIDTH = 5
MEDIUM_TEXT_HEIGHT = 7
MEDIUM_TEXT_WIDTH = 5
PROGRAM2_TEXT_ITERATIONS = 5
PROGRAM2_TRIGGER_CHANCE = 25
PROGRAM2_VEHICLE_COUNT = 25
+196
View File
@@ -0,0 +1,196 @@
T = True
F = False
# font7x5[ascii - 32][row 0..6][col 0..4]
FONT_7X5 = [
# ' ' (32)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# ! (33)
[[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,F,F,F],[F,F,T,F,F]],
# " (34)
[[F,T,F,T,F],[F,T,F,T,F],[F,T,F,T,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# # (35)
[[F,T,F,T,F],[F,T,F,T,F],[T,T,T,T,T],[F,T,F,T,F],[T,T,T,T,T],[F,T,F,T,F],[F,T,F,T,F]],
# $ (36)
[[F,F,T,F,F],[F,T,T,T,T],[T,F,T,F,F],[F,T,T,T,F],[F,F,T,F,T],[T,T,T,T,F],[F,F,T,F,F]],
# % (37)
[[T,T,F,F,T],[T,T,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,F,F,T,T],[T,F,F,T,T]],
# & (38)
[[F,T,T,F,F],[T,F,F,T,F],[T,F,F,T,F],[F,T,T,F,F],[T,F,T,F,T],[T,F,F,T,F],[F,T,T,F,T]],
# ' (39)
[[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# ( (40)
[[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,F,T,F,F],[F,F,F,T,F]],
# ) (41)
[[F,T,F,F,F],[F,F,T,F,F],[F,F,F,T,F],[F,F,F,T,F],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F]],
# * (42)
[[F,F,F,F,F],[F,T,F,T,F],[F,F,T,F,F],[T,T,T,T,T],[F,F,T,F,F],[F,T,F,T,F],[F,F,F,F,F]],
# + (43)
[[F,F,F,F,F],[F,F,T,F,F],[F,F,T,F,F],[T,T,T,T,T],[F,F,T,F,F],[F,F,T,F,F],[F,F,F,F,F]],
# , (44)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,T,T,F],[F,F,T,T,F],[F,T,T,F,F]],
# - (45)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[T,T,T,T,T],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# . (46)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,T,T,F,F],[F,T,T,F,F]],
# / (47)
[[F,F,F,F,T],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,F,F,F,F],[T,F,F,F,F]],
# 0 (48)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,T,T],[T,F,T,F,T],[T,T,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# 1 (49)
[[F,F,T,F,F],[F,T,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,T,T,T,F]],
# 2 (50)
[[F,T,T,T,F],[T,F,F,F,T],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,T,T,T,T]],
# 3 (51)
[[F,T,T,T,F],[T,F,F,F,T],[F,F,F,F,T],[F,F,T,T,F],[F,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# 4 (52)
[[F,F,F,T,F],[F,F,T,T,F],[F,T,F,T,F],[T,F,F,T,F],[T,T,T,T,T],[F,F,F,T,F],[F,F,F,T,F]],
# 5 (53)
[[T,T,T,T,T],[T,F,F,F,F],[T,T,T,T,F],[F,F,F,F,T],[F,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# 6 (54)
[[F,F,T,T,F],[F,T,F,F,F],[T,F,F,F,F],[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# 7 (55)
[[T,T,T,T,T],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F]],
# 8 (56)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# 9 (57)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,T],[F,F,F,F,T],[F,F,F,T,F],[F,T,T,F,F]],
# : (58)
[[F,F,F,F,F],[F,T,T,F,F],[F,T,T,F,F],[F,F,F,F,F],[F,T,T,F,F],[F,T,T,F,F],[F,F,F,F,F]],
# ; (59)
[[F,F,F,F,F],[F,T,T,F,F],[F,T,T,F,F],[F,F,F,F,F],[F,F,T,T,F],[F,F,T,T,F],[F,T,T,F,F]],
# < (60)
[[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,F,F,F,F],[F,T,F,F,F],[F,F,T,F,F],[F,F,F,T,F]],
# = (61)
[[F,F,F,F,F],[F,F,F,F,F],[T,T,T,T,T],[F,F,F,F,F],[T,T,T,T,T],[F,F,F,F,F],[F,F,F,F,F]],
# > (62)
[[F,T,F,F,F],[F,F,T,F,F],[F,F,F,T,F],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F]],
# ? (63)
[[F,T,T,T,F],[T,F,F,F,T],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,F,F,F,F],[F,F,T,F,F]],
# @ (64)
[[F,T,T,T,F],[T,F,F,F,T],[F,F,F,F,T],[F,T,T,F,T],[T,F,T,F,T],[T,F,T,T,T],[F,T,T,F,F]],
# A (65)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T]],
# B (66)
[[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F]],
# C (67)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,T],[F,T,T,T,F]],
# D (68)
[[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F]],
# E (69)
[[T,T,T,T,T],[T,F,F,F,F],[T,F,F,F,F],[T,T,T,T,F],[T,F,F,F,F],[T,F,F,F,F],[T,T,T,T,T]],
# F (70)
[[T,T,T,T,T],[T,F,F,F,F],[T,F,F,F,F],[T,T,T,T,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F]],
# G (71)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,F],[T,F,T,T,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,T]],
# H (72)
[[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T]],
# I (73)
[[T,T,T,T,T],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[T,T,T,T,T]],
# J (74)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,T,F],[F,F,F,F,F],[F,F,F,T,F],[F,T,F,T,F],[F,T,T,T,F]],
# K (75)
[[T,F,F,F,T],[T,F,F,T,F],[T,F,T,F,F],[T,T,F,F,F],[T,F,T,F,F],[T,F,F,T,F],[T,F,F,F,T]],
# L (76)
[[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F],[T,T,T,T,T]],
# M (77)
[[T,F,F,F,T],[T,T,F,T,T],[T,F,T,F,T],[T,F,T,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T]],
# N (78)
[[T,F,F,F,T],[T,F,F,F,T],[T,T,F,F,T],[T,F,T,F,T],[T,F,F,T,T],[T,F,F,F,T],[T,F,F,F,T]],
# O (79)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# P (80)
[[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F]],
# Q (81)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,T,F,T],[T,F,F,T,F],[F,T,T,F,T]],
# R (82)
[[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F],[T,F,T,F,F],[T,F,F,T,F],[T,F,F,F,T]],
# S (83)
[[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,F],[F,T,T,T,F],[F,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# T (84)
[[T,T,T,T,T],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F]],
# U (85)
[[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# V (86)
[[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,F,T,F],[F,F,T,F,F]],
# W (87)
[[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,T,F,T],[T,F,T,F,T],[T,T,F,T,T],[T,F,F,F,T]],
# X (88)
[[T,F,F,F,T],[T,F,F,F,T],[F,T,F,T,F],[F,F,T,F,F],[F,T,F,T,F],[T,F,F,F,T],[T,F,F,F,T]],
# Y (89)
[[T,F,F,F,T],[T,F,F,F,T],[F,T,F,T,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F]],
# Z (90)
[[T,T,T,T,T],[F,F,F,F,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,F,F,F,F],[T,T,T,T,T]],
# [ (91)
[[F,T,T,T,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,T,T,F]],
# \ (92)
[[T,F,F,F,F],[T,F,F,F,F],[F,T,F,F,F],[F,F,T,F,F],[F,F,F,T,F],[F,F,F,F,T],[F,F,F,F,T]],
# ] (93)
[[F,T,T,T,F],[F,F,F,T,F],[F,F,F,T,F],[F,F,F,T,F],[F,F,F,T,F],[F,F,F,T,F],[F,T,T,T,F]],
# ^ (94)
[[F,F,T,F,F],[F,T,F,T,F],[T,F,F,F,T],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# _ (95)
[[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[T,T,T,T,T]],
# ` (96)
[[F,T,T,F,F],[F,F,T,F,F],[F,F,F,T,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
# a (97)
[[F,F,F,F,F],[F,F,F,F,F],[F,T,T,T,F],[F,F,F,F,T],[F,T,T,T,T],[T,F,F,F,T],[F,T,T,T,T]],
# b (98)
[[T,F,F,F,F],[T,F,F,F,F],[T,F,T,T,F],[T,T,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F]],
# c (99)
[[F,F,F,F,F],[F,F,F,F,F],[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,F],[T,F,F,F,T],[F,T,T,T,F]],
# d (100)
[[F,F,F,F,T],[F,F,F,F,T],[F,T,T,F,T],[T,F,F,T,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,T]],
# e (101)
[[F,F,F,F,F],[F,F,F,F,F],[F,T,T,T,F],[T,F,F,F,T],[T,T,T,T,T],[T,F,F,F,F],[F,T,T,T,F]],
# f (102)
[[F,F,T,T,F],[F,T,F,F,T],[F,T,F,F,F],[T,T,T,F,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,F]],
# g (103)
[[F,F,F,F,F],[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,T],[F,F,F,F,T],[F,T,T,T,F]],
# h (104)
[[T,F,F,F,F],[T,F,F,F,F],[T,F,T,T,F],[T,T,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T]],
# i (105)
[[F,F,T,F,F],[F,F,F,F,F],[F,T,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,T,T,T,F]],
# j (106)
[[F,F,F,T,F],[F,F,F,F,F],[F,F,T,T,F],[F,F,F,T,F],[F,F,F,T,F],[T,F,F,T,F],[F,T,T,F,F]],
# k (107)
[[T,F,F,F,F],[T,F,F,F,F],[T,F,F,T,F],[T,F,T,F,F],[T,T,F,F,F],[T,F,T,F,F],[T,F,F,T,F]],
# l (108)
[[F,T,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,T,T,T,F]],
# m (109)
[[F,F,F,F,F],[F,F,F,F,F],[T,T,F,T,F],[T,F,T,F,T],[T,F,T,F,T],[T,F,T,F,T],[T,F,T,F,T]],
# n (110)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,T,T,F],[T,T,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T]],
# o (111)
[[F,F,F,F,F],[F,F,F,F,F],[F,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F]],
# p (112)
[[F,F,F,F,F],[F,F,F,F,F],[T,T,T,T,F],[T,F,F,F,T],[T,F,F,F,T],[T,T,T,T,F],[T,F,F,F,F]],
# q (113)
[[F,F,F,F,F],[F,T,T,T,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,T],[F,F,F,F,T],[F,F,F,F,T]],
# r (114)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,T,T,F],[T,T,F,F,T],[T,F,F,F,F],[T,F,F,F,F],[T,F,F,F,F]],
# s (115)
[[F,F,F,F,F],[F,F,F,F,F],[F,T,T,T,F],[T,F,F,F,F],[F,T,T,T,F],[F,F,F,F,T],[T,T,T,T,F]],
# t (116)
[[F,T,F,F,F],[F,T,F,F,F],[T,T,T,T,F],[F,T,F,F,F],[F,T,F,F,F],[F,T,F,F,T],[F,F,T,T,F]],
# u (117)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,T,T],[F,T,T,F,T]],
# v (118)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,F,F,T],[F,T,F,T,F],[F,F,T,F,F]],
# w (119)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,F,F,T],[T,F,F,F,T],[T,F,T,F,T],[T,F,T,F,T],[F,T,F,T,F]],
# x (120)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,F,F,T],[F,T,F,T,F],[F,F,T,F,F],[F,T,F,T,F],[T,F,F,F,T]],
# y (121)
[[F,F,F,F,F],[F,F,F,F,F],[T,F,F,F,T],[T,F,F,F,T],[F,T,T,T,F],[F,F,F,F,T],[F,T,T,T,F]],
# z (122)
[[F,F,F,F,F],[F,F,F,F,F],[T,T,T,T,T],[F,F,F,T,F],[F,F,T,F,F],[F,T,F,F,F],[T,T,T,T,T]],
# { (123)
[[F,F,F,T,F],[F,F,T,F,F],[F,F,T,F,F],[F,T,F,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,F,T,F]],
# | (124)
[[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,T,F,F]],
# } (125)
[[F,T,F,F,F],[F,F,T,F,F],[F,F,T,F,F],[F,F,F,T,F],[F,F,T,F,F],[F,F,T,F,F],[F,T,F,F,F]],
# ~ (126)
[[F,F,F,F,F],[F,T,F,F,F],[T,F,T,F,T],[F,F,F,T,F],[F,F,F,F,F],[F,F,F,F,F],[F,F,F,F,F]],
]
+78
View File
@@ -0,0 +1,78 @@
from 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
+431
View File
@@ -0,0 +1,431 @@
#!/usr/bin/env python3
"""
PTI LEDY - Raspberry Pi 4 port
Requires: sudo pip3 install rpi_ws281x flask
Run with: sudo python3 main.py
"""
import time
import random
import sys
from rpi_ws281x import PixelStrip, ws
import config
import low_level
import server
from fonts import FONT_7X5
# ---------------------------------------------------------------------------
# Strip init
# ---------------------------------------------------------------------------
strip = PixelStrip(
config.NUMPIXELS,
config.PIN,
freq_hz=800000,
dma=10,
invert=False,
brightness=255,
channel=0,
strip_type=ws.WS2811_STRIP_GRB,
)
strip.begin()
low_level.strip = strip
# ---------------------------------------------------------------------------
# Global state
# ---------------------------------------------------------------------------
global_node_id = 0
text_nodes = [None] * config.MAX_TEXT_NODES_COUNT
multi_color_text_nodes = [None] * config.MAX_TEXT_NODES_COUNT
for i in range(config.MAX_TEXT_NODES_COUNT):
text_nodes[i] = {
'global_id': 0, 'content': '', 'color': (0, 0, 0),
'char_height': config.MEDIUM_TEXT_HEIGHT, 'char_width': config.MEDIUM_TEXT_WIDTH,
'pos_x': 0, 'pos_y': 0, 'character_count': 0,
'scroll_slowness': 0, 'scroll_progress': 0,
'disappear_time': 0, 'is_scrolling': True, 'is_repeating': False,
}
multi_color_text_nodes[i] = {
'global_id': 0, 'content': '', 'colors': [], # list of (r,g,b,start_index)
'char_height': config.MEDIUM_TEXT_HEIGHT, 'char_width': config.MEDIUM_TEXT_WIDTH,
'pos_x': 0, 'pos_y': 0, 'character_count': 0, 'color_count': 0,
'scroll_slowness': 0, 'scroll_progress': 0,
'disappear_time': 0, 'is_scrolling': True, 'is_repeating': False,
}
main_animation_started = False
bottom_text_state = 0
scroll_node_global_id = 0
scroll_node_active = False
program2_active = False
bottom_text_iteration_count = 0
program2_vehicles_spawned = 0
program2_drain_counter = 0
program2_done = False
program2_scroll_progress = 0
program2_spacing_counter = 0
VEHICLES = [
[[0,0,0,0,0,0,0,0],[0,0,1,1,1,1,1,0],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[0,0,1,1,0,0,0,0],[0,1,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[0,0,1,1,0,1,1,1],[0,1,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[0,0,0,0,1,1,0,0],[0,0,1,1,1,1,1,0],[0,1,1,1,1,1,1,1],[0,0,1,0,0,0,1,0]],
[[0,0,0,0,0,0,0,0],[0,0,0,1,1,1,1,1],[0,0,1,1,1,1,1,1],[0,0,0,1,0,0,1,0]],
[[1,1,1,1,1,1,1,1],[0,1,0,1,0,1,0,1],[0,1,1,1,1,1,1,1],[0,0,1,0,0,0,1,0]],
[[0,0,0,0,0,0,0,0],[0,0,1,1,0,0,0,0],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[1,1,0,1,1,1,1,1],[1,1,0,1,1,1,1,1],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[0,0,0,1,1,1,1,1],[0,0,1,1,1,1,1,1],[1,1,1,1,1,1,1,1],[0,1,0,0,0,0,1,0]],
[[0,0,0,0,0,0,0,0],[0,1,1,1,0,0,0,1],[1,1,1,1,1,1,1,0],[0,1,0,0,0,1,0,0]],
[[0,1,0,1,1,1,1,1],[1,1,0,1,1,1,1,1],[1,1,1,1,1,1,1,1],[0,0,1,0,0,0,1,0]],
[[0,0,0,0,0,0,0,0],[0,0,0,0,0,1,1,0],[0,0,0,1,1,1,1,1],[0,0,0,0,1,0,1,0]],
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_text_node_x2(node):
if node['character_count'] == 0:
return node['pos_x']
return node['pos_x'] + (node['char_width'] * node['character_count']) + (node['character_count'] - 1) - 1
def _get_text_node_y2(node):
return node['pos_y'] + node['char_height'] - 1
def _color_for_char(r, g, b):
return (r, g, b)
def _add_text_node(content, color, pos_x, pos_y, scroll_slowness, is_scrolling, is_small, disappear_time, is_repeating):
global global_node_id
if not content:
return None
h = config.SMALL_TEXT_HEIGHT if is_small else config.MEDIUM_TEXT_HEIGHT
w = config.SMALL_TEXT_WIDTH if is_small else config.MEDIUM_TEXT_WIDTH
for node in text_nodes:
if node['disappear_time'] == 0:
node['content'] = content
node['color'] = color
node['pos_x'] = pos_x
node['pos_y'] = pos_y
node['char_height'] = h
node['char_width'] = w
node['character_count'] = len(content)
node['scroll_slowness'] = scroll_slowness
node['scroll_progress'] = 0
node['is_scrolling'] = is_scrolling
node['is_repeating'] = is_repeating
node['disappear_time'] = disappear_time
node['global_id'] = global_node_id
global_node_id += 1
return node
return None
def _add_multi_color_node(content, colors, pos_x, pos_y, scroll_slowness, is_scrolling, is_small, disappear_time, is_repeating):
global global_node_id
if not content:
return None
h = config.SMALL_TEXT_HEIGHT if is_small else config.MEDIUM_TEXT_HEIGHT
w = config.SMALL_TEXT_WIDTH if is_small else config.MEDIUM_TEXT_WIDTH
for node in multi_color_text_nodes:
if node['disappear_time'] == 0:
node['content'] = content
node['colors'] = list(colors)
node['color_count'] = len(colors)
node['pos_x'] = pos_x
node['pos_y'] = pos_y
node['char_height'] = h
node['char_width'] = w
node['character_count'] = len(content)
node['scroll_slowness'] = scroll_slowness
node['scroll_progress'] = 0
node['is_scrolling'] = is_scrolling
node['is_repeating'] = is_repeating
node['disappear_time'] = disappear_time
node['global_id'] = global_node_id
global_node_id += 1
return node
return None
def _is_node_existing(gid):
for node in text_nodes:
if node['global_id'] == gid and node['disappear_time'] != 0:
return True
for node in multi_color_text_nodes:
if node['global_id'] == gid and node['disappear_time'] != 0:
return True
return False
def _reset_global_id():
global global_node_id
global_node_id = 2
# ---------------------------------------------------------------------------
# Drawing
# ---------------------------------------------------------------------------
def _draw_char(ch, height, width, r, g, b, cursor):
if ord(ch) < ord('!') or ord(ch) > ord('~'):
ch = ' '
glyph = FONT_7X5[ord(ch) - ord(' ')]
low_level.draw_character(glyph, height, width, r, g, b, cursor)
def scroll_all_texts(split_scroll_mode=False):
for node in text_nodes:
if node['disappear_time'] == 0:
continue
if node['is_scrolling']:
if node['scroll_slowness'] > node['scroll_progress']:
node['scroll_progress'] += 1
else:
node['scroll_progress'] = 0
x2 = _get_text_node_x2(node)
if split_scroll_mode or node['pos_y'] >= 7:
if x2 < 0:
if node['is_repeating']:
node['pos_x'] = config.DISPLAY_MAX_X
else:
node['disappear_time'] = 1
continue
node['pos_x'] -= 1
else:
if node['pos_x'] > config.DISPLAY_MAX_X:
if node['is_repeating']:
node['pos_x'] = -x2 + node['pos_x']
else:
node['disappear_time'] = 1
continue
node['pos_x'] += 1
cursor = {'x': node['pos_x'], 'y': node['pos_y']}
r, g, b = node['color']
for ch in node['content']:
_draw_char(ch, node['char_height'], node['char_width'], r, g, b, cursor)
def scroll_all_multi_color_texts(split_scroll_mode=False):
for node in multi_color_text_nodes:
if node['disappear_time'] == 0:
continue
if node['is_scrolling']:
if node['scroll_slowness'] > node['scroll_progress']:
node['scroll_progress'] += 1
else:
node['scroll_progress'] = 0
x1 = node['pos_x']
x2 = x1 + (node['char_width'] * node['character_count']) + (node['character_count'] - 1) - 1
if split_scroll_mode or node['pos_y'] >= 7:
if x2 < 0:
if node['is_repeating']:
node['pos_x'] = config.DISPLAY_MAX_X
else:
node['disappear_time'] = 1
continue
node['pos_x'] -= 1
else:
if x1 > config.DISPLAY_MAX_X:
if node['is_repeating']:
node['pos_x'] = -x2 + x1
else:
node['disappear_time'] = 1
continue
node['pos_x'] += 1
cursor = {'x': node['pos_x'], 'y': node['pos_y']}
color_index = 0
r, g, b, _ = node['colors'][0]
for j, ch in enumerate(node['content']):
if color_index < node['color_count'] - 1 and node['colors'][color_index + 1][3] == j:
color_index += 1
r, g, b, _ = node['colors'][color_index]
_draw_char(ch, node['char_height'], node['char_width'], r, g, b, cursor)
def handle_disappear_timers():
for node in text_nodes:
if node['disappear_time'] > 0:
node['disappear_time'] -= 1
if node['disappear_time'] == 0:
low_level.fill_pixels(
node['pos_x'], node['pos_y'],
_get_text_node_x2(node), _get_text_node_y2(node),
0, 0, 0
)
def handle_multi_color_disappear_timers():
for node in multi_color_text_nodes:
if node['disappear_time'] > 0:
node['disappear_time'] -= 1
if node['disappear_time'] == 0:
x2 = node['pos_x'] + (node['char_width'] * node['character_count']) + (node['character_count'] - 1) - 1
low_level.fill_pixels(
node['pos_x'], node['pos_y'],
x2, node['pos_y'] + node['char_height'] - 1,
0, 0, 0
)
# ---------------------------------------------------------------------------
# Programs
# ---------------------------------------------------------------------------
def handle_program1():
global main_animation_started, scroll_node_active, scroll_node_global_id
global bottom_text_state, bottom_text_iteration_count, program2_active
s = server.state
if not s['program_top_text_enabled']:
for node in multi_color_text_nodes:
if node['disappear_time'] != 0:
node['disappear_time'] = 1
main_animation_started = False
elif not main_animation_started:
if not _is_node_existing(0) and not _is_node_existing(1):
main_animation_started = True
_add_multi_color_node(
"Witamy w PTI",
[(40, 40, 40, 0), (0, 0, 50, 9)],
0, 0, 3, True, True, -1, True
)
if not s['program_bottom_text_enabled']:
if scroll_node_active:
for node in text_nodes:
if node['disappear_time'] != 0:
node['disappear_time'] = 1
scroll_node_active = False
if program2_active:
program2_active = False
return
if program2_active:
return
if (main_animation_started or not s['program_top_text_enabled']) and \
(not scroll_node_active or not _is_node_existing(scroll_node_global_id)):
if scroll_node_active:
bottom_text_iteration_count += 1
scroll_node_active = False
if s['program_vehicles_enabled'] and bottom_text_iteration_count >= config.PROGRAM2_TEXT_ITERATIONS:
bottom_text_iteration_count = 0
if random.randint(0, 99) < config.PROGRAM2_TRIGGER_CHANCE:
program2_active = True
reset_program2()
low_level.fill_pixels(0, 9, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
return
texts = [
("Technik informatyk + ai, robotyka", (0, 0, 50)),
("Technik programista", (50, 40, 0)),
("Technik poligrafii i grafiki komputerowej", (0, 45, 0)),
("Technik reklamy", (60, 20, 0)),
]
text, color = texts[bottom_text_state]
node = _add_text_node(text, color, config.DISPLAY_MAX_X, 9, 1, True, True, -1, False)
if node is not None:
scroll_node_global_id = node['global_id']
scroll_node_active = True
bottom_text_state = (bottom_text_state + 1) % 4
def draw_vehicle(index, pos_x, pos_y):
vr, vg, vb = config.VEHICLE_COLOR
for row in range(4):
for col in range(8):
if VEHICLES[index][row][col]:
low_level.set_pixel(pos_x + col, pos_y + row, vr, vg, vb)
def reset_program2():
global program2_scroll_progress, program2_spacing_counter
global program2_vehicles_spawned, program2_drain_counter, program2_done
program2_scroll_progress = 0
program2_spacing_counter = 0
program2_vehicles_spawned = 0
program2_drain_counter = 0
program2_done = False
def handle_program2(spacing=4, scroll_slowness=1, y=0, max_vehicles=0):
global program2_scroll_progress, program2_spacing_counter
global program2_vehicles_spawned, program2_drain_counter, program2_done
if scroll_slowness > program2_scroll_progress:
program2_scroll_progress += 1
return
program2_scroll_progress = 0
low_level.shift_rectangle_left(0, y, config.DISPLAY_MAX_X, y + 3, 1)
if max_vehicles > 0 and program2_vehicles_spawned >= max_vehicles:
program2_drain_counter += 1
if program2_drain_counter >= config.DISPLAY_MAX_X + 1:
program2_done = True
return
program2_spacing_counter -= 1
if program2_spacing_counter <= 0:
draw_vehicle(random.randint(0, 11), config.DISPLAY_MAX_X - 7, y)
program2_spacing_counter = 8 + spacing
if max_vehicles > 0:
program2_vehicles_spawned += 1
# ---------------------------------------------------------------------------
# Main loop
# ---------------------------------------------------------------------------
def setup():
strip.setBrightness(255)
for i in range(config.NUMPIXELS):
strip.setPixelColor(i, 0)
strip.show()
server.start_server(port=80)
print("Server started. Running display loop.")
def loop():
global program2_active
s = server.state
low_level.fill_pixels(0, 0, config.DISPLAY_MAX_X, 7, 0, 0, 0)
handle_program1()
if s['program_top_text_enabled']:
scroll_all_multi_color_texts(split_scroll_mode=True)
handle_multi_color_disappear_timers()
if not s['program_bottom_text_enabled']:
if program2_active:
program2_active = False
low_level.fill_pixels(0, 9, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
elif program2_active and not s['program_vehicles_enabled']:
program2_active = False
low_level.fill_pixels(0, 9, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
elif program2_active:
handle_program2(spacing=4, scroll_slowness=1, y=12, max_vehicles=config.PROGRAM2_VEHICLE_COUNT)
if program2_done:
program2_active = False
low_level.fill_pixels(0, 9, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
else:
low_level.fill_pixels(0, 9, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
scroll_all_texts(split_scroll_mode=True)
handle_disappear_timers()
strip.show()
if __name__ == '__main__':
setup()
try:
while True:
loop()
time.sleep(0.02) # ~50 fps
except KeyboardInterrupt:
for i in range(config.NUMPIXELS):
strip.setPixelColor(i, 0)
strip.show()
print("\nStopped.")
sys.exit(0)
+2
View File
@@ -0,0 +1,2 @@
rpi_ws281x
flask
+151
View File
@@ -0,0 +1,151 @@
from flask import Flask, request, jsonify, Response
import threading
app = Flask(__name__)
# Shared state — written by Flask thread, read by main loop
state = {
'brightness': 100,
'program_top_text_enabled': True,
'program_bottom_text_enabled': True,
'program_vehicles_enabled': True,
}
_lock = threading.Lock()
INDEX_HTML = """<!DOCTYPE HTML><html>
<head>
<title>LED Panel Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
h1 { text-align: center; margin-bottom: 30px; font-size: 1.5em; }
.container { max-width: 400px; margin: 0 auto; }
.card { background: #16213e; border-radius: 12px; padding: 16px 20px; margin-bottom: 12px;
display: flex; align-items: center; justify-content: space-between; }
.card-label { font-size: 1em; }
.card-label small { display: block; color: #888; font-size: 0.75em; margin-top: 2px; }
.toggle { position: relative; width: 52px; height: 28px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.toggle .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background: #444; border-radius: 28px; transition: 0.3s; }
.toggle .slider:before { content: ""; position: absolute; height: 22px; width: 22px;
left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.3s; }
.toggle input:checked + .slider { background: #4CAF50; }
.toggle input:checked + .slider:before { transform: translateX(24px); }
.brightness-card { background: #16213e; border-radius: 12px; padding: 16px 20px; margin-bottom: 12px; }
.brightness-card label { font-size: 1em; display: block; margin-bottom: 8px; }
.brightness-row { display: flex; align-items: center; gap: 12px; }
.brightness-row input[type=range] { flex: 1; accent-color: #4CAF50; }
.brightness-row span { min-width: 32px; text-align: right; }
</style>
</head>
<body>
<div class="container">
<h1>LED Panel Control</h1>
<div class="card">
<div class="card-label">Top Text<small>Witamy w PTI</small></div>
<label class="toggle"><input type="checkbox" id="top_text" checked onchange="toggle('top_text', this.checked)"><span class="slider"></span></label>
</div>
<div class="card">
<div class="card-label">Bottom Text<small>School programs</small></div>
<label class="toggle"><input type="checkbox" id="bottom_text" checked onchange="toggle('bottom_text', this.checked)"><span class="slider"></span></label>
</div>
<div class="card">
<div class="card-label">Vehicles<small>Vehicle interruptions</small></div>
<label class="toggle"><input type="checkbox" id="vehicles" checked onchange="toggle('vehicles', this.checked)"><span class="slider"></span></label>
</div>
<div class="brightness-card">
<label>Brightness</label>
<div class="brightness-row">
<input type="range" id="brightness" min="0" max="100" value="100" oninput="updateBrightnessLabel()" onchange="sendBrightness()">
<span id="brightnessValue">100</span>
</div>
</div>
</div>
<script>
function toggle(program, enabled) {
var xhr = new XMLHttpRequest();
xhr.open("POST", "/toggle-program", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("program=" + program + "&enabled=" + (enabled ? "true" : "false"));
}
function updateBrightnessLabel() {
document.getElementById("brightnessValue").innerText = document.getElementById("brightness").value;
}
function sendBrightness() {
var val = document.getElementById("brightness").value;
var xhr = new XMLHttpRequest();
xhr.open("POST", "/brightness", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("value=" + val);
}
function fetchStatus() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "/program-status", true);
xhr.onload = function() {
if (xhr.status === 200) {
var s = JSON.parse(xhr.responseText);
document.getElementById("top_text").checked = s.top_text;
document.getElementById("bottom_text").checked = s.bottom_text;
document.getElementById("vehicles").checked = s.vehicles;
document.getElementById("brightness").value = s.brightness;
document.getElementById("brightnessValue").innerText = s.brightness;
}
};
xhr.send();
}
window.onload = fetchStatus;
</script>
</body>
</html>"""
@app.route('/')
def index():
return Response(INDEX_HTML, mimetype='text/html')
@app.route('/brightness', methods=['POST'])
def handle_brightness():
value = request.form.get('value')
if value is not None:
with _lock:
state['brightness'] = max(0, min(100, int(value)))
return 'OK'
@app.route('/toggle-program', methods=['POST'])
def handle_toggle():
program = request.form.get('program')
enabled_str = request.form.get('enabled')
if not program or enabled_str is None:
return 'Missing program or enabled parameter', 400
enabled = enabled_str.lower() == 'true'
key_map = {
'top_text': 'program_top_text_enabled',
'bottom_text': 'program_bottom_text_enabled',
'vehicles': 'program_vehicles_enabled',
}
if program not in key_map:
return 'Unknown program', 400
with _lock:
state[key_map[program]] = enabled
return 'OK'
@app.route('/program-status', methods=['GET'])
def handle_status():
with _lock:
s = dict(state)
return jsonify({
'top_text': s['program_top_text_enabled'],
'bottom_text': s['program_bottom_text_enabled'],
'vehicles': s['program_vehicles_enabled'],
'brightness': s['brightness'],
})
def start_server(host='0.0.0.0', port=80):
t = threading.Thread(target=lambda: app.run(host=host, port=port, use_reloader=False), daemon=True)
t.start()