feature/multi-robots-types #12

Merged
aqua3 merged 20 commits from feature/multi-robots-types into master 2024-12-05 11:36:57 -04:00
15 changed files with 302 additions and 132 deletions

1
.gitignore vendored
View File

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

View File

@ -36,6 +36,25 @@ ROS1 is considered end of life. It's recomended to use a ROS2 template instead
We're working on it... We're working on it...
## Special docker options
We support the following special docker options to make metadata available inside the SDK
* x-spiri-sdk-doc: ""
This will appear as a comment when creating a new robot
* x-spiri-sdk-default-enabled: true
Set this to false and that docker compose will be commented out by default when creating a new robot
* x-spiri-sdk-default-args: ""
Extra arguments to pass to docker compose. Should accept all docker compose arguments.
Common options would be `--build`. You might also appreciate `--pull ("always"|"missing"|"never")`.
## NVIDIA Container Toolkit ## NVIDIA Container Toolkit
[Source](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) [Source](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)

View File

@ -1,5 +1,3 @@
version: "3.8"
services: services:
gui-tools: gui-tools:
env_file: env_file:
@ -9,44 +7,42 @@ services:
environment: environment:
# Display settings # Display settings
- DISPLAY=${DISPLAY} DISPLAY: "${DISPLAY}"
- WAYLAND_DISPLAY=${WAYLAND_DISPLAY} WAYLAND_DISPLAY: "${WAYLAND_DISPLAY}"
# - QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-xcb} # Default to X11 # Uncomment below if using X11
# If running with Wayland # QT_QPA_PLATFORM: "${QT_QPA_PLATFORM:-xcb}"
- XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR} XDG_RUNTIME_DIR: "${XDG_RUNTIME_DIR}"
- ROS_MASTER_URI=http://ros-master:11311 ROS_MASTER_URI: "http://ros-master:11311"
# - NVIDIA_DRIVER_CAPABILITIES=compute,video,utility RMW_IMPLEMENTATION: rmw_cyclonedds_cpp
# - NVIDIA_VISIBLE_DEVICES=all SDK_ROOT: ${PWD}
volumes: volumes:
# X11 socket # X11 socket
- /tmp/.X11-unix:/tmp/.X11-unix - /tmp/.X11-unix:/tmp/.X11-unix
- ${XAUTHORITY:-~/.Xauthority}:/root/.Xauthority - ${XAUTHORITY:-~/.Xauthority}:/root/.Xauthority
# Wayland socket # Wayland socket
#- ${XDG_RUNTIME_DIR}/wayland-0:${XDG_RUNTIME_DIR}/wayland-0 #- ${XDG_RUNTIME_DIR}/wayland-0:${XDG_RUNTIME_DIR}/wayland-0
# Allow access to the host's GPU # Access to GPU devices
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
#Auto reload on code changes # Code and configuration
- ./guiTools/spiri_sdk_guitools/:/app/spiri_sdk_guitools/ - ./guiTools/spiri_sdk_guitools/:/app/spiri_sdk_guitools/
# Enable launching the SDK from the SDK
# - ./:/app/sdk
- ./robots:/robots - ./robots:/robots
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
devices: devices:
# Provide access to GPU devices # Provide access to GPU devices (supports non-NVIDIA GPUs)
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
network_mode: host network_mode: host
ports: ports:
- 8923:8923 - 8923:8923
ipc: host ipc: host
#user: "${UID}:${GID}" privileged: true # Required for GPU access
privileged: true # Allow privileged access if necessary (e.g., for GPU access)
# restart: unless-stopped
# command: /bin/bash -c "source /opt/ros/foxy/setup.bash && rvis2" # Replace with the actual command to run Gazebo Ignition
deploy:
resources:
reservations:
devices:
- driver: cdi
device_ids:
- nvidia.com/gpu=all
# deploy:
# resources:
# reservations:
# devices:
# - driver: cdi # Optional for advanced resource scheduling
# device_ids:
# - "all" # Use "all" for all available GPUs

View File

