We now cleantly show logs and new robots.
This commit is contained in:
parent
eb8077f8eb
commit
a272fdff0f
|
@ -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"
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue