Files
pti-ledy/rpi/main.py
T
2026-04-01 13:38:42 +02:00

455 lines
16 KiB
Python

#!/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 socket
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
_udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
_udp_addr = ('127.0.0.1', 4210)
def _send_frame():
buf = bytearray(48 * 16 * 3)
off = 0
for y in range(16):
for x in range(48):
r, g, b = low_level.get_pixel_color(x, y)
buf[off] = r
buf[off + 1] = g
buf[off + 2] = b
off += 3
_udp_sock.sendto(buf, _udp_addr)
# ---------------------------------------------------------------------------
# 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
force = s.get('force_vehicles', False)
if force:
with server._lock:
server.state['force_vehicles'] = False
if s['program_vehicles_enabled'] and (force or bottom_text_iteration_count >= config.PROGRAM2_TEXT_ITERATIONS):
if force or random.randint(0, 99) < config.PROGRAM2_TRIGGER_CHANCE:
bottom_text_iteration_count = 0
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()
_send_frame()
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)