@ -1,7 +1,6 @@
FROM osrf/ros:jazzy-desktop-full FROM osrf/ros:jazzy-desktop-full
RUN apt-get update RUN apt-get update && apt-get -y install qterminal mesa-utils \
RUN apt-get -y install qterminal mesa-utils \
libgstreamer1.0-dev \ libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \ libgstreamer-plugins-base1.0-dev \
docker-compose \ docker-compose \
@ -11,7 +10,8 @@ RUN apt-get -y install qterminal mesa-utils \
gstreamer1.0-gl \ gstreamer1.0-gl \
gstreamer1.0-plugins-good \ gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly gstreamer1.0-plugins-ugly \
ros-${ROS_DISTRO}-rmw-cyclonedds-cpp
COPY --from=git.spirirobotics.com/spiri/gazebo-resources:main /plugins /ardupilot_gazebo/plugins COPY --from=git.spirirobotics.com/spiri/gazebo-resources:main /plugins /ardupilot_gazebo/plugins

113
guiTools/poetry.lock generated
View File

@ -1,5 +1,19 @@
# 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 = "aioconsole"
version = "0.8.1"
description = "Asynchronous console and interfaces for asyncio"
optional = false
python-versions = ">=3.8"
files = [
{file = "aioconsole-0.8.1-py3-none-any.whl", hash = "sha256:e1023685cde35dde909fbf00631ffb2ed1c67fe0b7058ebb0892afbde5f213e5"},
{file = "aioconsole-0.8.1.tar.gz", hash = "sha256:0535ce743ba468fb21a1ba43c9563032c779534d4ecd923a46dbd350ad91d234"},
]
[package.extras]
dev = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-repeat", "uvloop"]
[[package]] [[package]]
name = "aiodocker" name = "aiodocker"
version = "0.23.0" version = "0.23.0"
@ -152,6 +166,30 @@ yarl = ">=1.12.0,<2.0"
[package.extras] [package.extras]
speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
[[package]]
name = "aiomonitor"
version = "0.7.1"
description = "Adds monitor and Python REPL capabilities for asyncio applications"
optional = false
python-versions = ">=3.8"
files = [
{file = "aiomonitor-0.7.1-py3-none-any.whl", hash = "sha256:10f50418ef8e60cd4b57efb3d2b984f62e01b3a7272772c6916e54f26877fd09"},
{file = "aiomonitor-0.7.1.tar.gz", hash = "sha256:beb1f14429bc4a3135bbac32381d242fe2019d74fcf9c86d3f4bd7405dc562e4"},
]
[package.dependencies]
aioconsole = ">=0.7.0"
aiohttp = ">=3.8.5"
attrs = ">=20"
click = ">=8.0"
janus = ">=1.0"
jinja2 = ">=3.1.2"
prompt-toolkit = ">=3.0"
telnetlib3 = ">=2.0.4"
terminaltables = "*"
trafaret = ">=2.1.1"
typing-extensions = ">=4.1"
[[package]] [[package]]
name = "aiosignal" name = "aiosignal"
version = "1.3.1" version = "1.3.1"
@ -690,6 +728,17 @@ files = [
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
] ]
[[package]]
name = "janus"
version = "1.1.0"
description = "Mixed sync-async queue to interoperate between asyncio tasks and classic threads"
optional = false
python-versions = ">=3.9"
files = [
{file = "janus-1.1.0-py3-none-any.whl", hash = "sha256:9a3daf0f1a16abda1a7c976e28dc1f6caf3b8d1de9b8c93b2ea84de424de7705"},
{file = "janus-1.1.0.tar.gz", hash = "sha256:0634df8b2b31f8afda4311abcf7fea912686fef717d13769eeaa01ae08d2b84c"},
]
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.4" version = "3.1.4"
@ -1095,6 +1144,20 @@ files = [
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
] ]
[[package]]
name = "prompt-toolkit"
version = "3.0.48"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"},
{file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"},
]
[package.dependencies]
wcwidth = "*"
[[package]] [[package]]
name = "propcache" name = "propcache"
version = "0.2.0" version = "0.2.0"
@ -1772,6 +1835,43 @@ anyio = ">=3.4.0,<5"
[package.extras] [package.extras]
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
[[package]]
name = "telnetlib3"
version = "2.0.4"
description = "Python 3 asyncio Telnet server and client Protocol library"
optional = false
python-versions = ">=3.7"
files = [
{file = "telnetlib3-2.0.4-py2.py3-none-any.whl", hash = "sha256:b3c0f984a7fb1b6ee16e6fdaa410c56389b0dc492174a99c6661b1ba4c9d457d"},
{file = "telnetlib3-2.0.4.tar.gz", hash = "sha256:dbcbc16456a0e03a62431be7cfefff00515ab2f4ce2afbaf0d3a0e51a98c948d"},
]
[[package]]
name = "terminaltables"
version = "3.1.10"
description = "Generate simple tables in terminals from a nested list of strings."
optional = false
python-versions = ">=2.6"
files = [
{file = "terminaltables-3.1.10-py2.py3-none-any.whl", hash = "sha256:e4fdc4179c9e4aab5f674d80f09d76fa436b96fdc698a8505e0a36bf0804a874"},
{file = "terminaltables-3.1.10.tar.gz", hash = "sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543"},
]
[[package]]
name = "trafaret"
version = "2.1.1"
description = "Validation and parsing library"
optional = false
python-versions = "*"
files = [
{file = "trafaret-2.1.1-py3-none-any.whl", hash = "sha256:1966f432586797aed663edd54cbc201fd7ba59eed1638f1a7a33f17977b3a569"},
{file = "trafaret-2.1.1.tar.gz", hash = "sha256:d9d00800318fbd343fdfb3353e947b2ebb5557159c844696c5ac24846f76d41c"},
]
[package.extras]
objectid = ["pymongo (>=2.4.1)"]
rfc3339 = ["python-dateutil (>=1.5)"]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -1985,6 +2085,17 @@ files = [
[package.dependencies] [package.dependencies]
anyio = ">=3.0.0" anyio = ">=3.0.0"
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "13.1" version = "13.1"
@ -2207,4 +2318,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 = "7607ff77762b700b8f487fd9b5ef6c07e875eccd429feace40047276d2f84280" content-hash = "bf8122985963391df3c60a13513784f7533309ec22095a82cac34034b6dae399"

View File

@ -14,6 +14,7 @@ sh = "^2.1.0"
docker = "^7.1.0" docker = "^7.1.0"
aiodocker = "^0.23.0" aiodocker = "^0.23.0"
numpy = "^2.1.3" numpy = "^2.1.3"
aiomonitor = "^0.7.1"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

View File

@ -1,32 +0,0 @@
#!/bin/bash
set -e #PR #6
source /opt/ros/$ROS_DISTRO/setup.bash
if [[ -z $SIM_DRONE_COUNT ]]
then
echo "SIM_DRONE_COUNT environment variable is not set."
exit 1
fi
gz sim -v -r $WORLD_FILE_NAME &
while true
do
topics=$(gz topic -l)
if [[ $topics == *"/world/$WORLD_NAME"* ]]
then
break
fi
sleep 1
done
cd /ardupilot_gazebo/models/$DRONE_MODEL
for (( j=0; j<$SIM_DRONE_COUNT; j++ ));
do
xacro -v gstreamer_udp_port:=$(($GSTREAMER_UDP_PORT + ($j * 10))) fdm_port_in:=$(($FDM_PORT_IN + ($j * 10))) model.xacro.sdf -o model.sdf
#! string is better than using -file option. File is not up to date in the next iteration.
value=$(</ardupilot_gazebo/models/$DRONE_MODEL/model.sdf)
gz_drone_name=spiri-$(($j + 1))
#? Maybe read from text file for formation of drones? x y z r p y
ros2 run ros_gz_sim create -world $WORLD_NAME -string "$value" -name $gz_drone_name -x $j -y 0 -z 0.195
done
exec "$@"

View File

@ -3,6 +3,7 @@ import subprocess
from collections import defaultdict from collections import defaultdict
import docker import docker
import time import time
from loguru import logger
docker_client = docker.from_env() docker_client = docker.from_env()
@ -12,7 +13,6 @@ applications = {
"rqt": ["rqt"], "rqt": ["rqt"],
"rviz2": ["rviz2"], "rviz2": ["rviz2"],
"Gazebo": ["/gz_entrypoint.sh"], "Gazebo": ["/gz_entrypoint.sh"],
"Gazebo Standalone": "gz sim -v4".split(),
# Add more applications here if needed # Add more applications here if needed
} }
@ -27,8 +27,7 @@ def launch_app(command):
ui.label("Spiri Robotics SDK").style('font-size: 40px; margin-bottom: 10px;').classes('w-full text-center') ui.label("Spiri Robotics SDK").style('font-size: 40px; margin-bottom: 10px;').classes('w-full text-center')
robots = [] robots = []
from spiri_sdk_guitools.sim_drone import Robot from spiri_sdk_guitools.sim_drone import robot_types
import aiodocker
import asyncio import asyncio
@ui.page('/') @ui.page('/')
@ -64,30 +63,20 @@ async def main():
#Add a new robot #Add a new robot
with new_robot_widget: with new_robot_widget:
ui.label("Add new robot").classes("text-3xl") for name, robot in robot_types.items():
newRobotParams = defaultdict(binding.BindableProperty) ui.label(f"Add new {name}").classes("text-3xl")
ui.number(value=1, label="SysID", min=1, max=254, await robot.launch_widget(robots_widget)
).bind_value(newRobotParams, 'sysid')
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():
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))
newRobotParams['sysid'] += 1 import aiomonitor
ui.button("Add", on_click=new_robot) async def amonitor():
#
logger.info("Starting aiomonitor")
loop = asyncio.get_running_loop()
run_forever = loop.create_future()
with aiomonitor.start_monitor(loop):
await run_forever
app.on_startup(amonitor)
# 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,13 +5,15 @@ from typing import List
import os import os
import sh import sh
import subprocess import subprocess
from nicegui import ui, run, app from nicegui import ui, run, app, binding
import yaml import yaml
import docker import docker
import aiodocker import aiodocker
import asyncio import asyncio
from spiri_sdk_guitools.video_button import EnableStreamingButton from spiri_sdk_guitools.video_button import EnableStreamingButton
from collections import defaultdict
import importlib.util
docker_client = docker.from_env() docker_client = docker.from_env()
@ -20,12 +22,15 @@ from rclpy.node import Node
import rclpy import rclpy
import threading import threading
async def ros_loop():
def ros_main() -> None:
rclpy.init() rclpy.init()
node = rclpy.create_node('async_subscriber')
while rclpy.ok():
rclpy.spin_once(node, timeout_sec=0)
await asyncio.sleep(0.1)
app.on_startup(lambda: threading.Thread(target=ros_main).start()) app.on_startup(ros_loop())
@contextlib.contextmanager @contextlib.contextmanager
@ -73,7 +78,29 @@ async def container_logs(container, element):
# ui.html(conv.convert(bytes(log,'utf-8').decode('utf-8', 'xmlcharrefreplace'), full=False)) # ui.html(conv.convert(bytes(log,'utf-8').decode('utf-8', 'xmlcharrefreplace'), full=False))
robot_types = {}
class Robot: class Robot:
def __init_subclass__(self):
#Register sub-classes as plugins
robot_types[self.robot_type] = self
for file in Path("/robots").glob("**/robot_plugins.py"):
logger.info(f"Loading plugin {file}")
spec = importlib.util.spec_from_file_location("robot_plugins", file)
plugin = importlib.util.module_from_spec(spec)
spec.loader.exec_module(plugin)
logger.info(f"Loaded plugin {file}")
async def topic_subscriber(element, topic_name):
while True:
with element:
element.clear()
ui.label(topic_name)
await asyncio.sleep(1)
class Spirimu(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):
@ -90,7 +117,51 @@ class Robot:
robots.add(self) robots.add(self)
#Ros doesn't like dashes in node names #Ros doesn't like dashes in node names
self.robot_name = f"{self.robot_type}_{self.sysid}".replace("-","_") self.robot_name = f"{self.robot_type}_{self.sysid}".replace("-","_")
self.world_name = "citadel_hill" # self.wold_name = os.environ.get("WORLD_NAME", "citadel_hill")

