Refactor Docker and Local Setup Commands

- Enhanced the docker_setup.py to streamline image building and pulling processes with improved logging and error handling.
- Introduced a new LogScreen for better user feedback during long-running tasks.
- Updated local_setup.py to utilize logging for installation steps and improved error handling.
- Refactored robot_setup.py to simplify configuration saving and user interaction.
- Added a new LogScreen class in tui.py for consistent logging across different setup processes.
- Modified install.sh to adjust the installation directory for better organization.
This commit is contained in:
Даниил Грабарь
2026-05-20 17:41:11 +03:00
parent bbfec17876
commit 5f16825cf7
6 changed files with 507 additions and 502 deletions
+26 -45
View File
@@ -6,13 +6,14 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, List, Optional, Tuple
from rich.console import Console
from ruamel.yaml import YAML
from textual.app import App
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.screen import Screen
from textual.widgets import Footer, Static
from cobot.tui import SCREEN_CSS, InputScreen, PickScreen
_console = Console()
_PROJECT_DIR = Path(__file__).parent.parent.parent
_CONFIG_PATH = _PROJECT_DIR / "cobot-setting.yaml"
@@ -20,17 +21,13 @@ _yaml = YAML()
_yaml.preserve_quotes = True
# ---------------------------------------------------------------------------
# Field descriptors
# ---------------------------------------------------------------------------
@dataclass
class _Field:
key: str # dot-separated path within the block, e.g. "webots.world"
question: str
default: Any
note: str = ""
options: Optional[List[str]] = None # if set PickScreen, else InputScreen
options: Optional[List[str]] = None # if set - PickScreen, else - InputScreen
def label(self) -> str:
return self.key.split(".")[-1]
@@ -43,10 +40,6 @@ class _Block:
fields: List[_Field]
# ---------------------------------------------------------------------------
# Block definitions — bottom to top order
# ---------------------------------------------------------------------------
_BLOCKS: List[_Block] = [
_Block(
yaml_key="foxglove",
@@ -117,10 +110,6 @@ _BLOCKS: List[_Block] = [
]
# ---------------------------------------------------------------------------
# Wizard app
# ---------------------------------------------------------------------------
def _coerce(value: str, original: Any) -> Any:
"""Try to preserve the original YAML scalar type."""
if isinstance(original, bool):
@@ -157,7 +146,20 @@ def _set_nested(mapping: Any, path: str, value: Any) -> None:
cur[keys[-1]] = _coerce(value, original)
class _Wizard(App[bool]):
class _SavedScreen(Screen[None]):
BINDINGS = [Binding("enter,escape", "close", "Close")]
def compose(self) -> ComposeResult:
yield Static("Done", id="step")
yield Static(f"Configuration saved to {_CONFIG_PATH.name}", id="question")
yield Static("Press Enter to close.", id="note")
yield Footer()
def action_close(self) -> None:
self.dismiss(None)
class _Wizard(App[None]):
CSS = SCREEN_CSS
def __init__(self, data: Any):
@@ -172,13 +174,11 @@ class _Wizard(App[bool]):
def on_mount(self) -> None:
self._next_block()
# ------------------------------------------------------------------
# Block-level flow
# ------------------------------------------------------------------
def _next_block(self) -> None:
if self._block_idx >= len(self._blocks):
self.exit(True)
_save_config(self._data)
self.push_screen(_SavedScreen(), lambda _: self.exit())
return
block = self._blocks[self._block_idx]
total = len(self._blocks)
@@ -195,7 +195,7 @@ class _Wizard(App[bool]):
def _got_block_choice(self, v: Optional[str], block: _Block) -> None:
if v is None:
self.exit(False)
self.exit()
return
self._block_idx += 1
if v == "Yes":
@@ -206,9 +206,6 @@ class _Wizard(App[bool]):
else:
self._next_block()
# ------------------------------------------------------------------
# Field-level flow
# ------------------------------------------------------------------
def _next_field(self) -> None:
if not self._pending_fields:
@@ -240,7 +237,7 @@ class _Wizard(App[bool]):
def _got_field(self, v: Optional[str], f: _Field) -> None:
if v is None:
self.exit(False)
self.exit()
return
block = self._current_block
_set_nested(self._data[block.yaml_key], f.key, v)
@@ -248,9 +245,6 @@ class _Wizard(App[bool]):
self._next_field()
# ---------------------------------------------------------------------------
# YAML read / write
# ---------------------------------------------------------------------------
def _load_config() -> Any:
with open(_CONFIG_PATH, "r", encoding="utf-8") as fh:
@@ -262,10 +256,6 @@ def _save_config(data: Any) -> None:
_yaml.dump(data, fh)
# ---------------------------------------------------------------------------
# CLI entry points
# ---------------------------------------------------------------------------
def register(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser("robot-setup", help="Configure cobot-setting.yaml interactively")
p.set_defaults(func=run)
@@ -273,18 +263,9 @@ def register(subparsers: argparse._SubParsersAction) -> None:
def run(args: argparse.Namespace) -> None:
if not _CONFIG_PATH.exists():
_console.print(f"[red]Config not found:[/red] {_CONFIG_PATH}")
from rich.console import Console
Console().print(f"[red]Config not found:[/red] {_CONFIG_PATH}")
sys.exit(1)
data = _load_config()
ok = _Wizard(data).run()
if not ok:
_console.print("[yellow]Setup cancelled.[/yellow]")
return
_save_config(data)
_console.print(f"\n[green]Configuration saved:[/green] {_CONFIG_PATH}")
_console.print(
" Start the robot container with: [bold]cobot robot-setup[/bold] "
"then run [bold]./docker/jazzy/ros-iiwa7-webots/run.sh[/bold]"
)
_Wizard(data).run()