Compare commits

...

44 Commits

Author SHA1 Message Date
Alex Davies 9298a57211 Update README.md
Build Docs / build (push) Failing after 27s Details
2024-11-19 11:53:08 -04:00
Burak Ozter 8c8e889571 Merge pull request 'feature/multiple-compose' (#11) from feature/multiple-compose into master
Build Docs / build (push) Failing after 3m53s Details
Reviewed-on: #11
2024-11-18 14:27:31 -04:00
Burak Ozter 3af11792a4 Merge remote-tracking branch 'origin' into feature/multiple-compose
Build Docs / build (push) Has been cancelled Details
2024-11-18 14:20:36 -04:00
Alex Davies 41493f935c Made detail widget uneditable
Build Docs / build (push) Failing after 49s Details
2024-11-15 13:15:09 -04:00
Alex Davies 271366bf7f Made robot container header larger
Build Docs / build (push) Has been cancelled Details
2024-11-15 13:14:26 -04:00
Alex Davies 43fead1589 Added more useful docker tools
Build Docs / build (push) Failing after 45s Details
2024-11-15 13:11:47 -04:00
Alex Davies af170070ab Simplified ros topic filter
Build Docs / build (push) Failing after 44s Details
2024-11-15 12:59:31 -04:00
Alex Davies f035a09321 Update for more reliable compose
Build Docs / build (push) Failing after 49s Details
2024-11-15 12:52:27 -04:00
Burak Ozter e15b036720 add missing apt package ros-gz-image
Build Docs / build (push) Failing after 49s Details
2024-11-15 11:35:47 -04:00
Alex Davies 980f0125f9 Merge pull request 'feature/multiple-compose' (#10) from feature/multiple-compose into master
Build Docs / build (push) Failing after 1m28s Details
Reviewed-on: #10
2024-11-15 11:17:51 -04:00
Burak Ozter 308c7087a2 use cyclone_dds for image bridge and use image_bridge command
Build Docs / build (push) Failing after 52s Details
2024-11-14 19:15:12 -04:00
Alex Davies d5c0b68ceb Also show model topics in rot_topics tab
Build Docs / build (push) Failing after 45s Details
2024-11-14 13:57:35 -04:00
Alex Davies 5bebe95492 Fixed virtual camera
Build Docs / build (push) Failing after 46s Details
2024-11-14 13:54:30 -04:00
Alex Davies cb290395aa Enable straming now takes robot name
Build Docs / build (push) Failing after 52s Details
2024-11-14 13:46:50 -04:00
Alex Davies 10d6bd0c8b Added virtual camera
Build Docs / build (push) Failing after 43s Details
2024-11-14 13:39:43 -04:00
Alex Davies e670e38ba1 Added logger catch so adding robot twice doesn't throw a real error
Build Docs / build (push) Failing after 58s Details
2024-11-14 13:36:46 -04:00
Alex Davies b5d96dc508 Show launch command
Build Docs / build (push) Failing after 48s Details
2024-11-14 12:27:53 -04:00
Alex Davies 2a9c5527f3 Add ros bridge with virtual camera
Build Docs / build (push) Failing after 52s Details
2024-11-14 11:58:06 -04:00
Alex Davies c80d37373b Better support for multiple docker images, more clear ros topic and
Build Docs / build (push) Failing after 46s Details
robot names
2024-11-14 11:46:00 -04:00
Alex Davies 0572d4ca1c Merge branch 'feature/webui-dev' of https://git.spirirobotics.com/Spiri/spiri-sdk
Build Docs / build (push) Failing after 46s Details
2024-11-14 10:31:59 -04:00
Burak Ozter 1dd84706a5 Merge pull request 'Run ui start in thread' (#9) from bugfix/ui-freezes-on-start into feature/webui-dev
Reviewed-on: #9
2024-11-08 09:51:28 -04:00
Alex Davies 98aafbb5e9 Merge pull request 'Move GUI toolkit from tkinter to nicegui for easier prototyping' (#7) from feature/webui into master
Build Docs / build (push) Failing after 41s Details
Reviewed-on: #7
2024-11-07 13:13:32 -04:00
Alex Davies a8810dea8a Merge branch 'ros2'
Build Docs / build (push) Failing after 51s Details
2024-11-05 10:27:49 -04:00
Alex Davies bdd55885ad Merge branch 'ros2' of https://git.spirirobotics.com/Spiri/spiri-sdk into ros2 2024-11-05 10:27:27 -04:00
Alex Davies 5ca555ffd4 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 30s Details
2024-10-19 14:05:01 -03:00
Alex Davies 75f60fa811 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 33s Details
2024-10-19 14:03:20 -03:00
Alex Davies 86460fa1d5 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 31s Details
2024-10-19 13:51:21 -03:00
Alex Davies 8e45ae3440 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 4s Details
2024-10-19 13:43:08 -03:00
Alex Davies 6ec9b219d7 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 35s Details
2024-10-19 13:41:31 -03:00
Alex Davies 11bc1200c9 Update .github/workflows/build-docs.yaml
Build Docs / build (push) Failing after 32s Details
2024-10-19 13:16:04 -03:00
Alex Davies a5b7541452 Fixed docs path
Build Docs / build (push) Failing after 32s Details
2024-10-19 13:10:19 -03:00
Alex Davies 63520ed0c8 Change doc path
Build Docs / build (push) Failing after 32s Details
2024-10-19 13:09:12 -03:00
Alex Davies f0ead63a6d Use new WRITE_TOKEN
Build Docs / build (push) Failing after 32s Details
2024-10-19 13:06:48 -03:00
Alex Davies 9d7fdde370 Test pages workflow
Build Docs / build (push) Failing after 32s Details
2024-10-19 13:04:25 -03:00
Alex Davies 88f6c46213 Build for github pages
Build Docs / build (push) Failing after 36s Details
2024-10-19 13:00:23 -03:00
Alex Davies 5c71841d12 Bump README
Build Docs / build (push) Successful in 30s Details
2024-10-16 14:07:01 -03:00
Alex Davies aba8b6c1f0 Include "instructions" on nvidia drivers
Build Docs / build (push) Successful in 32s Details
2024-10-16 14:01:31 -03:00
Alex Davies 76b42682c9 Update docs
Build Docs / build (push) Successful in 29s Details
2024-10-16 12:55:18 -03:00
Alex Davies 0cfd7cc78a feat: Enable syntax highlighting for PDFs in Sphinx configuration 2024-10-16 11:48:39 -03:00
Alex Davies 9fe800df0f docs: Update README with installation instructions and project overview 2024-10-16 11:47:57 -03:00
Alex Davies 24430eb69a feat: Configure LaTeX to show URLs as footnotes in Sphinx 2024-10-16 11:45:26 -03:00
Alex Davies b61a389b92 feat: Include README.md in Sphinx documentation with myst-parser 2024-10-16 10:52:35 -03:00
Alex Davies 14c3358e81 docs: Include README.md in the documentation index file 2024-10-16 10:52:34 -03:00
Alex Davies 16f49d4a3f Update docs
Build Docs / build (push) Successful in 29s Details
2024-10-16 09:13:46 -03:00
18 changed files with 368 additions and 244 deletions

5
.copier/answers.docs.yml Normal file
View File

@ -0,0 +1,5 @@
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
_commit: v1.0.2
_src_path: https://git.spirirobotics.com/Spiri/template-docs.git
author_name: Spiri Robotics
project_name: spiri-sdk

52
.github/workflows/build-docs.yaml vendored Normal file
View File

@ -0,0 +1,52 @@
name: Build Docs
on:
push:
env:
REGISTRY: git.spirirobotics.com
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
container: sphinxdoc/sphinx-latexpdf
steps:
- name: Install sphinx-rtd-theme
run: pip install sphinx-rtd-theme myst-parser
- name: Install node so that custom actions work.
run : apt-get update && apt-get --yes install nodejs git
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build Docs
run: make html latexpdf
working-directory: docs # assuming your documentation is in a 'docs' folder
- name: Save PDF Artifacts
run: mv docs/build/latex/*.pdf ${{ github.workspace }}/docs.pdf
- name: Compress HTML
run: tar -czvf docs_html.tar.gz -C docs/build/html .
- name: Upload Docs
uses: actions/upload-artifact@v3
with:
name: docs_html.tar.gz
path: docs_html.tar.gz
- name: Upload PDF
uses: actions/upload-artifact@v3
with:
name: docs.pdf
path: docs.pdf
- name: Deploy
uses: s0/git-publish-subdir-action@develop
env:
REPO: http://git-spirirobotics-com-app:3000/Spiri/spiri-sdk.git/
BRANCH: gh-pages
FOLDER: ./docs/build/html
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
docs/build/ docs/build/
.vscode .vscode
*.pyc *.pyc

View File

@ -72,28 +72,6 @@ sudo nvidia-ctk runtime configure --runtime=docker --cdi.enabled
sudo systemctl restart docker sudo systemctl restart docker
``` ```
##### Rootless Mode
To configure the container runtime for Docker running in [Rootless mode](https://docs.docker.com/engine/security/rootless/), follow these steps:
1. Configure the container runtime by using the nvidia-ctk command:
```bash
nvidia-ctk runtime configure --runtime=docker --config=$HOME/.config/docker/daemon.json
```
2. Restart the Rootless Docker daemon
```bash
systemctl --user restart docker
```
3. Configure /etc/nvidia-container-runtime/config.toml by using the sudo nvidia-ctk command:
```bash
sudo nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place
```
### Sample Workload ### Sample Workload
After you install and configure the toolkit and install an NVIDIA GPU Driver, you can verify your installation by running a sample workload. After you install and configure the toolkit and install an NVIDIA GPU Driver, you can verify your installation by running a sample workload.

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

11
docs/README.md Normal file
View File

@ -0,0 +1,11 @@
If you have a correctly configured sphinx environment you can build this project
using `make html latexpdf`.
You can also use nektos/act to build this project in the same way our build does.
```bash
cd ../ #Make sure you're in the project root, you should have a hidden folder
# named ./.github/workflows available.
act --artifact-server-path ./doc-build
```
Your compiled doc project will now be in the ./doc-build folder.

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -0,0 +1,34 @@
.wy-side-nav-search {
background: #FFFFFF !important;
}
.wy-nav-side {
background-color: #FFFFFF !important;
}
/* Add borders and box-shadow */
.wy-side-nav {
border: 1px solid #899CA3 !important;
}
.logo {
width: 100px !important;
}
.wy-menu-vertical a {
color: #899CA3 !important; /* Change to your desired color */
}
.document-title {
color: #000 !important;
font-size: 24px !important;
text-transform: uppercase !important;
}
.icon-home {
font-weight: bold !important;
text-transform: uppercase !important;
color: #000 !important;
}

70
docs/source/conf.py Normal file
View File

@ -0,0 +1,70 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import sphinx_rtd_theme
project = "spiri-sdk"
copyright = "2024, Spiri Robotics"
author = "Spiri Robotics"
html_logo = "logos/SPIRI_STLockup_Mixed_RGB.png" # For HTML output
html_logo_width = '200px'
latex_logo = "logos/SPIRI_STLockup_Mixed_RGB.png"
latex_logo_width = '5cm'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"myst_parser",
"sphinx.ext.duration",
"sphinx.ext.doctest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
]
numfig = True
todo_include_todos = True
todo_emit_warnings = True
todo_link_only = True
templates_path = ["_templates"]
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_static_path = ['_static']
html_css_files = [
'custom.css',
]
html_theme_options = {
'collapse_navigation': True,
'sticky_navigation': True,
'navigation_depth': 4, #could be set to -1 if we want unlimited depth
'includehidden': True,
'titles_only': False
}
latex_engine = "xelatex"
# Configure LaTeX options for PDF generation
latex_show_urls = 'footnote'
# Enable syntax highlighting for PDFs
pygments_style = 'sphinx'

21
docs/source/index.rst Normal file
View File

@ -0,0 +1,21 @@
.. spiri-sdk documentation master file, created by
sphinx-quickstart on Wed Feb 14 11:51:45 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to spiri-sdk's documentation!
============================================
.. toctree::
:maxdepth: 2
:caption: Contents:
.. include:: ../../README.md
:parser: myst_parser.sphinx_
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -68,9 +68,22 @@ async def main():
newRobotParams = defaultdict(binding.BindableProperty) newRobotParams = defaultdict(binding.BindableProperty)
ui.number(value=1, label="SysID", min=1, max=254, ui.number(value=1, label="SysID", min=1, max=254,
).bind_value(newRobotParams, 'sysid') ).bind_value(newRobotParams, 'sysid')
ui.textarea(value="/robots/spiri-mu/core/docker-compose.yaml", label="Compose files (comma or newline seperated)").bind_value(newRobotParams, 'compose_files') default_robot_compose = (
"/robots/spiri-mu/core/docker-compose.yaml\n"
"#/robots/spiri-mu/virtual_camera/docker-compose.yaml --build"
)
ui.label("Compose files").classes("text-xl")
ui.codemirror(value=default_robot_compose, language="bash", theme="basicDark").bind_value(newRobotParams, 'compose_files')
async def new_robot(): async def new_robot():
robot = Robot(**newRobotParams) compose_files = []
#Split on comma or newline, and remove comments
for line in newRobotParams['compose_files'].split('\n'):
line = line.split('#')[0].strip()
if line:
compose_files.append(line)
current_robot = newRobotParams.copy()
current_robot['compose_files'] = compose_files
robot = Robot(**current_robot)
asyncio.tasks.create_task(robot.ui(robots_widget)) asyncio.tasks.create_task(robot.ui(robots_widget))
newRobotParams['sysid'] += 1 newRobotParams['sysid'] += 1

View File

@ -6,6 +6,7 @@ import os
import sh import sh
import subprocess import subprocess
from nicegui import ui, run, app from nicegui import ui, run, app
import yaml
import docker import docker
import aiodocker import aiodocker
@ -73,7 +74,7 @@ async def container_logs(container, element):
class Robot: class Robot:
robot_type = "spiri-mu" robot_type = "spiri_mu"
def __init__(self, sysid: int, compose_files: List[Path] | str): def __init__(self, sysid: int, compose_files: List[Path] | str):
if sysid > 255 or sysid < 0: if sysid > 255 or sysid < 0:
@ -87,40 +88,56 @@ class Robot:
self.processes = [] self.processes = []
self.video_button = None self.video_button = None
robots.add(self) robots.add(self)
#Ros doesn't like dashes in node names
self.robot_name = f"{self.robot_type}_{self.sysid}".replace("-","_")
self.world_name = "citadel_hill"
async def ui_containers(self, element): async def ui_containers(self, element):
docker_elements = {} docker_elements = {}
container_status = {} container_status = {}
with element: with element:
while True: while True:
# Poll for data that changes with logger.catch():
for container in self.containers(): # Poll for data that changes
try: for container in self.containers():
health = container.attrs["State"]["Health"]["Status"] try:
except KeyError: health = container.attrs["State"]["Health"]["Status"]
health = "Unknown" except KeyError:
health = "Unknown"
container_status[container] = ( container_status[container] = (
f"{container.name} {container.status} {health}" f"{container.name} {container.status} {health}"
) )
if container not in docker_elements: if container not in docker_elements:
docker_elements[container] = ui.element().classes("w-full") docker_elements[container] = ui.element().classes("w-full")
with docker_elements[container]: with docker_elements[container]:
ui.label().bind_text(container_status, container).classes( ui.label().bind_text(container_status, container).classes(
"text-lg" "text-2xl"
) )
logelement = ( #Show the command the container is running
ui.expansion("Logs") # ui.label(container.attrs["Config"]["Cmd"])
.style("margin: 10px;") cmd_widget = ui.codemirror(" ".join(container.attrs["Config"]["Cmd"]), language="bash",theme="basicDark").classes('h-auto max-h-32')
.classes("w-full outline outline-1") cmd_widget.enabled = False
)
asyncio.create_task(container_logs(container, logelement)) with ui.expansion("Env Variables").classes("w-full outline outline-1").style("margin: 10px;"):
# Check for containers that have been removed env_widget = ui.codemirror("\n".join(container.attrs["Config"]["Env"]), language="bash",theme="basicDark")
removed = set(docker_elements.keys()) - set(self.containers()) env_widget.enabled = False
for container in removed:
self.robot_ui.remove(docker_elements[container]) logelement = (
docker_elements.pop(container) ui.expansion("Logs")
await asyncio.sleep(1) .style("margin: 10px;")
.classes("w-full outline outline-1")
)
asyncio.create_task(container_logs(container, logelement))
with ui.expansion("Full details").classes("w-full outline outline-1").style("margin: 10px;"):
details_widget = ui.codemirror(yaml.dump(container.attrs), language="yaml",theme="basicDark")
details_widget.enabled = False
# Check for containers that have been removed
removed = set(docker_elements.keys()) - set(self.containers())
for container in removed:
self.robot_ui.remove(docker_elements[container])
docker_elements.pop(container)
await asyncio.sleep(1)
async def ui_ros(self, element): async def ui_ros(self, element):
with element: with element:
@ -129,8 +146,10 @@ class Robot:
with scroll_area: with scroll_area:
while True: while True:
scroll_area.clear() scroll_area.clear()
#Filter for topics that start with self.robot_name
for topic in node_dummy.get_topic_names_and_types(): for topic in node_dummy.get_topic_names_and_types():
ui.label(topic) if self.robot_name in topic[0]:
ui.label(topic[0])
await asyncio.sleep(10) await asyncio.sleep(10)
async def ui(self, element): async def ui(self, element):
@ -143,7 +162,7 @@ class Robot:
ui.label(f"""Sysid: {self.sysid}""") ui.label(f"""Sysid: {self.sysid}""")
ui.button("Start", on_click=self.async_start).classes("m-2") ui.button("Start", on_click=self.async_start).classes("m-2")
ui.button("Stop", on_click=self.async_stop).classes("m-2") ui.button("Stop", on_click=self.async_stop).classes("m-2")
self.video_button = EnableStreamingButton(sysid=self.sysid).classes( self.video_button = EnableStreamingButton(robot_name=self.robot_name).classes(
"m-2" "m-2"
) )
@ -159,7 +178,6 @@ class Robot:
with ui.tab_panels(tabs, value=tab_containers): with ui.tab_panels(tabs, value=tab_containers):
tab = ui.tab_panel(tab_containers).classes("w-full") tab = ui.tab_panel(tab_containers).classes("w-full")
asyncio.create_task(self.ui_containers(tab)) asyncio.create_task(self.ui_containers(tab))
with ui.tab_panels(tabs, value=tab_ros):
tab = ui.tab_panel(tab_ros).classes("w-full") tab = ui.tab_panel(tab_ros).classes("w-full")
asyncio.create_task(self.ui_ros(tab)) asyncio.create_task(self.ui_ros(tab))
@ -172,7 +190,7 @@ class Robot:
if isinstance(self.video_button, EnableStreamingButton): if isinstance(self.video_button, EnableStreamingButton):
self.video_button.stop_video() self.video_button.stop_video()
# Delete gazebo model # Delete gazebo model
self.delete_gz_model(sysid=self.sysid) self.delete_gz_model()
# Signal all processes to stop # Signal all processes to stop
for process in self.processes: for process in self.processes:
process.terminate() process.terminate()
@ -184,7 +202,7 @@ class Robot:
def containers(self): def containers(self):
return docker_client.containers.list( return docker_client.containers.list(
all=True, filters={"name": f"robot-sim-{self.robot_type}-{self.sysid}"} all=True, filters={"name": f"robot-sim-{self.robot_name}"}
) )
async def async_start(self): async def async_start(self):
@ -207,35 +225,47 @@ class Robot:
SITL_PORT=str(int(env["SITL_PORT"]) + 10 * instance), SITL_PORT=str(int(env["SITL_PORT"]) + 10 * instance),
INSTANCE=str(instance), INSTANCE=str(instance),
DRONE_SYS_ID=str(self.sysid), DRONE_SYS_ID=str(self.sysid),
ROBOT_NAME=self.robot_name,
WORLD_NAME="citadel_hill",
): ):
self.spawn_gz_model(self.sysid) self.spawn_gz_model()
logger.info("Starting drone stack, this may take some time") logger.info("Starting drone stack, this may take some time")
for compose_file in self.compose_files: for compose_file in self.compose_files:
arguments = compose_file.split(" ")
arguments = [arg.strip() for arg in arguments]
compose_file = arguments[0]
arguments = arguments[1:]
if not isinstance(compose_file, Path): if not isinstance(compose_file, Path):
compose_file = Path(compose_file) compose_file = Path(compose_file)
if not compose_file.exists(): if not compose_file.exists():
raise FileNotFoundError(f"File {compose_file} does not exist") raise FileNotFoundError(f"File {compose_file} does not exist")
#Get the folder the compose file is in
compose_folder = compose_file.parent
args = [ args = [
"docker-compose", "docker-compose",
"--profile", "--profile",
"uav-sim", "uav-sim",
"-p", "-p",
f"robot-sim-{self.robot_type}-{sysid}", f"robot-sim-{self.robot_name}-{compose_folder.name}",
"-f", "-f",
compose_file.as_posix(), compose_file.as_posix(),
"up", "up",
*arguments,
] ]
command = " ".join(args) command = " ".join(args)
logger.info(f"Starting drone stack with command: {command}") logger.info(f"Starting drone stack with command: {command}")
docker_stack = subprocess.Popen( docker_stack = subprocess.Popen(
args, args,
stdout=subprocess.PIPE, # stdout=subprocess.PIPE,
stderr=subprocess.PIPE, # stderr=subprocess.PIPE,
) )
logger.info(f"Started drone stack with PID: {docker_stack.pid}")
@staticmethod @logger.catch
def spawn_gz_model(sysid): def spawn_gz_model(self):
sysid = self.sysid
logger.info("") logger.info("")
env = os.environ env = os.environ
GSTREAMER_UDP_PORT = env["GSTREAMER_UDP_PORT"] GSTREAMER_UDP_PORT = env["GSTREAMER_UDP_PORT"]
@ -252,7 +282,7 @@ class Robot:
] ]
# This path is breaking if this drone_model folder doesnt exist! # This path is breaking if this drone_model folder doesnt exist!
# TODO: fix this model path for minimal code maintenance # TODO: fix this model path for minimal code maintenance
ROS2_CMD = f"ros2 run ros_gz_sim create -world {WORLD_NAME} -file /ardupilot_gazebo/models/{DRONE_MODEL}/model.sdf -name spiri-{sysid} -x {sysid - 1} -y 0 -z 0.195" ROS2_CMD = f"ros2 run ros_gz_sim create -world {WORLD_NAME} -file /ardupilot_gazebo/models/{DRONE_MODEL}/model.sdf -name {self.robot_name} -x {sysid - 1} -y 0 -z 0.195"
xacro_proc = subprocess.Popen( xacro_proc = subprocess.Popen(
XACRO_CMD, XACRO_CMD,
@ -269,13 +299,14 @@ class Robot:
ros2_gz_create_proc.kill() ros2_gz_create_proc.kill()
return return
@staticmethod @logger.catch
def delete_gz_model(sysid): def delete_gz_model(self):
sysid = self.sysid
env = os.environ env = os.environ
WORLD_NAME = env["WORLD_NAME"] WORLD_NAME = env["WORLD_NAME"]
# http://osrf-distributions.s3.amazonaws.com/gazebo/api/7.1.0/classgazebo_1_1physics_1_1Entity.html # http://osrf-distributions.s3.amazonaws.com/gazebo/api/7.1.0/classgazebo_1_1physics_1_1Entity.html
ENTITY_TYPE_MODEL = 0x00000002 ENTITY_TYPE_MODEL = 0x00000002
REQUEST_ARG = f"name: 'spiri-{sysid}' type: {ENTITY_TYPE_MODEL}" REQUEST_ARG = f"name: '{self.robot_name}' type: {ENTITY_TYPE_MODEL}"
GZ_SERVICE_CMD = [ GZ_SERVICE_CMD = [
"gz", "gz",
"service", "service",

View File

@ -4,13 +4,13 @@ from loguru import logger
import os import os
GZ_TOPIC_INFO = ["gz", "topic", "-i", "-t"] GZ_TOPIC_INFO = ["gz", "topic", "-i", "-t"]
ENABLE_STREAMING_TOPIC = "/world/{world_name}/model/spiri-{sysid}/link/pitch_link/sensor/camera/image/enable_streaming" ENABLE_STREAMING_TOPIC = "/world/{world_name}/model/{robot_name}/link/pitch_link/sensor/camera/image/enable_streaming"
class EnableStreamingButton(ui.element): class EnableStreamingButton(ui.element):
def __init__(self, sysid, state: bool = False) -> None: def __init__(self, robot_name, state: bool = False) -> None:
super().__init__() super().__init__()
self.sysid = sysid self.robot_name = robot_name
self._state = state self._state = state
self.button = None self.button = None
with self.classes(): with self.classes():
@ -24,7 +24,7 @@ class EnableStreamingButton(ui.element):
async def on_click(self) -> None: async def on_click(self) -> None:
spinner = ui.spinner(size="lg") spinner = ui.spinner(size="lg")
# So we don't block UI # So we don't block UI
result = await run.cpu_bound(self.enable_streaming, self.sysid, not self._state) result = await run.cpu_bound(self.enable_streaming, self.robot_name, not self._state)
if result: if result:
ui.notify("Success", type="positive]") ui.notify("Success", type="positive]")
self.set_state(state=not self._state) self.set_state(state=not self._state)
@ -43,17 +43,17 @@ class EnableStreamingButton(ui.element):
def stop_video(self): def stop_video(self):
if self._state != False: if self._state != False:
self.enable_streaming(sysid=self.sysid, is_streaming=False) self.enable_streaming(robot_name=self.robot_name, is_streaming=False)
self.set_state(state=False) self.set_state(state=False)
@staticmethod @staticmethod
def enable_streaming(sysid: int, is_streaming: bool) -> bool: def enable_streaming(robot_name: str, is_streaming: bool) -> bool:
world_name = os.environ["WORLD_NAME"] world_name = os.environ["WORLD_NAME"]
# Check if this topic has any subscribers i.e. model is up # Check if this topic has any subscribers i.e. model is up
gz_topic_list_proc = subprocess.Popen( gz_topic_list_proc = subprocess.Popen(
GZ_TOPIC_INFO GZ_TOPIC_INFO
+ [ENABLE_STREAMING_TOPIC.format(world_name=world_name, sysid=sysid)], + [ENABLE_STREAMING_TOPIC.format(world_name=world_name, robot_name=robot_name)],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
@ -72,7 +72,7 @@ class EnableStreamingButton(ui.element):
"gz", "gz",
"topic", "topic",
"-t", "-t",
ENABLE_STREAMING_TOPIC.format(world_name=world_name, sysid=sysid), ENABLE_STREAMING_TOPIC.format(world_name=world_name, robot_name=robot_name),
"-m", "-m",
"gz.msgs.Boolean", "gz.msgs.Boolean",
"-p", "-p",

View File

@ -32,7 +32,7 @@ services:
env_file: env_file:
- .env - .env
image: git.spirirobotics.com/spiri/services-ros2-mavros:main image: git.spirirobotics.com/spiri/services-ros2-mavros:main
command: ros2 launch mavros apm.launch fcu_url:="udp://0.0.0.0:$MAVROS2_PORT@:14555" namespace:="spiri$DRONE_SYS_ID" tgt_system:="$DRONE_SYS_ID" command: ros2 launch mavros apm.launch fcu_url:="udp://0.0.0.0:$MAVROS2_PORT@:14555" namespace:="$ROBOT_NAME" tgt_system:="$DRONE_SYS_ID"
ipc: host ipc: host
network_mode: host network_mode: host
restart: always restart: always

View File

@ -0,0 +1,8 @@
FROM git.spirirobotics.com/spiri/services-ros2-mavros:main
RUN apt-get update
RUN apt-get --yes install ros-${ROS_DISTRO}-ros-gz-bridge \
ros-${ROS_DISTRO}-ros-gz-image \
ros-${ROS_DISTRO}-compressed-image-transport \
ros-${ROS_DISTRO}-rmw-cyclonedds-cpp

View File

@ -0,0 +1,13 @@
services:
front-gimbal:
ipc: host
network_mode: host
# image: git.spirirobotics.com/spiri/services-ros2-mavros:main
#Build the iamge, give it a name, don't try to pull the image
build: ./
image: spirisdk-virtual_camera
pull_policy: never
environment:
- RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
command: ros2 run ros_gz_image image_bridge /world/${WORLD_NAME}/model/${ROBOT_NAME}/link/pitch_link/sensor/camera/image

View File

@ -1,167 +0,0 @@
#!/bin/env python3
import typer
import os
import sys
import contextlib
from dotenv import load_dotenv
from typing import List
from loguru import logger
import sh
import atexit
load_dotenv()
logger.remove()
logger.add(
sys.stdout, format="<green>{time}</green> <level>{level}</level> {extra} {message}"
)
# px4Path = os.environ.get("SPIRI_SIM_PX4_PATH", "/opt/spiri-sdk/PX4-Autopilot/")
# logger.info(f"SPIRI_SIM_PX4_PATH={px4Path}")
app = typer.Typer()
# This is a list of processes that we need to .kill and .wait for on exit
processes = []
class outputLogger:
"""
Logs command output to loguru
"""
def __init__(self, name, instance):
self.name = name
self.instance = instance
def __call__(self, message):
with logger.contextualize(cmd=self.name, instance=self.instance):
if message.endswith("\n"):
message = message[:-1]
# ToDo, this doesn't work because the output is coloured
if message.startswith("INFO"):
message = message.lstrip("INFO")
logger.info(message)
elif message.startswith("WARN"):
message = message.lstrip("WARN")
logger.warning(message)
elif message.startswith("ERROR"):
message = message.lstrip("ERROR")
logger.error(message)
elif message.startswith("DEBUG"):
message = message.lstrip("DEBUG")
logger.debug(message)
else:
logger.info(message)
@contextlib.contextmanager
def modified_environ(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variables to remove.
:param update: Dictionary of environment variables and values to add/update.
"""
env = os.environ
update = update or {}
remove = remove or []
# List of environment variables being updated or removed.
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
# Environment variables and values to restore on exit.
update_after = {k: env[k] for k in stomped}
# Environment variables and values to remove on exit.
remove_after = frozenset(k for k in update if k not in env)
try:
env.update(update)
[env.pop(k, None) for k in remove]
yield
finally:
env.update(update_after)
[env.pop(k) for k in remove_after]
# @app.command()
def start(instance: int = 0, sys_id: int = 1):
"""Starts the simulated drone with a given sys_id,
each drone must have it's own unique ID.
"""
if sys_id < 1 or sys_id > 254:
logger.error("sys_id must be between 1 and 254")
raise typer.Exit(code=1)
with logger.contextualize(syd_id=sys_id):
env = os.environ
with modified_environ(
SERIAL0_PORT=str(int(env["SERIAL0_PORT"]) + 10 * instance),
MAVROS2_PORT=str(int(env["MAVROS2_PORT"]) + 10 * instance),
MAVROS1_PORT=str(int(env["MAVROS1_PORT"]) + 10 * instance),
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(sys_id),
):
logger.info("Starting drone stack, this may take some time")
docker_stack = sh.docker.compose(
"--profile",
"uav-sim",
"-p",
f"robot-sim-{sys_id}",
"up",
_out=outputLogger("docker_stack", sys_id),
_err=sys.stderr,
_bg=True,
)
processes.append(docker_stack)
@app.command()
def start_group():
env = os.environ
sim_drone_count = int(env["SIM_DRONE_COUNT"])
start_ros_master()
"""Start a group of robots"""
for i in range(sim_drone_count):
logger.info(f"start robot {i}")
start(instance=i, sys_id=i + 1)
# if i == 0:
# wait_for_gazebo()
def start_ros_master():
docker_stack = sh.docker.compose(
"--profile",
"ros-master",
"up",
_out=outputLogger("docker_stack", "ros-master"),
_err=sys.stderr,
_bg=True,
)
processes.append(docker_stack)
def cleanup():
# Wait for all subprocesses to exit
logger.info("Waiting for commands to exit")
try:
if processes:
print(processes)
for waitable in processes:
waitable.kill()
waitable.wait()
except Exception as e:
print(e)
atexit.register(cleanup)
if __name__ == "__main__":
try:
app()
except KeyboardInterrupt:
logger.info("KeyboardInterrupt caught, exiting...")
cleanup()
sys.exit(0)