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.
|
||||
|
||||
[[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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue