#!/usr/bin/env python3 """ GENERATORE VIDEO """ import time import spidev import RPi.GPIO as GPIO import signal import sys import os import subprocess import logging from PIL import Image # ==================== CONFIGURAZIONE LOGGING ==================== USER_HOME = os.path.expanduser("~") LOG_FILE = os.path.join(USER_HOME, "video_generator.log") logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # ==================== CONFIGURAZIONE HARDWARE ==================== DC_PIN = 25 RST_PIN = 24 ENC_CLK = 17 ENC_DT = 18 ENC_SW = 27 # ==================== COLORI RGB565 ==================== BLU = (0x00, 0x1F) BIANCO = (0xFF, 0xFF) NERO = (0x00, 0x00) ROSSO = (0xF8, 0x00) VERDE = (0x07, 0xE0) GIALLO = (0xFF, 0xE0) CIANO = (0x07, 0xFF) MAGENTA = (0xF8, 0x1F) ARANCIONE = (0xFC, 0x00) # ==================== VARIABILI AUDIO ==================== audio_process = None AUDIO_FREQ = 1000 SAMPLE_RATE = 44100 VOLUME = 1.0 # ==================== FONT 5x7 COMPLETO ==================== font_5x7 = { 'A': [" 000 ", "0 0", "0 0", "00000", "0 0", "0 0", "0 0"], 'B': ["0000 ", "0 0", "0000 ", "0 0", "0 0", "0 0", "0000 "], 'C': [" 000 ", "0 0", "0 ", "0 ", "0 ", "0 0", " 000 "], 'D': ["0000 ", "0 0", "0 0", "0 0", "0 0", "0 0", "0000 "], 'E': ["00000", "0 ", "0 ", "0000 ", "0 ", "0 ", "00000"], 'F': ["00000", "0 ", "0 ", "0000 ", "0 ", "0 ", "0 "], 'G': [" 000 ", "0 0", "0 ", "0 000", "0 0", "0 0", " 000 "], 'H': ["0 0", "0 0", "0 0", "00000", "0 0", "0 0", "0 0"], 'I': ["00000", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", "00000"], 'J': [" 00", " 0", " 0", " 0", "0 0", "0 0", " 000 "], 'K': ["0 0", "0 0 ", "0 0 ", "00 ", "0 0 ", "0 0 ", "0 0"], 'L': ["0 ", "0 ", "0 ", "0 ", "0 ", "0 ", "00000"], 'M': ["0 0", "00 00", "0 0 0", "0 0", "0 0", "0 0", "0 0"], 'N': ["0 0", "00 0", "0 0 0", "0 00", "0 0", "0 0", "0 0"], 'O': [" 000 ", "0 0", "0 0", "0 0", "0 0", "0 0", " 000 "], 'P': ["0000 ", "0 0", "0 0", "0000 ", "0 ", "0 ", "0 "], 'Q': [" 000 ", "0 0", "0 0", "0 0", "0 0 0", "0 0 ", " 00 0"], 'R': ["0000 ", "0 0", "0 0", "0000 ", "0 0 ", "0 0", "0 0"], 'S': [" 000 ", "0 0", "0 ", " 000 ", " 0", "0 0", " 000 "], 'T': ["00000", " 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " 0 "], 'U': ["0 0", "0 0", "0 0", "0 0", "0 0", "0 0", " 000 "], 'V': ["0 0", "0 0", "0 0", "0 0", " 0 0 ", " 0 0 ", " 0 "], 'W': ["0 0", "0 0", "0 0", "0 0 0", "0 0 0", "0 0 0", " 0 0 "], 'X': ["0 0", "0 0", " 0 0 ", " 0 ", " 0 0 ", "0 0", "0 0"], 'Y': ["0 0", "0 0", "0 0", " 000 ", " 0 ", " 0 ", " 0 "], 'Z': ["00000", " 0", " 0 ", " 0 ", " 0 ", "0 ", "00000"], '0': [" 000 ", "0 0", "0 0", "0 0", "0 0", "0 0", " 000 "], '1': [" 0 ", " 00 ", " 0 ", " 0 ", " 0 ", " 0 ", " 000 "], '2': [" 000 ", "0 0", " 0", " 0 ", " 0 ", " 0 ", "00000"], '3': [" 000 ", "0 0", " 0", " 00 ", " 0", "0 0", " 000 "], '4': [" 0 ", " 00 ", " 0 0 ", "0 0 ", "00000", " 0 ", " 0 "], '5': ["00000", "0 ", "0000 ", " 0", " 0", "0 0", " 000 "], '6': [" 000 ", "0 0", "0 ", "0000 ", "0 0", "0 0", " 000 "], '7': ["00000", " 0", " 0 ", " 0 ", " 0 ", "0 ", "0 "], '8': [" 000 ", "0 0", "0 0", " 000 ", "0 0", "0 0", " 000 "], '9': [" 000 ", "0 0", "0 0", " 000 ", " 0", "0 0", " 000 "], ' ': [" "] * 7, '.': [" ", " ", " ", " ", " ", " ", " 0 "], ':': [" ", " 0 ", " ", " ", " ", " 0 ", " "], '-': [" ", " ", " ", "00000", " ", " ", " "], '_': [" ", " ", " ", " ", " ", " ", "00000"], '!': [" 0 ", " 0 ", " 0 ", " 0 ", " 0 ", " ", " 0 "], '?': [" 000 ", "0 0", " 0 ", " 0 ", " 0 ", " ", " 0 "], '=': [" ", " ", "00000", " ", "00000", " ", " "], } font_button_5x7 = { 'H': ["0 0","0 0","00000","0 0","0 0","0 0","0 0"], 'D': ["0000 ","0 0","0 0","0 0","0 0","0 0","0000 "], 'M': ["0 0","00 00","0 0 0","0 0","0 0","0 0","0 0"], 'I': ["00000"," 0 "," 0 "," 0 "," 0 "," 0 ","00000"], 'V': ["0 0","0 0","0 0","0 0"," 0 0 "," 0 0 "," 0 "], 'G': [" 000 ","0 0","0 ","0 000","0 0","0 0"," 000 "], 'A': [" 000 ","0 0","0 0","00000","0 0","0 0","0 0"], 'C': [" 000 ","0 0","0 ","0 ","0 ","0 0"," 000 "], 'O': [" 000 ","0 0","0 0","0 0","0 0","0 0"," 000 "], 'P': ["0000 ","0 0","0 0","0000 ","0 ","0 ","0 "], ' ': [" "] * 7, '1': [" 0 "," 00 "," 0 "," 0 "," 0 "," 0 "," 000 "], '3': [" 000 ","0 0"," 0"," 00 "," 0","0 0"," 000 "], '4': [" 0 "," 00 "," 0 0 ","0 0 ","00000"," 0 "," 0 "], '6': [" 000 ","0 0","0 ","0000 ","0 0","0 0"," 000 "], '9': [" 000 ","0 0","0 0"," 000 "," 0","0 0"," 000 "], ':': [" "," 0 "," "," "," "," 0 "," "], } # ==================== INIZIALIZZAZIONE SPI ==================== spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 32000000 spi.mode = 0 # ==================== VARIABILI GLOBALI ==================== IMAGE_CACHE = {} hdmi_process = None comp_process = None USE_FBI = True image_index = 0 selected_index = 0 mode = "menu" last_hdmi_image = None last_comp_image = None last_encoder_time = 0 running = True RESET_DELAY = 3 encoder_last_state = 0 encoder_counter = 0 ENCODER_RESOLUTION = 2 # ==================== CARTELLE IMMAGINI ==================== TFT_IMAGES_FOLDER = "/boot/firmware/images_tft/" HDMI_IMAGES_FOLDER = "/boot/firmware/images_169/" VGA_IMAGES_FOLDER = "/boot/firmware/images_43/" COMP_IMAGES_FOLDER = "/boot/firmware/images_comp/" tft_images = [] hdmi_images = [] vga_images = [] comp_images = [] # ==================== FUNZIONI DI CONTROLLO HDMI ==================== def check_hdmi_connected(): """Verifica se l'HDMI è connesso""" try: with open('/sys/class/drm/card0-HDMI-A-1/status', 'r') as f: return f.read().strip() == "connected" except: try: result = subprocess.run(['tvservice', '-s'], capture_output=True, text=True) return "HDMI" in result.stdout except: return False def reset_to_rca_output(): """Resetta la configurazione video per tornare a RCA""" try: subprocess.run(['sudo', 'cp', '/boot/config.txt', '/boot/config.txt.backup'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) with open('/boot/config.txt', 'r') as f: lines = f.readlines() new_lines = [] hdmi_ignore_added = False for line in lines: line_lower = line.lower().strip() if 'hdmi_force_hotplug' in line_lower: new_lines.append('# ' + line.strip() + '\n') if not hdmi_ignore_added: new_lines.append('hdmi_ignore_hotplug=1\n') hdmi_ignore_added = True elif 'hdmi_mode' in line_lower or 'hdmi_group' in line_lower: new_lines.append('# ' + line.strip() + '\n') elif 'sdtv_mode' in line_lower: new_lines.append('sdtv_mode=2\n') elif 'sdtv_aspect' in line_lower: new_lines.append('sdtv_aspect=3\n') elif 'hdmi_ignore_hotplug' in line_lower: new_lines.append('hdmi_ignore_hotplug=1\n') hdmi_ignore_added = True else: new_lines.append(line) if not hdmi_ignore_added: new_lines.append('\n# Force composite output\n') new_lines.append('hdmi_ignore_hotplug=1\n') if not any('sdtv_mode' in line.lower() for line in new_lines): new_lines.append('\n# Composite output settings\n') new_lines.append('sdtv_mode=2\n') new_lines.append('sdtv_aspect=3\n') with open('/tmp/config.txt.new', 'w') as f: f.writelines(new_lines) subprocess.run(['sudo', 'mv', '/tmp/config.txt.new', '/boot/config.txt'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) logger.info("Configurazione video resettata a RCA") return True except Exception as e: logger.error(f"Errore nel reset video: {e}") return False # ==================== FUNZIONI AUDIO ==================== audio_process = None AUDIO_FREQ = 1000 def start_audio_tone(): global audio_process stop_audio_tone() time.sleep(0.1) try: # Usa sox per tono continuo (repeat infinito, no interruzioni buffer) cmd = ['play', '-n', 'synth', 'sine', str(AUDIO_FREQ), 'repeat', '-', 'vol', str(VOLUME)] audio_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) time.sleep(0.5) return True except Exception as e: logger.error(f"Errore start_audio_tone: {e}") return False def stop_audio_tone(): global audio_process if audio_process: try: os.killpg(os.getpgid(audio_process.pid), signal.SIGTERM) except: pass try: audio_process.terminate() audio_process.wait(timeout=0.5) except: try: audio_process.kill() except: pass audio_process = None try: subprocess.run(['pkill', '-9', 'play'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except: pass time.sleep(0.1) # ==================== FUNZIONI DISPLAY ==================== def cleanup(): global running logger.info("Pulizia in corso...") running = False stop_hdmi_output() stop_comp_output() stop_audio_tone() try: send_command(0x28) time.sleep(0.1) except: pass try: spi.close() except: pass try: GPIO.remove_event_detect(ENC_CLK) GPIO.remove_event_detect(ENC_DT) GPIO.remove_event_detect(ENC_SW) except: pass try: GPIO.cleanup() except: pass def signal_handler(sig, frame): cleanup() sys.exit(0) signal.signal(signal.SIGINT, signal_handler) def send_command(cmd): GPIO.output(DC_PIN, GPIO.LOW) spi.xfer([cmd]) def send_data(data): GPIO.output(DC_PIN, GPIO.HIGH) spi.xfer(data if isinstance(data, list) else [data]) def init_display(): GPIO.output(RST_PIN, GPIO.LOW) time.sleep(0.05) GPIO.output(RST_PIN, GPIO.HIGH) time.sleep(0.05) send_command(0x01) time.sleep(0.12) send_command(0x11) time.sleep(0.12) send_command(0x3A) send_data(0x55) send_command(0x36) send_data(0x28) send_command(0x29) time.sleep(0.1) def set_window(x1, y1, x2, y2): send_command(0x2A) send_data([x1 >> 8, x1 & 0xFF, x2 >> 8, x2 & 0xFF]) send_command(0x2B) send_data([y1 >> 8, y1 & 0xFF, y2 >> 8, y2 & 0xFF]) send_command(0x2C) def send_pixel_data(pixels_data): GPIO.output(DC_PIN, GPIO.HIGH) chunk_size = 4096 for i in range(0, len(pixels_data), chunk_size): chunk = pixels_data[i:i + chunk_size] spi.xfer2(chunk) def fill_rect_fast(x, y, w, h, color): if w <= 0 or h <= 0: return set_window(x, y, x + w - 1, y + h - 1) total_pixels = w * h pixel_data = [color[0], color[1]] * total_pixels send_pixel_data(pixel_data) send_command(0x2C) def clear_tft_completely(): set_window(0, 0, 319, 239) total_pixels = 320 * 240 pixel_data = [0x00, 0x00] * total_pixels send_pixel_data(pixel_data) time.sleep(0.01) set_window(0, 0, 319, 239) send_command(0x2C) time.sleep(0.01) # ==================== FUNZIONI GRAFICHE ==================== def draw_big_char(char, x, y, block_size, color, bg_color=(0,0)): char_upper = char.upper() if char_upper not in font_5x7: fill_rect_fast(x, y, 5*block_size, 7*block_size, bg_color) fill_rect_fast(x + block_size, y + block_size, 3*block_size, 5*block_size, color) return bitmap = font_5x7[char_upper] for row in range(7): for col in range(5): px_x = x + col * block_size px_y = y + row * block_size if bitmap[row][col] == '0': fill_rect_fast(px_x, px_y, block_size, block_size, color) elif bg_color != (0,0): fill_rect_fast(px_x, px_y, block_size, block_size, bg_color) def draw_big_text_centered(text, y, block_size, color, bg_color=(0,0)): spacing = 1 char_width = 5 * block_size + spacing * block_size text_width = len(text) * char_width - spacing * block_size x = max(0, (320 - text_width)//2) for i, char in enumerate(text): draw_big_char(char, x + i * char_width, y, block_size, color, bg_color) send_command(0x2C) def draw_button_text_only(text, x, y, scale, color): char_width = 5 * scale + scale for i, char in enumerate(text.upper()): if char not in font_button_5x7: continue bitmap = font_button_5x7[char] for row in range(7): for col in range(5): if bitmap[row][col] == '0': fill_rect_fast(x + i*char_width + col*scale, y + row*scale, scale, scale, color) send_command(0x2C) def clear_area(x, y, w, h): fill_rect_fast(x, y, w, h, NERO) # ==================== OUTPUT ESTERNI ==================== def kill_all_fbi(): try: subprocess.run(['pkill', '-9', 'fbi'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) time.sleep(0.1) except: pass def start_hdmi_output(filename, show_audio=True): global hdmi_process, last_hdmi_image if filename == last_hdmi_image: return last_hdmi_image = filename if not os.path.exists(filename): return kill_all_fbi() time.sleep(0.15) if USE_FBI: cmd = ['sudo', 'fbi', '-T', '1', '-a', '--noverbose', '--nocomments', '--once', filename] hdmi_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) if show_audio: time.sleep(0.3) start_audio_tone() def stop_hdmi_output(): global hdmi_process, last_hdmi_image last_hdmi_image = None kill_all_fbi() hdmi_process = None stop_audio_tone() def start_comp_output(filename): global comp_process, last_comp_image if filename == last_comp_image: return if not os.path.exists(filename): return last_comp_image = filename kill_all_fbi() time.sleep(0.15) if USE_FBI: cmd = ['sudo', 'fbi', '-T', '1', '-a', '--noverbose', '--nocomments', '--once', filename] comp_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=os.setsid) def stop_comp_output(): global comp_process, last_comp_image last_comp_image = None kill_all_fbi() comp_process = None # ==================== GALLERIE IMMAGINI ==================== def draw_image_gallery_hdmi(): global image_index if not tft_images: clear_tft_completely() return if image_index >= len(tft_images): image_index = 0 tft_filename = tft_images[image_index] try: clear_tft_completely() draw_image_fast(tft_filename) if hdmi_images and image_index < len(hdmi_images): hdmi_filename = hdmi_images[image_index] start_hdmi_output(hdmi_filename, show_audio=True) except: clear_tft_completely() def draw_image_gallery_vga(): global image_index if not vga_images: clear_tft_completely() draw_big_text_centered("NO VGA", 80, 5, VERDE, NERO) draw_big_text_centered("IMAGES", 130, 5, VERDE, NERO) return if image_index >= len(vga_images): image_index = 0 try: clear_tft_completely() if tft_images: tft_filename = tft_images[image_index % len(tft_images)] if os.path.exists(tft_filename): draw_image_fast(tft_filename) vga_filename = vga_images[image_index] start_hdmi_output(vga_filename, show_audio=False) except: clear_tft_completely() def draw_image_gallery_comp(): global image_index if not comp_images: clear_tft_completely() draw_big_text_centered("NO COMP", 80, 5, ROSSO, NERO) draw_big_text_centered("IMAGES", 130, 5, ROSSO, NERO) return if image_index >= len(comp_images): image_index = 0 try: clear_tft_completely() if tft_images: tft_filename = tft_images[image_index % len(tft_images)] if os.path.exists(tft_filename): draw_image_fast(tft_filename) comp_filename = comp_images[image_index] start_comp_output(comp_filename) except: clear_tft_completely() draw_big_text_centered("ERROR", 100, 5, ROSSO, NERO) def draw_image_fast(filename): if not os.path.exists(filename): return if filename in IMAGE_CACHE: pixel_data, width, height = IMAGE_CACHE[filename] else: try: img = Image.open(filename) if img.mode != 'RGB': img = img.convert('RGB') original_width, original_height = img.size scale_x = 320 / original_width if original_width > 0 else 1 scale_y = 240 / original_height if original_height > 0 else 1 scale = min(scale_x, scale_y) new_width = int(original_width * scale) new_height = int(original_height * scale) img_resized = img.resize((new_width, new_height), Image.Resampling.LANCZOS) pixel_data = bytearray(new_width * new_height * 2) pixels = img_resized.load() for y in range(new_height): for x in range(new_width): r, g, b = pixels[x, y] rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) idx = (y * new_width + x) * 2 pixel_data[idx] = rgb565 >> 8 pixel_data[idx + 1] = rgb565 & 0xFF IMAGE_CACHE[filename] = (pixel_data, new_width, new_height) img.close() img_resized.close() except: pixel_data = bytearray(320 * 240 * 2) for i in range(320 * 240): x = i % 320 y = i // 320 rgb565 = 0x001F if (x + y) % 20 < 10 else 0xFFE0 idx = i * 2 pixel_data[idx] = rgb565 >> 8 pixel_data[idx + 1] = rgb565 & 0xFF IMAGE_CACHE[filename] = (pixel_data, 320, 240) try: pixel_data, width, height = IMAGE_CACHE[filename] x_start = max(0, (320 - width) // 2) y_start = max(0, (240 - height) // 2) set_window(x_start, y_start, x_start + width - 1, y_start + height - 1) send_pixel_data(pixel_data) send_command(0x2C) except: pass # ==================== UI MENU ==================== buttons = ["16:9", "4:3", "COMP"] colors = [BLU, VERDE, ROSSO] option_width = 85 option_height = 45 option_spacing = 12 total_width = 3*option_width + 2*option_spacing start_x = (320 - total_width)//2 margin_bottom = 10 option_y = 240 - option_height - margin_bottom line_thickness = 4 def draw_menu_screen(): clear_tft_completely() time.sleep(0.05) draw_big_text_centered("GENERATORE", 40, 5, CIANO, NERO) draw_big_text_centered("VIDEO", 90, 5, MAGENTA, NERO) if check_hdmi_connected(): draw_big_text_centered("HDMI ATTIVO", 140, 3, GIALLO, NERO) draw_buttons_initial() send_command(0x2C) time.sleep(0.05) def draw_buttons_initial(): if mode != "menu": return hdmi_active = check_hdmi_connected() for i, name in enumerate(buttons): x = start_x + i*(option_width + option_spacing) text_x = x + (option_width - (5*3 + 3)*len(name))//2 text_y = option_y + (option_height - 7*3)//2 if name == "COMP" and hdmi_active: fill_rect_fast(x, option_y, option_width, option_height, GIALLO) fill_rect_fast(x + option_width - 12, option_y + 5, 10, 10, ROSSO) else: fill_rect_fast(x, option_y, option_width, option_height, colors[i]) draw_button_text_only(name, text_x, text_y, 3, NERO) draw_selected_button_frame(selected_index) send_command(0x2C) def draw_selected_button_frame(index): if mode != "menu": return x = start_x + index * (option_width + option_spacing) y = option_y fill_rect_fast(x, y + option_height, option_width, line_thickness, BIANCO) def redraw_button(index): if mode != "menu": return x = start_x + index*(option_width + option_spacing) text_x = x + (option_width - (5*3 + 3)*len(buttons[index]))//2 text_y = option_y + (option_height - 7*3)//2 hdmi_active = check_hdmi_connected() if buttons[index] == "COMP" and hdmi_active: fill_rect_fast(x, option_y, option_width, option_height, GIALLO) fill_rect_fast(x + option_width - 12, option_y + 5, 10, 10, ROSSO) else: fill_rect_fast(x, option_y, option_width, option_height, colors[index]) draw_button_text_only(buttons[index], text_x, text_y, 3, NERO) if index == selected_index: draw_selected_button_frame(index) else: fill_rect_fast(x, option_y + option_height, option_width, line_thickness, NERO) send_command(0x2C) # ==================== MESSAGGI DI RESET MINIMALI ==================== def show_reset_message(): """Messaggio semplice e chiaro per reset HDMI->RCA""" clear_tft_completely() time.sleep(0.1) # Messaggio essenziale draw_big_text_centered("DISCONNETTI", 70, 4, GIALLO, NERO) draw_big_text_centered("HDMI ORA", 110, 4, GIALLO, NERO) send_command(0x2C) time.sleep(2) # Dà tempo di leggere # Countdown clear_tft_completely() draw_big_text_centered("RIAVVIO IN", 90, 4, VERDE, NERO) for i in range(RESET_DELAY, 0, -1): clear_area(140, 130, 40, 40) draw_big_text_centered(str(i), 130, 6, ROSSO, NERO) send_command(0x2C) time.sleep(1) def show_config_updated(): """Conferma semplice""" clear_tft_completely() time.sleep(0.1) draw_big_text_centered("CONFIG", 100, 5, VERDE, NERO) draw_big_text_centered("AGGIORNATA", 140, 5, VERDE, NERO) send_command(0x2C) time.sleep(1) def show_rebooting_message(): """Solo 'RIAVVIO'""" clear_tft_completely() time.sleep(0.1) draw_big_text_centered("RIAVVIO", 100, 5, VERDE, NERO) # Punti animati minimali for i in range(3): fill_rect_fast(145 + i*20, 150, 10, 10, VERDE) send_command(0x2C) time.sleep(0.3) send_command(0x2C) time.sleep(1) # ==================== ENCODER ==================== def encoder_callback(channel): global selected_index, last_encoder_time, image_index global encoder_last_state, encoder_counter now = time.time() if now - last_encoder_time < 0.01: return last_encoder_time = now clk_state = GPIO.input(ENC_CLK) dt_state = GPIO.input(ENC_DT) current_state = (clk_state << 1) | dt_state transitions = { (0b00, 0b10): 1, (0b10, 0b11): 1, (0b11, 0b01): 1, (0b01, 0b00): 1, (0b00, 0b01): -1, (0b01, 0b11): -1, (0b11, 0b10): -1, (0b10, 0b00): -1, } transition = (encoder_last_state, current_state) if transition in transitions: direction = transitions[transition] encoder_counter += direction if abs(encoder_counter) >= ENCODER_RESOLUTION: handle_encoder_move(clockwise=(encoder_counter > 0)) encoder_counter = 0 encoder_last_state = current_state def handle_encoder_move(clockwise): global selected_index, image_index if mode == "menu": old_index = selected_index if clockwise: selected_index = (selected_index + 1) % 3 else: selected_index = (selected_index - 1) % 3 if old_index != selected_index: redraw_button(old_index) redraw_button(selected_index) elif mode == "gallery_hdmi" and tft_images: old = image_index image_index = (image_index + (1 if clockwise else -1)) % len(tft_images) if image_index != old: draw_image_gallery_hdmi() elif mode == "gallery_vga" and vga_images: old = image_index image_index = (image_index + (1 if clockwise else -1)) % len(vga_images) if image_index != old: draw_image_gallery_vga() elif mode == "gallery_comp" and comp_images: old = image_index image_index = (image_index + (1 if clockwise else -1)) % len(comp_images) if image_index != old: draw_image_gallery_comp() def button_pressed(channel): global mode, image_index, selected_index time.sleep(0.05) if mode == "menu" and selected_index == 2 and check_hdmi_connected(): show_reset_message() if reset_to_rca_output(): show_config_updated() show_rebooting_message() time.sleep(1) subprocess.run(['sudo', 'reboot', 'now']) return if mode == "menu": if selected_index == 0 and tft_images: mode = "gallery_hdmi" image_index = 0 stop_hdmi_output() stop_comp_output() time.sleep(0.2) draw_image_gallery_hdmi() elif selected_index == 1 and vga_images: mode = "gallery_vga" image_index = 0 stop_hdmi_output() stop_comp_output() time.sleep(0.2) draw_image_gallery_vga() elif selected_index == 2: if comp_images: mode = "gallery_comp" image_index = 0 stop_hdmi_output() stop_comp_output() time.sleep(0.2) draw_image_gallery_comp() else: clear_tft_completely() draw_big_text_centered("NO COMP", 80, 5, ROSSO, NERO) draw_big_text_centered("IMAGES", 130, 5, ROSSO, NERO) send_command(0x2C) time.sleep(2) draw_menu_screen() elif mode in ["gallery_hdmi", "gallery_vga", "gallery_comp"]: mode = "menu" stop_hdmi_output() stop_comp_output() time.sleep(0.2) draw_menu_screen() # ==================== CARICAMENTO IMMAGINI ==================== def load_image_lists(): global tft_images, hdmi_images, vga_images, comp_images if os.path.exists(TFT_IMAGES_FOLDER): tft_images = [os.path.join(TFT_IMAGES_FOLDER, f) for f in sorted(os.listdir(TFT_IMAGES_FOLDER)) if f.lower().endswith((".bmp", ".jpg", ".jpeg", ".png"))] if os.path.exists(HDMI_IMAGES_FOLDER): hdmi_images = [os.path.join(HDMI_IMAGES_FOLDER, f) for f in sorted(os.listdir(HDMI_IMAGES_FOLDER)) if f.lower().endswith((".bmp", ".jpg", ".jpeg", ".png"))] if os.path.exists(VGA_IMAGES_FOLDER): vga_images = [os.path.join(VGA_IMAGES_FOLDER, f) for f in sorted(os.listdir(VGA_IMAGES_FOLDER)) if f.lower().endswith((".bmp", ".jpg", ".jpeg", ".png"))] if os.path.exists(COMP_IMAGES_FOLDER): comp_images = [os.path.join(COMP_IMAGES_FOLDER, f) for f in sorted(os.listdir(COMP_IMAGES_FOLDER)) if f.lower().endswith((".bmp", ".jpg", ".jpeg", ".png"))] if not vga_images and hdmi_images: vga_images = hdmi_images.copy() if not hdmi_images and tft_images: hdmi_images = tft_images.copy() if not comp_images and tft_images: comp_images = tft_images.copy() # ==================== MAIN ==================== def main(): global USE_FBI, running, encoder_last_state GPIO.setmode(GPIO.BCM) GPIO.setup(DC_PIN, GPIO.OUT) GPIO.setup(RST_PIN, GPIO.OUT) GPIO.setup(ENC_CLK, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(ENC_DT, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.setup(ENC_SW, GPIO.IN, pull_up_down=GPIO.PUD_UP) clk_state = GPIO.input(ENC_CLK) dt_state = GPIO.input(ENC_DT) encoder_last_state = (clk_state << 1) | dt_state load_image_lists() try: result = subprocess.run(['which', 'fbi'], capture_output=True, text=True) USE_FBI = result.returncode == 0 except: USE_FBI = False try: init_display() GPIO.add_event_detect(ENC_CLK, GPIO.BOTH, callback=encoder_callback, bouncetime=10) GPIO.add_event_detect(ENC_DT, GPIO.BOTH, callback=encoder_callback, bouncetime=10) GPIO.add_event_detect(ENC_SW, GPIO.FALLING, callback=button_pressed, bouncetime=300) logger.info("=== GENERATORE VIDEO AVVIATO ===") draw_menu_screen() while running: time.sleep(0.01) except KeyboardInterrupt: logger.info("Programma interrotto dall'utente") except Exception as e: logger.error(f"Errore: {e}") finally: cleanup() logger.info("=== GENERATORE VIDEO TERMINATO ===") if __name__ == "__main__": main()