Not sure if this class variable has any effect in the code, and it has a typo

Not sure if this class variable has any effect in the code, and it has a typo
@classmethod
async def launch_widget(cls, robots_widget):
newRobotParams = defaultdict(binding.BindableProperty)
ui.number(value=1, label="SysID", min=1, max=254,
).bind_value(newRobotParams, 'sysid')
default_robot_compose = ""
for compose_file in Path("/robots").glob("**/docker-compose.yaml"):
specialArgs = ""
try:
data = yaml.safe_load(compose_file.read_text())
except yaml.YAMLError:
default_robot_compose += f"#{compose_file} not valid \n"
continue
specialArgs = data.get("x-spiri-sdk-default-args", "")
enabled = str(data.get("x-spiri-sdk-default-enabled", True))
enabled = enabled.lower() in ["true", "yes", "1"]
docstring = data.get("x-spiri-sdk-doc", "")
if docstring:
for line in docstring.split("\n"):
if line:
default_robot_compose += f"# {line}\n"
if not enabled:
default_robot_compose += "##"
default_robot_compose += f"{compose_file} {specialArgs}\n"
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():
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 = cls(**current_robot)
asyncio.tasks.create_task(robot.ui(robots_widget))
newRobotParams['sysid'] += 1
ui.button("Add", on_click=new_robot)
async def ui_containers(self, element): async def ui_containers(self, element):
docker_elements = {} docker_elements = {}
@ -143,14 +214,31 @@ class Robot:
with element: with element:
node_dummy = Node("_ros2cli_dummy_to_show_topic_list") node_dummy = Node("_ros2cli_dummy_to_show_topic_list")
scroll_area = ui.scroll_area() scroll_area = ui.scroll_area()
def refresh_topics():
with scroll_area: with scroll_area:
while True:
scroll_area.clear() scroll_area.clear()
#Filter for topics that start with self.robot_name #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():
if self.robot_name in topic[0]: if self.robot_name in topic[0]:
ui.label(topic[0]) ui.label(topic[0])
await asyncio.sleep(10) # expander = ui.expansion(topic[0]).classes('w-full')
# topic_subscriber(expander, topic[0])
ui.button("Refresh topics", on_click=refresh_topics).classes("m-2")
async def ui_actions(self, element):
pass
with element:
node_dummy = Node("_ros2cli_dummy_to_show_service_list")
scroll_area = ui.scroll_area()
def refresh_topics():
with scroll_area:
scroll_area.clear()
#Filter for topics that start with self.robot_name
for topic in node_dummy.get_action_names_and_types():
if self.robot_name in topic[0]:
ui.label(topic[0])
ui.button("Refresh topics", on_click=refresh_topics).classes("m-2")
async def ui(self, element): async def ui(self, element):
adocker = aiodocker.Docker() adocker = aiodocker.Docker()
@ -175,11 +263,14 @@ class Robot:
with ui.tabs() as tabs: with ui.tabs() as tabs:
tab_containers = ui.tab("Containers") tab_containers = ui.tab("Containers")
tab_ros = ui.tab("ROS Topics") tab_ros = ui.tab("ROS Topics")
# tab_actions = ui.tab("Actions")
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))
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))
# tab = ui.tab_panel(tab_actions).classes("w-full")
# asyncio.create_task(self.ui_actions(tab))
async def async_stop(self): async def async_stop(self):
return await run.io_bound(self.stop) return await run.io_bound(self.stop)
@ -226,7 +317,7 @@ class Robot:
INSTANCE=str(instance), INSTANCE=str(instance),
DRONE_SYS_ID=str(self.sysid), DRONE_SYS_ID=str(self.sysid),
ROBOT_NAME=self.robot_name, ROBOT_NAME=self.robot_name,
WORLD_NAME="citadel_hill", WORLD_NAME=env["WORLD_NAME"],
): ):
self.spawn_gz_model() self.spawn_gz_model()
logger.info("Starting drone stack, this may take some time") logger.info("Starting drone stack, this may take some time")
@ -295,7 +386,7 @@ class Robot:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
out, err = ros2_gz_create_proc.communicate(timeout=15) out, err = ros2_gz_create_proc.communicate(timeout=3)
ros2_gz_create_proc.kill() ros2_gz_create_proc.kill()
return return
@ -326,5 +417,5 @@ class Robot:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
) )
out, err = remove_entity_proc.communicate(timeout=15) out, err = remove_entity_proc.communicate(timeout=3)
remove_entity_proc.kill() remove_entity_proc.kill()

