#!/usr/bin/python3 """ Arch R - Panel Detection Wizard Runs on first boot (or after X-button reset) to let the user select their display panel. Provides audio feedback (beeps) for blind selection and visual feedback on tty1 for users who already have the correct panel. Flow: 1. Check /boot/panel-confirmed — if valid (>1 byte), exit immediately 2. Read variant from /etc/archr/variant (original or clone) 3. Initialize audio (speaker, 60% volume) 4. Cycle through panels (most common first): - Show panel name on tty1 - Play N beeps (N = position in list, capped at 5) - Wait 15s for input: A=confirm, B/DPAD_DOWN=next 5. On confirm: write panel.txt + panel-confirmed → sync → reboot 6. After 2 full cycles without confirm: auto-confirm default Panel selection is persistent: - panel.txt: U-Boot reads PanelDTBO variable (overlay path) - panel-confirmed: marker file (>1 byte = confirmed, ≤1 byte = reset) - Hold X during boot to reset (U-Boot overwrites panel-confirmed with 1 byte) """ import math import os import select import struct import subprocess import sys import time from pathlib import Path try: import evdev from evdev import ecodes except ImportError: print("ERROR: python-evdev not installed") sys.exit(1) # --- Paths --- BOOT_DIR = Path("/boot") PANEL_TXT = BOOT_DIR / "panel.txt" PANEL_CONFIRMED = BOOT_DIR / "panel-confirmed" VARIANT_FILE = Path("/etc/archr/variant") # --- Buttons (from rk3326-odroid-go.dtsi gpio-keys) --- BTN_A = ecodes.BTN_EAST # 305 — A button (confirm) BTN_B = ecodes.BTN_SOUTH # 304 — B button (next) BTN_X = ecodes.BTN_NORTH # 307 — X button (reset panel selection) BTN_DOWN = ecodes.BTN_DPAD_DOWN # 545 — D-pad down (next, alternative) # --- Audio --- BEEP_FREQ = 880 # Hz BEEP_DURATION = 0.12 # seconds BEEP_GAP = 0.15 # seconds between beeps SAMPLE_RATE = 44100 # --- Timing --- WAIT_PER_PANEL = 15 # seconds to wait for input per panel MAX_CYCLES = 2 # auto-confirm default after this many full cycles # --- Panel definitions per variant --- # (panel_num, dtbo_path, friendly_name) # Empty dtbo_path = default panel (hardcoded in base DTB, no overlay needed) # Order: most common first # R36S Original — 6 panels, default is Panel 4-V22 (~60% of units) PANELS_ORIGINAL = [ ("4", "", "Panel 4-V22 (Default)"), ("3", "ScreenFiles/Panel 3/mipi-panel.dtbo", "Panel 3-V20"), ("5", "ScreenFiles/Panel 5/mipi-panel.dtbo", "Panel 5-V22 Q8"), ("0", "ScreenFiles/Panel 0/mipi-panel.dtbo", "Panel 0"), ("1", "ScreenFiles/Panel 1/mipi-panel.dtbo", "Panel 1-V10"), ("2", "ScreenFiles/Panel 2/mipi-panel.dtbo", "Panel 2-V12"), ] # R36S Clone — 12 panels, default is Clone 8 ST7703 (G80CA-MB) PANELS_CLONE = [ ("C8", "", "Clone 8 ST7703 G80CA (Default)"), ("C1", "ScreenFiles/Clone Panel 1/mipi-panel.dtbo", "Clone 1 (ST7703)"), ("C3", "ScreenFiles/Clone Panel 3/mipi-panel.dtbo", "Clone 3 (NV3051D)"), ("C7", "ScreenFiles/Clone Panel 7/mipi-panel.dtbo", "Clone 7 (JD9365DA)"), ("C9", "ScreenFiles/Clone Panel 9/mipi-panel.dtbo", "Clone 9 (NV3051D)"), ("C10", "ScreenFiles/Clone Panel 10/mipi-panel.dtbo", "Clone 10 (ST7703)"), ("C2", "ScreenFiles/Clone Panel 2/mipi-panel.dtbo", "Clone 2 (ST7703)"), ("C4", "ScreenFiles/Clone Panel 4/mipi-panel.dtbo", "Clone 4 (NV3051D)"), ("C5", "ScreenFiles/Clone Panel 5/mipi-panel.dtbo", "Clone 5 (ST7703)"), ("C6", "ScreenFiles/Clone Panel 6/mipi-panel.dtbo", "Clone 6 (NV3051D)"), ("MAX", "ScreenFiles/R36 Max/mipi-panel.dtbo", "R36 Max (720x720)"), ("RX6S", "ScreenFiles/RX6S/mipi-panel.dtbo", "RX6S (NV3051D)"), ] def get_variant(): """Read variant from /etc/archr/variant (written by build-image.sh).""" try: return VARIANT_FILE.read_text().strip() except FileNotFoundError: # Fallback: detect via eMMC presence if os.path.exists('/sys/block/mmcblk1'): return "original" return "clone" def get_panels(variant): """Return panel list for this variant.""" if variant == "clone": return PANELS_CLONE return PANELS_ORIGINAL def is_confirmed(): """Check if panel selection is already confirmed.""" if not PANEL_CONFIRMED.exists(): return False return PANEL_CONFIRMED.stat().st_size > 1 def generate_beep_wav(freq=BEEP_FREQ, duration=BEEP_DURATION): """Generate a short sine wave beep as WAV data (16-bit mono PCM).""" n = int(SAMPLE_RATE * duration) samples = b''.join( struct.pack(' 1 byte = confirmed).""" fsync_write(PANEL_CONFIRMED, "confirmed\n") def init_audio(): """Initialize audio output for panel detection beeps.""" try: subprocess.run( ["amixer", "-q", "sset", "Playback Path", "SPK"], timeout=3, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) subprocess.run( ["amixer", "-q", "sset", "DAC", "60%"], timeout=3, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception: pass def wait_for_boot_mount(): """Wait for /boot to be mounted (FAT32 partition with panel files).""" for _ in range(30): if os.path.ismount("/boot"): return True time.sleep(1) return False def log_boot(msg): """Log to /boot for persistent debugging (survives tmpfs /var/log).""" try: with open("/boot/panel-detect.log", "a") as f: f.write(f"{time.strftime('%H:%M:%S')} {msg}\n") except Exception: pass def main(): # Ensure /boot is mounted before checking panel-confirmed if not os.path.ismount("/boot"): wait_for_boot_mount() log_boot(f"start: /boot mounted={os.path.ismount('/boot')}") log_boot(f" panel-confirmed exists={PANEL_CONFIRMED.exists()}") if PANEL_CONFIRMED.exists(): log_boot(f" panel-confirmed size={PANEL_CONFIRMED.stat().st_size}") # Find gamepad input device (needed for X-button check AND wizard) gamepad = None for _ in range(10): gamepad = find_gamepad() if gamepad: break time.sleep(1) # Check if X is held — reset panel selection (works on any board via evdev) if gamepad and is_x_held(gamepad): reset_panel() log_boot("panel reset by X button") # Quick exit if panel already confirmed if is_confirmed(): log_boot("confirmed — exiting") sys.exit(0) log_boot("NOT confirmed — starting wizard") # Determine variant and panel list variant = get_variant() panels = get_panels(variant) default_panel = panels[0] # First in list is always the default print(f"Arch R Panel Detection Wizard starting (variant: {variant})...") print(f" {len(panels)} panels available, default: {default_panel[2]}") # Initialize audio init_audio() # Generate beep WAVs beep = generate_beep_wav() intro_beep = generate_beep_wav(freq=660, duration=0.3) confirm_beep = generate_beep_wav(freq=1100, duration=0.1) if not gamepad: print("WARNING: Gamepad not found — auto-confirming default panel") write_panel_config(default_panel[0], default_panel[1]) confirm_panel() subprocess.run(["sync"]) sys.exit(0) print(f" Gamepad: {gamepad.name} ({gamepad.path})") # Intro: 2 long beeps to signal wizard is running play_beeps(2, intro_beep) time.sleep(0.5) # Panel selection loop for cycle in range(MAX_CYCLES): for idx, (panel_num, dtbo_path, name) in enumerate(panels): # Write panel config (ready for confirm) write_panel_config(panel_num, dtbo_path) # Visual feedback on tty1 position = f"[{idx + 1}/{len(panels)}]" if cycle > 0: position += f" (cycle {cycle + 1})" write_tty(f"{position} {name}") # Audio feedback: N beeps (capped at 5) beep_count = min(idx + 1, 5) play_beeps(beep_count, beep) print(f" {position} {name} — waiting...") # Wait for button input result = wait_for_button(gamepad, WAIT_PER_PANEL) if result == 'A': print(f" CONFIRMED: {name}") play_confirm_sound(confirm_beep) confirm_panel() subprocess.run(["sync"]) if dtbo_path: # Non-default panel: overlay applied by U-Boot on next reset write_tty(f"Confirmed: {name}\n\n Press RESET to apply.") print(f" Non-default panel — waiting for RESET") # Hold here until user presses RESET (no timeout) while True: time.sleep(60) else: # Default panel: no overlay needed, continue booting write_tty(f"Confirmed: {name}") print(f" Default panel — continuing boot") sys.exit(0) elif result == 'B': print(f" NEXT (B pressed)") continue else: print(f" TIMEOUT — advancing") continue # After MAX_CYCLES without confirmation: auto-confirm default print(f" No selection made — auto-confirming default {default_panel[2]}") write_panel_config(default_panel[0], default_panel[1]) write_tty(f"Auto-confirmed: {default_panel[2]}") play_confirm_sound(generate_beep_wav(freq=440, duration=0.2)) confirm_panel() subprocess.run(["sync"]) if __name__ == "__main__": main()