diff --git a/rpi/config.py b/rpi/config.py new file mode 100644 index 0000000..548b2ab --- /dev/null +++ b/rpi/config.py @@ -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 diff --git a/rpi/fonts.py b/rpi/fonts.py new file mode 100644 index 0000000..771177e --- /dev/null +++ b/rpi/fonts.py @@ -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]], +] diff --git a/rpi/low_level.py b/rpi/low_level.py new file mode 100644 index 0000000..1c371be --- /dev/null +++ b/rpi/low_level.py @@ -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 diff --git a/rpi/main.py b/rpi/main.py new file mode 100644 index 0000000..5613e4a --- /dev/null +++ b/rpi/main.py @@ -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) diff --git a/rpi/requirements.txt b/rpi/requirements.txt new file mode 100644 index 0000000..e0e30f7 --- /dev/null +++ b/rpi/requirements.txt @@ -0,0 +1,2 @@ +rpi_ws281x +flask diff --git a/rpi/server.py b/rpi/server.py new file mode 100644 index 0000000..f9c0998 --- /dev/null +++ b/rpi/server.py @@ -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 = """ +
+