View File

@ -0,0 +1,14 @@
x-spiri-sdk-doc: Capture camera images for mapping
x-spiri-sdk-default-enabled: false
services:
camera-capture:
ipc: host
network_mode: host
restart: unless-stopped
image: restreamio/gstreamer:2024-11-14T15-53-57Z-prod
volumes:
- ${SDK_ROOT}/survey/${ROBOT_NAME}:/survey/
environment:
- RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
command: sh -c "rm -f /survey/*; gst-launch-1.0 -vc udpsrc port=$GSTREAMER_UDP_PORT close-socket=false auto-multicast=true ! application/x-rtp, payload=96 ! rtph264depay ! decodebin3 ! videoconvert ! videorate ! video/x-raw,framerate=1/2 ! jpegenc ! multifilesink location=/survey/img_%05d.jpg throttle-time=1"

View File

@ -1,19 +0,0 @@
DRONE_SYS_ID=1
INSTANCE=0
SERIAL0_PORT=5760
SITL_PORT=5501
MAVROS2_PORT=14560
MAVROS1_PORT=14561
FDM_PORT_IN=9002
GSTREAMER_UDP_PORT=5600
ROS_MASTER_URI="http://0.0.0.0:11311"
ARDUPILOT_VEHICLE="-v copter -f gazebo-mu --model=JSON -L CitadelHill"
WORLD_FILE_NAME="citadel_hill_world.sdf"
WORLD_NAME="citadel_hill"
DRONE_MODEL="spiri_mu"
SIM_DRONE_COUNT=1
GCS_PORT=14550

