diff --git a/ports/RGSX/__main__.py b/ports/RGSX/__main__.py index aaac6cb..2835048 100644 --- a/ports/RGSX/__main__.py +++ b/ports/RGSX/__main__.py @@ -1,8 +1,14 @@ import os -os.environ["SDL_FBDEV"] = "/dev/fb0" -import pygame # type: ignore -import asyncio import platform +# Ne pas forcer SDL_FBDEV ici; si déjà défini par l'environnement, on le garde +try: + if "SDL_FBDEV" in os.environ: + pass # respecter la configuration existante +except Exception: + pass +import pygame # type: ignore +import time +import asyncio import logging import requests import queue @@ -27,8 +33,8 @@ from controls import handle_controls, validate_menu_state, process_key_repeats, from controls_mapper import map_controls, draw_controls_mapping, get_actions from controls import load_controls_config from utils import ( - detect_non_pc, load_sources, check_extension_before_download, extract_zip_data, - play_random_music, load_music_config, silence_alsa_warnings, enable_alsa_stderr_filter + load_sources, check_extension_before_download, extract_zip_data, + play_random_music, load_music_config ) from history import load_history, save_history from config import OTA_data_ZIP @@ -79,6 +85,37 @@ def _run_windows_gamelist_update(): _run_windows_gamelist_update() +# Vérifier et appliquer les mises à jour AVANT tout chargement des contrôles +try: + # Internet rapide (synchrone) avant init graphique complète + if test_internet(): + logger.debug("Pré-boot: connexion Internet OK, vérification des mises à jour") + # Initialiser un mini-contexte de chargement pour feedback si l'écran est prêt + config.menu_state = "loading" + config.current_loading_system = _("loading_check_updates") + config.loading_progress = 5.0 + config.needs_redraw = True + # Lancer la vérification en mode synchrone via boucle temporaire + # Utilise une boucle d'événements courte pour permettre pygame d'initialiser + try: + # Créer une boucle asyncio ad-hoc si non présente + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + success, _msg = loop.run_until_complete(check_for_updates()) + logger.debug(f"Pré-boot: check_for_updates terminé success={success}") + except Exception as e: + logger.error(f"Pré-boot: échec check_for_updates: {e}") + finally: + try: + loop.close() + except Exception: + pass + config.update_checked = True + else: + logger.debug("Pré-boot: pas d'Internet, pas de vérification des mises à jour") +except Exception as e: + logger.exception(f"Pré-boot update check a échoué: {e}") + # Initialisation de Pygame pygame.init() @@ -96,56 +133,7 @@ except Exception as e: logger.exception(f"Échec du nettoyage des anciens fichiers: {e}") -#Récupération des noms des joysticks si pas de joystick connecté, verifier si clavier connecté -joystick_names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())] -if not joystick_names: - joystick_names = ["Clavier"] - print("Aucun joystick détecté, utilisation du clavier par défaut") - logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.") - config.joystick = False - config.keyboard = True -else: - config.joystick = True - config.keyboard = False - print(f"Joysticks détectés: {joystick_names}") - logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.") - # Test des boutons du joystick - for name in joystick_names: - if "Xbox" in name or "360" in name or "X-Box" in name: - config.xbox_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "PlayStation" in name: - config.playstation_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - break - elif "Nintendo" in name: - config.nintendo_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - elif "Logitech" in name: - config.logitech_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - elif "8Bitdo" in name: - config.eightbitdo_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - elif "Steam" in name: - config.steam_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - elif "TRIMUI Smart Pro" in name: - config.trimui_controller = True - logger.debug(f"Controller detected : {name}") - print(f"Controller detected : {name}") - else: - config.generic_controller = True - logger.debug(f"Generic controller detected : {name}") - print(f"Generic controller detected : {name}") - + # Chargement des paramètres d'accessibilité config.accessibility_settings = load_accessibility_settings() # Appliquer la grille d'affichage depuis les paramètres @@ -169,8 +157,22 @@ config.sources_mode = get_sources_mode() config.custom_sources_url = get_custom_sources_url() logger.debug(f"Mode sources initial: {config.sources_mode}, URL custom: {config.custom_sources_url}") -# Détection du système non-PC -config.is_non_pc = detect_non_pc() +# Détection du système grace a une commande windows / linux (on oublie is non-pc c'est juste pour connaitre le materiel et le systeme d'exploitation) +def detect_system_info(): + """Détecte les informations système (OS, architecture) via des commandes appropriées.""" + try: + if platform.system() == "Windows": + # Commande pour Windows + result = subprocess.run(["wmic", "os", "get", "caption"], capture_output=True, text=True) + if result.returncode == 0: + logger.info(f"Système détecté (Windows): {result.stdout.strip()}") + else: + # Commande pour Linux + result = subprocess.run(["lsb_release", "-d"], capture_output=True, text=True) + if result.returncode == 0: + logger.info(f"Système détecté (Linux): {result.stdout.strip()}") + except Exception as e: + logger.error(f"Erreur lors de la détection du système: {e}") # Initialisation de l’écran screen = init_display() @@ -185,6 +187,64 @@ config.init_font() config.screen_width, config.screen_height = pygame.display.get_surface().get_size() logger.debug(f"Résolution d'écran : {config.screen_width}x{config.screen_height}") +# Détection des joysticks après init_display (plus stable sur Batocera) +try: + if platform.system() != "Windows": + time.sleep(0.05) # petite latence pour stabiliser SDL sur certains builds + count = pygame.joystick.get_count() +except Exception: + count = 0 +joystick_names = [] +for i in range(count): + try: + j = pygame.joystick.Joystick(i) + joystick_names.append(j.get_name()) + except Exception as e: + logger.debug(f"Impossible de lire le nom du joystick {i}: {e}") +normalized_names = [n.lower() for n in joystick_names] +if not joystick_names: + joystick_names = ["Clavier"] + print("Aucun joystick détecté, utilisation du clavier par défaut") + logger.debug("Aucun joystick détecté, utilisation du clavier par défaut.") + config.joystick = False + config.keyboard = True +else: + config.joystick = True + config.keyboard = False + print(f"Joysticks détectés: {joystick_names}") + logger.debug(f"Joysticks détectés: {joystick_names}, utilisation du joystick par défaut.") + for idx, name in enumerate(joystick_names): + lname = name.lower() + if ("xbox" in lname) or ("x-box" in lname) or ("xinput" in lname) or ("microsoft x-box" in lname) or ("x-box 360" in lname) or ("360" in lname): + config.xbox_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + break + elif "playstation" in lname: + config.playstation_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + break + elif "nintendo" in lname: + config.nintendo_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + elif "logitech" in lname: + config.logitech_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + elif "8bitdo" in name or "8-bitdo" in lname: + config.eightbitdo_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + elif "steam" in lname: + config.steam_controller = True + logger.debug(f"Controller detected : {name}") + print(f"Controller detected : {name}") + # Note: virtual keyboard display now depends on controller presence (config.joystick) + print(f"Generic controller detected : {name}") + logger.debug(f"Flags contrôleur: xbox={config.xbox_controller}, ps={config.playstation_controller}, nintendo={config.nintendo_controller}, eightbitdo={config.eightbitdo_controller}, steam={config.steam_controller}, trimui={config.trimui_controller}, logitech={config.logitech_controller}, generic={config.generic_controller}") + # Vérification des dossiers pour le débogage logger.debug(f"SYSTEM_FOLDER: {config.SYSTEM_FOLDER}") @@ -205,19 +265,12 @@ config.repeat_key = None config.repeat_start_time = 0 config.repeat_last_action = 0 -# Initialisation des variables pour la popup de musique - - -# Réduction du bruit ALSA (VM Batocera/alsa) -try: - silence_alsa_warnings() - enable_alsa_stderr_filter() -except Exception: - pass - # Initialisation du mixer Pygame pygame.mixer.pre_init(44100, -16, 2, 4096) -pygame.mixer.init() +try: + pygame.mixer.init() +except Exception as e: + logger.warning(f"Échec init mixer: {e}") # Charger la configuration de la musique AVANT de lancer la musique load_music_config() @@ -245,7 +298,7 @@ config.current_music = current_music # Met à jour la musique en cours dans con config.history = load_history() logger.debug(f"Historique de téléchargement : {len(config.history)} entrées") -# Vérification et chargement de la configuration des contrôles +# Vérification et chargement de la configuration des contrôles (après mises à jour et détection manette) config.controls_config = load_controls_config() # S'assurer que config.controls_config n'est jamais None @@ -267,14 +320,16 @@ else: # Initialisation du gamepad joystick = None if pygame.joystick.get_count() > 0: - joystick = pygame.joystick.Joystick(0) - joystick.init() - logger.debug("Gamepad initialisé") + try: + joystick = pygame.joystick.Joystick(0) + joystick.init() + logger.debug("Gamepad initialisé") + except Exception as e: + logger.warning(f"Échec initialisation gamepad: {e}") # Boucle principale async def main(): - # amazonq-ignore-next-line global current_music, music_files, music_folder logger.debug("Début main") running = True @@ -728,7 +783,7 @@ async def main(): draw_game_list(screen) if config.search_mode: draw_game_list(screen) - if config.is_non_pc: + if getattr(config, 'joystick', False): draw_virtual_keyboard(screen) elif config.menu_state == "download_progress": draw_progress_screen(screen) @@ -847,6 +902,14 @@ async def main(): config.needs_redraw = True logger.debug(f"Erreur : {config.error_message}") elif loading_step == "check_ota": + # Si mise à jour déjà vérifiée au pré-boot, sauter cette étape + if getattr(config, "update_checked", False): + logger.debug("Mises à jour déjà vérifiées au pré-boot, on saute check_for_updates()") + loading_step = "check_data" + config.current_loading_system = _("loading_downloading_games_images") + config.loading_progress = max(config.loading_progress, 50.0) + config.needs_redraw = True + continue logger.debug("Exécution de check_for_updates()") success, message = await check_for_updates() logger.debug(f"Résultat de check_for_updates : success={success}, message={message}") @@ -986,11 +1049,15 @@ async def main(): pygame.quit() logger.debug("Application terminée") - result2 = subprocess.run(["batocera-es-swissknife", "--emukill"]) - if result2 == 0: - logger.debug(f"Quitté avec succès") - else: - logger.debug("Error en essayant de quitter batocera-es-swissknife.") + try: + if platform.system() != "Windows": + result2 = subprocess.run(["batocera-es-swissknife", "--emukill"]) + if result2 == 0: + logger.debug(f"Quitté avec succès") + else: + logger.debug("Error en essayant de quitter batocera-es-swissknife.") + except FileNotFoundError: + logger.debug("batocera-es-swissknife introuvable, saut de l'étape d'arrêt (environnement non Batocera)") diff --git a/ports/RGSX/config.py b/ports/RGSX/config.py index 6479924..b28829c 100644 --- a/ports/RGSX/config.py +++ b/ports/RGSX/config.py @@ -4,7 +4,7 @@ import logging import platform # Version actuelle de l'application -app_version = "2.2.0.0" +app_version = "2.2.0.3" def get_operating_system(): """Renvoie le nom du système d'exploitation.""" @@ -196,7 +196,6 @@ last_state_change_time = 0 # Temps du dernier changement d'état pour debounce debounce_delay = 200 # Délai de debounce en millisecondes platform_dicts = [] # Liste des dictionnaires de plateformes selected_key = (0, 0) # Position du curseur dans le clavier virtuel -is_non_pc = True # Indicateur pour plateforme non-PC (par exemple, console) redownload_confirm_selection = 0 # Sélection pour la confirmation de redownload popup_message = "" # Message à afficher dans les popups popup_timer = 0 # Temps restant pour le popup en millisecondes (0 = inactif) @@ -208,6 +207,18 @@ batch_download_indices = [] # File d'attente des indices de jeux à traiter en batch_in_progress = False # Indique qu'un lot est en cours batch_pending_game = None # Données du jeu en attente de confirmation d'extension +# Indicateurs d'entrée (détectés au démarrage) +joystick = False +keyboard = False +xbox_controller = False +playstation_controller = False +nintendo_controller = False +logitech_controller = False +eightbitdo_controller = False +steam_controller = False +trimui_controller = False +generic_controller = False + # --- Filtre plateformes (UI) --- selected_filter_index = 0 # index dans la liste visible triée filter_platforms_scroll_offset = 0 # défilement si liste longue @@ -266,6 +277,9 @@ def init_font(): search_font = None small_font = None +# Indique si une vérification/installation des mises à jour a déjà été effectuée au démarrage +update_checked = False + def validate_resolution(): """Valide la résolution de l'écran par rapport aux capacités de l'écran.""" display_info = pygame.display.Info() diff --git a/ports/RGSX/controls.py b/ports/RGSX/controls.py index 10a3317..2267b3f 100644 --- a/ports/RGSX/controls.py +++ b/ports/RGSX/controls.py @@ -338,7 +338,7 @@ def handle_controls(event, sources, joystick, screen): # Jeux elif config.menu_state == "game": games = config.filtered_games if config.filter_active or config.search_mode else config.games - if config.search_mode and config.is_non_pc: + if config.search_mode and getattr(config, 'joystick', False): keyboard_layout = [ ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], ['A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], @@ -416,7 +416,7 @@ def handle_controls(event, sources, joystick, screen): config.filter_active = bool(config.search_query) config.needs_redraw = True logger.debug(f"Validation du filtre avec manette: query={config.search_query}, filter_active={config.filter_active}") - elif config.search_mode and not config.is_non_pc: + elif config.search_mode and not getattr(config, 'joystick', False): # Gestion de la recherche sur PC (clavier et manette) if is_input_matched(event, "confirm"): config.search_mode = False diff --git a/ports/RGSX/utils.py b/ports/RGSX/utils.py index a24a69d..37dcf42 100644 --- a/ports/RGSX/utils.py +++ b/ports/RGSX/utils.py @@ -30,115 +30,7 @@ unavailable_systems = [] # Cache/process flags for extensions generation/loading - -def silence_alsa_warnings(): - """Silence ALSA stderr spam (e.g., 'underrun occurred') on Linux by overriding the error handler. - - Safe no-op on non-Linux or if libasound is unavailable. - """ - try: - if platform.system() == "Linux": - import ctypes - import ctypes.util - lib = ctypes.util.find_library('asound') - if not lib: - return - asound = ctypes.CDLL(lib) - CErrorHandler = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p) - - def py_error_handler(filename, line, function, err, fmt): - return - - handler = CErrorHandler(py_error_handler) - try: - asound.snd_lib_error_set_handler(handler) - logger.info("ALSA warnings silenced via snd_lib_error_set_handler") - except Exception as inner: - logger.debug(f"snd_lib_error_set_handler not available: {inner}") - except Exception as e: - logger.debug(f"Unable to silence ALSA warnings: {e}") - - -def enable_alsa_stderr_filter(): - """Filter ALSA 'underrun occurred' spam from stderr by intercepting FD 2. - - Works on Linux by routing stderr through a pipe and dropping matching lines. - No-op on non-Linux systems. Safe to call multiple times; installs once. - """ - try: - if platform.system() != "Linux": - return - # Avoid double-install - if getattr(config, "_alsa_filter_installed", False): - return - - import os as _os - import threading as _threading - - patterns = [ - "ALSA lib pcm.c:", - "snd_pcm_recover) underrun occurred", - ] - - # Save original stderr fd and create pipe - save_fd = _os.dup(2) - rfd, wfd = _os.pipe() - _os.dup2(wfd, 2) # redirect current process stderr to pipe writer - _os.close(wfd) - - stop_event = _threading.Event() - - def _reader(): - try: - with _os.fdopen(rfd, 'rb', buffering=0) as r, _os.fdopen(save_fd, 'wb', buffering=0) as orig: - buf = b'' - while not stop_event.is_set(): - chunk = r.read(1024) - if not chunk: - break - buf += chunk - while b"\n" in buf: - line, buf = buf.split(b"\n", 1) - try: - s = line.decode('utf-8', errors='ignore') - if not any(p in s for p in patterns): - orig.write(line + b"\n") - orig.flush() - except Exception: - # Swallow any decode/write errors; keep filtering - pass - if buf: - try: - s = buf.decode('utf-8', errors='ignore') - if not any(p in s for p in patterns): - orig.write(buf) - orig.flush() - except Exception: - pass - except Exception as e: - try: - # Best-effort: restore original stderr on failure - _os.dup2(save_fd, 2) - except Exception: - pass - logger.debug(f"ALSA stderr filter reader error: {e}") - - t = _threading.Thread(target=_reader, daemon=True) - t.start() - - def _restore(): - try: - _os.dup2(save_fd, 2) - except Exception: - pass - stop_event.set() - - # Expose restore in config for future use if needed - config._alsa_filter_installed = True - config._alsa_filter_restore = _restore - logger.info("ALSA underrun stderr filter installed") - except Exception as e: - logger.debug(f"Unable to install ALSA stderr filter: {e}") + def restart_application(delay_ms: int = 2000): """Schedule a restart with a visible popup; actual restart happens in the main loop. @@ -181,22 +73,6 @@ _extensions_cache = None # type: ignore _extensions_json_regenerated = False -# Détection système non-PC -def detect_non_pc(): - arch = platform.machine() - try: - result = subprocess.run(["batocera-es-swissknife", "--arch"], capture_output=True, text=True, timeout=2) - if result.returncode == 0: - arch = result.stdout.strip() - #logger.debug(f"Architecture via batocera-es-swissknife: {arch}") - except (subprocess.SubprocessError, FileNotFoundError): - logger.debug(f"batocera-es-swissknife non disponible, utilisation de platform.machine(): {arch}") - - is_non_pc = arch not in ["x86_64", "amd64", "AMD64"] - logger.debug(f"Système détecté: {platform.system()}, architecture: {arch}, is_non_pc={is_non_pc}") - return is_non_pc - - # Fonction pour charger le fichier JSON des extensions supportées def load_extensions_json(): """Charge le JSON des extensions supportées.