Compare commits

..

4 Commits

Author SHA1 Message Date
Ramon Roche 63cec47786 feat(simulation): add ROS 2 swarm controller package
ROS 2 package for coordinating PX4 SIH multi-drone swarm missions via
XRCE-DDS. Includes:

- swarm_controller.py: orchestrates N drones through 6 phases
  (EV lock, arm/takeoff, grid hold, circle transition, circle hold,
  kamikaze dive) with DDS cross-talk filtering for multi-instance
  XRCE-DDS setups
- vision_provider.py: publishes noisy VehicleOdometry from ground
  truth for EKF2 external vision fusion (placeholder for non-GPS)
- swarm_launch.py: ROS 2 launch file for N vision providers + controller
- run_swarm.sh: Docker entrypoint that launches PX4 instances,
  XRCE-DDS agent, and ROS 2 nodes
- Dockerfile + build_docker.sh: containerized build with ROS 2 Jazzy,
  px4_msgs from PX4 source, and the swarm package

Validated with 4 drones: all phases complete, 4/4 impacts, ULogs
generated per instance (~16 MB each).

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-16 07:44:43 -07:00
Ramon Roche 8bb863f417 feat(simulation): add swarm infrastructure scripts
- swarm_formation.yaml: default 16-drone 4x4 grid formation config
- swarm_formation_designer.py: CLI to generate/preview formation YAML
  files with grid, circle, line, v, diamond, and random patterns
- sih_swarm_run.sh: launcher for N SIH instances with XRCE-DDS agent,
  reads formation YAML for per-drone positions
- collect_swarm_ulogs.py: collects ULog files from all instances into
  a single output directory with drone ID prefixes
- generate_swarm_ulogs.py: synthetic ULog generator for viewer testing
  with staggered starts, formation phases, and edge cases

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-16 07:44:32 -07:00
Ramon Roche 2ae4ca4bf4 fix(mavlink): remove port cap that forced instances 10+ to share port
The line `[ "$px4_instance" -gt 9 ] && udp_offboard_port_remote=14549`
forced all instances above 9 to share the same offboard port, breaking
multi-instance setups with 10+ drones. Without it, each instance gets
a unique port 14540+i with no overlap.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-16 07:44:19 -07:00
Ramon Roche 2fc70dea3e feat(simulation): add SIH SITL board and quadx_vision airframe
Add px4_sitl_sih board configuration (Gazebo disabled) and airframe
10046_sihsim_quadx_vision for GPS-based SIH multi-instance swarm
simulation. The airframe enables simulated GPS, baro, and mag sensors
with standard quadrotor X configuration.

Signed-off-by: Ramon Roche <mrpollo@gmail.com>
2026-03-16 07:44:12 -07:00
1439 changed files with 36579 additions and 92603 deletions
+2
View File
@@ -141,6 +141,8 @@ Checks: '*,
-cppcoreguidelines-avoid-goto,
-hicpp-avoid-goto,
-bugprone-branch-clone,
-bugprone-unhandled-self-assignment,
-cert-oop54-cpp,
-performance-enum-size,
-readability-avoid-nested-conditional-operator,
-cppcoreguidelines-prefer-member-initializer,
-28
View File
@@ -1,28 +0,0 @@
---
name: commit
description: Create a conventional commit for PX4 changes
disable-model-invocation: true
argument-hint: "[optional: description of changes]"
allowed-tools: Bash, Read, Glob, Grep
---
# PX4 Conventional Commit
Create a git commit: `type(scope): description`
**NEVER add Co-Authored-By lines. No Claude attribution in commits.**
Follow [CONTRIBUTING.md](../../CONTRIBUTING.md) for full project conventions.
## Steps
1. **Read [CONTRIBUTING.md](../../CONTRIBUTING.md)** for commit message format, types, scopes, and conventions.
2. Check branch (`git branch --show-current`). If on `main`, create a feature branch. Use `<username>/<description>` format where `<username>` comes from `gh api user --jq .login`. If unavailable, just use `<description>`.
3. Run `git status` and `git diff --staged`. If nothing staged, ask what to stage.
4. Follow the commit message convention from CONTRIBUTING.md: pick the correct **type** and **scope**, write a concise imperative description.
5. Body (if needed): explain **why**, not what.
6. Run `make format` or `./Tools/astyle/fix_code_style.sh <file>` on changed C/C++ files before committing.
7. Check if GPG signing is available: `git config --get user.signingkey`. If set, use `git commit -S -s`. Otherwise, use `git commit -s`.
8. Stage and commit. No `Co-Authored-By`.
If the user provided arguments, use them as context: $ARGUMENTS
-24
View File
@@ -1,24 +0,0 @@
---
name: pr
description: Create a pull request with conventional commit title and description
disable-model-invocation: true
argument-hint: "[optional: target branch or description]"
allowed-tools: Bash, Read, Glob, Grep
---
# PX4 Pull Request
**No Claude attribution anywhere (no Co-Authored-By, no "Generated with Claude").**
Follow [CONTRIBUTING.md](../../CONTRIBUTING.md) for full project conventions.
## Steps
1. Check branch. If on `main`, create a feature branch. Use `<username>/<description>` format where `<username>` comes from `gh api user --jq .login`. If unavailable, just use `<description>`.
2. Gather context: `git status`, `git log --oneline main..HEAD`, `git diff main...HEAD --stat`, check if remote tracking branch exists.
3. PR **title**: `type(scope): description` — under 72 chars, describes the overall change across all commits. This becomes the squash-merge commit message.
4. PR **body**: brief summary + bullet points for key changes. No filler.
5. Push with `-u` if needed, then `gh pr create`. Default base is `main` unless user says otherwise.
6. Return the PR URL.
If the user provided arguments, use them as context: $ARGUMENTS
-73
View File
@@ -1,73 +0,0 @@
---
name: rebase-onto-main
description: Rebase a branch onto main, handling squash-merged parent branches cleanly
argument-hint: "[optional: branch name, defaults to current branch]"
allowed-tools: Bash, Read, Glob, Grep, Agent
---
# Rebase Branch onto Main
Rebase the current (or specified) branch onto `main`, correctly handling the case where the branch was built on top of another branch that has since been squash-merged into `main`.
## Background
When a parent branch is squash-merged, its individual commits become a single new commit on `main` with a different hash. A normal `git rebase main` will try to replay the parent's original commits, causing messy conflicts. The fix is to **cherry-pick only the commits unique to this branch** onto a fresh branch from `main`.
## Steps
1. **Identify the branch.** Use `$ARGUMENTS` if provided, otherwise use the current branch.
2. **Fetch and update main:**
```
git fetch origin main:main
```
3. **Find the merge base** between the branch and `main`:
```
git merge-base <branch> main
```
4. **List all commits** on the branch since the merge base:
```
git log --oneline <merge-base>..<branch>
```
5. **Identify which commits are unique to this branch** vs. inherited from a parent branch. Look for:
- Squash-merged commits on `main` that correspond to a group of commits at the bottom of the branch's history (check PR titles, commit message keywords).
- The boundary commit: the first commit that belongs to *this* branch's work, not the parent's.
- If ALL commits are unique (no parent branch), just do a normal `git rebase main` and skip the rest.
6. **Create a fresh branch from `main`:**
```
git checkout -b <branch>-rebase main
```
7. **Cherry-pick only the unique commits** (oldest first):
```
git cherry-pick <first-unique-commit>^..<branch>
```
The `A^..B` range means "from the parent of A through B inclusive."
8. **Handle conflicts** if any arise during cherry-pick. Resolve and `git cherry-pick --continue`.
9. **Replace the old branch:**
```
git branch -m <branch> <branch>-old
git branch -m <branch>-rebase <branch>
```
10. **Verify** the result:
```
git log --oneline main..<branch>
```
Confirm only the expected commits are present.
11. **Ask the user** before force-pushing. When approved:
```
git push origin <branch> --force-with-lease
```
12. **Clean up** the old branch:
```
git branch -D <branch>-old
```
+1 -4
View File
@@ -265,8 +265,5 @@ jobs:
with:
draft: true
prerelease: ${{ steps.upload-location.outputs.is_prerelease == 'true' }}
files: |
artifacts/*.px4
artifacts/*.deb
artifacts/**/*.sbom.spdx.json
files: artifacts/*.px4
name: ${{ steps.upload-location.outputs.uploadlocation }}
-2
View File
@@ -46,8 +46,6 @@ jobs:
fetch-depth: 0
- name: Building [${{ matrix.check }}]
env:
PX4_SBOM_DISABLE: 1
run: |
cd "$GITHUB_WORKSPACE"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
+2 -2
View File
@@ -36,8 +36,8 @@ jobs:
px4io/px4-dev-ros-melodic:2021-09-08 \
bash -c '
git config --global --add safe.directory /workspace
PX4_SBOM_DISABLE=1 make px4_sitl_default
PX4_SBOM_DISABLE=1 make px4_sitl_default sitl_gazebo-classic
make px4_sitl_default
make px4_sitl_default sitl_gazebo-classic
./test/rostest_px4_run.sh \
mavros_posix_test_mission.test \
mission:=MC_mission_box \
+2 -2
View File
@@ -36,8 +36,8 @@ jobs:
px4io/px4-dev-ros-melodic:2021-09-08 \
bash -c '
git config --global --add safe.directory /workspace
PX4_SBOM_DISABLE=1 make px4_sitl_default
PX4_SBOM_DISABLE=1 make px4_sitl_default sitl_gazebo-classic
make px4_sitl_default
make px4_sitl_default sitl_gazebo-classic
./test/rostest_px4_run.sh \
mavros_posix_tests_offboard_posctl.test \
vehicle:=iris
+1 -11
View File
@@ -89,15 +89,7 @@ jobs:
. /opt/ros/galactic/setup.bash
mkdir -p /opt/px4_ws/src
cd /opt/px4_ws/src
BRANCH="${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}"
REPO_URL="https://github.com/Auterion/px4-ros2-interface-lib.git"
if git ls-remote --heads "$REPO_URL" "$BRANCH" | grep -q "$BRANCH"; then
echo "Cloning px4-ros2-interface-lib with matching branch: $BRANCH"
git clone --recursive --branch "$BRANCH" "$REPO_URL"
else
echo "Branch '$BRANCH' not found in px4-ros2-interface-lib, using default (main)"
git clone --recursive "$REPO_URL"
fi
git clone --recursive https://github.com/Auterion/px4-ros2-interface-lib.git
# Ignore python packages due to compilation issue (can be enabled when updating ROS)
touch px4-ros2-interface-lib/px4_ros2_py/COLCON_IGNORE || true
touch px4-ros2-interface-lib/examples/python/COLCON_IGNORE || true
@@ -110,8 +102,6 @@ jobs:
run: ccache -s
- name: Build PX4
env:
PX4_SBOM_DISABLE: 1
run: make px4_sitl_default
- name: ccache post-run px4/firmware
run: ccache -s
-42
View File
@@ -1,42 +0,0 @@
name: SBOM License Check
on:
push:
branches:
- 'main'
- 'release/**'
- 'stable'
paths:
- '.gitmodules'
- 'Tools/ci/license-overrides.yaml'
- 'Tools/ci/generate_sbom.py'
pull_request:
branches:
- '**'
paths:
- '.gitmodules'
- 'Tools/ci/license-overrides.yaml'
- 'Tools/ci/generate_sbom.py'
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify-licenses:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: false
- name: Install PyYAML
run: pip install pyyaml --break-system-packages
- name: Verify submodule licenses
run: python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir .
-132
View File
@@ -1,132 +0,0 @@
name: SBOM Monthly Audit
on:
schedule:
# First Monday of each month at 09:00 UTC
- cron: '0 9 1-7 * 1'
workflow_dispatch:
inputs:
branch:
description: 'Branch to audit (leave empty for current)'
required: false
type: string
permissions:
contents: read
issues: write
jobs:
audit:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.branch || github.ref }}
fetch-depth: 1
submodules: recursive
- name: Install PyYAML
run: pip install pyyaml --break-system-packages
- name: Run license verification
id: verify
continue-on-error: true
run: |
python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir . 2>&1 | tee /tmp/sbom-verify.txt
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
- name: Check for issues
id: check
run: |
if grep -q "NOASSERTION" /tmp/sbom-verify.txt; then
echo "has_issues=true" >> "$GITHUB_OUTPUT"
# Extract NOASSERTION lines
grep "NOASSERTION" /tmp/sbom-verify.txt | grep -v "skipped" > /tmp/sbom-issues.txt || true
# Extract copyleft lines
sed -n '/Copyleft licenses detected/,/^$/p' /tmp/sbom-verify.txt > /tmp/sbom-copyleft.txt || true
else
echo "has_issues=false" >> "$GITHUB_OUTPUT"
fi
- name: Create issue if problems found
if: steps.check.outputs.has_issues == 'true'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const fullOutput = fs.readFileSync('/tmp/sbom-verify.txt', 'utf8');
let issueLines = '';
try {
issueLines = fs.readFileSync('/tmp/sbom-issues.txt', 'utf8');
} catch (e) {
issueLines = 'No specific NOASSERTION lines captured.';
}
let copyleftLines = '';
try {
copyleftLines = fs.readFileSync('/tmp/sbom-copyleft.txt', 'utf8');
} catch (e) {
copyleftLines = '';
}
const date = new Date().toISOString().split('T')[0];
const branch = '${{ inputs.branch || github.ref_name }}';
// Check for existing open issue to avoid duplicates
const existing = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'sbom-audit',
state: 'open',
});
if (existing.data.length > 0) {
// Update existing issue with new findings
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existing.data[0].number,
body: `## Monthly audit update (${date})\n\nIssues still present:\n\n\`\`\`\n${issueLines}\n\`\`\`\n${copyleftLines ? `\n### Copyleft warnings\n\`\`\`\n${copyleftLines}\n\`\`\`` : ''}`,
});
return;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `chore(sbom): license audit found NOASSERTION entries on ${branch} (${date})`,
labels: ['sbom-audit'],
assignees: ['mrpollo'],
body: [
`## SBOM Monthly Audit -- ${branch} -- ${date}`,
'',
'The automated SBOM license audit found submodules with unresolved licenses.',
'',
'### NOASSERTION entries',
'',
'```',
issueLines,
'```',
'',
copyleftLines ? `### Copyleft warnings\n\n\`\`\`\n${copyleftLines}\n\`\`\`\n` : '',
'### How to fix',
'',
'1. Check the submodule repo for a LICENSE file',
'2. Add an override to `Tools/ci/license-overrides.yaml`',
'3. Run `python3 Tools/ci/generate_sbom.py --verify-licenses --source-dir .` to confirm',
'',
'### Full output',
'',
'<details>',
'<summary>Click to expand</summary>',
'',
'```',
fullOutput,
'```',
'',
'</details>',
'',
'cc @mrpollo',
].join('\n'),
});
-1
View File
@@ -71,7 +71,6 @@ jobs:
- name: Build PX4
env:
PX4_CMAKE_BUILD_TYPE: ${{matrix.config.build_type}}
PX4_SBOM_DISABLE: 1
run: make px4_sitl_default
- name: Cache Post-Run [px4_sitl_default]
@@ -1,43 +0,0 @@
name: Sync release branch to px4-ros2-interface-lib
on:
create:
workflow_dispatch:
inputs:
branch:
description: 'Release branch name (e.g. release/1.18)'
required: true
type: string
permissions: {}
jobs:
notify-interface-lib:
if: >-
github.repository == 'PX4/PX4-Autopilot' &&
(
(github.event_name == 'create' && github.ref_type == 'branch' && startsWith(github.ref_name, 'release/')) ||
github.event_name == 'workflow_dispatch'
)
runs-on: ubuntu-latest
steps:
- name: Determine branch name
id: params
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
BRANCH="${{ inputs.branch }}"
else
BRANCH="${{ github.ref_name }}"
fi
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "Dispatching for branch: $BRANCH"
- name: Dispatch release branch creation
run: |
BRANCH="${{ steps.params.outputs.branch }}"
curl -s -f -X POST \
-H "Authorization: token ${{ secrets.PX4BUILTBOT_PERSONAL_ACCESS_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/Auterion/px4-ros2-interface-lib/dispatches \
-d "{\"event_type\":\"px4_release_branch\",\"client_payload\":{\"branch\":\"$BRANCH\"}}"
echo "Dispatched px4_release_branch event for $BRANCH"
@@ -1,135 +0,0 @@
name: Tag px4_msgs from PX4 release tags
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
tag_name:
description: 'PX4 tag to propagate (example: v1.17.0)'
required: true
type: string
permissions:
contents: read
jobs:
tag_px4_msgs:
if: github.repository == 'PX4/PX4-Autopilot'
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu22-full-x64,"run-id=${{ github.run_id }}",spot=false]
env:
TAG_NAME: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref_name }}
steps:
- name: Checkout PX4 repo
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup git credentials
run: |
git config --global user.name "${{ secrets.PX4BUILDBOT_USER }}"
git config --global user.email "${{ secrets.PX4BUILDBOT_EMAIL }}"
- name: Resolve release branch from tag
id: tag_info
shell: bash
run: |
set -euo pipefail
if [[ ! "${TAG_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Tag format is not stable vX.Y.Z, skipping: ${TAG_NAME}"
echo "should_run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "should_run=true" >> "$GITHUB_OUTPUT"
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
release_branch="release/${major}.${minor}"
git show-ref --verify --quiet "refs/heads/${release_branch}" || {
echo "PX4 branch ${release_branch} not found"
exit 1
}
tag_date="$(git for-each-ref --format='%(creatordate:iso8601)' "refs/tags/${TAG_NAME}")"
if [[ -z "${tag_date}" ]]; then
echo "Unable to resolve tag date for ${TAG_NAME}"
exit 1
fi
echo "release_branch=${release_branch}" >> "$GITHUB_OUTPUT"
echo "tag_date=${tag_date}" >> "$GITHUB_OUTPUT"
- name: Clone px4_msgs repo
if: steps.tag_info.outputs.should_run == 'true'
run: |
git clone https://${{ secrets.PX4BUILTBOT_PERSONAL_ACCESS_TOKEN }}@github.com/PX4/px4_msgs.git
- name: Checkout matching px4_msgs release branch
if: steps.tag_info.outputs.should_run == 'true'
shell: bash
run: |
set -euo pipefail
cd px4_msgs
release_branch="${{ steps.tag_info.outputs.release_branch }}"
if git show-ref --verify --quiet "refs/remotes/origin/${release_branch}"; then
git checkout -B "${release_branch}" "origin/${release_branch}"
else
echo "px4_msgs branch ${release_branch} does not exist"
exit 1
fi
- name: Verify msg and srv trees are identical
if: steps.tag_info.outputs.should_run == 'true'
shell: bash
run: |
set -euo pipefail
release_branch="${{ steps.tag_info.outputs.release_branch }}"
git checkout "${release_branch}"
# Use the same synchronization logic as sync_to_px4_msgs.yml,
# then verify there are no changes in px4_msgs.
rm -f px4_msgs/msg/*.msg
rm -f px4_msgs/msg/versioned/*.msg
rm -f px4_msgs/srv/*.srv
rm -f px4_msgs/srv/versioned/*.srv
cp msg/*.msg px4_msgs/msg/
cp msg/versioned/*.msg px4_msgs/msg/ || true
cp srv/*.srv px4_msgs/srv/
cp srv/versioned/*.srv px4_msgs/srv/ || true
if ! git -C px4_msgs diff --exit-code -- msg srv; then
echo "Message/service definitions differ between PX4 ${release_branch} and px4_msgs ${release_branch}"
exit 1
fi
- name: Create and push tag in px4_msgs
if: steps.tag_info.outputs.should_run == 'true'
shell: bash
run: |
set -euo pipefail
cd px4_msgs
target="$(git rev-parse HEAD)"
existing_target="$(git rev-parse "refs/tags/${TAG_NAME}^{}" 2>/dev/null || true)"
if [[ -n "${existing_target}" ]]; then
if [[ "${existing_target}" == "${target}" ]]; then
echo "Tag ${TAG_NAME} already exists on ${target}; nothing to do"
exit 0
fi
echo "Tag ${TAG_NAME} already exists on ${existing_target}, expected ${target}"
exit 1
fi
GIT_COMMITTER_DATE="${{ steps.tag_info.outputs.tag_date }}" \
git tag -a "${TAG_NAME}" "${target}" \
-m "PX4 msgs and srvs definitions matching PX4 stable release ${TAG_NAME#v}"
git push origin "refs/tags/${TAG_NAME}"
-3
View File
@@ -112,6 +112,3 @@ keys/
# metadata
_emscripten_sdk/
# virtual Python environment
.venv
+1 -9
View File
@@ -240,15 +240,8 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE ${PX4_BUILD_TYPE} CACHE STRING "Build type" FORCE)
endif()
if(CONFIG_BOARD_SUPPORT_FORTIFIED_TOOLCHAIN)
set(PX4_DEBUG_OPT_LEVEL -Og)
message(STATUS "fortified toolchain support enabled: PX4_DEBUG_OPT_LEVEL=${PX4_DEBUG_OPT_LEVEL}")
else()
set(PX4_DEBUG_OPT_LEVEL -O0)
endif()
if((CMAKE_BUILD_TYPE STREQUAL "Debug") OR (CMAKE_BUILD_TYPE STREQUAL "Coverage"))
set(MAX_CUSTOM_OPT_LEVEL ${PX4_DEBUG_OPT_LEVEL})
set(MAX_CUSTOM_OPT_LEVEL -O0)
elseif(CMAKE_BUILD_TYPE MATCHES "Sanitizer")
set(MAX_CUSTOM_OPT_LEVEL -O1)
elseif(CMAKE_BUILD_TYPE MATCHES "Release")
@@ -491,7 +484,6 @@ include(bloaty)
include(metadata)
include(package)
include(sbom)
# install python requirements using configured python
add_custom_target(install_python_requirements
-10
View File
@@ -67,16 +67,6 @@ menu "Toolchain"
help
Enables Cmake Release for -O3 optimization
config BOARD_SUPPORT_FORTIFIED_TOOLCHAIN
bool "Fortified toolchain support"
default n
help
Enable compatibility with toolchains that define
_FORTIFY_SOURCE.
This switches PX4_DEBUG_OPT_LEVEL from -O0 to -Og. Keep this
disabled unless the fortified toolchain requires optimization.
config BOARD_ROMFSROOT
string "ROMFSROOT"
default "px4fmu_common"
+1 -21
View File
@@ -162,12 +162,6 @@ else
endif
# Prefer the interpreter from an active Python virtual environment.
# Otherwise leave PYTHON_EXECUTABLE unset and let CMake resolve Python.
ifneq ($(strip $(VIRTUAL_ENV)),)
PYTHON_EXECUTABLE ?= $(VIRTUAL_ENV)/bin/python
endif
# Pick up specific Python path if set
ifdef PYTHON_EXECUTABLE
override CMAKE_ARGS += -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}
@@ -232,22 +226,9 @@ CONFIG_TARGETS_DEFAULT := $(patsubst %_default,%,$(filter %_default,$(ALL_CONFIG
$(CONFIG_TARGETS_DEFAULT):
@$(call cmake-build,$@_default$(BUILD_DIR_SUFFIX))
# Multi-processor boards: build all processor targets together
# VOXL2 apps processor (default) depends on SLPI DSP being built first
modalai_voxl2_default: modalai_voxl2_slpi
modalai_voxl2: modalai_voxl2_slpi
modalai_voxl2_deb: modalai_voxl2_slpi
all_config_targets: $(ALL_CONFIG_TARGETS)
all_default_targets: $(CONFIG_TARGETS_DEFAULT)
# DEB package targets: builds _default config, then runs cpack.
# Multi-processor boards (e.g. VOXL2) chain companion builds automatically
# via existing cmake prerequisites.
%_deb:
@$(call cmake-build,$(subst _deb,_default,$@)$(BUILD_DIR_SUFFIX))
@cd "$(SRC_DIR)/build/$(subst _deb,_default,$@)" && cpack -G DEB
updateconfig:
@./Tools/kconfig/updateconfig.py
@@ -554,8 +535,7 @@ validate_module_configs:
-not -path "$(SRC_DIR)/src/modules/zenoh/zenoh-pico/*" \
-not -path "$(SRC_DIR)/src/lib/events/libevents/*" \
-not -path "$(SRC_DIR)/src/lib/cdrstream/*" \
-not -path "$(SRC_DIR)/src/lib/crypto/libtommath/*" \
-not -path "$(SRC_DIR)/src/lib/tensorflow_lite_micro/*" -print0 | \
-not -path "$(SRC_DIR)/src/lib/crypto/libtommath/*" -print0 | \
xargs -0 "$(SRC_DIR)"/Tools/validate_yaml.py --schema-file "$(SRC_DIR)"/validation/module_schema.yaml
# Cleanup
@@ -34,7 +34,6 @@ param set-default PWM_MAIN_FUNC2 102
param set-default PWM_MAIN_FUNC3 103
param set-default PWM_MAIN_FUNC4 104
param set-default SENS_GPS0_DELAY 0
param set-default SENS_GPS1_DELAY 0
param set-default EKF2_GPS_DELAY 0
param set SIH_VEHICLE_TYPE 0
@@ -44,8 +44,7 @@ param set-default PWM_MAIN_FUNC2 202
param set-default PWM_MAIN_FUNC3 203
param set-default PWM_MAIN_FUNC4 101
param set-default SENS_GPS0_DELAY 0
param set-default SENS_GPS1_DELAY 0
param set-default EKF2_GPS_DELAY 0
# Rate controllers
param set-default FW_RR_P 0.0500
@@ -11,8 +11,7 @@
PX4_SIMULATOR=${PX4_SIMULATOR:=sihsim}
PX4_SIM_MODEL=${PX4_SIM_MODEL:=xvert}
param set-default SENS_GPS0_DELAY 0
param set-default SENS_GPS1_DELAY 0
param set-default EKF2_GPS_DELAY 0
param set-default EKF2_FUSE_BETA 0 # side slip fusion is currently not supported for tailsitters
param set-default SENS_EN_GPSSIM 1
@@ -27,8 +27,7 @@ param set-default SENS_EN_BAROSIM 1
param set-default SENS_EN_MAGSIM 1
param set-default SENS_EN_ARSPDSIM 1
param set-default SENS_GPS0_DELAY 0
param set-default SENS_GPS1_DELAY 0
param set-default EKF2_GPS_DELAY 0
param set-default VT_TYPE 2
param set-default MPC_MAN_Y_MAX 60
@@ -18,7 +18,6 @@ param set-default SENS_EN_BAROSIM 1
param set-default SENS_EN_MAGSIM 1
param set SIH_VEHICLE_TYPE 4
param set-default MAV_TYPE 13
# Symmetric hexacopter X clockwise motor numbering
param set-default CA_ROTOR_COUNT 6
@@ -45,5 +44,4 @@ param set-default PWM_MAIN_FUNC4 104
param set-default PWM_MAIN_FUNC5 105
param set-default PWM_MAIN_FUNC6 106
param set-default SENS_GPS0_DELAY 0
param set-default SENS_GPS1_DELAY 0
param set-default EKF2_GPS_DELAY 0
@@ -0,0 +1,38 @@
#!/bin/sh
#
# @name QuadrotorX SITL for SIH (Swarm)
#
# @type Quadrotor
#
# @maintainer Ramon Roche <mrpollo@gmail.com>
#
. ${R}etc/init.d/rc.mc_defaults
PX4_SIMULATOR=${PX4_SIMULATOR:=sihsim}
PX4_SIM_MODEL=${PX4_SIM_MODEL:=quadx}
# Simulated sensors: GPS + baro + mag
param set-default SENS_EN_GPSSIM 1
param set-default SENS_EN_BAROSIM 1
param set-default SENS_EN_MAGSIM 1
# Square quadrotor X PX4 numbering
param set-default CA_ROTOR_COUNT 4
param set-default CA_ROTOR0_PX 1
param set-default CA_ROTOR0_PY 1
param set-default CA_ROTOR1_PX -1
param set-default CA_ROTOR1_PY -1
param set-default CA_ROTOR2_PX 1
param set-default CA_ROTOR2_PY -1
param set-default CA_ROTOR2_KM -0.05
param set-default CA_ROTOR3_PX -1
param set-default CA_ROTOR3_PY 1
param set-default CA_ROTOR3_KM -0.05
param set-default PWM_MAIN_FUNC1 101
param set-default PWM_MAIN_FUNC2 102
param set-default PWM_MAIN_FUNC3 103
param set-default PWM_MAIN_FUNC4 104
param set SIH_VEHICLE_TYPE 0
@@ -20,8 +20,8 @@ param set-default COM_DISARM_LAND 0.5
# EKF2 parameters
param set-default EKF2_DRAG_CTRL 1
param set-default EKF2_IMU_POS_X 0.02
param set-default SENS_GPS0_OFFX 0.055
param set-default SENS_GPS0_OFFZ -0.15
param set-default EKF2_GPS_POS_X 0.055
param set-default EKF2_GPS_POS_Z -0.15
param set-default EKF2_MIN_RNG 0.03
param set-default EKF2_OF_CTRL 1
param set-default EKF2_OF_POS_X 0.055
@@ -107,6 +107,7 @@ px4_add_romfs_files(
10043_sihsim_standard_vtol
10044_sihsim_hex
10045_sihsim_rover_ackermann
10046_sihsim_quadx_vision
17001_flightgear_tf-g1
17002_flightgear_tf-g2
@@ -3,7 +3,6 @@
udp_offboard_port_local=$((14580+px4_instance))
udp_offboard_port_remote=$((14540+px4_instance))
[ "$px4_instance" -gt 9 ] && udp_offboard_port_remote=14549 # use the same ports for more than 10 instances to avoid port overlaps
udp_onboard_payload_port_local=$((14280+px4_instance))
udp_onboard_payload_port_remote=$((14030+px4_instance))
udp_onboard_gimbal_port_local=$((13030+px4_instance))
@@ -2,8 +2,7 @@
# shellcheck disable=SC2154
# EKF2 specifics
param set-default SENS_GPS0_DELAY 10
param set-default SENS_GPS1_DELAY 10
param set-default EKF2_GPS_DELAY 10
param set-default EKF2_MULTI_IMU 3
param set-default SENS_IMU_MODE 0
+3 -4
View File
@@ -119,11 +119,10 @@ else
param set SYS_AUTOCONFIG 1
fi
# To trigger a parameter reset during boot SYS_AUTOCONFIG was set to 1 before
if param greater SYS_AUTOCONFIG 0
if param compare SYS_AUTOCONFIG 1
then
# Reset parameters except airframe, parameter version, sensor calibration, total flight time, flight UUID
param reset_all SYS_AUTOSTART SYS_PARAM_VER CAL_* LND_FLIGHT* TC_* COM_FLIGHT*
# Reset params except Airframe, RC calibration, sensor calibration, flight modes, total flight time, and next flight UUID.
param reset_all SYS_AUTOSTART RC* CAL_* COM_FLTMODE* LND_FLIGHT* TC_* COM_FLIGHT*
set AUTOCNF yes
fi
@@ -19,8 +19,8 @@ param set-default COM_DISARM_LAND 0.5
# EKF2 parameters
param set-default EKF2_DRAG_CTRL 1
param set-default EKF2_IMU_POS_X 0.02
param set-default SENS_GPS0_OFFX 0.055
param set-default SENS_GPS0_OFFZ -0.15
param set-default EKF2_GPS_POS_X 0.055
param set-default EKF2_GPS_POS_Z -0.15
param set-default EKF2_MIN_RNG 0.03
param set-default EKF2_OF_CTRL 1
param set-default EKF2_OF_POS_X 0.055
@@ -19,8 +19,8 @@ param set-default COM_DISARM_LAND 0.5
# EKF2 parameters
param set-default EKF2_DRAG_CTRL 1
param set-default EKF2_IMU_POS_X 0.02
param set-default SENS_GPS0_OFFX 0.055
param set-default SENS_GPS0_OFFZ -0.15
param set-default EKF2_GPS_POS_X 0.055
param set-default EKF2_GPS_POS_Z -0.15
param set-default EKF2_MIN_RNG 0.03
param set-default EKF2_OF_CTRL 1
param set-default EKF2_OF_POS_X 0.055
@@ -47,9 +47,8 @@ param set-default EKF2_BCOEF_Y 25.5
param set-default EKF2_DRAG_CTRL 1
param set-default SENS_GPS0_DELAY 100
param set-default SENS_GPS1_DELAY 100
param set-default SENS_GPS0_OFFX 0.06
param set-default EKF2_GPS_DELAY 100
param set-default EKF2_GPS_POS_X 0.06
param set-default EKF2_GPS_V_NOISE 0.5
param set-default EKF2_IMU_POS_X 0.06
+1 -1
View File
@@ -238,7 +238,7 @@ then
fi
# Start TMP102 temperature sensor
if param compare -s SENS_EN_TMP102 1
if param compare SENS_EN_TMP102 1
then
tmp102 start -X
fi
+7 -10
View File
@@ -188,11 +188,11 @@ else
netman update -i eth0
fi
# To trigger a parameter reset during boot SYS_AUTOCONFIG was set to 1 before
# To trigger a parameter reset during boot SYS_AUTCONFIG was set to 1 before
if param greater SYS_AUTOCONFIG 0
then
# Reset parameters except airframe, parameter version, sensor calibration, total flight time, flight UUID
param reset_all SYS_AUTOSTART SYS_PARAM_VER CAL_* LND_FLIGHT* TC_* COM_FLIGHT*
# Reset parameters except airframe, parameter version, RC calibration, sensor calibration, flight modes, total flight time, flight UUID
param reset_all SYS_AUTOSTART SYS_PARAM_VER RC* CAL_* COM_FLTMODE* LND_FLIGHT* TC_* COM_FLIGHT*
fi
#
@@ -633,15 +633,12 @@ else
#
# Start the VTX services.
#
if ! param compare -s VTX_SER_CFG 0
set RC_VTXTABLE ${R}etc/init.d/rc.vtxtable
if [ -f ${RC_VTXTABLE} ]
then
set RC_VTXTABLE ${R}etc/init.d/rc.vtxtable
if [ -f ${RC_VTXTABLE} ]
then
. ${RC_VTXTABLE}
fi
unset RC_VTXTABLE
. ${RC_VTXTABLE}
fi
unset RC_VTXTABLE
#
# Set additional parameters and env variables for selected AUTOSTART.
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#! /bin/bash
# exit when any command fails
set -e
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
if [[ $# -eq 0 ]] ; then
exit 0
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# Flash PX4 to a device running AuterionOS in the local network
if [ "$1" == "-h" ] || [ "$1" == "--help" ] || [ $# -lt 2 ]; then
echo "Usage: $0 -f <firmware.px4|.elf> [-c <configuration_dir>] -d <IP/Device> [-u <user>] [-p <ssh_port>] [--revert]"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# This script is meant to be used by the build_all.yml workflow in a github runner
# Please only modify if you know what you are doing
set -e
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#! /bin/bash
# Copy a git diff between two commits if msg versioning is added
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
-59
View File
@@ -189,65 +189,6 @@ for manufacturer in sorted(os.scandir(os.path.join(source_dir, '../boards')), ke
if target is not None:
build_configs.append(target)
# Remove companion targets from CI groups (parent target builds them via Make prerequisite)
for manufacturer in sorted(os.scandir(os.path.join(source_dir, '../boards')), key=lambda e: e.name):
if not manufacturer.is_dir():
continue
for board in sorted(os.scandir(manufacturer.path), key=lambda e: e.name):
if not board.is_dir():
continue
companion_file = os.path.join(board.path, 'companion_targets')
if os.path.exists(companion_file):
with open(companion_file) as f:
companions = {l.strip() for l in f if l.strip() and not l.startswith('#')}
for arch in grouped_targets:
for man in grouped_targets[arch]['manufacturers']:
grouped_targets[arch]['manufacturers'][man] = [
t for t in grouped_targets[arch]['manufacturers'][man]
if t not in companions
]
# Append _deb targets for boards that have cmake/package.cmake
for manufacturer in sorted(os.scandir(os.path.join(source_dir, '../boards')), key=lambda e: e.name):
if not manufacturer.is_dir():
continue
if manufacturer.name in excluded_manufacturers:
continue
for board in sorted(os.scandir(manufacturer.path), key=lambda e: e.name):
if not board.is_dir():
continue
board_name = manufacturer.name + '_' + board.name
if board_name in excluded_boards:
continue
package_cmake = os.path.join(board.path, 'cmake', 'package.cmake')
if os.path.exists(package_cmake):
deb_target = board_name + '_deb'
if target_filter and not any(deb_target.startswith(f) for f in target_filter):
continue
# Determine the container and group for this board
container = default_container
if board_name in board_container_overrides:
container = board_container_overrides[board_name]
target_entry = {'target': deb_target, 'container': container}
if args.group:
# Find the group where this board's _default target already lives
default_target = board_name + '_default'
group = None
for g in grouped_targets:
targets_in_group = grouped_targets[g].get('manufacturers', {}).get(manufacturer.name, [])
if default_target in targets_in_group:
group = g
break
if group is None:
group = 'base'
target_entry['arch'] = group
if group not in grouped_targets:
grouped_targets[group] = {'container': container, 'manufacturers': {}}
if manufacturer.name not in grouped_targets[group]['manufacturers']:
grouped_targets[group]['manufacturers'][manufacturer.name] = []
grouped_targets[group]['manufacturers'][manufacturer.name].append(deb_target)
build_configs.append(target_entry)
if(verbose):
import pprint
print("============================")
-603
View File
@@ -1,603 +0,0 @@
#!/usr/bin/env python3
"""Generate SPDX 2.3 JSON SBOM for a PX4 firmware build.
Produces one SBOM per board target containing:
- PX4 firmware as the primary package
- Git submodules as CONTAINS dependencies
- Python build requirements as BUILD_DEPENDENCY_OF packages
- Board-specific modules as CONTAINS packages
Requires PyYAML (pyyaml) for loading license overrides.
"""
import argparse
import configparser
import json
import re
import subprocess
import uuid
from datetime import datetime, timezone
from pathlib import Path
import yaml
# Ordered most-specific first: all keywords must appear for a match.
LICENSE_PATTERNS = [
# Copyleft licenses first (more specific keywords prevent false matches)
("GPL-3.0-only", ["GNU GENERAL PUBLIC LICENSE", "Version 3"]),
("GPL-2.0-only", ["GNU GENERAL PUBLIC LICENSE", "Version 2"]),
("LGPL-3.0-only", ["GNU LESSER GENERAL PUBLIC LICENSE", "Version 3"]),
("LGPL-2.1-only", ["GNU Lesser General Public License", "Version 2.1"]),
("AGPL-3.0-only", ["GNU AFFERO GENERAL PUBLIC LICENSE", "Version 3"]),
# Permissive licenses
("Apache-2.0", ["Apache License", "Version 2.0"]),
("MIT", ["Permission is hereby granted"]),
("BSD-3-Clause", ["Redistribution and use", "Neither the name"]),
("BSD-2-Clause", ["Redistribution and use", "THIS SOFTWARE IS PROVIDED"]),
("ISC", ["Permission to use, copy, modify, and/or distribute"]),
("EPL-2.0", ["Eclipse Public License", "2.0"]),
("Unlicense", ["The Unlicense", "unlicense.org"]),
]
COPYLEFT_LICENSES = {
"GPL-2.0-only", "GPL-3.0-only",
"LGPL-2.1-only", "LGPL-3.0-only",
"AGPL-3.0-only",
}
def load_license_overrides(source_dir):
"""Load license overrides and comments from YAML config file.
Returns (overrides, comments) dicts mapping submodule path to values.
Falls back to empty dicts if the file is missing.
"""
yaml_path = source_dir / "Tools" / "ci" / "license-overrides.yaml"
if not yaml_path.exists():
return {}, {}
with open(yaml_path) as f:
data = yaml.safe_load(f)
overrides = {}
comments = {}
for path, entry in (data.get("overrides") or {}).items():
overrides[path] = entry["license"]
if "comment" in entry:
comments[path] = entry["comment"]
return overrides, comments
LICENSE_FILENAMES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "COPYING", "COPYING.md"]
def detect_license(submodule_dir):
"""Auto-detect SPDX license ID from LICENSE/COPYING file in a directory.
Reads the first 100 lines of the first license file found and matches
keywords against LICENSE_PATTERNS. Returns 'NOASSERTION' if no file
is found or no pattern matches.
"""
for fname in LICENSE_FILENAMES:
license_file = submodule_dir / fname
if license_file.is_file():
try:
lines = license_file.read_text(errors="replace").splitlines()[:100]
text = "\n".join(lines)
except OSError:
continue
text_upper = text.upper()
for spdx_id_val, keywords in LICENSE_PATTERNS:
if all(kw.upper() in text_upper for kw in keywords):
return spdx_id_val
return "NOASSERTION"
return "NOASSERTION"
def get_submodule_license(source_dir, sub_path, license_overrides):
"""Return the SPDX license for a submodule: override > auto-detect."""
if sub_path in license_overrides:
return license_overrides[sub_path]
return detect_license(source_dir / sub_path)
def spdx_id(name: str) -> str:
"""Convert a name to a valid SPDX identifier (letters, digits, dots, hyphens)."""
return re.sub(r"[^a-zA-Z0-9.\-]", "-", name)
def parse_gitmodules(source_dir):
"""Parse .gitmodules and return list of {name, path, url}."""
gitmodules_path = source_dir / ".gitmodules"
if not gitmodules_path.exists():
return []
config = configparser.ConfigParser()
config.read(str(gitmodules_path))
submodules = []
for section in config.sections():
if section.startswith("submodule "):
name = section.split('"')[1] if '"' in section else section.split(" ", 1)[1]
path = config.get(section, "path", fallback="")
url = config.get(section, "url", fallback="")
submodules.append({"name": name, "path": path, "url": url})
return submodules
def get_submodule_commits(source_dir):
"""Get commit hashes for all submodules via git ls-tree -r (works without init)."""
try:
result = subprocess.run(
["git", "ls-tree", "-r", "HEAD"],
cwd=str(source_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError):
return {}
commits = {}
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) >= 4 and parts[1] == "commit":
commits[parts[3]] = parts[2]
return commits
def get_git_info(source_dir: Path) -> dict:
"""Get PX4 git version and hash."""
info = {"version": "unknown", "hash": "unknown"}
try:
result = subprocess.run(
["git", "describe", "--always", "--tags", "--dirty"],
cwd=str(source_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
info["version"] = result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
try:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=str(source_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
info["hash"] = result.stdout.strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
return info
def parse_requirements(requirements_path):
"""Parse pip requirements.txt into list of {name, version_spec}."""
if not requirements_path.exists():
return []
deps = []
for line in requirements_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or line.startswith("-"):
continue
# Split on version specifiers
match = re.match(r"^([a-zA-Z0-9_\-]+)(.*)?$", line)
if match:
deps.append({
"name": match.group(1),
"version_spec": match.group(2).strip() if match.group(2) else "",
})
return deps
def read_module_list(modules_file, source_dir):
"""Read board-specific module list from file.
Paths may be absolute; they are converted to relative paths under src/.
Duplicates are removed while preserving order.
"""
if not modules_file or not modules_file.exists():
return []
seen = set()
modules = []
source_str = str(source_dir.resolve()) + "/"
for line in modules_file.read_text().splitlines():
path = line.strip()
if not path or path.startswith("#"):
continue
# Convert absolute path to relative
if path.startswith(source_str):
path = path[len(source_str):]
if path not in seen:
seen.add(path)
modules.append(path)
return modules
def make_purl(pkg_type: str, namespace: str, name: str, version: str = "") -> str:
"""Construct a Package URL (purl)."""
purl = f"pkg:{pkg_type}/{namespace}/{name}"
if version:
purl += f"@{version}"
return purl
def extract_git_host_org_repo(url):
"""Extract host type, org, and repo from a git URL.
Returns (host, org, repo) where host is 'github', 'gitlab', or ''.
"""
match = re.search(r"github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$", url)
if match:
return "github", match.group(1), match.group(2)
match = re.search(r"gitlab\.com[:/](.+?)/([^/]+?)(?:\.git)?$", url)
if match:
return "gitlab", match.group(1), match.group(2)
return "", "", ""
def generate_sbom(source_dir, board, modules_file, compiler, platform=""):
"""Generate a complete SPDX 2.3 JSON document."""
license_overrides, license_comments = load_license_overrides(source_dir)
git_info = get_git_info(source_dir)
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Deterministic namespace using UUID5 from git hash + board
ns_seed = f"{git_info['hash']}:{board}"
doc_namespace = f"https://spdx.org/spdxdocs/{board}-{uuid.uuid5(uuid.NAMESPACE_URL, ns_seed)}"
doc = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": f"PX4 Firmware SBOM for {board}",
"documentNamespace": doc_namespace,
"creationInfo": {
"created": timestamp,
"creators": [
"Tool: px4-generate-sbom",
"Organization: Dronecode Foundation",
],
"licenseListVersion": "3.22",
},
"packages": [],
"relationships": [],
}
# Primary package: PX4 firmware
primary_spdx_id = f"SPDXRef-PX4-{spdx_id(board)}"
doc["packages"].append({
"SPDXID": primary_spdx_id,
"name": board,
"versionInfo": git_info["version"],
"packageFileName": f"{board}.px4",
"supplier": "Organization: Dronecode Foundation",
"downloadLocation": "https://github.com/PX4/PX4-Autopilot",
"filesAnalyzed": False,
"primaryPackagePurpose": "FIRMWARE",
"licenseConcluded": "BSD-3-Clause",
"licenseDeclared": "BSD-3-Clause",
"copyrightText": "Copyright (c) PX4 Development Team",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": make_purl(
"github", "PX4", "PX4-Autopilot", git_info["version"]
),
}
],
})
doc["relationships"].append({
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": primary_spdx_id,
})
# Git submodules (filtered to those relevant to this board's modules)
submodules = parse_gitmodules(source_dir)
submodule_commits = get_submodule_commits(source_dir)
modules = read_module_list(modules_file, source_dir)
def submodule_is_relevant(sub_path):
"""A submodule is relevant if any board module path overlaps with it."""
# NuttX platform submodules are only relevant for NuttX builds
if sub_path.startswith("platforms/nuttx/"):
return platform in ("nuttx", "")
if not modules:
return True # no module list means include all
# Other platform submodules are always relevant
if sub_path.startswith("platforms/"):
return True
for mod in modules:
# Module is under this submodule, or submodule is under a module
if mod.startswith(sub_path + "/") or sub_path.startswith(mod + "/"):
return True
return False
for sub in submodules:
if not submodule_is_relevant(sub["path"]):
continue
sub_path = sub["path"]
sub_path_id = sub_path.replace("/", "-")
sub_spdx_id = f"SPDXRef-Submodule-{spdx_id(sub_path_id)}"
commit = submodule_commits.get(sub_path, "unknown")
license_id = get_submodule_license(source_dir, sub_path, license_overrides)
host, org, repo = extract_git_host_org_repo(sub["url"])
download = sub["url"] if sub["url"] else "NOASSERTION"
# Use repo name from URL for human-readable name, fall back to last path component
display_name = repo if repo else sub_path.rsplit("/", 1)[-1]
pkg = {
"SPDXID": sub_spdx_id,
"name": display_name,
"versionInfo": commit,
"supplier": f"Organization: {org}" if org else "NOASSERTION",
"downloadLocation": download,
"filesAnalyzed": False,
"licenseConcluded": license_id,
"licenseDeclared": license_id,
"copyrightText": "NOASSERTION",
}
comment = license_comments.get(sub_path)
if comment:
pkg["licenseComments"] = comment
if host and org and repo:
pkg["externalRefs"] = [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": make_purl(host, org, repo, commit),
}
]
doc["packages"].append(pkg)
doc["relationships"].append({
"spdxElementId": primary_spdx_id,
"relationshipType": "CONTAINS",
"relatedSpdxElement": sub_spdx_id,
})
# Python build dependencies
requirements_path = source_dir / "Tools" / "setup" / "requirements.txt"
py_deps = parse_requirements(requirements_path)
for dep in py_deps:
dep_name = dep["name"]
dep_spdx_id = f"SPDXRef-PyDep-{spdx_id(dep_name)}"
version_str = dep["version_spec"] if dep["version_spec"] else "NOASSERTION"
doc["packages"].append({
"SPDXID": dep_spdx_id,
"name": dep_name,
"versionInfo": version_str,
"supplier": "NOASSERTION",
"downloadLocation": f"https://pypi.org/project/{dep_name}/",
"filesAnalyzed": False,
"primaryPackagePurpose": "APPLICATION",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": f"pkg:pypi/{dep_name}",
}
],
})
doc["relationships"].append({
"spdxElementId": dep_spdx_id,
"relationshipType": "BUILD_DEPENDENCY_OF",
"relatedSpdxElement": primary_spdx_id,
})
# Board-specific modules (already read above for submodule filtering)
for mod in modules:
mod_path_id = mod.replace("/", "-")
mod_spdx_id = f"SPDXRef-Module-{spdx_id(mod_path_id)}"
# Derive short name: strip leading src/ for readability
display_name = mod
if display_name.startswith("src/"):
display_name = display_name[4:]
doc["packages"].append({
"SPDXID": mod_spdx_id,
"name": display_name,
"versionInfo": git_info["version"],
"supplier": "Organization: Dronecode Foundation",
"downloadLocation": "https://github.com/PX4/PX4-Autopilot",
"filesAnalyzed": False,
"licenseConcluded": "BSD-3-Clause",
"licenseDeclared": "BSD-3-Clause",
"copyrightText": "NOASSERTION",
})
doc["relationships"].append({
"spdxElementId": primary_spdx_id,
"relationshipType": "CONTAINS",
"relatedSpdxElement": mod_spdx_id,
})
# Compiler as a build tool
if compiler:
compiler_spdx_id = f"SPDXRef-Compiler-{spdx_id(compiler)}"
doc["packages"].append({
"SPDXID": compiler_spdx_id,
"name": compiler,
"versionInfo": "NOASSERTION",
"supplier": "NOASSERTION",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": False,
"primaryPackagePurpose": "APPLICATION",
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
"copyrightText": "NOASSERTION",
})
doc["relationships"].append({
"spdxElementId": compiler_spdx_id,
"relationshipType": "BUILD_TOOL_OF",
"relatedSpdxElement": primary_spdx_id,
})
return doc
def verify_licenses(source_dir):
"""Verify license detection for all submodules. Returns exit code."""
license_overrides, _ = load_license_overrides(source_dir)
submodules = parse_gitmodules(source_dir)
if not submodules:
print("No submodules found in .gitmodules")
return 1
has_noassertion = False
print(f"{'Submodule Path':<65} {'Detected':<16} {'Override':<16} {'Final'}")
print("-" * 115)
for sub in submodules:
sub_path = sub["path"]
sub_dir = source_dir / sub_path
checked_out = sub_dir.is_dir() and any(sub_dir.iterdir())
if not checked_out:
detected = "(not checked out)"
override = license_overrides.get(sub_path, "")
final = override if override else "NOASSERTION"
else:
detected = detect_license(sub_dir)
override = license_overrides.get(sub_path, "")
final = override if override else detected
if final == "NOASSERTION" and checked_out:
has_noassertion = True
marker = " <-- NOASSERTION"
elif final == "NOASSERTION" and not checked_out:
marker = " (skipped)"
else:
marker = ""
print(f"{sub_path:<65} {str(detected):<16} {str(override) if override else '':<16} {final}{marker}")
# Copyleft warning (informational, not a failure)
copyleft_found = []
for sub in submodules:
sub_path = sub["path"]
sub_dir = source_dir / sub_path
checked_out = sub_dir.is_dir() and any(sub_dir.iterdir())
override = license_overrides.get(sub_path, "")
if checked_out:
final_lic = override if override else detect_license(sub_dir)
else:
final_lic = override if override else "NOASSERTION"
for cl in COPYLEFT_LICENSES:
if cl in final_lic:
copyleft_found.append((sub_path, final_lic))
break
print()
if copyleft_found:
print("Copyleft licenses detected (informational):")
for path, lic in copyleft_found:
print(f" {path}: {lic}")
print()
if has_noassertion:
print("FAIL: Some submodules resolved to NOASSERTION. "
"Add an entry to Tools/ci/license-overrides.yaml or check the LICENSE file.")
return 1
print("OK: All submodules have a resolved license.")
return 0
def main():
parser = argparse.ArgumentParser(
description="Generate SPDX 2.3 JSON SBOM for PX4 firmware"
)
parser.add_argument(
"--source-dir",
type=Path,
default=Path.cwd(),
help="PX4 source directory (default: cwd)",
)
parser.add_argument(
"--verify-licenses",
action="store_true",
help="Verify license detection for all submodules and exit",
)
parser.add_argument(
"--board",
default=None,
help="Board target name (e.g. px4_fmu-v5x_default)",
)
parser.add_argument(
"--modules-file",
type=Path,
default=None,
help="Path to config_module_list.txt",
)
parser.add_argument(
"--compiler",
default="",
help="Compiler identifier (e.g. arm-none-eabi-gcc)",
)
parser.add_argument(
"--platform",
default="",
help="PX4 platform (nuttx, posix, qurt). Filters platform-specific submodules.",
)
parser.add_argument(
"--output",
type=Path,
default=None,
help="Output SBOM file path",
)
args = parser.parse_args()
if args.verify_licenses:
raise SystemExit(verify_licenses(args.source_dir))
if not args.board:
parser.error("--board is required when not using --verify-licenses")
if not args.output:
parser.error("--output is required when not using --verify-licenses")
sbom = generate_sbom(
source_dir=args.source_dir,
board=args.board,
modules_file=args.modules_file,
compiler=args.compiler,
platform=args.platform,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w") as f:
json.dump(sbom, f, indent=2)
f.write("\n")
pkg_count = len(sbom["packages"])
print(f"SBOM generated: {args.output} ({pkg_count} packages)")
if __name__ == "__main__":
main()
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env python3
"""Inspect a PX4 SPDX SBOM file.
Usage:
inspect_sbom.py <sbom.spdx.json> # full summary
inspect_sbom.py <sbom.spdx.json> search <term> # search packages by name
inspect_sbom.py <sbom.spdx.json> ntia # NTIA minimum elements check
inspect_sbom.py <sbom.spdx.json> licenses # license summary
inspect_sbom.py <sbom.spdx.json> list <type> # list packages (Submodule|PyDep|Module|all)
"""
import json
import sys
from collections import Counter
from pathlib import Path
def load(path):
return json.loads(Path(path).read_text())
def pkg_type(pkg):
spdx_id = pkg["SPDXID"]
for prefix in ("Submodule", "PyDep", "Module", "Compiler", "PX4"):
if f"-{prefix}-" in spdx_id or spdx_id.startswith(f"SPDXRef-{prefix}"):
return prefix
return "Other"
def summary(doc):
print(f"spdxVersion: {doc['spdxVersion']}")
print(f"name: {doc['name']}")
print(f"namespace: {doc['documentNamespace']}")
print(f"created: {doc['creationInfo']['created']}")
print(f"creators: {', '.join(doc['creationInfo']['creators'])}")
print()
types = Counter(pkg_type(p) for p in doc["packages"])
print(f"Packages: {len(doc['packages'])}")
for t, c in types.most_common():
print(f" {t}: {c}")
print()
rc = Counter(r["relationshipType"] for r in doc["relationships"])
print(f"Relationships: {len(doc['relationships'])}")
for t, n in rc.most_common():
print(f" {t}: {n}")
print()
primary = doc["packages"][0]
print(f"Primary package:")
print(f" name: {primary['name']}")
print(f" version: {primary['versionInfo']}")
print(f" purpose: {primary.get('primaryPackagePurpose', 'N/A')}")
print(f" license: {primary['licenseDeclared']}")
print()
noassert = [
p["name"]
for p in doc["packages"]
if pkg_type(p) == "Submodule" and p["licenseDeclared"] == "NOASSERTION"
]
if noassert:
print(f"WARNING: {len(noassert)} submodules with NOASSERTION license:")
for n in noassert:
print(f" - {n}")
else:
print("All submodule licenses mapped")
print(f"\nFile size: {Path(sys.argv[1]).stat().st_size // 1024}KB")
def search(doc, term):
term = term.lower()
found = [p for p in doc["packages"] if term in p["name"].lower()]
if not found:
print(f"No packages matching '{term}'")
return
print(f"Found {len(found)} packages matching '{term}':\n")
for p in found:
print(json.dumps(p, indent=2))
print()
def ntia_check(doc):
required = ["SPDXID", "name", "versionInfo", "supplier", "downloadLocation"]
missing = []
for p in doc["packages"]:
for f in required:
if f not in p or p[f] in ("", None):
missing.append((p["name"], f))
if missing:
print(f"FAIL: {len(missing)} missing fields:")
for name, field in missing:
print(f" {name}: missing {field}")
else:
print(f"PASS: All {len(doc['packages'])} packages have required fields")
print(f"\nCreators: {doc['creationInfo']['creators']}")
print(f"Timestamp: {doc['creationInfo']['created']}")
rels = [r for r in doc["relationships"] if r["relationshipType"] == "DESCRIBES"]
print(f"DESCRIBES relationships: {len(rels)}")
return len(missing) == 0
def licenses(doc):
by_license = {}
for p in doc["packages"]:
lic = p.get("licenseDeclared", "NOASSERTION")
by_license.setdefault(lic, []).append(p["name"])
for lic in sorted(by_license.keys()):
names = by_license[lic]
print(f"\n{lic} ({len(names)}):")
for n in sorted(names):
print(f" {n}")
def list_packages(doc, filter_type):
filter_type = filter_type.lower()
for p in sorted(doc["packages"], key=lambda x: x["name"]):
t = pkg_type(p)
if filter_type != "all" and t.lower() != filter_type:
continue
lic = p.get("licenseDeclared", "?")
ver = p["versionInfo"][:20] if len(p["versionInfo"]) > 20 else p["versionInfo"]
print(f" {t:10s} {p['name']:50s} {ver:20s} {lic}")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
doc = load(sys.argv[1])
cmd = sys.argv[2] if len(sys.argv) > 2 else "summary"
if cmd == "summary":
summary(doc)
elif cmd == "search":
if len(sys.argv) < 4:
print("Usage: inspect_sbom.py <file> search <term>")
sys.exit(1)
search(doc, sys.argv[3])
elif cmd == "ntia":
if not ntia_check(doc):
sys.exit(1)
elif cmd == "licenses":
licenses(doc)
elif cmd == "list":
filter_type = sys.argv[3] if len(sys.argv) > 3 else "all"
list_packages(doc, filter_type)
else:
print(f"Unknown command: {cmd}")
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
main()
-56
View File
@@ -1,56 +0,0 @@
# SPDX license overrides for submodules where auto-detection fails or is wrong.
# Each entry maps a submodule path to its SPDX license identifier and an
# optional comment explaining why the override exists.
#
# Run `python3 Tools/ci/generate_sbom.py --verify-licenses` to validate.
overrides:
src/modules/mavlink/mavlink:
license: "LGPL-3.0-only AND MIT"
comment: "Generator is LGPL-3.0; PX4 ships only MIT-licensed generated headers."
src/lib/cdrstream/cyclonedds:
license: "EPL-2.0 OR BSD-3-Clause"
comment: >-
Dual-licensed. PX4 elects BSD-3-Clause.
No board currently enables CONFIG_LIB_CDRSTREAM.
src/lib/cdrstream/rosidl:
license: "Apache-2.0"
src/lib/crypto/monocypher:
license: "BSD-2-Clause OR CC0-1.0"
comment: >-
Dual-licensed. LICENCE.md offers BSD-2-Clause with CC0-1.0 as
public domain fallback.
src/lib/crypto/libtomcrypt:
license: "Unlicense"
comment: "Public domain dedication. Functionally equivalent to Unlicense."
src/lib/crypto/libtommath:
license: "Unlicense"
comment: "Public domain dedication. Functionally equivalent to Unlicense."
platforms/nuttx/NuttX/nuttx:
license: "Apache-2.0"
comment: >-
Composite LICENSE (6652 lines) includes BSD/MIT/ISC sub-components.
Primary license is Apache-2.0. NOTICE file contains FAT LFN patent warnings.
platforms/nuttx/NuttX/apps:
license: "Apache-2.0"
boards/modalai/voxl2/libfc-sensor-api:
license: "NOASSERTION"
comment: >-
No LICENSE file in repo. README describes it as public interface
for proprietary sensor library.
boards/modalai/voxl2/src/lib/mpa/libmodal-json:
license: "LGPL-3.0-only"
comment: "LGPL-3.0 weak copyleft. Used via header includes in VOXL2 mpa library."
boards/modalai/voxl2/src/lib/mpa/libmodal-pipe:
license: "LGPL-3.0-only"
comment: "LGPL-3.0 weak copyleft. Used via header includes in VOXL2 mpa library."
+1 -4
View File
@@ -1,9 +1,8 @@
#!/usr/bin/env bash
#!/bin/bash
mkdir artifacts
cp **/**/*.px4 artifacts/ 2>/dev/null || true
cp **/**/*.elf artifacts/ 2>/dev/null || true
cp **/**/*.deb artifacts/ 2>/dev/null || true
for build_dir_path in build/*/ ; do
build_dir_path=${build_dir_path::${#build_dir_path}-1}
build_dir=${build_dir_path#*/}
@@ -29,8 +28,6 @@ for build_dir_path in build/*/ ; do
# Events
mkdir -p artifacts/$build_dir/events/
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/events/ 2>/dev/null || true
# SBOM
cp $build_dir_path/*.sbom.spdx.json artifacts/$build_dir/ 2>/dev/null || true
ls -la artifacts/$build_dir
echo "----------"
done
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# This script runs the fuzz tests from a given binary for a certain amount of time
set -e
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#! /bin/bash
# Copy msgs and the message translation node into a ROS workspace directory
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#! /bin/bash
if [ -z ${PX4_DOCKER_REPO+x} ]; then
PX4_DOCKER_REPO="px4io/px4-dev:v1.17.0-beta1"
+3 -13
View File
@@ -128,9 +128,6 @@ class SourceParser:
# start waiting for the next part - long comment.
if state == "wait-short-end":
state = "wait-long"
elif state == "wait-long-end":
# Preserve paragraph breaks in long description
long_desc += "\n"
else:
m = self.re_comment_tag.match(comment_content)
if m:
@@ -211,7 +208,8 @@ class SourceParser:
raise Exception('short description too long (150 max, is {:}, parameter: {:})'.format(len(short_desc), name))
param.fields["short_desc"] = self.re_remove_dots.sub('', short_desc)
if long_desc is not None:
param.fields["long_desc"] = long_desc.rstrip('\n')
long_desc = self.re_remove_carriage_return.sub(' ', long_desc)
param.fields["long_desc"] = long_desc
for tag in tags:
if tag == "group":
group = tags[tag]
@@ -409,15 +407,7 @@ def generate_yaml(filename: str, groups: list[ParameterGroup]) -> str:
g["definitions"][parameter.name] = p
data["parameters"].append(g)
# Use block scalar style for multi-line strings
class BlockStyleDumper(yaml.SafeDumper):
pass
def str_representer(dumper, data):
if '\n' in data:
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
BlockStyleDumper.add_representer(str, str_representer)
return yaml.dump(data, Dumper=BlockStyleDumper, sort_keys=False)
return yaml.dump(data, sort_keys=False)
def main():
+1 -4
View File
@@ -108,7 +108,7 @@ def parse_yaml_parameters_config(yaml_config, ethernet_supported):
tags = '@group {:}'.format(param_group)
if param['type'] == 'enum':
param_type = 'INT32'
for key in sorted(param['values'], key=float):
for key in param['values']:
tags += '\n * @value {:} {:}'.format(key, param['values'][key])
elif param['type'] == 'bitmask':
param_type = 'INT32'
@@ -124,9 +124,6 @@ def parse_yaml_parameters_config(yaml_config, ethernet_supported):
param_type = 'INT32'
elif param['type'] == 'float':
param_type = 'FLOAT'
if 'values' in param:
for key in sorted(param['values'], key=float):
tags += '\n * @value {:} {:}'.format(key, param['values'][key])
else:
raise Exception("unknown param type {:}".format(param['type']))
+1 -3
View File
@@ -316,9 +316,7 @@ Param | Units | Range/Enum | Description
if val.minValue or val.maxValue:
rangeVal = f"[{val.minValue if val.minValue else '-'} : {val.maxValue if val.maxValue else '-' }]"
units_str = ", ".join(val.units)
enums_str = ', '.join("[{}](#{})".format(e, e) for e in val.enums)
output+=f"{i} | {units_str}|{enums_str}{rangeVal} | {val.description}\n"
output+=f"{i} | {", ".join(val.units)}|{', '.join(f"[{e}](#{e})" for e in val.enums)}{rangeVal} | {val.description}\n"
else:
output+=f"{i} | | | ?\n"
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# Script to run ShellCheck (a static analysis tool for shell scripts) over a
# script directory
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
GREEN='\033[0;32m'
NO_COLOR='\033[0m' # No Color
-7
View File
@@ -79,13 +79,6 @@ if [[ $INSTALL_SIM == "--sim-tools" ]]; then
elif [[ $REINSTALL_FORMULAS == "--reinstall" ]]; then
brew reinstall px4-sim
fi
# jMAVSim requires a JDK (Java 17 LTS recommended)
if ! brew ls --versions openjdk@17 > /dev/null; then
echo "[macos.sh] Installing OpenJDK 17 (required for jMAVSim)"
brew install openjdk@17
sudo ln -sfn $(brew --prefix openjdk@17)/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
fi
fi
echo "[macos.sh] All set! The PX4 Autopilot toolchain was installed."
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Collect ULog files from SIH swarm simulation instances.
Gathers .ulg files from per-instance log directories and copies them
into a single output folder with filenames that identify the source instance.
"""
import argparse
import glob
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path
def find_repo_root() -> Path:
"""Walk up from script location to find the repository root."""
path = Path(__file__).resolve().parent
while path != path.parent:
if (path / "ROMFS").is_dir() and (path / "src").is_dir():
return path
path = path.parent
return Path(__file__).resolve().parent.parent.parent
def parse_instance_number(filepath: str) -> int | None:
"""Extract the instance number from a path containing instance_N."""
parts = Path(filepath).parts
for part in parts:
if part.startswith("instance_"):
try:
return int(part.split("_", 1)[1])
except (ValueError, IndexError):
continue
return None
def collect_ulogs(build_dir: Path, output_dir: Path, max_instances: int | None) -> None:
"""Find and copy ULog files from swarm instance directories."""
pattern = str(build_dir / "instance_*" / "log" / "**" / "*.ulg")
ulog_files = sorted(glob.glob(pattern, recursive=True))
if not ulog_files:
print(f"No ULog files found matching: {pattern}")
print()
print("Troubleshooting:")
print(f" - Verify build directory exists: {build_dir}")
print(f" - Check for instance_* subdirectories in {build_dir}")
print(" - Ensure the swarm simulation has run and produced logs")
sys.exit(1)
# Filter by instance count if specified.
if max_instances is not None:
ulog_files = [
f for f in ulog_files
if (n := parse_instance_number(f)) is not None and n < max_instances
]
if not ulog_files:
assert max_instances is not None
print(f"No ULog files found for the requested instance range (0..{max_instances - 1}).")
sys.exit(1)
output_dir.mkdir(parents=True, exist_ok=True)
total_size = 0
instances_seen: set[int] = set()
copied = 0
for src in ulog_files:
instance = parse_instance_number(src)
if instance is None:
print(f" skipping (could not determine instance): {src}")
continue
instances_seen.add(instance)
original_name = Path(src).name
dest_name = f"drone_{instance:02d}_{original_name}"
dest_path = output_dir / dest_name
shutil.copy2(src, dest_path)
file_size = os.path.getsize(src)
total_size += file_size
copied += 1
print(f" [{instance:02d}] {original_name} ({file_size / 1024:.1f} KB)")
print()
print("Summary")
print("-" * 40)
print(f" Files collected : {copied}")
print(f" Instances : {sorted(instances_seen)}")
print(f" Total size : {total_size / (1024 * 1024):.2f} MB")
print(f" Output directory: {output_dir.resolve()}")
def main() -> None:
repo_root = find_repo_root()
default_build = str(repo_root / "build" / "px4_sitl_sih")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
parser = argparse.ArgumentParser(
description="Collect ULog files from SIH swarm simulation instances.",
)
parser.add_argument(
"--build-dir",
default=default_build,
help=f"Path to the build directory (default: {default_build})",
)
parser.add_argument(
"--output-dir",
default=None,
help="Output directory (default: swarm_ulogs_YYYYMMDD_HHMMSS)",
)
parser.add_argument(
"--instances",
type=int,
default=None,
help="Number of instances to collect from (default: auto-detect)",
)
args = parser.parse_args()
build_dir = Path(args.build_dir)
if not build_dir.is_dir():
print(f"Error: build directory does not exist: {build_dir}")
sys.exit(1)
if args.output_dir is not None:
output_dir = Path(args.output_dir)
else:
output_dir = Path(f"swarm_ulogs_{timestamp}")
print(f"Collecting ULog files from: {build_dir}")
print(f"Output directory: {output_dir}")
print()
collect_ulogs(build_dir, output_dir, args.instances)
if __name__ == "__main__":
main()
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
#
# Setup environment to make PX4 visible to Gazebo.
#
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/bin/bash
# run multiple instances of the 'px4' binary, with the gazebo SITL simulation
# It assumes px4 is already built, with 'make px4_sitl_default sitl_gazebo-classic'
+487
View File
@@ -0,0 +1,487 @@
#!/usr/bin/env python3
"""Generate synthetic ULog files for testing mavsim-viewer swarm replay.
Creates N drone ULog files with crafted trajectories in valid ULog binary
format, without requiring any real PX4 simulation.
"""
import argparse
import json
import math
import os
import struct
# ---------------------------------------------------------------------------
# ULog binary writer
# ---------------------------------------------------------------------------
ULOG_MAGIC = b'\x55\x4c\x6f\x67\x01\x12\x35'
ULOG_VERSION = 1
class ULogWriter:
"""Write a minimal but valid ULog binary file."""
def __init__(self, path: str, timestamp_us: int = 0):
self._path = path
self._fp = open(path, 'wb')
self._msg_id_counter = 0
self._subscriptions: dict[str, int] = {} # topic_name -> msg_id
self._write_header(timestamp_us)
self._write_flag_bits()
# -- low-level helpers --------------------------------------------------
def _write_raw(self, data: bytes):
self._fp.write(data)
def _write_message(self, msg_type: int, payload: bytes):
"""Write a ULog message: uint16 size + uint8 type + payload."""
self._write_raw(struct.pack('<HB', len(payload), msg_type))
self._write_raw(payload)
def _write_header(self, timestamp_us: int):
self._write_raw(ULOG_MAGIC)
self._write_raw(struct.pack('<BQ', ULOG_VERSION, timestamp_us))
def _write_flag_bits(self):
# 'B' message: 8 compat + 8 incompat + 3*uint64 appended offsets
payload = b'\x00' * 8 + b'\x00' * 8 + struct.pack('<QQQ', 0, 0, 0)
self._write_message(0x42, payload)
# -- definition messages ------------------------------------------------
def write_format(self, fmt_string: str):
"""Write a Format ('F') definition message.
fmt_string example: "vehicle_local_position:uint64_t timestamp;float x;float y"
"""
payload = fmt_string.encode('ascii')
self._write_message(0x46, payload)
def write_info(self, key: str, value: str):
"""Write an Info ('I') message with a string value."""
key_with_type = f"char[{len(value)}] {key}"
key_bytes = key_with_type.encode('ascii')
value_bytes = value.encode('ascii')
payload = struct.pack('<B', len(key_bytes)) + key_bytes + value_bytes
self._write_message(0x49, payload)
def add_subscription(self, topic_name: str, multi_id: int = 0) -> int:
"""Write an AddSubscription ('A') message. Returns the assigned msg_id."""
msg_id = self._msg_id_counter
self._msg_id_counter += 1
self._subscriptions[topic_name] = msg_id
name_bytes = topic_name.encode('ascii')
payload = struct.pack('<BH', multi_id, msg_id) + name_bytes
self._write_message(0x41, payload)
return msg_id
def write_data(self, topic_name: str, data_bytes: bytes):
"""Write a Data ('D') message for a subscribed topic.
data_bytes must be the fully serialized payload (starting with
uint64 timestamp) matching the format definition.
"""
msg_id = self._subscriptions[topic_name]
payload = struct.pack('<H', msg_id) + data_bytes
self._write_message(0x44, payload)
def close(self):
self._fp.close()
# ---------------------------------------------------------------------------
# Topic serialization helpers
# ---------------------------------------------------------------------------
def pack_vehicle_local_position(timestamp_us: int, x: float, y: float, z: float,
vx: float, vy: float, vz: float,
ref_lat: float, ref_lon: float, ref_alt: float) -> bytes:
return struct.pack('<Qffffffddf',
timestamp_us, x, y, z, vx, vy, vz,
ref_lat, ref_lon, ref_alt)
def pack_vehicle_attitude(timestamp_us: int, q: tuple[float, ...]) -> bytes:
return struct.pack('<Qffff', timestamp_us, q[0], q[1], q[2], q[3])
def pack_vehicle_global_position(timestamp_us: int, lat: float, lon: float, alt: float) -> bytes:
return struct.pack('<Qddf', timestamp_us, lat, lon, alt)
def pack_vehicle_status(timestamp_us: int, vehicle_type: int, nav_state: int) -> bytes:
return struct.pack('<QBB', timestamp_us, vehicle_type, nav_state)
# ---------------------------------------------------------------------------
# Format definition strings (must match pack functions above)
# ---------------------------------------------------------------------------
FMT_LOCAL_POS = (
"vehicle_local_position:"
"uint64_t timestamp;"
"float x;float y;float z;"
"float vx;float vy;float vz;"
"double ref_lat;double ref_lon;float ref_alt"
)
FMT_ATTITUDE = (
"vehicle_attitude:"
"uint64_t timestamp;"
"float[4] q"
)
FMT_GLOBAL_POS = (
"vehicle_global_position:"
"uint64_t timestamp;"
"double lat;double lon;float alt"
)
FMT_STATUS = (
"vehicle_status:"
"uint64_t timestamp;"
"uint8_t vehicle_type;uint8_t nav_state"
)
# ---------------------------------------------------------------------------
# Trajectory math
# ---------------------------------------------------------------------------
REF_LAT = 47.397742
REF_LON = 8.545594
REF_ALT = 488.0
NAV_TAKEOFF = 2
NAV_OFFBOARD = 14
NAV_LAND = 18
def quat_from_yaw(yaw: float) -> tuple[float, float, float, float]:
return (math.cos(yaw / 2.0), 0.0, 0.0, math.sin(yaw / 2.0))
def smooth_interp(t: float) -> float:
"""Cosine interpolation factor 0..1."""
return 0.5 - 0.5 * math.cos(math.pi * max(0.0, min(1.0, t)))
def grid_position(drone_id: int) -> tuple[float, float]:
"""4x4 grid, 3m spacing, centered on origin."""
row = drone_id // 4
col = drone_id % 4
x = (row - 1.5) * 3.0
y = (col - 1.5) * 3.0
return x, y
def circle_position(drone_id: int, num_drones: int, radius: float = 30.0) -> tuple[float, float]:
angle = 2.0 * math.pi * drone_id / num_drones
return radius * math.cos(angle), radius * math.sin(angle)
def local_to_global(x: float, y: float, z: float) -> tuple[float, float, float]:
lat = REF_LAT + (x / 111320.0)
lon = REF_LON + (y / (111320.0 * math.cos(math.radians(REF_LAT))))
alt = REF_ALT - z
return lat, lon, alt
# ---------------------------------------------------------------------------
# Phase computations
# ---------------------------------------------------------------------------
TARGET = (200.0, 0.0, 0.0) # NED target for kamikaze
def compute_state(drone_id: int, t: float, num_drones: int, _duration: float):
"""Return (x, y, z, vx, vy, vz, yaw, nav_state) for a drone at time t.
Returns None if the drone hasn't started yet or has impacted.
"""
start_time = drone_id * 0.5 # staggered start
if t < start_time:
return None
# Failure drone 12 -- crash at t=70, log ends
if drone_id == 12 and t > 70.0:
return None
# Default nav state
nav_state = NAV_OFFBOARD
yaw = 0.0
vx, vy, vz = 0.0, 0.0, 0.0
gx, gy = grid_position(drone_id)
cx, cy = circle_position(drone_id, num_drones)
if t < 10.0:
# Phase 1: staggered start, on ground rising slightly
frac = smooth_interp((t - start_time) / max(0.01, 10.0 - start_time))
x, y = gx * frac, gy * frac
z = 0.0
nav_state = NAV_TAKEOFF
elif t < 25.0:
# Phase 2: grid takeoff, ascend to -20 NED
frac = smooth_interp((t - 10.0) / 15.0)
x, y = gx, gy
z = -20.0 * frac
vz = -20.0 / 15.0 if frac < 1.0 else 0.0
nav_state = NAV_TAKEOFF
elif t < 40.0:
# Phase 3: grid -> ring morph
frac = smooth_interp((t - 25.0) / 15.0)
x = gx + (cx - gx) * frac
y = gy + (cy - gy) * frac
z = -20.0
vx = (cx - gx) / 15.0
vy = (cy - gy) / 15.0
nav_state = NAV_OFFBOARD
elif t < 55.0:
# Phase 4: ring cruise CW at 3 m/s
angular_speed = 3.0 / 30.0 # omega = v/r
base_angle = 2.0 * math.pi * drone_id / num_drones
angle = base_angle - angular_speed * (t - 40.0) # CW -> negative
x = 30.0 * math.cos(angle)
y = 30.0 * math.sin(angle)
z = -20.0
vx = 30.0 * angular_speed * math.sin(angle)
vy = -30.0 * angular_speed * math.cos(angle)
yaw = math.atan2(-y, -x) # face center
nav_state = NAV_OFFBOARD
else:
# Phase 5/6: Kamikaze dive toward target
# Starting position at t=55
base_angle = 2.0 * math.pi * drone_id / num_drones
angular_speed = 3.0 / 30.0
angle55 = base_angle - angular_speed * 15.0
start_x = 30.0 * math.cos(angle55)
start_y = 30.0 * math.sin(angle55)
start_z = -20.0
dx = TARGET[0] - start_x
dy = TARGET[1] - start_y
dz = TARGET[2] - start_z
dist = math.sqrt(dx * dx + dy * dy + dz * dz)
speed = 12.0 # m/s max
nx, ny, nz = dx / dist, dy / dist, dz / dist
dt = t - 55.0
x = start_x + nx * speed * dt
y = start_y + ny * speed * dt
z = start_z + nz * speed * dt
vx = nx * speed
vy = ny * speed
vz = nz * speed
yaw = math.atan2(ny, nx)
nav_state = NAV_LAND
# Impact: close to target or above ground
cur_dist = math.sqrt((TARGET[0] - x) ** 2 + (TARGET[1] - y) ** 2 + (TARGET[2] - z) ** 2)
if cur_dist < 2.0 or z > -1.0:
return None
# Drone 12 failure behavior (attitude wobble from t=60)
if drone_id == 12 and t >= 60.0:
wobble_t = t - 60.0
wobble_amp = min(wobble_t * 0.1, 0.8) # growing oscillation
roll = wobble_amp * math.sin(wobble_t * 5.0)
pitch = wobble_amp * math.cos(wobble_t * 4.0)
# drift sideways
x += wobble_t * 2.0
y += wobble_t * 1.5
z += wobble_t * 0.5 # descending
# Build quaternion with roll/pitch/yaw
cr, sr = math.cos(roll / 2), math.sin(roll / 2)
cp, sp = math.cos(pitch / 2), math.sin(pitch / 2)
cy, sy = math.cos(yaw / 2), math.sin(yaw / 2)
q = (
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
)
return (x, y, z, vx, vy, vz, q, nav_state)
return (x, y, z, vx, vy, vz, quat_from_yaw(yaw), nav_state)
# ---------------------------------------------------------------------------
# Generate a single drone ULog
# ---------------------------------------------------------------------------
def generate_drone_ulog(drone_id: int, num_drones: int, duration: float,
output_dir: str, scenario: str):
filename = os.path.join(output_dir, f"drone_{drone_id:02d}.ulg")
start_us = drone_id * 500_000 # stagger by 0.5s in timestamps
writer = ULogWriter(filename, timestamp_us=start_us)
# Write format definitions
writer.write_format(FMT_LOCAL_POS)
writer.write_format(FMT_ATTITUDE)
writer.write_format(FMT_GLOBAL_POS)
writer.write_format(FMT_STATUS)
# Write info messages
writer.write_info("sys_name", "PX4")
writer.write_info("ver_hw", "SITL")
writer.write_info("scenario", scenario)
writer.write_info("drone_id", str(drone_id))
# Add subscriptions
writer.add_subscription("vehicle_local_position")
writer.add_subscription("vehicle_attitude")
writer.add_subscription("vehicle_global_position")
writer.add_subscription("vehicle_status")
# Determine rates per drone (edge cases)
pos_dt = 0.02 # 50 Hz
att_dt = 0.01 # 100 Hz
if drone_id == 13:
pos_dt = 0.1 # 10 Hz low rate
if drone_id == 14:
att_dt = 0.005 # 200 Hz high rate
# Generate data
# We iterate at the finest resolution needed and emit when due
fine_dt = min(pos_dt, att_dt, 0.001)
# Use the finest rate needed for this drone
fine_dt = min(pos_dt, att_dt)
status_interval = 1.0 # 1 Hz
next_pos_t = 0.0
next_att_t = 0.0
next_status_t = 0.0
last_nav_state = -1
t = 0.0
samples_written = 0
while t <= duration:
state = compute_state(drone_id, t, num_drones, duration)
# Drone 15 data gap at t=50-52
in_gap = (drone_id == 15 and 50.0 <= t <= 52.0)
if state is not None and not in_gap:
x, y, z, vx, vy, vz, q, nav_state = state
ts = int(t * 1_000_000)
# Position data
if t >= next_pos_t:
writer.write_data("vehicle_local_position",
pack_vehicle_local_position(
ts, x, y, z, vx, vy, vz,
REF_LAT, REF_LON, REF_ALT))
lat, lon, alt = local_to_global(x, y, z)
writer.write_data("vehicle_global_position",
pack_vehicle_global_position(ts, lat, lon, alt))
next_pos_t = t + pos_dt
samples_written += 2
# Attitude data
if t >= next_att_t:
writer.write_data("vehicle_attitude",
pack_vehicle_attitude(ts, q))
next_att_t = t + att_dt
samples_written += 1
# Status data
if t >= next_status_t or nav_state != last_nav_state:
writer.write_data("vehicle_status",
pack_vehicle_status(ts, 2, nav_state))
next_status_t = t + status_interval
last_nav_state = nav_state
samples_written += 1
t += fine_dt
writer.close()
return samples_written
# ---------------------------------------------------------------------------
# Scenario metadata
# ---------------------------------------------------------------------------
def write_scenario_json(output_dir: str, scenario: str, num_drones: int, duration: float):
meta = {
"scenario": scenario,
"num_drones": num_drones,
"duration_s": duration,
"origin": {
"lat": REF_LAT,
"lon": REF_LON,
"alt": REF_ALT,
},
"phases": [
{"name": "staggered_start", "start_s": 0, "end_s": 10},
{"name": "grid_takeoff", "start_s": 10, "end_s": 25},
{"name": "grid_to_ring", "start_s": 25, "end_s": 40},
{"name": "ring_cruise", "start_s": 40, "end_s": 55},
{"name": "kamikaze_dive", "start_s": 55, "end_s": 75},
],
"edge_cases": {
"drone_12": "failure at t=60, attitude oscillation, crash at t=70",
"drone_13": "low-rate position data (10Hz)",
"drone_14": "high-rate attitude data (200Hz)",
"drone_15": "2-second data gap at t=50-52",
},
"files": [f"drone_{i:02d}.ulg" for i in range(num_drones)],
}
path = os.path.join(output_dir, "scenario.json")
with open(path, 'w') as f:
json.dump(meta, f, indent=2)
print(f" Wrote {path}")
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Generate synthetic ULog files for swarm replay testing.")
parser.add_argument("--output-dir", default="synthetic_swarm_ulogs",
help="Output directory (default: synthetic_swarm_ulogs)")
parser.add_argument("--num-drones", type=int, default=16,
help="Number of drones (default: 16)")
parser.add_argument("--duration", type=float, default=90.0,
help="Scenario duration in seconds (default: 90)")
parser.add_argument("--scenario", default="hawk_descent",
help="Scenario name (default: hawk_descent)")
args = parser.parse_args()
os.makedirs(args.output_dir, exist_ok=True)
print(f"Generating {args.num_drones} drone ULog files for scenario '{args.scenario}'")
print(f" Duration: {args.duration}s")
print(f" Output: {os.path.abspath(args.output_dir)}/")
print()
for i in range(args.num_drones):
samples = generate_drone_ulog(i, args.num_drones, args.duration,
args.output_dir, args.scenario)
tag = ""
if i == 12:
tag = " [failure drone]"
elif i == 13:
tag = " [low-rate pos]"
elif i == 14:
tag = " [high-rate att]"
elif i == 15:
tag = " [data gap]"
print(f" drone_{i:02d}.ulg ({samples} messages){tag}")
write_scenario_json(args.output_dir, args.scenario, args.num_drones, args.duration)
print()
print(f"Done. {args.num_drones} ULog files written to {os.path.abspath(args.output_dir)}/")
if __name__ == "__main__":
main()
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# Launch N SIH quadrotor instances with XRCE-DDS agent.
# Usage: ./sih_swarm_run.sh [CONFIG_YAML] [SPEED_FACTOR]
#
# Requires: build/px4_sitl_sih (make px4_sitl_sih sihsim_quadx_vision)
set -euo pipefail
ulimit -n 4096
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
src_path="$SCRIPT_DIR/../../"
build_path="${src_path}/build/px4_sitl_sih"
CONFIG_YAML="${1:-${SCRIPT_DIR}/swarm_formation.yaml}"
SPEED_FACTOR="${2:-1}"
if [ ! -f "$CONFIG_YAML" ]; then
echo "ERROR: config not found: $CONFIG_YAML"
exit 1
fi
if [ ! -d "$build_path" ]; then
echo "ERROR: build directory not found: $build_path"
echo "Run: make px4_sitl_sih"
exit 1
fi
# --- Parse YAML config via Python ---
eval "$(python3 -c "
import yaml, sys
with open('${CONFIG_YAML}') as f:
cfg = yaml.safe_load(f)
origin = cfg['origin']
print(f\"ORIGIN_LAT={origin['lat']}\")
print(f\"ORIGIN_LON={origin['lon']}\")
print(f\"ORIGIN_ALT={origin['alt']}\")
target = cfg['target']
print(f\"TARGET_X={target['x']}\")
print(f\"TARGET_Y={target['y']}\")
print(f\"TARGET_Z={target['z']}\")
drones = cfg['formation']
print(f\"DRONE_COUNT={len(drones)}\")
for i, d in enumerate(drones):
print(f\"DRONE_{i}_ID={d['id']}\")
print(f\"DRONE_{i}_X={d['x']}\")
print(f\"DRONE_{i}_Y={d['y']}\")
print(f\"DRONE_{i}_Z={d['z']}\")
")"
echo "Swarm config: $DRONE_COUNT drones, origin ($ORIGIN_LAT, $ORIGIN_LON, ${ORIGIN_ALT}m)"
echo "Target: ($TARGET_X, $TARGET_Y, $TARGET_Z)"
echo "Speed factor: $SPEED_FACTOR"
echo ""
# --- Cleanup ---
CHILD_PIDS=()
AGENT_PID=""
cleanup() {
echo ""
echo "Shutting down..."
for pid in "${CHILD_PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
if [ -n "$AGENT_PID" ]; then
kill "$AGENT_PID" 2>/dev/null || true
fi
pkill -x px4 2>/dev/null || true
wait 2>/dev/null
echo "All processes stopped."
}
trap cleanup SIGINT SIGTERM
# --- Kill existing instances ---
echo "Killing running instances..."
pkill -x px4 2>/dev/null || true
sleep 1
# --- Start XRCE-DDS agent (if available) ---
if command -v MicroXRCEAgent &>/dev/null; then
echo "Starting MicroXRCEAgent on UDP port 8888..."
MicroXRCEAgent udp4 -p 8888 > /dev/null 2>&1 &
AGENT_PID=$!
echo "XRCE-DDS agent PID: $AGENT_PID"
else
echo "WARNING: MicroXRCEAgent not found on host."
echo " Start it separately (e.g. from Docker container):"
echo " MicroXRCEAgent udp4 -p 8888"
fi
echo ""
# --- Port map ---
printf "%-10s %-12s %-14s %-14s\n" "Instance" "MAV_SYS_ID" "DDS Namespace" "Offboard Port"
printf "%-10s %-12s %-14s %-14s\n" "--------" "----------" "-------------" "-------------"
n=0
while [ $n -lt "$DRONE_COUNT" ]; do
sysid=$((n + 1))
printf "%-10s %-12s %-14s %-14s\n" "$n" "$sysid" "px4_${n}" "$((14540 + n))"
n=$((n + 1))
done
echo ""
# --- Launch instances ---
export PX4_SIM_MODEL=sihsim_quadx_vision
n=0
while [ $n -lt "$DRONE_COUNT" ]; do
eval "FX=\$DRONE_${n}_X"
eval "FY=\$DRONE_${n}_Y"
eval "FZ=\$DRONE_${n}_Z"
working_dir="$build_path/instance_$n"
[ ! -d "$working_dir" ] && mkdir -p "$working_dir"
export PX4_HOME_LAT="$ORIGIN_LAT"
export PX4_HOME_LON="$ORIGIN_LON"
export PX4_HOME_ALT="$ORIGIN_ALT"
export PX4_FORMATION_X="$FX"
export PX4_FORMATION_Y="$FY"
export PX4_FORMATION_Z="$FZ"
export PX4_TARGET_X="$TARGET_X"
export PX4_TARGET_Y="$TARGET_Y"
export PX4_TARGET_Z="$TARGET_Z"
export PX4_SIM_SPEED_FACTOR="$SPEED_FACTOR"
# Force consistent DDS namespace for all instances (including 0)
export PX4_UXRCE_DDS_NS="px4_$n"
pushd "$working_dir" &>/dev/null
echo "Starting instance $n (formation: $FX, $FY, $FZ)"
"$build_path/bin/px4" -i "$n" -d "$build_path/etc" >out.log 2>err.log &
CHILD_PIDS+=($!)
popd &>/dev/null
n=$((n + 1))
done
echo ""
echo "All $DRONE_COUNT instances launched. Press Ctrl-C to stop."
wait
+11 -14
View File
@@ -1,28 +1,25 @@
#!/usr/bin/env bash
# Run multiple instances of the 'px4' binary, without starting an external simulator.
# It assumes px4 is already built with the specified build target.
#
# Usage: ./Tools/simulation/sitl_multiple_run.sh [num_instances] [model] [build_target]
# Examples:
# ./Tools/simulation/sitl_multiple_run.sh 3 sihsim_quadx px4_sitl_sih
# ./Tools/simulation/sitl_multiple_run.sh 2 gazebo-classic_iris px4_sitl_default
# ./Tools/simulation/sitl_multiple_run.sh # defaults: 2 instances, gazebo-classic_iris, px4_sitl_default
#!/bin/bash
# run multiple instances of the 'px4' binary, but w/o starting the simulator.
# It assumes px4 is already built, with 'make px4_sitl_default'
sitl_num=${1:-2}
sim_model=${2:-gazebo-classic_iris}
build_target=${3:-px4_sitl_default}
# The simulator is expected to send to TCP port 4560+i for i in [0, N-1]
# For example jmavsim can be run like this:
#./Tools/simulation/jmavsim/jmavsim_run.sh -p 4561 -l
sitl_num=2
[ -n "$1" ] && sitl_num="$1"
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
src_path="$SCRIPT_DIR/../../"
build_path=${src_path}/build/${build_target}
build_path=${src_path}/build/px4_sitl_default
echo "killing running instances"
pkill -x px4 || true
sleep 1
export PX4_SIM_MODEL=${sim_model}
export PX4_SIM_MODEL=gazebo-classic_iris
n=0
while [ $n -lt $sitl_num ]; do
+32
View File
@@ -0,0 +1,32 @@
# Formation config for SIH swarm simulation
# Used by sih_swarm_run.sh and swarm_ros2 coordinator
origin:
lat: 47.397742
lon: 8.545594
alt: 488.0
formation:
# NED offsets from origin (meters). 4x4 grid, 1m spacing, at 20m altitude
- {id: 0, x: 0.0, y: 0.0, z: -20.0}
- {id: 1, x: 0.0, y: 1.0, z: -20.0}
- {id: 2, x: 0.0, y: 2.0, z: -20.0}
- {id: 3, x: 0.0, y: 3.0, z: -20.0}
- {id: 4, x: 1.0, y: 0.0, z: -20.0}
- {id: 5, x: 1.0, y: 1.0, z: -20.0}
- {id: 6, x: 1.0, y: 2.0, z: -20.0}
- {id: 7, x: 1.0, y: 3.0, z: -20.0}
- {id: 8, x: 2.0, y: 0.0, z: -20.0}
- {id: 9, x: 2.0, y: 1.0, z: -20.0}
- {id: 10, x: 2.0, y: 2.0, z: -20.0}
- {id: 11, x: 2.0, y: 3.0, z: -20.0}
- {id: 12, x: 3.0, y: 0.0, z: -20.0}
- {id: 13, x: 3.0, y: 1.0, z: -20.0}
- {id: 14, x: 3.0, y: 2.0, z: -20.0}
- {id: 15, x: 3.0, y: 3.0, z: -20.0}
target:
# Car position: 200m north of origin, ground level
x: 200.0
y: 0.0
z: 0.0
+485
View File
@@ -0,0 +1,485 @@
#!/usr/bin/env python3
"""
Swarm Formation Designer - Create and preview formation YAML files for PX4 SIH swarm simulation.
Generates swarm_formation.yaml files with preset formation patterns (grid, circle, line,
v, diamond, random) for use with sih_swarm_run.sh and swarm_ros2 coordinator.
Usage examples:
# 16-drone grid with 2m spacing, preview in terminal
python3 swarm_formation_designer.py --pattern grid --count 16 --spacing 2.0 --preview
# 8-drone circle, radius 10m, write to file
python3 swarm_formation_designer.py --pattern circle --count 8 --radius 10 -o formation.yaml
# V formation with custom origin and target
python3 swarm_formation_designer.py --pattern v --count 7 --spacing 3.0 \\
--origin-lat 37.4 --origin-lon -122.0 --target-north 150 --target-east 50
"""
import argparse
import math
import random
import sys
from typing import List, Tuple
# Defaults matching PX4 SIH test site (Zurich area)
DEFAULT_LAT = 47.397742
DEFAULT_LON = 8.545594
DEFAULT_ALT = 488.0
DEFAULT_ALTITUDE = 20.0
DEFAULT_SPACING = 1.0
DEFAULT_RADIUS = 5.0
DEFAULT_COUNT = 16
DEFAULT_TARGET_NORTH = 200.0
DEFAULT_TARGET_EAST = 0.0
MIN_SEPARATION = 0.5 # meters, minimum allowed distance between any two drones
def generate_grid(count: int, spacing: float, altitude: float) -> List[dict]:
"""Arrange drones in a square grid pattern (ceil(sqrt(count)) columns)."""
cols = math.ceil(math.sqrt(count))
positions = []
for i in range(count):
row = i // cols
col = i % cols
positions.append({
"id": i,
"x": round(row * spacing, 2),
"y": round(col * spacing, 2),
"z": round(-altitude, 2),
})
return positions
def generate_circle(count: int, radius: float, altitude: float) -> List[dict]:
"""Arrange drones evenly spaced around a circle."""
positions = []
for i in range(count):
angle = 2.0 * math.pi * i / count
positions.append({
"id": i,
"x": round(radius * math.cos(angle), 2),
"y": round(radius * math.sin(angle), 2),
"z": round(-altitude, 2),
})
return positions
def generate_line(count: int, spacing: float, altitude: float) -> List[dict]:
"""Arrange drones in a line along the east (y) axis, centered at origin."""
positions = []
offset = (count - 1) * spacing / 2.0
for i in range(count):
positions.append({
"id": i,
"x": 0.0,
"y": round(i * spacing - offset, 2),
"z": round(-altitude, 2),
})
return positions
def generate_v(count: int, spacing: float, altitude: float) -> List[dict]:
"""Classic V formation with leader at front (northernmost position).
Leader is id 0 at the tip. Remaining drones alternate left/right,
each row stepping back (south) and outward (east/west).
"""
positions = [{"id": 0, "x": 0.0, "y": 0.0, "z": round(-altitude, 2)}]
wing_index = 1
for i in range(1, count):
side = 1 if (i % 2 == 1) else -1
row = (i + 1) // 2
positions.append({
"id": wing_index,
"x": round(-row * spacing, 2), # step back (south)
"y": round(side * row * spacing, 2), # spread east/west
"z": round(-altitude, 2),
})
wing_index += 1
return positions
def generate_diamond(count: int, spacing: float, altitude: float) -> List[dict]:
"""Diamond/rhombus formation.
Builds concentric diamond rings outward from the center. Ring k has 4*k
positions (except the center which has 1). Drones fill from center outward.
"""
# Generate diamond coordinates ring by ring
coords: List[Tuple[float, float]] = [(0.0, 0.0)]
ring = 1
while len(coords) < count:
# Each ring has 4 sides, each side has 'ring' segments
for side in range(4):
for step in range(ring):
if side == 0: # top-right edge
x = (ring - step) * spacing
y = step * spacing
elif side == 1: # bottom-right edge
x = -step * spacing
y = (ring - step) * spacing
elif side == 2: # bottom-left edge
x = -(ring - step) * spacing
y = -step * spacing
else: # top-left edge
x = step * spacing
y = -(ring - step) * spacing
coords.append((round(x, 2), round(y, 2)))
if len(coords) >= count:
break
if len(coords) >= count:
break
ring += 1
positions = []
for i in range(count):
positions.append({
"id": i,
"x": coords[i][0],
"y": coords[i][1],
"z": round(-altitude, 2),
})
return positions
def generate_random(count: int, spacing: float, altitude: float) -> List[dict]:
"""Random positions within a bounding box, enforcing minimum separation.
The bounding box size scales with count and spacing. Uses rejection
sampling to guarantee minimum separation between all drones.
"""
box_size = max(spacing * math.sqrt(count) * 2, spacing * 4)
positions = []
coords: List[Tuple[float, float]] = []
max_attempts = count * 1000
rng = random.Random(42) # deterministic for reproducibility
attempts = 0
while len(coords) < count and attempts < max_attempts:
x = round(rng.uniform(-box_size / 2, box_size / 2), 2)
y = round(rng.uniform(-box_size / 2, box_size / 2), 2)
too_close = False
for cx, cy in coords:
if math.sqrt((x - cx) ** 2 + (y - cy) ** 2) < MIN_SEPARATION:
too_close = True
break
if not too_close:
coords.append((x, y))
attempts += 1
if len(coords) < count:
print(
f"Warning: could only place {len(coords)}/{count} drones "
f"with minimum separation {MIN_SEPARATION}m in bounding box {box_size:.1f}m",
file=sys.stderr,
)
for i, (x, y) in enumerate(coords):
positions.append({
"id": i,
"x": x,
"y": y,
"z": round(-altitude, 2),
})
return positions
PATTERN_GENERATORS = {
"grid": generate_grid,
"circle": generate_circle,
"line": generate_line,
"v": generate_v,
"diamond": generate_diamond,
"random": generate_random,
}
def validate_no_overlap(positions: List[dict]) -> bool:
"""Check that no two drones occupy the same horizontal position (within MIN_SEPARATION)."""
for i in range(len(positions)):
for j in range(i + 1, len(positions)):
dx = positions[i]["x"] - positions[j]["x"]
dy = positions[i]["y"] - positions[j]["y"]
dist = math.sqrt(dx * dx + dy * dy)
if dist < MIN_SEPARATION:
print(
f"Error: drones {positions[i]['id']} and {positions[j]['id']} "
f"are only {dist:.2f}m apart (minimum: {MIN_SEPARATION}m)",
file=sys.stderr,
)
return False
return True
def build_formation_dict(
positions: List[dict],
origin_lat: float,
origin_lon: float,
origin_alt: float,
target_north: float,
target_east: float,
) -> dict:
"""Build the formation dictionary matching swarm_formation.yaml structure."""
return {
"origin": {
"lat": origin_lat,
"lon": origin_lon,
"alt": origin_alt,
},
"formation": positions,
"target": {
"x": target_north,
"y": target_east,
"z": 0.0,
},
}
def format_yaml(data: dict) -> str:
"""Format the formation data as YAML with flow-style formation entries.
Produces output matching the established swarm_formation.yaml style with
inline dicts for formation entries and block style for origin/target.
"""
lines = [
"# Formation config for SIH swarm simulation",
"# Generated by swarm_formation_designer.py",
"",
"origin:",
f" lat: {data['origin']['lat']}",
f" lon: {data['origin']['lon']}",
f" alt: {data['origin']['alt']}",
"",
"formation:",
]
# Determine padding width for aligned id field
max_id = max(p["id"] for p in data["formation"])
id_width = len(str(max_id))
for pos in data["formation"]:
id_str = str(pos["id"]).rjust(id_width)
lines.append(
f" - {{id: {id_str}, "
f"x: {pos['x']}, "
f"y: {pos['y']}, "
f"z: {pos['z']}}}"
)
lines.extend([
"",
"target:",
f" x: {data['target']['x']}",
f" y: {data['target']['y']}",
f" z: {data['target']['z']}",
"",
])
return "\n".join(lines)
def ascii_preview(positions: List[dict], target_north: float, target_east: float) -> str:
"""Render an ASCII top-down map showing drone positions and target.
North is up, East is right. Axes are labeled. The target is shown as 'T'
and drones as their id number (or '*' if id >= 10).
"""
width = 60
height = 30
# Collect all points (drones + target)
all_x = [p["x"] for p in positions] + [target_north]
all_y = [p["y"] for p in positions] + [target_east]
min_x, max_x = min(all_x), max(all_x)
min_y, max_y = min(all_y), max(all_y)
# Add margin
range_x = max_x - min_x if max_x != min_x else 1.0
range_y = max_y - min_y if max_y != min_y else 1.0
margin = 0.1
min_x -= range_x * margin
max_x += range_x * margin
min_y -= range_y * margin
max_y += range_y * margin
range_x = max_x - min_x
range_y = max_y - min_y
def to_grid(north: float, east: float) -> Tuple[int, int]:
col = int((east - min_y) / range_y * (width - 1))
row = int((max_x - north) / range_x * (height - 1)) # north is up
col = max(0, min(width - 1, col))
row = max(0, min(height - 1, row))
return row, col
# Initialize grid
grid = [[" " for _ in range(width)] for _ in range(height)]
# Place target
tr, tc = to_grid(target_north, target_east)
grid[tr][tc] = "T"
# Place drones (overwrite target if on same cell)
for pos in positions:
r, c = to_grid(pos["x"], pos["y"])
label = str(pos["id"]) if pos["id"] < 10 else "*"
grid[r][c] = label
# Build output
out = []
out.append(f" Formation Preview ({len(positions)} drones)")
out.append(f" North ^ (x range: {min_x:.1f} to {max_x:.1f} m)")
out.append(" " + "-" * (width + 2))
for row in grid:
out.append(" |" + "".join(row) + "|")
out.append(" " + "-" * (width + 2))
out.append(f" East -> (y range: {min_y:.1f} to {max_y:.1f} m)")
out.append("")
out.append(" Legend: 0-9 = drone id, * = drone id >= 10, T = target")
return "\n".join(out)
def parse_args(argv: "List[str] | None" = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Create and preview swarm formation YAML files for PX4 SIH simulation.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" %(prog)s --pattern grid --count 16 --spacing 2 --preview\n"
" %(prog)s --pattern circle --count 8 --radius 10 -o my_formation.yaml\n"
" %(prog)s --pattern v --count 7 --spacing 3 --preview -o v_formation.yaml\n"
),
)
parser.add_argument(
"--pattern",
choices=list(PATTERN_GENERATORS.keys()),
default="grid",
help="Formation pattern (default: grid)",
)
parser.add_argument(
"--count",
type=int,
default=DEFAULT_COUNT,
help=f"Number of drones (default: {DEFAULT_COUNT})",
)
parser.add_argument(
"--spacing",
type=float,
default=DEFAULT_SPACING,
help=f"Spacing between drones in meters (default: {DEFAULT_SPACING})",
)
parser.add_argument(
"--altitude",
type=float,
default=DEFAULT_ALTITUDE,
help=f"Flight altitude in meters AGL (default: {DEFAULT_ALTITUDE})",
)
parser.add_argument(
"--radius",
type=float,
default=DEFAULT_RADIUS,
help=f"Radius for circle pattern in meters (default: {DEFAULT_RADIUS})",
)
parser.add_argument(
"--origin-lat",
type=float,
default=DEFAULT_LAT,
help=f"Origin latitude (default: {DEFAULT_LAT})",
)
parser.add_argument(
"--origin-lon",
type=float,
default=DEFAULT_LON,
help=f"Origin longitude (default: {DEFAULT_LON})",
)
parser.add_argument(
"--origin-alt",
type=float,
default=DEFAULT_ALT,
help=f"Origin altitude AMSL in meters (default: {DEFAULT_ALT})",
)
parser.add_argument(
"--target-north",
type=float,
default=DEFAULT_TARGET_NORTH,
help=f"Target position north of origin in meters (default: {DEFAULT_TARGET_NORTH})",
)
parser.add_argument(
"--target-east",
type=float,
default=DEFAULT_TARGET_EAST,
help=f"Target position east of origin in meters (default: {DEFAULT_TARGET_EAST})",
)
parser.add_argument(
"--preview",
action="store_true",
help="Show ASCII preview of the formation",
)
parser.add_argument(
"-o", "--output",
type=str,
default=None,
help="Output YAML file path (default: print to stdout)",
)
args = parser.parse_args(argv)
if args.count < 1:
parser.error("--count must be at least 1")
if args.spacing <= 0:
parser.error("--spacing must be positive")
if args.altitude <= 0:
parser.error("--altitude must be positive")
if args.radius <= 0:
parser.error("--radius must be positive")
return args
def main(argv: "List[str] | None" = None) -> int:
args = parse_args(argv)
# Select generator and dispatch with appropriate arguments
generator = PATTERN_GENERATORS[args.pattern]
if args.pattern == "circle":
positions = generator(args.count, args.radius, args.altitude)
else:
positions = generator(args.count, args.spacing, args.altitude)
# Validate
if not validate_no_overlap(positions):
return 1
# Build output
data = build_formation_dict(
positions,
args.origin_lat,
args.origin_lon,
args.origin_alt,
args.target_north,
args.target_east,
)
yaml_text = format_yaml(data)
# Preview
if args.preview:
print(ascii_preview(positions, args.target_north, args.target_east))
print()
# Output
if args.output:
with open(args.output, "w") as f:
f.write(yaml_text)
print(f"Wrote {len(positions)} drone formation to {args.output}")
else:
if not args.preview:
print(yaml_text, end="")
else:
print(yaml_text, end="")
return 0
if __name__ == "__main__":
sys.exit(main())
+2
View File
@@ -0,0 +1,2 @@
# Staging directory created by build_docker.sh (ephemeral)
_px4_msgs_defs/
+95
View File
@@ -0,0 +1,95 @@
# ROS 2 Jazzy layer on top of PX4 dev environment
# For swarm simulation with px4-ros2-interface-lib
#
# Build (from PX4-Autopilot root):
# Tools/simulation/swarm_ros2/build_docker.sh
#
# Run:
# docker run -it --rm -v /path/to/PX4-Autopilot:/px4 px4-swarm-ros2
#
FROM px4io/px4-dev:v1.17.0-rc1
LABEL maintainer="Ramon Roche <mrpollo@gmail.com>"
ENV DEBIAN_FRONTEND=noninteractive
ENV ROS_DISTRO=jazzy
# Add ROS 2 Jazzy repository (Ubuntu 24.04 = noble)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
gnupg \
lsb-release \
software-properties-common \
&& curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
-o /usr/share/keyrings/ros-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
http://packages.ros.org/ros2/ubuntu noble main" \
> /etc/apt/sources.list.d/ros2.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ros-jazzy-ros-base \
ros-jazzy-rmw-cyclonedds-cpp \
python3-colcon-common-extensions \
python3-rosdep \
python3-numpy \
&& rm -rf /var/lib/apt/lists/*
# Initialize rosdep
RUN rosdep init 2>/dev/null || true \
&& su user -c "rosdep update --rosdistro jazzy" 2>/dev/null || true
# Set ROS 2 middleware to CycloneDDS (better multi-vehicle support)
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
# Build Micro XRCE-DDS Agent from source
RUN git clone --depth 1 https://github.com/eProsima/Micro-XRCE-DDS-Agent.git /tmp/xrce-agent \
&& cd /tmp/xrce-agent \
&& mkdir build && cd build \
&& cmake .. -DCMAKE_BUILD_TYPE=Release \
&& make -j$(nproc) \
&& make install \
&& ldconfig \
&& rm -rf /tmp/xrce-agent
# Build px4_msgs from PX4 source tree msg definitions (ensures type hash match).
# The build_docker.sh script copies msg/ and srv/ into _px4_msgs_defs/ before build.
ENV ROS2_WS=/opt/ros2_ws
COPY _px4_msgs_defs/ /tmp/px4_msg_defs/
RUN mkdir -p ${ROS2_WS}/src \
&& cd ${ROS2_WS}/src \
&& git clone --depth 1 https://github.com/PX4/px4_msgs.git \
&& rm -rf px4_msgs/msg/*.msg px4_msgs/srv/*.srv \
&& cp /tmp/px4_msg_defs/msg/*.msg px4_msgs/msg/ \
&& cp /tmp/px4_msg_defs/srv/*.srv px4_msgs/srv/ \
&& rm -rf /tmp/px4_msg_defs \
&& cd ${ROS2_WS} \
&& . /opt/ros/jazzy/setup.sh \
&& colcon build --packages-select px4_msgs --cmake-args -DCMAKE_BUILD_TYPE=Release \
&& rm -rf log
# Build px4-ros2-interface-lib (depends on px4_msgs built above)
RUN cd ${ROS2_WS}/src \
&& git clone --depth 1 --recursive https://github.com/Auterion/px4-ros2-interface-lib.git \
&& cd ${ROS2_WS} \
&& . /opt/ros/jazzy/setup.sh \
&& . install/setup.sh \
&& colcon build --packages-skip px4_msgs --cmake-args -DCMAKE_BUILD_TYPE=Release \
&& rm -rf log src
# Copy swarm_ros2 package and build it
COPY . ${ROS2_WS}/src/swarm_ros2/
RUN cd ${ROS2_WS} \
&& . /opt/ros/jazzy/setup.sh \
&& . install/setup.sh \
&& colcon build --packages-select swarm_ros2 \
&& rm -rf log
# Source everything on shell entry
RUN echo "source /opt/ros/jazzy/setup.bash" >> /etc/bash.bashrc \
&& echo "source ${ROS2_WS}/install/setup.bash" >> /etc/bash.bashrc
# XRCE-DDS UDP port
EXPOSE 8888/udp
WORKDIR ${ROS2_WS}
CMD ["/bin/bash"]
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# Build the px4-swarm-ros2 Docker image with px4_msgs matching this PX4 commit.
# Run from anywhere; the script locates PX4 root automatically.
#
# Usage: ./build_docker.sh
set -eo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PX4_ROOT="$( cd "${SCRIPT_DIR}/../../.." && pwd )"
echo "PX4 root: ${PX4_ROOT}"
echo "Staging px4_msgs definitions from this PX4 commit..."
# Stage msg/srv files into the Docker build context
STAGING_DIR="${SCRIPT_DIR}/_px4_msgs_defs"
rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}/msg" "${STAGING_DIR}/srv"
cp "${PX4_ROOT}"/msg/*.msg "${STAGING_DIR}/msg/"
cp "${PX4_ROOT}"/msg/versioned/*.msg "${STAGING_DIR}/msg/"
cp "${PX4_ROOT}"/srv/*.srv "${STAGING_DIR}/srv/"
echo "Staged $(ls "${STAGING_DIR}/msg/" | wc -l | tr -d ' ') .msg files and $(ls "${STAGING_DIR}/srv/" | wc -l | tr -d ' ') .srv files"
echo ""
echo "Building Docker image..."
docker build -t px4-swarm-ros2 "${SCRIPT_DIR}"
# Clean up staged files
rm -rf "${STAGING_DIR}"
echo ""
echo "Done. Run with:"
echo " docker run -it --rm -v ${PX4_ROOT}:/px4 px4-swarm-ros2 bash /px4/Tools/simulation/swarm_ros2/run_swarm.sh 4"
@@ -0,0 +1,80 @@
"""
Launch file for the PX4 swarm coordination stack.
Starts one vision_provider node per drone and a single swarm_controller node.
Usage:
ros2 launch swarm_ros2 swarm_launch.py num_drones:=16 speed_factor:=1.0
"""
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
# Declare launch arguments.
num_drones_arg = DeclareLaunchArgument(
'num_drones',
default_value='16',
description='Number of drones in the swarm',
)
speed_factor_arg = DeclareLaunchArgument(
'speed_factor',
default_value='1.0',
description='Simulation speed factor',
)
formation_config_arg = DeclareLaunchArgument(
'formation_config',
default_value='',
description='Path to YAML formation configuration file',
)
num_drones = LaunchConfiguration('num_drones')
speed_factor = LaunchConfiguration('speed_factor')
# We need a concrete integer to iterate, so we support a fixed max and
# conditionally launch. ROS 2 launch does not natively support dynamic
# loops over LaunchConfiguration integers, so we generate nodes for a
# reasonable upper bound and rely on the controller's num_drones param
# to limit actual usage. For simplicity, generate exactly 16 nodes
# (the default). For other counts, users can call this launch file
# programmatically or adjust the constant below.
MAX_DRONES = 16
vision_nodes = []
for i in range(MAX_DRONES):
ns = f'px4_{i}'
vision_nodes.append(
Node(
package='swarm_ros2',
executable='vision_provider',
name=f'vision_provider_{i}',
parameters=[{
'namespace': ns,
'instance_id': i,
}],
output='screen',
)
)
# Single swarm controller.
controller_node = Node(
package='swarm_ros2',
executable='swarm_controller',
name='swarm_controller',
parameters=[{
'num_drones': num_drones,
'speed_factor': speed_factor,
}],
output='screen',
)
return LaunchDescription([
num_drones_arg,
speed_factor_arg,
formation_config_arg,
*vision_nodes,
controller_node,
])
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>swarm_ros2</name>
<version>0.1.0</version>
<description>Swarm coordination for PX4 multi-vehicle simulation using px4-ros2-interface-lib concepts</description>
<maintainer email="mrpollo@gmail.com">Ramon Roche</maintainer>
<license>BSD-3-Clause</license>
<depend>rclpy</depend>
<depend>px4_msgs</depend>
<depend>px4_ros2_cpp</depend>
<depend>std_msgs</depend>
<depend>geometry_msgs</depend>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
# Run the full swarm test inside Docker: PX4 SIH + XRCE-DDS + ROS 2
# Usage: ./run_swarm.sh [NUM_DRONES] [SPEED_FACTOR]
#
# Must be run inside the px4-swarm-ros2 container with PX4 source mounted at /px4:
# docker run -it --rm -v /path/to/PX4-Autopilot:/px4 px4-swarm-ros2 /opt/ros2_ws/src/swarm_ros2/run_swarm.sh 4
set -eo pipefail
NUM_DRONES="${1:-4}"
SPEED_FACTOR="${2:-1}"
PX4_SRC="${PX4_SRC:-/px4}"
BUILD_DIR="${PX4_SRC}/build/px4_sitl_sih"
echo "=== PX4 SIH Swarm Test ==="
echo " Drones: $NUM_DRONES"
echo " Speed factor: $SPEED_FACTOR"
echo ""
# Check PX4 build exists
if [ ! -f "$BUILD_DIR/bin/px4" ]; then
echo "PX4 binary not found at $BUILD_DIR/bin/px4"
echo "Building PX4 (this may take a few minutes)..."
cd "$PX4_SRC"
make px4_sitl_sih
fi
# Source ROS 2
source /opt/ros/jazzy/setup.bash
source /opt/ros2_ws/install/setup.bash
# Cleanup handler
PIDS=()
cleanup() {
echo ""
echo "Shutting down..."
for pid in "${PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
pkill -x px4 2>/dev/null || true
pkill -x MicroXRCEAgent 2>/dev/null || true
wait 2>/dev/null
echo "Done."
}
trap cleanup EXIT SIGINT SIGTERM
# Kill any existing px4
pkill -x px4 2>/dev/null || true
sleep 1
# Start XRCE-DDS agent
echo "Starting XRCE-DDS agent on port 8888..."
MicroXRCEAgent udp4 -p 8888 > /dev/null 2>&1 &
PIDS+=($!)
sleep 1
# Launch PX4 instances
export PX4_SIM_MODEL=sihsim_quadx_vision
export PX4_SIM_SPEED_FACTOR="$SPEED_FACTOR"
# Clean stale instance data (old parameters.bson can override param set-default)
echo "Cleaning stale instance data..."
for n in $(seq 0 $((NUM_DRONES - 1))); do
rm -rf "$BUILD_DIR/instance_$n"
done
echo "Launching $NUM_DRONES PX4 SIH instances..."
for n in $(seq 0 $((NUM_DRONES - 1))); do
working_dir="$BUILD_DIR/instance_$n"
mkdir -p "$working_dir"
pushd "$working_dir" &>/dev/null
# Force consistent DDS namespace for all instances (including 0)
# so ROS 2 nodes can use px4_0, px4_1, ... uniformly
PX4_UXRCE_DDS_NS="px4_$n" "$BUILD_DIR/bin/px4" -i "$n" -d "$BUILD_DIR/etc" >out.log 2>err.log &
PIDS+=($!)
popd &>/dev/null
echo " Instance $n started (PID ${PIDS[-1]})"
done
# Wait for PX4 instances to initialize
echo ""
echo "Waiting 5s for PX4 instances to initialize..."
sleep 5
# Launch ROS 2 swarm (blocks until mission completes)
echo ""
echo "Launching ROS 2 swarm controller..."
echo "=========================================="
ros2 launch swarm_ros2 swarm_launch.py num_drones:="$NUM_DRONES"
echo ""
echo "=== Mission complete ==="
# Collect ULogs
echo "Collecting ULog files..."
python3 "$PX4_SRC/Tools/simulation/collect_swarm_ulogs.py" \
--build-dir "$BUILD_DIR" \
--output-dir "$PX4_SRC/swarm_ulogs_$(date +%Y%m%d_%H%M%S)"
+5
View File
@@ -0,0 +1,5 @@
[develop]
script_dir=$base/lib/swarm_ros2
[install]
install_scripts=$base/lib/swarm_ros2
+29
View File
@@ -0,0 +1,29 @@
from setuptools import setup
import os
from glob import glob
package_name = 'swarm_ros2'
setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Ramon Roche',
maintainer_email='mrpollo@gmail.com',
description='Swarm coordination for PX4 multi-vehicle simulation',
license='BSD-3-Clause',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'vision_provider = swarm_ros2.vision_provider:main',
'swarm_controller = swarm_ros2.swarm_controller:main',
],
},
)
@@ -0,0 +1,599 @@
"""
Swarm controller node for PX4 multi-vehicle coordination.
Manages all drones through a phased state machine:
1. Wait for EV lock
2. Arm + takeoff to formation
3. Hold grid formation
4. Transition to circle via cosine interpolation
5. Hold circle and rotate
6. Kamikaze dive to target
Uses PX4 offboard control via px4_msgs topics.
"""
import math
import os
from enum import IntEnum, auto
import numpy as np
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
from px4_msgs.msg import (
OffboardControlMode,
TrajectorySetpoint,
VehicleCommand,
VehicleLocalPosition,
VehicleStatus,
)
PX4_QOS = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
history=HistoryPolicy.KEEP_LAST,
depth=1,
)
# Volatile QoS for subscriptions to avoid stale data from before
# namespace discovery is complete.
PX4_SUB_QOS = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.VOLATILE,
history=HistoryPolicy.KEEP_LAST,
depth=1,
)
class Phase(IntEnum):
"""State machine phases."""
WAIT_EV_LOCK = auto()
ARM_TAKEOFF = auto()
HOLD_GRID = auto()
TRANSITION_CIRCLE = auto()
HOLD_CIRCLE = auto()
KAMIKAZE = auto()
DONE = auto()
class DroneState:
"""Per-drone bookkeeping."""
def __init__(self, drone_id: int, namespace: str):
self.drone_id = drone_id
self.namespace = namespace
# Telemetry
self.xy_valid = False
self.z_valid = False
self.position = np.zeros(3) # NED [x, y, z]
self.has_initial_pos = False
self._init_samples: list = [] # Buffer for initial position consensus
self.armed = False
self.nav_state = 0
# Formation target (grid)
self.grid_pos = np.zeros(3)
# Circle target
self.circle_angle = 0.0
# Mission status
self.impacted = False
class SwarmController(Node):
"""Coordinates N drones through the swarm mission phases."""
# Phase durations in seconds
EV_LOCK_TIMEOUT = 15.0
TAKEOFF_DURATION = 20.0
HOLD_GRID_DURATION = 10.0
TRANSITION_DURATION = 15.0
HOLD_CIRCLE_DURATION = 10.0
KAMIKAZE_TIMEOUT = 120.0
# Circle parameters
CIRCLE_RADIUS = 4.0 # metres
CIRCLE_SPEED = 3.0 # m/s tangential
# Kamikaze parameters
KAMIKAZE_SPEED = 12.0 # m/s (MPC_XY_VEL_MAX)
IMPACT_DIST = 2.0 # metres
IMPACT_Z_THRESH = -1.0 # NED (z > -1 means within 1m of ground)
# PX4 command constants
VEHICLE_CMD_COMPONENT_ARM_DISARM = 400
VEHICLE_CMD_DO_SET_MODE = 176
CONTROL_HZ = 20.0
def __init__(self):
super().__init__('swarm_controller')
# Parameters
self.declare_parameter('num_drones', 16)
self.declare_parameter('speed_factor', 1.0)
self.declare_parameter('target_x', 200.0)
self.declare_parameter('target_y', 0.0)
self.declare_parameter('target_z', 0.0)
self.declare_parameter('takeoff_altitude', -5.0) # NED, negative = up
self._num_drones = self.get_parameter('num_drones').value
self._speed_factor = self.get_parameter('speed_factor').value
# Kamikaze target (NED)
self._target = np.array([
float(os.environ.get('PX4_KAMIKAZE_X',
self.get_parameter('target_x').value)),
float(os.environ.get('PX4_KAMIKAZE_Y',
self.get_parameter('target_y').value)),
float(os.environ.get('PX4_KAMIKAZE_Z',
self.get_parameter('target_z').value)),
])
takeoff_alt = self.get_parameter('takeoff_altitude').value
self.get_logger().info(
f'Swarm controller: {self._num_drones} drones, '
f'speed_factor={self._speed_factor}, '
f'target={self._target.tolist()}'
)
# Build grid formation (4x4 or NxM, 2m spacing, centered at origin).
cols = int(math.ceil(math.sqrt(self._num_drones)))
rows = int(math.ceil(self._num_drones / cols))
grid_center_x = (cols - 1) / 2.0
grid_center_y = (rows - 1) / 2.0
# ------------------------------------------------------------------ #
# Per-drone state and ROS interfaces
# ------------------------------------------------------------------ #
self._drones: list[DroneState] = []
self._pub_offboard: list[rclpy.publisher.Publisher] = []
self._pub_traj: list[rclpy.publisher.Publisher] = []
self._pub_cmd: list[rclpy.publisher.Publisher] = []
for i in range(self._num_drones):
ns = f'px4_{i}'
drone = DroneState(i, ns)
# Grid position: 2m spacing, centered, at takeoff altitude.
col = i % cols
row = i // cols
drone.grid_pos = np.array([
(col - grid_center_x) * 2.0,
(row - grid_center_y) * 2.0,
float(takeoff_alt),
])
# Circle angle for this drone.
drone.circle_angle = 2.0 * math.pi * i / self._num_drones
self._drones.append(drone)
# Publishers
self._pub_offboard.append(
self.create_publisher(
OffboardControlMode,
f'/{ns}/fmu/in/offboard_control_mode',
PX4_QOS,
)
)
self._pub_traj.append(
self.create_publisher(
TrajectorySetpoint,
f'/{ns}/fmu/in/trajectory_setpoint',
PX4_QOS,
)
)
self._pub_cmd.append(
self.create_publisher(
VehicleCommand,
f'/{ns}/fmu/in/vehicle_command',
PX4_QOS,
)
)
# Subscribers
# PX4 XRCE-DDS uses versioned topic names: _v<MESSAGE_VERSION>
# VehicleLocalPosition: MESSAGE_VERSION=1 -> _v1
# VehicleStatus: MESSAGE_VERSION=2 -> _v2
self.create_subscription(
VehicleLocalPosition,
f'/{ns}/fmu/out/vehicle_local_position_v1',
lambda msg, idx=i: self._local_pos_cb(idx, msg),
PX4_SUB_QOS,
)
self.create_subscription(
VehicleStatus,
f'/{ns}/fmu/out/vehicle_status_v2',
lambda msg, idx=i: self._vehicle_status_cb(idx, msg),
PX4_SUB_QOS,
)
# ------------------------------------------------------------------ #
# State machine
# ------------------------------------------------------------------ #
self._phase = Phase.WAIT_EV_LOCK
self._phase_start_time = self.get_clock().now()
self._mission_start_time = self.get_clock().now()
self._circle_center = np.array([0.0, 0.0, float(takeoff_alt)])
self._last_ev_print = 0
# Main control timer
period_s = 1.0 / self.CONTROL_HZ
self._timer = self.create_timer(period_s, self._control_loop)
# ===================================================================== #
# Subscriber callbacks
# ===================================================================== #
def _local_pos_cb(self, idx: int, msg: VehicleLocalPosition):
d = self._drones[idx]
d.xy_valid = msg.xy_valid
d.z_valid = msg.z_valid
new_pos = np.array([msg.x, msg.y, msg.z])
if not d.has_initial_pos:
if not (d.xy_valid and d.z_valid):
return
# All drones start near origin. Reject samples > 20m from origin
# during initialization (DDS cross-talk from other instances).
if np.linalg.norm(new_pos) > 20.0:
return
# Require 5 consistent samples within 5m to establish initial pos.
d._init_samples.append(new_pos)
if len(d._init_samples) >= 5:
ref = d._init_samples[-1]
consistent = all(
np.linalg.norm(s - ref) < 5.0
for s in d._init_samples[-5:]
)
if consistent:
d.position = new_pos
d.has_initial_pos = True
else:
d._init_samples = d._init_samples[-1:]
return
# Guard against DDS cross-talk: reject position jumps > 50m/tick.
# At 50Hz and 12m/s max, real movement is < 0.3m per tick.
jump = np.linalg.norm(new_pos - d.position)
if jump > 50.0:
return # Discard cross-talk sample
d.position = new_pos
def _vehicle_status_cb(self, idx: int, msg: VehicleStatus):
d = self._drones[idx]
d.armed = (msg.arming_state == VehicleStatus.ARMING_STATE_ARMED)
d.nav_state = msg.nav_state
# ===================================================================== #
# Publishing helpers
# ===================================================================== #
def _publish_offboard_mode(self, idx: int, *, position: bool = False,
velocity: bool = False):
"""Send OffboardControlMode for drone idx."""
msg = OffboardControlMode()
msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
msg.position = position
msg.velocity = velocity
msg.acceleration = False
msg.attitude = False
msg.body_rate = False
self._pub_offboard[idx].publish(msg)
def _publish_trajectory(self, idx: int, *,
position: list[float] | None = None,
velocity: list[float] | None = None,
yaw: float = float('nan'),
yawspeed: float = float('nan')):
"""Send TrajectorySetpoint for drone idx."""
msg = TrajectorySetpoint()
msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
nan3 = [float('nan')] * 3
msg.position = list(position) if position is not None else nan3
msg.velocity = list(velocity) if velocity is not None else nan3
msg.yaw = yaw
msg.yawspeed = yawspeed
self._pub_traj[idx].publish(msg)
def _publish_vehicle_command(self, idx: int, command: int,
param1: float = 0.0, param2: float = 0.0,
param3: float = 0.0):
"""Send VehicleCommand for drone idx."""
msg = VehicleCommand()
msg.timestamp = int(self.get_clock().now().nanoseconds / 1000)
msg.command = command
msg.param1 = param1
msg.param2 = param2
msg.param3 = param3
msg.target_system = idx + 1 # MAV_SYS_ID
msg.target_component = 1
msg.source_system = 255 # GCS
msg.source_component = 0
msg.from_external = True
self._pub_cmd[idx].publish(msg)
def _arm(self, idx: int):
self._publish_vehicle_command(
idx, self.VEHICLE_CMD_COMPONENT_ARM_DISARM, param1=1.0)
def _disarm_force(self, idx: int):
self._publish_vehicle_command(
idx, self.VEHICLE_CMD_COMPONENT_ARM_DISARM,
param1=0.0, param2=21196.0)
def _set_offboard_mode(self, idx: int):
self._publish_vehicle_command(
idx, self.VEHICLE_CMD_DO_SET_MODE,
param1=1.0, param2=6.0, param3=0.0)
# ===================================================================== #
# Phase elapsed time helper
# ===================================================================== #
def _phase_elapsed(self) -> float:
"""Seconds since current phase started."""
return (self.get_clock().now() - self._phase_start_time).nanoseconds / 1e9
def _mission_elapsed(self) -> float:
"""Seconds since mission started."""
return (self.get_clock().now() - self._mission_start_time).nanoseconds / 1e9
def _advance_phase(self, new_phase: Phase):
self.get_logger().info(f'Phase transition: {self._phase.name} -> {new_phase.name}')
self._phase = new_phase
self._phase_start_time = self.get_clock().now()
# ===================================================================== #
# Main control loop (20 Hz)
# ===================================================================== #
def _control_loop(self):
if self._phase == Phase.DONE:
return
if self._phase == Phase.WAIT_EV_LOCK:
self._phase_wait_ev_lock()
elif self._phase == Phase.ARM_TAKEOFF:
self._phase_arm_takeoff()
elif self._phase == Phase.HOLD_GRID:
self._phase_hold_grid()
elif self._phase == Phase.TRANSITION_CIRCLE:
self._phase_transition_circle()
elif self._phase == Phase.HOLD_CIRCLE:
self._phase_hold_circle()
elif self._phase == Phase.KAMIKAZE:
self._phase_kamikaze()
# ------------------------------------------------------------------ #
# Phase 1: Wait for EV lock
# ------------------------------------------------------------------ #
def _phase_wait_ev_lock(self):
locked = sum(1 for d in self._drones if d.xy_valid and d.z_valid)
elapsed = self._phase_elapsed()
# Print status every second.
elapsed_int = int(elapsed)
if elapsed_int > self._last_ev_print:
self._last_ev_print = elapsed_int
self.get_logger().info(
f'{locked}/{self._num_drones} drones have EV lock '
f'({elapsed:.0f}s elapsed)')
if locked == self._num_drones or elapsed > self.EV_LOCK_TIMEOUT:
if locked < self._num_drones:
self.get_logger().warn(
f'EV lock timeout: only {locked}/{self._num_drones} locked')
self._advance_phase(Phase.ARM_TAKEOFF)
# ------------------------------------------------------------------ #
# Phase 2: Arm + takeoff
# ------------------------------------------------------------------ #
def _phase_arm_takeoff(self):
elapsed = self._phase_elapsed()
for i in range(self._num_drones):
d = self._drones[i]
# Keep sending offboard + setpoint before and after arming.
self._publish_offboard_mode(i, position=True)
self._publish_trajectory(
i,
position=d.grid_pos.tolist(),
yaw=0.0,
)
# Arm and set offboard mode repeatedly for the first few seconds.
if elapsed < 5.0:
self._set_offboard_mode(i)
self._arm(i)
# Log status every 5 seconds.
elapsed_int = int(elapsed)
if elapsed_int > 0 and elapsed_int % 5 == 0 and elapsed - elapsed_int < 0.1:
armed = sum(1 for d in self._drones if d.armed)
self.get_logger().info(
f'ARM_TAKEOFF {elapsed:.0f}s: {armed}/{self._num_drones} armed, '
f'positions: ' + ', '.join(
f'D{i}=[{d.position[2]:.1f}m]'
for i, d in enumerate(self._drones)
)
)
if elapsed > self.TAKEOFF_DURATION:
self._advance_phase(Phase.HOLD_GRID)
# ------------------------------------------------------------------ #
# Phase 3: Hold grid
# ------------------------------------------------------------------ #
def _phase_hold_grid(self):
for i in range(self._num_drones):
d = self._drones[i]
self._publish_offboard_mode(i, position=True)
self._publish_trajectory(
i,
position=d.grid_pos.tolist(),
yaw=0.0,
)
if self._phase_elapsed() > self.HOLD_GRID_DURATION:
# Compute circle center as the mean of grid positions.
mean_pos = np.mean([d.grid_pos for d in self._drones], axis=0)
self._circle_center = mean_pos.copy()
self._advance_phase(Phase.TRANSITION_CIRCLE)
# ------------------------------------------------------------------ #
# Phase 4: Transition to circle (cosine interpolation)
# ------------------------------------------------------------------ #
def _phase_transition_circle(self):
elapsed = self._phase_elapsed()
alpha = 0.5 - 0.5 * math.cos(math.pi * min(elapsed / self.TRANSITION_DURATION, 1.0))
for i in range(self._num_drones):
d = self._drones[i]
# Target circle position.
cx = self._circle_center[0] + self.CIRCLE_RADIUS * math.cos(d.circle_angle)
cy = self._circle_center[1] + self.CIRCLE_RADIUS * math.sin(d.circle_angle)
cz = self._circle_center[2]
circle_pos = np.array([cx, cy, cz])
# Interpolate between grid and circle.
target = (1.0 - alpha) * d.grid_pos + alpha * circle_pos
# Yaw facing center.
yaw = math.atan2(
self._circle_center[1] - target[1],
self._circle_center[0] - target[0],
)
self._publish_offboard_mode(i, position=True)
self._publish_trajectory(i, position=target.tolist(), yaw=yaw)
if elapsed > self.TRANSITION_DURATION:
self._advance_phase(Phase.HOLD_CIRCLE)
# ------------------------------------------------------------------ #
# Phase 5: Hold circle (rotate CW)
# ------------------------------------------------------------------ #
def _phase_hold_circle(self):
elapsed = self._phase_elapsed()
omega = self.CIRCLE_SPEED / self.CIRCLE_RADIUS # rad/s
for i in range(self._num_drones):
d = self._drones[i]
# CW rotation: subtract omega*t from angle.
angle = d.circle_angle - omega * elapsed
px = self._circle_center[0] + self.CIRCLE_RADIUS * math.cos(angle)
py = self._circle_center[1] + self.CIRCLE_RADIUS * math.sin(angle)
pz = self._circle_center[2]
# Yaw facing center.
yaw = math.atan2(
self._circle_center[1] - py,
self._circle_center[0] - px,
)
self._publish_offboard_mode(i, position=True)
self._publish_trajectory(
i,
position=[px, py, pz],
yaw=yaw,
)
if elapsed > self.HOLD_CIRCLE_DURATION:
self._advance_phase(Phase.KAMIKAZE)
# ------------------------------------------------------------------ #
# Phase 6: Kamikaze dive
# ------------------------------------------------------------------ #
def _phase_kamikaze(self):
all_done = True
elapsed = self._phase_elapsed()
for i in range(self._num_drones):
d = self._drones[i]
if d.impacted:
continue
all_done = False
# Direction to target.
to_target = self._target - d.position
dist = np.linalg.norm(to_target)
# Impact detection.
if dist < self.IMPACT_DIST and d.position[2] > self.IMPACT_Z_THRESH:
self.get_logger().info(
f'[Drone {i}] Impact! pos=[{d.position[0]:.1f}, '
f'{d.position[1]:.1f}, {d.position[2]:.1f}]')
d.impacted = True
self._disarm_force(i)
continue
# Velocity toward target.
if dist > 0.01:
direction = to_target / dist
else:
direction = np.array([1.0, 0.0, 0.0])
vel = (direction * self.KAMIKAZE_SPEED).tolist()
# Yaw toward target.
yaw = math.atan2(to_target[1], to_target[0])
self._publish_offboard_mode(i, velocity=True)
self._publish_trajectory(i, velocity=vel, yaw=yaw)
# Log kamikaze progress every 10 seconds.
elapsed_int = int(elapsed)
if elapsed_int > 0 and elapsed_int % 10 == 0 and elapsed - elapsed_int < 0.1:
for i, d in enumerate(self._drones):
if not d.impacted:
dist = np.linalg.norm(self._target - d.position)
self.get_logger().info(
f'KAMIKAZE {elapsed:.0f}s: D{i} pos=[{d.position[0]:.1f}, '
f'{d.position[1]:.1f}, {d.position[2]:.1f}] '
f'dist={dist:.1f}m armed={d.armed}')
if all_done or elapsed > self.KAMIKAZE_TIMEOUT:
self._print_summary()
self._advance_phase(Phase.DONE)
# Shutdown after a short delay to let final messages flush.
self.create_timer(1.0, lambda: self._shutdown())
def _print_summary(self):
impacted = sum(1 for d in self._drones if d.impacted)
total_time = self._mission_elapsed()
self.get_logger().info('=' * 50)
self.get_logger().info('SWARM MISSION SUMMARY')
self.get_logger().info(f' Drones: {self._num_drones}')
self.get_logger().info(f' Impacts: {impacted}/{self._num_drones}')
self.get_logger().info(f' Total time: {total_time:.1f}s')
self.get_logger().info('=' * 50)
def _shutdown(self):
self.get_logger().info('Swarm controller shutting down.')
self._timer.cancel()
raise SystemExit(0)
def main(args=None):
rclpy.init(args=args)
node = SwarmController()
try:
rclpy.spin(node)
except (KeyboardInterrupt, SystemExit):
node.get_logger().info('Swarm controller exiting.')
finally:
node.destroy_node()
rclpy.try_shutdown()
if __name__ == '__main__':
main()
@@ -0,0 +1,156 @@
"""
Vision provider node for PX4 EKF2 external vision fusion.
Subscribes to ground truth local position and publishes noisy visual odometry
estimates, simulating an external vision system (e.g., motion capture, VIO).
One instance runs per drone. The node decimates to 30 Hz and adds configurable
Gaussian noise to position and velocity.
"""
import numpy as np
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
from px4_msgs.msg import VehicleLocalPosition as VehicleLocalPositionGroundtruth
from px4_msgs.msg import VehicleOdometry
# QoS profile matching PX4 uORB-to-DDS bridge defaults.
PX4_QOS = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.TRANSIENT_LOCAL,
history=HistoryPolicy.KEEP_LAST,
depth=1,
)
class VisionProvider(Node):
"""Publishes noisy VehicleOdometry derived from ground truth."""
# Noise standard deviations
POS_NOISE_STD = 0.5 # metres
ANG_VEL_NOISE_STD = 0.1 # rad/s (unused but kept for completeness)
# Output rate target
OUTPUT_HZ = 30.0
OUTPUT_PERIOD_US = int(1e6 / OUTPUT_HZ)
def __init__(self):
# Declare parameters before calling super().__init__ so they are
# available immediately.
super().__init__('vision_provider')
self.declare_parameter('namespace', 'px4_0')
self.declare_parameter('instance_id', 0)
self._namespace = self.get_parameter('namespace').value
self._instance_id = self.get_parameter('instance_id').value
self.get_logger().info(
f'Starting vision_provider_{self._instance_id} '
f'for namespace /{self._namespace}'
)
self.get_logger().warn(
'Note: vehicle_local_position_groundtruth is NOT published '
'over DDS by PX4. This node is a no-op when using '
'sensor_agp_sim (SENS_EN_AGPSIM=1) for position. '
'It will only produce output if EV fusion is enabled and '
'ground truth is published via a custom DDS bridge.'
)
self._last_pub_time_us: int = 0
self._msg_count: int = 0
self._rng = np.random.default_rng()
# -- Subscriber: ground truth local position --
# Note: PX4 SIH does not publish this topic over XRCE-DDS.
# With sensor_agp_sim, EKF2 gets position internally.
gt_topic = f'/{self._namespace}/fmu/out/vehicle_local_position_groundtruth'
self._sub_gt = self.create_subscription(
VehicleLocalPositionGroundtruth,
gt_topic,
self._gt_callback,
PX4_QOS,
)
# -- Publisher: visual odometry for EKF2 --
vo_topic = f'/{self._namespace}/fmu/in/vehicle_visual_odometry'
self._pub_vo = self.create_publisher(VehicleOdometry, vo_topic, PX4_QOS)
# --------------------------------------------------------------------- #
# Callback
# --------------------------------------------------------------------- #
def _gt_callback(self, msg: VehicleLocalPositionGroundtruth):
"""Decimate ground truth and publish noisy visual odometry."""
now_us = int(self.get_clock().now().nanoseconds / 1000)
# Rate-limit to OUTPUT_HZ.
if (now_us - self._last_pub_time_us) < self.OUTPUT_PERIOD_US:
return
self._last_pub_time_us = now_us
# Build VehicleOdometry message.
vo = VehicleOdometry()
vo.timestamp = now_us
vo.timestamp_sample = msg.timestamp
# Frames: NED
vo.pose_frame = VehicleOdometry.POSE_FRAME_NED
vo.velocity_frame = VehicleOdometry.VELOCITY_FRAME_NED
# Position with Gaussian noise.
noise_pos = self._rng.normal(0.0, self.POS_NOISE_STD, size=3).astype(np.float32)
vo.position = [
msg.x + float(noise_pos[0]),
msg.y + float(noise_pos[1]),
msg.z + float(noise_pos[2]),
]
# Quaternion (pass through, no noise on orientation).
vo.q = [msg.heading, 0.0, 0.0, 0.0]
# If ground truth provides a full quaternion, prefer that. The
# VehicleLocalPosition message only carries heading; construct a
# yaw-only quaternion.
half_yaw = msg.heading / 2.0
vo.q = [
float(np.cos(half_yaw)),
0.0,
0.0,
float(np.sin(half_yaw)),
]
# Velocity with small noise.
noise_vel = self._rng.normal(0.0, 0.1, size=3).astype(np.float32)
vo.velocity = [
msg.vx + float(noise_vel[0]),
msg.vy + float(noise_vel[1]),
msg.vz + float(noise_vel[2]),
]
# Angular velocity (not used, set to zero).
vo.angular_velocity = [0.0, 0.0, 0.0]
# Variances (squared standard deviations).
vo.position_variance = [0.25, 0.25, 0.25] # 0.5^2
vo.orientation_variance = [0.01, 0.01, 0.01]
vo.velocity_variance = [0.1, 0.1, 0.1]
self._pub_vo.publish(vo)
def main(args=None):
rclpy.init(args=args)
node = VisionProvider()
try:
rclpy.spin(node)
except KeyboardInterrupt:
node.get_logger().info('Shutting down vision provider.')
finally:
node.destroy_node()
rclpy.try_shutdown()
if __name__ == '__main__':
main()
+3 -3
View File
@@ -259,7 +259,7 @@
#define GPIO_VDD_5V_PERIPH_nOC /* PE15 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTE|GPIO_PIN15)
#define GPIO_VDD_5V_HIPOWER_nEN /* PG10 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTG|GPIO_PIN10)
#define GPIO_VDD_5V_HIPOWER_nOC /* PF13 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTF|GPIO_PIN13)
#define GPIO_VDD_3V3_SENSORS_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SENSORS4_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SPEKTRUM_POWER_EN /* PH2 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN2)
#define GPIO_VDD_3V3_SD_CARD_EN /* PC13 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTC|GPIO_PIN13)
@@ -286,7 +286,7 @@
#define VDD_5V_PERIPH_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_PERIPH_nEN, !(on_true))
#define VDD_5V_HIPOWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_HIPOWER_nEN, !(on_true))
#define VDD_3V3_SENSORS_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS_EN, (on_true))
#define VDD_3V3_SENSORS4_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS4_EN, (on_true))
#define VDD_3V3_SPEKTRUM_POWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SPEKTRUM_POWER_EN, (on_true))
#define READ_VDD_3V3_SPEKTRUM_POWER_EN() px4_arch_gpioread(GPIO_VDD_3V3_SPEKTRUM_POWER_EN)
#define VDD_3V3_SD_CARD_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SD_CARD_EN, (on_true))
@@ -452,7 +452,7 @@
GPIO_VDD_5V_PERIPH_nOC, \
GPIO_VDD_5V_HIPOWER_nEN, \
GPIO_VDD_5V_HIPOWER_nOC, \
GPIO_VDD_3V3_SENSORS_EN, \
GPIO_VDD_3V3_SENSORS4_EN, \
GPIO_VDD_3V3_SPEKTRUM_POWER_EN, \
GPIO_VDD_3V3_SD_CARD_EN, \
GPIO_PD15, \
+3 -3
View File
@@ -107,7 +107,7 @@ __EXPORT void board_peripheral_reset(int ms)
VDD_5V_PERIPH_EN(false);
board_control_spi_sensors_power(false, 0xffff);
VDD_3V3_SENSORS_EN(false);
VDD_3V3_SENSORS4_EN(false);
bool last = READ_VDD_3V3_SPEKTRUM_POWER_EN();
/* Keep Spektum on to discharge rail*/
@@ -122,7 +122,7 @@ __EXPORT void board_peripheral_reset(int ms)
/* switch the peripheral rail back on */
VDD_3V3_SPEKTRUM_POWER_EN(last);
board_control_spi_sensors_power(true, 0xffff);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_5V_PERIPH_EN(true);
}
@@ -219,7 +219,7 @@ __EXPORT int board_app_initialize(uintptr_t arg)
VDD_3V3_SD_CARD_EN(true);
VDD_5V_PERIPH_EN(true);
VDD_5V_HIPOWER_EN(true);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_3V3_SPEKTRUM_POWER_EN(true);
/* Need hrt running before using the ADC */
+1 -1
View File
@@ -48,7 +48,7 @@ constexpr px4_spi_bus_t px4_spi_buses[SPI_BUS_MAX_BUS_ITEMS] = {
}, {GPIO::PortE, GPIO::Pin7}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
initSPIBus(SPI::Bus::SPI5, {
initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
-1
View File
@@ -1 +0,0 @@
CONFIG_MAVLINK_DIALECT="development"
+6 -3
View File
@@ -273,7 +273,7 @@
#define GPIO_VDD_5V_PERIPH_nOC /* PE15 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTE|GPIO_PIN15)
#define GPIO_VDD_5V_HIPOWER_nEN /* PG10 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTG|GPIO_PIN10)
#define GPIO_VDD_5V_HIPOWER_nOC /* PF13 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTF|GPIO_PIN13)
#define GPIO_VDD_3V3_SENSORS_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SENSORS4_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SPEKTRUM_POWER_EN /* PH2 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN2)
#define GPIO_VDD_3V3_SD_CARD_EN /* PC13 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTC|GPIO_PIN13)
@@ -295,7 +295,7 @@
#define VDD_5V_PERIPH_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_PERIPH_nEN, !(on_true))
#define VDD_5V_HIPOWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_HIPOWER_nEN, !(on_true))
#define VDD_3V3_SENSORS_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS_EN, (on_true))
#define VDD_3V3_SENSORS4_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS4_EN, (on_true))
#define VDD_3V3_SPEKTRUM_POWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SPEKTRUM_POWER_EN, (on_true))
#define READ_VDD_3V3_SPEKTRUM_POWER_EN() px4_arch_gpioread(GPIO_VDD_3V3_SPEKTRUM_POWER_EN)
#define VDD_3V3_SD_CARD_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SD_CARD_EN, (on_true))
@@ -421,6 +421,9 @@
/* This board provides a DMA pool and APIs */
#define BOARD_DMA_ALLOC_POOL_SIZE 5120
/* This board has 4 DMA channels available for bidirectional dshot */
#define BOARD_DMA_NUM_DSHOT_CHANNELS 4
/* This board provides the board_on_reset interface */
#define BOARD_HAS_ON_RESET 1
@@ -452,7 +455,7 @@
GPIO_VDD_5V_PERIPH_nOC, \
GPIO_VDD_5V_HIPOWER_nEN, \
GPIO_VDD_5V_HIPOWER_nOC, \
GPIO_VDD_3V3_SENSORS_EN, \
GPIO_VDD_3V3_SENSORS4_EN, \
GPIO_VDD_3V3_SPEKTRUM_POWER_EN, \
GPIO_VDD_3V3_SD_CARD_EN, \
GPIO_PD15, \
+3 -3
View File
@@ -108,7 +108,7 @@ __EXPORT void board_peripheral_reset(int ms)
VDD_5V_HIPOWER_EN(false);
VDD_5V_PERIPH_EN(false);
board_control_spi_sensors_power(false, 0xffff);
VDD_3V3_SENSORS_EN(false);
VDD_3V3_SENSORS4_EN(false);
SPI6_RESET(true);
bool last = READ_VDD_3V3_SPEKTRUM_POWER_EN();
@@ -124,7 +124,7 @@ __EXPORT void board_peripheral_reset(int ms)
/* switch the peripheral rail back on */
VDD_3V3_SPEKTRUM_POWER_EN(last);
board_control_spi_sensors_power(true, 0xffff);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_5V_HIPOWER_EN(true);
VDD_5V_PERIPH_EN(true);
@@ -221,7 +221,7 @@ __EXPORT int board_app_initialize(uintptr_t arg)
/* Power on Interfaces */
VDD_5V_PERIPH_EN(true);
VDD_5V_HIPOWER_EN(true);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_3V3_SPEKTRUM_POWER_EN(true);
SPI6_RESET(false);
+2 -2
View File
@@ -48,7 +48,7 @@ constexpr px4_spi_bus_all_hw_t px4_spi_buses_all_hw[BOARD_NUM_SPI_CFG_HW_VERSION
}, {GPIO::PortE, GPIO::Pin7}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
initSPIBus(SPI::Bus::SPI5, {
initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
@@ -71,7 +71,7 @@ constexpr px4_spi_bus_all_hw_t px4_spi_buses_all_hw[BOARD_NUM_SPI_CFG_HW_VERSION
}, {GPIO::PortE, GPIO::Pin7}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
initSPIBus(SPI::Bus::SPI5, {
initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
-19
View File
@@ -1,19 +0,0 @@
4001_quad_x
4050_generic_250
6001_hexa_x
12001_octo_cox
13100_generic_vtol_tiltrotor
5001_quad_+
24001_dodeca_cox
2100_standard_plane
13000_generic_vtol_standard
4601_droneblocks_dexi_5
11001_hexa_cox
14001_generic_mc_with_tilt
16001_helicopter
9001_octo_+
7001_hexa_+
3000_generic_wing
2106_albatross
13200_generic_vtol_tailsitter
13030_generic_vtol_quad_tiltrotor
-1
View File
@@ -1 +0,0 @@
CONFIG_MAVLINK_DIALECT="development"
+3
View File
@@ -306,6 +306,9 @@
/* This board provides a DMA pool and APIs */
#define BOARD_DMA_ALLOC_POOL_SIZE 5120
/* This board has 3 DMA channels available for bidirectional dshot */
#define BOARD_DMA_NUM_DSHOT_CHANNELS 3
/* This board provides the board_on_reset interface */
#define BOARD_HAS_ON_RESET 1
-1
View File
@@ -1 +0,0 @@
CONFIG_MAVLINK_DIALECT="development"
+3
View File
@@ -311,6 +311,9 @@
/* This board provides a DMA pool and APIs */
#define BOARD_DMA_ALLOC_POOL_SIZE 5120
/* This board has 4 DMA channels available for bidirectional dshot */
#define BOARD_DMA_NUM_DSHOT_CHANNELS 4
/* This board provides the board_on_reset interface */
#define BOARD_HAS_ON_RESET 1
+2 -2
View File
@@ -48,7 +48,7 @@ constexpr px4_spi_bus_all_hw_t px4_spi_buses_all_hw[BOARD_NUM_SPI_CFG_HW_VERSION
}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
// initSPIBus(SPI::Bus::SPI5, {
// initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
@@ -69,7 +69,7 @@ constexpr px4_spi_bus_all_hw_t px4_spi_buses_all_hw[BOARD_NUM_SPI_CFG_HW_VERSION
}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
// initSPIBus(SPI::Bus::SPI5, {
// initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
+3 -3
View File
@@ -262,7 +262,7 @@
#define GPIO_VDD_5V_PERIPH_nOC /* PE15 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTE|GPIO_PIN15)
#define GPIO_VDD_5V_HIPOWER_nEN /* PG10 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_SET|GPIO_PORTG|GPIO_PIN10)
#define GPIO_VDD_5V_HIPOWER_nOC /* PF13 */ (GPIO_INPUT |GPIO_FLOAT|GPIO_PORTF|GPIO_PIN13)
#define GPIO_VDD_3V3_SENSORS_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SENSORS4_EN /* PG8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN8)
#define GPIO_VDD_3V3_SPEKTRUM_POWER_EN /* PH2 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN2)
#define GPIO_VDD_3V3_SD_CARD_EN /* PC13 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTC|GPIO_PIN13)
@@ -289,7 +289,7 @@
#define VDD_5V_PERIPH_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_PERIPH_nEN, !(on_true))
#define VDD_5V_HIPOWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_5V_HIPOWER_nEN, !(on_true))
#define VDD_3V3_SENSORS_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS_EN, (on_true))
#define VDD_3V3_SENSORS4_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SENSORS4_EN, (on_true))
#define VDD_3V3_SPEKTRUM_POWER_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SPEKTRUM_POWER_EN, (on_true))
#define READ_VDD_3V3_SPEKTRUM_POWER_EN() px4_arch_gpioread(GPIO_VDD_3V3_SPEKTRUM_POWER_EN)
#define VDD_3V3_SD_CARD_EN(on_true) px4_arch_gpiowrite(GPIO_VDD_3V3_SD_CARD_EN, (on_true))
@@ -453,7 +453,7 @@
GPIO_VDD_5V_PERIPH_nOC, \
GPIO_VDD_5V_HIPOWER_nEN, \
GPIO_VDD_5V_HIPOWER_nOC, \
GPIO_VDD_3V3_SENSORS_EN, \
GPIO_VDD_3V3_SENSORS4_EN, \
GPIO_VDD_3V3_SPEKTRUM_POWER_EN, \
GPIO_VDD_3V3_SD_CARD_EN, \
GPIO_PD15, \
+3 -3
View File
@@ -107,7 +107,7 @@ __EXPORT void board_peripheral_reset(int ms)
VDD_5V_PERIPH_EN(false);
board_control_spi_sensors_power(false, 0xffff);
VDD_3V3_SENSORS_EN(false);
VDD_3V3_SENSORS4_EN(false);
bool last = READ_VDD_3V3_SPEKTRUM_POWER_EN();
/* Keep Spektum on to discharge rail*/
@@ -122,7 +122,7 @@ __EXPORT void board_peripheral_reset(int ms)
/* switch the peripheral rail back on */
VDD_3V3_SPEKTRUM_POWER_EN(last);
board_control_spi_sensors_power(true, 0xffff);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_5V_PERIPH_EN(true);
}
@@ -215,7 +215,7 @@ __EXPORT int board_app_initialize(uintptr_t arg)
VDD_3V3_SD_CARD_EN(true);
VDD_5V_PERIPH_EN(true);
VDD_5V_HIPOWER_EN(true);
VDD_3V3_SENSORS_EN(true);
VDD_3V3_SENSORS4_EN(true);
VDD_3V3_SPEKTRUM_POWER_EN(true);
/* Need hrt running before using the ADC */
+1 -1
View File
@@ -48,7 +48,7 @@ constexpr px4_spi_bus_t px4_spi_buses[SPI_BUS_MAX_BUS_ITEMS] = {
}, {GPIO::PortE, GPIO::Pin7}),
// initSPIBus(SPI::Bus::SPI4, {
// // no devices
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS_EN from board_config.h
// TODO: if enabled, remove GPIO_VDD_3V3_SENSORS4_EN from board_config.h
// }, {GPIO::PortG, GPIO::Pin8}),
initSPIBus(SPI::Bus::SPI5, {
initSPIDevice(SPIDEV_FLASH(0), SPI::CS{GPIO::PortG, GPIO::Pin7})
@@ -42,8 +42,6 @@ px4_add_module(
syslink_bridge.cpp
syslink_memory.cpp
syslink.c
MODULE_CONFIG
syslink_params.yaml
DEPENDS
battery
)
@@ -1,6 +1,6 @@
/****************************************************************************
*
* Copyright (c) 2026 PX4 Development Team. All rights reserved.
* Copyright (c) 2016 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -10,7 +10,8 @@
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the distribution.
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
@@ -30,39 +31,42 @@
*
****************************************************************************/
#include "rallyPointCheck.hpp"
/**
* @file syslink_params.c
*
* Parameters defined by the syslink module and the exposed NRF51 radio
*
* @author Dennis Shtatnov <densht@gmail.com>
*/
RallyPointChecks::RallyPointChecks()
: _param_rtl_type_handle(param_find("RTL_TYPE"))
{
}
/**
* Operating channel of the NRF51
*
* @min 0
* @max 125
* @group Syslink
*/
PARAM_DEFINE_INT32(SLNK_RADIO_CHAN, 80);
void RallyPointChecks::checkAndReport(const Context &context, Report &reporter)
{
int32_t rtl_type = 0;
/**
* Operating datarate of the NRF51
*
* @min 0
* @max 2
* @group Syslink
*/
PARAM_DEFINE_INT32(SLNK_RADIO_RATE, 2);
if (param_get(_param_rtl_type_handle, &rtl_type) != 0 || rtl_type != 5) {
// Only enforce rally point requirement when RTL_TYPE == 5 (safe points only)
return;
}
/**
* Operating address of the NRF51 (most significant byte)
*
* @group Syslink
*/
PARAM_DEFINE_INT32(SLNK_RADIO_ADDR1, 231); // 0xE7
rtl_status_s rtl_status;
if (!_rtl_status_sub.copy(&rtl_status) || rtl_status.safe_point_index == UINT8_MAX) {
/* EVENT
* @description
* Upload at least one rally point before arming, or change <param>RTL_TYPE</param>.
*
* <profile name="dev">
* This check is active when RTL_TYPE is set to 5 (safe points only).
* </profile>
*/
reporter.armingCheckFailure(NavModes::All, health_component_t::system,
events::ID("check_rally_point_missing"),
events::Log::Error, "No rally point available");
if (reporter.mavlink_log_pub()) {
mavlink_log_critical(reporter.mavlink_log_pub(), "Preflight Fail: No rally point available\t");
}
}
}
/**
* Operating address of the NRF51 (least significant 4 bytes)
*
* @group Syslink
*/
PARAM_DEFINE_INT32(SLNK_RADIO_ADDR2, 3890735079); // 0xE7E7E7E7
@@ -1,28 +0,0 @@
module_name: syslink
parameters:
- group: Syslink
definitions:
SLNK_RADIO_CHAN:
description:
short: Operating channel of the NRF51
type: int32
default: 80
min: 0
max: 125
SLNK_RADIO_RATE:
description:
short: Operating datarate of the NRF51
type: int32
default: 2
min: 0
max: 2
SLNK_RADIO_ADDR1:
description:
short: Operating address of the NRF51 (most significant byte)
type: int32
default: 231
SLNK_RADIO_ADDR2:
description:
short: Operating address of the NRF51 (least significant 4 bytes)
type: int32
default: -404232217
Binary file not shown.
+1 -1
View File
@@ -104,7 +104,7 @@
#define OSC_FREQ 8
#define BOARD_PIN_LED_ACTIVITY GPIO_nLED_BLUE // BLUE
#define BOARD_PIN_LED_BOOTLOADER GPIO_nLED_RED // RED
#define BOARD_PIN_LED_BOOTLOADER GPIO_nLED_GREEN // GREEN
#define BOARD_LED_ON 0
#define BOARD_LED_OFF 1
-1
View File
@@ -169,7 +169,6 @@ __EXPORT int board_app_initialize(uintptr_t arg)
drv_led_start();
led_off(LED_RED);
led_off(LED_BLUE);
led_off(LED_GREEN);
if (board_hardfault_init(2, true) != 0) {
led_on(LED_BLUE);
+3 -22
View File
@@ -63,23 +63,12 @@ extern void led_toggle(int led);
__END_DECLS
# define xlat(p) (p)
// index: 0=BLUE, 1=RED, 2=SAFETY, 3=GREEN
static uint32_t g_ledmap[] = {
GPIO_nLED_BLUE, // LED_BLUE (0)
GPIO_nLED_RED, // LED_RED (1)
0, // LED_SAFETY (2) - no independent safety LED, use 0 placeholder
GPIO_nLED_GREEN, // LED_GREEN (3)
GPIO_nLED_GREEN, // Indexed by BOARD_LED_GREEN
GPIO_nLED_BLUE, // Indexed by BOARD_LED_BLUE
GPIO_nLED_RED, // Indexed by BOARD_LED_RED
};
#ifndef arraySize
#define arraySize(a) (sizeof((a))/sizeof(((a)[0])))
#endif
static inline bool valid_led_index(int led)
{
return (led >= 0) && ((size_t)led < arraySize(g_ledmap));
}
__EXPORT void led_init(void)
{
/* Configure LED GPIOs for output */
@@ -92,10 +81,6 @@ __EXPORT void led_init(void)
static void phy_set_led(int led, bool state)
{
if (!valid_led_index(led)) {
return;
}
/* Drive Low to switch on */
if (g_ledmap[led] != 0) {
stm32_gpiowrite(g_ledmap[led], !state);
@@ -104,10 +89,6 @@ static void phy_set_led(int led, bool state)
static bool phy_get_led(int led)
{
if (!valid_led_index(led)) {
return false;
}
/* If Low it is on */
if (g_ledmap[led] != 0) {
return !stm32_gpioread(g_ledmap[led]);
@@ -37,8 +37,6 @@ px4_add_module(
SRCS
pwm_voltage.cpp
MODULE_CONFIG
parameters.yaml
DEPENDS
px4_work_queue
)
@@ -0,0 +1,44 @@
/****************************************************************************
*
* Copyright (c) 2024 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
/**
* Control PWM output voltage
*
* Enable: PWM output voltage 5V
* Disable: PWM output voltage 3.3V
*
* @boolean
* @reboot_required true
* @group PWM Outputs
*/
PARAM_DEFINE_INT32(PWM_LEVEL_CONT, 0);
@@ -1,13 +0,0 @@
module_name: pwm_voltage
parameters:
- group: PWM Outputs
definitions:
PWM_LEVEL_CONT:
description:
short: Control PWM output voltage
long: |-
Enable: PWM output voltage 5V
Disable: PWM output voltage 3.3V
type: boolean
default: 0
reboot_required: true

Some files were not shown because too many files have changed in this diff Show More