View File

@ -1,7 +1,8 @@
x-spiri-sdk-doc: |
Runs the core services for a simulated Spiri Mu
services: services:
ardupilot: ardupilot:
env_file:
- .env
image: git.spirirobotics.com/spiri/ardupilot:spiri-master image: git.spirirobotics.com/spiri/ardupilot:spiri-master
command: command:
- /bin/bash - /bin/bash
@ -14,14 +15,13 @@ services:
network_mode: host network_mode: host
mavproxy: mavproxy:
env_file:
- .env
image: git.spirirobotics.com/spiri/services-mavproxy:main image: git.spirirobotics.com/spiri/services-mavproxy:main
command: > command: >
mavproxy.py --non-interactive mavproxy.py --non-interactive
--master tcp:127.0.0.1:$SERIAL0_PORT --master tcp:127.0.0.1:$SERIAL0_PORT
--out udpout:0.0.0.0:$MAVROS2_PORT --out udpout:0.0.0.0:$MAVROS2_PORT
--out udpout:0.0.0.0:$MAVROS1_PORT --out udpout:0.0.0.0:$MAVROS1_PORT
--out udpout:0.0.0.0:18761
--sitl 127.0.0.1:$SITL_PORT --sitl 127.0.0.1:$SITL_PORT
--out udp:0.0.0.0:$GCS_PORT --out udp:0.0.0.0:$GCS_PORT
ipc: host ipc: host
@ -29,9 +29,9 @@ services:
restart: always restart: always
mavros2: mavros2:
env_file:
- .env
image: git.spirirobotics.com/spiri/services-ros2-mavros:main image: git.spirirobotics.com/spiri/services-ros2-mavros:main
environment:
- RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
command: ros2 launch mavros apm.launch fcu_url:="udp://0.0.0.0:$MAVROS2_PORT@:14555" namespace:="$ROBOT_NAME" 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
@ -54,8 +54,6 @@ services:
mavros: mavros:
#This service bridges our mavlink-based robot-coprosessor into ROS #This service bridges our mavlink-based robot-coprosessor into ROS
#In this example it connects to a simulated coprocessor. #In this example it connects to a simulated coprocessor.
env_file:
- .env
image: git.spirirobotics.com/spiri/services-ros1-mavros:master image: git.spirirobotics.com/spiri/services-ros1-mavros:master
command: rosrun mavros mavros_node __name:=spiri$DRONE_SYS_ID _fcu_url:="udp://0.0.0.0:$MAVROS1_PORT@:14559" _target_system_id:="$DRONE_SYS_ID" command: rosrun mavros mavros_node __name:=spiri$DRONE_SYS_ID _fcu_url:="udp://0.0.0.0:$MAVROS1_PORT@:14559" _target_system_id:="$DRONE_SYS_ID"
profiles: profiles:
@ -74,8 +72,6 @@ services:
hard: 524288 hard: 524288
ros-master: ros-master:
env_file:
- .env
image: git.spirirobotics.com/spiri/services-ros1-core:main image: git.spirirobotics.com/spiri/services-ros1-core:main
command: stdbuf -o L roscore command: stdbuf -o L roscore
profiles: profiles:

View File

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

View File

@ -1,4 +1,8 @@
x-spiri-sdk-doc: |
Start a virtual camera that can be included in ros
x-spiri-sdk-default-args: --build
services: services:
front-gimbal: front-gimbal:
ipc: host ipc: host