Compare commits

..

88 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 f37841d7bb Run ui start in thread 2024-11-08 09:41:34 -04:00
Burak Ozter df5df4b497 FIX: button state when stop/delete and video stream was enabled 2024-11-07 13:54:04 -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
Burak Ozter c1484b58ce Alex D: fix rclpy.spin no node error 2024-11-07 13:12:19 -04:00
Burak Ozter a752ed603e enable/disable video stream, spawn/delete gazebo entity 2024-11-07 13:09:37 -04:00
Alex Davies f4f5b0890e Added stub ros topics in preperation for future ros tooling 2024-11-07 12:45:08 -04:00
Alex Davies 1a409a063a Moved robot config to own folder structure 2024-11-07 12:18:53 -04:00
Alex Davies 4ae96ff8b9 Fixed bug preventing deleting robots 2024-11-07 10:03:14 -04:00
Alex Davies b8a353b9af Moved robot container listing to own tab and method for better composability 2024-11-07 09:45:08 -04:00
Alex Davies e42b5f1740 A few more style tweaks 2024-11-06 16:40:07 -04:00
Alex Davies 2a252cb6a6 Minore UI tweaks 2024-11-06 16:27:25 -04:00
Alex Davies 5f5aa7f830 Some minor UI tweaks 2024-11-06 15:59:04 -04:00
Alex Davies a272fdff0f We now cleantly show logs and new robots. 2024-11-06 15:44:18 -04:00
Alex Davies eb8077f8eb Update UI 2024-11-06 12:58:14 -04:00
Alex Davies ec7a53201f Work on moving sim-drone to our user interface 2024-11-05 14:49:32 -04:00
Alex Davies 15110bd830 Add tabs 2024-11-05 11:22:15 -04:00
Alex Davies f30740fe2d Move GUI toolkit from tkinter to nicegui for easier prototyping 2024-11-05 10:29:34 -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
Burak Ozter 72878c06e8 update README 2024-11-04 17:29:28 -04:00
Burak Ozter 2fa4e45818 rename tracker_instance to instance 2024-11-04 17:29:16 -04:00
Burak Ozter ed6097388d change default value, rename, and fix value type 2024-11-04 17:28:38 -04:00
Burak Ozter c53205eadb #6 (comment) and #6 (comment) 2024-11-04 11:47:21 -04:00
Burak Ozter d661b1df33 remove container names, use project name for docker-compose 2024-11-01 15:32:43 -03:00
Burak Ozter 3078feb2fb #6 (comment) and fix drone name 2024-11-01 12:47:35 -03:00
Burak Ozter dfdcb284f8 auto formatting 2024-11-01 12:20:39 -03:00
Burak Ozter de3aab205b auto formatting 2024-11-01 12:20:08 -03:00
Burak Ozter 0a0e7207c9 add gstreamer port env variable 2024-10-31 13:28:48 -03:00
Burak Ozter 4ea8d7d1b9 typo 2024-10-31 13:28:21 -03:00
Burak Ozter 5639f03d46 add gstreamer port 2024-10-31 13:28:03 -03:00
Burak Ozter 834a71b298 typo 2024-10-31 12:27:09 -03:00
Burak Ozter 57a21d371b add gstreamer apt packages for video streaming 2024-10-31 12:19:58 -03:00
Burak Ozter af90430fb2 remove ardupilot_gazebo files, add ros 1 services to docker-compose 2024-10-29 17:00:57 -03:00
Burak Ozter f7f3bfe528 add comments 2024-10-29 10:31:59 -03:00
Burak Ozter a968663ec8 update readme 2024-10-29 10:29:01 -03:00
Burak Ozter fc52a3522f add missing variable from .env file. 2024-10-28 09:50:49 -03:00
Burak Ozter 7acee12640 remove ros1 docker services 2024-10-28 09:09:57 -03:00
Burak Ozter 76810ecc46 typo 2024-10-25 14:50:34 -03:00
Burak Ozter 56456f5950 working gazebo sim and sim_drone.py 2024-10-25 14:49:07 -03: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
Burak Ozter e56e3c06c0 somehow all files changed to executable.. revert to not executable.. 2024-10-18 14:13:49 -03:00
Burak Ozter f34cfe98f2 add gazebo-sim address, add --no-mavproxy 2024-10-18 14:08:48 -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
Burak Ozter 9cf8bfca88 update docker image name for services-ros2-mavros 2024-10-09 14:57:30 -03:00
Burak Ozter 73849a366b added ros2-mavros service 2024-10-03 17:25:09 -03:00
Alex Davies 8346fb42b5 Update README.md 2024-09-20 08:12:27 -03:00
Alex Davies b99f617166 Gui launcher tweaks 2024-09-19 14:33:29 -03:00
Alex Davies 69589259e1 Merge branch 'master' of https://git.spirirobotics.com/Spiri/spiri-sdk 2024-09-19 14:14:42 -03:00
Alex Davies f3acf21c6a Added gui runner for tools like rqt and ignition 2024-09-19 14:14:07 -03:00
28 changed files with 3506 additions and 63 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

19
.env Normal file
View File

@ -0,0 +1,19 @@
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

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 }}

3
.gitignore vendored
View File

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

238
README.md
View File

@ -1,10 +1,10 @@
# Spiri SDK - Simulated robot
The Spiri SDK consists of a number of components. What you're looking at right now
The Spiri SDK consists of a number of components. What you're looking at right now
is the drone simulation component, which is the core of the SDK.
Spiri Robots run a number of docker containers to achieve their core functionality,
we try to keep these essential docker containers in one docker compose file. The
we try to keep these essential docker containers in one docker compose file. The
docker compose file you'll find in this repository starts an ardupilot-based UAV simulation
as well as a ROS master, and mavproxy to tie it together.
@ -13,6 +13,8 @@ To get started you can simply clone this repository and run `docker compose --pr
Once the simulated UAV is running you can connect to it with QGroundControl or other
MavLink compatible software. We expose the UAVs Mavlink conenction on tcp port 5760.
There is experimental GUI support you can enable by running `docker compose --profile uav-sim --profile ui up`.
## Creating a new project
We provide project templates you can use for development that integrate seamlessly into
@ -23,13 +25,239 @@ These templates are intended to be used with VSCode.
To get started with our project templates install the [copier](https://copier.readthedocs.io/en/stable/) project
templating utility.
* [template-service-ros1-catkin](https://git.spirirobotics.com/Spiri/template-service-ros1-catkin)
- [template-service-ros1-catkin](https://git.spirirobotics.com/Spiri/template-service-ros1-catkin)
This template uses the last stable release of ROS1 (ros noetic) and supports python and c++ programming
This template uses the last stable release of ROS1 (ros noetic) and supports python and c++ programming
languages.
ROS1 is considered end of life. It's recomended to use a ROS2 template instead
* ROS2 template
- ROS2 template
We're working on it...
## NVIDIA Container Toolkit
[Source](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html)
### Installing with Apt
```bash
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
```
```bash
sudo apt-get update
```
```bash
sudo apt-get install -y nvidia-container-toolkit
```
#### Configuration
##### Prerequisites
- You installed a supported container engine (Docker, Containerd, CRI-O, Podman).
- You installed the NVIDIA Container Toolkit.
```bash
sudo nvidia-ctk runtime configure --runtime=docker --cdi.enabled
```
```bash
sudo systemctl restart docker
```
### 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.
```bash
sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi
```
Expected output:
```console
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 535.86.10 Driver Version: 535.86.10 CUDA Version: 12.2 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 On | 00000000:00:1E.0 Off | 0 |
| N/A 34C P8 9W / 70W | 0MiB / 15109MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
```
## Support for Container Device Interface(CDI)
[Source](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/cdi-support.html)
### Prerequisites
- You installed either the NVIDIA Container Toolkit or you installed the `nvidia-container-toolkit-base` package.
The base package includes the container runtime and the `nvidia-ctk` command-line interface, but avoids installing the container runtime hook and transitive dependencies.
The hook and dependencies are not needed on machines that use CDI exclusively
- You installed an NVIDIA GPU Driver.
Two common locations for CDI specifications are `/etc/cdi/` and `/var/run/cdi/`. The contents of the `/var/run/cdi/` directory are cleared on boot.
1. Generate the CDI specification file:
```bash
sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
```
Example output
```console
INFO[0000] Auto-detected mode as "nvml"
INFO[0000] Selecting /dev/nvidia0 as /dev/nvidia0
INFO[0000] Selecting /dev/dri/card1 as /dev/dri/card1
INFO[0000] Selecting /dev/dri/renderD128 as /dev/dri/renderD128
INFO[0000] Using driver version xxx.xxx.xx
...
```
2. (Optional) Check the names of the generated devices:
```bash
nvidia-ctk cdi list
```
Output
```console
INFO[0000] Found 9 CDI devices
nvidia.com/gpu=all
nvidia.com/gpu=0
```
### Sample Workload
```bash
docker run --rm -ti --runtime=nvidia \
-e NVIDIA_VISIBLE_DEVICES=nvidia.com/gpu=all \
ubuntu nvidia-smi -L
```
Output
```console
GPU 0: NVIDIA GeForce RTX 3080 Laptop GPU (UUID: GPU-17c2b9a6-6be2-3857-f8e0-88143e2e621b)
```
## Technologies
* Ubuntu 22.04.1 amd64
* Docker Compose version v2.29.7
* Python 3.10.12
* pip 22.0.2
* NVIDIA Container Toolkit CLI version 1.16.2
* NVIDIA Driver 550.120
* CUDA Version 12.4
## How to Run
Ensure variables in the `.env` file are correct.
Install the required libraries in the python script.
```bash
pip install -r requirements.txt
```
### First Terminal
1. Start the user interface with the following command.
```bash
docker compose --profile ui up
```
2. Click `Launch Gazebo` on the menu.
Simulated world and the drone model should be up and running in a Gazebo instance.
### Second Terminal
This will launch ardupilot, mavproxy and mavros services, and will scale up by the `SIM_DRONE_COUNT` env variable.
1. Start the docker services with the following python script.
```bash
python3 sim_drone.py
```
### QGroundControl
Simulated vehicle(s) should be connected to QGroundControl. `SIM_DRONE_COUNT` value should match the detected vehicle count on GCS.
## Simulation Environment Variables
`DRONE_SYS_ID` and `INSTANCE` environment variables are incremented by 1 for each additional simulated vehicle. `SERIAL0_PORT`, `SITL_PORT`, `MAVROS2_PORT`, `MAVROS1_PORT`,`FDM_PORT_IN` and `GSTREAMER_UDP_PORT` are incremented by 10. Without in-depth knowledge, changing the default value for these ports are not recommended.
| Variable | Type | Default | Description |
| :----------------: | :----: | :--------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------- |
| DRONE_SYS_ID | int | 1 | System-ID for the simulated drone. |
| INSTANCE | int | 0 | Instance of simulator. |
| SERIAL0_PORT | int | 5760 | Mavproxy master port the simulation communicating on. |
| SITL_PORT | int | 5501 | Mavproxy Software in the Loop(SITL) port to send simulated RC input for the simulator. |
| MAVROS2_PORT | int | 14560 | MAVROS ROS 2 UDP port |
| MAVROS1_PORT | int | 14561 | MAVROS ROS 1 UDP port |
| FDM_PORT_IN | int | 9002 | Gazebo Flight Dynamics Model (FDM) UDP port |
| GSTREAMER_UDP_PORT | int | 5600 | UDP Video Streaming port |
| ROS_MASTER_URI | string | "http://0.0.0.0:11311" | This tells ROS 1 nodes where they can locate the master |
| ARDUPILOT_VEHICLE | string | "-v copter -f gazebo-mu --model=JSON -L CitadelHill" | "-v" is vehicle type,"-L" start location, "-f" is vehicle frame type, "--model" overrides simulation model to use |
| WORLD_FILE_NAME | string | "citadel_hill_world.sdf" | Name of the file that exists in the worlds folder. |
| WORLD_NAME | string | "citadel_hill" | Name of the world defined in the world file. |
| DRONE_MODEL | string | "spiri_mu" | Drone model that exists in the models folder. |
| SIM_DRONE_COUNT | int | 1 | Number of drones to be simulated. |
| GCS_PORT | int | 14550 | Ground Control Station(GCS) UDP connection port. |
### Understanding Multi-Vehicle Simulation Parameters
For instance, if `SIM_DRONE_COUNT` is 2, each additional vehicle's ports are incremented by 10.
First simulated vehicle would have these following values,
| Variable | Value |
| :----------------: | :---: |
| 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 |
Second Vehicle,
| Variable | Value |
| :----------------: | :---: |
| DRONE_SYS_ID | 2 |
| INSTANCE | 1 |
| SERIAL0_PORT | 5770 |
| SITL_PORT | 5511 |
| MAVROS2_PORT | 14570 |
| MAVROS1_PORT | 14571 |
| FDM_PORT_IN | 9012 |
| GSTREAMER_UDP_PORT | 5610 |
Video stream would be available on UDP port on 5610 for the second vehicle after enabling streaming.

View File

@ -1,64 +1,52 @@
version: '3.8'
version: "3.8"
services:
ardupilot:
image: git.spirirobotics.com/spiri/ardupilot:spiri-master
command: >
./Tools/autotest/sim_vehicle.py -v copter --no-rebuild
--out=udpin:0.0.0.0:5000
--enable-dds
stdin_open: true
tty: true
gui-tools:
env_file:
- .env
build:
context: ./guiTools/
mavproxy:
image: git.spirirobotics.com/spiri/services-mavproxy:main
command: >
mavproxy.py --non-interactive
--out=tcpin:0.0.0.0:5760
--master=udpout:ardupilot:5000
restart: always
ports:
- 5760:5760
mavros:
#This service bridges our mavlink-based robot-coprosessor into ROS
#In this example it connects to a simulated coprocessor.
image: git.spirirobotics.com/spiri/services-ros1-mavros:master
command: roslaunch mavros px4.launch fcu_url:="udp://:14555@mavproxy:14550" tgt_system:="1"
environment:
- "ROS_MASTER_URI=http://ros-master:11311"
depends_on:
ros-master:
condition: service_healthy
mavproxy:
condition: service_started
restart: always
# Display settings
- DISPLAY=${DISPLAY}
- WAYLAND_DISPLAY=${WAYLAND_DISPLAY}
# - QT_QPA_PLATFORM=${QT_QPA_PLATFORM:-xcb} # Default to X11
# If running with Wayland
- XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR}
- ROS_MASTER_URI=http://ros-master:11311
# - NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
# - NVIDIA_VISIBLE_DEVICES=all
volumes:
# X11 socket
- /tmp/.X11-unix:/tmp/.X11-unix
- ${XAUTHORITY:-~/.Xauthority}:/root/.Xauthority
# Wayland socket
#- ${XDG_RUNTIME_DIR}/wayland-0:${XDG_RUNTIME_DIR}/wayland-0
# Allow access to the host's GPU
- /dev/dri:/dev/dri
#Auto reload on code changes
- ./guiTools/spiri_sdk_guitools/:/app/spiri_sdk_guitools/
# Enable launching the SDK from the SDK
# - ./:/app/sdk
- ./robots:/robots
- /var/run/docker.sock:/var/run/docker.sock
devices:
# Provide access to GPU devices
- /dev/dri:/dev/dri
network_mode: host
ports:
- 8923:8923
ipc: host
#user: "${UID}:${GID}"
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:
limits:
# cpus: '0.01'
memory: 200M
ulimits:
nofile:
soft: 1024
hard: 524288
ros-master:
image: git.spirirobotics.com/spiri/services-ros1-core:main
command: stdbuf -o L roscore
environment:
- "ROS_MASTER_URI=http://localhost:11311"
restart: always
ports:
- "127.0.0.1:11311:11311"
deploy:
resources:
limits:
memory: 1G
# Madness, setting a low ulimit here fixes memory leaks
# https://answers.ros.org/question/336963/rosout-high-memory-usage/
ulimits:
nofile:
soft: 1024
hard: 524288
reservations:
devices:
- driver: cdi
device_ids:
- nvidia.com/gpu=all

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

46
guiTools/Dockerfile Normal file
View File

@ -0,0 +1,46 @@
FROM osrf/ros:jazzy-desktop-full
RUN apt-get update
RUN apt-get -y install qterminal mesa-utils \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
docker-compose \
python3.12-venv \
python3-pip \
gstreamer1.0-libav \
gstreamer1.0-gl \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly
COPY --from=git.spirirobotics.com/spiri/gazebo-resources:main /plugins /ardupilot_gazebo/plugins
COPY --from=git.spirirobotics.com/spiri/gazebo-resources:main /models /ardupilot_gazebo/models
COPY --from=git.spirirobotics.com/spiri/gazebo-resources:main /worlds /ardupilot_gazebo/worlds
ENV GZ_SIM_SYSTEM_PLUGIN_PATH=/ardupilot_gazebo/plugins
ENV GZ_SIM_RESOURCE_PATH=/ardupilot_gazebo/models:/ardupilot_gazebo/worlds
COPY ./gz_entrypoint.sh /gz_entrypoint.sh
RUN chmod +x /gz_entrypoint.sh
WORKDIR /app
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip3 install poetry
COPY ./pyproject.toml ./poetry.lock ./README.md ./
COPY ./spiri_sdk_guitools ./spiri_sdk_guitools
RUN poetry env use python3
RUN poetry config virtualenvs.create false && poetry install --no-dev --no-interaction --no-ansi
CMD poetry run python3 spiri_sdk_guitools/launcher.py

0
guiTools/README.md Normal file
View File

View File

@ -0,0 +1,6 @@
#!/bin/bash
set -e #PR #6
source /opt/ros/$ROS_DISTRO/setup.bash
gz sim -v -r $WORLD_FILE_NAME
exec "$@"

2210
guiTools/poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

21
guiTools/pyproject.toml Normal file
View File

@ -0,0 +1,21 @@
[tool.poetry]
name = "spiri_sdk_guitools"
version = "0.1.0"
description = ""
authors = ["Spiri Robotics"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
nicegui = "^2.5.0"
pywebview = "^5.3.2"
loguru = "^0.7.2"
sh = "^2.1.0"
docker = "^7.1.0"
aiodocker = "^0.23.0"
numpy = "^2.1.3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

32
guiTools/spawn_drones.sh Normal file
View File

@ -0,0 +1,32 @@
#!/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

View File

@ -0,0 +1,93 @@
from nicegui import ui, binding, app, run
import subprocess
from collections import defaultdict
import docker
import time
docker_client = docker.from_env()
# Dictionary of applications: key is the button text, value is the command to execute
applications = {
"Terminal": ["qterminal"],
"rqt": ["rqt"],
"rviz2": ["rviz2"],
"Gazebo": ["/gz_entrypoint.sh"],
"Gazebo Standalone": "gz sim -v4".split(),
# Add more applications here if needed
}
# Function to launch an application
def launch_app(command):
try:
subprocess.Popen(command)
except FileNotFoundError:
print(f"{command[0]} not found. Make sure it's installed and accessible in the PATH.")
# Create the NiceGUI interface
ui.label("Spiri Robotics SDK").style('font-size: 40px; margin-bottom: 10px;').classes('w-full text-center')
robots = []
from spiri_sdk_guitools.sim_drone import Robot
import aiodocker
import asyncio
@ui.page('/')
async def main():
with ui.tabs().classes('w-full') as tabs:
tab_tools = ui.tab('Tools')
tab_robots = ui.tab('Robots')
# two = ui.tab('Two')
with ui.tab_panels(tabs, value=tab_tools).classes("w-full"):
with ui.tab_panel(tab_tools):
# Create and place buttons dynamically based on the dictionary
with ui.grid(columns=3):
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):
new_robot_widget = ui.element().classes("w-1/2")
with ui.element().classes("w-1/2"):
ui.label("Debug").classes("text-xl")
def cleanup_all_containers():
#Find all containers that start with spiri-sdk
containers = docker_client.containers.list(all=True)
removal_count = 0
for container in containers:
if container.name.startswith("robot-sim-"):
container.remove(force=True)
removal_count += 1
ui.label(f"Removed {removal_count} containers")
ui.button("Cleanup all containers", on_click=cleanup_all_containers)
ui.separator()
ui.label("Current robots").classes("text-3xl")
robots_widget = ui.element().classes("w-full")
#Add a new robot
with new_robot_widget:
ui.label("Add new robot").classes("text-3xl")
newRobotParams = defaultdict(binding.BindableProperty)
ui.number(value=1, label="SysID", min=1, max=254,
).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
ui.button("Add", on_click=new_robot)
# Start the NiceGUI application
ui.run(title="Spiri SDK Launcher", port=8923, dark=None)

View File

@ -0,0 +1,330 @@
from loguru import logger
from pathlib import Path
import contextlib
from typing import List
import os
import sh
import subprocess
from nicegui import ui, run, app
import yaml
import docker
import aiodocker
import asyncio
from spiri_sdk_guitools.video_button import EnableStreamingButton
docker_client = docker.from_env()
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
import rclpy
import threading
def ros_main() -> None:
rclpy.init()
app.on_startup(lambda: threading.Thread(target=ros_main).start())
@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]
robots = set()
async def container_logs(container, element):
adocker = aiodocker.Docker()
with element:
with ui.scroll_area():
acontainer = await adocker.containers.get(container.id)
async for log in acontainer.log(stdout=True, stderr=True, follow=True):
for line in log.splitlines():
ui.label(line)
# ui.html(conv.convert(bytes(log,'utf-8').decode('utf-8', 'xmlcharrefreplace'), full=False))
class Robot:
robot_type = "spiri_mu"
def __init__(self, sysid: int, compose_files: List[Path] | str):
if sysid > 255 or sysid < 0:
raise ValueError("sysid must be between 0 and 255")
self.sysid = int(sysid)
if isinstance(compose_files, str):
compose_files = [
Path(file) for file in compose_files.replace("\n", ",").split(",")
]
self.compose_files = compose_files
self.processes = []
self.video_button = None
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):
docker_elements = {}
container_status = {}
with element:
while True:
with logger.catch():
# 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().classes("w-full")
with docker_elements[container]:
ui.label().bind_text(container_status, container).classes(
"text-2xl"
)
#Show the command the container is running
# ui.label(container.attrs["Config"]["Cmd"])
cmd_widget = ui.codemirror(" ".join(container.attrs["Config"]["Cmd"]), language="bash",theme="basicDark").classes('h-auto max-h-32')
cmd_widget.enabled = False
with ui.expansion("Env Variables").classes("w-full outline outline-1").style("margin: 10px;"):
env_widget = ui.codemirror("\n".join(container.attrs["Config"]["Env"]), language="bash",theme="basicDark")
env_widget.enabled = False
logelement = (
ui.expansion("Logs")
.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):
with element:
node_dummy = Node("_ros2cli_dummy_to_show_topic_list")
scroll_area = ui.scroll_area()
with scroll_area:
while True:
scroll_area.clear()
#Filter for topics that start with self.robot_name
for topic in node_dummy.get_topic_names_and_types():
if self.robot_name in topic[0]:
ui.label(topic[0])
await asyncio.sleep(10)
async def ui(self, element):
adocker = aiodocker.Docker()
with element:
self.robot_ui = ui.element().classes("w-full outline p-4")
with self.robot_ui:
ui.label(f"{self.robot_type} {self.sysid}").classes("text-2xl")
ui.label(f"""Sysid: {self.sysid}""")
ui.button("Start", on_click=self.async_start).classes("m-2")
ui.button("Stop", on_click=self.async_stop).classes("m-2")
self.video_button = EnableStreamingButton(robot_name=self.robot_name).classes(
"m-2"
)
async def delete_robot():
await self.async_stop()
robots.remove(self)
element.remove(self.robot_ui)
ui.button("Delete", on_click=delete_robot).classes("m-2")
with ui.tabs() as tabs:
tab_containers = ui.tab("Containers")
tab_ros = ui.tab("ROS Topics")
with ui.tab_panels(tabs, value=tab_containers):
tab = ui.tab_panel(tab_containers).classes("w-full")
asyncio.create_task(self.ui_containers(tab))
tab = ui.tab_panel(tab_ros).classes("w-full")
asyncio.create_task(self.ui_ros(tab))
async def async_stop(self):
return await run.io_bound(self.stop)
def stop(self):
# If video was enabled, and we stop. Button state is wrong.
# Stop video if it is enabled.
if isinstance(self.video_button, EnableStreamingButton):
self.video_button.stop_video()
# Delete gazebo model
self.delete_gz_model()
# Signal all processes to stop
for process in self.processes:
process.terminate()
process.kill()
self.processes = []
for container in self.containers():
# container.stop()
container.remove(force=True)
def containers(self):
return docker_client.containers.list(
all=True, filters={"name": f"robot-sim-{self.robot_name}"}
)
async def async_start(self):
return await run.io_bound(self.start)
def start(self):
"""Starts the simulated drone with a given sysid,
each drone must have it's own unique ID.
"""
instance = self.sysid - 1
sysid = self.sysid
with logger.contextualize(syd_id=sysid):
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),
GSTREAMER_UDP_PORT=str(int(env["GSTREAMER_UDP_PORT"]) + 10 * instance),
SITL_PORT=str(int(env["SITL_PORT"]) + 10 * instance),
INSTANCE=str(instance),
DRONE_SYS_ID=str(self.sysid),
ROBOT_NAME=self.robot_name,
WORLD_NAME="citadel_hill",
):
self.spawn_gz_model()
logger.info("Starting drone stack, this may take some time")
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):
compose_file = Path(compose_file)
if not compose_file.exists():
raise FileNotFoundError(f"File {compose_file} does not exist")
#Get the folder the compose file is in
compose_folder = compose_file.parent
args = [
"docker-compose",
"--profile",
"uav-sim",
"-p",
f"robot-sim-{self.robot_name}-{compose_folder.name}",
"-f",
compose_file.as_posix(),
"up",
*arguments,
]
command = " ".join(args)
logger.info(f"Starting drone stack with command: {command}")
docker_stack = subprocess.Popen(
args,
# stdout=subprocess.PIPE,
# stderr=subprocess.PIPE,
)
logger.info(f"Started drone stack with PID: {docker_stack.pid}")
@logger.catch
def spawn_gz_model(self):
sysid = self.sysid
logger.info("")
env = os.environ
GSTREAMER_UDP_PORT = env["GSTREAMER_UDP_PORT"]
FDM_PORT_IN = env["FDM_PORT_IN"]
WORLD_NAME = env["WORLD_NAME"]
DRONE_MODEL = env["DRONE_MODEL"]
XACRO_CMD = [
"xacro",
f"gstreamer_udp_port:={GSTREAMER_UDP_PORT}",
f"fdm_port_in:={FDM_PORT_IN}",
"model.xacro.sdf",
"-o",
"model.sdf",
]
# This path is breaking if this drone_model folder doesnt exist!
# 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 {self.robot_name} -x {sysid - 1} -y 0 -z 0.195"
xacro_proc = subprocess.Popen(
XACRO_CMD,
cwd=f"/ardupilot_gazebo/models/{DRONE_MODEL}",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
ros2_gz_create_proc = subprocess.Popen(
ROS2_CMD.split(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = ros2_gz_create_proc.communicate(timeout=15)
ros2_gz_create_proc.kill()
return
@logger.catch
def delete_gz_model(self):
sysid = self.sysid
env = os.environ
WORLD_NAME = env["WORLD_NAME"]
# http://osrf-distributions.s3.amazonaws.com/gazebo/api/7.1.0/classgazebo_1_1physics_1_1Entity.html
ENTITY_TYPE_MODEL = 0x00000002
REQUEST_ARG = f"name: '{self.robot_name}' type: {ENTITY_TYPE_MODEL}"
GZ_SERVICE_CMD = [
"gz",
"service",
"-s",
f"/world/{WORLD_NAME}/remove",
"--reqtype",
"gz.msgs.Entity",
"--reptype",
"gz.msgs.Boolean",
"--timeout",
"5000",
"--req",
REQUEST_ARG,
]
remove_entity_proc = subprocess.Popen(
GZ_SERVICE_CMD,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = remove_entity_proc.communicate(timeout=15)
remove_entity_proc.kill()

View File

@ -0,0 +1,82 @@
from nicegui import run, ui
import subprocess
from loguru import logger
import os
GZ_TOPIC_INFO = ["gz", "topic", "-i", "-t"]
ENABLE_STREAMING_TOPIC = "/world/{world_name}/model/{robot_name}/link/pitch_link/sensor/camera/image/enable_streaming"
class EnableStreamingButton(ui.element):
def __init__(self, robot_name, state: bool = False) -> None:
super().__init__()
self.robot_name = robot_name
self._state = state
self.button = None
with self.classes():
with ui.row():
self.button = ui.button(on_click=self.on_click)
self.button._text = (
f'{"Disable" if self._state else "Enable"}' + " Video"
)
self.button.props(f'color={"red" if self._state else "green"}')
async def on_click(self) -> None:
spinner = ui.spinner(size="lg")
# So we don't block UI
result = await run.cpu_bound(self.enable_streaming, self.robot_name, not self._state)
if result:
ui.notify("Success", type="positive]")
self.set_state(state=not self._state)
else:
self.set_state(state=False)
ui.notify("No Video Streaming Available..", type="negative")
spinner.delete()
def set_state(self, state: bool):
self._state = state
self.update()
def update(self) -> None:
self.button._text = f'{"Disable" if self._state else "Enable"}' + " Video"
self.button.props(f'color={"red" if self._state else "green"}')
def stop_video(self):
if self._state != False:
self.enable_streaming(robot_name=self.robot_name, is_streaming=False)
self.set_state(state=False)
@staticmethod
def enable_streaming(robot_name: str, is_streaming: bool) -> bool:
world_name = os.environ["WORLD_NAME"]
# Check if this topic has any subscribers i.e. model is up
gz_topic_list_proc = subprocess.Popen(
GZ_TOPIC_INFO
+ [ENABLE_STREAMING_TOPIC.format(world_name=world_name, robot_name=robot_name)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Check exceptions.. timeout, error etc.
output, error = gz_topic_list_proc.communicate(timeout=15)
if gz_topic_list_proc.returncode != 0:
logger.error(error)
return False
if "No subscribers".casefold() in output.casefold():
logger.error("No Subscribers on enable_streaming topic.")
return False
gz_topic_pub = [
"gz",
"topic",
"-t",
ENABLE_STREAMING_TOPIC.format(world_name=world_name, robot_name=robot_name),
"-m",
"gz.msgs.Boolean",
"-p",
f"data:{is_streaming}",
]
subprocess.run((gz_topic_pub))
return True

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
typer==0.12.5
loguru==0.7.2
sh==2.1.0
python-dotenv==1.0.1

19
robots/spiri-mu/core/.env Normal file
View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,95 @@
services:
ardupilot:
env_file:
- .env
image: git.spirirobotics.com/spiri/ardupilot:spiri-master
command:
- /bin/bash
- -c
- |
./Tools/autotest/sim_vehicle.py $ARDUPILOT_VEHICLE --no-rebuild \
--no-mavproxy --enable-dds --sysid $DRONE_SYS_ID -I$INSTANCE
stdin_open: true
tty: true
network_mode: host
mavproxy:
env_file:
- .env
image: git.spirirobotics.com/spiri/services-mavproxy:main
command: >
mavproxy.py --non-interactive
--master tcp:127.0.0.1:$SERIAL0_PORT
--out udpout:0.0.0.0:$MAVROS2_PORT
--out udpout:0.0.0.0:$MAVROS1_PORT
--sitl 127.0.0.1:$SITL_PORT
--out udp:0.0.0.0:$GCS_PORT
ipc: host
network_mode: host
restart: always
mavros2:
env_file:
- .env
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:="$ROBOT_NAME" tgt_system:="$DRONE_SYS_ID"
ipc: host
network_mode: host
restart: always
depends_on:
ardupilot:
condition: service_started
mavproxy:
condition: service_started
deploy:
resources:
limits:
# cpus: '0.01'
memory: 200M
ulimits:
nofile:
soft: 1024
hard: 524288
mavros:
#This service bridges our mavlink-based robot-coprosessor into ROS
#In this example it connects to a simulated coprocessor.
env_file:
- .env
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"
profiles:
- ros1
ipc: host
network_mode: host
restart: always
deploy:
resources:
limits:
# cpus: '0.01'
memory: 200M
ulimits:
nofile:
soft: 1024
hard: 524288
ros-master:
env_file:
- .env
image: git.spirirobotics.com/spiri/services-ros1-core:main
command: stdbuf -o L roscore
profiles:
- ros1-master
ipc: host
network_mode: host
restart: always
deploy:
resources:
limits:
memory: 1G
# Madness, setting a low ulimit here fixes memory leaks
# https://answers.ros.org/question/336963/rosout-high-memory-usage/
ulimits:
nofile:
soft: 1024
hard: 524288

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