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. # 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]] [[package]]
name = "aiofiles" name = "aiofiles"
version = "24.1.0" version = "24.1.0"
@ -158,6 +177,21 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, {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]] [[package]]
name = "anyio" name = "anyio"
version = "4.6.2.post1" version = "4.6.2.post1"
@ -2124,4 +2158,4 @@ propcache = ">=0.2.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "1735aa5be80786db18fc032b6442a3cc1f9dcdbf0187d207080401e87de421ce" content-hash = "fb89def5cebe48bacf2ecf52d3a52988c2ca964a523857d34ea037ff287120c6"

View File

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

View File

@ -1,10 +1,12 @@
from nicegui import ui, binding, app from nicegui import ui, binding, app, run
import subprocess import subprocess
from collections import defaultdict from collections import defaultdict
import docker import docker
import time import time
docker_client = docker.from_env() 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 # Dictionary of applications: key is the button text, value is the command to execute
applications = { applications = {
@ -28,35 +30,11 @@ ui.label("Spiri Robotics SDK").style('font-size: 40px; margin-bottom: 10px;').cl
robots = [] robots = []
from spiri_sdk_guitools.sim_drone import Robot from spiri_sdk_guitools.sim_drone import Robot
import aiodocker
@ui.refreshable import asyncio
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('/') @ui.page('/')
def main(): async def main():
with ui.tabs().classes('w-full') as tabs: with ui.tabs().classes('w-full') as tabs:
tab_tools = ui.tab('Tools') tab_tools = ui.tab('Tools')
tab_robots = ui.tab('Robots') tab_robots = ui.tab('Robots')
@ -68,21 +46,7 @@ def main():
for app_name, command in applications.items(): 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;') 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.tab_panel(tab_robots):
new_robot_widget = ui.element()
#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)
with ui.element(): with ui.element():
ui.label("Debug") ui.label("Debug")
def cleanup_all_containers(): def cleanup_all_containers():
@ -96,9 +60,23 @@ def main():
ui.label(f"Removed {removal_count} containers") ui.label(f"Removed {removal_count} containers")
ui.button("Cleanup all containers", on_click=cleanup_all_containers) ui.button("Cleanup all containers", on_click=cleanup_all_containers)
ui.separator() ui.separator()
ui.label("Current robots") 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 # Start the NiceGUI application
ui.run(title="Spiri SDK Launcher", port=8923, dark=None) ui.run(title="Spiri SDK Launcher", port=8923, dark=None)

View File

@ -5,9 +5,11 @@ from typing import List
import os import os
import sh import sh
import subprocess import subprocess
from nicegui import ui from nicegui import ui, run
import docker import docker
import aiodocker
import asyncio
docker_client = docker.from_env() docker_client = docker.from_env()
@contextlib.contextmanager @contextlib.contextmanager
@ -40,7 +42,15 @@ def modified_environ(*remove, **update):
env.update(update_after) env.update(update_after)
[env.pop(k) for k in remove_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: class Robot:
robot_type = "spiri-mu" robot_type = "spiri-mu"
@ -52,6 +62,49 @@ class Robot:
compose_files = [ Path(file) for file in compose_files.replace("\n",",").split(",") ] compose_files = [ Path(file) for file in compose_files.replace("\n",",").split(",") ]
self.compose_files = compose_files self.compose_files = compose_files
self.processes = [] 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): def stop(self):
#Signal all processes to stop #Signal all processes to stop
@ -60,7 +113,7 @@ class Robot:
process.kill() process.kill()
self.processes = [] self.processes = []
for container in self.containers(): for container in self.containers():
container.stop() # container.stop()
container.remove(force=True) container.remove(force=True)
def containers(self): def containers(self):