Move GUI toolkit from tkinter to nicegui for easier prototyping #7
|
@ -4,6 +4,7 @@ RUN apt-get update
|
|||
RUN apt-get -y install qterminal mesa-utils \
|
||||
libgstreamer1.0-dev \
|
||||
libgstreamer-plugins-base1.0-dev \
|
||||
docker-compose \
|
||||
python3.12-venv \
|
||||
python3-pip \
|
||||
gstreamer1.0-libav \
|
||||
|
|
|
@ -369,6 +369,28 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docker"
|
||||
version = "7.1.0"
|
||||
description = "A Python library for the Docker Engine API."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"},
|
||||
{file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""}
|
||||
requests = ">=2.26.0"
|
||||
urllib3 = ">=1.26.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"]
|
||||
docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
|
||||
ssh = ["paramiko (>=2.4.3)"]
|
||||
websockets = ["websocket-client (>=1.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.21.2"
|
||||
|
@ -1483,6 +1505,33 @@ pyside2 = ["PySide2", "QtPy"]
|
|||
pyside6 = ["PySide6", "QtPy"]
|
||||
qt = ["PyQt5", "QtPy", "pyqtwebengine"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "308"
|
||||
description = "Python for Window Extensions"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"},
|
||||
{file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"},
|
||||
{file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"},
|
||||
{file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"},
|
||||
{file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"},
|
||||
{file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"},
|
||||
{file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"},
|
||||
{file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"},
|
||||
{file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"},
|
||||
{file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"},
|
||||
{file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"},
|
||||
{file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"},
|
||||
{file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"},
|
||||
{file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"},
|
||||
{file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"},
|
||||
{file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"},
|
||||
{file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"},
|
||||
{file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
|
@ -1583,6 +1632,17 @@ urllib3 = ">=1.21.1,<3"
|
|||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "sh"
|
||||
version = "2.1.0"
|
||||
description = "Python subprocess replacement"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8.1"
|
||||
files = [
|
||||
{file = "sh-2.1.0-py3-none-any.whl", hash = "sha256:bf5e44178dd96a542126c2774e9b7ab1d89bfe0e2ef84d92e6d0ed7358d63d01"},
|
||||
{file = "sh-2.1.0.tar.gz", hash = "sha256:7e27301c574bec8ca5bf6f211851357526455ee97cd27a7c4c6cc5e2375399cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-websocket"
|
||||
version = "1.1.0"
|
||||
|
@ -2064,4 +2124,4 @@ propcache = ">=0.2.0"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "941bf4750c3b8a3045cfd01bb1ab4af7c1e8fd2d5fa54cd85d8ac3a0c8837238"
|
||||
content-hash = "1735aa5be80786db18fc032b6442a3cc1f9dcdbf0187d207080401e87de421ce"
|
||||
|
|
|
@ -10,6 +10,8 @@ python = "^3.11"
|
|||
nicegui = "^2.5.0"
|
||||
pywebview = "^5.3.2"
|
||||
loguru = "^0.7.2"
|
||||
sh = "^2.1.0"
|
||||
docker = "^7.1.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
from nicegui import ui, binding, app
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
import docker
|
||||
import time
|
||||
|
||||
docker_client = docker.from_env()
|
||||
|
||||
# Dictionary of applications: key is the button text, value is the command to execute
|
||||
applications = {
|
||||
|
@ -25,6 +29,31 @@ ui.label("Spiri Robotics SDK").style('font-size: 40px; margin-bottom: 10px;').cl
|
|||
robots = []
|
||||
from spiri_sdk_guitools.sim_drone import Robot
|
||||
|
||||
@ui.refreshable
|
||||
def show_robots():
|
||||
|
||||
with ui.element().classes('w-full'):
|
||||
for robot in robots:
|
||||
@ui.refreshable
|
||||
def container_status(robot):
|
||||
ui.label("Containers")
|
||||
for container in robot.containers():
|
||||
ui.label(f"{container.name} {container.status}")
|
||||
with ui.card().style('margin: 10px;').classes('w-full'):
|
||||
ui.label(f"{robot.robot_type}")
|
||||
ui.label(f"""Sysid: {robot.sysid}""")
|
||||
ui.button("Start", on_click=robot.start)
|
||||
ui.button("Stop", on_click=robot.stop)
|
||||
def delete_robot():
|
||||
robot.stop()
|
||||
robots.remove(robot)
|
||||
show_robots.refresh()
|
||||
ui.button("Delete", on_click=delete_robot)
|
||||
with ui.expansion("Details").style('margin: 10px;').classes('w-full'):
|
||||
container_status(robot)
|
||||
ui.timer(1, container_status.refresh)
|
||||
logwidget = ui.expansion("Logs").style('margin: 10px;').classes('w-full')
|
||||
|
||||
@ui.page('/')
|
||||
def main():
|
||||
|
||||
|
@ -32,28 +61,44 @@ def main():
|
|||
tab_tools = ui.tab('Tools')
|
||||
tab_robots = ui.tab('Robots')
|
||||
# two = ui.tab('Two')
|
||||
with ui.tab_panels(tabs, value=tab_tools).classes():
|
||||
with ui.tab_panels(tabs, value=tab_tools).classes("w-full"):
|
||||
with ui.tab_panel(tab_tools):
|
||||
# Create and place buttons dynamically based on the dictionary
|
||||
with ui.grid(columns=3):
|
||||
for app_name, command in applications.items():
|
||||
ui.button(app_name, on_click=lambda cmd=command: launch_app(cmd)).style('width: 150px; height: 50px; margin: 5px;')
|
||||
with ui.tab_panel(tab_robots):
|
||||
with ui.grid(columns=3):
|
||||
for robot in robots:
|
||||
with ui.row():
|
||||
ui.label(f"Robot {robot.sys_id}")
|
||||
ui.button(f"Start Robot {robot.sys_id}", on_click=lambda: robot.start()).style('width: 150px; height: 50px; margin: 5px;')
|
||||
|
||||
#Add a new robot
|
||||
with ui.element():
|
||||
ui.label("Add new robot")
|
||||
newRobotParams = defaultdict(binding.BindableProperty)
|
||||
ui.number(value=0, label="SysID").bind_value(newRobotParams, 'sysid')
|
||||
ui.input(value="./sdk/docker-compose.yml", label="Compose files (comma seperated)").bind_value(newRobotParams, 'compose_files')
|
||||
ui.number(value=1, label="SysID", min=1, max=254,
|
||||
).bind_value(newRobotParams, 'sysid')
|
||||
ui.textarea(value="./sdk/docker-compose.yml", label="Compose files (comma or newline seperated)").bind_value(newRobotParams, 'compose_files')
|
||||
def new_robot():
|
||||
robot = Robot(**newRobotParams)
|
||||
# robot.start()
|
||||
robots.append(robot)
|
||||
ui.label(str(robots))
|
||||
newRobotParams['sysid'] += 1
|
||||
show_robots.refresh()
|
||||
ui.button("Add", on_click=new_robot)
|
||||
with ui.element():
|
||||
ui.label("Debug")
|
||||
def cleanup_all_containers():
|
||||
#Find all containers that start with spiri-sdk
|
||||
containers = docker_client.containers.list(all=True)
|
||||
removal_count = 0
|
||||
for container in containers:
|
||||
if container.name.startswith("robot-sim-"):
|
||||
container.remove(force=True)
|
||||
removal_count += 1
|
||||
ui.label(f"Removed {removal_count} containers")
|
||||
ui.button("Cleanup all containers", on_click=cleanup_all_containers)
|
||||
|
||||
|
||||
ui.separator()
|
||||
ui.label("Current robots")
|
||||
robotwidget = show_robots()
|
||||
# Start the NiceGUI application
|
||||
ui.run(title="Spiri SDK Launcher", port=8923, dark=None)
|
||||
|
|
|
@ -2,6 +2,13 @@ from loguru import logger
|
|||
from pathlib import Path
|
||||
import contextlib
|
||||
from typing import List
|
||||
import os
|
||||
import sh
|
||||
import subprocess
|
||||
from nicegui import ui
|
||||
|
||||
import docker
|
||||
docker_client = docker.from_env()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def modified_environ(*remove, **update):
|
||||
|
@ -34,18 +41,38 @@ def modified_environ(*remove, **update):
|
|||
[env.pop(k) for k in remove_after]
|
||||
|
||||
|
||||
|
||||
class Robot:
|
||||
def __init__(self, sysid: int, compose_files: List[Path]):
|
||||
robot_type = "spiri-mu"
|
||||
def __init__(self, sysid: int, compose_files: List[Path] | str ):
|
||||
if sysid > 255 or sysid < 0:
|
||||
raise ValueError("sysid must be between 0 and 255")
|
||||
self.sysid = int(sysid)
|
||||
if isinstance(compose_files, str):
|
||||
compose_files = [ Path(file) for file in compose_files.replace("\n",",").split(",") ]
|
||||
self.compose_files = compose_files
|
||||
self.processes = []
|
||||
|
||||
def stop(self):
|
||||
#Signal all processes to stop
|
||||
for process in self.processes:
|
||||
process.terminate()
|
||||
process.kill()
|
||||
self.processes = []
|
||||
for container in self.containers():
|
||||
container.stop()
|
||||
container.remove(force=True)
|
||||
|
||||
def containers(self):
|
||||
return docker_client.containers.list(all=True, filters={"name": f"robot-sim-{self.robot_type}-{self.sysid}"})
|
||||
|
||||
def start(self):
|
||||
"""Starts the simulated drone with a given sys_id,
|
||||
"""Starts the simulated drone with a given sysid,
|
||||
each drone must have it's own unique ID.
|
||||
"""
|
||||
instance = self.sysid-1
|
||||
with logger.contextualize(syd_id=sys_id):
|
||||
sysid = self.sysid
|
||||
with logger.contextualize(syd_id=sysid):
|
||||
env = os.environ
|
||||
with modified_environ(
|
||||
SERIAL0_PORT=str(int(env["SERIAL0_PORT"]) + 10 * instance),
|
||||
|
@ -54,17 +81,30 @@ class Robot:
|
|||
FDM_PORT_IN=str(int(env["FDM_PORT_IN"]) + 10 * instance),
|
||||
SITL_PORT=str(int(env["SITL_PORT"]) + 10 * instance),
|
||||
INSTANCE=str(instance),
|
||||
DRONE_SYS_ID=str(self.sys_id),
|
||||
DRONE_SYS_ID=str(self.sysid),
|
||||
):
|
||||
logger.info("Starting drone stack, this may take some time")
|
||||
docker_stack = sh.docker.compose(
|
||||
"--profile",
|
||||
"uav-sim",
|
||||
"-p",
|
||||
f"spiri-sdk-{sys_id}",
|
||||
"up",
|
||||
_out=outputLogger("docker_stack", sys_id),
|
||||
_err=sys.stderr,
|
||||
_bg=True,
|
||||
)
|
||||
self.processes.append(docker_stack)
|
||||
for compose_file in self.compose_files:
|
||||
if not isinstance(compose_file, Path):
|
||||
compose_file = Path(compose_file)
|
||||
if not compose_file.exists():
|
||||
raise FileNotFoundError(f"File {compose_file} does not exist")
|
||||
ui.label(f"Starting drone stack {compose_file}")
|
||||
args = [
|
||||
"docker-compose",
|
||||
"--profile",
|
||||
"uav-sim",
|
||||
"-p",
|
||||
f"robot-sim-{self.robot_type}-{sysid}",
|
||||
"-f",
|
||||
compose_file.as_posix(),
|
||||
"up",
|
||||
]
|
||||
command = " ".join(args)
|
||||
|
||||
logger.info(f"Starting drone stack with command: {command}")
|
||||
docker_stack = subprocess.Popen(
|
||||
args,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
|
|
@ -109,7 +109,7 @@ def start(instance: int = 0, sys_id: int = 1):
|
|||
"--profile",
|
||||
"uav-sim",
|
||||
"-p",
|
||||
f"spiri-sdk-{sys_id}",
|
||||
f"robot-sim-{sys_id}",
|
||||
"up",
|
||||
_out=outputLogger("docker_stack", sys_id),
|
||||
_err=sys.stderr,
|
||||
|
|
Loading…
Reference in New Issue