Files
pti-ledy/rpi/main.py
T
gitGnome 0d06ab7ff2 add dvd
2026-04-01 14:29:02 +02:00

593 lines
20 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
top_text_node_global_id = 0
top_text_cycle_count = 0
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
program2_draw_col = 0
program2_current_vehicle = None
dvd_pending = False
dvd_active = False
dvd_x = 0
dvd_y = 0
dvd_dx = 1
dvd_dy = 1
dvd_iteration = 0
dvd_move_counter = 0
DVD_TEXT = "PTI"
DVD_TEXT_W = 17 # 3 chars * 5px + 2 gaps
DVD_TEXT_H = 7
DVD_MAX_X = config.DISPLAY_MAX_X - DVD_TEXT_W + 1 # 31
DVD_MAX_Y = config.DISPLAY_MAX_Y - DVD_TEXT_H + 1 # 9
DVD_COLOR = (0, 0, 50)
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
global top_text_node_global_id, top_text_cycle_count
global dvd_pending, dvd_active
s = server.state
# --- DVD active: handled entirely in loop() ---
if dvd_active:
return
# --- DVD pending: wait for bottom area to clear ---
if dvd_pending:
if scroll_node_active and not _is_node_existing(scroll_node_global_id):
scroll_node_active = False
if not scroll_node_active and not program2_active:
dvd_pending = False
dvd_active = True
reset_dvd()
low_level.fill_pixels(0, 0, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
return
# --- Top text management ---
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
node = _add_multi_color_node(
"Witamy w PTI",
[(40, 40, 40, 0), (0, 0, 50, 9)],
0, 0, 3, True, True, -1, False
)
if node:
top_text_node_global_id = node['global_id']
elif not _is_node_existing(top_text_node_global_id):
# Top text cycle ended
top_text_cycle_count += 1
force_dvd = s.get('force_dvd', False)
if force_dvd:
with server._lock:
server.state['force_dvd'] = False
if force_dvd or (top_text_cycle_count >= config.DVD_TOP_TEXT_CYCLES
and random.randint(0, 99) < config.DVD_TRIGGER_CHANCE):
top_text_cycle_count = 0
dvd_pending = True
main_animation_started = False
return
# Re-add top text from right edge
node = _add_multi_color_node(
"Witamy w PTI",
[(40, 40, 40, 0), (0, 0, 50, 9)],
config.DISPLAY_MAX_X, 0, 3, True, True, -1, False
)
if node:
top_text_node_global_id = node['global_id']
# --- Bottom text management ---
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 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:
if force:
with server._lock:
server.state['force_vehicles'] = False
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 reset_dvd():
global dvd_x, dvd_y, dvd_dx, dvd_dy, dvd_iteration, dvd_move_counter
dvd_x = random.randint(0, DVD_MAX_X)
dvd_y = random.randint(0, DVD_MAX_Y)
dvd_dx = random.choice([-1, 1])
dvd_dy = random.choice([-1, 1])
dvd_iteration = 0
dvd_move_counter = 0
def handle_dvd():
global dvd_x, dvd_y, dvd_dx, dvd_dy, dvd_iteration, dvd_move_counter
global dvd_active, main_animation_started
if dvd_iteration >= config.DVD_ITERATIONS:
dvd_active = False
main_animation_started = False
return
dvd_iteration += 1
dvd_move_counter += 1
if dvd_move_counter >= 2:
dvd_move_counter = 0
new_x = dvd_x + dvd_dx
new_y = dvd_y + dvd_dy
if new_x < 0 or new_x > DVD_MAX_X:
dvd_dx = -dvd_dx
new_x = dvd_x + dvd_dx
if new_y < 0 or new_y > DVD_MAX_Y:
dvd_dy = -dvd_dy
new_y = dvd_y + dvd_dy
dvd_x = new_x
dvd_y = new_y
cursor = {'x': dvd_x, 'y': dvd_y}
for ch in DVD_TEXT:
_draw_char(ch, config.SMALL_TEXT_HEIGHT, config.SMALL_TEXT_WIDTH, *DVD_COLOR, cursor)
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
global program2_draw_col, program2_current_vehicle
program2_scroll_progress = 0
program2_spacing_counter = 0
program2_vehicles_spawned = 0
program2_drain_counter = 0
program2_done = False
program2_draw_col = 0
program2_current_vehicle = None
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
global program2_draw_col, program2_current_vehicle
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
if program2_spacing_counter > 0:
program2_spacing_counter -= 1
return
veh = VEHICLES[random.randint(0, 11)] if program2_draw_col == 0 else program2_current_vehicle
if program2_draw_col == 0:
program2_current_vehicle = veh
vr, vg, vb = config.VEHICLE_COLOR
for row in range(4):
if veh[row][program2_draw_col]:
low_level.set_pixel(config.DISPLAY_MAX_X, y + row, vr, vg, vb)
program2_draw_col += 1
if program2_draw_col >= 8:
program2_draw_col = 0
program2_spacing_counter = 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
if dvd_active:
low_level.fill_pixels(0, 0, config.DISPLAY_MAX_X, config.DISPLAY_MAX_Y, 0, 0, 0)
handle_dvd()
strip.show()
_send_frame()
return
low_level.fill_pixels(0, 0, config.DISPLAY_MAX_X, 7, 0, 0, 0)
handle_program1()
if dvd_active:
strip.show()
_send_frame()
return
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)