diff --git a/.gitignore b/.gitignore index cd1ca32..692fbf1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ env/ __pycache__/ .vscode/ .venv +*.egg*/ \ No newline at end of file diff --git a/README.md b/README.md index 78653cd..f7a3e04 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,31 @@ # `rovercli` -A multi-purpose CLI for UBC Rover operations. Add commands as needed +A command line interface and Textual TUI for UBC Rover operations. # Installation -`rovercli` has no dependencies outside the Python standard library. To use it: -1. Clone this repo -2. From the root of the repo, run `python main.py [COMMAND] [ARGS]` -3. **[OPTIONAL BUT RECOMMENDED]** To install the CLI on the system root and run commands from anywhere using `rovercli [COMMAND] [ARGS]`: - 1. Ensure you are in the root of the repo - 2. `chmod +x main.py` - 3. `ln -s "$(pwd)/main.py" ~/.local/bin/rovercli` - 4. Verify that the symlink worked by running `rovercli` +From the repository root, install the package in a virtual environment: + +```sh +python -m pip install -e . +``` + +This installs Textual and creates the `rovercli` terminal command. Running +`rovercli` without arguments opens the TUI. # Commands +## TUI + +```sh +rovercli +rovercli tui +``` + ## `sync` Sync files between devices on the UBC Rover network, without unecessary copying of files that haven't changed. ### Usage ``` -python main.py sync [SOURCE ROOT] [DEST ROOT] [DEST ADDRESS] +rovercli sync --src-root RoverFlake2 --dst-root RoverFlake2 --remote-host rv@192.168.1.4 ``` -For example: -``` -python main.py sync ~/RoverFlake2 ~/RoverFlake2 rv@192.168.1.4 -``` \ No newline at end of file + +Other commands are `rovercli print-ip-table` and `rovercli time-sync`. \ No newline at end of file diff --git a/cmds/__init__.py b/cmds/__init__.py deleted file mode 100644 index a5f932a..0000000 --- a/cmds/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .sync import sync -from .print_ip_table import print_ip_table -from .time_sync import time_sync \ No newline at end of file diff --git a/cmds/print_ip_table.py b/cmds/print_ip_table.py deleted file mode 100644 index c710b2e..0000000 --- a/cmds/print_ip_table.py +++ /dev/null @@ -1,24 +0,0 @@ -JETSON = ["Jetson", "192.168.1.5", "jet"] -ONBOARD = ["Rover Onboard Computer", "192.168.1.4", "rover"] -BASE = ["Control Base Station", "192.168.1.50", "cbs"] -COMMS_PI = ["Comms Pi", "192.168.1.51", "comms_pi"] -RELAY = ["Relay", "192.168.1.22", "relay"] -ONBOARD_ANTENNA = ["Onboard Antenna", "192.168.1.21", "rv_bullet"] -DISH = ["Dish", "192.168.1.20", "dish"] -BEN = ["Ben's laptop", "192.168.1.55", "ben"] -PTZ = ["PTZ Camera", "192.168.1.88", "ptz"] -RELAY_PI = ["Relay Pi", "192.168.1.52", "relay_pi"] -ROWAN = ["Rowan's Laptop", "192.168.1.40", "rowan"] -ALL_DEVICES = [JETSON, ONBOARD, BASE, COMMS_PI, RELAY, ONBOARD_ANTENNA, DISH, BEN, PTZ, RELAY_PI, ROWAN] - -def print_ip_table(): - print("To create an alias, run sudo nano /etc/hosts and paste the following lines:") - for device in ALL_DEVICES: - name, ip, alias = device - print(f"{ip}\t{alias}") - - print(f"\n\n{'Device':<25} {'IP Address':<15} {'Alias':<10}") - print("-" * 50) - for device in ALL_DEVICES: - name, ip, alias = device - print(f"{name:<25} {ip:<15} {alias:<10}") diff --git a/cmds/sync.py b/cmds/sync.py deleted file mode 100644 index d3af20b..0000000 --- a/cmds/sync.py +++ /dev/null @@ -1,101 +0,0 @@ -import os -from pathlib import Path -import subprocess -from typing import Optional - -def sync(src: str, dst: str, remote_host: str, packages: Optional[list[str]], build: bool = True, external_pkgs: bool = False): - - src_path = Path.home() / src - dst_root = Path("~") / dst - if packages: - dst_path = dst_root / "src" - else: - dst_path = dst_root - - print(f"Syncing from {src_path} to {remote_host}:{dst_path} with build={build} and packages={packages}") - - src_dir = src_path / "src" - - if not src_dir.exists(): - raise ValueError(f"Source directory '{src_dir}' does not exist.") - - print(f"Transferring source files to {remote_host}:{dst_path}...") - - rsync_cmd = ["rsync", "-azc", "--stats"] - - if packages: - if external_pkgs: - external_packages = src_dir / "external_pkgs" - if external_packages.exists(): - rsync_cmd.append(str(external_packages)) - for package in packages: - package_path = src_dir / package - if not package_path.exists(): - print(f"Error: Package directory '{package}' does not exist.") - raise ValueError(f"Package directory '{package}' does not exist.") - rsync_cmd.append(str(package_path)) - - remote_target = f"{remote_host}:{dst_path}" - else: - rsync_cmd.append("--delete") - rsync_cmd.append(str(src_dir)) - remote_target = f"{remote_host}:{dst_path}" - - rsync_cmd.append(remote_target) - - - print(f"Running rsync command: {' '.join(rsync_cmd)}") - rsync_result = subprocess.run(rsync_cmd) - if rsync_result.returncode != 0: - print("Rsync src transfer failed") - raise RuntimeError("Rsync src transfer failed") - - rsync_cmd2 = ["rsync", "-azc", "--stats"] - defaults_file = src_path / "colcon.defaults.yaml" - if defaults_file.exists(): - rsync_cmd2.append(str(defaults_file)) - - meta = src_path / "colcon.meta" - if meta.exists(): - rsync_cmd2.append(str(meta)) - - remote_target2 = f"{remote_host}:{dst_root}" - rsync_cmd2.append(remote_target2) - - if len(rsync_cmd2) > 3: # Only run if there are files to transfer - print(f"Running rsync command: {' '.join(rsync_cmd2)}") - rsync_result2 = subprocess.run(rsync_cmd2) - if rsync_result2.returncode != 0: - print("Rsync meta transfer failed") - raise RuntimeError("Rsync meta transfer failed") - - print("Source transfer complete!") - - if build: - # Limit number of cores to not freeze less powerful machines like Raspberry Pi - env_vars = os.environ.copy() - env_vars["MAKEFLAGS"] = "-j3" - - print("Building roverflake remotely...") - remote_colcon_cmd = ( - f"cd {str(dst_root)}; " - + "source /opt/ros/humble/setup.bash && " - + "colcon build " - + f"--base-paths {str(dst_root / 'src')} " - + f"--build-base {str(dst_root / 'build')} " - + f"--install-base {str(dst_root / 'install')} " - + f"--symlink-install " - ) - - if packages: - if external_pkgs: - for package in external_packages.iterdir(): - if package.is_dir() and package.name not in packages: - packages.append(package.name) - print(f"Building only selected packages: {packages}") - remote_colcon_cmd += f" --packages-select {' '.join(packages)}" - - remote_build_result = subprocess.run(["ssh", remote_host, remote_colcon_cmd]) - if remote_build_result.returncode != 0: - print("Remote build failed") - raise RuntimeError("Remote build failed") \ No newline at end of file diff --git a/cmds/time_sync.py b/cmds/time_sync.py deleted file mode 100644 index ee67713..0000000 --- a/cmds/time_sync.py +++ /dev/null @@ -1,13 +0,0 @@ -import subprocess - -def time_sync(remote_host: str = "rover"): - print(f"Synchronizing time with remote host: {remote_host}") - try: - pw = input("Enter password for sudo: ").encode() - date_output = int(subprocess.check_output(["date", "-u", "+%s"]).decode().strip()) + 1 - date_output = str(date_output).strip() - - subprocess.run(["ssh", remote_host, "sudo", "-S", "date", f"--set='@{date_output}'"], check=True, input=pw, capture_output=True) - print("Time synchronization successful.") - except subprocess.CalledProcessError as e: - print(f"Time synchronization failed: {e}") \ No newline at end of file diff --git a/jetson@192.168.1.5 b/jetson@192.168.1.5 deleted file mode 100644 index 84e0700..0000000 --- a/jetson@192.168.1.5 +++ /dev/null @@ -1 +0,0 @@ -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCv4CeDtGChh0J1+JwYzjfOko7KrB+9PbdE1+0MT/qoEN/3Q2f2LGUcWGr8silt8v9stFBIzmnwThd6pqSgsm+i+b63qIYJjvFZCbFcgBOZ6MzQJIYbv/RTu6zTQU9NQHXh+dWk9qJA9NChXJdJGE1qaHt4OsliKOdhgDkkDt7ev4TRfxkdFUfO3i9Ew8f6B2unh+opqauqCSHLYBFS0LV5FhQW2Hi2RX8RyOjUyFOyIaSoDifHD5avBkMXTXvhsP7tEu3ViNI3H1oRhYfNSju4HhfT5QbsK+AVW+Ux6whG9a63cprnMCbr8tNKAVABac3V97XnVXLqF4UV0UoHrncaEvvFSlt/86Qu+BAm4CtLOyrdlVH/WlSE+vL092wka/tVlpo8Tke71fkzVm44qEX6kHymtm+atyoCxNcb7gl3BXd4Ly/QnITBct/Mqfdg/bkmVO5BRwyrFYPHBFGRx1HxuGqvGrgqp6wfqfRt97GEOis9m1bZRv0y/oqMwmtniAE= rv@sux diff --git a/main.py b/main.py index 3a85f23..fb58e1a 100755 --- a/main.py +++ b/main.py @@ -1,59 +1,7 @@ #!/usr/bin/env python3 -import argparse -import cmds +from rovercli.cli import main -def main(): - app = argparse.ArgumentParser(description="Rover Command Line Interface") - subparsers = app.add_subparsers(dest="command") - - setup_sync(subparsers) - setup_print_ip_table(subparsers) - setup_time_sync(subparsers) - args = app.parse_args() - if hasattr(args, "func"): - args.func(args) - else: - app.print_help() - -def setup_sync(subparsers): - def sync(args): - cmds.sync( - args.src_root, - args.dst_root, - remote_host=args.remote_host, - build=not args.no_build, - packages=args.packages, - external_pkgs=args.external_pkgs, - ) - - sync_parser = subparsers.add_parser("sync") - sync_parser.add_argument("--src-root", default="RoverFlake2", help="Sync source from home directory") - sync_parser.add_argument("--dst-root", default="RoverFlake2", help="Sync remote destination from home directory") - sync_parser.add_argument("--remote-host", default="rv@192.168.1.4", help="Remote host for syncing") - sync_parser.add_argument( - "--no-build", - action="store_true", - help="Disable building the project after syncing", - ) - sync_parser.add_argument("--packages", nargs = "+", default=None, help="Space-separated list of packages to transfer and build (e.g., 'arm_control drive_control')") - sync_parser.add_argument("--external-pkgs", action="store_true", help="Include external packages in the sync process") - sync_parser.set_defaults(func=sync) - -def setup_print_ip_table(subparsers): - def print_ip_table(args): - cmds.print_ip_table() - - print_ip_table_parser = subparsers.add_parser("print-ip-table") - print_ip_table_parser.set_defaults(func=print_ip_table) - -def setup_time_sync(subparsers): - def time_sync(args): - cmds.time_sync() - - time_sync_parser = subparsers.add_parser("time-sync") - time_sync_parser.add_argument("--remote-host", default="rover", help="Remote host for time synchronization") - time_sync_parser.set_defaults(func=time_sync) if __name__ == "__main__": main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..028c553 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "rovercli" +version = "0.1.0" +description = "Command line tools and Textual TUI for UBC Rover operations" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "textual>=0.50", + "PyYAML>=6.0", +] + +[project.scripts] +rovercli = "rovercli.cli:main" + +[tool.setuptools.packages.find] +include = ["rovercli*"] + +[tool.setuptools.package-data] +rovercli = ["package_lists/*.yaml"] \ No newline at end of file diff --git a/rovercli/__init__.py b/rovercli/__init__.py new file mode 100644 index 0000000..a97051b --- /dev/null +++ b/rovercli/__init__.py @@ -0,0 +1,3 @@ +"""Tools for operating the UBC Rover network.""" + +__version__ = "0.1.0" \ No newline at end of file diff --git a/rovercli/cli.py b/rovercli/cli.py new file mode 100644 index 0000000..5551d6a --- /dev/null +++ b/rovercli/cli.py @@ -0,0 +1,60 @@ +import argparse + +from .commands import print_ip_table, sync, time_sync + + +def build_parser(): + parser = argparse.ArgumentParser(description="Rover Command Line Interface") + subparsers = parser.add_subparsers(dest="command") + + sync_parser = subparsers.add_parser("sync", help="Sync files to a rover computer") + sync_parser.add_argument("--src-root", default="RoverFlake2") + sync_parser.add_argument("--dst-root", default="RoverFlake2") + sync_parser.add_argument("--remote-host", default="rv@192.168.1.4") + sync_parser.add_argument("--no-build", action="store_true") + sync_parser.add_argument("--packages", nargs="+", default=None) + sync_parser.add_argument("--package-list", type=str, default=None) + sync_parser.add_argument("--no-external-pkgs", action="store_true") + sync_parser.set_defaults(func=_run_sync) + + ip_parser = subparsers.add_parser("print-ip-table", help="Print rover network addresses") + ip_parser.set_defaults(func=lambda args: print_ip_table()) + + time_parser = subparsers.add_parser("time-sync", help="Synchronize time with a rover computer") + time_parser.add_argument("--remote-host", default="rover") + time_parser.set_defaults(func=lambda args: time_sync(remote_host=args.remote_host)) + + tui_parser = subparsers.add_parser("tui", help="Open the Textual interface") + tui_parser.set_defaults(func=lambda args: _run_tui()) + return parser + + +def _run_sync(args): + sync( + args.src_root, + args.dst_root, + remote_host=args.remote_host, + build=not args.no_build, + packages=args.packages, + external_pkgs=not args.no_external_pkgs, + package_list=args.package_list, + ) + + +def _run_tui(): + from .tui import run_tui + + run_tui() + + +def main(): + parser = build_parser() + args = parser.parse_args() + if hasattr(args, "func"): + args.func(args) + else: + _run_tui() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/rovercli/commands/__init__.py b/rovercli/commands/__init__.py new file mode 100644 index 0000000..f2a2bd0 --- /dev/null +++ b/rovercli/commands/__init__.py @@ -0,0 +1,5 @@ +from .ip_table import print_ip_table +from .sync import sync +from .time_sync import time_sync + +__all__ = ["print_ip_table", "sync", "time_sync"] \ No newline at end of file diff --git a/rovercli/commands/ip_table.py b/rovercli/commands/ip_table.py new file mode 100644 index 0000000..9bd2be4 --- /dev/null +++ b/rovercli/commands/ip_table.py @@ -0,0 +1,24 @@ +DEVICES = [ + ("Jetson", "192.168.1.5", "jet"), + ("Rover Onboard Computer", "192.168.1.4", "rover"), + ("Control Base Station", "192.168.1.50", "cbs"), + ("Comms Pi", "192.168.1.51", "comms_pi"), + ("Relay", "192.168.1.22", "relay"), + ("Onboard Antenna", "192.168.1.21", "rv_bullet"), + ("Dish", "192.168.1.20", "dish"), + ("Ben's laptop", "192.168.1.55", "ben"), + ("PTZ Camera", "192.168.1.88", "ptz"), + ("Relay Pi", "192.168.1.52", "relay_pi"), + ("Rowan's Laptop", "192.168.1.40", "rowan"), +] + + +def print_ip_table(): + print("To create an alias, run sudo nano /etc/hosts and paste the following lines:") + for _, ip, alias in DEVICES: + print(f"{ip}\t{alias}") + + print(f"\n\n{'Device':<25} {'IP Address':<15} {'Alias':<10}") + print("-" * 50) + for name, ip, alias in DEVICES: + print(f"{name:<25} {ip:<15} {alias:<10}") \ No newline at end of file diff --git a/rovercli/commands/sync.py b/rovercli/commands/sync.py new file mode 100644 index 0000000..ba58f72 --- /dev/null +++ b/rovercli/commands/sync.py @@ -0,0 +1,77 @@ +import os +import subprocess +from pathlib import Path +from typing import Optional + +import yaml + + +def sync( + src: str, + dst: str, + remote_host: str, + packages: Optional[list[str]] = None, + build: bool = True, + external_pkgs: bool = True, + package_list: Optional[str] = None, +): + src_path = Path.home() / src + dst_root = Path("~") / dst + dst_path = dst_root / "src" if packages else dst_root + print(f"Syncing from {src_path} to {remote_host}:{dst_path} with build={build} and packages={packages}") + + src_dir = src_path / "src" + if not src_dir.exists(): + raise ValueError(f"Source directory '{src_dir}' does not exist.") + + rsync_cmd = ["rsync", "-azc", "--stats"] + if packages or package_list: + if external_pkgs: + external_packages = src_dir / "external_pkgs" + if external_packages.exists(): + rsync_cmd.append(str(external_packages)) + if package_list: + with open(package_list) as package_file: + listed_packages = yaml.safe_load(package_file)["packages"] + packages = (packages or []) + listed_packages + for package in packages or []: + package_path = src_dir / package + if not package_path.exists(): + raise ValueError(f"Package directory '{package}' does not exist.") + rsync_cmd.append(str(package_path)) + else: + rsync_cmd.extend(["--delete", str(src_dir)]) + + rsync_cmd.append(f"{remote_host}:{dst_path}") + print(f"Running rsync command: {' '.join(rsync_cmd)}") + if subprocess.run(rsync_cmd).returncode != 0: + raise RuntimeError("Rsync src transfer failed") + + metadata_command = ["rsync", "-azc", "--stats"] + for filename in ("colcon.defaults.yaml", "colcon.meta"): + metadata_file = src_path / filename + if metadata_file.exists(): + metadata_command.append(str(metadata_file)) + metadata_command.append(f"{remote_host}:{dst_root}") + if len(metadata_command) > 4 and subprocess.run(metadata_command).returncode != 0: + raise RuntimeError("Rsync meta transfer failed") + + print("Source transfer complete!") + if not build: + return + + ros_distro = os.environ.get("ROS_DISTRO") + if not ros_distro: + raise ValueError("ROS_DISTRO environment variable is not set.") + + env = os.environ.copy() + env["MAKEFLAGS"] = "-j3" + remote_command = ( + f"cd {dst_root}; source /opt/ros/{ros_distro}/setup.bash && colcon build " + f"--base-paths {dst_root / 'src'} --build-base {dst_root / 'build'} " + f"--install-base {dst_root / 'install'} --symlink-install" + ) + if packages: + remote_command += f" --packages-select {' '.join(packages)}" + if subprocess.run(["ssh", remote_host, remote_command], env=env).returncode != 0: + raise RuntimeError("Remote build failed") \ No newline at end of file diff --git a/rovercli/commands/time_sync.py b/rovercli/commands/time_sync.py new file mode 100644 index 0000000..00ad84e --- /dev/null +++ b/rovercli/commands/time_sync.py @@ -0,0 +1,19 @@ +import getpass +import subprocess +from typing import Optional + + +def time_sync(remote_host: str = "rover", password: Optional[str] = None): + print(f"Synchronizing time with remote host: {remote_host}") + try: + password = password if password is not None else getpass.getpass("Enter password for sudo: ") + date_output = str(int(subprocess.check_output(["date", "-u", "+%s"]).decode().strip()) + 1) + subprocess.run( + ["ssh", remote_host, "sudo", "-S", "date", f"--set='@{date_output}'"], + check=True, + input=password.encode(), + capture_output=True, + ) + print("Time synchronization successful.") + except subprocess.CalledProcessError as error: + print(f"Time synchronization failed: {error}") \ No newline at end of file diff --git a/rovercli/package_lists/comms_pi.yaml b/rovercli/package_lists/comms_pi.yaml new file mode 100644 index 0000000..a8166bc --- /dev/null +++ b/rovercli/package_lists/comms_pi.yaml @@ -0,0 +1,5 @@ +packages: + - comms_base_control + - rover_manager + - rover_msgs + - rover_utils \ No newline at end of file diff --git a/rovercli/package_lists/jetson.yaml b/rovercli/package_lists/jetson.yaml new file mode 100644 index 0000000..e41f6be --- /dev/null +++ b/rovercli/package_lists/jetson.yaml @@ -0,0 +1,7 @@ +packages: + - cameras_cpp + - ptz_cam + - rover_launchers + - rover_msgs + - rover_utils + - rover_manager \ No newline at end of file diff --git a/rovercli/package_lists/rover.yaml b/rovercli/package_lists/rover.yaml new file mode 100644 index 0000000..b80ffc4 --- /dev/null +++ b/rovercli/package_lists/rover.yaml @@ -0,0 +1,11 @@ +packages: + - arm_hardware_interface + - cameras_cpp + - drive_control + - ptz_cam + - rover_arm_common + - rover_gnss + - rover_launchers + - rover_msgs + - rover_utils + - rover_manager \ No newline at end of file diff --git a/rovercli/tui.py b/rovercli/tui.py new file mode 100644 index 0000000..f97fe9a --- /dev/null +++ b/rovercli/tui.py @@ -0,0 +1,152 @@ +import io +from contextlib import redirect_stdout +from pathlib import Path + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Container, Horizontal, Vertical +from textual.widgets import Button, Checkbox, ContentSwitcher, Footer, Header, Input, Label, Log, Select, Static + +from .commands import print_ip_table, sync, time_sync + +PACKAGE_LISTS_DIR = Path(__file__).parent / "package_lists" + + +def _package_list_options(): + files = sorted(PACKAGE_LISTS_DIR.glob("*.yaml")) + return [(f.stem, str(f)) for f in files] + + +class RoverApp(App): + TITLE = "Rover TUI" + BINDINGS = [ + Binding("1", "show_ip_table", "IP table", priority=True), + Binding("2", "show_time_sync", "Time sync", priority=True), + Binding("3", "show_sync", "Sync", priority=True), + Binding("q", "exit", "Exit", priority=True), + ] + CSS = """ + Screen { align: center middle; } + #main { width: 96%; height: 94%; } + #workspace { height: 1fr; } + #command-rail { width: 24; padding: 1; border: round $panel; } + #command-rail Button { width: 100%; margin-bottom: 1; } + #command-rail Button.-selected { background: $accent; color: $text; } + #content { width: 1fr; padding-left: 1; } + .command-panel { height: auto; padding: 1; border: round $accent; } + .command-panel Input { margin: 0 1; } + #log { height: 1fr; border: round $panel; padding: 1; } + """ + + def compose(self) -> ComposeResult: + yield Header() + with Container(id="main"): + yield Static("Rover operations", id="heading") + with Horizontal(id="workspace"): + with Vertical(id="command-rail"): + yield Button("1 IP table", id="select-ip") + yield Button("2 Time sync", id="select-time") + yield Button("3 Sync", id="select-sync") + yield Button("Exit", id="exit", variant="error") + with Vertical(id="content"): + with ContentSwitcher(initial="ip-panel", id="command-content"): + yield Static("", id="ip-panel") + with Vertical(classes="command-panel", id="sync-panel"): + yield Label("Sync workspace") + yield Input("RoverFlake2", placeholder="Source root", id="src-root") + yield Input("RoverFlake2", placeholder="Destination root", id="dst-root") + yield Input("rv@192.168.1.4", placeholder="Remote host", id="remote-host") + yield Select( + _package_list_options(), + prompt="Package list (optional)", + id="package-list", + ) + yield Input(placeholder="Extra packages (space separated)", id="extra-packages") + yield Checkbox("Build after sync", value=True, id="build") + yield Checkbox("Include external packages", value=True, id="external-pkgs") + yield Button("Run sync", id="run-sync", variant="success") + with Vertical(classes="command-panel", id="time-panel"): + yield Label("Time sync") + yield Input(placeholder="Remote host for time sync", id="time-host") + yield Input(placeholder="sudo password", password=True, id="time-password") + yield Button("Run time sync", id="run-time-sync", variant="primary") + yield Log(id="log", highlight=True) + yield Footer() + + def on_mount(self) -> None: + for button in self.query(Button): + button.can_focus = False + heading = self.query_one("#heading", Static) + heading.can_focus = True + heading.focus() + self._write_output(print_ip_table) + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "select-ip": + self.action_show_ip_table() + elif event.button.id == "select-time": + self.action_show_time_sync() + elif event.button.id == "select-sync": + self.action_show_sync() + elif event.button.id == "exit": + self.exit() + elif event.button.id == "run-time-sync": + self._write_output( + time_sync, + remote_host=self.query_one("#time-host", Input).value or "rover", + password=self.query_one("#time-password", Input).value, + ) + elif event.button.id == "run-sync": + self._run_sync() + + def action_show_ip_table(self) -> None: + self._select_command("ip-panel") + self._write_output(print_ip_table) + + def action_show_time_sync(self) -> None: + self._select_command("time-panel") + + def action_show_sync(self) -> None: + self._select_command("sync-panel") + + def _select_command(self, panel_id) -> None: + self.query_one("#log", Log).clear() + self.query_one("#command-content", ContentSwitcher).current = panel_id + selected_button = { + "ip-panel": "#select-ip", + "time-panel": "#select-time", + "sync-panel": "#select-sync", + }[panel_id] + for button in self.query("#command-rail Button"): + button.remove_class("-selected") + self.query_one(selected_button, Button).add_class("-selected") + + def action_exit(self) -> None: + self.exit() + + def _write_output(self, function, *args, **kwargs): + output = io.StringIO() + try: + with redirect_stdout(output): + function(*args, **kwargs) + except Exception as error: + output.write(f"Error: {error}\n") + self.query_one("#log", Log).write(output.getvalue()) + + def _run_sync(self): + package_list = self.query_one("#package-list", Select).value + extra_packages = self.query_one("#extra-packages", Input).value.split() + self._write_output( + sync, + self.query_one("#src-root", Input).value, + self.query_one("#dst-root", Input).value, + remote_host=self.query_one("#remote-host", Input).value, + packages=extra_packages or None, + build=self.query_one("#build", Checkbox).value, + external_pkgs=self.query_one("#external-pkgs", Checkbox).value, + package_list=package_list if package_list != Select.BLANK else None, + ) + + +def run_tui(): + RoverApp().run() \ No newline at end of file diff --git a/sync_jetson.bash b/sync_jetson.bash deleted file mode 100644 index 0f87617..0000000 --- a/sync_jetson.bash +++ /dev/null @@ -1 +0,0 @@ -python3 main.py sync --remote-host jetson@192.168.1.5 --external-pkgs --packages cameras_cpp ptz_cam rover_launchers rover_msgs rover_utils rover_manager \ No newline at end of file diff --git a/sync_pi.bash b/sync_pi.bash deleted file mode 100644 index 7c4dbc3..0000000 --- a/sync_pi.bash +++ /dev/null @@ -1 +0,0 @@ -python3 main.py sync --remote-host ubuntu@192.168.1.51 --packages comms_base_control rover_manager rover_msgs rover_utils diff --git a/sync_rv.bash b/sync_rv.bash deleted file mode 100644 index 7f44baa..0000000 --- a/sync_rv.bash +++ /dev/null @@ -1 +0,0 @@ -python3 main.py sync --remote-host rv@192.168.1.4 --external-pkgs --packages arm_hardware_interface cameras_cpp drive_control ptz_cam rover_arm_common rover_gnss rover_launchers rover_msgs rover_utils rover_manager \ No newline at end of file