We now cleantly show logs and new robots.

This commit is contained in:
Alex Davies 2024-11-06 15:44:18 -04:00
parent eb8077f8eb
commit a272fdff0f
4 changed files with 115 additions and 48 deletions

36
guiTools/poetry.lock generated
View File

@ -1,5 +1,24 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiodocker"
version = "0.23.0"
description = "A simple Docker HTTP API wrapper written with asyncio and aiohttp."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "aiodocker-0.23.0-py3-none-any.whl", hash = "sha256:8c7ff2fc9e557898ae77bc9c1af8916f269285f230aedf1abbb81436054baed4"},
{file = "aiodocker-0.23.0.tar.gz", hash = "sha256:45ede291063c7d1c24e78a766013c25e85b354a3bdcca68fe2bca64348e4dee2"},
]
[package.dependencies]
aiohttp = ">=3.8"
[package.extras]
ci = ["aiohttp (==3.10.5)", "async-timeout (==4.0.3)", "multidict (==6.0.5)", "yarl (==1.11.1)"]
dev = ["async-timeout (==4.0.3)", "codecov (==2.1.13)", "mypy (==1.11.2)", "packaging (==24.1)", "pre-commit (>=3.5.0)", "pytest (==8.3.2)", "pytest-asyncio (==0.24.0)", "pytest-cov (==5.0.0)", "pytest-sugar (==1.0.0)", "ruff (==0.6.3)", "ruff-lsp (==0.0.54)", "towncrier (==24.8.0)"]
doc = ["alabaster (==1.0.0)", "sphinx (==8.0.2)", "sphinx-autodoc-typehints (==2.4.4)", "sphinxcontrib-asyncio (==0.3.0)"]
[[package]]
name = "aiofiles"
version = "24.1.0"
@ -158,6 +177,21 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "ansi2html"
version = "1.9.2"
description = "Convert text with ANSI color codes to HTML or to LaTeX"
optional = false
python-versions = ">=3.7"
files = [
{file = "ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5"},
{file = "ansi2html-1.9.2.tar.gz", hash = "sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5"},
]
[package.extras]
docs = ["mkdocs", "mkdocs-material", "mkdocs-material-extensions", "mkdocstrings", "mkdocstrings-python", "pymdown-extensions"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "anyio"
version = "4.6.2.post1"
@ -2124,4 +2158,4 @@ propcache = ">=0.2.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "1735aa5be80786db18fc032b6442a3cc1f9dcdbf0187d207080401e87de421ce"
content-hash = "fb89def5cebe48bacf2ecf52d3a52988c2ca964a523857d34ea037ff287120c6"

View File

@ -12,6 +12,8 @@ pywebview = "^5.3.2"
loguru = "^0.7.2"
sh = "^2.1.0"
docker = "^7.1.0"
aiodocker = "^0.23.0"
ansi2html = "^1.9.2"
[build-system]
requires = ["poetry-core"]

View File

@ -1,10 +1,12 @@
from nicegui import ui, binding, app
from nicegui import ui, binding, app, run
import subprocess
from collections import defaultdict
import docker
import time
docker_client = docker.from_env()
from ansi2html import Ansi2HTMLConverter
conv = Ansi2HTMLConverter()
# Dictionary of applications: key is the button text, value is the command to execute
applications = {
@ -28,35 +30,11 @@ 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')
import aiodocker
import asyncio
@ui.page('/')
def main():
async def main():
with ui.tabs().classes('w-full') as tabs:
tab_tools = ui.tab('Tools')
tab_robots = ui.tab('Robots')
@ -68,21 +46,7 @@ def main():
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):
#Add a new robot
with ui.element():
ui.label("Add new robot")
newRobotParams = defaultdict(binding.BindableProperty)
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)
newRobotParams['sysid'] += 1
show_robots.refresh()
ui.button("Add", on_click=new_robot)
new_robot_widget = ui.element()
with ui.element():
ui.label("Debug")
def cleanup_all_containers():
@ -96,9 +60,23 @@ def main():
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()
robots_widget = ui.element()
#Add a new robot
with new_robot_widget:
ui.label("Add new robot")
newRobotParams = defaultdict(binding.BindableProperty)
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')
async def new_robot():
robot = Robot(**newRobotParams)
asyncio.tasks.create_task(robot.ui(robots_widget))
newRobotParams['sysid'] += 1
ui.button("Add", on_click=new_robot)
# Start the NiceGUI application
ui.run(title="Spiri SDK Launcher", port=8923, dark=None)

View File

@ -5,9 +5,11 @@ from typing import List
import os
import sh
import subprocess
from nicegui import ui
from nicegui import ui, run
import docker
import aiodocker
import asyncio
docker_client = docker.from_env()
@contextlib.contextmanager
@ -40,7 +42,15 @@ def modified_environ(*remove, **update):
env.update(update_after)
[env.pop(k) for k in remove_after]
robots = set()
async def container_logs(container, element):
adocker = aiodocker.Docker()
with element:
acontainer = await adocker.containers.get(container.id)
async for log in acontainer.log(stdout=True, stderr=True, follow=True):
ui.label(log)
# ui.html(conv.convert(log)
class Robot:
robot_type = "spiri-mu"
@ -52,6 +62,49 @@ class Robot:
compose_files = [ Path(file) for file in compose_files.replace("\n",",").split(",") ]
self.compose_files = compose_files
self.processes = []
robots.add(self)
async def async_stop(self):
return await run.io_bound(self.stop)
async def ui(self, element):
adocker = aiodocker.Docker()
with element:
robot_ui = ui.element()
with robot_ui:
ui.label(f"{self.robot_type}")
ui.label(f"""Sysid: {self.sysid}""")
ui.button("Start", on_click=self.start)
ui.button("Stop", on_click=self.async_stop)
async def delete_robot():
await self.async_stop()
robots.remove(self)
element.remove(robot_ui)
ui.button("Delete", on_click=delete_robot)
docker_elements = {}
container_status = {}
while True:
#Poll for data that changes
for container in self.containers():
try:
health = container.attrs['State']['Health']['Status']
except KeyError:
health = "Unknown"
container_status[container] = f"{container.name} {container.status} {health}"
if container not in docker_elements:
docker_elements[container] = ui.element()
with docker_elements[container]:
ui.label().bind_text(container_status, container)
logelement = ui.expansion("Logs").style('margin: 10px;').classes('w-full')
asyncio.create_task(container_logs(container, logelement))
#Check for containers that have been removed
removed = set(docker_elements.keys()) - set(self.containers())
for container in removed:
robot_ui.remove(docker_elements[container])
docker_elements.pop(container)
await asyncio.sleep(1)
def stop(self):
#Signal all processes to stop
@ -60,7 +113,7 @@ class Robot:
process.kill()
self.processes = []
for container in self.containers():
container.stop()
# container.stop()
container.remove(force=True)
def containers(self):