#!/usr/bin/env python3 # -*- coding: utf-8 -*- import argparse import re import shutil import subprocess import sys from pathlib import Path from typing import Dict, List, Tuple from rich.console import Console from rich.panel import Panel from rich.table import Table console = Console() def info(msg: str) -> None: console.print(f"• {msg}") def ok(msg: str) -> None: console.print(f"[green]✔ {msg}[/]") def warn(msg: str) -> None: console.print(f"[yellow]▲ {msg}[/]") def die(msg: str, code: int = 1) -> "None": console.print(f"[bold red]✖ {msg}[/]") sys.exit(code) def ensure_project_layout() -> None: missing: List[str] = [] if not Path("robot_class.py").is_file(): missing.append("robot_class.py") if not Path(".scripts").is_dir(): missing.append(".scripts/") if missing: die("Non sei nella cartella giusta: mancano " + ", ".join(missing), code=9) # errore custom separato per robot_local.py (richiesta utente) if not Path("robot_local.py").is_file(): die( "File personale 'robot_local.py' non trovato. " "Crealo (es. copia 'robot_local.example.py') e riprova.", code=5, ) def find_matching_paren(s: str, open_idx: int) -> int | None: """Trova la ')' che chiude la '(' a open_idx. Ignora stringhe e backslash.""" depth = 0 i = open_idx in_str = False quote: str | None = None while i < len(s): ch = s[i] if in_str: if ch == "\\": i += 2 continue if ch == quote: in_str = False i += 1 continue if ch in ("'", '"'): in_str = True quote = ch i += 1 continue if ch == "(": depth += 1 elif ch == ")": depth -= 1 if depth == 0: return i i += 1 return None def parse_robots_py(robots_py: Path) -> Dict[str, str]: """ Ritorna dict {var_name: hub_name} leggendo definizioni tipo: cbrobot = LazyRobot(..., name="cbhub", ...) Parsing robusto: trova inizio 'X = LazyRobot(' e bilancia parentesi. """ if not robots_py.exists(): die(f"File non trovato: {robots_py}", code=3) text = robots_py.read_text(encoding="utf-8") start_rx = re.compile(r"^\s*(?P[A-Za-z_]\w*)\s*=\s*LazyRobot\s*\(", re.MULTILINE) name_rx = re.compile(r"\bname\s*=\s*(['\"])(?P.+?)\1", re.DOTALL) mapping: Dict[str, str] = {} for m in start_rx.finditer(text): var = m.group("var") open_idx = m.end() - 1 # '(' close_idx = find_matching_paren(text, open_idx) if close_idx is None: continue body = text[open_idx + 1 : close_idx] m_name = name_rx.search(body) if not m_name: continue hub = m_name.group("hub") mapping[var] = hub if not mapping: die("Nessun robot trovato in robots.py (pattern LazyRobot(..., name=...)).", code=3) return mapping def choose_robot(mapping: Dict[str, str], ident: str) -> Tuple[str, str]: """ Accetta come 'ident' o il nome variabile (cbrobot) o il nome hub (cbhub). Ritorna (var_name, hub_name). """ ident_lc = ident.strip().lower() # match per variabile for var, hub in mapping.items(): if var.lower() == ident_lc: return var, hub # match per hub for var, hub in mapping.items(): if hub.lower() == ident_lc: return var, hub # non trovato: mostra tabella e esci warn(f"Robot '{ident}' non trovato in robots.py.") table = Table(title="Robot conosciuti (da robots.py)") table.add_column("Variabile", style="cyan", no_wrap=True) table.add_column("Nome hub (BLE)", style="green") for var, hub in mapping.items(): table.add_row(var, hub) console.print(table) sys.exit(4) def rewrite_robot_local(robot_local: Path, var_name: str) -> None: header = ( "# robot_local.py — generato automaticamente da pbd.py\n" "# Non commitare questo file: deve essere nel .gitignore.\n" ) body = f"from robots import {var_name} as robot\n" try: robot_local.write_text(header + body, encoding="utf-8") except Exception as e: # pragma: no cover (best-effort) die(f"Impossibile scrivere {robot_local}: {e}") def run_pybricksdev(file_path: Path, hub_name: str) -> int: if not file_path.exists(): die(f"File target da inviare non trovato: {file_path}", code=6) exe = shutil.which("pybricksdev") if exe is None: die("Comando 'pybricksdev' non trovato nel PATH. Installalo e riprova.", code=7) cmd = [exe, "run", "ble", str(file_path), "--name", hub_name] return subprocess.run(cmd, check=False).returncode def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="pbd.py", description="Imposta robot_local.py e lancia: pybricksdev run ble --name ", ) p.add_argument("nomerobot", help="Variabile di robots.py (es. 'cbrobot') oppure nome hub BLE (es. 'cbhub').") p.add_argument("nomefile", help="Percorso del file .py da eseguire sul hub.") return p def main() -> None: parser = build_parser() args = parser.parse_args() ensure_project_layout() mapping = parse_robots_py(Path("robots.py")) var, hub = choose_robot(mapping, args.nomerobot) info(f"Selezionato robot: [cyan]{var}[/] → hub BLE: [green]{hub}[/]") rewrite_robot_local(Path("robot_local.py"), var) ok(f"Aggiornato robot_local.py con 'from robots import {var} as robot'") info(f"Eseguo: pybricksdev run ble {args.nomefile} --name {hub}") rc = run_pybricksdev(Path(args.nomefile), hub) if rc == 0: ok("pybricksdev completato con successo.") else: die(f"pybricksdev terminato con exit code {rc}.", code=rc) if __name__ == "__main__": main()