Compare commits

..

1 Commits

Author SHA1 Message Date
Hamish Willee ea3f7bcfb2 Copilot Review instructions: Drivers 2026-02-11 10:45:28 +11:00
3169 changed files with 36917 additions and 88271 deletions
-75
View File
@@ -105,81 +105,6 @@ Checks: '*,
-readability-redundant-declaration,
-readability-static-accessed-through-instance,
-readability-static-definition-in-anonymous-namespace,
-altera-struct-pack-align,
-bugprone-easily-swappable-parameters,
-concurrency-mt-unsafe,
-cppcoreguidelines-avoid-const-or-ref-data-members,
-cppcoreguidelines-macro-usage,
-cppcoreguidelines-non-private-member-variables-in-classes,
-hicpp-uppercase-literal-suffix,
-llvm-qualified-auto,
-misc-non-private-member-variables-in-classes,
-misc-use-anonymous-namespace,
-modernize-concat-nested-namespaces,
-readability-const-return-type,
-readability-identifier-length,
-readability-isolate-declaration,
-readability-qualified-auto,
-readability-redundant-access-specifiers,
-cppcoreguidelines-avoid-do-while,
-misc-include-cleaner,
-misc-const-correctness,
-llvm-else-after-return,
-readability-function-cognitive-complexity,
-cppcoreguidelines-init-variables,
-bugprone-reserved-identifier,
-cert-dcl37-c,
-cert-dcl51-cpp,
-modernize-use-nodiscard,
-misc-confusable-identifiers,
-cert-err33-c,
-readability-redundant-inline-specifier,
-readability-uppercase-literal-suffix,
-bugprone-narrowing-conversions,
-cppcoreguidelines-narrowing-conversions,
-bugprone-switch-missing-default-case,
-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,
-cppcoreguidelines-explicit-virtual-functions,
-readability-convert-member-functions-to-static,
-readability-make-member-function-const,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-signed-char-misuse,
-cppcoreguidelines-avoid-non-const-global-variables,
-cppcoreguidelines-use-default-member-init,
-hicpp-multiway-paths-covered,
-hicpp-named-parameter,
-misc-header-include-cycle,
-misc-no-recursion,
-performance-no-int-to-ptr,
-readability-avoid-return-with-void-value,
-readability-avoid-unconditional-preprocessor-if,
-readability-delete-null-pointer,
-readability-redundant-casting,
-readability-redundant-member-init,
-readability-reference-to-constructed-temporary,
-readability-simplify-boolean-expr,
-cert-msc32-c,
-cert-msc33-c,
-cert-msc51-cpp,
-cert-str34-c,
-cppcoreguidelines-macro-to-enum,
-modernize-macro-to-enum,
-abseil-string-find-str-contains,
-bugprone-suspicious-include,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
-clang-analyzer-optin.core.EnumCastOutOfRange,
-modernize-type-traits,
-misc-definitions-in-headers,
-bugprone-casting-through-void,
-readability-redundant-string-init,
'
WarningsAsErrors: '*'
CheckOptions:
+1 -3
View File
@@ -21,10 +21,8 @@ applyTo: "docs/en/**"
- Do not apply bold or italic styling inside a heading.
- **Formatting:**
- **Bold:** Only for UI elements (buttons, menu items).
- **Italics (Emphasis):** For tool names (e.g., *QGroundControl*).
- **Inline Code:** Use backticks for file paths, parameters, and CLI commands (e.g., `prettier`).
Backticks are optional for hyperlinked CLI commands and tool names.
- **Italics (Emphasis):** Use for application names (e.g., *QGroundControl*).
Emphasis is optional for hyperlinked applications.
- **Structure:** End every line at the end of a sentence (Semantic Line Breaks).
## Linking & Navigation
@@ -0,0 +1,212 @@
---
applyTo: "src/drivers/**"
---
# PX4 Driver Review Instructions
This file provides guidelines for reviewing driver files in the `src/drivers/` directory.
## Copyright Statements
### Required Files
All source files (`.cpp`, `.c`, `.hpp`, `.h`, `CMakeLists.txt`) MUST include a copyright statement at the top.
**Exceptions:**
- `Kconfig` files
- `module.yaml` files
### Copyright Format
**For new files (created in 2026):**
```cpp
/****************************************************************************
*
* Copyright (c) 2026 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.
*
****************************************************************************/
```
**For updated files (originally created in 2015, updated in 2026):**
```cpp
/****************************************************************************
*
* Copyright (c) 2015-2026 PX4 Development Team. All rights reserved.
*
* [... rest of copyright text ...]
*
****************************************************************************/
```
### Key Points
- First year should be the original creation year
- When updating an existing file, update the year range: `YYYY-2026`
- Use the current year (2026 in this case) for new files
- Maintain consistent formatting with other PX4 driver files
---
## Driver Self-Documentation
All drivers MUST be self-documenting through the `print_usage()` method.
### Required Implementation
Every driver `.cpp` file must implement a `print_usage()` method that includes:
1. **PRINT_MODULE_DESCRIPTION macro** - Contains markdown documentation
2. **Module identification** - Using PRINT_MODULE_USAGE_NAME and optionally PRINT_MODULE_USAGE_SUBCATEGORY
3. **Parameter documentation** - Relevant parameters, especially enablement parameters
### PRINT_MODULE_DESCRIPTION Structure
The description should follow this markdown format:
```cpp
PRINT_MODULE_DESCRIPTION(
R"DESCR_STR(
### Description
[Clear description of what the driver does and its primary functionality]
### Implementation
[Optional: High-level overview of how the driver works]
### Examples
[Optional: CLI usage examples if non-trivial]
$ module_name start -d /dev/ttyS1
$ module_name stop
)DESCR_STR");
```
### Required Sections
#### 1. Description Section
- Brief explanation of the driver's purpose
- Key features/capabilities
- Important parameters (especially enable parameters like `SENS_XX_CFG`)
#### 2. Documentation Links
When applicable, include links to user documentation:
```cpp
Setup/usage information: https://docs.px4.io/main/en/sensor/[sensor-name].html
```
#### 3. Examples Section (when relevant)
Provide CLI usage examples for non-trivial commands:
```cpp
### Examples
Attempt to start driver on a specified serial device.
$ vectornav start -d /dev/ttyS1
Stop driver
$ vectornav stop
```
### Complete Example
```cpp
int MyDriver::print_usage(const char *reason)
{
if (reason) {
PX4_WARN("%s\n", reason);
}
PRINT_MODULE_DESCRIPTION(
R"DESCR_STR(
### Description
This driver interfaces with the XYZ sensor via I2C/SPI. It provides
distance measurements and is automatically started when SENS_XYZ_CFG
is set to the appropriate value.
Key features:
- Distance range: 0.2m to 50m
- Update rate: up to 100Hz
- I2C and SPI support
Setup/usage information: https://docs.px4.io/main/en/sensor/xyz_sensor.html
### Examples
Start driver on I2C bus 1 with address 0x29:
$ xyz_sensor start -X -b 1 -a 0x29
)DESCR_STR");
PRINT_MODULE_USAGE_NAME("xyz_sensor", "driver");
PRINT_MODULE_USAGE_SUBCATEGORY("distance_sensor");
PRINT_MODULE_USAGE_COMMAND("start");
PRINT_MODULE_USAGE_PARAMS_I2C_SPI_DRIVER(true, true);
PRINT_MODULE_USAGE_PARAMS_I2C_ADDRESS(0x29);
PRINT_MODULE_USAGE_DEFAULT_COMMANDS();
return 0;
}
```
### Common Patterns by Driver Type
**For UART/Serial Drivers:**
```cpp
PRINT_MODULE_USAGE_PARAM_STRING('d', "/dev/ttyS3", "<file:dev>", "Serial device", true);
```
**For I2C/SPI Drivers:**
```cpp
PRINT_MODULE_USAGE_PARAMS_I2C_SPI_DRIVER(true, true);
PRINT_MODULE_USAGE_PARAMS_I2C_ADDRESS(0x76);
```
**For Sensor Drivers:**
```cpp
PRINT_MODULE_USAGE_SUBCATEGORY("distance_sensor"); // or imu, magnetometer, etc.
```
---
## Review Checklist
When reviewing driver files, verify:
- [ ] Copyright header is present (except Kconfig and module.yaml)
- [ ] Copyright year is correct (current year for new files, year range for updates)
- [ ] `print_usage()` method exists
- [ ] `PRINT_MODULE_DESCRIPTION` macro is present with markdown content
- [ ] Description section explains driver purpose clearly
- [ ] Relevant parameters are documented (especially enable parameters)
- [ ] Documentation links are included when available
- [ ] Examples are provided for complex CLI usage
- [ ] Module name and category are correctly specified
- [ ] Standard commands (start, stop, status) are documented
---
## Additional Resources
- Driver template examples: `src/drivers/*/` (various sensor types)
- Module macros: `platforms/common/include/px4_platform_common/module.h`
- Similar drivers for reference patterns (DShot, VectorNav, CrsfRc, etc.)
+27 -54
View File
@@ -2,37 +2,6 @@
# - If you want to keep the tests running in GitHub Actions you need to uncomment the "runs-on: ubuntu-latest" lines
# and comment the "runs-on: [runs-on,runner=..." lines.
# - If you would like to duplicate this setup try setting up "RunsOn" on your own AWS account try https://runs-on.com
#
# ===================================================================================
# RELEASE UPLOAD LOGIC
# ===================================================================================
# This workflow handles building firmware and uploading to S3 + GitHub Releases.
#
# S3 Bucket Structure (s3://px4-travis/Firmware/):
# - master/ <- Latest main branch build (for QGC compatibility)
# - stable/ <- Latest stable release, controlled by 'stable' branch
# - beta/ <- Latest pre-release, controlled by 'beta' branch
# - vX.Y.Z/ <- Archived stable release
# - vX.Y.Z-beta1/ <- Archived pre-release
#
# Trigger Behavior:
# - Tag v1.16.1 -> Upload to: v1.16.1/ only (versioned archive)
# - Tag v1.17.0-beta1 -> Upload to: v1.17.0-beta1/ only (versioned archive)
# - Branch main -> Upload to: master/ (for QGC compatibility)
# - Branch stable -> Upload to: stable/ (QGC stable firmware)
# - Branch beta -> Upload to: beta/ (QGC beta firmware)
# - Branch release/** -> Build only, no S3 upload (CI validation)
# - Pull requests -> Build only, no S3 upload (CI validation)
#
# GitHub Releases:
# - All version tags create a draft GitHub Release
# - Pre-releases (alpha/beta/rc suffixes) are automatically marked as such
#
# IMPORTANT: Version tags do NOT upload to stable/ or beta/. Only the
# corresponding branch pushes control those directories. This prevents
# pre-release tags from accidentally overwriting stable firmware (#26340)
# and avoids race conditions between tag and branch builds.
# ===================================================================================
name: Build all targets
@@ -60,7 +29,6 @@ concurrency:
permissions:
contents: write
actions: read
packages: read
jobs:
group_targets:
@@ -126,9 +94,6 @@ jobs:
fail-fast: false
container:
image: ${{ matrix.container }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: runs-on/action@v2
- uses: actions/checkout@v4
@@ -194,13 +159,6 @@ jobs:
path: ~/.ccache
key: ${{ steps.cc_restore.outputs.cache-primary-key }}
# ===========================================================================
# ARTIFACT UPLOAD JOB
# ===========================================================================
# Uploads build artifacts to S3 and creates GitHub Releases.
# Runs for version tags (v*), main, stable, and beta branch pushes.
# See header comments for full upload logic documentation.
# ===========================================================================
artifacts:
name: Upload Artifacts
# runs-on: ubuntu-latest
@@ -219,31 +177,31 @@ jobs:
- name: Choose Upload Location
id: upload-location
run: |
# Determine upload location based on branch or tag with the following considerations:
# Destination: AWS S3 bucket px4-travis in folder Firmware/
# - If branch is main -> upload to master/
# - Older versions of QGC are hardocded to look for master/
# - If branch is stable or beta -> upload to stable/ or beta/
# - If a tag vX.Y.Z -> upload to vX.Y.Z/
# - Also update stable/ to point to the same version
#. - Older versions of QGC are hardocded to look for stable/
# - If a pull request -> do not upload
set -euo pipefail
ref="${GITHUB_REF}"
branch=${{ needs.group_targets.outputs.branchname }}
location="$branch"
is_prerelease="false"
# Main branch uploads to "master" for QGC backward compatibility
if [[ "$branch" == "main" ]]; then
location="master"
fi
# Version tags: upload to versioned directory (e.g., v1.16.1/)
if [[ "$ref" == refs/tags/v[0-9]* ]]; then
tag="${ref#refs/tags/}"
location="$tag"
# Pre-release tags contain -alpha, -beta, or -rc suffix
if [[ "$tag" =~ -(alpha|beta|rc) ]]; then
is_prerelease="true"
fi
fi
echo "uploadlocation=$location" >> $GITHUB_OUTPUT
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
- name: Uploading Artifacts to S3 [${{ steps.upload-location.outputs.uploadlocation }}]
uses: jakejarvis/s3-sync-action@master
@@ -257,13 +215,28 @@ jobs:
SOURCE_DIR: artifacts/
DEST_DIR: Firmware/${{ steps.upload-location.outputs.uploadlocation }}/
# Create a draft GitHub Release for all version tags
# Pre-releases are automatically marked as such
# if we are uploading artifacts to a versioned folder
# we should also update the stable folder in the s3 bucket
- name: Uploading Artifacts to S3 [stable]
uses: jakejarvis/s3-sync-action@master
if: startsWith(github.ref, 'refs/tags/v')
with:
args: --acl public-read
env:
AWS_S3_BUCKET: 'px4-travis'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'us-west-1'
SOURCE_DIR: artifacts/
DEST_DIR: Firmware/stable/
# if build is a release triggered by a versioned tag then create a github release
# and upload the build artifacts. A draft release is created so that the release
# can be reviewed before publishing
- name: Upload Artifacts to GitHub Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
prerelease: ${{ steps.upload-location.outputs.is_prerelease == 'true' }}
files: artifacts/*.px4
name: ${{ steps.upload-location.outputs.uploadlocation }}
+8 -9
View File
@@ -19,10 +19,6 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
container:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
strategy:
fail-fast: false
matrix:
@@ -39,17 +35,20 @@ jobs:
"px4_sitl_allyes",
"module_documentation",
]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Building [${{ matrix.check }}]
run: |
cd "$GITHUB_WORKSPACE"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
make ${{ matrix.check }}
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
options: -v ${{ github.workspace }}:/workspace
run: |
cd /workspace
git config --global --add safe.directory /workspace
make ${{ matrix.check }}
- name: Uploading Coverage to Codecov.io
if: contains(matrix.check, 'coverage')
+11 -50
View File
@@ -1,4 +1,4 @@
name: Static Analysis
name: Clang Tidy
on:
push:
@@ -11,59 +11,20 @@ on:
- '**'
paths-ignore:
- 'docs/**'
permissions:
contents: read
jobs:
clang_tidy:
name: Clang-Tidy
runs-on: [runs-on, runner=16cpu-linux-x64, "run-id=${{ github.run_id }}", "extras=s3-cache"]
container:
image: px4io/px4-dev:v1.17.0-beta1
build:
runs-on: ubuntu-latest
steps:
- uses: runs-on/action@v2
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Configure Git Safe Directory
run: git config --system --add safe.directory '*'
- name: Restore Compiler Cache
id: cc_restore
uses: actions/cache/restore@v4
- name: Testing (clang-tidy)
uses: addnab/docker-run-action@v3
with:
path: ~/.ccache
key: ccache-clang-tidy-${{ github.head_ref || github.ref_name }}
restore-keys: |
ccache-clang-tidy-${{ github.head_ref || github.ref_name }}-
ccache-clang-tidy-main-
ccache-clang-tidy-
- name: Configure Compiler Cache
run: |
mkdir -p ~/.ccache
echo "base_dir = ${GITHUB_WORKSPACE}" > ~/.ccache/ccache.conf
echo "compression = true" >> ~/.ccache/ccache.conf
echo "compression_level = 6" >> ~/.ccache/ccache.conf
echo "max_size = 120M" >> ~/.ccache/ccache.conf
echo "hash_dir = false" >> ~/.ccache/ccache.conf
echo "compiler_check = content" >> ~/.ccache/ccache.conf
ccache -s
ccache -z
- name: Run Clang-Tidy Analysis
run: make -j16 clang-tidy
- name: Compiler Cache Stats
if: always()
run: ccache -s
- name: Save Compiler Cache
if: always()
uses: actions/cache/save@v4
with:
path: ~/.ccache
key: ${{ steps.cc_restore.outputs.cache-primary-key }}
image: px4io/px4-dev-clang:2021-09-08
options: -v ${{ github.workspace }}:/workspace
run: |
cd /workspace
git config --global --add safe.directory /workspace
make clang-tidy
-148
View File
@@ -1,148 +0,0 @@
name: Commit Quality
on:
pull_request:
types: [opened, edited, synchronize, reopened]
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
jobs:
pr-title:
name: PR Title
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: Tools/ci
fetch-depth: 1
- name: Check PR title
id: check
run: |
python3 Tools/ci/check_pr_title.py "${{ github.event.pull_request.title }}" --markdown-file comment.md && rc=0 || rc=$?
echo "exit_code=$rc" >> "$GITHUB_OUTPUT"
- name: Post or clear comment
if: env.IS_FORK == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ steps.check.outputs.exit_code }}" != "0" ]; then
python3 Tools/ci/pr_comment.py --marker pr-title --pr "$PR_NUMBER" --result fail < comment.md
else
python3 Tools/ci/pr_comment.py --marker pr-title --pr "$PR_NUMBER" --result pass
fi
- name: Result
if: steps.check.outputs.exit_code != '0'
run: |
echo "::error::PR title does not follow conventional commits format. See the PR comment for details."
exit 1
commit-messages:
name: Commit Messages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: Tools/ci
fetch-depth: 1
- name: Check commit messages
id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits?per_page=100" \
| python3 Tools/ci/check_commit_messages.py --markdown-file comment.md && rc=0 || rc=$?
echo "exit_code=$rc" >> "$GITHUB_OUTPUT"
# Check for warnings (non-empty markdown on exit 0)
if [ "$rc" -eq 0 ] && [ -s comment.md ]; then
echo "has_warnings=true" >> "$GITHUB_OUTPUT"
else
echo "has_warnings=false" >> "$GITHUB_OUTPUT"
fi
- name: Post or clear comment
if: env.IS_FORK == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ steps.check.outputs.exit_code }}" != "0" ]; then
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result fail < comment.md
elif [ "${{ steps.check.outputs.has_warnings }}" == "true" ]; then
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result warn < comment.md
else
python3 Tools/ci/pr_comment.py --marker commit-msgs --pr "$PR_NUMBER" --result pass
fi
- name: Result
if: steps.check.outputs.exit_code != '0'
run: |
echo "::error::Commit message errors found. See the PR comment for details."
exit 1
pr-body:
name: PR Description
runs-on: ubuntu-latest
steps:
- name: Checkout
if: env.IS_FORK == 'false'
uses: actions/checkout@v4
with:
sparse-checkout: Tools/ci
fetch-depth: 1
- name: Check PR body
id: check
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
message=""
if [ -z "$PR_BODY" ]; then
message="PR description is empty. Please add a summary of the changes."
echo "::warning::PR description is empty."
else
cleaned=$(echo "$PR_BODY" | sed 's/<!--.*-->//g' | tr -d '[:space:]')
if [ -z "$cleaned" ]; then
message="PR description contains only template comments. Please fill in the details."
echo "::warning::PR description contains only template comments."
fi
fi
echo "message=$message" >> "$GITHUB_OUTPUT"
- name: Post or clear comment
if: env.IS_FORK == 'false'
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ -n "${{ steps.check.outputs.message }}" ]; then
printf '%s\n' \
"## PR Description (advisory)" \
"" \
"This is **not blocking**, but your PR description appears to be empty or incomplete." \
"" \
"${{ steps.check.outputs.message }}" \
"" \
"A good PR description helps reviewers understand what changed and why." \
"" \
"---" \
"*This comment will be automatically removed once the issue is resolved.*" \
| python3 Tools/ci/pr_comment.py --marker pr-body --pr "$PR_NUMBER" --result warn
else
python3 Tools/ci/pr_comment.py --marker pr-body --pr "$PR_NUMBER" --result pass
fi
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
shell: cmake -P {0}
run: |
string(TIMESTAMP current_date "%Y-%m-%d-%H;%M;%S" UTC)
file(APPEND "$ENV{GITHUB_OUTPUT}" "timestamp=${current_date}\n")
message("::set-output name=timestamp::${current_date}")
- name: ccache cache files
uses: actions/cache@v4
with:
+2 -2
View File
@@ -130,8 +130,8 @@ jobs:
load: false
push: ${{ startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_registry) }}
provenance: false
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
cache-from: type=gha,version=1,scope=${{ matrix.arch }}
cache-to: type=gha,version=1,mode=max,scope=${{ matrix.arch }}
deploy:
name: Deploy To Registry
-416
View File
@@ -1,416 +0,0 @@
# Docs - Orchestrator
#
# Trigger paths:
# push (main, release/**) → metadata-regen → build-site → deploy-aws
# pull_request → detect-changes → pr-metadata-regen → link-check → build-site (if docs/source changed)
# workflow_dispatch → metadata-regen → build-site → deploy-aws
#
# Container jobs (pr-metadata-regen, metadata-regen) run in px4-dev image and
# require safe.directory + fetch-depth: 0 for git operations.
name: Docs - Orchestrator
on:
push:
branches:
- "main"
- "release/**"
paths:
- "docs/**"
- "src/**"
- "msg/**"
- "ROMFS/**"
- "Tools/module_config/**"
- ".github/workflows/docs-orchestrator.yml"
pull_request:
paths:
- "docs/**"
- ".github/workflows/docs-orchestrator.yml"
workflow_dispatch:
concurrency:
group: docs-orchestrator-${{ github.ref }}
cancel-in-progress: true
jobs:
# =============================================================================
# Detect Changes (PR only)
# =============================================================================
detect-changes:
name: "T1: Detect Changes"
if: github.event_name == 'pull_request'
permissions:
contents: read
runs-on: ubuntu-latest
outputs:
source_changed: ${{ steps.changes.outputs.source }}
docs_changed: ${{ steps.changes.outputs.docs }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
source:
- 'src/**'
- 'msg/**'
- 'ROMFS/**'
- 'Tools/module_config/**'
docs:
- 'docs/**'
# =============================================================================
# PR Metadata Regen (conditional - only when PR touches source files)
# =============================================================================
pr-metadata-regen:
name: "T2: PR Metadata"
needs: [detect-changes]
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.source_changed == 'true'
permissions:
contents: read
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
container:
image: px4io/px4-dev:v1.17.0-beta1
steps:
- uses: runs-on/action@v1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Git ownership workaround
run: git config --system --add safe.directory '*'
- name: Cache Restore - ccache
id: cache-ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-docs-metadata-${{ github.sha }}
restore-keys: |
ccache-docs-metadata-
- name: Setup ccache
run: |
mkdir -p ~/.ccache
echo "max_size = 1G" > ~/.ccache/ccache.conf
- name: Build px4_sitl_default
run: |
make px4_sitl_default
env:
CCACHE_DIR: ~/.ccache
- name: Cache Save - ccache
uses: actions/cache/save@v4
if: always()
with:
path: ~/.ccache
key: ccache-docs-metadata-${{ github.sha }}
- name: Generate and sync metadata
run: Tools/ci/metadata_sync.sh --generate --sync parameters airframes modules msg_docs failsafe_web
env:
CCACHE_DIR: ~/.ccache
- name: Upload metadata artifact
uses: actions/upload-artifact@v4
with:
name: pr-metadata
path: docs/
retention-days: 1
# =============================================================================
# Push Metadata Regen (main/release branches)
# =============================================================================
metadata-regen:
name: "T2: Metadata Sync"
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
permissions:
contents: write
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
container:
image: px4io/px4-dev:v1.17.0-beta1
steps:
- uses: runs-on/action@v1
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
token: ${{ secrets.PX4BUILTBOT_PERSONAL_ACCESS_TOKEN }}
- name: Git ownership workaround
run: git config --system --add safe.directory '*'
- name: Cache Restore - ccache
id: cache-ccache
uses: actions/cache/restore@v4
with:
path: ~/.ccache
key: ccache-docs-metadata-${{ github.sha }}
restore-keys: |
ccache-docs-metadata-
- name: Setup ccache
run: |
mkdir -p ~/.ccache
echo "max_size = 1G" > ~/.ccache/ccache.conf
- name: Build px4_sitl_default
run: |
make px4_sitl_default
env:
CCACHE_DIR: ~/.ccache
- name: Cache Save - ccache
uses: actions/cache/save@v4
if: always()
with:
path: ~/.ccache
key: ccache-docs-metadata-${{ github.sha }}
- name: Generate and sync metadata
run: Tools/ci/metadata_sync.sh --generate --sync parameters airframes modules msg_docs failsafe_web
env:
CCACHE_DIR: ~/.ccache
- name: Install Node.js and Yarn
run: |
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
corepack enable
- name: Format markdown with Prettier
run: |
cd docs
yarn install --frozen-lockfile
yarn prettier --write "en/**/*.md"
- name: Commit and push changes
run: |
git config --global user.name "${{ secrets.PX4BUILDBOT_USER }}"
git config --global user.email "${{ secrets.PX4BUILDBOT_EMAIL }}"
git add docs/
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "docs: auto-sync metadata [skip ci]
Co-Authored-By: PX4 BuildBot <${{ secrets.PX4BUILDBOT_EMAIL }}>"
git push
fi
# =============================================================================
# Link Check
# =============================================================================
link-check:
name: "T2: Link Check"
needs: [detect-changes, pr-metadata-regen]
if: always() && (github.event_name == 'pull_request')
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Download metadata artifact
if: needs.pr-metadata-regen.result == 'success'
uses: actions/download-artifact@v4
with:
name: pr-metadata
path: docs/
- name: Get changed doc files
id: changed-files
uses: tj-actions/changed-files@v46.0.5
with:
json: true
write_output_files: true
output_dir: ./logs
base_sha: ${{ github.event.pull_request.base.sha }}
sha: ${{ github.event.pull_request.head.sha }}
files: |
docs/en/**/*.md
- name: Save changed files list
run: |
mv ./logs/all_changed_files.json ./logs/prFiles.json
echo "Changed files:"
cat ./logs/prFiles.json
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run filtered link checker (changed files)
run: |
npm -g install markdown_link_checker_sc@0.0.138
if [ "$(jq length ./logs/prFiles.json)" -gt 0 ]; then
markdown_link_checker_sc \
-r "$GITHUB_WORKSPACE" \
-d docs \
-e en \
-f ./logs/prFiles.json \
-i assets \
-u docs.px4.io/main/ \
> ./logs/filtered-link-check-results.md || true
fi
if [ ! -s ./logs/filtered-link-check-results.md ]; then
echo "No broken links found in changed files." > ./logs/filtered-link-check-results.md
fi
cat ./logs/filtered-link-check-results.md
- name: Run full link checker
run: |
markdown_link_checker_sc \
-r "$GITHUB_WORKSPACE" \
-d docs \
-e en \
-i assets \
-u docs.px4.io/main/ \
> ./logs/link-check-results.md || true
cat ./logs/link-check-results.md
- name: Post PR comment with link check results
if: github.event.pull_request.head.repo.full_name == github.repository
uses: marocchino/sticky-pull-request-comment@v2
with:
header: flaws
path: ./logs/filtered-link-check-results.md
- name: Upload link check results
uses: actions/upload-artifact@v4
with:
name: link-check-results
path: logs/
retention-days: 7
# =============================================================================
# Build Site
# =============================================================================
build-site:
name: "T3: Build Site"
needs: [detect-changes, pr-metadata-regen, metadata-regen, link-check]
if: >-
always() &&
(needs.metadata-regen.result == 'success' || needs.metadata-regen.result == 'skipped') &&
(needs.link-check.result == 'success' || needs.link-check.result == 'skipped') &&
(github.event_name != 'pull_request' || needs.detect-changes.outputs.docs_changed == 'true' || needs.detect-changes.outputs.source_changed == 'true')
permissions:
contents: read
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
outputs:
branchname: ${{ steps.set-branch.outputs.branchname }}
releaseversion: ${{ steps.set-version.outputs.releaseversion }}
steps:
- uses: runs-on/action@v1
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Download metadata artifact (PR)
if: github.event_name == 'pull_request' && needs.pr-metadata-regen.result == 'success'
uses: actions/download-artifact@v4
with:
name: pr-metadata
path: docs/
- id: set-branch
run: echo "branchname=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
- id: set-version
run: |
branch="${{ steps.set-branch.outputs.branchname }}"
if [[ "$branch" == "main" ]]; then
version="main"
elif [[ "$branch" =~ ^release/ ]]; then
version="v${branch#release/}"
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
version="main"
else
echo "::error::Unsupported branch for docs deploy: $branch (expected main or release/*)"
exit 1
fi
echo "releaseversion=$version" >> $GITHUB_OUTPUT
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: ./docs/yarn.lock
- name: Install dependencies
run: yarn install --frozen-lockfile --cwd ./docs
- name: Build with VitePress
working-directory: ./docs
env:
BRANCH_NAME: ${{ steps.set-version.outputs.releaseversion }}
run: |
npm run docs:build_ubuntu
touch .vitepress/dist/.nojekyll
npm run docs:sitemap
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: px4_docs_build
path: docs/.vitepress/dist/
retention-days: 1
# =============================================================================
# Deploy to AWS (push + workflow_dispatch)
# =============================================================================
deploy-aws:
name: "T4: Deploy"
needs: [metadata-regen, build-site]
if: >-
always() &&
needs.metadata-regen.result == 'success' &&
needs.build-site.result == 'success' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
permissions:
id-token: write
runs-on: ubuntu-latest
steps:
- name: Download Artifact
uses: actions/download-artifact@v4
with:
name: px4_docs_build
path: ~/_book
- name: Configure AWS from OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-west-2
- name: Sanity check AWS credentials
run: aws sts get-caller-identity
- name: Upload HTML with short cache
run: |
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build-site.outputs.releaseversion }}/ \
--delete \
--exclude "*" --include "*.html" \
--cache-control "public, max-age=60"
- name: Upload assets with long cache
run: |
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build-site.outputs.releaseversion }}/ \
--delete \
--exclude "*.html" \
--cache-control "public, max-age=86400, immutable"
+3 -3
View File
@@ -34,13 +34,13 @@ jobs:
upload_sources: false
upload_translations: false
download_translations: true
commit_message: 'docs(i18n): PX4 guide translations (Crowdin) - ${{ matrix.lc }}'
commit_message: New Crowdin translations - ${{ matrix.lc }}
localization_branch_name: l10n_crowdin_docs_translations_${{ matrix.lc }}
crowdin_branch_name: main
create_pull_request: true
pull_request_base_branch_name: 'main'
pull_request_title: 'docs(i18n): PX4 guide translations (Crowdin) - ${{ matrix.lc }}'
pull_request_body: 'docs(i18n): PX4 guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action) for ${{ matrix.lc }}'
pull_request_title: New PX4 guide translations (Crowdin) - ${{ matrix.lc }}
pull_request_body: 'New PX4 guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action) for ${{ matrix.lc }}'
pull_request_labels: 'Documentation 📑'
pull_request_reviewers: hamishwillee
download_language: ${{ matrix.lc }}
+116
View File
@@ -0,0 +1,116 @@
name: Docs - Deploy PX4 User Guide to AWS
on:
push:
branches:
- "main"
- "release/**"
paths:
- "docs/en/**"
- "docs/zh/**"
- "docs/uk/**"
- "docs/ko/**"
pull_request:
paths:
- "docs/en/**"
- "docs/zh/**"
- "docs/uk/**"
- "docs/ko/**"
workflow_dispatch:
permissions:
contents: read
actions: read
id-token: write # for AWS OIDC
concurrency:
group: docs-deploy
cancel-in-progress: false
jobs:
build:
runs-on: [runs-on,runner=4cpu-linux-x64,image=ubuntu24-full-x64,"run-id=${{ github.run_id }}",spot=false,extras=s3-cache]
outputs:
branchname: ${{ steps.set-branch.outputs.branchname }}
releaseversion: ${{ steps.set-version.outputs.releaseversion }}
steps:
- uses: runs-on/action@v1
- name: Checkout
uses: actions/checkout@v4
- id: set-branch
run: echo "branchname=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
- id: set-version
run: |
branch="${{ steps.set-branch.outputs.branchname }}"
if [[ "$branch" == "main" ]]; then
version="main"
else
version="v${branch#release/}"
fi
echo "releaseversion=$version" >> $GITHUB_OUTPUT
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: ./docs/yarn.lock
- name: Install dependencies
run: yarn install --frozen-lockfile --cwd ./docs
- name: Build with VitePress
working-directory: ./docs
env:
BRANCH_NAME: ${{ steps.set-version.outputs.releaseversion }}
run: |
npm run docs:build_ubuntu
touch .vitepress/dist/.nojekyll
npm run docs:sitemap
- name: Upload artifact
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged) || github.event_name == 'workflow_dispatch' }}
uses: actions/upload-artifact@v4
with:
name: px4_docs_build
path: docs/.vitepress/dist/
retention-days: 1
deploy:
if: ${{ github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.merged) || github.event_name == 'workflow_dispatch' }}
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Artifact
uses: actions/download-artifact@v4
with:
name: px4_docs_build
path: ~/_book
- name: Configure AWS from OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-west-2
- name: Sanity check AWS credentials
run: aws sts get-caller-identity
- name: Upload HTML with short cache
run: |
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build.outputs.releaseversion }}/ \
--delete \
--exclude "*" --include "*.html" \
--cache-control "public, max-age=60"
- name: Upload assets with long cache
run: |
aws s3 sync ~/_book/ s3://px4-docs/${{ needs.build.outputs.releaseversion }}/ \
--delete \
--exclude "*.html" \
--cache-control "public, max-age=86400, immutable"
+85
View File
@@ -0,0 +1,85 @@
name: Docs - Check for flaws in PX4 Guide Source
# So far:
# Modifications of translations files
# Broken internal links
on:
pull_request_target:
types: [opened, edited, synchronize]
paths:
- 'docs/en/**'
jobs:
check_flaws:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Create logs directory
run: |
mkdir logs
- name: Get changed english doc files
id: get_changed_markdown_english
uses: tj-actions/changed-files@v46.0.5
with:
json: true
base_sha: "${{ github.event.pull_request.base.sha }}"
sha: "${{ github.event.pull_request.head.sha }}"
# Below are used to output files to a directory. May use in flaw checker.
# write_output_files: true
# output_dir: "./logs"
files: |
docs/en/**/*.md
- name: Save JSON file containing files to link check
run: |
echo "$ALL_CHANGED_FILES"
# echo "$ALL_CHANGED_FILES" > ./logs/prFiles.json
echo "$ALL_CHANGED_FILES" | sed 's/\\//g' | jq '.' > ./logs/prFiles.json
env:
ALL_CHANGED_FILES: ${{ steps.get_changed_markdown_english.outputs.all_changed_files }}
- name: Run link checker
id: link-check
run: |
npm -g install markdown_link_checker_sc@0.0.138
markdown_link_checker_sc \
-r "$GITHUB_WORKSPACE" \
-d docs \
-e en \
-f ./logs/prFiles.json \
-i assets \
-u docs.px4.io/main/ \
> ./logs/errorsFilteredByPrPages.md
mkdir -p ./pr
cp ./logs/errorsFilteredByPrPages.md ./pr/errorsFilteredByPrPages.md
- name: Read errorsFilteredByPrPages.md file
id: read-errors-by-page
uses: juliangruber/read-file-action@v1
with:
path: ./logs/errorsFilteredByPrPages.md
- name: Echo Errors by Page
run: echo "$ERRORS"
env:
ERRORS: ${{ steps.read-errors-by-page.outputs.content }}
- name: Save PR number
run: echo "$PR_NUMBER" > ./pr/pr_number
env:
PR_NUMBER: ${{ github.event.number }}
- uses: actions/upload-artifact@v4
with:
name: pr_number
path: pr/
+111
View File
@@ -0,0 +1,111 @@
name: Docs - Comment Workflow
on:
workflow_run:
workflows: ["Docs - Check for flaws in PX4 Guide Source"]
types:
- completed
jobs:
comment:
permissions:
pull-requests: write # for marocchino/sticky-pull-request-comment
name: Comments
runs-on: ubuntu-latest
steps:
- name: 'Download PR artifact'
uses: actions/github-script@v6
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr_number"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
- name: 'Unzip artifact'
run: unzip pr_number.zip
# Doesn't work across workflows
#- name: Get artifacts from flaw checker workflow
# uses: actions/download-artifact@v3
# with:
# name: logs_and_errors
# #path: ./logs
- name: Read errorsFilteredByPrPages.md file
id: read-errors-by-page
uses: juliangruber/read-file-action@v1
with:
path: ./errorsFilteredByPrPages.md
- name: Read PR number
id: read-error-pr-number
uses: juliangruber/read-file-action@v1
with:
path: ./pr_number
- name: File detail info
run: |
echo "$ERRORS"
echo "$PRNUM"
env:
ERRORS: ${{ steps.read-errors-by-page.outputs.content }}
PRNUM: ${{ steps.read-error-pr-number.outputs.content }}
- name: Create or update comment
id: comment_to_pr
uses: marocchino/sticky-pull-request-comment@v2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
recreate: true
number: ${{ steps.read-error-pr-number.outputs.content }}
header: flaws
message: ${{ steps.read-errors-by-page.outputs.content || 'No flaws found' }}
#- name: Dump GitHub context
# env:
# GITHUB_CONTEXT: ${{ toJSON(github) }}
# run: echo "$GITHUB_CONTEXT"
# Would like to do this, but it doesn't work (for me).
# Moving to time-based, or triggering on workflow
#- name: Wait for artifacts upload to succeed
# uses: lewagon/wait-on-check-action@v1.3.1
# with:
# ref: ${{ github.ref }}
# check-name: 'Archive production artifacts'
# repo-token: ${{ secrets.GITHUB_TOKEN }}
# wait-interval: 80
# Not needed for now - trying to trigger off the workflow
#- name: Sleep for 80 seconds
# run: sleep 80s
# shell: bash
#- name: Find Comment
# uses: peter-evans/find-comment@v2
# id: fc
# with:
# issue-number: ${{ steps.read-error-pr-number.outputs.content }}
# comment-author: 'github-actions[bot]'
# body-includes: Flaws (may be none)
#- name: Create or update comment
# uses: peter-evans/create-or-update-comment@v3
# with:
# comment-id: ${{ steps.fc.outputs.comment-id }}
# issue-number: ${{ steps.read-error-pr-number.outputs.content }}
# body: |
# Flaws (may be none)
# ${{ steps.read-errors-by-page.outputs.content }}
# edit-mode: replace
@@ -15,21 +15,21 @@ concurrency:
jobs:
unit_tests:
runs-on: ubuntu-latest
container:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: main test
- name: main test
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
options: -v ${{ github.workspace }}:/workspace
run: |
cd "$GITHUB_WORKSPACE"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
cd /workspace
git config --global --add safe.directory /workspace
make tests TESTFILTER=EKF
- name: Check if there is a functional change
run: git diff --exit-code
working-directory: src/modules/ekf2/test/change_indication
- name: Check if there is a functional change
run: git diff --exit-code
working-directory: src/modules/ekf2/test/change_indication
@@ -8,47 +8,40 @@ on:
jobs:
unit_tests:
runs-on: ubuntu-latest
container:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
env:
GIT_COMMITTER_EMAIL: bot@px4.io
GIT_COMMITTER_NAME: PX4BuildBot
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: main test
- name: main test
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
options: -v ${{ github.workspace }}:/workspace
run: |
cd "$GITHUB_WORKSPACE"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
cd /workspace
git config --global --add safe.directory /workspace
make tests TESTFILTER=EKF
- name: Check if there exists diff and save result in variable
id: diff-check
working-directory: src/modules/ekf2/test/change_indication
run: |
if git diff --quiet; then
echo "CHANGE_INDICATED=false" >> $GITHUB_OUTPUT
else
echo "CHANGE_INDICATED=true" >> $GITHUB_OUTPUT
fi
- name: Check if there exists diff and save result in variable
id: diff-check
run: echo "CHANGE_INDICATED=$(git diff --exit-code --output=/dev/null || echo $?)" >> $GITHUB_OUTPUT
working-directory: src/modules/ekf2/test/change_indication
- name: auto-commit any changes to change indication
if: steps.diff-check.outputs.CHANGE_INDICATED == 'true'
uses: stefanzweifel/git-auto-commit-action@v4
with:
file_pattern: 'src/modules/ekf2/test/change_indication/*.csv'
commit_user_name: ${{ env.GIT_COMMITTER_NAME }}
commit_user_email: ${{ env.GIT_COMMITTER_EMAIL }}
commit_message: |
[AUTO COMMIT] update change indication
- name: auto-commit any changes to change indication
uses: stefanzweifel/git-auto-commit-action@v4
with:
file_pattern: 'src/modules/ekf2/test/change_indication/*.csv'
commit_user_name: ${GIT_COMMITTER_NAME}
commit_user_email: ${GIT_COMMITTER_EMAIL}
commit_message: |
'[AUTO COMMIT] update change indication'
See .github/workflows/ekf_update_change_indicator.yml for more details
See .github/workflopws/ekf_update_change_indicator.yml for more details
- name: if there is a functional change, fail check
if: steps.diff-check.outputs.CHANGE_INDICATED == 'true'
run: exit 1
- name: if there is a functional change, fail check
if: ${{ steps.diff-check.outputs.CHANGE_INDICATED }}
run: exit 1
+16 -18
View File
@@ -19,27 +19,25 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
config:
- {vehicle: "iris", mission: "MC_mission_box"}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build SITL and Run Tests (inside old ROS container)
- name: Build SITL and Run Tests
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev-ros-melodic:2021-09-08
options: -v ${{ github.workspace }}:/workspace
run: |
docker run --rm \
-v "${GITHUB_WORKSPACE}:/workspace" \
-w /workspace \
px4io/px4-dev-ros-melodic:2021-09-08 \
bash -c '
git config --global --add safe.directory /workspace
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 \
vehicle:=iris
'
cd /workspace
git config --global --add safe.directory /workspace
make px4_sitl_default
make px4_sitl_default sitl_gazebo-classic
./test/rostest_px4_run.sh mavros_posix_test_mission.test mission:=${{matrix.config.mission}} vehicle:=${{matrix.config.vehicle}}
+18 -17
View File
@@ -19,26 +19,27 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
strategy:
fail-fast: false
matrix:
config:
- {test_file: "mavros_posix_tests_offboard_posctl.test", vehicle: "iris"}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build SITL and Run Tests (inside old ROS container)
- name: Build PX4 and Run Tests
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev-ros-melodic:2021-09-08
options: -v ${{ github.workspace }}:/workspace
run: |
docker run --rm \
-v "${GITHUB_WORKSPACE}:/workspace" \
-w /workspace \
px4io/px4-dev-ros-melodic:2021-09-08 \
bash -c '
git config --global --add safe.directory /workspace
make px4_sitl_default
make px4_sitl_default sitl_gazebo-classic
./test/rostest_px4_run.sh \
mavros_posix_tests_offboard_posctl.test \
vehicle:=iris
'
cd /workspace
git config --global --add safe.directory /workspace
make px4_sitl_default
make px4_sitl_default sitl_gazebo-classic
./test/rostest_px4_run.sh ${{matrix.config.test_file}} vehicle:=${{matrix.config.vehicle}}
+14 -15
View File
@@ -19,28 +19,27 @@ concurrency:
jobs:
build:
runs-on: ubuntu-latest
container:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
strategy:
matrix:
config:
- px4_fmu-v5_default
config: [
px4_fmu-v5_default,
]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build PX4 and Run Test [${{ matrix.config }}]
- name: Build PX4 and Run Test [${{ matrix.config }}]
uses: addnab/docker-run-action@v3
with:
image: px4io/px4-dev:v1.16.0-rc1-258-g0369abd556
options: -v ${{ github.workspace }}:/workspace
run: |
cd "$GITHUB_WORKSPACE"
git config --global --add safe.directory "$GITHUB_WORKSPACE"
export PX4_EXTRA_NUTTX_CONFIG='CONFIG_NSH_LOGIN_PASSWORD="test";CONFIG_NSH_CONSOLE_LOGIN=y'
cd /workspace
git config --global --add safe.directory /workspace
export PX4_EXTRA_NUTTX_CONFIG="CONFIG_NSH_LOGIN_PASSWORD=\"test\";CONFIG_NSH_CONSOLE_LOGIN=y"
echo "PX4_EXTRA_NUTTX_CONFIG: $PX4_EXTRA_NUTTX_CONFIG"
make ${{ matrix.config }} nuttx_context
echo "Check that the config option is set"
grep CONFIG_NSH_LOGIN_PASSWORD build/${{ matrix.config }}/NuttX/nuttx/.config
+2 -4
View File
@@ -33,10 +33,8 @@ jobs:
matrix:
config:
- {model: "iris", latitude: "59.617693", longitude: "-151.145316", altitude: "48", build_type: "RelWithDebInfo" } # Alaska
# VTOL/tailsitter disabled: persistent flaky CI failures (timeouts, erratic
# transitions). Re-enable once the test infrastructure is stabilized.
# - {model: "tailsitter" , latitude: "29.660316", longitude: "-82.316658", altitude: "30", build_type: "RelWithDebInfo" } # Florida
# - {model: "standard_vtol", latitude: "47.397742", longitude: "8.545594", altitude: "488", build_type: "Coverage" } # Zurich
- {model: "tailsitter" , latitude: "29.660316", longitude: "-82.316658", altitude: "30", build_type: "RelWithDebInfo" } # Florida
- {model: "standard_vtol", latitude: "47.397742", longitude: "8.545594", altitude: "488", build_type: "Coverage" } # Zurich
steps:
- uses: actions/checkout@v4
-3
View File
@@ -109,6 +109,3 @@ src/systemcmds/topic_listener/listener_generated.cpp
# colcon
log/
keys/
# metadata
_emscripten_sdk/
-6
View File
@@ -109,9 +109,3 @@
[submodule "src/lib/rl_tools/rl_tools"]
path = src/lib/rl_tools/rl_tools
url = https://github.com/rl-tools/rl-tools.git
[submodule "libmodal-json"]
path = boards/modalai/voxl2/src/lib/mpa/libmodal-json
url = https://gitlab.com/voxl-public/voxl-sdk/core-libs/libmodal-json.git
[submodule "libmodal-pipe"]
path = boards/modalai/voxl2/src/lib/mpa/libmodal-pipe
url = https://gitlab.com/voxl-public/voxl-sdk/core-libs/libmodal-pipe.git
-5
View File
@@ -336,11 +336,6 @@ CONFIG:
buildType: MinSizeRel
settings:
CONFIG: cuav_x25-evo_default
cuav_x25-super_default:
short: cuav_x25-super
buildType: MinSizeRel
settings:
CONFIG: cuav_x25-super_default
cubepilot_cubeorange_test:
short: cubepilot_cubeorange
buildType: MinSizeRel
-27
View File
@@ -1,27 +0,0 @@
cff-version: 1.2.0
title: "PX4 Autopilot"
message: "If you use PX4 in your research, please cite it using this metadata."
type: software
authors:
- family-names: Meier
given-names: Lorenz
- name: "The PX4 Contributors"
repository-code: "https://github.com/PX4/PX4-Autopilot"
url: "https://px4.io"
abstract: >-
PX4 is an open-source autopilot stack for drones and
unmanned vehicles. It supports multirotors, fixed-wing,
VTOL, rovers, and many more platforms. PX4 runs on both
RTOS and POSIX-compatible operating systems.
keywords:
- autopilot
- drone
- uav
- flight-controller
- robotics
- ros2
license: BSD-3-Clause
identifiers:
- type: doi
value: "10.5281/zenodo.595432"
description: "Zenodo concept DOI (resolves to latest version)"
+21 -147
View File
@@ -1,170 +1,44 @@
# Contributing to PX4-Autopilot
# Contributing to PX4 Firmware
We follow the [GitHub flow](https://guides.github.com/introduction/flow/) development model.
We follow the [Github flow](https://guides.github.com/introduction/flow/) development model.
## Fork the project, then clone your repo
### Fork the project, then clone your repo
First [fork and clone](https://help.github.com/articles/fork-a-repo) the project.
First [fork and clone](https://help.github.com/articles/fork-a-repo) the project project.
## Create a feature branch
### Create a feature branch
Always branch off `main` for new features.
*Always* branch off main for new features.
```
git checkout -b mydescriptivebranchname
```
## Edit and build the code
### Edit and build the code
The [developer guide](https://docs.px4.io/main/en/development/development.html) explains how to set up the development environment on Mac OS, Linux or Windows.
The [developer guide](https://docs.px4.io/main/en/development/development.html) explains how to set up the development environment on Mac OS, Linux or Windows. Please take note of our [coding style](https://docs.px4.io/main/en/contribute/code.html) when editing files.
### Coding standards
### Commit your changes
All C/C++ code must follow the [PX4 coding style](https://docs.px4.io/main/en/contribute/code.html). Formatting is enforced by [astyle](http://astyle.sourceforge.net/) in CI (`make check_format`). Code quality checks run via [clang-tidy](https://clang.llvm.org/extra/clang-tidy/). Pull requests that fail either check will not be merged.
Always write descriptive commit messages and add a fixes or relates note to them with an [issue number](https://github.com/px4/Firmware/issues) (Github will link these then conveniently)
Python code is checked with [mypy](https://mypy-lang.org/) and [flake8](https://flake8.pycqa.org/).
## Commit message convention
PX4 uses [conventional commits](https://www.conventionalcommits.org/) for all commit messages and PR titles.
### Format
**Example:**
```
type(scope): short description of the change
Change how the attitude controller works
- Fixes rate feed forward
- Allows a local body rate override
Fixes issue #123
```
| Part | Rule |
|------|------|
| **type** | Category of change (see types table below) |
| **scope** | The module, driver, board, or area of PX4 affected |
| **`!`** (optional) | Append before `:` to mark a breaking change |
| **description** | What the change does, at least 5 characters, written in imperative form |
### Test your changes
### Types
Since we care about safety, we will regularly ask you for test results. Best is to do a test flight (or bench test where it applies) and upload the logfile from it (on the microSD card in the logs directory) to Google Drive or Dropbox and share the link.
| Type | Description |
|------|-------------|
| `feat` | A new feature |
| `fix` | A bug fix |
| `docs` | Documentation only changes |
| `style` | Formatting, whitespace, no code change |
| `refactor` | Code change that neither fixes a bug nor adds a feature |
| `perf` | Performance improvement |
| `test` | Adding or correcting tests |
| `build` | Build system or external dependencies |
| `ci` | CI configuration files and scripts |
| `chore` | Other changes that don't modify src or test files |
| `revert` | Reverts a previous commit |
### Push your changes
### Scopes
The scope identifies which part of PX4 is affected. Common scopes:
| Scope | Area |
|-------|------|
| `ekf2` | Extended Kalman Filter (state estimation) |
| `mavlink` | MAVLink messaging protocol |
| `commander` | Commander and mode management |
| `navigator` | Mission, RTL, Land, and other navigation modes |
| `sensors` | Sensor drivers and processing |
| `drivers` | Hardware drivers |
| `boards/px4_fmu-v6x` | Board-specific changes (use the board name) |
| `mc_att_control` | Multicopter attitude control |
| `mc_pos_control` | Multicopter position control |
| `fw_att_control` | Fixed-wing attitude control |
| `vtol` | VTOL-specific logic |
| `actuators` | Mixer and actuator output |
| `battery` | Battery monitoring and estimation |
| `logger` | On-board logging |
| `param` | Parameter system |
| `simulation` | SITL, Gazebo, SIH |
| `ci` | Continuous integration and workflows |
| `docs` | Documentation |
| `build` | CMake, toolchain, build system |
| `uorb` | Inter-module messaging |
For changes spanning multiple subsystems, use the primary one affected. Look at the directory path of the files you changed to find the right scope: `src/modules/ekf2/` uses `ekf2`, `src/drivers/imu/` uses `drivers/imu`, `.github/workflows/` uses `ci`.
### Breaking changes
Append `!` before the colon to indicate a breaking change:
```
feat(ekf2)!: remove deprecated height fusion API
```
### Good commit messages
```
feat(ekf2): add height fusion timeout
fix(mavlink): correct BATTERY_STATUS_V2 parsing
refactor(navigator): simplify RTL altitude logic
ci(workflows): migrate to reusable workflows
docs(ekf2): update tuning guide
feat(boards/px4_fmu-v6x)!: remove deprecated driver API
perf(mc_rate_control): reduce loop latency
```
### Commits to avoid
These will be flagged by CI and should be squashed or reworded before merging:
```
fix # too vague, no type or scope
update # too vague, no type or scope
ekf2: fix something # missing type prefix
apply suggestions from code review # squash into parent commit
do make format # squash into parent commit
WIP: trying something # not ready for main
oops # not descriptive
```
### PR titles
The PR title follows the same `type(scope): description` format. This is enforced by CI and is especially important because the PR title becomes the commit message when a PR is squash-merged.
### Merge policy
Commits should be atomic and independently revertable. Squash at reviewer discretion for obvious cases (multiple WIP commits, messy review-response history). When your commits are clean and logical, they will be preserved as individual commits on `main`.
### Cleaning up commits
If CI flags your commit messages, you can fix them with an interactive rebase:
```bash
# Squash all commits into one:
git rebase -i HEAD~N # replace N with the number of commits
# mark all commits except the first as 'squash' or 'fixup'
# reword the remaining commit to follow the format
git push --force-with-lease
# Or reword specific commits:
git rebase -i HEAD~N
# mark the bad commits as 'reword'
git push --force-with-lease
```
## Test your changes
PX4 is safety-critical software. All contributions must include adequate testing where practical:
- **New features** must include unit tests and/or integration tests that exercise the new functionality, where practical. Hardware-dependent changes that cannot be tested in SITL should include bench test or flight test evidence.
- **Bug fixes** must include a regression test where practical. When automated testing is not feasible (hardware-specific issues, race conditions, etc.), provide a link to a flight log demonstrating the fix and the reproduction steps for the original bug.
- **Reviewers** will verify that tests or test evidence exist before approving a pull request.
### Types of tests
| Test type | When to use | How to run |
|-----------|-------------|------------|
| **Unit tests** (gtest) | Module-level logic, math, parsing | `make tests` |
| **SITL integration tests** (MAVSDK) | Flight behavior, failsafes, missions | `test/mavsdk_tests/` |
| **Bench tests / flight logs** | Hardware-dependent changes | Upload logs to [Flight Review](https://logs.px4.io) |
Since we care about safety, we will regularly ask you for test results. Best is to do a test flight (or bench test where it applies) and upload the log file from it (on the microSD card in the logs directory) to Google Drive or Dropbox and share the link.
## Push your changes
Push changes to your repo and send a [pull request](https://github.com/PX4/PX4-Autopilot/compare/).
Push changes to your repo and send a [pull request](https://github.com/PX4/Firmware/compare/).
Make sure to provide some testing feedback and if possible the link to a flight log file. Upload flight log files to [Flight Review](http://logs.px4.io) and link the resulting report.
+3 -27
View File
@@ -332,7 +332,6 @@ bootloaders_update: \
cuav_7-nano_bootloader \
cuav_fmu-v6x_bootloader \
cuav_x25-evo_bootloader \
cuav_x25-super_bootloader \
cubepilot_cubeorange_bootloader \
cubepilot_cubeorangeplus_bootloader \
hkust_nxt-dual_bootloader \
@@ -413,7 +412,7 @@ tests:
$(call cmake-build,px4_sitl_test)
# work around lcov bug #316; remove once lcov is fixed (see https://github.com/linux-test-project/lcov/issues/316)
LCOBUG = --ignore-errors mismatch,negative
LCOBUG = --ignore-errors mismatch
tests_coverage:
@$(MAKE) clean
@$(MAKE) --no-print-directory tests PX4_CMAKE_BUILD_TYPE=Coverage
@@ -493,29 +492,13 @@ px4_sitl_default-clang:
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && cmake "$(SRC_DIR)" $(CMAKE_ARGS) -G"$(PX4_CMAKE_GENERATOR)" -DCONFIG=px4_sitl_default -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
@$(PX4_MAKE) -C "$(SRC_DIR)"/build/px4_sitl_default-clang
# Paths to exclude from clang-tidy (auto-generated from .gitmodules + manual additions):
# - All submodules (external code we consume, not edit)
# - Test code (allowed looser style)
# - Example code (educational, not production)
# - Vendored third-party code (e.g., CMSIS_5)
# - NuttX-only drivers excluded at CMake level (mcp_common); I2C-dependent libs excluded here (smbus)
# - GPIO excluded here (NuttX platform headers)
# - Emscripten failsafe web build: source path + Unity build path (failsafe_test.dir)
# because CMake Unity Builds merge sources into a generated .cxx under build/
#
# To add manual exclusions, append to CLANG_TIDY_EXCLUDE_EXTRA below.
# Submodules are automatically excluded - no action needed when adding new ones.
CLANG_TIDY_SUBMODULES := $(shell git config --file .gitmodules --get-regexp path | awk '{print $$2}' | tr '\n' '|' | sed 's/|$$//')
CLANG_TIDY_EXCLUDE_EXTRA := src/systemcmds/tests|src/examples|src/modules/gyro_fft/CMSIS_5|src/lib/drivers/smbus|src/drivers/gpio|src/modules/commander/failsafe/emscripten|failsafe_test\.dir|\.pb\.cc
CLANG_TIDY_EXCLUDE := $(CLANG_TIDY_SUBMODULES)|$(CLANG_TIDY_EXCLUDE_EXTRA)
clang-tidy: px4_sitl_default-clang
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -exclude="$(CLANG_TIDY_EXCLUDE)" -p .
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -p .
# to automatically fix a single check at a time, eg modernize-redundant-void-arg
# % run-clang-tidy-4.0.py -fix -j4 -checks=-\*,modernize-redundant-void-arg -p .
clang-tidy-fix: px4_sitl_default-clang
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -exclude="$(CLANG_TIDY_EXCLUDE)" -fix -p .
@cd "$(SRC_DIR)"/build/px4_sitl_default-clang && "$(SRC_DIR)"/Tools/run-clang-tidy.py -header-filter=".*\.hpp" -j$(j_clang_tidy) -fix -p .
# TODO: Fix cppcheck errors then try --enable=warning,performance,portability,style,unusedFunction or --enable=all
cppcheck: px4_sitl_default
@@ -615,10 +598,3 @@ failsafe_web:
run_failsafe_web_server: failsafe_web
@cd build/px4_sitl_default_failsafe_web && \
python3 -m http.server
# Generate reference documentation for uORB messages
.PHONY: msg_docs
msg_docs:
$(call colorecho,'Generating uORB message reference docs')
@mkdir -p build/msg_docs
@./Tools/msg/generate_msg_docs.py -d build/msg_docs
+37 -85
View File
@@ -1,110 +1,62 @@
<p align="center">
<a href="https://px4.io">
<img src="docs/assets/site/px4_logo.svg" alt="PX4 Autopilot" width="240">
</a>
</p>
# PX4 Drone Autopilot
<p align="center">
<em>The autopilot stack the industry builds on.</em>
</p>
[![Releases](https://img.shields.io/github/release/PX4/PX4-Autopilot.svg)](https://github.com/PX4/PX4-Autopilot/releases) [![DOI](https://zenodo.org/badge/22634/PX4/PX4-Autopilot.svg)](https://zenodo.org/badge/latestdoi/22634/PX4/PX4-Autopilot)
<p align="center">
<a href="https://github.com/PX4/PX4-Autopilot/releases"><img src="https://img.shields.io/github/release/PX4/PX4-Autopilot.svg" alt="Releases"></a>
<a href="https://www.bestpractices.dev/projects/6520"><img src="https://www.bestpractices.dev/projects/6520/badge" alt="OpenSSF Best Practices"></a>
<a href="https://zenodo.org/badge/latestdoi/22634/PX4/PX4-Autopilot"><img src="https://zenodo.org/badge/22634/PX4/PX4-Autopilot.svg" alt="DOI"></a>
<a href="https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml"><img src="https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml/badge.svg?branch=main" alt="Build Targets"></a>
<a href="https://discord.gg/dronecode"><img src="https://discordapp.com/api/guilds/1022170275984457759/widget.png?style=shield" alt="Discord"></a>
</p>
[![Build Targets](https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml/badge.svg?branch=main)](https://github.com/PX4/PX4-Autopilot/actions/workflows/build_all_targets.yml) [![SITL Tests](https://github.com/PX4/PX4-Autopilot/workflows/SITL%20Tests/badge.svg?branch=master)](https://github.com/PX4/PX4-Autopilot/actions?query=workflow%3A%22SITL+Tests%22)
---
[![Discord Shield](https://discordapp.com/api/guilds/1022170275984457759/widget.png?style=shield)](https://discord.gg/dronecode)
## About
This repository holds the [PX4](http://px4.io) flight control solution for drones, with the main applications located in the [src/modules](https://github.com/PX4/PX4-Autopilot/tree/main/src/modules) directory. It also contains the PX4 Drone Middleware Platform, which provides drivers and middleware to run drones.
PX4 is an open-source autopilot stack for drones and unmanned vehicles. It supports multirotors, fixed-wing, VTOL, rovers, and many more experimental platforms from racing quads to industrial survey aircraft. It runs on [NuttX](https://nuttx.apache.org/), Linux, and macOS. Licensed under [BSD 3-Clause](LICENSE).
PX4 is highly portable, OS-independent and supports Linux, NuttX and MacOS out of the box.
## Why PX4
* Official Website: http://px4.io (License: BSD 3-clause, [LICENSE](https://github.com/PX4/PX4-Autopilot/blob/main/LICENSE))
* [Supported airframes](https://docs.px4.io/main/en/airframes/airframe_reference.html) ([portfolio](https://px4.io/ecosystem/commercial-systems/)):
* [Multicopters](https://docs.px4.io/main/en/frames_multicopter/)
* [Fixed wing](https://docs.px4.io/main/en/frames_plane/)
* [VTOL](https://docs.px4.io/main/en/frames_vtol/)
* [Autogyro](https://docs.px4.io/main/en/frames_autogyro/)
* [Rover](https://docs.px4.io/main/en/frames_rover/)
* many more experimental types (Blimps, Boats, Submarines, High Altitude Balloons, Spacecraft, etc)
* Releases: [Downloads](https://github.com/PX4/PX4-Autopilot/releases)
**Modular architecture.** PX4 is built around [uORB](https://docs.px4.io/main/en/middleware/uorb.html), a [DDS](https://docs.px4.io/main/en/middleware/uxrce_dds.html)-compatible publish/subscribe middleware. Modules are fully parallelized and thread safe. You can build custom configurations and trim what you don't need.
## Releases
**Wide hardware support.** PX4 runs on a wide range of [autopilot boards](https://docs.px4.io/main/en/flight_controller/) and supports an extensive set of sensors, telemetry radios, and actuators through the [Pixhawk](https://pixhawk.org/) ecosystem.
Release notes and supporting information for PX4 releases can be found on the [Developer Guide](https://docs.px4.io/main/en/releases/).
**Developer friendly.** First-class support for [MAVLink](https://mavlink.io/) and [DDS / ROS 2](https://docs.px4.io/main/en/ros2/) integration. Comprehensive [SITL simulation](https://docs.px4.io/main/en/simulation/), hardware-in-the-loop testing, and [log analysis](https://docs.px4.io/main/en/log/flight_log_analysis.html) tools. An active developer community on [Discord](https://discord.gg/dronecode) and the [weekly dev call](https://docs.px4.io/main/en/contribute/).
## Building a PX4 based drone, rover, boat or robot
**Vendor neutral governance.** PX4 is hosted under the [Dronecode Foundation](https://www.dronecode.org/), part of the Linux Foundation. Business-friendly BSD-3 license. No single vendor controls the roadmap.
The [PX4 User Guide](https://docs.px4.io/main/en/) explains how to assemble [supported vehicles](https://docs.px4.io/main/en/airframes/airframe_reference.html) and fly drones with PX4. See the [forum and chat](https://docs.px4.io/main/en/#getting-help) if you need help!
## Supported Vehicles
<table>
<tr>
<td align="center">
<a href="https://docs.px4.io/main/en/frames_multicopter/">
<img src="docs/assets/airframes/types/QuadRotorX.svg" width="50" alt="Multicopter"><br>
<sub>Multicopter</sub>
</a>
</td>
<td align="center">
<a href="https://docs.px4.io/main/en/frames_plane/">
<img src="docs/assets/airframes/types/Plane.svg" width="50" alt="Fixed Wing"><br>
<sub>Fixed Wing</sub>
</a>
</td>
<td align="center">
<a href="https://docs.px4.io/main/en/frames_vtol/">
<img src="docs/assets/airframes/types/VTOLPlane.svg" width="50" alt="VTOL"><br>
<sub>VTOL</sub>
</a>
</td>
<td align="center">
<a href="https://docs.px4.io/main/en/frames_rover/">
<img src="docs/assets/airframes/types/Rover.svg" width="50" alt="Rover"><br>
<sub>Rover</sub>
</a>
</td>
</tr>
</table>
## Changing Code and Contributing
<sub>…and many more: helicopters, autogyros, airships, submarines, boats, and other experimental platforms. These frames have basic support but are not part of the regular flight-test program. See the <a href="https://docs.px4.io/main/en/airframes/airframe_reference.html">full airframe reference</a>.</sub>
This [Developer Guide](https://docs.px4.io/main/en/development/development.html) is for software developers who want to modify the flight stack and middleware (e.g. to add new flight modes), hardware integrators who want to support new flight controller boards and peripherals, and anyone who wants to get PX4 working on a new (unsupported) airframe/vehicle.
## Quick Start
Developers should read the [Guide for Contributions](https://docs.px4.io/main/en/contribute/).
See the [forum and chat](https://docs.px4.io/main/en/#getting-help) if you need help!
```bash
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
cd PX4-Autopilot
make px4_sitl
```
> [!NOTE]
> See the [Development Guide](https://docs.px4.io/main/en/development/development.html) for toolchain setup and build options.
## Weekly Dev Call
## Documentation & Resources
The PX4 Dev Team syncs up on a [weekly dev call](https://docs.px4.io/main/en/contribute/).
| Resource | Description |
| --- | --- |
| [User Guide](https://docs.px4.io/main/en/) | Build, configure, and fly with PX4 |
| [Developer Guide](https://docs.px4.io/main/en/development/development.html) | Modify the flight stack, add peripherals, port to new hardware |
| [Airframe Reference](https://docs.px4.io/main/en/airframes/airframe_reference.html) | Full list of supported frames |
| [Autopilot Hardware](https://docs.px4.io/main/en/flight_controller/) | Compatible flight controllers |
| [Release Notes](https://docs.px4.io/main/en/releases/) | What's new in each release |
| [Contribution Guide](https://docs.px4.io/main/en/contribute/) | How to contribute to PX4 |
> **Note** The dev call is open to all interested developers (not just the core dev team). This is a great opportunity to meet the team and contribute to the ongoing development of the platform. It includes a QA session for newcomers. All regular calls are listed in the [Dronecode calendar](https://www.dronecode.org/calendar/).
## Community
- **Weekly Dev Call** — open to all developers ([Dronecode calendar](https://www.dronecode.org/calendar/))
- **Discord** — [Join the Dronecode server](https://discord.gg/dronecode)
- **Discussion Forum** — [PX4 Discuss](https://discuss.px4.io/)
- **Maintainers** — see [`MAINTAINERS.md`](MAINTAINERS.md)
- **Contributor Stats** — [LFX Insights](https://insights.lfx.linuxfoundation.org/foundation/dronecode)
## Maintenance Team
## Contributing
See the latest list of maintainers on [MAINTAINERS](MAINTAINERS.md) file at the root of the project.
We welcome contributions of all kinds — bug reports, documentation, new features, and code reviews. Please read the [Contribution Guide](https://docs.px4.io/main/en/contribute/) to get started.
For the latest stats on contributors please see the latest stats for the Dronecode ecosystem in our project dashboard under [LFX Insights](https://insights.lfx.linuxfoundation.org/foundation/dronecode). For information on how to update your profile and affiliations please see the following support link on how to [Complete Your LFX Profile](https://docs.linuxfoundation.org/lfx/my-profile/complete-your-lfx-profile). Dronecode publishes a yearly snapshot of contributions and achievements on its [website under the Reports section](https://dronecode.org).
## Governance
## Supported Hardware
The PX4 Autopilot project is hosted by the [Dronecode Foundation](https://www.dronecode.org/), a [Linux Foundation](https://www.linuxfoundation.org/) Collaborative Project. Dronecode holds all PX4 trademarks and serves as the project's legal guardian, ensuring vendor-neutral stewardship — no single company owns the name or controls the roadmap. The source code is licensed under the [BSD 3-Clause](LICENSE) license, so you are free to use, modify, and distribute it in your own projects.
For the most up to date information, please visit [PX4 User Guide > Autopilot Hardware](https://docs.px4.io/main/en/flight_controller/).
<p align="center">
<a href="https://www.dronecode.org/">
<img src="docs/assets/site/dronecode_logo.svg" alt="Dronecode Logo" width="180">
</a>
</p>
## Project Governance
The PX4 Autopilot project including all of its trademarks is hosted under [Dronecode](https://www.dronecode.org/), part of the Linux Foundation.
<a href="https://www.dronecode.org/" style="padding:20px" ><img src="https://dronecode.org/wp-content/uploads/sites/24/2020/08/dronecode_logo_default-1.png" alt="Dronecode Logo" width="110px"/></a>
<div style="padding:10px">&nbsp;</div>
@@ -1,38 +0,0 @@
#!/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
@@ -44,6 +44,8 @@ param set-default FW_T_SINK_MIN 3
param set-default FW_W_EN 1
param set-default FD_ESCS_EN 0
param set-default MIS_TAKEOFF_ALT 30
param set-default NAV_ACC_RAD 15
@@ -101,6 +101,6 @@ param set-default NAV_ACC_RAD 5
param set-default NAV_DLL_ACT 2
param set-default VT_FWD_THRUST_EN 4
param set-default VT_PITCH_MIN -5
param set-default VT_F_TRANS_THR 1
param set-default VT_TYPE 2
param set-default FD_ESCS_EN 0
@@ -26,6 +26,7 @@ param set-default SENS_EN_GPSSIM 1
param set-default SENS_EN_BAROSIM 1
param set-default SENS_EN_MAGSIM 1
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
param set-default FD_ESCS_EN 0 # We don't have ESCs - but maybe we need this later?
# Set proper failsafes
param set-default COM_ACT_FAIL_ACT 0
@@ -41,21 +42,23 @@ param set-default FD_FAIL_R 0
param set-default CA_ROTOR_COUNT 8
param set-default CA_R_REV 255
param set-default CA_ROTOR0_AX 1
param set-default CA_ROTOR0_AY -1
param set-default CA_ROTOR0_AX -1
param set-default CA_ROTOR0_AY 1
param set-default CA_ROTOR0_AZ 0
param set-default CA_ROTOR0_KM 0
param set-default CA_ROTOR0_PX 0.14
param set-default CA_ROTOR0_PY 0.10
param set-default CA_ROTOR0_PZ 0.06
#param set-default CA_ROTOR0_PZ 0.0
param set-default CA_ROTOR1_AX 1
param set-default CA_ROTOR1_AY 1
param set-default CA_ROTOR1_AX -1
param set-default CA_ROTOR1_AY -1
param set-default CA_ROTOR1_AZ 0
param set-default CA_ROTOR1_KM 0
param set-default CA_ROTOR1_PX 0.14
param set-default CA_ROTOR1_PY -0.10
param set-default CA_ROTOR1_PZ 0.06
#param set-default CA_ROTOR1_PZ 0.0
param set-default CA_ROTOR2_AX 1
param set-default CA_ROTOR2_AY 1
@@ -64,6 +67,7 @@ param set-default CA_ROTOR2_KM 0
param set-default CA_ROTOR2_PX -0.14
param set-default CA_ROTOR2_PY 0.10
param set-default CA_ROTOR2_PZ 0.06
#param set-default CA_ROTOR2_PZ 0.0
param set-default CA_ROTOR3_AX 1
param set-default CA_ROTOR3_AY -1
@@ -75,7 +79,7 @@ param set-default CA_ROTOR3_PZ 0.06
param set-default CA_ROTOR4_AX 0
param set-default CA_ROTOR4_AY 0
param set-default CA_ROTOR4_AZ -1
param set-default CA_ROTOR4_AZ 1
param set-default CA_ROTOR4_KM 0
param set-default CA_ROTOR4_PX 0.12
param set-default CA_ROTOR4_PY 0.22
@@ -99,7 +103,7 @@ param set-default CA_ROTOR6_PZ 0
param set-default CA_ROTOR7_AX 0
param set-default CA_ROTOR7_AY 0
param set-default CA_ROTOR7_AZ -1
param set-default CA_ROTOR7_AZ 1
param set-default CA_ROTOR7_KM 0
param set-default CA_ROTOR7_PX -0.12
param set-default CA_ROTOR7_PY -0.22
@@ -28,6 +28,7 @@ param set-default SIM_GZ_EN 1
param set-default SENS_EN_MAGSIM 1
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
param set-default FD_ESCS_EN 0
param set-default CA_AIRFRAME 14
param set-default MAV_TYPE 45
@@ -28,6 +28,7 @@ param set-default SIM_GZ_EN 1
param set-default SENS_EN_MAGSIM 1
param set-default COM_ARM_CHK_ESCS 0 # We don't have ESCs
param set-default FD_ESCS_EN 0
param set-default CA_AIRFRAME 14
param set-default MAV_TYPE 45
@@ -107,7 +107,6 @@ 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,6 +3,7 @@
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,7 +2,7 @@
#
# @name HolyBro QAV250
#
# @url https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini
# @url https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini.html
#
# @type Quadrotor x
# @class Copter
@@ -2,7 +2,7 @@
#
# @name Aion Robotics R1 UGV
#
# @url https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1
# @url https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1.html
#
# @type Rover
# @class Rover
@@ -22,9 +22,6 @@
. ${R}etc/init.d/rc.uuv_defaults
# Overwrite DDS AG IP to `192.168.0.1`
param set-default UXRCE_DDS_AG_IP -1062731775
# param set-default MAV_1_CONFIG 102
param set-default BAT1_A_PER_V 37.8798
@@ -10,6 +10,9 @@ set VEHICLE_TYPE uuv
# MAV_TYPE_SUBMARINE 12
param set-default MAV_TYPE 12
# Set micro-dds-client to use ethernet and IP-address 192.168.0.1
param set-default UXRCE_DDS_AG_IP -1062731775
# Disable preflight disarm to not interfere with external launching
param set-default COM_DISARM_PRFLT -1
param set-default CBRK_SUPPLY_CHK 894281
+12 -28
View File
@@ -2,40 +2,24 @@
## Supported Versions
The following versions receive security updates:
The following is a list of versions the development team is currently supporting.
| Version | Supported |
| ------- | ------------------ |
| 1.16.x | :white_check_mark: |
| < 1.16 | :x: |
| 1.4.x | :white_check_mark: |
| 1.3.3 | :white_check_mark: |
| < 1.3 | :x: |
## Reporting a Vulnerability
We receive security vulnerability reports through GitHub Security Advisories.
We currently only receive security vulnerability reports through GitHub.
To begin a report, go to the [PX4/PX4-Autopilot](https://github.com/PX4/PX4-Autopilot) repository
and click on the **Security** tab. If you are on mobile, click the **...** dropdown menu, then click **Security**.
To begin a report, please go to the top-level repository, for example, PX4/PX4-Autopilot,
and click on the Security tab. If you are on mobile, click the ... dropdown menu, and then click Security.
Click **Report a Vulnerability** to open the advisory form. Fill in the advisory details form.
Make sure your title is descriptive and the description contains all relevant details needed
to verify the issue. We welcome logs, screenshots, photos, and videos.
Click Report a Vulnerability to open the advisory form. Fill in the advisory details form.
Make sure your title is descriptive, and the development team can find all of the relevant details needed
to verify on the description box. We recommend you add as much data as possible. We welcome logs,
screenshots, photos, and videos, anything that can help us verify and identify the issues being reported.
At the bottom of the form, click **Submit report**.
## Response Process
1. **Acknowledgment**: The maintainer team will acknowledge your report within **7 days**.
2. **Triage**: We will assess severity and impact and communicate next steps.
3. **Disclosure**: We coordinate disclosure with the reporter. We follow responsible disclosure practices and will credit reporters in the advisory unless they request anonymity.
If you do not receive acknowledgment within 7 days, please follow up by emailing the [release managers](MAINTAINERS.md).
## Secure Development Practices
The PX4 development team applies the following practices to reduce security risk:
- **Code review**: All changes require peer review before merging.
- **Static analysis**: [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) runs on every pull request with warnings treated as errors.
- **Fuzzing**: A daily fuzzing pipeline using [Google fuzztest](https://github.com/google/fuzztest) tests MAVLink message handling and GNSS driver protocol parsing.
- **Input validation**: All external inputs (MAVLink messages, RC signals, sensor data) are validated against expected ranges before use.
- **Compiler hardening**: Builds use `-Wall -Werror`, stack protectors, and other hardening flags where supported by the target platform.
At the bottom of the form, click Submit report. The maintainer team will be notified and will get back to you ASAP.
@@ -39,8 +39,6 @@ exec find boards msg src platforms test \
-path src/lib/cdrstream/rosidl -prune -o \
-path src/modules/zenoh/zenoh-pico -prune -o \
-path boards/modalai/voxl2/libfc-sensor-api -prune -o \
-path boards/modalai/voxl2/src/lib/mpa/libmodal-json -prune -o \
-path boards/modalai/voxl2/src/lib/mpa/libmodal-pipe -prune -o \
-path src/drivers/actuators/vertiq_io/iq-module-communication-cpp -prune -o \
-path src/lib/tensorflow_lite_micro/tflite_micro -prune -o \
-path src/drivers/ins/sbgecom/sbgECom -prune -o \
-331
View File
@@ -1,331 +0,0 @@
#!/usr/bin/env python3
"""Validate commit messages in a PR against conventional commits format.
Reads a JSON array of GitHub commit objects from stdin (as returned by the
GitHub API's /pulls/{n}/commits endpoint) and checks each message for
blocking errors and advisory warnings.
With --markdown, outputs a formatted PR comment body instead of plain text.
"""
import json
import sys
from conventional_commits import (
EXEMPT_PREFIXES,
parse_header,
)
# Blocking: prefixes that indicate unsquashed fixup commits
FIXUP_PREFIXES = ('fixup!', 'squash!', 'amend!')
# Blocking: single-word throwaway messages (case-insensitive exact match)
THROWAWAY_WORDS = frozenset({
'fix', 'fixed', 'fixes',
'update', 'updated', 'updates',
'test', 'tests', 'testing',
'tmp', 'temp',
'oops', 'wip',
'debug', 'cleanup',
})
# Blocking: debug session leftovers
DEBUG_KEYWORDS = ('tmate',)
# Warning: review-response messages (case-insensitive substring match)
REVIEW_RESPONSE_PATTERNS = (
'address review',
'apply suggestions from code review',
'code review',
)
# Warning: formatter-only commits
FORMATTER_PATTERNS = (
'do make format',
'make format',
'run formatter',
'apply format',
)
MIN_MESSAGE_LENGTH = 5
def check_commit(message: str) -> tuple[list[str], list[str]]:
"""Return (errors, warnings) for a single commit message."""
errors: list[str] = []
warnings: list[str] = []
first_line = message.split('\n', 1)[0].strip()
lower = first_line.lower()
# --- Blocking checks ---
for prefix in FIXUP_PREFIXES:
if lower.startswith(prefix):
errors.append(f'Unsquashed commit: starts with "{prefix}"')
if lower == 'wip' or lower.startswith('wip ') or lower.startswith('wip:'):
errors.append('WIP commit should not be merged')
if len(first_line) < MIN_MESSAGE_LENGTH:
errors.append(f'Message too short ({len(first_line)} chars, minimum {MIN_MESSAGE_LENGTH})')
if first_line.strip() and first_line.strip().lower() in THROWAWAY_WORDS:
errors.append(f'Single-word throwaway message: "{first_line.strip()}"')
for kw in DEBUG_KEYWORDS:
if kw in lower:
errors.append(f'Debug session leftover: contains "{kw}"')
# --- Warning checks ---
for pattern in REVIEW_RESPONSE_PATTERNS:
if pattern in lower:
warnings.append('Review-response commit')
break
for pattern in FORMATTER_PATTERNS:
if pattern in lower:
warnings.append('Formatter-only commit')
break
if not parse_header(first_line):
# Exempt merge commits
for prefix in EXEMPT_PREFIXES:
if first_line.startswith(prefix):
break
else:
warnings.append(
'Missing conventional commit format '
'(e.g. "feat(ekf2): add something")'
)
return errors, warnings
def suggest_commit(message: str) -> str | None:
"""Suggest how to fix a bad commit message."""
first_line = message.split('\n', 1)[0].strip()
lower = first_line.lower()
for prefix in FIXUP_PREFIXES:
if lower.startswith(prefix):
return 'Squash this into the commit it fixes'
if lower == 'wip' or lower.startswith('wip ') or lower.startswith('wip:'):
return 'Reword with a descriptive message (e.g. "feat(scope): what changed")'
if len(first_line) < MIN_MESSAGE_LENGTH:
return 'Reword with a descriptive message (e.g. "feat(ekf2): what changed")'
if first_line.strip().lower() in THROWAWAY_WORDS:
return 'Reword with a descriptive message (e.g. "fix(scope): what changed")'
return None
def format_plain(data: list) -> tuple[bool, bool]:
"""Print plain text output. Returns (has_blocking, has_warnings)."""
has_blocking = False
has_warnings = False
for commit in data:
sha = commit.get('sha', '?')[:10]
message = commit.get('commit', {}).get('message', '')
first_line = message.split('\n', 1)[0].strip()
errors, warnings = check_commit(message)
if errors or warnings:
print(f"\n {sha} {first_line}")
for err in errors:
print(f" ERROR: {err}")
has_blocking = True
for warn in warnings:
print(f" WARNING: {warn}")
has_warnings = True
if has_blocking:
print(
"\n"
"ERROR = must fix before merging (CI will block the PR)\n"
"WARNING = advisory, not blocking, but recommended to fix\n"
"\n"
"See the contributing guide for details:\n"
" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
)
elif has_warnings:
print(
"\n"
"WARNING = advisory, not blocking, but recommended to fix\n"
"\n"
"See the contributing guide for details:\n"
" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
)
return has_blocking, has_warnings
def format_markdown_blocking(data: list) -> str:
"""Format a blocking error markdown comment."""
error_groups: dict[str, list[str]] = {}
unique_commits: list[tuple[str, str, list[str], str]] = []
for commit in data:
sha = commit.get('sha', '?')[:10]
message = commit.get('commit', {}).get('message', '')
first_line = message.split('\n', 1)[0].strip()
errors, _ = check_commit(message)
if not errors:
continue
suggestion = suggest_commit(message) or ''
unique_commits.append((sha, first_line, errors, suggestion))
for err in errors:
error_groups.setdefault(err, []).append(sha)
lines = [
"## \u274c Commit messages need attention before merging",
"",
]
has_large_group = any(len(shas) > 3 for shas in error_groups.values())
if has_large_group:
lines.extend([
"**Issues found:**",
"",
])
for err_msg, shas in error_groups.items():
if len(shas) > 3:
lines.append(f"- **{len(shas)} commits**: {err_msg} "
f"(`{shas[0]}`, `{shas[1]}`, ... `{shas[-1]}`)")
else:
sha_list = ', '.join(f'`{s}`' for s in shas)
lines.append(f"- {err_msg}: {sha_list}")
distinct_messages = {msg for _, msg, _, _ in unique_commits}
if len(distinct_messages) <= 5:
lines.extend(["", "**Affected commits:**", ""])
for sha, msg, errors, suggestion in unique_commits:
safe_msg = msg.replace('|', '\\|')
lines.append(f"- `{sha}` {safe_msg}")
else:
lines.extend([
"| Commit | Message | Issue | Suggested fix |",
"|--------|---------|-------|---------------|",
])
for sha, msg, errors, suggestion in unique_commits:
issues = '; '.join(errors)
safe_msg = msg.replace('|', '\\|')
lines.append(f"| `{sha}` | {safe_msg} | {issues} | {suggestion} |")
lines.extend([
"",
"See [CONTRIBUTING.md](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
"for how to clean up commits.",
"",
"---",
"*This comment will be automatically removed once the issues are resolved.*",
])
return '\n'.join(lines)
def format_markdown_advisory(data: list) -> str:
"""Format an advisory warning markdown comment."""
lines = [
"## \U0001f4a1 Commit messages could be improved",
"",
"Not blocking, but these commit messages could use some cleanup.",
"",
"| Commit | Message | Suggestion |",
"|--------|---------|------------|",
]
for commit in data:
sha = commit.get('sha', '?')[:10]
message = commit.get('commit', {}).get('message', '')
first_line = message.split('\n', 1)[0].strip()
_, warnings = check_commit(message)
if not warnings:
continue
suggestion = '; '.join(warnings)
safe_msg = first_line.replace('|', '\\|')
lines.append(f"| `{sha}` | {safe_msg} | {suggestion} |")
lines.extend([
"",
"See the [commit message convention](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
"for details.",
"",
"---",
"*This comment will be automatically removed once the issues are resolved.*",
])
return '\n'.join(lines)
def main() -> None:
markdown_stdout = '--markdown' in sys.argv
markdown_file = None
for i, a in enumerate(sys.argv):
if a == '--markdown-file' and i + 1 < len(sys.argv):
markdown_file = sys.argv[i + 1]
elif a.startswith('--markdown-file='):
markdown_file = a.split('=', 1)[1]
try:
data = json.load(sys.stdin)
except json.JSONDecodeError as exc:
print(f"Failed to parse JSON input: {exc}", file=sys.stderr)
sys.exit(2)
if not isinstance(data, list):
print("Expected a JSON array of commit objects.", file=sys.stderr)
sys.exit(2)
# Always compute blocking/warning state
has_blocking = False
has_warnings = False
for commit in data:
message = commit.get('commit', {}).get('message', '')
errors, warnings = check_commit(message)
if errors:
has_blocking = True
if warnings:
has_warnings = True
# Generate markdown if needed
md = None
if has_blocking:
md = format_markdown_blocking(data)
elif has_warnings:
md = format_markdown_advisory(data)
if md:
if markdown_stdout:
print(md)
if markdown_file:
with open(markdown_file, 'w') as f:
f.write(md + '\n')
elif markdown_file:
with open(markdown_file, 'w') as f:
pass
# Plain text output to stderr for CI logs (always, unless --markdown only)
if not markdown_stdout:
has_blocking, _ = format_plain(data)
sys.exit(1 if has_blocking else 0)
if __name__ == '__main__':
main()
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env python3
"""Validate that a PR title follows conventional commits format.
Format: type(scope): description
Can output plain text for CI logs or markdown for PR comments.
"""
import re
import sys
from conventional_commits import (
CONVENTIONAL_TYPES,
EXEMPT_PREFIXES,
parse_header,
suggest_scope,
suggest_type,
)
def suggest_title(title: str) -> str | None:
"""Try to suggest a corrected title in conventional commits format."""
stripped = title.strip()
# Remove common bracket prefixes like [docs], [CI], etc.
bracket_match = re.match(r'^\[([^\]]+)\]\s*(.+)', stripped)
if bracket_match:
prefix = bracket_match.group(1).strip().lower()
rest = bracket_match.group(2).strip()
rest = re.sub(r'^[\-:]\s*', '', rest).strip()
if len(rest) >= 5:
# Try to map bracket content to a type
commit_type = prefix if prefix in CONVENTIONAL_TYPES else suggest_type(rest)
scope = suggest_scope(rest)
if scope:
return f"{commit_type}({scope}): {rest}"
# Already has old-style "subsystem: description" format - convert it
colon_match = re.match(r'^([a-zA-Z][a-zA-Z0-9_/\-\. ]*): (.+)$', stripped)
if colon_match:
old_subsystem = colon_match.group(1).strip()
desc = colon_match.group(2).strip()
if len(desc) >= 5:
commit_type = suggest_type(desc)
# Use the old subsystem as scope (clean it up)
scope = old_subsystem.lower().replace(' ', '_')
return f"{commit_type}({scope}): {desc}"
# No format at all - try to guess both type and scope
commit_type = suggest_type(stripped)
scope = suggest_scope(stripped)
if scope:
desc = stripped[0].lower() + stripped[1:] if stripped else stripped
return f"{commit_type}({scope}): {desc}"
return None
def check_title(title: str) -> bool:
title = title.strip()
if not title:
print("PR title is empty.", file=sys.stderr)
return False
for prefix in EXEMPT_PREFIXES:
if title.startswith(prefix):
return True
if parse_header(title):
return True
types_str = ', '.join(f'`{t}`' for t in CONVENTIONAL_TYPES.keys())
print(
f"PR title does not match conventional commits format.\n"
f"\n"
f" Title: {title}\n"
f"\n"
f"Expected format: type(scope): description\n"
f"\n"
f"Valid types: {types_str}\n"
f"\n"
f"Good examples:\n"
f" feat(ekf2): add height fusion timeout\n"
f" fix(mavlink): correct BATTERY_STATUS_V2 parsing\n"
f" ci(workflows): migrate to reusable workflows\n"
f" feat(boards/px4_fmu-v6x)!: remove deprecated driver API\n"
f"\n"
f"Bad examples:\n"
f" fix stuff\n"
f" Update file\n"
f" ekf2: fix something (missing type prefix)\n"
f"\n"
f"See the contributing guide for details:\n"
f" https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention\n",
file=sys.stderr,
)
return False
def format_markdown(title: str) -> str:
"""Format a markdown PR comment body for a bad title."""
lines = [
"## \u274c PR title needs conventional commit format",
"",
"Expected format: `type(scope): description` "
"([conventional commits](https://www.conventionalcommits.org/)).",
"",
"**Your title:**",
f"> {title}",
"",
]
suggestion = suggest_title(title)
if suggestion:
lines.extend([
"**Suggested fix:**",
f"> {suggestion}",
"",
])
lines.extend([
"**To fix this:** click the ✏️ next to the PR title at the top "
"of this page and update it.",
"",
"See [CONTRIBUTING.md](https://github.com/PX4/PX4-Autopilot/blob/main/CONTRIBUTING.md#commit-message-convention) "
"for details.",
"",
"---",
"*This comment will be automatically removed once the issue is resolved.*",
])
return '\n'.join(lines)
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description='Check PR title format')
parser.add_argument('title', help='The PR title to validate')
parser.add_argument('--markdown', action='store_true',
help='Output markdown to stdout on failure')
parser.add_argument('--markdown-file', metavar='FILE',
help='Write markdown to FILE on failure')
args = parser.parse_args()
passed = check_title(args.title)
if not passed:
md = format_markdown(args.title)
if args.markdown:
print(md)
if args.markdown_file:
with open(args.markdown_file, 'w') as f:
f.write(md + '\n')
elif args.markdown_file:
with open(args.markdown_file, 'w') as f:
pass
sys.exit(0 if passed else 1)
if __name__ == '__main__':
main()
-146
View File
@@ -1,146 +0,0 @@
"""Shared constants and helpers for conventional commit validation.
Format: type(scope): description
Optional breaking change marker: type(scope)!: description
"""
import re
CONVENTIONAL_TYPES = {
'feat': 'A new feature',
'fix': 'A bug fix',
'docs': 'Documentation only changes',
'style': 'Formatting, whitespace, no code change',
'refactor': 'Code change that neither fixes a bug nor adds a feature',
'perf': 'Performance improvement',
'test': 'Adding or correcting tests',
'build': 'Build system or external dependencies',
'ci': 'CI configuration files and scripts',
'chore': 'Other changes that don\'t modify src or test files',
'revert': 'Reverts a previous commit',
}
# type(scope)[!]: description
# - type: one of CONVENTIONAL_TYPES keys
# - scope: required, alphanumeric with _/-/.
# - !: optional breaking change marker
# - description: at least 5 chars
HEADER_PATTERN = re.compile(
r'^(' + '|'.join(CONVENTIONAL_TYPES.keys()) + r')'
r'\(([a-zA-Z0-9_/\-\.]+)\)'
r'(!)?'
r': (.{5,})$'
)
EXEMPT_PREFIXES = ('Merge ',)
# Common PX4 subsystem scopes for suggestions
KNOWN_SCOPES = [
'ekf2', 'mavlink', 'commander', 'navigator', 'sensors',
'mc_att_control', 'mc_pos_control', 'mc_rate_control',
'fw_att_control', 'fw_pos_control', 'fw_rate_control',
'vtol', 'actuators', 'battery', 'param', 'logger',
'uorb', 'drivers', 'boards', 'simulation', 'sitl',
'gps', 'rc', 'safety', 'can', 'serial',
'ci', 'docs', 'build', 'cmake', 'tools',
'mixer', 'land_detector', 'airspeed', 'gyroscope',
'accelerometer', 'magnetometer', 'barometer',
]
# Keyword patterns to suggest scopes from description text
KEYWORD_SCOPES = [
(r'\b(ekf|estimator|height|fusion|imu|baro)\b', 'ekf2'),
(r'\b(mavlink|MAVLink|MAVLINK|command_int|heartbeat)\b', 'mavlink'),
(r'\b(uorb|orb|pub|sub|topic)\b', 'uorb'),
(r'\b(board|fmu|nuttx|stm32)\b', 'boards'),
(r'\b(mixer|actuator|motor|servo|pwm|dshot)\b', 'actuators'),
(r'\b(battery|power)\b', 'battery'),
(r'\b(param|parameter)\b', 'param'),
(r'\b(log|logger|sdlog)\b', 'logger'),
(r'\b(sensor|accel|gyro)\b', 'sensors'),
(r'\b(land|takeoff|rtl|mission|navigator|geofence)\b', 'navigator'),
(r'\b(position|velocity|attitude|rate)\s*(control|ctrl)\b', 'mc_att_control'),
(r'\b(mc|multicopter|quad)\b', 'mc_att_control'),
(r'\b(fw|fixedwing|fixed.wing|plane)\b', 'fw_att_control'),
(r'\b(vtol|transition)\b', 'vtol'),
(r'\b(ci|workflow|github.action|pipeline)\b', 'ci'),
(r'\b(doc|docs|documentation|readme)\b', 'docs'),
(r'\b(cmake|make|toolchain|compiler)\b', 'build'),
(r'\b(sitl|simulation|gazebo|jmavsim|sih)\b', 'simulation'),
(r'\b(can|uavcan|cyphal|dronecan)\b', 'can'),
(r'\b(serial|uart|spi|i2c)\b', 'serial'),
(r'\b(safety|failsafe|arm|disarm|kill)\b', 'safety'),
(r'\b(rc|radio|sbus|crsf|elrs|dsm)\b', 'rc'),
(r'\b(gps|gnss|rtk|ubx)\b', 'gps'),
(r'\b(optical.flow|flow|rangefinder|lidar|distance)\b', 'sensors'),
(r'\b(orbit|follow|offboard)\b', 'commander'),
(r'\b(driver)\b', 'drivers'),
]
# Verb patterns to suggest conventional commit type
VERB_TYPE_MAP = [
(r'^fix(e[ds])?[\s:]', 'fix'),
(r'^bug[\s:]', 'fix'),
(r'^add(s|ed|ing)?[\s:]', 'feat'),
(r'^implement', 'feat'),
(r'^introduce', 'feat'),
(r'^support', 'feat'),
(r'^enable', 'feat'),
(r'^update[ds]?[\s:]', 'feat'),
(r'^improv(e[ds]?|ing)', 'perf'),
(r'^optimi[zs](e[ds]?|ing)', 'perf'),
(r'^refactor', 'refactor'),
(r'^clean\s*up', 'refactor'),
(r'^restructure', 'refactor'),
(r'^simplif(y|ied)', 'refactor'),
(r'^remov(e[ds]?|ing)', 'refactor'),
(r'^delet(e[ds]?|ing)', 'refactor'),
(r'^deprecat', 'refactor'),
(r'^replac(e[ds]?|ing)', 'refactor'),
(r'^renam(e[ds]?|ing)', 'refactor'),
(r'^migrat', 'refactor'),
(r'^revert', 'revert'),
(r'^doc(s|ument)', 'docs'),
(r'^test', 'test'),
(r'^format', 'style'),
(r'^lint', 'style'),
(r'^whitespace', 'style'),
(r'^build', 'build'),
(r'^ci[\s:]', 'ci'),
]
def parse_header(text: str) -> dict | None:
"""Parse a conventional commit header into components.
Returns dict with keys {type, scope, breaking, subject} or None if
the text doesn't match conventional commits format.
"""
text = text.strip()
m = HEADER_PATTERN.match(text)
if not m:
return None
return {
'type': m.group(1),
'scope': m.group(2),
'breaking': m.group(3) == '!',
'subject': m.group(4),
}
def suggest_type(text: str) -> str:
"""Infer a conventional commit type from description text."""
lower = text.strip().lower()
for pattern, commit_type in VERB_TYPE_MAP:
if re.search(pattern, lower):
return commit_type
return 'feat'
def suggest_scope(text: str) -> str | None:
"""Infer a scope from keywords in the text."""
lower = text.strip().lower()
for pattern, scope in KEYWORD_SCOPES:
if re.search(pattern, lower, re.IGNORECASE):
return scope
return None
+7 -29
View File
@@ -36,20 +36,11 @@ if args.filter:
target_filter.append(target)
default_container = 'ghcr.io/px4/px4-dev:v1.16.0-rc1-258-g0369abd556'
voxl2_container = 'ghcr.io/px4/px4-dev-voxl2:v1.5'
build_configs = []
grouped_targets = {}
excluded_boards = ['px4_ros2', 'espressif_esp32'] # TODO: fix and enable
excluded_boards = ['modalai_voxl2', 'px4_ros2', 'espressif_esp32'] # TODO: fix and enable
excluded_manufacturers = ['atlflight']
excluded_platforms = []
# Container overrides for platforms/boards that need a non-default container
platform_container_overrides = {
'qurt': voxl2_container,
}
board_container_overrides = {
'modalai_voxl2': voxl2_container,
}
excluded_platforms = ['qurt']
excluded_labels = [
'stackcheck',
'nolockstep', 'replay', 'test',
@@ -97,20 +88,7 @@ def process_target(px4board_file, target_name):
if platform not in excluded_platforms:
container = default_container
# Extract board name (manufacturer_board) from target name
board_name = '_'.join(target_name.split('_')[:2])
# Apply container overrides for specific platforms or boards
if platform in platform_container_overrides:
container = platform_container_overrides[platform]
if board_name in board_container_overrides:
container = board_container_overrides[board_name]
# Boards with container overrides get their own group
if board_name in board_container_overrides or platform in platform_container_overrides:
group = 'voxl2'
elif platform == 'posix':
if platform == 'posix':
group = 'base'
if toolchain:
if toolchain.startswith('aarch64'):
@@ -148,18 +126,18 @@ grouped_targets['base']['manufacturers'] = {}
grouped_targets['base']['manufacturers']['px4'] = []
grouped_targets['base']['manufacturers']['px4'] += metadata_targets
for manufacturer in sorted(os.scandir(os.path.join(source_dir, '../boards')), key=lambda e: e.name):
for manufacturer in os.scandir(os.path.join(source_dir, '../boards')):
if not manufacturer.is_dir():
continue
if manufacturer.name in excluded_manufacturers:
if verbose: print(f'excluding manufacturer {manufacturer.name}')
continue
for board in sorted(os.scandir(manufacturer.path), key=lambda e: e.name):
for board in os.scandir(manufacturer.path):
if not board.is_dir():
continue
for files in sorted(os.scandir(board.path), key=lambda e: e.name):
for files in os.scandir(board.path):
if files.is_file() and files.name.endswith('.px4board'):
board_name = manufacturer.name + '_' + board.name
@@ -225,7 +203,7 @@ if (args.group):
if(verbose):
print(f'=:Architectures: [{grouped_targets.keys()}]')
for arch in grouped_targets:
runner = 'x64' if arch in ('nuttx', 'voxl2') else 'arm64'
runner = 'x64' if arch == 'nuttx' else 'arm64'
if(verbose):
print(f'=:Processing: [{arch}]')
temp_group = []
-431
View File
@@ -1,431 +0,0 @@
#!/usr/bin/env bash
#
# metadata_sync.sh - Unified metadata generation and synchronization for PX4 docs
#
# Usage:
# Tools/ci/metadata_sync.sh [OPTIONS] [TYPES...]
#
# Types:
# parameters - Parameter reference (docs/en/advanced_config/parameter_reference.md)
# airframes - Airframe reference (docs/en/airframes/airframe_reference.md)
# modules - Module documentation (docs/en/modules/*.md)
# msg_docs - uORB message docs (docs/en/msg_docs/*.md + docs/en/middleware/dds_topics.md)
# uorb_graphs - uORB graph JSONs (docs/public/middleware/*.json)
# failsafe_web - Failsafe simulator (docs/public/config/failsafe/*.{js,wasm,json})
# all - All of the above (default)
#
# Options:
# --generate Build the make targets to generate fresh metadata
# --sync Copy generated files to docs/
# --verbose Show detailed output
# --help Show this help
#
# Exit codes:
# 0 - Success (files synced or already up-to-date)
# 1 - Error (build failed, missing files, etc.)
#
# Examples:
# # Full regeneration and sync (orchestrator use case)
# Tools/ci/metadata_sync.sh --generate --sync all
#
# # Just sync specific type (assumes already built)
# Tools/ci/metadata_sync.sh --sync parameters
#
# # Generate only, don't copy
# Tools/ci/metadata_sync.sh --generate uorb_graphs
#
set -euo pipefail
shopt -s nullglob
# ═══════════════════════════════════════════════════════════════════════════════
# Configuration
# ═══════════════════════════════════════════════════════════════════════════════
EMSCRIPTEN_VERSION="3.1.64"
EMSDK_DIR="${EMSDK_DIR:-_emscripten_sdk}"
# All available metadata types
ALL_TYPES=(parameters airframes modules msg_docs uorb_graphs failsafe_web)
# ═══════════════════════════════════════════════════════════════════════════════
# Logging
# ═══════════════════════════════════════════════════════════════════════════════
VERBOSE=false
log() {
echo "[metadata_sync] $*"
}
log_verbose() {
if [[ "$VERBOSE" == "true" ]]; then
echo "[metadata_sync] $*"
fi
}
die() {
echo "[metadata_sync] ERROR: $*" >&2
exit 1
}
# ═══════════════════════════════════════════════════════════════════════════════
# Help
# ═══════════════════════════════════════════════════════════════════════════════
show_help() {
head -n 35 "$0" | tail -n +2 | sed 's/^# \?//'
exit 0
}
# ═══════════════════════════════════════════════════════════════════════════════
# Emscripten Setup
# ═══════════════════════════════════════════════════════════════════════════════
ensure_emscripten() {
if command -v emcc >/dev/null 2>&1; then
log_verbose "Emscripten already available: $(emcc --version | head -1)"
return 0
fi
log "Setting up Emscripten ${EMSCRIPTEN_VERSION}..."
if [[ ! -d "$EMSDK_DIR" ]]; then
log_verbose "Cloning emsdk to $EMSDK_DIR"
if [[ "$VERBOSE" == "true" ]]; then
git clone https://github.com/emscripten-core/emsdk.git "$EMSDK_DIR"
else
git clone https://github.com/emscripten-core/emsdk.git "$EMSDK_DIR" >/dev/null 2>&1
fi
fi
pushd "$EMSDK_DIR" >/dev/null
if [[ "$VERBOSE" == "true" ]]; then
./emsdk install "$EMSCRIPTEN_VERSION"
./emsdk activate "$EMSCRIPTEN_VERSION"
else
./emsdk install "$EMSCRIPTEN_VERSION" >/dev/null 2>&1
./emsdk activate "$EMSCRIPTEN_VERSION" >/dev/null 2>&1
fi
popd >/dev/null
# shellcheck source=/dev/null
source "${EMSDK_DIR}/emsdk_env.sh" >/dev/null 2>&1
log_verbose "Emscripten ready: $(emcc --version | head -1)"
}
# ═══════════════════════════════════════════════════════════════════════════════
# Generation Functions
# ═══════════════════════════════════════════════════════════════════════════════
generate_parameters() {
log "Generating parameters metadata..."
if [[ "$VERBOSE" == "true" ]]; then
make parameters_metadata
else
make parameters_metadata >/dev/null
fi
}
generate_airframes() {
log "Generating airframes metadata..."
if [[ "$VERBOSE" == "true" ]]; then
make airframe_metadata
else
make airframe_metadata >/dev/null
fi
}
generate_modules() {
log "Generating modules documentation..."
if [[ "$VERBOSE" == "true" ]]; then
make module_documentation
else
make module_documentation >/dev/null
fi
}
generate_msg_docs() {
log "Generating message documentation..."
if [[ "$VERBOSE" == "true" ]]; then
make msg_docs
else
make msg_docs >/dev/null
fi
}
generate_uorb_graphs() {
log "Generating uORB graphs..."
if [[ "$VERBOSE" == "true" ]]; then
make uorb_graphs
else
make uorb_graphs >/dev/null
fi
}
generate_failsafe_web() {
ensure_emscripten
log "Generating failsafe web..."
if [[ "$VERBOSE" == "true" ]]; then
make failsafe_web
else
make failsafe_web >/dev/null
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Sync Functions
# ═══════════════════════════════════════════════════════════════════════════════
sync_parameters() {
local src="build/px4_sitl_default/docs/parameters.md"
local dest="docs/en/advanced_config/parameter_reference.md"
log "Syncing parameters..."
if [[ ! -f "$src" ]]; then
die "Source file not found: $src (did you run --generate?)"
fi
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
log_verbose " $src -> $dest"
}
sync_airframes() {
local src="build/px4_sitl_default/docs/airframes.md"
local dest="docs/en/airframes/airframe_reference.md"
log "Syncing airframes..."
if [[ ! -f "$src" ]]; then
die "Source file not found: $src (did you run --generate?)"
fi
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
log_verbose " $src -> $dest"
}
sync_modules() {
local src_dir="build/px4_sitl_default/docs/modules"
local dest_dir="docs/en/modules"
log "Syncing modules..."
if [[ ! -d "$src_dir" ]]; then
die "Source directory not found: $src_dir (did you run --generate?)"
fi
local src_files=("$src_dir"/*.md)
if [[ ${#src_files[@]} -eq 0 ]]; then
die "No .md files found in $src_dir"
fi
mkdir -p "$dest_dir"
for src in "${src_files[@]}"; do
local name
name=$(basename "$src")
cp "$src" "$dest_dir/$name"
log_verbose " $src -> $dest_dir/$name"
done
}
sync_msg_docs() {
local src_dir="build/msg_docs"
local dest_dir="docs/en/msg_docs"
local middleware_dir="docs/en/middleware"
log "Syncing message docs..."
if [[ ! -d "$src_dir" ]]; then
die "Source directory not found: $src_dir (did you run --generate?)"
fi
local src_files=("$src_dir"/*.md)
if [[ ${#src_files[@]} -eq 0 ]]; then
die "No .md files found in $src_dir"
fi
mkdir -p "$dest_dir"
mkdir -p "$middleware_dir"
for src in "${src_files[@]}"; do
local name
name=$(basename "$src")
# dds_topics.md goes to middleware dir
if [[ "$name" == "dds_topics.md" ]]; then
cp "$src" "$middleware_dir/$name"
log_verbose " $src -> $middleware_dir/$name"
else
cp "$src" "$dest_dir/$name"
log_verbose " $src -> $dest_dir/$name"
fi
done
}
sync_uorb_graphs() {
local src_dir="Tools/uorb_graph"
local dest_dir="docs/public/middleware"
log "Syncing uORB graphs..."
local src_files=("$src_dir"/*.json)
if [[ ${#src_files[@]} -eq 0 ]]; then
die "No .json files found in $src_dir (did you run --generate?)"
fi
mkdir -p "$dest_dir"
for src in "${src_files[@]}"; do
local name
name=$(basename "$src")
cp "$src" "$dest_dir/$name"
log_verbose " $src -> $dest_dir/$name"
done
}
sync_failsafe_web() {
local src_dir="build/px4_sitl_default_failsafe_web"
local dest_dir="docs/public/config/failsafe"
log "Syncing failsafe web..."
if [[ ! -d "$src_dir" ]]; then
die "Source directory not found: $src_dir (did you run --generate?)"
fi
# Gather js, wasm, json files
local src_files=()
for ext in js wasm json; do
src_files+=("$src_dir"/*."$ext")
done
if [[ ${#src_files[@]} -eq 0 ]]; then
die "No .js/.wasm/.json files found in $src_dir"
fi
mkdir -p "$dest_dir"
for src in "${src_files[@]}"; do
local name
name=$(basename "$src")
cp "$src" "$dest_dir/$name"
log_verbose " $src -> $dest_dir/$name"
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# Main Logic
# ═══════════════════════════════════════════════════════════════════════════════
DO_GENERATE=false
DO_SYNC=false
SELECTED_TYPES=()
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--generate)
DO_GENERATE=true
shift
;;
--sync)
DO_SYNC=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--help|-h)
show_help
;;
-*)
die "Unknown option: $1"
;;
*)
# It's a type
SELECTED_TYPES+=("$1")
shift
;;
esac
done
# Default to all types if none specified
if [[ ${#SELECTED_TYPES[@]} -eq 0 ]]; then
SELECTED_TYPES=("all")
fi
# Expand "all" to all types
local expanded_types=()
for t in "${SELECTED_TYPES[@]}"; do
if [[ "$t" == "all" ]]; then
expanded_types+=("${ALL_TYPES[@]}")
else
expanded_types+=("$t")
fi
done
SELECTED_TYPES=("${expanded_types[@]}")
# Validate types
for t in "${SELECTED_TYPES[@]}"; do
local valid=false
for valid_type in "${ALL_TYPES[@]}"; do
if [[ "$t" == "$valid_type" ]]; then
valid=true
break
fi
done
if [[ "$valid" == "false" ]]; then
die "Unknown type: $t (valid: ${ALL_TYPES[*]})"
fi
done
# Must specify at least one action
if [[ "$DO_GENERATE" == "false" && "$DO_SYNC" == "false" ]]; then
die "Must specify at least one of: --generate, --sync"
fi
}
main() {
parse_args "$@"
log "Selected types: ${SELECTED_TYPES[*]}"
[[ "$DO_GENERATE" == "true" ]] && log "Actions: generate"
[[ "$DO_SYNC" == "true" ]] && log "Actions: sync"
# Remove duplicates from SELECTED_TYPES
local -A seen
local unique_types=()
for t in "${SELECTED_TYPES[@]}"; do
if [[ -z "${seen[$t]:-}" ]]; then
seen[$t]=1
unique_types+=("$t")
fi
done
SELECTED_TYPES=("${unique_types[@]}")
# Generate phase
if [[ "$DO_GENERATE" == "true" ]]; then
log "=== Generation Phase ==="
for t in "${SELECTED_TYPES[@]}"; do
"generate_$t"
done
fi
# Sync phase
if [[ "$DO_SYNC" == "true" ]]; then
log "=== Sync Phase ==="
for t in "${SELECTED_TYPES[@]}"; do
"sync_$t"
done
fi
log "Done."
exit 0
}
main "$@"
+22 -34
View File
@@ -1,47 +1,37 @@
#!/bin/bash
mkdir artifacts
cp **/**/*.px4 artifacts/ 2>/dev/null || true
cp **/**/*.elf artifacts/ 2>/dev/null || true
cp **/**/*.px4 artifacts/
cp **/**/*.elf artifacts/
for build_dir_path in build/*/ ; do
build_dir_path=${build_dir_path::${#build_dir_path}-1}
build_dir=${build_dir_path#*/}
mkdir -p artifacts/$build_dir
mkdir artifacts/$build_dir
find artifacts/ -maxdepth 1 -type f -name "*$build_dir*"
# Airframe (NuttX: build root, SITL: docs/ subdirectory)
airframes_src=""
if [ -f "$build_dir_path/airframes.xml" ]; then
airframes_src="$build_dir_path/airframes.xml"
elif [ -f "$build_dir_path/docs/airframes.xml" ]; then
airframes_src="$build_dir_path/docs/airframes.xml"
fi
if [ -n "$airframes_src" ]; then
cp "$airframes_src" "artifacts/$build_dir/"
fi
# Airframe
cp $build_dir_path/airframes.xml artifacts/$build_dir/
# Parameters
cp $build_dir_path/parameters.xml artifacts/$build_dir/ 2>/dev/null || true
cp $build_dir_path/parameters.json artifacts/$build_dir/ 2>/dev/null || true
cp $build_dir_path/parameters.json.xz artifacts/$build_dir/ 2>/dev/null || true
cp $build_dir_path/parameters.xml artifacts/$build_dir/
cp $build_dir_path/parameters.json artifacts/$build_dir/
cp $build_dir_path/parameters.json.xz artifacts/$build_dir/
# Actuators
cp $build_dir_path/actuators.json artifacts/$build_dir/ 2>/dev/null || true
cp $build_dir_path/actuators.json.xz artifacts/$build_dir/ 2>/dev/null || true
cp $build_dir_path/actuators.json artifacts/$build_dir/
cp $build_dir_path/actuators.json.xz artifacts/$build_dir/
# Events
mkdir -p artifacts/$build_dir/events/
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/events/ 2>/dev/null || true
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/
# ROS 2 msgs
cp $build_dir_path/events/all_events.json.xz artifacts/$build_dir/
# Module Docs
ls -la artifacts/$build_dir
echo "----------"
done
if [ -d artifacts/px4_sitl_default ]; then
# general metadata (used by Flight Review and other downstream consumers)
mkdir -p artifacts/_general/
# general metadata
mkdir artifacts/_general/
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
# Airframe
if [ -f artifacts/px4_sitl_default/airframes.xml ]; then
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
else
echo "Error: expected 'artifacts/px4_sitl_default/airframes.xml' not found." >&2
exit 1
fi
cp artifacts/px4_sitl_default/airframes.xml artifacts/_general/
# Parameters
cp artifacts/px4_sitl_default/parameters.xml artifacts/_general/
cp artifacts/px4_sitl_default/parameters.json artifacts/_general/
@@ -50,11 +40,9 @@ if [ -d artifacts/px4_sitl_default ]; then
cp artifacts/px4_sitl_default/actuators.json artifacts/_general/
cp artifacts/px4_sitl_default/actuators.json.xz artifacts/_general/
# Events
if [ -f artifacts/px4_sitl_default/events/all_events.json.xz ]; then
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
else
echo "Error: expected 'artifacts/px4_sitl_default/events/all_events.json.xz' not found." >&2
exit 1
fi
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
# ROS 2 msgs
cp artifacts/px4_sitl_default/events/all_events.json.xz artifacts/_general/
# Module Docs
ls -la artifacts/_general/
fi
-95
View File
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
"""Post, update, or delete a PR comment with deduplication.
Uses hidden HTML markers to find existing comments and avoid duplicates.
Reads comment body from stdin when posting or updating.
Usage:
echo "comment body" | python3 pr_comment.py --marker pr-title --pr 123 --result fail
python3 pr_comment.py --marker pr-title --pr 123 --result pass
Results:
fail - post/update comment with body from stdin
warn - post/update comment with body from stdin
pass - delete existing comment if any
Requires GH_TOKEN and GITHUB_REPOSITORY environment variables.
"""
import argparse
import json
import os
import subprocess
import sys
def gh_api(endpoint: str, method: str = 'GET', body: dict | None = None) -> str:
"""Call the GitHub API via gh cli."""
cmd = ['gh', 'api', endpoint, '-X', method]
if body:
for key, value in body.items():
cmd.extend(['-f', f'{key}={value}'])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 and method != 'DELETE':
print(f"gh api error: {result.stderr}", file=sys.stderr)
return result.stdout
def find_comment(repo: str, pr: int, marker: str) -> str | None:
"""Find an existing comment by its hidden marker. Returns comment ID or None."""
response = gh_api(f"repos/{repo}/issues/{pr}/comments?per_page=100")
try:
comments = json.loads(response)
except json.JSONDecodeError:
return None
if not isinstance(comments, list):
return None
for comment in comments:
if isinstance(comment, dict) and comment.get('body', '').startswith(marker):
return str(comment['id'])
return None
def main() -> None:
parser = argparse.ArgumentParser(description='Manage PR quality comments')
parser.add_argument('--marker', required=True,
help='Marker name (e.g. pr-title, commit-msgs, pr-body)')
parser.add_argument('--pr', required=True, type=int,
help='Pull request number')
parser.add_argument('--result', required=True, choices=['pass', 'fail', 'warn'],
help='Check result: pass deletes comment, fail/warn posts it')
args = parser.parse_args()
repo = os.environ.get('GITHUB_REPOSITORY', '')
if not repo:
print("GITHUB_REPOSITORY not set", file=sys.stderr)
sys.exit(2)
marker = f"<!-- commit-quality-{args.marker} -->"
existing_id = find_comment(repo, args.pr, marker)
if args.result == 'pass':
if existing_id:
gh_api(f"repos/{repo}/issues/comments/{existing_id}", method='DELETE')
return
# Read comment body from stdin
body_content = sys.stdin.read().strip()
if not body_content:
print("No comment body provided on stdin", file=sys.stderr)
sys.exit(2)
full_body = f"{marker}\n{body_content}"
if existing_id:
gh_api(f"repos/{repo}/issues/comments/{existing_id}", method='PATCH',
body={'body': full_body})
else:
gh_api(f"repos/{repo}/issues/{args.pr}/comments", method='POST',
body={'body': full_body})
if __name__ == '__main__':
main()
-163
View File
@@ -1,163 +0,0 @@
#!/usr/bin/env bash
#
# test_metadata_sync.sh - Test metadata_sync.sh locally using Docker
#
# Usage:
# Tools/ci/test_metadata_sync.sh [OPTIONS] [TYPES...]
#
# Options:
# --shell Drop into interactive shell instead of running sync
# --verbose Pass --verbose to metadata_sync.sh
# --skip-build Skip SITL build (use existing build artifacts)
# --help Show this help
#
# Types:
# Same as metadata_sync.sh: parameters, airframes, modules, msg_docs, uorb_graphs, failsafe_web, all
#
# Examples:
# # Test full regeneration
# Tools/ci/test_metadata_sync.sh all
#
# # Test just parameters (faster)
# Tools/ci/test_metadata_sync.sh parameters
#
# # Drop into shell for debugging
# Tools/ci/test_metadata_sync.sh --shell
#
# # Skip build if you already have artifacts
# Tools/ci/test_metadata_sync.sh --skip-build --verbose all
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
DOCKER_IMAGE="px4io/px4-dev:v1.17.0-alpha1"
CONTAINER_NAME="px4-metadata-test-$$"
SHELL_MODE=false
VERBOSE=""
SKIP_BUILD=false
TYPES=()
show_help() {
head -n 28 "$0" | tail -n +2 | sed 's/^# \?//'
exit 0
}
cleanup() {
echo "[test] Cleaning up container..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--shell)
SHELL_MODE=true
shift
;;
--verbose)
VERBOSE="--verbose"
shift
;;
--skip-build)
SKIP_BUILD=true
shift
;;
--help|-h)
show_help
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
TYPES+=("$1")
shift
;;
esac
done
# Default to all types
if [[ ${#TYPES[@]} -eq 0 ]]; then
TYPES=("all")
fi
}
main() {
parse_args "$@"
cd "$REPO_ROOT"
echo "[test] Using Docker image: $DOCKER_IMAGE"
echo "[test] Repository root: $REPO_ROOT"
# Pull image if not present
if ! docker image inspect "$DOCKER_IMAGE" >/dev/null 2>&1; then
echo "[test] Pulling Docker image..."
docker pull "$DOCKER_IMAGE"
fi
trap cleanup EXIT
# Handle git worktrees: the .git file points to the main repo's .git directory
# We need to mount that directory too so git works inside the container
local git_mounts=()
if [[ -f "$REPO_ROOT/.git" ]]; then
# It's a worktree - read the gitdir path and mount it
local gitdir
gitdir=$(grep '^gitdir:' "$REPO_ROOT/.git" | cut -d' ' -f2)
if [[ -n "$gitdir" ]]; then
# Mount the gitdir at the same path so the .git file reference works
git_mounts+=("-v" "$gitdir:$gitdir:ro")
# Also need the main .git directory (parent of worktrees/)
local main_git_dir
main_git_dir=$(dirname "$(dirname "$gitdir")")
git_mounts+=("-v" "$main_git_dir:$main_git_dir:ro")
echo "[test] Detected git worktree, mounting git directories"
fi
fi
if [[ "$SHELL_MODE" == "true" ]]; then
echo "[test] Starting interactive shell..."
echo "[test] Run: Tools/ci/metadata_sync.sh --generate --sync all"
docker run -it --rm \
--name "$CONTAINER_NAME" \
-v "$REPO_ROOT:/src" \
"${git_mounts[@]}" \
-w /src \
"$DOCKER_IMAGE" \
/bin/bash
else
echo "[test] Running metadata sync for: ${TYPES[*]}"
# Build the command
local cmd=""
if [[ "$SKIP_BUILD" == "false" ]]; then
cmd="Tools/ci/metadata_sync.sh --generate --sync $VERBOSE ${TYPES[*]}"
else
cmd="Tools/ci/metadata_sync.sh --sync $VERBOSE ${TYPES[*]}"
fi
echo "[test] Command: $cmd"
docker run --rm \
--name "$CONTAINER_NAME" \
-v "$REPO_ROOT:/src" \
"${git_mounts[@]}" \
-w /src \
"$DOCKER_IMAGE" \
/bin/bash -c "$cmd"
echo ""
echo "[test] Done! Check git status for changes:"
echo " git status -s docs/"
echo ""
echo "[test] To see what changed:"
echo " git diff docs/"
fi
}
main "$@"
+24 -4
View File
@@ -1,27 +1,47 @@
#! /bin/bash
if [ -z ${PX4_DOCKER_REPO+x} ]; then
PX4_DOCKER_REPO="px4io/px4-dev:v1.17.0-beta1"
echo "guessing PX4_DOCKER_REPO based on input";
if [[ $@ =~ .*clang.* ]] || [[ $@ =~ .*scan-build.* ]]; then
# clang tools
PX4_DOCKER_REPO="px4io/px4-dev-clang:2021-02-04"
elif [[ $@ =~ .*tests* ]]; then
# run all tests with simulation
PX4_DOCKER_REPO="px4io/px4-dev-simulation-bionic:2021-12-11"
fi
else
echo "PX4_DOCKER_REPO is set to '$PX4_DOCKER_REPO'";
fi
# otherwise default to nuttx
if [ -z ${PX4_DOCKER_REPO+x} ]; then
PX4_DOCKER_REPO="px4io/px4-dev:v1.16.0-rc1-258-g0369abd556"
fi
echo "PX4_DOCKER_REPO: $PX4_DOCKER_REPO";
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
SRC_DIR=${SCRIPT_DIR}/../
PWD=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
SRC_DIR=$PWD/../
CCACHE_DIR=${HOME}/.ccache
mkdir -p "${CCACHE_DIR}"
docker run -it --rm -w "${SRC_DIR}" \
--user="$(id -u):$(id -g)" \
--env=AWS_ACCESS_KEY_ID \
--env=AWS_SECRET_ACCESS_KEY \
--env=BRANCH_NAME \
--env=CCACHE_DIR="${CCACHE_DIR}" \
--env=CI \
--env=CODECOV_TOKEN \
--env=COVERALLS_REPO_TOKEN \
--env=PX4_ASAN \
--env=PX4_MSAN \
--env=PX4_TSAN \
--env=PX4_UBSAN \
--env=TRAVIS_BRANCH \
--env=TRAVIS_BUILD_ID \
--publish 14556:14556/udp \
--volume=${CCACHE_DIR}:${CCACHE_DIR}:rw \
--volume=${SRC_DIR}:${SRC_DIR}:rw \
${PX4_DOCKER_REPO} /bin/bash -c "$@"
${PX4_DOCKER_REPO} /bin/bash -c "$1 $2 $3"
@@ -234,14 +234,14 @@ def get_actuator_output(yaml_config, output_functions, timer_config_file, verbos
param_prefix = process_param_prefix(group['param_prefix'])
standard_params = group.get('standard_params', {})
standard_params_array = [
( 'function', 'Function', 'FUNC', False, True ),
( 'disarmed', 'Disarmed', 'DIS', False, True ),
( 'min', 'Minimum', 'MIN', False, True ),
( 'center', 'Center\n(for Servos)', 'CENT', False, False ),
( 'max', 'Maximum', 'MAX', False, True ),
( 'failsafe', 'Failsafe', 'FAIL', True, True ),
( 'function', 'Function', 'FUNC', False ),
( 'disarmed', 'Disarmed', 'DIS', False ),
( 'min', 'Minimum', 'MIN', False ),
( 'max', 'Maximum', 'MAX', False ),
( 'center', 'Center\n(for Servos)', 'CENT', False ),
( 'failsafe', 'Failsafe', 'FAIL', True ),
]
for key, label, param_suffix, advanced, has_function in standard_params_array:
for key, label, param_suffix, advanced in standard_params_array:
show_if = None
if key in standard_params and 'show_if' in standard_params[key]:
show_if = standard_params[key]['show_if']
@@ -250,12 +250,13 @@ def get_actuator_output(yaml_config, output_functions, timer_config_file, verbos
param = {
'label': label,
'name': param_prefix+'_'+param_suffix+'${i}',
'function': key,
}
if has_function: param['function'] = key
if advanced: param['advanced'] = True
if show_if: param['show-if'] = show_if
per_channel_params.append(param)
param = {
'label': 'Rev Range\n(for Servos)',
'name': param_prefix+'_REV',
+5 -1
View File
@@ -312,7 +312,11 @@ When set to -1 (default), the value depends on the function (see {:}).
if standard_params[key]['max'] >= 1<<16:
raise Exception('maximum value for {:} expected <= {:} (got {:})'.format(key, 1<<16, standard_params[key]['max']))
if key == 'failsafe' or key == 'center':
if key == 'failsafe':
standard_params[key]['default'] = -1
standard_params[key]['min'] = -1
if key == 'center':
standard_params[key]['default'] = -1
standard_params[key]['min'] = -1
+41 -40
View File
@@ -17,10 +17,10 @@ VALID_FIELDS = { #Note, also have to add the message types as those can be field
'uint32'
}
ALLOWED_UNITS = set(["m", "m/s", "m/s^2", "(m/s)^2", "deg", "deg/s", "rad", "rad/s", "rad^2", "rpm" ,"V", "A", "mA", "mAh", "W", "Wh", "dBm", "h", "minutes", "s", "ms", "us", "Ohm", "MB", "Kb/s", "degC","Pa", "%", "norm", "-"])
ALLOWED_UNITS = set(["m", "m/s", "m/s^2", "(m/s)^2", "deg", "deg/s", "rad", "rad/s", "rad^2", "rpm" ,"V", "A", "mA", "mAh", "W", "dBm", "h", "s", "ms", "us", "Ohm", "MB", "Kb/s", "degC","Pa","%","-"])
invalid_units = set()
ALLOWED_FRAMES = set(["NED", "Body", "FRD", "ENU"])
ALLOWED_INVALID_VALUES = set(["NaN", "0", "-1"])
ALLOWED_FRAMES = set(["NED","Body"])
ALLOWED_INVALID_VALUES = set(["NaN", "0"])
ALLOWED_CONSTANTS_NOT_IN_ENUM = set(["ORB_QUEUE_LENGTH","MESSAGE_VERSION"])
class Error:
@@ -36,14 +36,14 @@ class Error:
if 'trailing_whitespace' == self.type:
if self.issueString.strip():
if self.issueString.strip():
print(f"NOTE: Line has trailing whitespace ({self.message}: {self.linenumber}): {self.issueString}")
else:
print(f"NOTE: Line has trailing whitespace ({self.message}: {self.linenumber})")
elif 'leading_whitespace_field_or_constant' == self.type:
print(f"NOTE: Whitespace before field or constant ({self.message}: {self.linenumber}): {self.issueString}")
print(f"NOTE: Whitespace before field or constant ({self.message}: {self.linenumber}): {self.issueString}")
elif 'field_or_constant_has_multiple_whitepsace' == self.type:
print(f"NOTE: Field/constant has more than one sequential whitespace character ({self.message}: {self.linenumber}): {self.issueString}")
print(f"NOTE: Field/constant has more than one sequential whitespace character ({self.message}: {self.linenumber}): {self.issueString}")
elif 'empty_start_line' == self.type:
print(f"NOTE: Empty line at start of file ({self.message}: {self.linenumber})")
elif 'internal_comment' == self.type:
@@ -191,7 +191,7 @@ class CommandParam:
if not "unknown_frame" in self.parent.errors:
self.parent.errors["unknown_frame"] = []
self.parent.errors["unknown_frame"].append(error)
"""
"""
else:
print(f"WARNING: Unhandled metadata in message comment: {item}")
# TODO - report errors for different kinds of metadata
@@ -202,9 +202,9 @@ class CommandParam:
if item == "-":
unit = ""
if unit and unit not in self.units:
self.units.append(unit)
self.units.append(unit)
if unit not in ALLOWED_UNITS:
invalid_units.add(unit)
@@ -221,7 +221,7 @@ class CommandParam:
print(f" paramText: {self.paramText}\n unit: {self.units}\n enums: {self.enums}\n lineNumber: {self.lineNumber}\n range: {self.range}\n minValue: {self.minValue}\n maxValue: {self.maxValue}\n invalidValue: {self.invalidValue}\n frameValue: {self.frameValue}\n parent: {self.parent}\n ")
class CommandConstant:
"""
Represents a constant that is a command definition.
@@ -252,9 +252,9 @@ class CommandConstant:
if not self.comment: # This is an bug for a command
#print(f"Debug WARNING: NO COMMENT in CommandConstant: {self.name}") ## TODO make into ERROR
return
# Parse command comment to get the description and parameters.
# print(f"Debug CommandConstant: {self.comment}")
# print(f"Debug CommandConstant: {self.comment}")
if not "|" in self.comment:
# This is an error for a command constant
error = Error("command_no_params_pipes", self.parent.filename, self.line_number, self.comment, self.name)
@@ -263,7 +263,7 @@ class CommandConstant:
self.parent.errors["command_no_params_pipes"] = []
self.parent.errors["command_no_params_pipes"].append(error)
return
# Split on pipes
commandSplit = self.comment.split("|")
if len(commandSplit) < 9:
@@ -318,7 +318,7 @@ Param | Units | Range/Enum | Description
output+=f"{i} | {", ".join(val.units)}|{', '.join(f"[{e}](#{e})" for e in val.enums)}{rangeVal} | {val.description}\n"
else:
output+=f"{i} | | | ?\n"
output+=f"{i} | | | ?\n"
output+=f"\n"
return output
@@ -419,7 +419,7 @@ class MessageField:
class UORBMessage:
"""
Represents a whole message, including fields, enums, commands, constants.
The parser function delegates the parsing of each part of the message to
The parser function delegates the parsing of each part of the message to
more appropriate classes, once the specific type of line has been identified.
"""
@@ -511,11 +511,11 @@ pageClass: is-wide-page
markdown += "--- | --- | --- |---\n"
for name, command in self.commandConstants.items():
description = f" {command.comment} " if enum.comment else " "
markdown += f'<a id="#{name}"></a> {name} | `{command.type}` | {command.value} |{description}\n'
markdown += f'<a href="#{name}"></a> {name} | `{command.type}` | {command.value} |{description}\n'
"""
for commandConstant in self.commandConstants.values():
#print(commandConstant)
markdown += commandConstant.markdown_out()
markdown += commandConstant.markdown_out()
# Generate enum docs
if len(self.enums) > 0:
@@ -529,7 +529,7 @@ pageClass: is-wide-page
for enumValueName, enumValue in enum.enumValues.items():
description = f" {enumValue.comment} " if enumValue.comment else " "
markdown += f'<a id="#{enumValueName}"></a> {enumValueName} | `{enumValue.type}` | {enumValue.value} |{description}\n'
markdown += f'<a href="#{enumValueName}"></a> {enumValueName} | `{enumValue.type}` | {enumValue.value} |{description}\n'
# Generate table for constants docs
if len(self.constantFields) > 0:
@@ -538,7 +538,7 @@ pageClass: is-wide-page
markdown += "--- | --- | --- |---\n"
for name, enum in self.constantFields.items():
description = f" {enum.comment} " if enum.comment else " "
markdown += f'<a id="#{name}"></a> {name} | `{enum.type}` | {enum.value} |{description}\n'
markdown += f'<a href="#{name}"></a> {name} | `{enum.type}` | {enum.value} |{description}\n'
@@ -635,8 +635,8 @@ pageClass: is-wide-page
temp = fieldOrConstant.split("=")
value = temp[-1]
typeAndName = temp[0].split(" ")
type = typeAndName[0].strip()
name = typeAndName[1].strip()
type = typeAndName[0]
name = typeAndName[1]
if name.startswith("VEHICLE_CMD_") and parentMessage.name == 'VehicleCommand': #it's a command.
#print(f"DEBUG: startswith VEHICLE_CMD_ {name}")
commandConstant = CommandConstant(name, type, value, comment, line_number, parentMessage)
@@ -708,7 +708,7 @@ pageClass: is-wide-page
if stripped_line.startswith("#"):
# Its an internal comment
stripped_line=stripped_line[1:].strip()
if stripped_line:
#print(f"{self.filename}: Internal comment: [{line_number}]\n {line}")
error = Error("internal_comment", self.filename, line_number, line)
@@ -723,16 +723,16 @@ pageClass: is-wide-page
self.errors["internal_comment_empty"].append(error)
#pass # Empty comment
continue
# Must be a field or a comment.
self.handleField(line, line_number, parentMessage=self)
# Fix up topics if the topic is empty
def camel_to_snake(name):
# Insert underscore between lowercase/digit and uppercase letter
s1 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', name)
# Insert underscore between consecutive uppercase and uppercase+lowercase
return re.sub('([A-Z]+)([A-Z][a-z])', r'\1_\2', s1).lower()
# Match upper case not at start of string
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
# Handle cases with multiple capital first letter
return re.sub('([A-Z]+)([A-Z][a-z]*)', r'\1_\2', s1).lower()
defaultTopic = camel_to_snake(self.name)
if len(self.topics) == 0:
@@ -745,7 +745,7 @@ pageClass: is-wide-page
error = Error("topic_error", self.filename, "", f"WARNING: TOPIC {defaultTopic} unnecessarily declared for {self.name}")
else:
# Declared topic is not default topic
error = Error("topic_error", self.filename, "", f"NOTE: TOPIC {self.topics[0]}: Only Declared topic is not default topic {defaultTopic} for {self.name}")
error = Error("topic_error", self.filename, "", f"NOTE: TOPIC {self.topics[1]}: Only Declared topic is not default topic {defaultTopic} for {self.name}")
if not "topic_error" in self.errors:
self.errors["topic_error"] = []
self.errors["topic_error"].append(error)
@@ -833,9 +833,11 @@ def generate_dds_yaml_doc(allMessageFiles, output_file = 'dds_topics.md'):
for message in data["subscriptions"]:
all_message_types.add(message['type'].split("::")[-1])
all_topics.add(message['topic'].split('/')[-1])
for message in (data.get("subscriptions_multi") or []):
all_message_types.add(message['type'].split("::")[-1])
all_topics.add(message['topic'].split('/')[-1])
if data["subscriptions_multi"]: # There is none now
dds_markdown += "None\n"
for message in data["subscriptions_multi"]:
all_message_types.add(message['type'].split("::")[-1])
all_topics.add(message['topic'].split('/')[-1])
for message in allMessageFiles:
all_messages_in_source.add(message.split('/')[-1].split('.')[0])
messagesNotExported = all_messages_in_source - all_message_types
@@ -872,17 +874,13 @@ Topic | Type| Rate Limit
dds_markdown += "\n## Subscriptions Multi\n\n"
subscriptions_multi = data.get("subscriptions_multi") or []
if not subscriptions_multi:
if not data["subscriptions_multi"]: # There is none now
dds_markdown += "None\n"
else:
dds_markdown += "Topic | Type | Route Field | Max Instances\n--- | --- | --- | ---\n"
for message in subscriptions_multi:
type = message['type']
px4Type = type.split("::")[-1]
route_field = f"`{message['route_field']}`" if 'route_field' in message else "-"
max_instances = message.get('max_instances', '-')
dds_markdown += f"{message['topic']} | [{type}](../msg_docs/{px4Type}.md) | {route_field} | {max_instances}\n"
print("Warning - we now have subscription_multi data - check format")
dds_markdown += "Topic | Type\n--- | ---\n"
for message in data["subscriptions_multi"]:
dds_markdown += f"{message['topic']} | {message['type']}\n"
if messagesNotExported:
# Print the topics that are not exported to DDS
@@ -946,6 +944,9 @@ if __name__ == "__main__":
for msg_file in msg_files:
# Add messages to set of allowed types (compound types)
#msg_type = msg_file.rsplit('/')[-1]
#msg_type = msg_type.rsplit('\\')[-1]
#msg_type = msg_type.rsplit('.')[0]
msg_name = os.path.splitext(os.path.basename(msg_file))[0]
msgTypes.add(msg_name)
+8 -8
View File
@@ -42,7 +42,6 @@
import argparse
import json
import base64
import os
import zlib
import time
import subprocess
@@ -100,13 +99,14 @@ if args.summary != None:
if args.description != None:
desc['description'] = str(args.description)
if args.git_identity != None:
git_dir = os.path.join(args.git_identity, '.git')
p = subprocess.run(["git", "--git-dir", git_dir, "describe", "--exclude", "ext/*", "--always", "--tags"],
stdout=subprocess.PIPE, text=True)
desc['git_identity'] = p.stdout.strip()
p = subprocess.run(["git", "--git-dir", git_dir, "rev-parse", "--verify", "HEAD"],
stdout=subprocess.PIPE, text=True)
desc['git_hash'] = p.stdout.strip()
cmd = "git --git-dir '{:}/.git' describe --exclude ext/* --always --tags".format(args.git_identity)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout
desc['git_identity'] = p.read().strip().decode('utf-8')
p.close()
cmd = "git --git-dir '{:}/.git' rev-parse --verify HEAD".format(args.git_identity)
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout
desc['git_hash'] = p.read().strip().decode('utf-8')
p.close()
if args.parameter_xml != None:
f = open(args.parameter_xml, "rb")
bytes = f.read()
-5
View File
@@ -144,8 +144,6 @@ def main():
help='number of tidy instances to be run in parallel.')
parser.add_argument('files', nargs='*', default=['.*'],
help='files to be processed (regex on path)')
parser.add_argument('-exclude', dest='exclude', default=None,
help='regular expression matching files to exclude')
parser.add_argument('-fix', action='store_true', help='apply fix-its')
parser.add_argument('-format', action='store_true', help='Reformat code '
'after applying fixes')
@@ -194,7 +192,6 @@ def main():
# Build up a big regexy filter from all command line arguments.
file_name_re = re.compile('(' + ')|('.join(args.files) + ')')
exclude_re = re.compile(args.exclude) if args.exclude else None
try:
# Spin up a bunch of tidy-launching threads.
@@ -208,8 +205,6 @@ def main():
# Fill the queue with files.
for name in files:
if file_name_re.search(name):
if exclude_re and exclude_re.search(name):
continue
queue.put(name)
# Wait for all threads to be done.
+3 -1
View File
@@ -4,7 +4,7 @@ GREEN='\033[0;32m'
NO_COLOR='\033[0m' # No Color
SCRIPTID="${GREEN}[docker-entrypoint.sh]${NO_COLOR}"
echo -e "$SCRIPTID $( uname -m ) | $(date -u +%FT%TZ)"
echo -e "$SCRIPTID Starting"
# Start virtual X server in the background
# - DISPLAY default is :99, set in dockerfile
@@ -22,4 +22,6 @@ if [ -n "${ROS_DISTRO}" ]; then
source "/opt/ros/$ROS_DISTRO/setup.bash"
fi
echo -e "$SCRIPTID ($( uname -m ))"
exec "$@"
+1 -1
View File
@@ -74,7 +74,7 @@ python3 -m pip install --user -r ${DIR}/requirements.txt
# Optional, but recommended additional simulation tools:
if [[ $INSTALL_SIM == "--sim-tools" ]]; then
if ! brew ls --versions px4-sim > /dev/null; then
if brew ls --versions px4-sim > /dev/null; then
brew install px4-sim
elif [[ $REINSTALL_FORMULAS == "--reinstall" ]]; then
brew reinstall px4-sim
+31 -17
View File
@@ -6,9 +6,9 @@ set -e
## Can also be used in docker.
##
## Installs:
## - Common dependencies and tools for nuttx, Gazebo
## - Common dependencies and tools for nuttx, jMAVSim, Gazebo
## - NuttX toolchain (omit with arg: --no-nuttx)
## - Gazebo Harmonic simulator (omit with arg: --no-sim-tools)
## - jMAVSim and Gazebo9 simulator (omit with arg: --no-sim-tools)
##
INSTALL_NUTTX="true"
@@ -196,11 +196,6 @@ if [[ $INSTALL_NUTTX == "true" ]]; then
fi
fi
if [[ "${UBUNTU_RELEASE}" == "25.10" ]]; then
echo "[ubuntu.sh] Gazebo binaries are not available for 25.10, skipping installation"
INSTALL_SIM="false"
fi
# Simulation tools
if [[ $INSTALL_SIM == "true" ]]; then
@@ -212,18 +207,37 @@ if [[ $INSTALL_SIM == "true" ]]; then
bc \
;
# Gazebo Harmonic installation (Ubuntu 22.04+)
echo "[ubuntu.sh] Gazebo (Harmonic) will be installed"
# Add Gazebo binary repository
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
sudo apt-get update -y --quiet
# Gazebo / Gazebo classic installation
if [[ "${UBUNTU_RELEASE}" == "18.04" || "${UBUNTU_RELEASE}" == "20.04" ]]; then
sudo sh -c 'echo "deb http://packages.osrfoundation.org/gazebo/ubuntu-stable `lsb_release -cs` main" > /etc/apt/sources.list.d/gazebo-stable.list'
wget http://packages.osrfoundation.org/gazebo.key -O - | sudo apt-key add -
# Update list, since new gazebo-stable.list has been added
sudo apt-get update -y --quiet
# Install Gazebo
gazebo_packages="gz-harmonic libunwind-dev"
# Install Gazebo classic
if [[ "${UBUNTU_RELEASE}" == "18.04" ]]; then
gazebo_classic_version=9
gazebo_packages="gazebo$gazebo_classic_version libgazebo$gazebo_classic_version-dev"
else
# default and Ubuntu 20.04
gazebo_classic_version=11
gazebo_packages="gazebo$gazebo_classic_version libgazebo$gazebo_classic_version-dev"
fi
else
# Expects Ubuntu 22.04 > by default
echo "[ubuntu.sh] Gazebo (Harmonic) will be installed"
echo "[ubuntu.sh] Earlier versions will be removed"
# Add Gazebo binary repository
sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null
sudo apt-get update -y --quiet
if [[ "${UBUNTU_RELEASE}" == "24.04" ]]; then
gazebo_packages="$gazebo_packages cppzmq-dev"
# Install Gazebo
gazebo_packages="gz-harmonic libunwind-dev"
if [[ "${UBUNTU_RELEASE}" == "24.04" ]]; then
gazebo_packages="$gazebo_packages cppzmq-dev"
fi
fi
sudo DEBIAN_FRONTEND=noninteractive apt-get -y --quiet --no-install-recommends install \
-142
View File
@@ -1,142 +0,0 @@
#!/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()
-487
View File
@@ -1,487 +0,0 @@
#!/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
@@ -1,141 +0,0 @@
#!/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
-32
View File
@@ -1,32 +0,0 @@
# 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
@@ -1,485 +0,0 @@
#!/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
@@ -1,2 +0,0 @@
# Staging directory created by build_docker.sh (ephemeral)
_px4_msgs_defs/
-95
View File
@@ -1,95 +0,0 @@
# 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"]
@@ -1,35 +0,0 @@
#!/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"
@@ -1,80 +0,0 @@
"""
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
@@ -1,24 +0,0 @@
<?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
@@ -1,99 +0,0 @@
#!/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
@@ -1,5 +0,0 @@
[develop]
script_dir=$base/lib/swarm_ros2
[install]
install_scripts=$base/lib/swarm_ros2
-29
View File
@@ -1,29 +0,0 @@
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',
],
},
)
@@ -1,599 +0,0 @@
"""
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()
@@ -1,156 +0,0 @@
"""
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 -5
View File
@@ -221,10 +221,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -444,7 +442,7 @@
GPIO_CAN1_RX, \
GPIO_CAN2_TX, \
GPIO_CAN2_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_nPOWER_IN_A, \
GPIO_nPOWER_IN_B, \
GPIO_nPOWER_IN_C, \
+4
View File
@@ -28,8 +28,12 @@ CONFIG_DRIVERS_IMU_INVENSENSE_IIM42653=y
CONFIG_DRIVERS_IMU_MURATA_SCH16T=y
CONFIG_COMMON_LIGHT=y
CONFIG_DRIVERS_MAGNETOMETER_BOSCH_BMM150=y
CONFIG_DRIVERS_MAGNETOMETER_HMC5883=y
CONFIG_DRIVERS_MAGNETOMETER_QMC5883L=y
CONFIG_DRIVERS_MAGNETOMETER_ISENTEK_IST8308=y
CONFIG_DRIVERS_MAGNETOMETER_ISENTEK_IST8310=y
CONFIG_DRIVERS_MAGNETOMETER_LIS3MDL=y
CONFIG_DRIVERS_MAGNETOMETER_LSM303AGR=y
CONFIG_DRIVERS_MAGNETOMETER_RM3100=y
CONFIG_DRIVERS_MAGNETOMETER_ST_IIS2MDC=y
CONFIG_DRIVERS_POWER_MONITOR_INA226=y
+6 -6
View File
@@ -17,21 +17,21 @@ param set-default MAV_2_UDP_PRT 14550
param set-default SENS_EN_INA226 1
param set-default SENS_EN_THERMAL 1
param set-default SENS_IMU_MODE 1
param set-default HEATER1_TEMP 10.0
#param set-default HEATER1_TEMP_FF 0.0
#param set-default HEATER1_TEMP_I 0.025
#param set-default HEATER1_TEMP_P 1.0
param set-default SENS_IMU_TEMP 10.0
#param set-default SENS_IMU_TEMP_FF 0.0
#param set-default SENS_IMU_TEMP_I 0.025
#param set-default SENS_IMU_TEMP_P 1.0
param set-default UAVCAN_ESC_IFACE 2
if ver hwtypecmp ARKV6X000
then
param set-default HEATER1_IMU_ID 2818058
param set-default SENS_TEMP_ID 2818058
fi
if ver hwtypecmp ARKV6X001
then
param set-default HEATER1_IMU_ID 3014666
param set-default SENS_TEMP_ID 3014666
fi
safety_button start
+1 -1
View File
@@ -100,7 +100,7 @@ bmp388 -I start
# Start an external PWM generator
if param greater PCA9685_EN_BUS 0
then
pca9685_pwm_out start -X
pca9685_pwm_out start
fi
unset HAVE_PM2
+3 -5
View File
@@ -224,10 +224,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -447,7 +445,7 @@
GPIO_CAN1_RX, \
GPIO_CAN2_TX, \
GPIO_CAN2_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_nPOWER_IN_A, \
GPIO_nPOWER_IN_B, \
GPIO_nPOWER_IN_C, \
-5
View File
@@ -1,5 +0,0 @@
# CONFIG_BOARD_UAVCAN_TIMER_OVERRIDE is not set
CONFIG_BOARD_CONSTRAINED_FLASH=y
CONFIG_BOARD_CONSTRAINED_MEMORY=y
CONFIG_MODULES_UXRCE_DDS_CLIENT=n
CONFIG_MODULES_ZENOH=y
+5 -7
View File
@@ -15,19 +15,17 @@ fi
# TODO: Tune the following parameters
param set-default SENS_EN_THERMAL 1
param set-default HEATER1_TEMP 10.0
#param set-default HEATER1_TEMP_FF 0.0
#param set-default HEATER1_TEMP_I 0.025
#param set-default HEATER1_TEMP_P 1.0
param set-default SENS_IMU_TEMP 10.0
#param set-default SENS_IMU_TEMP_FF 0.0
#param set-default SENS_IMU_TEMP_I 0.025
#param set-default SENS_IMU_TEMP_P 1.0
if ver hwtypecmp ARKFPV000
then
param set-default HEATER1_IMU_ID 3014666
param set-default SENS_TEMP_ID 3014666
fi
param set-default BAT1_V_DIV 21.0
param set-default BAT1_V_FILT 0.075
param set-default BAT1_I_FILT 0.5
param set-default RC_CRSF_PRT_CFG 300
param set-default RC_SBUS_PRT_CFG 0
+1 -1
View File
@@ -20,5 +20,5 @@ bmp388 -I -b 2 start
# Start an external PWM generator
if param greater PCA9685_EN_BUS 0
then
pca9685_pwm_out start -X
pca9685_pwm_out start
fi
+6 -5
View File
@@ -176,6 +176,9 @@
#define BOARD_BATTERY1_V_DIV (21.0f) // (20k + 1k) / 1k = 21
#define BOARD_BATTERY_ADC_VOLTAGE_FILTER_S 0.075f
#define BOARD_BATTERY_ADC_CURRENT_FILTER_S 0.125f
#define ADC_SCALED_PAYLOAD_SENSE ADC_SCALED_12V_CHANNEL
/* HW has to large of R termination on ADC todo:change when HW value is chosen */
@@ -205,10 +208,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -330,7 +331,7 @@
GPIO_HW_VER_REV_DRIVE, \
GPIO_CAN1_TX, \
GPIO_CAN1_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_VDD_5V_PGOOD, \
GPIO_VDD_12V_PGOOD, \
GPIO_VDD_12V_EN, \
+5 -5
View File
@@ -21,17 +21,17 @@ param set-default SENS_EN_INA226 1
# TODO: Tune the following parameters
param set-default SENS_EN_THERMAL 1
param set-default HEATER1_TEMP 10.0
#param set-default HEATER1_TEMP_FF 0.0
#param set-default HEATER1_TEMP_I 0.025
#param set-default HEATER1_TEMP_P 1.0
param set-default SENS_IMU_TEMP 10.0
#param set-default SENS_IMU_TEMP_FF 0.0
#param set-default SENS_IMU_TEMP_I 0.025
#param set-default SENS_IMU_TEMP_P 1.0
param set-default UAVCAN_ESC_IFACE 1
if ver hwtypecmp ARKPI6X000
then
# TODO: Add the correct sensor ID
param set-default HEATER1_IMU_ID 2490378
param set-default SENS_TEMP_ID 2490378
fi
param set-default EKF2_MULTI_IMU 0
+1 -1
View File
@@ -38,5 +38,5 @@ afbrs50 start
# Start an external PWM generator
if param greater PCA9685_EN_BUS 0
then
pca9685_pwm_out start -X
pca9685_pwm_out start
fi
+3 -5
View File
@@ -188,10 +188,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -335,7 +333,7 @@
GPIO_HW_VER_REV_DRIVE, \
GPIO_CAN1_TX, \
GPIO_CAN1_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_VDD_5V_HIPOWER_nEN, \
GPIO_VDD_5V_HIPOWER_nOC, \
GPIO_VDD_3V3_SD_CARD_EN, \
+2 -4
View File
@@ -121,9 +121,7 @@
#define BOARD_REAR_LED_MASK (1 << 1) | (1 << 2)
/* HEATER */
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PA7 T14CH1 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTA|GPIO_PIN7)
#define GPIO_HEATER_OUTPUT /* PA7 T14CH1 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTA|GPIO_PIN7)
#define BOARD_HAS_LED_PWM 1
#define BOARD_LED_PWM_DRIVE_ACTIVE_LOW 1
@@ -184,7 +182,7 @@
PX4_ADC_GPIO, \
GPIO_HW_REV_DRIVE, \
GPIO_HW_VER_DRIVE, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_VDD_3V3_SD_CARD_EN, \
GPIO_OTGFS_VBUS \
}
-1
View File
@@ -55,7 +55,6 @@ CONFIG_MODULES_HARDFAULT_STREAM=y
CONFIG_MODULES_LAND_DETECTOR=y
CONFIG_MODULES_LANDING_TARGET_ESTIMATOR=y
CONFIG_MODULES_LOAD_MON=y
CONFIG_MODULES_TASK_WATCHDOG=y
CONFIG_MODULES_LOGGER=y
CONFIG_LOGGER_STACK_SIZE=4100
CONFIG_MODULES_MAG_BIAS_ESTIMATOR=y
@@ -34,8 +34,5 @@ nshterm /dev/ttyS3 &
# Start the time_persistor to cyclically store the RTC in FRAM
time_persistor start
# Start the task_watchdog as we do not have the logger watchdog
task_watchdog start
# Start the ESC telemetry
dshot telemetry -d /dev/ttyS5 -x
@@ -77,5 +77,5 @@ ist8310 -X -b 1 -R 10 start
# Start an external PWM generator
if param greater PCA9685_EN_BUS 0
then
pca9685_pwm_out start -X
pca9685_pwm_out start
fi
+3 -5
View File
@@ -224,10 +224,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -445,7 +443,7 @@
GPIO_CAN1_RX, \
GPIO_CAN2_TX, \
GPIO_CAN2_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_nPOWER_IN_A, \
GPIO_nPOWER_IN_B, \
GPIO_nPOWER_IN_C, \
@@ -28,7 +28,7 @@ then
echo "ads1115 not found."
fi
if ! pca9685_pwm_out start -X
if ! pca9685_pwm_out start
then
echo "pca9685_pwm_out not found."
fi
+2 -2
View File
@@ -66,15 +66,15 @@ then
fi
fi
iim42652 -R 6 -s -C 32768 start
bmi088 -A -R 4 -s start
bmi088 -G -R 4 -s start
iim42652 -R 6 -s -C 32768 start
icm45686 -R 2 -s start
rm3100 -I -b 4 start
bmp581 -b 2 -X -a 0x47 start
icp201xx -I -a 0x64 start
bmp581 -b 2 -X -a 0x47 start
# External compass on GPS1/I2C1 (the 3rd external bus): standard Holybro Pixhawk 4 or CUAV V5 GPS/compass puck (with lights, safety button, and buzzer)
ist8310 -X -b 1 -R 10 start
+3 -5
View File
@@ -230,10 +230,8 @@
/* HEATER
* PWM in future
*/
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PB10 T2CH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN10)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PE6 is nARMED
* The GPIO will be set as input while not armed HW will have external HW Pull UP.
@@ -455,7 +453,7 @@
GPIO_CAN1_RX, \
GPIO_CAN2_TX, \
GPIO_CAN2_RX, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_nPOWER_IN_A, \
GPIO_nPOWER_IN_B, \
GPIO_nPOWER_IN_C, \
+5 -7
View File
@@ -104,10 +104,8 @@
#define GPIO_CAN2_SILENT_S1 /* PH3 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTH|GPIO_PIN3)
/* HEATER */
#define GPIO_HEATER_OUTPUT
#define HEATER_NUM 1
#define GPIO_HEATER1_OUTPUT /* PA8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTA|GPIO_PIN8)
#define HEATER1_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER1_OUTPUT, (on_true))
#define GPIO_HEATER_OUTPUT /* PA8 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTA|GPIO_PIN8)
#define HEATER_OUTPUT_EN(on_true) px4_arch_gpiowrite(GPIO_HEATER_OUTPUT, (on_true))
/* PWM */
#define DIRECT_PWM_OUTPUT_CHANNELS 14
@@ -129,8 +127,8 @@
#define GPIO_VDD_5V_RC_EN /* PG5 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN5)
#define GPIO_VDD_3V3_SD_CARD_EN /* PG7 */ (GPIO_OUTPUT|GPIO_PUSHPULL|GPIO_SPEED_2MHz|GPIO_OUTPUT_CLEAR|GPIO_PORTG|GPIO_PIN7)
#define GPIO_VDD_5V_HIPOWER_OC /* PJ3 */ (GPIO_INPUT|GPIO_PULLUP|GPIO_PORTJ|GPIO_PIN3)
#define GPIO_nVDD_5V_PERIPH_OC /* PJ4 */ (GPIO_INPUT|GPIO_PULLUP|GPIO_PORTJ|GPIO_PIN4)
#define GPIO_VDD_5V_HIPOWER_OC /* PJ3 */ (GPIO_INPUT|GPIO_FLOAT|GPIO_PORTJ|GPIO_PIN3)
#define GPIO_nVDD_5V_PERIPH_OC /* PJ4 */ (GPIO_INPUT|GPIO_FLOAT|GPIO_PORTJ|GPIO_PIN4)
/* Power switch controls ******************************************************/
#define VDD_5V_PERIPH_EN(on_true) px4_arch_gpiowrite(GPIO_nVDD_5V_PERIPH_EN, (on_true))
@@ -214,7 +212,7 @@
GPIO_CAN2_RX, \
GPIO_CAN1_SILENT_S0, \
GPIO_CAN2_SILENT_S1, \
GPIO_HEATER1_OUTPUT, \
GPIO_HEATER_OUTPUT, \
GPIO_nPOWER_IN_CAN, \
GPIO_nPOWER_IN_ADC, \
GPIO_nPOWER_IN_C, \
+1
View File
@@ -31,4 +31,5 @@
#
############################################################################
add_subdirectory(core_heater)
add_subdirectory(pwm_voltage)
@@ -1,6 +1,6 @@
############################################################################
#
# Copyright (c) 2026 PX4 Development Team. All rights reserved.
# Copyright (c) 2025 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
@@ -30,12 +30,10 @@
# POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
px4_add_module(
MODULE modules__task_watchdog
MAIN task_watchdog
MODULE drivers__core_heater
MAIN core_heater
COMPILE_FLAGS
SRCS
TaskWatchdog.cpp
DEPENDS
version
)
core_heater.cpp
)
@@ -0,0 +1,261 @@
/****************************************************************************
*
* Copyright (c) 2025 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.
*
****************************************************************************/
/**
* @file core_heater.cpp
*
*/
#include "core_heater.h"
#include <px4_platform_common/getopt.h>
#include <px4_platform_common/log.h>
#include <drivers/drv_hrt.h>
#include <drivers/drv_io_heater.h>
# ifndef GPIO_CORE_HEATER_OUTPUT
# error "To use the heater driver, the board_config.h must define and initialize GPIO_CORE_HEATER_OUTPUT"
# endif
Core_Heater::Core_Heater() :
ModuleParams(nullptr),
ScheduledWorkItem(MODULE_NAME, px4::wq_configurations::lp_default)
{
_heater_status_pub.advertise();
}
Core_Heater::~Core_Heater()
{
disable_core_heater();
}
int Core_Heater::custom_command(int argc, char *argv[])
{
// Check if the driver is running.
if (!is_running()) {
PX4_INFO("not running");
return PX4_ERROR;
}
return print_usage("Unrecognized command.");
}
void Core_Heater::disable_core_heater()
{
// Reset heater to off state.
px4_arch_unconfiggpio(GPIO_CORE_HEATER_OUTPUT);
}
void Core_Heater::initialize_core_heater_io()
{
// Initialize heater to off state.
px4_arch_configgpio(GPIO_CORE_HEATER_OUTPUT);
}
void Core_Heater::core_heater_off()
{
CORE_HEATER_OUTPUT_EN(false);
}
void Core_Heater::core_heater_on()
{
CORE_HEATER_OUTPUT_EN(true);
}
bool Core_Heater::initialize_topics()
{
for (uint8_t i = 0; i < ORB_MULTI_MAX_INSTANCES; i++) {
uORB::SubscriptionData<sensor_accel_s> sensor_accel_sub{ORB_ID(sensor_accel), i};
if (sensor_accel_sub.get().timestamp != 0 &&
sensor_accel_sub.get().device_id != 0 &&
PX4_ISFINITE(sensor_accel_sub.get().temperature)) {
// If the correct ID is found, exit the for-loop with _sensor_accel_sub pointing to the correct instance.
if (sensor_accel_sub.get().device_id == (uint32_t)_param_core_temp_id.get()) {
_sensor_accel_sub.ChangeInstance(i);
_sensor_device_id = sensor_accel_sub.get().device_id;
initialize_core_heater_io();
return true;
}
}
}
return false;
}
void Core_Heater::Run()
{
if (should_exit()) {
exit_and_cleanup();
return;
}
update_params();
if (_sensor_device_id == 0) {
if (!initialize_topics()) {
// if sensor still not found try again in 1 second
ScheduleDelayed(1_s);
return;
}
}
sensor_accel_s sensor_accel;
float temperature_delta {0.f};
if (_core_heater_on) {
// Turn the heater off.
_core_heater_on = false;
core_heater_off();
ScheduleDelayed(_controller_period_usec - _controller_time_on_usec);
} else if (_sensor_accel_sub.update(&sensor_accel)) {
// Update the current IMU sensor temperature if valid.
if (PX4_ISFINITE(sensor_accel.temperature)) {
temperature_delta = _param_core_imu_temp.get() - sensor_accel.temperature;
_temperature_last = sensor_accel.temperature;
}
_proportional_value = temperature_delta * _param_core_imu_temp_p.get();
_integrator_value += temperature_delta * _param_core_imu_temp_i.get();
_integrator_value = math::constrain(_integrator_value, -0.25f, 0.25f);
_controller_time_on_usec = static_cast<int>((_param_core_imu_temp_ff.get() + _proportional_value +
_integrator_value) * static_cast<float>(_controller_period_usec));
_controller_time_on_usec = math::constrain(_controller_time_on_usec, 0, _controller_period_usec);
if (fabsf(temperature_delta) < TEMPERATURE_TARGET_THRESHOLD) {
_temperature_target_met = true;
} else {
_temperature_target_met = false;
}
_core_heater_on = true;
core_heater_on();
ScheduleDelayed(_controller_time_on_usec);
}
publish_status();
}
void Core_Heater::publish_status()
{
heater_status_s status{};
status.device_id = _sensor_device_id;
status.heater_on = _core_heater_on;
status.temperature_sensor = _temperature_last;
status.temperature_target = _param_core_imu_temp.get();
status.temperature_target_met = _temperature_target_met;
status.controller_period_usec = _controller_period_usec;
status.controller_time_on_usec = _controller_time_on_usec;
status.proportional_value = _proportional_value;
status.integrator_value = _integrator_value;
status.feed_forward_value = _param_core_imu_temp_ff.get();
status.mode = heater_status_s::MODE_GPIO;
status.timestamp = hrt_absolute_time();
_heater_status_pub.publish(status);
}
int Core_Heater::start()
{
// Exit the driver if the sensor ID does not match the desired sensor.
if (_param_core_temp_id.get() == 0) {
PX4_ERR("Valid CORE_TEMP_ID required");
request_stop();
return PX4_ERROR;
}
update_params(true);
ScheduleNow();
return PX4_OK;
}
int Core_Heater::task_spawn(int argc, char *argv[])
{
Core_Heater *core_heater = new Core_Heater();
if (!core_heater) {
PX4_ERR("driver allocation failed");
return PX4_ERROR;
}
_object.store(core_heater);
_task_id = task_id_is_work_queue;
core_heater->start();
return 0;
}
void Core_Heater::update_params(const bool force)
{
if (_parameter_update_sub.updated() || force) {
// clear update
parameter_update_s param_update;
_parameter_update_sub.copy(&param_update);
// update parameters from storage
ModuleParams::updateParams();
}
}
int Core_Heater::print_usage(const char *reason)
{
if (reason) {
printf("%s\n\n", reason);
}
PRINT_MODULE_DESCRIPTION(
R"DESCR_STR(
### Description
Background process running periodically on the LP work queue to regulate IMU temperature at a setpoint.
)DESCR_STR");
PRINT_MODULE_USAGE_NAME("core_heater", "system");
PRINT_MODULE_USAGE_COMMAND("start");
PRINT_MODULE_USAGE_DEFAULT_COMMANDS();
return 0;
}
extern "C" __EXPORT int core_heater_main(int argc, char *argv[])
{
return Core_Heater::main(argc, argv);
}

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