FLL/pbd.sh
2025-10-26 19:00:36 +01:00

200 lines
5.9 KiB
Bash
Executable file

#!/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<var>[A-Za-z_]\w*)\s*=\s*LazyRobot\s*\(", re.MULTILINE)
name_rx = re.compile(r"\bname\s*=\s*(['\"])(?P<hub>.+?)\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 <file> --name <hub>",
)
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()