diff --git a/.dockerignore b/.dockerignore
index 371dcb14d3b9..3698d7ab4aac 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,15 +1,81 @@
-# Ignore Git repository files
+# ==============================================================================
+# Docker Build Context Exclusions
+# ==============================================================================
+
+# Git
.git
.gitignore
.gitattributes
+.gitmodules
+.mailmap
-# Ignore node_modules or other build artifacts
-node_modules
-build
-
-# Ignore Docker-related files
-Dockerfile*
-docker-compose.yml
+# GitHub (except build-config.json needed by Dockerfile)
+.github/
+!.github/build-config.json
+# Documentation (not needed for build)
+docs/
README.md
+CHANGELOG.md
+LICENSE-*
+
+# Build artifacts
+build/
+cmake-build-*/
+out/
+cpm_modules/
+compile_commands.json
CMakeLists.txt.user*
+CMakeUserPresets.json
+
+# IDEs & Editors
+.idea/
+.vscode/
+.qtcreator/
+.fleet/
+.vs/
+.settings/
+*.swp
+*~
+tags
+
+# Node.js (docs build)
+node_modules/
+package-lock.json
+
+# Python
+*.pyc
+__pycache__/
+.ruff_cache/
+
+# Local development
+.claude/
+.cache/
+sessions/
+CLAUDE.md
+AGENTS.md
+GEMINI.md
+
+# Platform artifacts
+.DS_Store
+*.exe
+*.dll
+*.so
+*.dylib
+*.app
+*.dmg
+*.deb
+*.rpm
+*.AppImage
+*.apk
+*.aab
+*.ipa
+
+# Test artifacts
+*.gcov
+*.gcda
+*.gcno
+*.coverage
+
+# Vagrant
+.vagrant/
diff --git a/.github/CITATION.cff b/.github/CITATION.cff
new file mode 100644
index 000000000000..cd479d33ba00
--- /dev/null
+++ b/.github/CITATION.cff
@@ -0,0 +1,25 @@
+cff-version: 1.2.0
+title: QGroundControl
+message: >-
+ If you use QGroundControl in your research, please cite it using
+ the metadata from this file.
+type: software
+authors:
+ - name: QGroundControl Development Team
+ website: https://qgroundcontrol.com
+repository-code: https://github.com/mavlink/qgroundcontrol
+url: https://qgroundcontrol.com
+abstract: >-
+ QGroundControl is an intuitive and powerful ground control station
+ for UAVs. It provides full flight control and mission planning for
+ any MAVLink-enabled drone, including PX4 and ArduPilot vehicles.
+keywords:
+ - ground-control-station
+ - uav
+ - drone
+ - mavlink
+ - px4
+ - ardupilot
+ - flight-planning
+ - telemetry
+license: Apache-2.0 AND GPL-3.0-only
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000000..65c607ee5ac2
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,15 @@
+# CODEOWNERS - Auto-assign reviewers to pull requests
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+
+# Default owners for everything
+# * @mavlink/qgc-maintainers
+
+# Documentation
+/docs/ @hamishwillee
+
+# QML/UI
+*.qml @DonLakeFlyer
+
+# Build system
+CMakeLists.txt @HTRamsey
+/cmake/ @HTRamsey
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index ff794fcc9826..7192165eac94 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -21,7 +21,7 @@ Before you begin, please:
1. Read the [Developer Guide](https://dev.qgroundcontrol.com/en/)
2. Review the [Build Instructions](https://dev.qgroundcontrol.com/en/getting_started/)
-3. Familiarize yourself with the [Architecture](copilot-instructions.md)
+3. Familiarize yourself with the [Architecture Patterns](#architecture-patterns) in this guide
### Development Environment
@@ -59,6 +59,10 @@ Feature requests are welcome! Please:
3. Consider implementation complexity
4. Be prepared to contribute code if possible
+### Contributing Translations
+
+QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for community translations. See [tools/translations/README.md](../tools/translations/README.md) for details on how translations are managed.
+
### Contributing Code
1. **Fork the repository**
@@ -96,6 +100,8 @@ Feature requests are welcome! Please:
## Coding Standards
+For the complete coding style guide with examples, see [CODING_STYLE.md](../CODING_STYLE.md).
+
### C++ Guidelines
- **Standard**: C++20
@@ -126,7 +132,8 @@ Feature requests are welcome! Please:
- **Code formatting**:
- Run `clang-format` before committing
- - Follow `.clang-format` in the repository
+ - Follow `.clang-format`, `.clang-tidy`, `.editorconfig` in repo root
+ - See `CodingStyle.h`, `CodingStyle.cc`, `CodingStyle.qml` for examples
- 4 spaces for indentation (no tabs)
### QML Guidelines
@@ -151,36 +158,66 @@ qCCritical(MyComponentLog) << "Critical error";
### Architecture Patterns
-#### Fact System (Required for Parameters)
+#### Fact System (Most Important!)
-Always use the Fact System for vehicle parameters:
+The Fact System handles ALL vehicle parameters. Never create custom parameter storage.
```cpp
+// Access parameters (always null-check!)
Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME");
-if (param) {
- param->setCookedValue(newValue); // For display values
- // param->setRawValue(newValue); // For MAVLink values
+if (param && param->validate(newValue, false).isEmpty()) {
+ param->setCookedValue(newValue); // Use cookedValue for UI (with units)
+ // param->rawValue() for MAVLink/storage
}
```
-#### Multi-Vehicle Awareness
+**Key classes:**
+- `Fact` - Single parameter with validation, units, metadata
+- `FactGroup` - Hierarchical container (handles MAVLink via `handleMessage()`)
+- `FactMetaData` - JSON-based metadata (min/max, enums, descriptions)
+
+**Rules:**
+- Wait for `parametersReady` signal before accessing
+- Use `cookedValue` (display) vs `rawValue` (storage)
+- Metadata in `*.FactMetaData.json` files
-Always check for null vehicles:
+#### Multi-Vehicle Support
+
+Always null-check the active vehicle:
```cpp
Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle();
-if (vehicle) {
- // Use vehicle
-}
+if (!vehicle) return;
+
+// Other managers
+SettingsManager::instance()->appSettings()->...
+LinkManager::instance()->...
```
#### Firmware Plugin System
-Use FirmwarePlugin for firmware-specific behavior instead of hardcoding:
+Use FirmwarePlugin for firmware-specific behavior:
```cpp
-vehicle->firmwarePlugin()->isCapable(capability);
+// FirmwarePlugin - Firmware behavior (flight modes, capabilities)
vehicle->firmwarePlugin()->flightModes();
+vehicle->firmwarePlugin()->isCapable(capability);
+
+// AutoPilotPlugin - Vehicle setup UI
+// VehicleComponent - Individual setup items (Radio, Sensors, Safety)
+```
+
+#### QML/C++ Integration
+
+```cpp
+Q_OBJECT
+QML_ELEMENT // Creatable in QML
+QML_SINGLETON // Singleton
+QML_UNCREATABLE("") // C++-only
+
+Q_PROPERTY(Type name READ getter WRITE setter NOTIFY signal)
+Q_INVOKABLE void method();
+Q_ENUM(EnumType)
```
---
@@ -308,7 +345,6 @@ For more details, see [COPYING.md](COPYING.md).
- **User Manual**: https://docs.qgroundcontrol.com/en/
- **Developer Guide**: https://dev.qgroundcontrol.com/en/
-- **API Documentation**: In-code documentation and `copilot-instructions.md`
- **Support Guide**: For help and community resources, see [SUPPORT.md](SUPPORT.md)
- **Discussion Forum**: https://discuss.px4.io/c/qgroundcontrol
- **Discord**: https://discord.gg/dronecode
diff --git a/.github/DISCUSSION_TEMPLATE/polls.yml b/.github/DISCUSSION_TEMPLATE/polls.yml
new file mode 100644
index 000000000000..7bfd1855c961
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/polls.yml
@@ -0,0 +1,18 @@
+title: "[Poll] "
+labels: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Create a poll to gather community feedback. After creating this discussion, use the poll button in the editor to add your poll options.
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Poll Context
+ description: Provide background information for your poll
+ placeholder: |
+ What decision or feedback are you seeking?
+ Why is this important to the community?
+ validations:
+ required: true
diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml
new file mode 100644
index 000000000000..0c6348d8e73d
--- /dev/null
+++ b/.github/DISCUSSION_TEMPLATE/q-a.yml
@@ -0,0 +1,86 @@
+title: "[Q&A] "
+labels: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for your question! Please check the following resources first:
+ - [User Guide](https://docs.qgroundcontrol.com/)
+ - [Developer Guide](https://dev.qgroundcontrol.com/)
+ - [Existing discussions](https://github.com/mavlink/qgroundcontrol/discussions)
+
+ - type: dropdown
+ id: category
+ attributes:
+ label: Question Category
+ description: What area does your question relate to?
+ options:
+ - Setup / Installation
+ - Vehicle Connection
+ - Flight Planning / Missions
+ - Video Streaming
+ - Telemetry / Parameters
+ - Custom Builds
+ - Development / Building from Source
+ - Other
+ validations:
+ required: true
+
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Platform
+ description: What platform are you using?
+ options:
+ - Windows
+ - macOS
+ - Linux
+ - Android
+ - iOS
+ - Multiple / N/A
+ validations:
+ required: true
+
+ - type: dropdown
+ id: firmware
+ attributes:
+ label: Flight Stack
+ description: What firmware are you using?
+ options:
+ - PX4
+ - ArduPilot
+ - Both / Either
+ - N/A
+ validations:
+ required: false
+
+ - type: input
+ id: version
+ attributes:
+ label: QGC Version
+ description: Which version of QGroundControl?
+ placeholder: "e.g., 4.4.0, daily build"
+ validations:
+ required: false
+
+ - type: textarea
+ id: question
+ attributes:
+ label: Question
+ description: Describe your question in detail
+ placeholder: |
+ What I'm trying to do:
+
+ What I've tried:
+
+ What I expected vs what happened:
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: Any additional information, screenshots, or logs
+ validations:
+ required: false
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000000..537969bcaf4f
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,4 @@
+# Funding links displayed on repository sidebar
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
+
+custom: ['https://www.dronecode.org']
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ef52ed76da6f..a81ac38a42a5 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,6 +1,6 @@
name: Bug report
description: Create a report to help us improve
-labels: ["Report: Bug"]
+labels: ["bug"]
type: bug
body:
@@ -30,7 +30,7 @@ body:
1. **Expected Behavior** – What you expected to happen.
2. **Current Behavior** – What actually happened instead.
3. **Steps To Reproduce** – Step-by-step list of actions to reproduce the issue.
- 3. **Additional Details** – Any other context that helps explain the problem.
+ 4. **Additional Details** – Any other context that helps explain the problem.
value: |
**Expected Behavior**
Describe what you thought should happen here.
@@ -48,6 +48,31 @@ body:
validations:
required: true
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Platform
+ description: Which platform are you running QGC on?
+ options:
+ - Windows
+ - macOS
+ - Linux
+ - Android
+ - iOS
+ validations:
+ required: true
+
+ - type: dropdown
+ id: flight-stack
+ attributes:
+ label: Flight Stack
+ description: Which flight stack are you using?
+ options:
+ - PX4
+ - ArduPilot
+ - Other
+ - N/A
+
- type: textarea
id: system-information
attributes:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 791982fa7e52..ddae3345f48d 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,20 @@
blank_issues_enabled: true
contact_links:
+ - name: "❓ Questions & Help"
+ url: https://github.com/mavlink/qgroundcontrol/discussions/categories/q-a
+ about: "Ask questions in GitHub Discussions - please don't open issues for support questions."
- name: "💬 Developer Chat"
url: https://discord.gg/dronecode
- about: "QGroundControl developers (and many regular/deeply-involved users) can be found on the #QGroundControl channel on the Dronecode Discord."
+ about: "Chat with developers on the #QGroundControl channel on Dronecode Discord."
+ - name: "📖 User Guide"
+ url: https://docs.qgroundcontrol.com/
+ about: "Check the QGC User Guide for usage questions and troubleshooting."
+ - name: "🛠️ Developer Guide"
+ url: https://dev.qgroundcontrol.com/
+ about: "Developer documentation for building and contributing to QGC."
+ - name: "💬 PX4 Forum"
+ url: https://discuss.px4.io/c/qgroundcontrol/15
+ about: "Community discussion forum for QGC topics."
+ - name: "🔍 Search Existing Issues"
+ url: https://github.com/mavlink/qgroundcontrol/issues?q=is%3Aissue
+ about: "Search for existing issues before creating a new one."
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 7c29789ac7d0..0cb1cdc35796 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,6 +1,6 @@
name: Feature request
description: Tell us about your new idea
-labels: ["Feature Request"]
+labels: ["enhancement"]
type: feature
body:
@@ -22,6 +22,13 @@ body:
validations:
required: true
+ - type: textarea
+ id: use-case
+ attributes:
+ label: Use Case
+ description: What problem does this feature solve? Describe the scenario where this would be useful.
+ placeholder: "As a user, I want to... so that I can..."
+
- type: dropdown
id: flight-stacks
attributes:
@@ -42,4 +49,20 @@ body:
- Multirotor
- Fixed-wing
- VTOL
+ - Rover
+ - Boat
- Submarine
+
+ - type: dropdown
+ id: platforms
+ attributes:
+ label: Platforms
+ description: Select the platforms this feature should work on.
+ multiple: true
+ options:
+ - Windows
+ - macOS
+ - Linux
+ - Android
+ - iOS
+ - All
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index 40a3ec505eaa..404d9b665b1d 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -1,6 +1,7 @@
name: Question
description: Ask a question related to QGC source
-labels: ["Question"]
+labels: ["question"]
+type: question
body:
- type: markdown
@@ -11,9 +12,27 @@ body:
- If your question still isn't answered, please check the forums: https://discuss.px4.io/c/qgroundcontrol/15.
- If it is about Qt or any QGC dependencies, please refer to them instead.
+ **Note:** For more involved discussions or design questions, please use [GitHub Discussions](https://github.com/mavlink/qgroundcontrol/discussions) instead.
+
+ - type: dropdown
+ id: area
+ attributes:
+ label: Area
+ description: What area of QGC is your question about?
+ options:
+ - UI/QML
+ - Video Streaming
+ - MAVLink/Communication
+ - Vehicle/Flight Stack
+ - Build System
+ - Testing
+ - Other
+
- type: textarea
id: question
attributes:
label: Your Question
description: Write your question related to the QGC source code.
placeholder: Enter your question here.
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml
new file mode 100644
index 000000000000..3bcbdbea9a97
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/task.yml
@@ -0,0 +1,13 @@
+name: Task
+description: Record a task for maintainers
+labels: ["task"]
+type: task
+
+body:
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: What needs to be done?
+ validations:
+ required: true
diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md
new file mode 100644
index 000000000000..9441431f6d5a
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md
@@ -0,0 +1,39 @@
+## Bug Description
+
+
+Fixes #
+
+## Root Cause
+
+
+
+## Solution
+
+
+
+## Testing
+- [ ] Tested locally
+- [ ] Added regression test
+- [ ] Tested with simulator (SITL)
+- [ ] Tested with hardware
+
+### Platforms Tested
+- [ ] Linux
+- [ ] Windows
+- [ ] macOS
+- [ ] Android
+- [ ] iOS
+
+### Flight Stacks Tested
+- [ ] PX4
+- [ ] ArduPilot
+- [ ] N/A
+
+## Checklist
+- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] My code follows the project's coding standards
+- [ ] I have added a test that reproduces the bug
+- [ ] New and existing unit tests pass locally
+
+---
+By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).
diff --git a/.github/PULL_REQUEST_TEMPLATE/ci.md b/.github/PULL_REQUEST_TEMPLATE/ci.md
new file mode 100644
index 000000000000..f2a1a38aa58d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/ci.md
@@ -0,0 +1,24 @@
+## CI/Build Changes
+
+
+
+## Reason
+
+
+
+## Testing
+- [ ] Tested workflow locally (act or similar)
+- [ ] Verified YAML syntax
+- [ ] Tested on fork before submitting
+
+## Impact
+
+
+
+## Checklist
+- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] Workflow permissions follow least-privilege principle
+- [ ] No secrets are exposed in logs
+
+---
+By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).
diff --git a/.github/PULL_REQUEST_TEMPLATE/docs.md b/.github/PULL_REQUEST_TEMPLATE/docs.md
new file mode 100644
index 000000000000..a7e4800801cc
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/docs.md
@@ -0,0 +1,20 @@
+## Documentation Changes
+
+
+
+## Reason
+
+
+
+## Checklist
+- [ ] I have checked for spelling/grammar errors
+- [ ] Links are valid and working
+- [ ] Screenshots are up to date (if applicable)
+- [ ] I have previewed the changes locally
+
+## Related Issues
+
+
+
+---
+By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).
diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md
new file mode 100644
index 000000000000..2a9ecbd6e1d6
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/feature.md
@@ -0,0 +1,41 @@
+## Feature Description
+
+
+
+## Implementation Details
+
+
+
+## Testing
+- [ ] Tested locally
+- [ ] Added/updated unit tests
+- [ ] Tested with simulator (SITL)
+- [ ] Tested with hardware
+
+### Platforms Tested
+- [ ] Linux
+- [ ] Windows
+- [ ] macOS
+- [ ] Android
+- [ ] iOS
+
+### Flight Stacks Tested
+- [ ] PX4
+- [ ] ArduPilot
+
+## Screenshots / Demo
+
+
+
+## Checklist
+- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] My code follows the project's coding standards
+- [ ] I have added tests that prove my feature works
+- [ ] New and existing unit tests pass locally
+
+## Related Issues
+
+
+
+---
+By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).
diff --git a/.github/PULL_REQUEST_TEMPLATE/maintainer.md b/.github/PULL_REQUEST_TEMPLATE/maintainer.md
new file mode 100644
index 000000000000..d94fb27084b6
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/maintainer.md
@@ -0,0 +1,6 @@
+## Summary
+
+
+
+## Related Issues
+
diff --git a/.github/SECURITY.md b/.github/SECURITY.md
index 572629861a8b..24d78e216736 100644
--- a/.github/SECURITY.md
+++ b/.github/SECURITY.md
@@ -6,9 +6,8 @@ The following is a list of versions the development team is currently supporting
| Version | Supported |
| ------- | ------------------ |
-| 5.0 | :white_check_mark: |
-| 4.4.x | :white_check_mark: |
-| < 4.4 | :x: |
+| 5.0.x | :white_check_mark: |
+| < 5.0 | :x: |
## Reporting a Vulnerability
diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md
index 5b140f13f73f..1e387ed91ec5 100644
--- a/.github/SUPPORT.md
+++ b/.github/SUPPORT.md
@@ -7,8 +7,8 @@ Welcome to the QGroundControl support guide. This document provides information
## 1. Getting Started
- **Documentation**: Comprehensive user and developer guides are available on the official website:
- - User Guide: https://docs.qgroundcontrol.com/master/en/qgc-user-guide/
- - Developer Guide: https://docs.qgroundcontrol.com/master/en/qgc-dev-guide/
+ - User Guide: https://docs.qgroundcontrol.com/
+ - Developer Guide: https://dev.qgroundcontrol.com/
---
@@ -51,22 +51,20 @@ We welcome contributions of all kinds:
- **Source Repository**: https://github.com/mavlink/qgroundcontrol
- **Contribution Guide**: https://dev.qgroundcontrol.com/en/contribute/contribute.html
-- **Coding Standards**: Follow Qt 6 and C++17 guidelines; see `.clang-format`, `.pre-commit-config.yaml`, and CodingStyle files.
+- **Coding Standards**: Follow Qt 6 and C++20 guidelines; see `.clang-format` and `.pre-commit-config.yaml`.
- **Pull Requests**: Ensure all CI checks pass and include relevant test coverage.
---
## 6. Security and Privacy
-To report security vulnerabilities or privacy concerns:
-
-- Submit an issue and include a detailed description and any proof-of-concept code.
+To report security vulnerabilities, please use GitHub's Security tab to privately report the issue. See [SECURITY.md](SECURITY.md) for details.
---
## 7. License
-QGroundControl is licensed under the GNU General Public License v3.0. See [LICENSE](https://github.com/mavlink/qgroundcontrol/blob/master/LICENSE) for details.
+QGroundControl is dual-licensed under the Apache License 2.0 and the GNU General Public License v3. See [COPYING.md](COPYING.md) for details.
---
diff --git a/.github/actions/build-action/action.yml b/.github/actions/build-action/action.yml
index 9f7f22c4f54b..700186f96d62 100644
--- a/.github/actions/build-action/action.yml
+++ b/.github/actions/build-action/action.yml
@@ -11,13 +11,13 @@ inputs:
runs:
using: composite
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
with:
repository: ${{ inputs.repo }}
ref: ${{ inputs.ref }}
path: build-action
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
diff --git a/.github/actions/build-config/action.yml b/.github/actions/build-config/action.yml
new file mode 100644
index 000000000000..dd04ab5ea4cd
--- /dev/null
+++ b/.github/actions/build-config/action.yml
@@ -0,0 +1,66 @@
+name: Build Config
+description: Read build toolchain versions from central config
+outputs:
+ qt_version:
+ description: Qt version
+ value: ${{ steps.read.outputs.qt_version }}
+ qt_modules:
+ description: Qt modules (desktop)
+ value: ${{ steps.read.outputs.qt_modules }}
+ qt_modules_ios:
+ description: Qt modules (iOS) - excludes unsupported modules
+ value: ${{ steps.read.outputs.qt_modules_ios }}
+ gstreamer_version:
+ description: GStreamer version
+ value: ${{ steps.read.outputs.gstreamer_version }}
+ xcode_version:
+ description: Xcode version for macOS/iOS builds
+ value: ${{ steps.read.outputs.xcode_version }}
+ ndk_version:
+ description: Android NDK version
+ value: ${{ steps.read.outputs.ndk_version }}
+ java_version:
+ description: Java version for Android builds
+ value: ${{ steps.read.outputs.java_version }}
+ android_platform:
+ description: Android platform/API level
+ value: ${{ steps.read.outputs.android_platform }}
+ ccache_version:
+ description: ccache version
+ value: ${{ steps.read.outputs.ccache_version }}
+runs:
+ using: composite
+ steps:
+ - name: Validate build config
+ shell: bash
+ run: |
+ CONFIG_FILE="${GITHUB_WORKSPACE}/.github/build-config.json"
+ if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
+ echo "::error::Invalid JSON in $CONFIG_FILE"
+ exit 1
+ fi
+
+ - name: Read build config
+ id: read
+ shell: bash
+ run: |
+ CONFIG_FILE="${GITHUB_WORKSPACE}/.github/build-config.json"
+ QT_VERSION=$(jq -r '.qt_version' "$CONFIG_FILE")
+ QT_MODULES=$(jq -r '.qt_modules' "$CONFIG_FILE")
+ GST_VERSION=$(jq -r '.gstreamer_version // "1.22.12"' "$CONFIG_FILE")
+ XCODE_VERSION=$(jq -r '.xcode_version // "latest-stable"' "$CONFIG_FILE")
+ NDK_VERSION=$(jq -r '.ndk_version // "r27c"' "$CONFIG_FILE")
+ JAVA_VERSION=$(jq -r '.java_version // "17"' "$CONFIG_FILE")
+ ANDROID_PLATFORM=$(jq -r '.android_platform // "35"' "$CONFIG_FILE")
+ CCACHE_VERSION=$(jq -r '.ccache_version // "4.11.3"' "$CONFIG_FILE")
+ # Filter out modules not supported on iOS (no serial port support)
+ QT_MODULES_IOS=$(echo "$QT_MODULES" | sed 's/qtserialport//g; s/ */ /g; s/^ *//; s/ *$//')
+ echo "qt_version=$QT_VERSION" >> $GITHUB_OUTPUT
+ echo "qt_modules=$QT_MODULES" >> $GITHUB_OUTPUT
+ echo "qt_modules_ios=$QT_MODULES_IOS" >> $GITHUB_OUTPUT
+ echo "gstreamer_version=$GST_VERSION" >> $GITHUB_OUTPUT
+ echo "xcode_version=$XCODE_VERSION" >> $GITHUB_OUTPUT
+ echo "ndk_version=$NDK_VERSION" >> $GITHUB_OUTPUT
+ echo "java_version=$JAVA_VERSION" >> $GITHUB_OUTPUT
+ echo "android_platform=$ANDROID_PLATFORM" >> $GITHUB_OUTPUT
+ echo "ccache_version=$CCACHE_VERSION" >> $GITHUB_OUTPUT
diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml
index f846bfa8cb22..28e9a26f8cf9 100644
--- a/.github/actions/cache/action.yml
+++ b/.github/actions/cache/action.yml
@@ -10,61 +10,64 @@ inputs:
build-type:
description: Build Type
required: true
- type: choice
- options:
- - Debug
- - Release
cpm-modules:
description: Path to CPM Modules
required: false
ccache-version:
- description: ccache Version to Install
+ description: ccache version (use build-config.outputs.ccache_version)
required: false
- default: 4.11.3
+ default: '4.11.3'
+ ccache-max-size:
+ description: Maximum ccache size
+ required: false
+ default: '2G'
+ save-cache:
+ description: Save cache (set to false for PR builds to save storage)
+ required: false
+ default: 'true'
runs:
using: composite
steps:
+ - name: Cache ccache binary (Linux)
+ if: runner.os == 'Linux' && inputs.target != 'linux_gcc_arm64'
+ id: ccache-binary
+ uses: actions/cache@v4
+ with:
+ path: /usr/local/bin/ccache
+ key: ccache-binary-${{ inputs.ccache-version }}-linux-x86_64
+
- name: Install ccache (Linux)
- if: inputs.host == 'linux' && inputs.target != 'linux_gcc_arm64'
+ if: runner.os == 'Linux' && inputs.target != 'linux_gcc_arm64' && steps.ccache-binary.outputs.cache-hit != 'true'
shell: bash
run: |
set -e
- echo "Downloading ccache..."
wget --quiet https://github.com/ccache/ccache/releases/download/v${{ inputs.ccache-version }}/ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz
- echo "Extracting archive..."
- tar -xvf ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz
- cd ccache-${{ inputs.ccache-version }}-linux-x86_64
- echo "Installing ccache..."
- sudo make install
-
- # - name: Setup sccache (Windows)
- # if: runner.os == 'Windows' && inputs.target != 'android'
- # uses: mozilla-actions/sccache-action@v0.0.9
+ tar -xf ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz
+ sudo make -C ccache-${{ inputs.ccache-version }}-linux-x86_64 install
- - name: Setup sccache (Windows)
- if: runner.os == 'Windows' && inputs.target != 'android'
- uses: ./.github/actions/build-action
- with:
- repo: mozilla-actions/sccache-action
- ref: main
+ - name: Setup sccache (Windows x64 only)
+ if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64'
+ uses: mozilla-actions/sccache-action@v0.0.9
- run: echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
- if: runner.os == 'Windows' && inputs.target != 'android'
+ if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64'
shell: bash
- name: Setup Build Cache
- uses: hendrikmuhs/ccache-action@main
+ uses: hendrikmuhs/ccache-action@v1.2
with:
- variant: ${{ runner.os == 'Windows' && inputs.target != 'android' && 'sccache' || 'ccache' }}
+ variant: ${{ runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' && 'sccache' || 'ccache' }}
key: ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}
restore-keys: |
${{ inputs.host }}-${{ inputs.target }}
${{ inputs.host }}-
- max-size: 1G
- verbose: 1
+ max-size: ${{ inputs.ccache-max-size }}
+ verbose: 2
evict-old-files: job
+ save: ${{ inputs.save-cache == 'true' }}
- name: Ensure cpm-modules directory exists
+ if: inputs.cpm-modules != ''
run: mkdir -p "${{ inputs.cpm-modules }}"
shell: bash
@@ -75,4 +78,3 @@ runs:
path: ${{ inputs.cpm-modules }}
key: ${{ github.workflow }}-cpm-modules-${{ hashFiles('**/CMakeLists.txt', '**/*.cmake') }}
restore-keys: ${{ github.workflow }}-cpm-modules-
- # enableCrossOsArchive: true
diff --git a/.github/actions/checks/action.yml b/.github/actions/checks/action.yml
deleted file mode 100644
index 949a76acd196..000000000000
--- a/.github/actions/checks/action.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: Source Checks
-description: Run Various Checks on Source
-inputs:
- format:
- description: Run Clang Format
- default: 'false'
- spelling:
- description: Run Spelling
- default: 'false'
-runs:
- using: "composite"
- steps:
- - uses: actions/checkout@v4
-
- - name: Run clang-format style check for C++ Source Files.
- if: inputs.format == 'true'
- uses: jidicula/clang-format-action@main
- with:
- clang-format-version: '17'
- check-path: 'src'
-
- - name: Check spelling
- if: inputs.spelling == 'true'
- uses: crate-ci/typos@master
diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml
index 37ff9f3e364b..8e599acdc741 100644
--- a/.github/actions/common/action.yml
+++ b/.github/actions/common/action.yml
@@ -5,15 +5,10 @@ runs:
steps:
- uses: lukka/get-cmake@latest
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
python-version: '>=3.9'
- name: Create build directory
shell: bash
run: mkdir -p "$RUNNER_TEMP/build"
-
- - name: Fetch all tags (for version-string logic)
- shell: bash
- working-directory: ${{ github.workspace }}
- run: git fetch --all --tags --force --depth 1
diff --git a/.github/actions/custom-build/action.yml b/.github/actions/custom-build/action.yml
new file mode 100644
index 000000000000..ab0a86964452
--- /dev/null
+++ b/.github/actions/custom-build/action.yml
@@ -0,0 +1,23 @@
+name: Enable Custom Build
+description: Copies custom-example folder to enable custom build configuration
+
+runs:
+ using: composite
+ steps:
+ - name: Enable custom build (Windows)
+ if: runner.os == 'Windows'
+ shell: cmd
+ run: |
+ if not exist ".\custom-example" (
+ echo Directory ".\custom-example" does not exist. && exit /b 1
+ )
+ xcopy /E /I ".\custom-example" ".\custom"
+
+ - name: Enable custom build (Unix)
+ if: runner.os != 'Windows'
+ shell: bash
+ run: |
+ if [ ! -d "./custom-example" ]; then
+ echo "Directory ./custom-example does not exist." && exit 1
+ fi
+ cp -r ./custom-example ./custom
diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml
index 2f981476ec45..b52524fdf25c 100644
--- a/.github/actions/docker/action.yml
+++ b/.github/actions/docker/action.yml
@@ -1,8 +1,44 @@
-# action.yml
-name: "QGC Linux Builder"
-description: "Helper action to build QGC in Ubuntu 22.04"
+name: Docker Build
+description: Build QGC using Docker (Ubuntu)
+
+inputs:
+ build-type:
+ description: CMake build type (Release or Debug)
+ required: false
+ default: 'Release'
+
runs:
- using: "docker"
- image: "./deploy/docker/Dockerfile-build-ubuntu"
- args:
- - ${{ inputs.build-type }}
+ using: composite
+ steps:
+ - name: Get build config
+ id: config
+ uses: ./.github/actions/build-config
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./deploy/docker/Dockerfile-build-ubuntu
+ tags: qgc-ubuntu-builder:latest
+ load: true
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ build-args: |
+ QT_VERSION=${{ steps.config.outputs.qt_version }}
+
+ - name: Run Docker build
+ shell: bash
+ run: |
+ mkdir -p "${{ github.workspace }}/build"
+ docker run \
+ --rm \
+ --cap-add SYS_ADMIN \
+ --device /dev/fuse \
+ --security-opt apparmor:unconfined \
+ -v "${{ github.workspace }}:/project/source" \
+ -v "${{ github.workspace }}/build:/project/build" \
+ -e BUILD_TYPE="${{ inputs.build-type }}" \
+ qgc-ubuntu-builder:latest
diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml
index 650a4c434ca0..8ccd42e37d7b 100644
--- a/.github/actions/gstreamer/action.yml
+++ b/.github/actions/gstreamer/action.yml
@@ -1,107 +1,47 @@
name: Build GStreamer
-description: Builds GStreamer using Meson
+description: Build GStreamer from source for QGC video streaming
inputs:
- gst_version:
- description: Version of GStreamer to Build
- required: true
- default: 1.24.8
- build_type:
- description: Build Type "release" or "debug"
+ version:
+ description: GStreamer version (use build-config.outputs.gstreamer_version)
required: true
+ build-type:
+ description: Build type (release or debug)
+ required: false
default: release
- working_directory:
- description: Where to clone GStreamer source
- required: true
+ work-dir:
+ description: Working directory for source
+ required: false
default: ${{ runner.temp }}
- install_directory:
- description: Where to install GStreamer Build
- required: true
+ prefix:
+ description: Install prefix
+ required: false
default: ${{ runner.temp }}/gst
runs:
- using: "composite"
+ using: composite
steps:
- - name: Clone GStreamer
- working-directory: ${{ inputs.working_directory }}
- run: git clone --depth 1 --branch ${{ inputs.gst_version }} https://github.com/GStreamer/gstreamer.git
- shell: bash
-
- - name: Install Dependencies
- run: python3 -m pip install --user ninja meson
- shell: bash
-
- - name: Configure GStreamer
- working-directory: ${{ inputs.working_directory }}/gstreamer
- run: meson setup
- --prefix=${{ inputs.install_directory }}
- --buildtype=${{ inputs.build_type }}
- --wrap-mode=forcefallback
- --strip
- -Dauto_features=disabled
- -Dgst-full-libraries=video,gl
- -Dgpl=enabled
- -Dlibav=enabled
- -Dorc=enabled
- -Dqt6=enabled
- -Dvaapi=enabled
- -Dbase=enabled
- -Dgst-plugins-base:app=enabled
- -Dgst-plugins-base:gl=enabled
- -Dgst-plugins-base:gl_api=opengl,gles2
- -Dgst-plugins-base:gl_platform=glx,egl
- -Dgst-plugins-base:gl_winsys=x11,egl,wayland
- -Dgst-plugins-base:playback=enabled
- -Dgst-plugins-base:tcp=enabled
- -Dgst-plugins-base:x11=enabled
- -Dgood=enabled
- -Dgst-plugins-good:isomp4=enabled
- -Dgst-plugins-good:matroska=enabled
- -Dgst-plugins-good:qt-egl=enabled
- -Dgst-plugins-good:qt-method=auto
- -Dgst-plugins-good:qt-wayland=enabled
- -Dgst-plugins-good:qt-x11=enabled
- -Dgst-plugins-good:qt6=enabled
- -Dgst-plugins-good:rtp=enabled
- -Dgst-plugins-good:rtpmanager=enabled
- -Dgst-plugins-good:rtsp=enabled
- -Dgst-plugins-good:udp=enabled
- -Dbad=enabled
- -Dgst-plugins-bad:gl=enabled
- -Dgst-plugins-bad:mpegtsdemux=enabled
- -Dgst-plugins-bad:rtp=enabled
- -Dgst-plugins-bad:sdp=enabled
- -Dgst-plugins-bad:va=enabled
- -Dgst-plugins-bad:videoparsers=enabled
- -Dgst-plugins-bad:wayland=enabled
- -Dgst-plugins-bad:x11=enabled
- -Dgst-plugins-bad:x265=enabled
- -Dugly=enabled
- -Dgst-plugins-ugly:x264=enabled
- builddir
- # --default-library=static
- # --prefer_static=true
- # -Dgst-full-target-type=static_library
- # -Dgstreamer:gstreamer-static-full=true
- shell: bash
-
- - name: Compile GStreamer
- working-directory: ${{ inputs.working_directory }}/gstreamer
- run: meson compile -C builddir
- shell: bash
-
- - name: Install GStreamer
- working-directory: ${{ inputs.working_directory }}/gstreamer
- run: meson install -C builddir
+ - name: Build GStreamer
shell: bash
+ run: |
+ "${{ github.workspace }}/tools/setup/build-gstreamer.sh" \
+ --version "${{ inputs.version }}" \
+ --type "${{ inputs.build-type }}" \
+ --work-dir "${{ inputs.work-dir }}" \
+ --prefix "${{ inputs.prefix }}"
- name: Setup Environment
- working-directory: ${{ runner.temp }}/gstreamer
- run: >
- echo "PKG_CONFIG_PATH=${{ inputs.install_directory }}/lib/x86_64-linux-gnu/pkgconfig:${{
- inputs.install_directory }}/lib/x86_64-linux-gnu/gstreamer-1.0/pkgconfig:${{ env.PKG_CONFIG_PATH }}" >> "$GITHUB_ENV"
shell: bash
+ run: |
+ ARCH=$(uname -m)
+ case "$ARCH" in
+ x86_64) ARCH_DIR="x86_64-linux-gnu" ;;
+ aarch64) ARCH_DIR="aarch64-linux-gnu" ;;
+ *) ARCH_DIR="$ARCH-linux-gnu" ;;
+ esac
+ echo "PKG_CONFIG_PATH=${{ inputs.prefix }}/lib/$ARCH_DIR/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV"
+ echo "GST_PLUGIN_PATH=${{ inputs.prefix }}/lib/$ARCH_DIR/gstreamer-1.0" >> "$GITHUB_ENV"
- name: Save artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
- name: GStreamer-${{ inputs.build_type }}
- path: ${{ inputs.install_directory }}
+ name: GStreamer-${{ inputs.build-type }}
+ path: ${{ inputs.prefix }}
diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml
new file mode 100644
index 000000000000..f67a0d58dc52
--- /dev/null
+++ b/.github/actions/install-dependencies/action.yml
@@ -0,0 +1,60 @@
+name: Install Dependencies
+description: Install platform-specific build dependencies (GStreamer, etc.)
+
+inputs:
+ gstreamer:
+ description: 'Install GStreamer (Windows x64 only)'
+ required: false
+ default: 'true'
+ gstreamer-version:
+ description: 'GStreamer version (default: from build-config.json)'
+ required: false
+ default: ''
+ vulkan:
+ description: 'Install Vulkan SDK (Windows only, for validation layers)'
+ required: false
+ default: 'false'
+ extra-packages:
+ description: 'Additional packages to install (space-separated, Linux only)'
+ required: false
+ default: ''
+
+runs:
+ using: composite
+ steps:
+ - name: Install dependencies (Windows)
+ if: runner.os == 'Windows'
+ shell: pwsh
+ run: |
+ $params = @{}
+ if ('${{ inputs.gstreamer-version }}') {
+ $params['GStreamerVersion'] = '${{ inputs.gstreamer-version }}'
+ }
+ if ('${{ inputs.gstreamer }}' -ne 'true') {
+ $params['SkipGStreamer'] = $true
+ }
+ if ('${{ inputs.vulkan }}' -eq 'true') {
+ $params['InstallVulkan'] = $true
+ }
+ & "${{ github.workspace }}/tools/setup/install-dependencies-windows.ps1" @params
+
+ - name: Install dependencies (Linux)
+ if: runner.os == 'Linux'
+ shell: bash
+ run: |
+ if [ -f "./tools/setup/install-dependencies-debian.sh" ]; then
+ chmod a+x ./tools/setup/install-dependencies-debian.sh
+ sudo ./tools/setup/install-dependencies-debian.sh
+ fi
+ if [ -n "${{ inputs.extra-packages }}" ]; then
+ sudo apt-get install -y ${{ inputs.extra-packages }}
+ fi
+
+ - name: Install dependencies (macOS)
+ if: runner.os == 'macOS'
+ shell: bash
+ run: |
+ if [ -f "./tools/setup/install-dependencies-osx.sh" ]; then
+ chmod a+x ./tools/setup/install-dependencies-osx.sh
+ ./tools/setup/install-dependencies-osx.sh
+ fi
diff --git a/.github/actions/playstore/action.yml b/.github/actions/playstore/action.yml
index d3364f9d8b18..ca2f1765bcb9 100644
--- a/.github/actions/playstore/action.yml
+++ b/.github/actions/playstore/action.yml
@@ -1,5 +1,5 @@
name: Publish Android Build to Play Store
-description: Checks out the QGC repo with all the correct settings
+description: Upload Android APK to Google Play Store
inputs:
artifact:
description: Build File To Upload
@@ -11,8 +11,8 @@ runs:
using: "composite"
steps:
- name: Deploy to Play Store
- if: ${{ github.event_name != 'pull_request' && github.ref_name == 'master' }}
- uses: r0adkll/upload-google-play@v1
+ if: github.event_name != 'pull_request' && (github.ref_name == 'master' || github.ref_type == 'tag')
+ uses: r0adkll/upload-google-play@v1.1.3
with:
serviceAccountJsonPlainText: ${{ inputs.service_account_json }}
packageName: com.mavlink.qgroundcontrol
diff --git a/.github/actions/qt-android/action.yml b/.github/actions/qt-android/action.yml
index 3a2283354240..1747f76222cd 100644
--- a/.github/actions/qt-android/action.yml
+++ b/.github/actions/qt-android/action.yml
@@ -9,8 +9,10 @@ inputs:
required: true
version:
description: Qt Version
- required: false
- default: 6.10.1
+ required: true
+ modules:
+ description: Qt Modules
+ required: true
abis:
description: ABIs to Build
required: false
@@ -18,14 +20,38 @@ inputs:
cpm-modules:
description: CPM Cache Path
required: false
+ build-type:
+ description: Build Type (Debug or Release)
+ required: false
+ default: Release
+ ndk-version:
+ description: Android NDK version (use build-config.outputs.ndk_version)
+ required: false
+ default: 'r27c'
+ java-version:
+ description: Java version (use build-config.outputs.java_version)
+ required: false
+ default: '17'
+ android-platform:
+ description: Android platform/API level (use build-config.outputs.android_platform)
+ required: false
+ default: '35'
+ ccache-version:
+ description: ccache version (use build-config.outputs.ccache_version)
+ required: false
+ default: '4.11.3'
+ save-cache:
+ description: Save cache (set to false for PR builds)
+ required: false
+ default: 'true'
runs:
using: composite
steps:
- name: Setup Java Environment
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
distribution: temurin
- java-version: 17
+ java-version: ${{ inputs.java-version }}
cache: gradle
cache-dependency-path: |
**/*.gradle*
@@ -36,14 +62,14 @@ runs:
id: setup-android
with:
cmdline-tools-version: 13114758
- packages: 'platform-tools platforms;android-35 build-tools;35.0.0' # ndk;27.2.12479018'
+ packages: 'platform-tools platforms;android-${{ inputs.android-platform }} build-tools;${{ inputs.android-platform }}.0.0'
log-accepted-android-sdk-licenses: false
- name: Install Android NDK
uses: nttld/setup-ndk@v1
id: setup-ndk
with:
- ndk-version: r27c
+ ndk-version: ${{ inputs.ndk-version }}
add-to-path: false
local-cache: true
@@ -66,8 +92,10 @@ runs:
with:
host: ${{ inputs.host }}
target: android
- build-type: ${{ matrix.BuildType }}
+ build-type: ${{ inputs.build-type }}
cpm-modules: ${{ inputs.cpm-modules }}
+ ccache-version: ${{ inputs.ccache-version }}
+ save-cache: ${{ inputs.save-cache }}
- name: Install Qt for ${{ runner.os }}
uses: jurplel/install-qt-action@v4
@@ -77,12 +105,12 @@ runs:
target: desktop
arch: ${{ inputs.arch }}
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ inputs.modules }}
setup-python: false
cache: true
- name: Install Qt for Android (arm64_v8a)
- if: contains( inputs.abis, 'arm64-v8a')
+ if: contains(inputs.abis, 'arm64-v8a')
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.version }}
@@ -90,12 +118,12 @@ runs:
target: android
arch: android_arm64_v8a
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ inputs.modules }}
setup-python: false
cache: true
- name: Install Qt for Android (armv7)
- if: contains( inputs.abis, 'armeabi-v7a')
+ if: contains(inputs.abis, 'armeabi-v7a')
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.version }}
@@ -103,12 +131,12 @@ runs:
target: android
arch: android_armv7
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ inputs.modules }}
setup-python: false
cache: true
- name: Install Qt for Android (x86_64)
- if: contains( inputs.abis, 'x86_64')
+ if: contains(inputs.abis, 'x86_64')
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.version }}
@@ -116,12 +144,12 @@ runs:
target: android
arch: android_x86_64
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ inputs.modules }}
setup-python: false
cache: true
- name: Install Qt for Android (x86)
- if: contains( inputs.abis, 'x86')
+ if: contains(inputs.abis, 'x86')
uses: jurplel/install-qt-action@v4
with:
version: ${{ inputs.version }}
@@ -129,6 +157,6 @@ runs:
target: android
arch: android_x86
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ inputs.modules }}
setup-python: false
cache: true
diff --git a/.github/actions/upload/action.yml b/.github/actions/upload/action.yml
index be52339f418e..921f41abbad9 100644
--- a/.github/actions/upload/action.yml
+++ b/.github/actions/upload/action.yml
@@ -16,22 +16,24 @@ inputs:
aws_distribution_id:
description: AWS distribution ID
required: false
- github_token:
- description: GitHub Token
- required: false
upload_aws:
description: Upload artifact to AWS
- required: true
+ required: false
default: 'true'
+ retention-days:
+ description: Number of days to retain the artifact
+ required: false
+ default: '90'
runs:
using: composite
steps:
- name: Save artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v5
with:
name: ${{ inputs.package_name }}
path: ${{ runner.temp }}/build/${{ inputs.artifact_name }}
+ retention-days: ${{ inputs.retention-days }}
- name: Configure AWS Credentials
if: ${{ fromJSON(inputs.upload_aws) && github.event_name == 'push' && github.repository_owner == 'mavlink' && inputs.aws_key_id != '' && inputs.aws_secret_access_key != '' }}
@@ -67,7 +69,3 @@ runs:
--distribution-id "${{ inputs.aws_distribution_id }}" \
--paths "/latest/${{ inputs.artifact_name }}"
shell: bash
- env:
- AWS_ACCESS_KEY_ID: ${{ inputs.aws_key_id }}
- AWS_SECRET_ACCESS_KEY: ${{ inputs.aws_secret_access_key }}
- AWS_REGION: us-west-2
diff --git a/.github/build-config.json b/.github/build-config.json
new file mode 100644
index 000000000000..4aebb6212d31
--- /dev/null
+++ b/.github/build-config.json
@@ -0,0 +1,10 @@
+{
+ "qt_version": "6.10.1",
+ "qt_modules": "qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml",
+ "gstreamer_version": "1.24.13",
+ "xcode_version": "16.x",
+ "ndk_version": "r27c",
+ "java_version": "17",
+ "android_platform": "35",
+ "ccache_version": "4.11.3"
+}
diff --git a/codecov.yml b/.github/codecov.yml
similarity index 85%
rename from codecov.yml
rename to .github/codecov.yml
index cef4e70c46ed..7890eb6edf53 100644
--- a/codecov.yml
+++ b/.github/codecov.yml
@@ -35,10 +35,17 @@ ignore:
- "tools/**/*"
- "deploy/**/*"
- "custom/**/*"
+ - "custom-example/**/*"
- "libs/**/*"
+ - "docs/**/*"
+ - "translations/**/*"
+ - "android/**/*"
- "*.qml"
- "*.js"
- - "android/**/*"
+ - "**/moc_*.cpp"
+ - "**/qrc_*.cpp"
+ - "**/ui_*.h"
+ - "**/*_autogen/**"
flags:
debug:
diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
new file mode 100644
index 000000000000..a4b734c7b5b5
--- /dev/null
+++ b/.github/codeql/codeql-config.yml
@@ -0,0 +1,29 @@
+# =============================================================================
+# CodeQL Configuration
+# Custom configuration for C++ security analysis
+# =============================================================================
+
+name: "QGroundControl CodeQL Config"
+
+# Paths to analyze
+paths:
+ - src
+ - test
+
+# Paths to exclude from analysis
+paths-ignore:
+ - '**/moc_*'
+ - '**/qrc_*'
+ - '**/ui_*'
+ - '**/*_autogen/**'
+ - '**/libs/**'
+ - '**/3rdparty/**'
+ - '**/build/**'
+
+# Query suites to use
+# Options: security-extended, security-and-quality, or specific query packs
+queries:
+ - uses: security-extended
+
+# Trap caching for faster subsequent runs
+trap-caching: true
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index e6419818cc34..b34cf104b8b4 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,171 +1,38 @@
-# QGroundControl Development Guide
+# QGroundControl - AI Assistant Guide
Ground Control Station for MAVLink-enabled UAVs supporting PX4 and ArduPilot.
-## Quick Start for AI Assistants
+## Quick Start
-**First-time here?** Start with these critical files:
-1. **This file** - Architecture patterns and coding standards
-2. `.github/CONTRIBUTING.md` - Contribution workflow
-3. `src/FactSystem/Fact.h` - The most important pattern in QGC
-4. `src/Vehicle/Vehicle.h` - Core vehicle model
-
-**Most Critical Pattern**: The **Fact System** handles ALL vehicle parameters. Never create custom parameter storage - always use Facts.
-
-**Golden Rule**: Multi-vehicle support means ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()`.
+**Read these files first:**
+1. **[CONTRIBUTING.md](CONTRIBUTING.md)** - Coding standards and architecture patterns
+2. `src/FactSystem/Fact.h` - The most important pattern in QGC
+3. `src/Vehicle/Vehicle.h` - Core vehicle model
## Tech Stack
+
- **C++20** with **Qt 6.10.1** (QtQml, QtQuick)
- **Build**: CMake 3.25+, Ninja
- **Protocol**: MAVLink 2.0
- **Platforms**: Windows, macOS, Linux, Android, iOS
-## Critical Architecture Patterns
-
-### 1. Fact System (Parameter Management)
-**Most important pattern in QGC** - All vehicle parameters use this.
-
-```cpp
-// Access parameters (always null-check!)
-Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME");
-if (param && param->validate(newValue, false).isEmpty()) {
- param->setCookedValue(newValue); // Use cookedValue for UI (with units)
- // param->rawValue() for MAVLink/storage
-}
-
-// Expose to QML
-Q_PROPERTY(Fact* myParam READ myParam CONSTANT)
-```
-
-**Key classes:**
-- `Fact` - Single parameter with validation, units, metadata
-- `FactGroup` - Hierarchical container (handles MAVLink via `handleMessage()`)
-- `FactMetaData` - JSON-based metadata (min/max, enums, descriptions)
-
-**Rules:**
-- Wait for `parametersReady` signal before accessing
-- Use `cookedValue` (display) vs `rawValue` (storage)
-- Metadata in `*.FactMetaData.json` files
-
-### 2. Plugin Architecture
-Three types handle firmware customization:
-
-**FirmwarePlugin** - Firmware behavior (flight modes, capabilities)
-```cpp
-virtual QList flightModes() override;
-virtual bool setFlightMode(const QString& mode) override;
-```
-
-**AutoPilotPlugin** - Vehicle setup UI (returns `VehicleComponent` list)
-
-**VehicleComponent** - Individual setup items (Radio, Sensors, Safety)
-```cpp
-virtual QString name() const override;
-virtual bool setupComplete() const override;
-virtual QUrl setupSource() const override; // QML UI
-```
-
-### 3. Manager Singletons
-```cpp
-// Always null-check activeVehicle (multi-vehicle support)
-Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle();
-if (vehicle) {
- vehicle->parameterManager()->getParameter(...);
-}
-
-// Other managers
-SettingsManager::instance()->appSettings()->...
-LinkManager::instance()->...
-```
-
-### 4. QML/C++ Integration
-```cpp
-Q_OBJECT
-QML_ELEMENT // Creatable in QML
-QML_SINGLETON // Singleton
-QML_UNCREATABLE("") // C++-only
-
-Q_PROPERTY(Type name READ getter WRITE setter NOTIFY signal)
-Q_INVOKABLE void method();
-Q_ENUM(EnumType)
-```
-
-### 5. State Machines
-For complex workflows (parameter loading, calibration):
-```cpp
-QGCStateMachine machine("Workflow", vehicle);
-auto* sendCmd = new SendMavlinkCommandState("SendCmd", &machine);
-auto* waitMsg = new WaitForMavlinkMessageState("WaitResponse", &machine);
-sendCmd->addTransition(sendCmd, &SendMavlinkCommandState::done, waitMsg);
-machine.start();
-```
-
-### 6. Settings Framework
-```cpp
-class MySettings : public SettingsGroup {
- DEFINE_SETTINGFACT(settingName) // Creates Fact with JSON metadata
-};
-// Access: SettingsManager::instance()->mySettings()->settingName()
-```
+## Build Commands
-## Code Structure
-```
-src/
-├── Vehicle/ # Vehicle state/comms (Vehicle.h is key)
-├── FactSystem/ # Parameter management (READ THIS FIRST!)
-├── FirmwarePlugin/ # PX4/ArduPilot abstraction
-├── AutoPilotPlugins/ # Vehicle setup UI
-├── MissionManager/ # Mission planning
-├── MAVLink/ # Protocol handling
-├── Comms/ # Serial/UDP/TCP/Bluetooth
-├── Settings/ # Persistent settings
-├── UI/ # QML UI (MainWindow.qml)
-└── Utilities/ # StateMachine, helpers
+```bash
+git submodule update --init --recursive
+~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
+cmake --build build --config Debug
+./build/Debug/QGroundControl --unittest # Run tests
```
-## Coding Standards
-
-**Naming:**
-- Classes/Enums: `PascalCase`
-- Methods/properties: `camelCase`
-- Private members: `_leadingUnderscore`
-- Files: `ClassName.h`, `ClassName.cc`
+## Critical Rules
-**Logging:**
-```cpp
-Q_DECLARE_LOGGING_CATEGORY(MyLog)
-QGC_LOGGING_CATEGORY(MyLog, "qgc.component.name")
-qCDebug(MyLog) << "Message:" << value;
-```
-
-**Defensive Coding (Critical!):**
-```cpp
-void method(Vehicle* vehicle) {
- if (!vehicle) {
- qCWarning(Log) << "Invalid vehicle";
- return; // Early return on invalid state
- }
- // Always use braces
- if (condition) {
- doSomething();
- }
-}
-```
+### The Two Golden Rules
-**Code Style Tools:**
-- Use `.clang-format`, `.clang-tidy`, `.editorconfig` configured in repo root
-- Keep comments minimal - code should be self-documenting
-- Do NOT create verbose file headers or unnecessary documentation files
-- Do NOT add README files unless explicitly requested
+1. **Fact System**: ALL vehicle parameters use the Fact System. Never create custom parameter storage.
+2. **Multi-Vehicle**: ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()`.
-**Security & Dependencies:**
-- Never commit secrets, API keys, or credentials
-- Validate all external inputs (MAVLink messages, file uploads, user input)
-- Use Qt's built-in sanitization for SQL and string operations
-- When adding dependencies, check for known vulnerabilities
-- Prefer Qt's built-in functionality over external libraries
-
-## Common Pitfalls (DO NOT!)
+### Common Pitfalls (DO NOT!)
1. ❌ Assume single vehicle - Always null-check `activeVehicle()`
2. ❌ Access Facts before `parametersReady` signal
@@ -175,84 +42,13 @@ void method(Vehicle* vehicle) {
6. ❌ Ignore platform differences - Check `#ifdef Q_OS_*`
7. ❌ Mix cookedValue/rawValue without conversion
8. ❌ Create custom parameter storage - Use Fact System
-9. ❌ Access FactGroups before `telemetryAvailable`
-10. ❌ Forget to emit property signals (breaks QML bindings)
-
-## Build Commands
-```bash
-git submodule update --init --recursive
-~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
-cmake --build build --config Debug
-./build/Debug/QGroundControl --unittest # Run tests
-```
-
-**Key CMake Options** (cmake/CustomOptions.cmake):
-- `QGC_STABLE_BUILD` - Release vs daily
-- `QGC_BUILD_TESTING` - Unit tests
-- `QGC_ENABLE_BLUETOOTH` - Bluetooth support
-- `QGC_DISABLE_APM_PLUGIN` / `QGC_DISABLE_PX4_PLUGIN`
-
-## Testing
-
-### Running Tests
-```bash
-# Run all unit tests
-./build/Debug/QGroundControl --unittest
-
-# Run specific test
-./build/Debug/QGroundControl --unittest:
-
-# Run with verbose output
-./build/Debug/QGroundControl --unittest --logging:full
-```
-
-### Test Structure
-- Tests mirror `src/` structure in `test/` directory
-- Use `UnitTest` base class from Qt Test framework
-- Mock vehicle connections when testing vehicle-dependent code
-- Always test with null vehicle checks
+9. ❌ Forget to emit property signals (breaks QML bindings)
-### Adding New Tests
-```cpp
-class MyComponentTest : public UnitTest {
- Q_OBJECT
-private slots:
- void init(); // Called before each test
- void cleanup(); // Called after each test
- void testMyFunction();
-};
-```
-
-## Troubleshooting
-
-### Build Issues
-- **Qt not found**: Set `CMAKE_PREFIX_PATH` to Qt installation, or use qt-cmake
-- **Submodule errors**: Run `git submodule update --init --recursive`
-- **Missing dependencies**: Check platform-specific build instructions at https://dev.qgroundcontrol.com/
-- **CMake cache issues**: Delete `build/` directory and reconfigure
-
-### Runtime Issues
-- **Crash on startup**: Check log files in `~/.local/share/QGroundControl/` (Linux/macOS) or `%LOCALAPPDATA%\QGroundControl` (Windows)
-- **Vehicle not connecting**: Verify MAVLink protocol compatibility, check link configuration
-- **Parameter load failures**: Ensure `parametersReady` signal before accessing Facts
-
-### Development Environment
-- **Qt Creator recommended**: Import CMakeLists.txt as project
-- **clangd for VSCode**: Uses `.clangd` config in repo root
-- **Pre-commit hooks**: Run `pre-commit install` to enable automatic formatting
-
-## Performance Tips
-- Batch updates: `fact->setSendValueChangedSignals(false)`
-- Suppress live updates: `factGroup->setLiveUpdates(false)`
-- Cache frequently accessed values
-- MAVLink handled on separate thread
-- Qt parent/child ownership for cleanup
-
-## Common Tasks
+## Code Examples
### Working with Vehicle Parameters
+
```cpp
-// 1. Get parameter (always null-check!)
Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle();
if (!vehicle) return;
@@ -262,32 +58,26 @@ if (!param) {
return;
}
-// 2. Validate before setting
QString error = param->validate(newValue, false);
if (!error.isEmpty()) {
qCWarning(Log) << "Invalid value:" << error;
return;
}
-// 3. Set value (cookedValue for UI with units)
param->setCookedValue(newValue);
-
-// 4. Listen to changes
-connect(param, &Fact::valueChanged, this, [](QVariant value) {
- qCDebug(Log) << "Parameter changed:" << value;
-});
```
### Creating a Settings Group
+
```cpp
-// 1. Define in MySettings.h
+// MySettings.h
class MySettings : public SettingsGroup {
Q_OBJECT
public:
- DEFINE_SETTINGFACT(mySetting) // Creates Fact with JSON metadata
+ DEFINE_SETTINGFACT(mySetting)
};
-// 2. Create MySettings.SettingsGroup.json with metadata
+// MySettings.SettingsGroup.json
{
"mySetting": {
"shortDescription": "My setting",
@@ -298,39 +88,18 @@ public:
}
}
-// 3. Access anywhere
+// Access anywhere
int value = SettingsManager::instance()->mySettings()->mySetting()->rawValue().toInt();
```
-### Adding a Vehicle Component
-```cpp
-// 1. Create MyComponent.h (subclass VehicleComponent)
-class MyComponent : public VehicleComponent {
- Q_OBJECT
-public:
- MyComponent(Vehicle* vehicle, AutoPilotPlugin* autopilot, QObject* parent = nullptr);
-
- QString name() const override { return "My Component"; }
- QString description() const override { return "Component description"; }
- QString iconResource() const override { return "/qmlimages/MyComponentIcon.svg"; }
- bool requiresSetup() const override { return true; }
- bool setupComplete() const override { return _setupComplete; }
- QUrl setupSource() const override { return QUrl::fromUserInput("qrc:/qml/MyComponentSetup.qml"); }
-};
-
-// 2. Register in AutoPilotPlugin::getVehicleComponents()
-```
-
### Handling MAVLink Messages
+
```cpp
-// In a FactGroup or custom component
void MyFactGroup::handleMessage(Vehicle* vehicle, mavlink_message_t& message) {
switch (message.msgid) {
case MAVLINK_MSG_ID_MY_MESSAGE: {
mavlink_my_message_t msg;
mavlink_msg_my_message_decode(&message, &msg);
-
- // Update Facts (thread-safe via Qt signals)
myFact()->setRawValue(msg.value);
break;
}
@@ -338,47 +107,61 @@ void MyFactGroup::handleMessage(Vehicle* vehicle, mavlink_message_t& message) {
}
```
-### Adding a QML UI Component
+### QML Component
+
```qml
-// 1. Create MyControl.qml
import QtQuick
import QGroundControl
import QGroundControl.Controls
QGCButton {
text: "My Action"
-
property var vehicle: QGroundControl.multiVehicleManager.activeVehicle
-
enabled: vehicle && vehicle.armed
onClicked: {
if (vehicle) {
- // Always null-check vehicle!
vehicle.sendMavCommand(...)
}
}
}
```
-## Essential Files to Read
-1. `.github/CONTRIBUTING.md` - Contribution guidelines
-2. `src/FactSystem/Fact.h` - Parameter system (CRITICAL!)
-3. `src/Vehicle/Vehicle.h` - Vehicle model
-4. `src/FirmwarePlugin/FirmwarePlugin.h` - Firmware abstraction
+## Code Structure
+
+```
+src/
+├── Vehicle/ # Vehicle state/comms (Vehicle.h is key)
+├── FactSystem/ # Parameter management (READ THIS FIRST!)
+├── FirmwarePlugin/ # PX4/ArduPilot abstraction
+├── AutoPilotPlugins/ # Vehicle setup UI
+├── MissionManager/ # Mission planning
+├── MAVLink/ # Protocol handling
+├── Comms/ # Serial/UDP/TCP/Bluetooth
+├── Settings/ # Persistent settings
+├── UI/ # QML UI (MainWindow.qml)
+└── Utilities/ # StateMachine, helpers
+```
+
+## Style Reference
+
+For complete coding standards, naming conventions, and architecture patterns, see **[CONTRIBUTING.md](CONTRIBUTING.md#coding-standards)**.
+
+**Quick reference:**
+- Classes/Enums: `PascalCase`
+- Methods/properties: `camelCase`
+- Private members: `_leadingUnderscore`
+- Files: `ClassName.h`, `ClassName.cc`
+- Use `.clang-format`, `.clang-tidy` from repo root
+- Minimal comments - code should be self-documenting
## Resources
-- **User Manual**: https://docs.qgroundcontrol.com/
+
- **Dev Guide**: https://dev.qgroundcontrol.com/
+- **User Manual**: https://docs.qgroundcontrol.com/
- **MAVLink**: https://mavlink.io/
- **Qt Docs**: https://doc.qt.io/qt-6/
-## Memory & Threading
-- Qt parent/child ownership (auto-cleanup)
-- Use `deleteLater()` for objects with active signals
-- `Qt::QueuedConnection` for cross-thread signals
-- MAVLink on LinkManager thread, UI on main thread
-
---
**Key Principle**: Match the style of code you're editing. Use defensive coding, validate inputs, handle errors gracefully, and respect the Fact System architecture.
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 000000000000..5d5b19bd6179
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,72 @@
+# Labeler configuration - auto-label PRs based on changed files
+# https://github.com/actions/labeler
+
+Docs:
+ - changed-files:
+ - any-glob-to-any-file: 'docs/**'
+
+github_actions:
+ - changed-files:
+ - any-glob-to-any-file: '.github/**'
+
+"Platform: Android":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'android/**'
+ - 'deploy/android/**'
+
+"Platform: iOS":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'deploy/ios/**'
+
+"Platform: Linux":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'deploy/linux/**'
+ - 'deploy/docker/**'
+ - 'tools/setup/*debian*'
+ - 'tools/setup/*linux*'
+
+"Platform: Windows":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'deploy/windows/**'
+ - 'tools/setup/*windows*'
+
+"Platform: OSX":
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'deploy/macos/**'
+ - 'tools/setup/*macos*'
+
+Tests:
+ - changed-files:
+ - any-glob-to-any-file: 'test/**'
+
+CMake:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'CMakeLists.txt'
+ - 'cmake/**'
+ - '**/CMakeLists.txt'
+
+Video:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/VideoManager/**'
+ - 'src/VideoReceiver/**'
+
+Translations:
+ - changed-files:
+ - any-glob-to-any-file: 'translations/**'
+
+QML:
+ - changed-files:
+ - any-glob-to-any-file: 'src/**/*.qml'
+
+MAVLink:
+ - changed-files:
+ - any-glob-to-any-file:
+ - 'src/MAVLink/**'
+ - 'src/comm/**'
diff --git a/.github/markdownlint.json b/.github/markdownlint.json
new file mode 100644
index 000000000000..cb7ea2ed8fa8
--- /dev/null
+++ b/.github/markdownlint.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json",
+ "default": true,
+ "MD003": { "style": "atx" },
+ "MD004": { "style": "dash" },
+ "MD007": { "indent": 2 },
+ "MD013": false,
+ "MD024": { "siblings_only": true },
+ "MD033": false,
+ "MD041": false,
+ "MD046": { "style": "fenced" }
+}
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 83187b3d7819..6bbb501c14ab 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,23 +1,52 @@
-
+## Description
+
-Description
------------
-
-Test Steps
------------
-
+## Type of Change
+
+- [ ] Bug fix (non-breaking change that fixes an issue)
+- [ ] New feature (non-breaking change that adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to change)
+- [ ] Documentation update
+- [ ] Refactoring (no functional changes)
+- [ ] CI/Build changes
+- [ ] Other
-Checklist:
-----------
-
-- [ ] [Review Contribution Guidelines](https://github.com/mavlink/qgroundcontrol/blob/master/.github/CONTRIBUTING.md).
-- [ ] [Review Code of Conduct](https://github.com/mavlink/qgroundcontrol/blob/master/.github/CODE_OF_CONDUCT.md).
-- [ ] I have tested my changes.
+## Testing
+
+- [ ] Tested locally
+- [ ] Added/updated unit tests
+- [ ] Tested with simulator (SITL)
+- [ ] Tested with hardware
-Related Issue
------------
-
+### Platforms Tested
+
+- [ ] Linux
+- [ ] Windows
+- [ ] macOS
+- [ ] Android
+- [ ] iOS
+### Flight Stacks Tested
+
+- [ ] PX4
+- [ ] ArduPilot
-By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
+## Screenshots
+
+
+
+## Checklist
+
+- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] I have read the [Code of Conduct](CODE_OF_CONDUCT.md)
+- [ ] My code follows the project's coding standards
+- [ ] I have added tests that prove my fix/feature works
+- [ ] New and existing unit tests pass locally
+
+## Related Issues
+
+
+
+---
+By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3).
diff --git a/.github/release.yml b/.github/release.yml
index e7de10f55d71..d087963f045d 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -1,4 +1,10 @@
changelog:
+ exclude:
+ labels:
+ - dependencies
+ authors:
+ - dependabot
+ - dependabot[bot]
categories:
- title: Features
labels:
@@ -15,6 +21,12 @@ changelog:
labels:
- "RN: BUGFIX"
- "RN: BUGFIX - CUSTOM BUILD"
- - title: Targets
+ - title: Hardware Support
labels:
- "RN: NEW BOARD SUPPORT"
+ - title: Documentation
+ labels:
+ - documentation
+ - title: Other Changes
+ labels:
+ - "*"
diff --git a/.github/renovate.json b/.github/renovate.json
new file mode 100644
index 000000000000..6063f9e66f8c
--- /dev/null
+++ b/.github/renovate.json
@@ -0,0 +1,57 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended",
+ ":dependencyDashboard",
+ ":semanticCommitTypeAll(chore)"
+ ],
+ "schedule": ["before 6am on monday"],
+ "timezone": "UTC",
+ "labels": ["dependencies"],
+ "prConcurrentLimit": 5,
+ "prHourlyLimit": 2,
+
+ "packageRules": [
+ {
+ "description": "Group all GitHub Actions updates",
+ "matchManagers": ["github-actions"],
+ "groupName": "GitHub Actions",
+ "schedule": ["before 6am on monday"]
+ },
+ {
+ "description": "Auto-merge patch updates for GitHub Actions",
+ "matchManagers": ["github-actions"],
+ "matchUpdateTypes": ["patch"],
+ "automerge": true,
+ "automergeType": "pr",
+ "platformAutomerge": true
+ },
+ {
+ "description": "Group all git submodule updates",
+ "matchManagers": ["git-submodules"],
+ "groupName": "Git Submodules",
+ "schedule": ["before 6am on the first day of the month"]
+ },
+ {
+ "description": "Group Node.js updates (docs)",
+ "matchManagers": ["npm"],
+ "groupName": "Node.js Dependencies",
+ "schedule": ["before 6am on monday"]
+ },
+ {
+ "description": "Group Python updates (tools)",
+ "matchManagers": ["pip_requirements", "pip_setup"],
+ "groupName": "Python Dependencies",
+ "schedule": ["before 6am on monday"]
+ }
+ ],
+
+ "git-submodules": {
+ "enabled": true
+ },
+
+ "vulnerabilityAlerts": {
+ "labels": ["security", "priority"],
+ "schedule": ["at any time"]
+ }
+}
diff --git a/.github/vale.ini b/.github/vale.ini
new file mode 100644
index 000000000000..56899f9a9853
--- /dev/null
+++ b/.github/vale.ini
@@ -0,0 +1,23 @@
+# Vale Prose Linting Configuration
+# https://vale.sh/docs/
+
+StylesPath = vale/styles
+
+MinAlertLevel = suggestion
+
+Packages = Google, write-good
+
+[*.md]
+BasedOnStyles = Vale, Google, write-good
+
+# Disable overly strict rules
+Google.Acronyms = NO
+Google.FirstPerson = NO
+Google.We = NO
+Google.Will = NO
+write-good.Passive = suggestion
+write-good.Weasel = suggestion
+write-good.TooWordy = suggestion
+
+[*.txt]
+BasedOnStyles = Vale
diff --git a/.github/workflows/android-linux.yml b/.github/workflows/android-linux.yml
deleted file mode 100644
index 898a85cff51e..000000000000
--- a/.github/workflows/android-linux.yml
+++ /dev/null
@@ -1,112 +0,0 @@
-name: Android-Linux
-
-on:
- push:
- branches:
- - master
- - 'Stable*'
- tags:
- - 'v*'
- paths-ignore:
- - 'docs/**'
- pull_request:
- paths:
- - '.github/workflows/android-linux.yml'
- - 'deploy/android/**'
- - 'src/**'
- - 'android/**'
- - 'CMakeLists.txt'
- - 'cmake/**'
- - 'translations/*'
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
-
-jobs:
- build:
- name: Build Android-Linux ${{ matrix.qt_version }} ${{ matrix.build_type }}
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- build_type: [Release]
- qt_version: [6.10.1]
-
- defaults:
- run:
- shell: bash
-
- env:
- PACKAGE: QGroundControl
- QT_VERSION: ${{ matrix.qt_version }}
- QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore
- QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore
- QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_ABIS: ${{ matrix.build_type == 'Release' && 'arm64-v8a;armeabi-v7a' || 'arm64-v8a' }}
-
- steps:
- - name: Checkout repo
- uses: actions/checkout@v6
- with:
- submodules: recursive
- fetch-depth: 1
- fetch-tags: true
-
- - name: Initial Setup
- uses: ./.github/actions/common
-
- - name: Free Disk Space
- if: runner.os == 'Linux'
- uses: jlumbroso/free-disk-space@main
- with:
- tool-cache: false
- android: false
- large-packages: false
-
- - name: Install Qt for Android
- uses: ./.github/actions/qt-android
- with:
- host: linux
- arch: linux_gcc_64
- version: ${{ matrix.qt_version }}
- abis: ${{ env.QT_ANDROID_ABIS }}
- cpm-modules: ${{ runner.temp }}/build/cpm_modules
-
- - name: Configure
- working-directory: ${{ runner.temp }}/build
- run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
- -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
- -DCMAKE_WARN_DEPRECATED=FALSE
- -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}"
- -DQT_ANDROID_BUILD_ALL_ABIS=OFF
- -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../gcc_64"
- -DQT_ANDROID_SIGN_APK=${{ env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }}
- -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
-
- - name: Build
- working-directory: ${{ runner.temp }}/build
- run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel
-
- - run: cp ${{ runner.temp }}/build/android-build/*.apk ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk
-
- - name: Upload Build File
- if: matrix.build_type == 'Release'
- uses: ./.github/actions/upload
- with:
- artifact_name: ${{ env.PACKAGE }}.apk
- package_name: ${{ env.PACKAGE }}
- aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
- aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }}
- github_token: ${{ secrets.GITHUB_TOKEN }}
- upload_aws: true
-
- # - name: Deploy to Play Store
- # if: matrix.build_type == 'Release'
- # uses: ./.github/actions/playstore
- # with:
- # artifact_name: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk
- # service_account_json: ${{ secrets.SERVICE_ACCOUNT }}
diff --git a/.github/workflows/android-macos.yml b/.github/workflows/android-macos.yml
deleted file mode 100644
index 89c208608973..000000000000
--- a/.github/workflows/android-macos.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-name: Android-MacOS
-
-on:
- push:
- branches:
- - master
- - 'Stable*'
- tags:
- - 'v*'
- paths-ignore:
- - 'docs/**' # Do not trigger for any changes under docs
- pull_request:
- paths:
- - '.github/workflows/android-macos.yml'
- - 'deploy/android/**'
- - 'src/**'
- - 'android/**'
- - 'CMakeLists.txt'
- - 'cmake/**'
- - 'translations/*'
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
-
-jobs:
- build:
- runs-on: macos-latest
-
- strategy:
- matrix:
- BuildType: [Release]
-
- defaults:
- run:
- shell: bash
-
- env:
- ARTIFACT: QGroundControl.apk
- QT_VERSION: 6.10.1
- QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore
- QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore
- QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_ABIS: 'arm64-v8a'
-
- steps:
- - name: Checkout repo
- uses: actions/checkout@v6
- with:
- submodules: recursive
- fetch-depth: 1
- fetch-tags: true
-
- - name: Initial Setup
- uses: ./.github/actions/common
-
- - name: Install Qt for Android
- uses: ./.github/actions/qt-android
- with:
- host: mac
- arch: clang_64
- version: ${{ env.QT_VERSION }}
- abis: ${{ env.QT_ANDROID_ABIS }}
- cpm-modules: ${{ runner.temp }}/build/cpm_modules
-
- - name: Configure
- working-directory: ${{ runner.temp }}/build
- run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
- -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }}
- -DCMAKE_WARN_DEPRECATED=FALSE
- -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}"
- -DQT_ANDROID_BUILD_ALL_ABIS=OFF
- -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../macos"
- -DQT_ANDROID_SIGN_APK=${{ env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }}
- -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
-
- - name: Build
- working-directory: ${{ runner.temp }}/build
- run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel
-
- - name: Save APK
- uses: actions/upload-artifact@v5
- with:
- name: ${{ env.ARTIFACT }}
- path: ${{ runner.temp }}/build/android-build/*.apk
diff --git a/.github/workflows/android-windows.yml b/.github/workflows/android-windows.yml
deleted file mode 100644
index aeacbc26b35d..000000000000
--- a/.github/workflows/android-windows.yml
+++ /dev/null
@@ -1,86 +0,0 @@
-name: Android-Windows
-
-on:
- push:
- branches:
- - master
- - 'Stable*'
- tags:
- - 'v*'
- paths-ignore:
- - 'docs/**' # Do not trigger for any changes under docs
- pull_request:
- paths:
- - '.github/workflows/android-windows.yml'
- - 'deploy/android/**'
- - 'src/**'
- - 'android/**'
- - 'CMakeLists.txt'
- - 'cmake/**'
- - 'translations/*'
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
-
-jobs:
- build:
- runs-on: windows-latest
-
- strategy:
- matrix:
- BuildType: [Release]
-
- defaults:
- run:
- shell: cmd
-
- env:
- ARTIFACT: QGroundControl.apk
- QT_VERSION: 6.10.1
- QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}\deploy\android\android_release.keystore
- QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore
- QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
- QT_ANDROID_ABIS: 'arm64-v8a'
-
- steps:
- - name: Checkout repo
- uses: actions/checkout@v6
- with:
- submodules: recursive
- fetch-depth: 1
- fetch-tags: true
-
- - name: Initial Setup
- uses: ./.github/actions/common
-
- - name: Install Qt for Android
- uses: ./.github/actions/qt-android
- with:
- host: windows
- arch: win64_msvc2022_64
- version: ${{ env.QT_VERSION }}
- abis: ${{ env.QT_ANDROID_ABIS }}
- cpm-modules: ${{ runner.temp }}\build\cpm_modules
-
- - name: Configure
- working-directory: ${{ runner.temp }}/build
- run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
- -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }}
- -DCMAKE_WARN_DEPRECATED=FALSE
- -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}"
- -DQT_ANDROID_BUILD_ALL_ABIS=OFF
- -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../msvc2022_64"
- -DQT_ANDROID_SIGN_APK=OFF
- -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
-
- - name: Build
- working-directory: ${{ runner.temp }}/build
- run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel
-
- - name: Save APK
- uses: actions/upload-artifact@v5
- with:
- name: ${{ env.ARTIFACT }}
- path: ${{ runner.temp }}/build/android-build/*.apk
diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 000000000000..ad551551a373
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,164 @@
+name: Android
+
+on:
+ push:
+ branches:
+ - master
+ - 'Stable*'
+ tags:
+ - 'v*'
+ paths-ignore:
+ - 'docs/**'
+ pull_request:
+ paths:
+ - '.github/workflows/android.yml'
+ - '.github/actions/**'
+ - 'deploy/android/**'
+ - 'src/**'
+ - 'test/**'
+ - 'android/**'
+ - 'CMakeLists.txt'
+ - 'cmake/**'
+ - 'translations/*'
+ workflow_dispatch:
+ inputs:
+ build_type:
+ description: 'Build type'
+ required: false
+ default: 'Release'
+ type: choice
+ options:
+ - Release
+ - Debug
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: Android (${{ matrix.host }})
+ runs-on: ${{ matrix.runner }}
+ timeout-minutes: 120
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - host: linux
+ runner: ubuntu-latest
+ arch: linux_gcc_64
+ qt_host_path: gcc_64
+ shell: bash
+ primary: true
+ - host: mac
+ runner: macos-latest
+ arch: clang_64
+ qt_host_path: macos
+ shell: bash
+ primary: false
+ - host: windows
+ runner: windows-latest
+ arch: win64_msvc2022_64
+ qt_host_path: msvc2022_64
+ shell: cmd
+ primary: false
+
+ defaults:
+ run:
+ shell: ${{ matrix.shell }}
+
+ env:
+ PACKAGE: QGroundControl
+ QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore
+ QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore
+ QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
+ QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
+ # Primary build (Linux) includes both ABIs, others just arm64
+ QT_ANDROID_ABIS: ${{ matrix.primary && 'arm64-v8a;armeabi-v7a' || 'arm64-v8a' }}
+
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v6
+ with:
+ submodules: recursive
+ fetch-depth: 1
+ fetch-tags: true
+
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
+ - name: Initial Setup
+ uses: ./.github/actions/common
+
+ - name: Free Disk Space
+ if: matrix.host == 'linux'
+ uses: jlumbroso/free-disk-space@v1.3.1
+ with:
+ tool-cache: false
+ android: false
+ large-packages: false
+
+ - name: Install Qt for Android
+ uses: ./.github/actions/qt-android
+ with:
+ host: ${{ matrix.host }}
+ arch: ${{ matrix.arch }}
+ version: ${{ steps.build-config.outputs.qt_version }}
+ modules: ${{ steps.build-config.outputs.qt_modules }}
+ abis: ${{ env.QT_ANDROID_ABIS }}
+ build-type: ${{ inputs.build_type || 'Release' }}
+ cpm-modules: ${{ runner.temp }}/build/cpm_modules
+ ndk-version: ${{ steps.build-config.outputs.ndk_version }}
+ java-version: ${{ steps.build-config.outputs.java_version }}
+ android-platform: ${{ steps.build-config.outputs.android_platform }}
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
+
+ - name: Configure
+ working-directory: ${{ runner.temp }}/build
+ run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
+ -DCMAKE_BUILD_TYPE=${{ inputs.build_type || 'Release' }}
+ -DCMAKE_WARN_DEPRECATED=FALSE
+ -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}"
+ -DQT_ANDROID_BUILD_ALL_ABIS=OFF
+ -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../${{ matrix.qt_host_path }}"
+ -DQT_ANDROID_SIGN_APK=${{ matrix.primary && env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }}
+ -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
+ -DQGC_CUSTOM_GST_PACKAGE=ON
+
+ - name: Build
+ working-directory: ${{ runner.temp }}/build
+ run: cmake --build . --target all --config ${{ inputs.build_type || 'Release' }} --parallel
+
+ - name: Prepare Artifact
+ run: cp ${{ runner.temp }}/build/android-build/*.apk ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk
+
+ - name: Upload Build File
+ if: matrix.primary
+ uses: ./.github/actions/upload
+ with:
+ artifact_name: ${{ env.PACKAGE }}.apk
+ package_name: ${{ env.PACKAGE }}
+ aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }}
+ upload_aws: true
+
+ - name: Save APK
+ if: ${{ !matrix.primary }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: ${{ env.PACKAGE }}-${{ matrix.host }}.apk
+ path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk
+
+ # - name: Deploy to Play Store
+ # if: matrix.primary
+ # uses: ./.github/actions/playstore
+ # with:
+ # artifact_name: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk
+ # service_account_json: ${{ secrets.SERVICE_ACCOUNT }}
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
new file mode 100644
index 000000000000..2bc19c0d767d
--- /dev/null
+++ b/.github/workflows/auto-merge.yml
@@ -0,0 +1,30 @@
+name: Auto-merge Dependabot
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ auto-merge:
+ name: Auto-merge Dependabot PRs
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ if: github.actor == 'dependabot[bot]'
+
+ steps:
+ - name: Fetch Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Enable auto-merge for patch/minor updates
+ if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
+ run: gh pr merge --auto --squash "$PR_URL"
+ env:
+ PR_URL: ${{ github.event.pull_request.html_url }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/build-summary.yml b/.github/workflows/build-summary.yml
new file mode 100644
index 000000000000..d19af7d6cdae
--- /dev/null
+++ b/.github/workflows/build-summary.yml
@@ -0,0 +1,152 @@
+name: Build Summary
+
+on:
+ workflow_run:
+ workflows: [Linux, Windows, MacOS, Android]
+ types: [completed]
+
+permissions:
+ pull-requests: write
+ actions: read
+
+jobs:
+ summary:
+ name: Post Build Summary
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.event == 'pull_request'
+ timeout-minutes: 10
+
+ steps:
+ - name: Get PR number
+ id: pr
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.payload.workflow_run.id
+ });
+
+ // Try to get PR number from the workflow run
+ const run = context.payload.workflow_run;
+ if (run.pull_requests && run.pull_requests.length > 0) {
+ return run.pull_requests[0].number;
+ }
+
+ // Fallback: search for PR by head SHA
+ const prs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ head: `${context.repo.owner}:${run.head_branch}`
+ });
+
+ if (prs.data.length > 0) {
+ return prs.data[0].number;
+ }
+
+ return null;
+
+ - name: Collect build results
+ if: steps.pr.outputs.result != 'null'
+ id: results
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = ${{ steps.pr.outputs.result }};
+ const headSha = context.payload.workflow_run.head_sha;
+
+ // Get all workflow runs for this commit
+ const runs = await github.rest.actions.listWorkflowRunsForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head_sha: headSha,
+ per_page: 100
+ });
+
+ const platforms = {
+ 'Linux': { status: '⏳', conclusion: 'pending' },
+ 'Windows': { status: '⏳', conclusion: 'pending' },
+ 'MacOS': { status: '⏳', conclusion: 'pending' },
+ 'Android': { status: '⏳', conclusion: 'pending' }
+ };
+
+ for (const run of runs.data.workflow_runs) {
+ if (platforms[run.name]) {
+ if (run.status === 'completed') {
+ platforms[run.name].status = run.conclusion === 'success' ? '✅' : '❌';
+ platforms[run.name].conclusion = run.conclusion;
+ platforms[run.name].url = run.html_url;
+ } else if (run.status === 'in_progress') {
+ platforms[run.name].status = '🔄';
+ platforms[run.name].conclusion = 'in_progress';
+ platforms[run.name].url = run.html_url;
+ }
+ }
+ }
+
+ return platforms;
+
+ - name: Post or update comment
+ if: steps.pr.outputs.result != 'null'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prNumber = ${{ steps.pr.outputs.result }};
+ const platforms = ${{ steps.results.outputs.result }};
+ const marker = '';
+
+ // Build the comment body
+ let body = `${marker}\n## 🏗️ Build Status\n\n`;
+ body += `| Platform | Status | Details |\n`;
+ body += `|----------|--------|--------|\n`;
+
+ for (const [name, info] of Object.entries(platforms)) {
+ const link = info.url ? `[View](${info.url})` : '-';
+ body += `| ${name} | ${info.status} | ${link} |\n`;
+ }
+
+ const allComplete = Object.values(platforms).every(p =>
+ p.conclusion === 'success' || p.conclusion === 'failure' || p.conclusion === 'cancelled'
+ );
+ const allSuccess = Object.values(platforms).every(p => p.conclusion === 'success');
+
+ if (allComplete) {
+ if (allSuccess) {
+ body += `\n✅ **All builds passed!**`;
+ } else {
+ body += `\n❌ **Some builds failed.** Please check the logs.`;
+ }
+ } else {
+ body += `\n⏳ *Some builds are still in progress...*`;
+ }
+
+ body += `\n\nLast updated: ${new Date().toISOString()}`;
+
+ // Find existing comment
+ const comments = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber
+ });
+
+ const existingComment = comments.data.find(c => c.body.includes(marker));
+
+ if (existingComment) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existingComment.id,
+ body: body
+ });
+ console.log(`Updated comment ${existingComment.id}`);
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: prNumber,
+ body: body
+ });
+ console.log(`Created new comment on PR #${prNumber}`);
+ }
diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml
index 876007676c2c..b9a7abbb98cd 100644
--- a/.github/workflows/cache-cleanup.yml
+++ b/.github/workflows/cache-cleanup.yml
@@ -5,9 +5,20 @@ on:
- closed
workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: false
+
jobs:
cleanup:
+ name: Cleanup Branch Caches
runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ defaults:
+ run:
+ shell: bash
+
permissions:
# `actions:write` permission is required to delete caches
# See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml
new file mode 100644
index 000000000000..1c697666a746
--- /dev/null
+++ b/.github/workflows/check-links.yml
@@ -0,0 +1,55 @@
+name: Check Links
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - 'docs/**'
+ - '**.md'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '**.md'
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * 0' # Weekly on Sunday
+
+permissions:
+ contents: read
+
+jobs:
+ check-links:
+ name: Check Links
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Check links
+ uses: lycheeverse/lychee-action@v2
+ with:
+ args: >-
+ --verbose
+ --no-progress
+ --exclude-mail
+ --exclude '^https://github.com/mavlink/qgroundcontrol/(pull|issues|compare)/'
+ --exclude 'localhost'
+ --exclude '127\.0\.0\.1'
+ --exclude '192\.168\.'
+ --exclude '10\.\d+\.'
+ --accept 200,204,301,302,403,429
+ '**/*.md'
+ 'docs/**'
+ # Fail on broken links for pushes to master, advisory for PRs
+ fail: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
+
+ - name: Upload report
+ uses: actions/upload-artifact@v5
+ if: always()
+ with:
+ name: link-check-report
+ path: ./lychee/out.md
+ retention-days: 7
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 000000000000..e12fbcf9cfa4
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,106 @@
+# =============================================================================
+# CodeQL Security Analysis
+# C++ security scanning for QGroundControl
+#
+# NOTE: This workflow is disabled by default because the upstream repo has
+# "default setup" CodeQL enabled, which conflicts with custom workflows.
+# The default setup currently only scans JS/Python/Actions, NOT C++.
+#
+# To enable C++ security scanning, either:
+# 1. Ask maintainers to add C++ to the default setup in repo settings
+# 2. Or disable default setup and uncomment the triggers below
+# =============================================================================
+
+name: "CodeQL"
+
+on:
+ # Disabled: conflicts with default setup. Uncomment when default setup is disabled.
+ # push:
+ # branches: [master, 'Stable*']
+ # paths: ['src/**', 'test/**']
+ # pull_request:
+ # branches: [master]
+ # paths: ['src/**', 'test/**']
+ # schedule:
+ # - cron: '0 6 * * 1' # Weekly on Monday at 6 AM UTC
+
+ # Manual trigger only (doesn't conflict with default setup)
+ workflow_dispatch:
+ inputs:
+ query_suite:
+ description: 'Query suite to use'
+ required: false
+ default: 'security-extended'
+ type: choice
+ options:
+ - default
+ - security-extended
+ - security-and-quality
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+ security-events: write
+ actions: read
+
+jobs:
+ analyze:
+ name: Analyze C++
+ runs-on: ubuntu-latest
+ timeout-minutes: 120
+
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: recursive
+ fetch-depth: 1
+
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: c-cpp
+ queries: ${{ inputs.query_suite || 'security-extended' }}
+ config-file: ./.github/codeql/codeql-config.yml
+
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
+
+ - name: Install Qt
+ uses: jurplel/install-qt-action@v4
+ with:
+ version: ${{ steps.build-config.outputs.qt_version }}
+ host: linux
+ target: desktop
+ arch: linux_gcc_64
+ dir: ${{ runner.temp }}
+ modules: ${{ steps.build-config.outputs.qt_modules }}
+ setup-python: false
+ cache: true
+
+ - name: Configure
+ working-directory: ${{ runner.temp }}/build
+ run: |
+ ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DQGC_BUILD_TESTING=OFF
+
+ - name: Build
+ working-directory: ${{ runner.temp }}/build
+ run: cmake --build . --target all --config Debug --parallel
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:c-cpp"
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 000000000000..acda19dca135
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,129 @@
+name: Code Coverage
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - 'src/**'
+ - 'test/**'
+ - 'CMakeLists.txt'
+ - 'cmake/**'
+ pull_request:
+ paths:
+ - 'src/**'
+ - 'test/**'
+ schedule:
+ - cron: '0 7 * * 1' # Weekly on Monday at 7 AM UTC
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
+jobs:
+ coverage:
+ name: Generate Coverage
+ runs-on: ubuntu-22.04
+ timeout-minutes: 120
+
+ defaults:
+ run:
+ shell: bash
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: recursive
+ fetch-depth: 1
+
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
+ - name: Initial Setup
+ uses: ./.github/actions/common
+
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
+
+ - name: Install Coverage Tools
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y lcov
+ pip install gcovr
+
+ - name: Setup Caching
+ uses: ./.github/actions/cache
+ with:
+ host: linux
+ target: linux_gcc_64
+ build-type: Coverage
+ cpm-modules: ${{ runner.temp }}/build/cpm_modules
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
+
+ - name: Install Qt
+ uses: jurplel/install-qt-action@v4
+ with:
+ version: ${{ steps.build-config.outputs.qt_version }}
+ host: linux
+ target: desktop
+ arch: linux_gcc_64
+ dir: ${{ runner.temp }}
+ modules: ${{ steps.build-config.outputs.qt_modules }}
+ setup-python: false
+ cache: true
+
+ - name: Configure with Coverage
+ working-directory: ${{ runner.temp }}/build
+ run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
+ -DCMAKE_BUILD_TYPE=Debug
+ -DQGC_ENABLE_COVERAGE=ON
+ -DQGC_BUILD_TESTING=ON
+
+ - name: Build
+ working-directory: ${{ runner.temp }}/build
+ run: cmake --build . --target all --config Debug --parallel
+
+ - name: Run Tests
+ working-directory: ${{ runner.temp }}/build
+ run: xvfb-run -a ctest --output-on-failure --timeout 300
+ continue-on-error: true
+
+ - name: Generate Coverage Report
+ working-directory: ${{ runner.temp }}/build
+ run: |
+ gcovr --root ${{ github.workspace }} \
+ --filter '${{ github.workspace }}/src/' \
+ --exclude '.*moc_.*' \
+ --exclude '.*qrc_.*' \
+ --exclude '.*ui_.*' \
+ --exclude '.*_autogen.*' \
+ --xml coverage.xml \
+ --html coverage.html \
+ --html-details \
+ --print-summary
+
+ - name: Upload to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: ${{ runner.temp }}/build/coverage.xml
+ flags: unittests
+ name: qgc-coverage
+ fail_ci_if_error: false
+ verbose: true
+
+ - name: Upload Coverage Artifact
+ uses: actions/upload-artifact@v5
+ with:
+ name: coverage-report
+ path: |
+ ${{ runner.temp }}/build/coverage.xml
+ ${{ runner.temp }}/build/coverage.html
+ ${{ runner.temp }}/build/coverage.*.html
+ retention-days: 14
diff --git a/.github/workflows/crowdin_docs_download.yml b/.github/workflows/crowdin_docs_download.yml
index da8aa03d8ae1..ca3896827717 100644
--- a/.github/workflows/crowdin_docs_download.yml
+++ b/.github/workflows/crowdin_docs_download.yml
@@ -7,13 +7,23 @@ on:
- cron: '0 0 * * 0' # Runs every Sunday at 00:00 UTC
workflow_dispatch:
+concurrency:
+ group: crowdin-download
+ cancel-in-progress: false
+
permissions:
contents: write
pull-requests: write
jobs:
synchronize-with-crowdin:
+ name: Download from Crowdin
runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ defaults:
+ run:
+ shell: bash
steps:
- name: Checkout
@@ -31,6 +41,7 @@ jobs:
create_pull_request: true
pull_request_title: 'New QGC guide translations (Crowdin)'
pull_request_body: 'New QGC guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
+ pull_request_labels: 'translations, automated'
pull_request_base_branch_name: 'master'
env:
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
diff --git a/.github/workflows/crowdin_docs_upload.yml b/.github/workflows/crowdin_docs_upload.yml
index 43a1d0ef7fac..e0df08fae22b 100644
--- a/.github/workflows/crowdin_docs_upload.yml
+++ b/.github/workflows/crowdin_docs_upload.yml
@@ -8,19 +8,24 @@ on:
- master
paths:
- 'docs/en/**'
- pull_request:
- types:
- - closed
- branches:
- - master
- paths:
- - 'docs/en/**'
workflow_dispatch:
+concurrency:
+ group: crowdin-upload
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+
jobs:
upload-to-crowdin:
- if: github.event.pull_request.merged == true || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
+ name: Upload to Crowdin
runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ defaults:
+ run:
+ shell: bash
steps:
- name: Checkout
diff --git a/.github/workflows/custom.yml b/.github/workflows/custom.yml
deleted file mode 100644
index e490b07bab77..000000000000
--- a/.github/workflows/custom.yml
+++ /dev/null
@@ -1,97 +0,0 @@
-name: Custom build
-
-on:
- push:
- branches:
- - master
- - Stable*
- tags:
- - v*
- paths-ignore:
- - docs/**
- pull_request:
- paths:
- - .github/workflows/windows.yml
- - deploy/windows/**
- - src/**
- - custom-example/**
- - CMakeLists.txt
- - cmake/**
- - tools/setup/*windows*
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
-
-jobs:
- build:
- runs-on: windows-latest
-
- defaults:
- run:
- shell: cmd
-
- env:
- QT_VERSION: 6.10.1
- GST_VERSION: 1.22.12
-
- steps:
- - name: Checkout repo
- uses: actions/checkout@v6
- with:
- submodules: recursive
- fetch-depth: 1
- fetch-tags: true
-
- - name: Initial Setup
- uses: ./.github/actions/common
-
- - name: Enable custom build
- run: |
- if not exist ".\custom-example" (
- echo Directory ".\custom-example" does not exist. && exit /b 1
- )
- xcopy /E /I ".\custom-example" ".\custom"
-
- - name: Install GStreamer
- uses: blinemedical/setup-gstreamer@v1
- with:
- version: ${{ env.GST_VERSION }}
-
- - name: Setup Caching
- uses: ./.github/actions/cache
- with:
- host: windows
- target: win64_msvc2022_64
- build-type: Release
- cpm-modules: ${{ runner.temp }}\build\cpm_modules
-
- - name: Install Qt
- uses: jurplel/install-qt-action@v4
- with:
- version: ${{ env.QT_VERSION }}
- host: windows
- target: desktop
- arch: win64_msvc2022_64
- dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
- setup-python: false
- cache: true
-
- - name: Set up MSVC environment
- uses: ilammy/msvc-dev-cmd@v1
-
- - name: Configure
- working-directory: ${{ runner.temp }}\build
- run: |
- ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja ^
- -DCMAKE_BUILD_TYPE=Release ^
- -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
-
- - name: Build
- working-directory: ${{ runner.temp }}\build
- run: cmake --build . --target all --config Release --parallel
-
- - name: Verify executable
- working-directory: ${{ runner.temp }}\build\Release
- run: Custom-QGroundControl.exe --simple-boot-test
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
new file mode 100644
index 000000000000..935f23114c3b
--- /dev/null
+++ b/.github/workflows/dependency-review.yml
@@ -0,0 +1,29 @@
+name: Dependency Review
+
+on:
+ pull_request:
+ paths:
+ - '**/CMakeLists.txt'
+ - '**/*.cmake'
+ - '.gitmodules'
+ - 'package*.json'
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ dependency-review:
+ name: Review Dependencies
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Dependency Review
+ uses: actions/dependency-review-action@v4
+ with:
+ fail-on-severity: high
+ comment-summary-in-pr: always
diff --git a/.github/workflows/docker-linux.yml b/.github/workflows/docker-linux.yml
index 7db2a1d64b43..2c747691ce9f 100644
--- a/.github/workflows/docker-linux.yml
+++ b/.github/workflows/docker-linux.yml
@@ -12,19 +12,26 @@ on:
pull_request:
paths:
- '.github/workflows/docker-linux.yml'
+ - '.github/actions/**'
- 'deploy/docker/**'
- 'deploy/linux/**'
- 'src/**'
- 'CMakeLists.txt'
- 'cmake/**'
+ workflow_dispatch:
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
jobs:
build:
+ name: Docker Linux
runs-on: ubuntu-latest
+ timeout-minutes: 120
defaults:
run:
@@ -38,11 +45,5 @@ jobs:
fetch-depth: 1
fetch-tags: true
- - name: Get all tags for correct version determination
- working-directory: ${{ github.workspace }}
- run: git fetch --all --tags --force --depth 1
-
- - run: chmod a+x ./deploy/docker/run-docker-ubuntu.sh
-
- - name: Run Docker Build
- run: ./deploy/docker/run-docker-ubuntu.sh
+ - name: Build with Docker
+ uses: ./.github/actions/docker
diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml
index 94ee4aad16d8..265e4fa6db36 100644
--- a/.github/workflows/docs_deploy.yml
+++ b/.github/workflows/docs_deploy.yml
@@ -14,12 +14,26 @@ on:
- 'package*.json'
workflow_dispatch:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
jobs:
build:
+ name: Build Docs
runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ defaults:
+ run:
+ shell: bash
+
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -46,13 +60,19 @@ jobs:
retention-days: 1
deploy:
- if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged) }}
+ name: Deploy Docs
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
needs: build
runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ defaults:
+ run:
+ shell: bash
steps:
- name: Download Artifact
- uses: actions/download-artifact@v6
+ uses: actions/download-artifact@v5
with:
name: qgc_docs_build
path: ~/_book
diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml
index 65c847a4db2e..8a97d2bec9b7 100644
--- a/.github/workflows/flatpak.yml
+++ b/.github/workflows/flatpak.yml
@@ -1,11 +1,29 @@
name: flatpak
-on: [workflow_dispatch]
+
+on:
+ workflow_dispatch:
+ inputs:
+ gnome_version:
+ description: 'GNOME runtime version'
+ required: false
+ default: '47'
+ type: string
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
+
jobs:
flatpak-builder:
name: Flatpak
runs-on: ubuntu-latest
+ timeout-minutes: 120
container:
- image: bilelmoussaoui/flatpak-github-actions:gnome-47
+ # Update gnome version periodically: https://hub.docker.com/r/bilelmoussaoui/flatpak-github-actions/tags
+ image: bilelmoussaoui/flatpak-github-actions:gnome-${{ inputs.gnome_version || '47' }}
options: --privileged
steps:
- uses: actions/checkout@v6
diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml
index 9abdc12ddb81..ee2b36752fb8 100644
--- a/.github/workflows/ios.yml
+++ b/.github/workflows/ios.yml
@@ -3,26 +3,29 @@ name: iOS
on:
workflow_dispatch:
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
jobs:
build:
+ name: iOS
runs-on: macos-latest
-
- strategy:
- matrix:
- BuildType: [Release]
+ timeout-minutes: 120
defaults:
run:
shell: bash
+ strategy:
+ matrix:
+ build_type: [Release]
+
env:
- ARTIFACT: QGroundControl.app
PACKAGE: QGroundControl
- QT_VERSION: 6.10.1
steps:
- name: Checkout repo
@@ -32,55 +35,61 @@ jobs:
fetch-depth: 1
fetch-tags: true
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
- name: Initial Setup
uses: ./.github/actions/common
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: latest-stable
+ xcode-version: ${{ steps.build-config.outputs.xcode_version }}
- name: Setup Caching
uses: ./.github/actions/cache
with:
host: mac
target: ios
- build-type: ${{ matrix.BuildType }}
+ build-type: ${{ matrix.build_type }}
cpm-modules: ${{ runner.temp }}/build/cpm_modules
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
- name: Install Qt for MacOS
uses: jurplel/install-qt-action@v4
with:
- version: ${{ env.QT_VERSION }}
+ version: ${{ steps.build-config.outputs.qt_version }}
host: mac
target: desktop
arch: clang_64
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ steps.build-config.outputs.qt_modules }}
setup-python: false
cache: true
- name: Install Qt for iOS
uses: jurplel/install-qt-action@v4
with:
- version: ${{ env.QT_VERSION }}
+ version: ${{ steps.build-config.outputs.qt_version }}
host: mac
target: ios
arch: ios
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors
+ modules: ${{ steps.build-config.outputs.qt_modules_ios }}
cache: true
- name: Configure
working-directory: ${{ runner.temp }}/build
run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
- -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }}
+ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../macos"
-DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
- name: Build
working-directory: ${{ runner.temp }}/build
- run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel
+ run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel
- name: Save App
uses: actions/upload-artifact@v5
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 000000000000..c94f3909bb00
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,21 @@
+name: Labeler
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ label:
+ name: Label PR
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - uses: actions/labeler@v5
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ sync-labels: true
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 2efb5e8513d1..62cc522e90a1 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -8,7 +8,7 @@ on:
tags:
- 'v*'
paths-ignore:
- - 'docs/**' # Do not trigger for any changes under docs
+ - 'docs/**'
pull_request:
paths:
- '.github/workflows/linux.yml'
@@ -19,15 +19,29 @@ on:
- 'CMakeLists.txt'
- 'cmake/**'
- 'tools/setup/*debian*'
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+ workflow_dispatch:
+ inputs:
+ build_type:
+ description: 'Build type'
+ required: false
+ default: 'Release'
+ type: choice
+ options:
+ - Release
+ - Debug
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
jobs:
build:
name: Build ${{ matrix.arch }} ${{ matrix.build_type }}
runs-on: ${{ matrix.os }}
+ timeout-minutes: 120
strategy:
fail-fast: false
@@ -53,9 +67,6 @@ jobs:
run:
shell: bash
- env:
- QT_VERSION: 6.10.1
-
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -64,13 +75,15 @@ jobs:
fetch-depth: 1
fetch-tags: true
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
- name: Initial Setup
uses: ./.github/actions/common
- name: Install Dependencies
- run: |
- chmod a+x ./tools/setup/install-dependencies-debian.sh
- sudo ./tools/setup/install-dependencies-debian.sh
+ uses: ./.github/actions/install-dependencies
- name: Setup Caching
uses: ./.github/actions/cache
@@ -79,16 +92,18 @@ jobs:
target: ${{ matrix.arch }}
build-type: ${{ matrix.build_type }}
cpm-modules: ${{ runner.temp }}/build/cpm_modules
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
- name: Install Qt for Linux
uses: jurplel/install-qt-action@v4
with:
- version: ${{ env.QT_VERSION }}
+ version: ${{ steps.build-config.outputs.qt_version }}
host: ${{ matrix.host }}
target: desktop
arch: ${{ matrix.arch }}
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ steps.build-config.outputs.qt_modules }}
setup-python: false
cache: true
@@ -134,4 +149,3 @@ jobs:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }}
- github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/lupdate.yaml b/.github/workflows/lupdate.yml
similarity index 80%
rename from .github/workflows/lupdate.yaml
rename to .github/workflows/lupdate.yml
index 5418aa2420b2..b5469f7be21b 100644
--- a/.github/workflows/lupdate.yaml
+++ b/.github/workflows/lupdate.yml
@@ -2,19 +2,23 @@ name: Update Translations
on:
workflow_dispatch:
-# schedule:
-# - cron: '0 0 * * 0'
+ schedule:
+ - cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
concurrency:
group: 'lupdate'
cancel-in-progress: true
-permissions:
- contents: write
-
jobs:
update-translations:
+ name: Update Translations
runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ defaults:
+ run:
+ shell: bash
+
permissions:
contents: write
pull-requests: write
@@ -23,10 +27,14 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v6
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
- name: Install Qt for Linux
uses: jurplel/install-qt-action@v4
with:
- version: 6.10.1
+ version: ${{ steps.build-config.outputs.qt_version }}
cache: true
- name: Update translation files
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
index 3730dd03fbb9..c51eed2a19d5 100644
--- a/.github/workflows/macos.yml
+++ b/.github/workflows/macos.yml
@@ -12,30 +12,44 @@ on:
pull_request:
paths:
- '.github/workflows/macos.yml'
+ - '.github/actions/**'
- 'deploy/macos/**'
- 'src/**'
+ - 'test/**'
- 'CMakeLists.txt'
- 'cmake/**'
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+ workflow_dispatch:
+ inputs:
+ build_type:
+ description: 'Build type'
+ required: false
+ default: 'Release'
+ type: choice
+ options:
+ - Release
+ - Debug
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
jobs:
build:
+ name: macOS
runs-on: macos-latest
+ timeout-minutes: 120
strategy:
matrix:
- BuildType: [Release]
+ build_type: [Release]
defaults:
run:
shell: bash
- env:
- QT_VERSION: 6.10.1
-
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -44,48 +58,53 @@ jobs:
fetch-depth: 1
fetch-tags: true
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
- name: Initial Setup
uses: ./.github/actions/common
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
- xcode-version: '<=16.x'
+ xcode-version: ${{ steps.build-config.outputs.xcode_version }}
- - name: Install Dependencies (include GStreamer)
- working-directory: ${{ github.workspace }}/tools/setup
- run: sh install-dependencies-osx.sh
+ - name: Install Dependencies
+ uses: ./.github/actions/install-dependencies
- name: Setup Caching
uses: ./.github/actions/cache
with:
host: mac
target: clang_64
- build-type: ${{ matrix.BuildType }}
+ build-type: ${{ matrix.build_type }}
cpm-modules: ${{ runner.temp }}/build/cpm_modules
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
- version: ${{ env.QT_VERSION }}
+ version: ${{ steps.build-config.outputs.qt_version }}
host: mac
target: desktop
arch: clang_64
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ steps.build-config.outputs.qt_modules }}
setup-python: false
cache: true
- name: CMake configure
working-directory: ${{ runner.temp }}/build
run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja
- -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }}
+ -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }}
-DQGC_MACOS_SIGN_WITH_IDENTITY=${{ github.event_name != 'pull_request' && 'ON' || 'OFF' }}
- name: Build
working-directory: ${{ runner.temp }}/build
- run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel
+ run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel
- name: Sanity check dev build executable
working-directory: ${{ runner.temp }}/build/Release/QGroundControl.app/Contents/MacOS
@@ -100,7 +119,7 @@ jobs:
- name: Create signed/notarized/stapled app bundle
working-directory: ${{ runner.temp }}/build
- run: cmake --install . --config ${{ matrix.BuildType }}
+ run: cmake --install . --config ${{ matrix.build_type }}
env:
QGC_MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
QGC_MACOS_NOTARIZATION_USERNAME: ${{ secrets.MACOS_NOTARIZATION_USERNAME }}
@@ -116,7 +135,7 @@ jobs:
run: ./QGroundControl --simple-boot-test
- name: Upload Build File
- if: matrix.BuildType == 'Release'
+ if: matrix.build_type == 'Release'
uses: ./.github/actions/upload
with:
artifact_name: QGroundControl.dmg
@@ -124,4 +143,3 @@ jobs:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }}
- github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index f484f6b15571..08c984da7cf5 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -1,12 +1,44 @@
name: pre-commit
-on: [push, pull_request]
+on:
+ push:
+ paths:
+ - 'src/**'
+ - 'test/**'
+ - '.pre-commit-config.yaml'
+ - '.github/workflows/pre-commit.yml'
+ pull_request:
+ paths:
+ - 'src/**'
+ - 'test/**'
+ - '.pre-commit-config.yaml'
+ - '.github/workflows/pre-commit.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: read
jobs:
pre-commit:
+ name: Pre-commit Checks
runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ defaults:
+ run:
+ shell: bash
+
steps:
- uses: actions/checkout@v6
+ with:
+ fetch-tags: true
+
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
- uses: actions/setup-python@v6
- name: Install Qt and qmllint
@@ -17,7 +49,7 @@ jobs:
pipx ensurepath
USER_BASE_BIN="$(python3 -m site --user-base)/bin"
export PATH="$USER_BASE_BIN:$PATH"
- QT_VERSION=6.10.1
+ QT_VERSION=${{ steps.build-config.outputs.qt_version }}
QT_ARCH=linux_gcc_64
QT_PATH=/opt/Qt
aqt install-qt linux desktop $QT_VERSION $QT_ARCH -O $QT_PATH
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000000..2317bb26f0d7
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,211 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to create release for (e.g., v4.4.0)'
+ required: true
+ type: string
+
+permissions:
+ contents: write
+
+jobs:
+ wait-for-builds:
+ name: Wait for Platform Builds
+ runs-on: ubuntu-latest
+ timeout-minutes: 180
+
+ steps:
+ - name: Wait for builds to complete
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const tag = context.ref.replace('refs/tags/', '') || '${{ inputs.tag }}';
+ console.log(`Waiting for builds for tag: ${tag}`);
+
+ // Workflows to wait for
+ const requiredWorkflows = ['Linux', 'Windows', 'MacOS', 'Android'];
+ const maxWait = 170 * 60 * 1000; // 170 minutes
+ const pollInterval = 60 * 1000; // 1 minute
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < maxWait) {
+ const runs = await github.rest.actions.listWorkflowRunsForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ head_sha: context.sha,
+ per_page: 100
+ });
+
+ const workflowStatus = {};
+ for (const run of runs.data.workflow_runs) {
+ if (requiredWorkflows.includes(run.name)) {
+ workflowStatus[run.name] = run.status === 'completed' ? run.conclusion : run.status;
+ }
+ }
+
+ console.log('Current status:', JSON.stringify(workflowStatus, null, 2));
+
+ const allComplete = requiredWorkflows.every(w =>
+ workflowStatus[w] === 'success' || workflowStatus[w] === 'failure'
+ );
+
+ if (allComplete) {
+ const failed = requiredWorkflows.filter(w => workflowStatus[w] !== 'success');
+ if (failed.length > 0) {
+ core.setFailed(`Builds failed: ${failed.join(', ')}`);
+ return;
+ }
+ console.log('All builds completed successfully!');
+ return;
+ }
+
+ console.log(`Waiting ${pollInterval / 1000}s for builds to complete...`);
+ await new Promise(r => setTimeout(r, pollInterval));
+ }
+
+ core.setFailed('Timeout waiting for builds to complete');
+
+ create-release:
+ name: Create GitHub Release
+ needs: wait-for-builds
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Get tag info
+ id: tag
+ run: |
+ TAG="${GITHUB_REF#refs/tags/}"
+ if [ -z "$TAG" ] || [ "$TAG" = "$GITHUB_REF" ]; then
+ TAG="${{ inputs.tag }}"
+ fi
+ echo "tag=$TAG" >> $GITHUB_OUTPUT
+ echo "Tag: $TAG"
+
+ # Check if this is a prerelease
+ if [[ "$TAG" == *"-"* ]]; then
+ echo "prerelease=true" >> $GITHUB_OUTPUT
+ else
+ echo "prerelease=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v5
+ with:
+ path: artifacts
+ merge-multiple: true
+
+ - name: List artifacts
+ run: find artifacts -type f | head -50 || echo "No artifacts found"
+
+ - name: Generate changelog
+ id: changelog
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const tag = '${{ steps.tag.outputs.tag }}';
+
+ // Get previous tag
+ const tags = await github.rest.repos.listTags({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ per_page: 10
+ });
+
+ let previousTag = null;
+ for (let i = 0; i < tags.data.length; i++) {
+ if (tags.data[i].name === tag && i + 1 < tags.data.length) {
+ previousTag = tags.data[i + 1].name;
+ break;
+ }
+ }
+
+ console.log(`Generating changelog from ${previousTag || 'beginning'} to ${tag}`);
+
+ // Get commits between tags
+ let commits;
+ if (previousTag) {
+ const comparison = await github.rest.repos.compareCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ base: previousTag,
+ head: tag
+ });
+ commits = comparison.data.commits;
+ } else {
+ const commitList = await github.rest.repos.listCommits({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ sha: tag,
+ per_page: 50
+ });
+ commits = commitList.data;
+ }
+
+ // Categorize commits
+ const categories = {
+ 'Features': [],
+ 'Bug Fixes': [],
+ 'Documentation': [],
+ 'CI/Build': [],
+ 'Other': []
+ };
+
+ for (const commit of commits) {
+ const msg = commit.commit.message.split('\n')[0];
+ const sha = commit.sha.substring(0, 7);
+ const entry = `- ${msg} (${sha})`;
+
+ if (msg.match(/^feat|^add|^new/i)) {
+ categories['Features'].push(entry);
+ } else if (msg.match(/^fix|^bug/i)) {
+ categories['Bug Fixes'].push(entry);
+ } else if (msg.match(/^doc/i)) {
+ categories['Documentation'].push(entry);
+ } else if (msg.match(/^ci|^build|^chore/i)) {
+ categories['CI/Build'].push(entry);
+ } else {
+ categories['Other'].push(entry);
+ }
+ }
+
+ // Build changelog
+ let changelog = '';
+ for (const [category, entries] of Object.entries(categories)) {
+ if (entries.length > 0) {
+ changelog += `### ${category}\n${entries.join('\n')}\n\n`;
+ }
+ }
+
+ if (!changelog) {
+ changelog = 'No notable changes in this release.';
+ }
+
+ const fs = require('fs');
+ fs.writeFileSync('CHANGELOG.md', changelog);
+ return changelog;
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: ${{ steps.tag.outputs.tag }}
+ name: QGroundControl ${{ steps.tag.outputs.tag }}
+ body_path: CHANGELOG.md
+ prerelease: ${{ steps.tag.outputs.prerelease }}
+ files: |
+ artifacts/**/*.AppImage
+ artifacts/**/*.dmg
+ artifacts/**/*.exe
+ artifacts/**/*.apk
+ fail_on_unmatched_files: false
diff --git a/.github/workflows/repo-stats.yml b/.github/workflows/repo-stats.yml
new file mode 100644
index 000000000000..7e2fcf387292
--- /dev/null
+++ b/.github/workflows/repo-stats.yml
@@ -0,0 +1,130 @@
+name: Repository Stats
+
+on:
+ schedule:
+ - cron: '0 9 * * 1' # Weekly on Monday at 9 AM UTC
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ stats:
+ name: Generate Weekly Stats
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - name: Generate statistics
+ id: stats
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const now = new Date();
+ const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
+ const weekAgoISO = weekAgo.toISOString();
+
+ // Get repo stats
+ const repo = await github.rest.repos.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo
+ });
+
+ // Get issues opened this week
+ const newIssues = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'all',
+ since: weekAgoISO,
+ per_page: 100
+ });
+
+ const issuesOpened = newIssues.data.filter(i =>
+ !i.pull_request && new Date(i.created_at) > weekAgo
+ ).length;
+
+ const issuesClosed = newIssues.data.filter(i =>
+ !i.pull_request && i.state === 'closed' && i.closed_at && new Date(i.closed_at) > weekAgo
+ ).length;
+
+ // Get PRs
+ const prsOpened = newIssues.data.filter(i =>
+ i.pull_request && new Date(i.created_at) > weekAgo
+ ).length;
+
+ const prsClosed = newIssues.data.filter(i =>
+ i.pull_request && i.state === 'closed' && i.closed_at && new Date(i.closed_at) > weekAgo
+ ).length;
+
+ // Get stale issues count
+ const staleIssues = await github.rest.issues.listForRepo({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ labels: 'stale',
+ per_page: 1
+ });
+
+ const netIssues = issuesOpened - issuesClosed;
+ const netPRs = prsOpened - prsClosed;
+ const stats = [
+ '## 📊 Weekly Repository Stats',
+ '',
+ `**Week of ${weekAgo.toDateString()} - ${now.toDateString()}**`,
+ '',
+ '### Repository',
+ '| Metric | Count |',
+ '|--------|-------|',
+ `| ⭐ Stars | ${repo.data.stargazers_count.toLocaleString()} |`,
+ `| 🍴 Forks | ${repo.data.forks_count.toLocaleString()} |`,
+ `| 👀 Watchers | ${repo.data.subscribers_count.toLocaleString()} |`,
+ '',
+ '### This Week\'s Activity',
+ '| Metric | Opened | Closed | Net |',
+ '|--------|--------|--------|-----|',
+ `| Issues | ${issuesOpened} | ${issuesClosed} | ${netIssues >= 0 ? '+' : ''}${netIssues} |`,
+ `| PRs | ${prsOpened} | ${prsClosed} | ${netPRs >= 0 ? '+' : ''}${netPRs} |`,
+ '',
+ '### Current State',
+ '| Metric | Count |',
+ '|--------|-------|',
+ `| 📋 Open Issues/PRs | ${repo.data.open_issues_count} |`,
+ `| 🏷️ Stale Items | ${staleIssues.data.length > 0 ? '1+' : '0'} |`,
+ '',
+ '---',
+ `*Generated automatically by GitHub Actions on ${now.toISOString()}*`
+ ].join('\n');
+
+ console.log(stats);
+
+ // Output to workflow summary
+ const fs = require('fs');
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, stats);
+
+ // Save stats as JSON for historical tracking
+ const jsonStats = {
+ date: now.toISOString(),
+ week_start: weekAgo.toISOString(),
+ repository: {
+ stars: repo.data.stargazers_count,
+ forks: repo.data.forks_count,
+ watchers: repo.data.subscribers_count
+ },
+ weekly: {
+ issues_opened: issuesOpened,
+ issues_closed: issuesClosed,
+ prs_opened: prsOpened,
+ prs_closed: prsClosed
+ },
+ current: {
+ open_issues: repo.data.open_issues_count
+ }
+ };
+ fs.writeFileSync('stats.json', JSON.stringify(jsonStats, null, 2));
+
+ - name: Upload stats artifact
+ uses: actions/upload-artifact@v5
+ with:
+ name: repo-stats-${{ github.run_id }}
+ path: stats.json
+ retention-days: 90
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 000000000000..ddefc8be57ae
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,39 @@
+name: OpenSSF Scorecard
+
+on:
+ push:
+ branches:
+ - master
+ schedule:
+ - cron: '0 5 * * 1' # Weekly on Monday at 5 AM UTC
+ workflow_dispatch:
+
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard Analysis
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ permissions:
+ security-events: write
+ id-token: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
+ - name: Run Scorecard
+ uses: ossf/scorecard-action@v2.4.0
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ publish_results: true
+
+ - name: Upload results to GitHub Security tab
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
diff --git a/.github/workflows/size-check.yml b/.github/workflows/size-check.yml
new file mode 100644
index 000000000000..7221446828b5
--- /dev/null
+++ b/.github/workflows/size-check.yml
@@ -0,0 +1,123 @@
+name: Size Check
+
+on:
+ workflow_run:
+ workflows: [Linux, Windows, MacOS, Android]
+ types: [completed]
+ branches: [master]
+
+permissions:
+ contents: read
+ actions: read
+
+jobs:
+ check-size:
+ name: Check Artifact Sizes
+ runs-on: ubuntu-latest
+ if: github.event.workflow_run.conclusion == 'success'
+ timeout-minutes: 15
+
+ steps:
+ - name: Download artifacts
+ uses: actions/download-artifact@v5
+ with:
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path: artifacts
+
+ - name: Check sizes
+ id: sizes
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const path = require('path');
+
+ // Size thresholds in bytes
+ const thresholds = {
+ 'AppImage': 200 * 1024 * 1024, // 200 MB
+ 'dmg': 150 * 1024 * 1024, // 150 MB
+ 'exe': 200 * 1024 * 1024, // 200 MB
+ 'apk': 150 * 1024 * 1024 // 150 MB
+ };
+
+ // Warning threshold (% increase from baseline)
+ const warningPercent = 10;
+
+ function getFiles(dir, files = []) {
+ if (!fs.existsSync(dir)) return files;
+ const items = fs.readdirSync(dir);
+ for (const item of items) {
+ const fullPath = path.join(dir, item);
+ if (fs.statSync(fullPath).isDirectory()) {
+ getFiles(fullPath, files);
+ } else {
+ files.push(fullPath);
+ }
+ }
+ return files;
+ }
+
+ const files = getFiles('artifacts');
+ const sizes = {};
+ const warnings = [];
+
+ for (const file of files) {
+ const stats = fs.statSync(file);
+ const ext = path.extname(file).slice(1);
+ const name = path.basename(file);
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
+
+ sizes[name] = {
+ bytes: stats.size,
+ mb: sizeMB,
+ extension: ext
+ };
+
+ console.log(`${name}: ${sizeMB} MB`);
+
+ // Check against threshold
+ const threshold = thresholds[ext];
+ if (threshold && stats.size > threshold) {
+ const thresholdMB = (threshold / (1024 * 1024)).toFixed(0);
+ warnings.push(`⚠️ ${name} (${sizeMB} MB) exceeds ${thresholdMB} MB threshold`);
+ }
+ }
+
+ // Output sizes as JSON
+ fs.writeFileSync('sizes.json', JSON.stringify(sizes, null, 2));
+
+ // Create summary
+ let summary = '## 📦 Artifact Sizes\n\n';
+ summary += '| Artifact | Size |\n';
+ summary += '|----------|------|\n';
+
+ for (const [name, info] of Object.entries(sizes)) {
+ summary += `| ${name} | ${info.mb} MB |\n`;
+ }
+
+ if (warnings.length > 0) {
+ summary += '\n### ⚠️ Warnings\n';
+ summary += warnings.join('\n');
+ }
+
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
+
+ return { sizes, warnings };
+
+ - name: Upload size report
+ uses: actions/upload-artifact@v5
+ with:
+ name: size-report-${{ github.event.workflow_run.id }}
+ path: sizes.json
+ retention-days: 90
+
+ - name: Warn on size regression
+ if: steps.sizes.outputs.result != ''
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const result = ${{ steps.sizes.outputs.result }};
+ if (result.warnings && result.warnings.length > 0) {
+ core.warning(`Artifact size warnings:\n${result.warnings.join('\n')}`);
+ }
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index ddfd97eae73c..36f56fd189ca 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -3,14 +3,27 @@ on:
schedule:
- cron: '30 1 * * *'
+permissions:
+ issues: write
+ pull-requests: write
+
jobs:
stale:
+ name: Mark Stale Issues
runs-on: ubuntu-latest
+ timeout-minutes: 15
steps:
- uses: actions/stale@v10
with:
- days-before-stale: 30
+ days-before-stale: 90
days-before-close: -1
stale-issue-label: 'stale'
stale-pr-label: 'stale'
remove-stale-when-updated: true
+ stale-issue-message: >
+ This issue has been automatically marked as stale due to 90 days of inactivity.
+ It will remain open but may receive lower priority. If this is still relevant,
+ please comment to remove the stale label.
+ stale-pr-message: >
+ This PR has been automatically marked as stale due to 90 days of inactivity.
+ Please rebase and update if still relevant, or it may be closed in the future.
diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml
new file mode 100644
index 000000000000..7a5c3efc35ed
--- /dev/null
+++ b/.github/workflows/welcome.yml
@@ -0,0 +1,40 @@
+name: Welcome
+
+on:
+ pull_request_target:
+ types: [opened]
+ issues:
+ types: [opened]
+
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ welcome:
+ name: Welcome First-time Contributors
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+
+ steps:
+ - uses: actions/first-interaction@v1
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ issue-message: |
+ Thanks for opening your first issue! 👋
+
+ A maintainer will review this soon. In the meantime:
+ - 📖 Check our [User Guide](https://docs.qgroundcontrol.com/) and [Dev Guide](https://dev.qgroundcontrol.com/)
+ - 💬 Join [GitHub Discussions](https://github.com/mavlink/qgroundcontrol/discussions) for Q&A
+ - 🔍 Search [existing issues](https://github.com/mavlink/qgroundcontrol/issues) for similar reports
+
+ Please make sure you've provided all requested information in the template.
+ pr-message: |
+ Thanks for your first pull request! 🎉
+
+ A maintainer will review this soon. Please ensure:
+ - [ ] CI checks pass
+ - [ ] Code follows [coding standards](https://github.com/mavlink/qgroundcontrol/blob/master/CODING_STYLE.md)
+ - [ ] Changes tested on relevant platforms
+
+ We appreciate your contribution to QGroundControl!
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 25189b49d67b..065f6dfd3718 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -4,28 +4,45 @@ on:
push:
branches:
- master
- - Stable*
+ - 'Stable*'
tags:
- - v*
+ - 'v*'
paths-ignore:
- - docs/**
+ - 'docs/**'
pull_request:
paths:
- - .github/workflows/windows.yml
- - deploy/windows/**
- - src/**
- - CMakeLists.txt
- - cmake/**
- - tools/setup/*windows*
-
-# concurrency:
-# group: ${{ github.workflow }}-${{ github.ref }}
-# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
+ - '.github/workflows/windows.yml'
+ - '.github/actions/**'
+ - 'deploy/windows/**'
+ - 'src/**'
+ - 'test/**'
+ - 'custom-example/**'
+ - 'CMakeLists.txt'
+ - 'cmake/**'
+ - 'tools/setup/*windows*'
+ workflow_dispatch:
+ inputs:
+ build_type:
+ description: 'Build type'
+ required: false
+ default: 'Release'
+ type: choice
+ options:
+ - Release
+ - Debug
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
+
+permissions:
+ contents: read
jobs:
build:
- name: Build ${{ matrix.arch }} ${{ matrix.build_type }}
+ name: Build ${{ matrix.custom_build && 'Custom' || matrix.arch }} ${{ matrix.build_type }}
runs-on: ${{ matrix.os }}
+ timeout-minutes: 120
strategy:
fail-fast: false
@@ -52,13 +69,17 @@ jobs:
package: QGroundControl-installer-AMD64-ARM64
qt_version: '6.10.0'
+ # Custom build example (tests custom-example template)
+ - os: windows-latest
+ host: windows
+ arch: win64_msvc2022_64
+ build_type: Release
+ custom_build: true
+
defaults:
run:
shell: cmd
- env:
- QT_VERSION: 6.10.1
-
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -67,18 +88,22 @@ jobs:
fetch-depth: 1
fetch-tags: true
+ - name: Enable custom build
+ if: matrix.custom_build
+ uses: ./.github/actions/custom-build
+
+ - name: Get build config
+ id: build-config
+ uses: ./.github/actions/build-config
+
- name: Common setup
uses: ./.github/actions/common
- # - name: Install dependencies
- # shell: pwsh
- # run: ./tools/setup/install-dependencies-windows.ps1 -Force
-
- name: Install GStreamer
if: matrix.arch == 'win64_msvc2022_64'
uses: blinemedical/setup-gstreamer@v1
with:
- version: 1.22.12
+ version: ${{ steps.build-config.outputs.gstreamer_version }}
- name: Setup cache
uses: ./.github/actions/cache
@@ -87,16 +112,18 @@ jobs:
target: ${{ matrix.arch }}
build-type: ${{ matrix.build_type }}
cpm-modules: ${{ runner.temp }}\build\cpm_modules
+ ccache-version: ${{ steps.build-config.outputs.ccache_version }}
+ save-cache: ${{ github.event_name != 'pull_request' }}
- - name: Install Qt ${{ matrix.qt_version || env.QT_VERSION }}
+ - name: Install Qt ${{ matrix.qt_version || steps.build-config.outputs.qt_version }}
uses: jurplel/install-qt-action@v4
with:
- version: ${{ matrix.qt_version || env.QT_VERSION }}
+ version: ${{ matrix.qt_version || steps.build-config.outputs.qt_version }}
host: ${{ matrix.host }}
target: desktop
arch: ${{ matrix.arch }}
dir: ${{ runner.temp }}
- modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml
+ modules: ${{ steps.build-config.outputs.qt_modules }}
setup-python: false
cache: true
@@ -121,14 +148,15 @@ jobs:
- name: Verify Executable
if: matrix.build_type == 'Release' && matrix.arch != 'win64_msvc2022_arm64_cross_compiled'
working-directory: ${{ runner.temp }}\build\${{ matrix.build_type }}
- run: QGroundControl.exe --simple-boot-test
+ run: ${{ matrix.custom_build && 'Custom-QGroundControl.exe' || 'QGroundControl.exe' }} --simple-boot-test
- name: Create Installer
+ if: ${{ !matrix.custom_build }}
working-directory: ${{ runner.temp }}\build
run: cmake --install . --config ${{ matrix.build_type }}
- name: Upload artifact (installer)
- if: matrix.build_type == 'Release'
+ if: matrix.build_type == 'Release' && !matrix.custom_build
uses: ./.github/actions/upload
with:
artifact_name: ${{ matrix.package }}.exe
@@ -136,5 +164,4 @@ jobs:
aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }}
- github_token: ${{ secrets.GITHUB_TOKEN }}
upload_aws: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }}
diff --git a/.gitignore b/.gitignore
index 2e3cd5566edd..1844b942c44d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,11 @@
# IDEs & Editors
# ------------------------------------------------------------------------------
.idea/
-.vscode/
+.vscode/*
+!.vscode/settings.json
+!.vscode/extensions.json
+!.vscode/tasks.json
+!.vscode/launch.json
.qtcreator/
.fleet/
.vs/
@@ -71,8 +75,9 @@ bin/mac
*.dylib
*.dll
-# Build tool artifacts
-Makefile
+# Build tool artifacts (CMake-generated)
+**/Makefile
+!/Makefile
*.ninja
.ninja_log
build.ninja
@@ -152,9 +157,13 @@ Qt*-linux*.tar.*
# Claude Code
.claude/
+# AI assistant config files
+AGENTS.md
+CLAUDE.md
+GEMINI.md
+
# cc-sessions - personal AI development workflow
sessions/
-CLAUDE.md
# Pre-commit hooks
.ruff_cache/
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 000000000000..d093e4f534c4
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,30 @@
+# Git author name/email normalization
+# Format: Canonical Name Alternate Name
+
+# Don Gagne - primary maintainer
+Don Gagne
+Don Gagne
+Don Gagne DonLakeFlyer
+Don Gagne
+Don Gagne DoinLakeFlyer
+Don Gagne
+Don Gagne
+Don Gagne Donald Gagne
+Don Gagne Don Gagne <>
+
+# Gus Grubba
+Gus Grubba
+Gus Grubba
+Gus Grubba dogmaphobic
+
+# Lorenz Meier - founder
+Lorenz Meier
+Lorenz Meier
+Lorenz Meier LM
+Lorenz Meier lm
+Lorenz Meier lm
+Lorenz Meier pixhawk
+
+# Bots
+PX4BuildBot
+PX4BuildBot PX4 Build Bot
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 000000000000..209e3ef4b624
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 62121cbb2eb5..241836389501 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -11,6 +11,7 @@ repos:
- id: check-yaml
args: [--allow-multiple-documents, --unsafe]
- id: check-json
+ exclude: ^\.vscode/ # VSCode uses JSONC (JSON with comments)
- id: check-xml
exclude: ^test/
- id: check-merge-conflict
@@ -47,6 +48,7 @@ repos:
rev: v0.11.0.1
hooks:
- id: shellcheck
+ args: ['-x', '-e', 'SC1091'] # -x follows sources, -e excludes SC1091 (not following)
# YAML validation
- repo: https://github.com/adrienverge/yamllint
@@ -86,19 +88,83 @@ repos:
- id: check-no-qassert
name: Check for Q_ASSERT in production code
- entry: bash
+ entry: python3
language: system
files: \.(h|cc|cpp)$
- exclude: ^(test/|UnitTest\.h|UnitTest\.cc)
- pass_filenames: false
+ exclude: ^test/
+ pass_filenames: true
+ verbose: true
+ args:
+ - -c
+ - |
+ import sys, re
+ found = []
+ for filepath in sys.argv[1:]:
+ try:
+ with open(filepath) as f:
+ for i, line in enumerate(f, 1):
+ if 'Q_ASSERT' in line and not line.strip().startswith('//'):
+ found.append(f"{filepath}:{i}: {line.rstrip()}")
+ except Exception:
+ pass
+ if found:
+ print("Warning: Q_ASSERT found. Consider defensive checks with early returns instead.")
+ print("Q_ASSERT is removed in release builds and can hide bugs.\n")
+ for f in found:
+ print(f" {f}")
+ # Exit 0 to warn but not block (change to sys.exit(1) to enforce)
+ sys.exit(0)
+
+ - id: check-old-qml-connections
+ name: Check for deprecated QML Connections syntax
+ entry: python3
+ language: system
+ files: \.qml$
+ exclude: ^build/
+ pass_filenames: true
args:
- -c
- |
- FILES=$(git diff --cached --name-only --diff-filter=d | grep -E '\.(h|cc|cpp)$' | grep -v '^test/' || true)
- if [ -n "$FILES" ]; then
- if echo "$FILES" | xargs grep -n 'Q_ASSERT' 2>/dev/null; then
- echo "Warning: Q_ASSERT found in production code. Consider using defensive checks with early returns."
- fi
- fi
+ import sys, re
+
+ # Pattern for old-style signal handlers: onSignalName: (not function onSignalName)
+ OLD_HANDLER = re.compile(r'^\s+on[A-Z][a-zA-Z0-9]*\s*:\s*[^/]')
+ CONNECTIONS_START = re.compile(r'Connections\s*\{')
+ FUNCTION_HANDLER = re.compile(r'^\s+function\s+on[A-Z]')
+
+ errors = []
+ for filepath in sys.argv[1:]:
+ try:
+ with open(filepath) as f:
+ content = f.read()
+ lines = content.split('\n')
+
+ # Only check files that have Connections blocks
+ if 'Connections' not in content:
+ continue
+
+ in_connections = 0
+ for i, line in enumerate(lines, 1):
+ if CONNECTIONS_START.search(line):
+ in_connections += 1
+ elif in_connections > 0:
+ if '{' in line:
+ in_connections += line.count('{')
+ if '}' in line:
+ in_connections -= line.count('}')
+ # Check for old-style handler inside Connections
+ if OLD_HANDLER.match(line) and not FUNCTION_HANDLER.match(line):
+ errors.append(f"{filepath}:{i}: {line.strip()}")
+ except Exception as e:
+ print(f"Error reading {filepath}: {e}", file=sys.stderr)
+
+ if errors:
+ print("Deprecated QML Connections syntax found. Use 'function onSignal()' instead of 'onSignal:'")
+ print("See: https://doc.qt.io/qt-6/qml-qtqml-connections.html")
+ print()
+ for err in errors:
+ print(f" {err}")
+ # Exit 0 to warn but not block (change to sys.exit(1) to enforce)
+ sys.exit(0)
exclude: ^build/
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000000..b647e56bf51a
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://json.schemastore.org/prettierrc",
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "es5",
+ "bracketSpacing": true,
+ "proseWrap": "preserve",
+ "endOfLine": "lf",
+ "overrides": [
+ {
+ "files": "*.md",
+ "options": {
+ "tabWidth": 2,
+ "proseWrap": "preserve"
+ }
+ },
+ {
+ "files": ["*.yml", "*.yaml"],
+ "options": {
+ "tabWidth": 2,
+ "singleQuote": false
+ }
+ },
+ {
+ "files": "*.json",
+ "options": {
+ "tabWidth": 2
+ }
+ }
+ ]
+}
diff --git a/.qmlformat.ini b/.qmlformat.ini
new file mode 100644
index 000000000000..17f19ccedfe1
--- /dev/null
+++ b/.qmlformat.ini
@@ -0,0 +1,8 @@
+# QML Formatting Configuration
+# https://doc.qt.io/qt-6/qtquick-tool-qmlformat.html
+
+[General]
+IndentWidth=4
+UseTabs=false
+NormalizeOrder=true
+NewlineType=native
diff --git a/.typos.toml b/.typos.toml
new file mode 100644
index 000000000000..fa402cb0449a
--- /dev/null
+++ b/.typos.toml
@@ -0,0 +1,57 @@
+# Typos Configuration
+# https://github.com/crate-ci/typos
+
+[default]
+extend-ignore-identifiers-re = [
+ # Hexadecimal values
+ "[0-9a-fA-F]+",
+]
+
+[default.extend-words]
+# Project-specific terms
+QGroundControl = "QGroundControl"
+MAVLink = "MAVLink"
+ArduPilot = "ArduPilot"
+PX4 = "PX4"
+
+# Common abbreviations in codebase
+Ack = "Ack"
+ack = "ack"
+Nack = "Nack"
+nack = "nack"
+Navit = "Navit"
+Ser = "Ser"
+ser = "ser"
+TELEM = "TELEM"
+telem = "telem"
+RSSI = "RSSI"
+rssi = "rssi"
+VTOL = "VTOL"
+vtol = "vtol"
+ADSB = "ADSB"
+adsb = "adsb"
+RTK = "RTK"
+rtk = "rtk"
+RTL = "RTL"
+rtl = "rtl"
+HSV = "HSV"
+NED = "NED"
+ENU = "ENU"
+FTP = "FTP"
+ftp = "ftp"
+ULog = "ULog"
+ulog = "ulog"
+
+# Parameter names that look like typos
+Slew = "Slew"
+slew = "slew"
+
+[files]
+extend-exclude = [
+ "build/",
+ "translations/",
+ "*.ts",
+ "libs/",
+ "*.json",
+ "*.geojson",
+]
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 000000000000..011925b20f0e
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,19 @@
+{
+ "recommendations": [
+ "ms-vscode.cpptools",
+ "ms-vscode.cmake-tools",
+ "twxs.cmake",
+ "llvm-vs-code-extensions.vscode-clangd",
+ "AoiHosizora.vscode-qt-qmake",
+ "nicothin.qt-qml",
+ "streetsidesoftware.code-spell-checker",
+ "DavidAnson.vscode-markdownlint",
+ "EditorConfig.EditorConfig",
+ "eamodio.gitlens",
+ "mhutchie.git-graph",
+ "ms-vscode.hexeditor",
+ "redhat.vscode-yaml",
+ "tamasfe.even-better-toml"
+ ],
+ "unwantedRecommendations": []
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000000..e46f01b9fe57
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,114 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Launch QGroundControl",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/QGroundControl",
+ "args": [],
+ "stopAtEntry": false,
+ "cwd": "${workspaceFolder}",
+ "environment": [
+ {
+ "name": "QT_QPA_PLATFORM",
+ "value": "xcb"
+ },
+ {
+ "name": "QML_IMPORT_TRACE",
+ "value": "0"
+ }
+ ],
+ "externalConsole": false,
+ "MIMode": "gdb",
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ },
+ {
+ "description": "Load Qt pretty printers",
+ "text": "source ${workspaceFolder}/tools/gdb-pretty-printers/qt6.py",
+ "ignoreFailures": true
+ }
+ ],
+ "preLaunchTask": "CMake: Build"
+ },
+ {
+ "name": "Launch QGroundControl (LLDB)",
+ "type": "lldb",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/QGroundControl",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "env": {
+ "QT_QPA_PLATFORM": "cocoa",
+ "QML_IMPORT_TRACE": "0"
+ },
+ "preLaunchTask": "CMake: Build"
+ },
+ {
+ "name": "Run Unit Tests (Debug)",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/QGroundControl",
+ "args": ["--unittest"],
+ "stopAtEntry": false,
+ "cwd": "${workspaceFolder}",
+ "environment": [],
+ "externalConsole": false,
+ "MIMode": "gdb",
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ }
+ ],
+ "preLaunchTask": "CMake: Build"
+ },
+ {
+ "name": "Debug Specific Test",
+ "type": "cppdbg",
+ "request": "launch",
+ "program": "${workspaceFolder}/build/QGroundControl",
+ "args": ["--unittest:${input:testClass}"],
+ "stopAtEntry": false,
+ "cwd": "${workspaceFolder}",
+ "environment": [],
+ "externalConsole": false,
+ "MIMode": "gdb",
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ }
+ ],
+ "preLaunchTask": "CMake: Build"
+ },
+ {
+ "name": "Attach to QGroundControl",
+ "type": "cppdbg",
+ "request": "attach",
+ "program": "${workspaceFolder}/build/QGroundControl",
+ "processId": "${command:pickProcess}",
+ "MIMode": "gdb",
+ "setupCommands": [
+ {
+ "description": "Enable pretty-printing for gdb",
+ "text": "-enable-pretty-printing",
+ "ignoreFailures": true
+ }
+ ]
+ }
+ ],
+ "inputs": [
+ {
+ "id": "testClass",
+ "type": "promptString",
+ "description": "Test class name (e.g., FactSystemTest)"
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000000..590c6c6f4bde
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,45 @@
+{
+ "editor.formatOnSave": false,
+ "editor.rulers": [120],
+ "editor.tabSize": 4,
+ "editor.insertSpaces": true,
+ "files.trimTrailingWhitespace": true,
+ "files.insertFinalNewline": true,
+ "files.associations": {
+ "*.qml": "qml",
+ "*.SettingsGroup.json": "json",
+ "*.FactMetaData.json": "json",
+ "CMakePresets.json": "jsonc"
+ },
+ "files.watcherExclude": {
+ "**/build/**": true,
+ "**/.cache/**": true
+ },
+ "search.exclude": {
+ "**/build": true,
+ "**/.cache": true,
+ "**/libs": true
+ },
+ "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
+ "C_Cpp.codeAnalysis.clangTidy.enabled": true,
+ "C_Cpp.codeAnalysis.clangTidy.useBuildPath": true,
+ "C_Cpp.formatting.clangFormatStyle": "file",
+ "C_Cpp.default.cppStandard": "c++20",
+ "cmake.configureOnOpen": true,
+ "cmake.buildDirectory": "${workspaceFolder}/build",
+ "cmake.generator": "Ninja",
+ "cmake.parallelJobs": 0,
+ "clangd.arguments": [
+ "--background-index",
+ "--clang-tidy",
+ "--header-insertion=never",
+ "--completion-style=detailed",
+ "--function-arg-placeholders=false"
+ ],
+ "qml.format.enabled": true,
+ "python.defaultInterpreterPath": "python3",
+ "[markdown]": {
+ "editor.wordWrap": "on"
+ },
+ "git.ignoreLimitWarning": true
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000000..a28691a6886a
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,116 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "CMake: Configure (Debug)",
+ "type": "shell",
+ "command": "cmake",
+ "args": [
+ "--preset", "debug"
+ ],
+ "group": "build",
+ "problemMatcher": []
+ },
+ {
+ "label": "CMake: Configure (Release)",
+ "type": "shell",
+ "command": "cmake",
+ "args": [
+ "--preset", "release"
+ ],
+ "group": "build",
+ "problemMatcher": []
+ },
+ {
+ "label": "CMake: Build",
+ "type": "shell",
+ "command": "cmake",
+ "args": [
+ "--build", "build",
+ "--parallel"
+ ],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "problemMatcher": "$gcc"
+ },
+ {
+ "label": "CMake: Clean",
+ "type": "shell",
+ "command": "cmake",
+ "args": [
+ "--build", "build",
+ "--target", "clean"
+ ],
+ "group": "build",
+ "problemMatcher": []
+ },
+ {
+ "label": "Run Unit Tests",
+ "type": "shell",
+ "command": "${workspaceFolder}/build/QGroundControl",
+ "args": ["--unittest"],
+ "group": {
+ "kind": "test",
+ "isDefault": true
+ },
+ "problemMatcher": [],
+ "dependsOn": "CMake: Build"
+ },
+ {
+ "label": "Run Specific Test",
+ "type": "shell",
+ "command": "${workspaceFolder}/build/QGroundControl",
+ "args": ["--unittest:${input:testClass}"],
+ "group": "test",
+ "problemMatcher": [],
+ "dependsOn": "CMake: Build"
+ },
+ {
+ "label": "Format: Check Changed Files",
+ "type": "shell",
+ "command": "${workspaceFolder}/tools/format-check.sh",
+ "args": ["--check"],
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Format: Fix Changed Files",
+ "type": "shell",
+ "command": "${workspaceFolder}/tools/format-check.sh",
+ "args": [],
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Static Analysis",
+ "type": "shell",
+ "command": "${workspaceFolder}/tools/analyze.sh",
+ "group": "none",
+ "problemMatcher": "$gcc"
+ },
+ {
+ "label": "Clean All",
+ "type": "shell",
+ "command": "${workspaceFolder}/tools/clean.sh",
+ "group": "none",
+ "problemMatcher": []
+ },
+ {
+ "label": "Update Translations",
+ "type": "shell",
+ "command": "source",
+ "args": ["${workspaceFolder}/tools/translations/qgc-lupdate.sh"],
+ "group": "none",
+ "problemMatcher": []
+ }
+ ],
+ "inputs": [
+ {
+ "id": "testClass",
+ "type": "promptString",
+ "description": "Test class name (e.g., FactSystemTest)"
+ }
+ ]
+}
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index a212caa12fd6..000000000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# QGroundControl Quick Reference for AI Assistants
-
-**Ground Control Station** for UAVs using MAVLink protocol. **C++20/Qt 6.10.1** with QML UI.
-
-## 🔑 Most Critical Architecture Pattern
-
-**Fact System** - QGC's type-safe parameter management. Every vehicle parameter uses it.
-
-```cpp
-// ALWAYS use this pattern for parameters
-Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME");
-if (param) {
- param->setCookedValue(newValue); // cookedValue = UI (with units)
- // param->rawValue() = MAVLink/storage
-}
-```
-
-**Rules:**
-- Wait for `parametersReady` signal before accessing
-- Use cookedValue (display) vs rawValue (storage)
-- Never create custom parameter storage
-
-## 🏗️ Key Patterns
-
-1. **Plugins**: FirmwarePlugin (PX4/ArduPilot behavior), AutoPilotPlugin (setup UI), VehicleComponent (individual items)
-2. **Managers**: Singleton pattern - `MultiVehicleManager::instance()->activeVehicle()` (always null-check!)
-3. **QML Integration**: `QML_ELEMENT`, `Q_PROPERTY`, `Q_INVOKABLE`
-4. **State Machines**: Use `QGCStateMachine` for complex workflows (calibration, parameter loading)
-
-## 📂 Code Structure
-
-```
-src/
-├── FactSystem/ # Parameter management (READ FIRST!)
-├── Vehicle/ # Vehicle state (Vehicle.h is critical)
-├── FirmwarePlugin/ # PX4/ArduPilot abstraction
-├── AutoPilotPlugins/ # Vehicle setup UI
-├── MissionManager/ # Mission planning
-├── Comms/ # Serial/UDP/TCP/Bluetooth links
-```
-
-## ⚡ Quick Build
-
-```bash
-git submodule update --init --recursive
-~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
-cmake --build build --config Debug
-./build/Debug/QGroundControl --unittest # Run tests
-```
-
-## ❌ Common Mistakes (DO NOT!)
-
-1. Assume single vehicle (always null-check `activeVehicle()`)
-2. Access Facts before `parametersReady`
-3. Use `Q_ASSERT` in production code
-4. Bypass FirmwarePlugin for firmware-specific behavior
-5. Mix cookedValue/rawValue without conversion
-
-## 📖 Essential Reading
-
-1. **`.github/copilot-instructions.md`** - Detailed architecture guide
-2. **`.github/CONTRIBUTING.md`** - Contribution guidelines
-3. **`src/FactSystem/Fact.h`** - Parameter system (MOST CRITICAL!)
-4. **`src/Vehicle/Vehicle.h`** - Vehicle model (~1477 lines)
-
-## 🧑💻 Coding Style
-
-- **Naming**: Classes `PascalCase`, methods `camelCase`, privates `_leadingUnderscore`
-- **Files**: `ClassName.h`, `ClassName.cc`
-- **Defensive**: Always validate inputs, null-check pointers, early returns
-- **Logging**: `QGC_LOGGING_CATEGORY(MyLog, "qgc.component")` + `qCDebug(MyLog)`
-- **Braces**: Always use, even for single-line if statements
-- **Formatting**: `.clang-format`, `.clang-tidy`, `.editorconfig` configured in repo root
-- **Comments**: Keep minimal - code should be self-documenting. No verbose headers or documentation files unless explicitly requested.
-
-## 🔗 Resources
-
-- **Dev Guide**: https://dev.qgroundcontrol.com/
-- **User Docs**: https://docs.qgroundcontrol.com/
-- **MAVLink**: https://mavlink.io/
-- **Qt 6**: https://doc.qt.io/qt-6/
-
----
-
-**Key Principle**: Match existing code style. Use defensive coding. Respect the Fact System architecture.
diff --git a/CMakePresets.json.template b/CMakePresets.json.template
index c5593e94083d..4b2aca0dac2b 100644
--- a/CMakePresets.json.template
+++ b/CMakePresets.json.template
@@ -1,9 +1,9 @@
{
- "version": 4,
+ "version": 6,
"cmakeMinimumRequired": {
"major": 3,
- "minor": 22,
- "patch": 1
+ "minor": 25,
+ "patch": 0
},
"include": [
"cmake/presets/common.json",
diff --git a/CODING_STYLE.md b/CODING_STYLE.md
new file mode 100644
index 000000000000..ddf2d459e13a
--- /dev/null
+++ b/CODING_STYLE.md
@@ -0,0 +1,268 @@
+# QGroundControl Coding Style Guide
+
+This document describes the coding conventions for QGroundControl. For complete examples, see the reference files:
+
+- [CodingStyle.h](tools/coding-style/CodingStyle.h) - C++ header example
+- [CodingStyle.cc](tools/coding-style/CodingStyle.cc) - C++ implementation example
+- [CodingStyle.qml](tools/coding-style/CodingStyle.qml) - QML example
+
+## General
+
+- **Indentation**: 4 spaces (no tabs)
+- **Line endings**: LF (Unix-style)
+- **File encoding**: UTF-8
+- **Max line length**: No hard limit, use judgment
+
+## Naming Conventions
+
+| Element | Convention | Example |
+|---------|------------|---------|
+| Classes | PascalCase | `VehicleManager` |
+| Methods/Functions | camelCase | `getActiveVehicle()` |
+| Variables | camelCase | `activeVehicle` |
+| Private members | _leadingUnderscore | `_vehicleList` |
+| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
+| Enums | PascalCase (scoped) | `enum class FlightMode` |
+| Files | ClassName.h/.cc | `Vehicle.h`, `Vehicle.cc` |
+
+## C++ Style
+
+### Headers
+
+```cpp
+#pragma once
+
+// System headers first
+#include
+#include
+
+// Qt headers second (use full paths)
+#include
+#include
+
+// Project headers last
+#include "Vehicle.h"
+```
+
+### Class Declaration Order
+
+```cpp
+class MyClass : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT
+ Q_PROPERTY(...)
+
+public:
+ // Constructors/destructor
+ // Enums
+ // Public methods
+ // Getters/setters
+
+signals:
+ // Signals
+
+public slots:
+ // Public slots (only if connected externally)
+
+protected:
+ // Protected members (only for base classes)
+
+private slots:
+ // Private slots
+
+private:
+ // Private methods (prefixed with _)
+ // Private members (prefixed with _)
+};
+```
+
+### Modern C++ (C++20)
+
+QGroundControl uses C++20. Prefer modern features:
+
+```cpp
+// Use [[nodiscard]] for functions with important return values
+[[nodiscard]] bool isValid() const;
+
+// Use std::string_view for read-only string parameters (non-Qt code)
+bool validate(std::string_view input);
+
+// Use std::span instead of pointer + size
+void processData(std::span data);
+
+// Use ranges for cleaner algorithms
+auto filtered = data | std::views::filter([](int n) { return n > 0; });
+
+// Use designated initializers for structs
+Config config{.timeout = 30, .retries = 3};
+
+// Use constexpr for compile-time constants
+static constexpr int MaxRetries = 5;
+```
+
+### Defensive Coding
+
+```cpp
+// Always null-check pointers
+Vehicle* vehicle = _manager->activeVehicle();
+if (!vehicle) {
+ qCWarning(MyLog) << "No active vehicle";
+ return;
+}
+
+// Validate inputs early
+if (param.isEmpty()) {
+ return;
+}
+
+// Avoid Q_ASSERT in production - use defensive checks instead
+// Q_ASSERT is removed in release builds
+```
+
+### Logging
+
+```cpp
+// Declare in header
+Q_DECLARE_LOGGING_CATEGORY(MyComponentLog)
+
+// Define in source (use QGC macro for runtime configuration)
+QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name")
+
+// Use categorized logging
+qCDebug(MyComponentLog) << "Debug message";
+qCWarning(MyComponentLog) << "Warning message";
+```
+
+## Qt6 / QML Integration
+
+### Exposing C++ to QML
+
+```cpp
+class MyClass : public QObject
+{
+ Q_OBJECT
+ QML_ELEMENT // For QML-creatable types
+ QML_UNCREATABLE("C++ only") // For C++-only instantiation
+ QML_SINGLETON // For singletons
+
+ Q_MOC_INCLUDE("Vehicle.h") // For forward-declared types in Q_PROPERTY
+
+ Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
+ Q_PROPERTY(Vehicle* vehicle READ vehicle CONSTANT)
+};
+```
+
+### Signal Emission
+
+```cpp
+void MyClass::setValue(int newValue)
+{
+ if (_value != newValue) {
+ _value = newValue;
+ emit valueChanged(_value); // Always emit when property changes
+ }
+}
+```
+
+## QML Style
+
+### File Structure
+
+```qml
+import QtQuick
+import QtQuick.Controls
+
+import QGroundControl
+import QGroundControl.Controls
+
+Item {
+ id: root
+
+ // 1. Property bindings (width, height, anchors)
+ width: ScreenTools.defaultFontPixelHeight * 10
+
+ // 2. Public properties
+ property int myProperty: 0
+
+ // 3. Private properties (underscore prefix)
+ readonly property bool _isValid: myProperty > 0
+
+ // 4. Signals
+ signal clicked()
+
+ // 5. Functions
+ function doSomething() { }
+
+ // 6. Visual children
+ QGCButton {
+ text: qsTr("Click Me")
+ onClicked: root.clicked()
+ }
+
+ // 7. Connections (use function syntax)
+ Connections {
+ target: someObject
+ function onSignalName() { } // NOT: onSignalName: { }
+ }
+
+ // 8. Component.onCompleted
+ Component.onCompleted: { }
+}
+```
+
+### QML Guidelines
+
+- **No hardcoded sizes**: Use `ScreenTools.defaultFontPixelHeight/Width`
+- **No hardcoded colors**: Use `QGCPalette` for theming
+- **Use QGC controls**: `QGCButton`, `QGCLabel`, `QGCTextField`, etc.
+- **Translations**: Wrap user-visible strings with `qsTr()`
+- **Null checks**: Always check `_activeVehicle` before use
+
+### Connections Syntax (Qt6)
+
+```qml
+// CORRECT - Qt6 function syntax
+Connections {
+ target: vehicle
+ function onArmedChanged() {
+ console.log("Armed state changed")
+ }
+}
+
+// DEPRECATED - Old Qt5 syntax (triggers pre-commit warning)
+Connections {
+ target: vehicle
+ onArmedChanged: { } // Don't use this
+}
+```
+
+## Common Pitfalls
+
+1. **Assuming single vehicle** - Always null-check `activeVehicle()`
+2. **Accessing Facts before ready** - Wait for `parametersReady` signal
+3. **Bypassing FirmwarePlugin** - Use plugin for firmware-specific behavior
+4. **Using Q_ASSERT in production** - Use defensive checks instead
+5. **Mixing cookedValue/rawValue** - Understand the difference
+6. **Hardcoded QML sizes/colors** - Use ScreenTools and QGCPalette
+
+## Formatting Tools
+
+The repository includes configuration for automatic formatting:
+
+- `.clang-format` - C++ formatting
+- `.clang-tidy` - C++ static analysis
+- `.qmlformat.ini` - QML formatting
+- `.qmllint.ini` - QML linting
+- `.editorconfig` - Editor settings
+
+Run pre-commit checks:
+```bash
+pre-commit run --all-files
+```
+
+## Additional Resources
+
+- [Qt 6 Documentation](https://doc.qt.io/qt-6/)
+- [QGroundControl Dev Guide](https://dev.qgroundcontrol.com/)
+- [MAVLink Protocol](https://mavlink.io/)
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000000..4f6dfbe0de7f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,54 @@
+# QGroundControl Development Makefile
+# Convenience wrapper for common commands
+
+.PHONY: help configure build test clean lint format docker submodules
+
+# Default target
+help:
+ @echo "QGroundControl Development Commands"
+ @echo ""
+ @echo " make configure - Configure CMake build (Debug)"
+ @echo " make build - Build the project"
+ @echo " make test - Run unit tests"
+ @echo " make clean - Remove build directory"
+ @echo " make lint - Run pre-commit checks"
+ @echo " make format - Format C++ code with clang-format"
+ @echo " make docker - Build using Docker (Ubuntu)"
+ @echo " make submodules - Initialize git submodules"
+ @echo ""
+ @echo "Environment variables:"
+ @echo " QT_DIR - Qt installation directory (default: ~/Qt//gcc_64)"
+ @echo " BUILD_TYPE - CMake build type (default: Debug)"
+ @echo ""
+ @echo "Configuration from .github/build-config.json:"
+ @echo " Qt version: $(QT_VERSION)"
+
+# Configuration - reads Qt version from centralized config
+QT_VERSION := $(shell ./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1")
+QT_DIR ?= $(HOME)/Qt/$(QT_VERSION)/gcc_64
+BUILD_TYPE ?= Debug
+BUILD_DIR := build
+
+submodules:
+ git submodule update --init --recursive
+
+configure: submodules
+ $(QT_DIR)/bin/qt-cmake -B $(BUILD_DIR) -G Ninja -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
+
+build:
+ cmake --build $(BUILD_DIR) --config $(BUILD_TYPE) --parallel
+
+test:
+ ./$(BUILD_DIR)/$(BUILD_TYPE)/QGroundControl --unittest
+
+clean:
+ rm -rf $(BUILD_DIR)
+
+lint:
+ pre-commit run --all-files
+
+format:
+ find src -name "*.h" -o -name "*.cc" -o -name "*.cpp" | xargs clang-format -i
+
+docker:
+ ./deploy/docker/run-docker-ubuntu.sh
diff --git a/README.md b/README.md
index b9405404a77c..ba1c18427d8d 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,20 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -30,7 +42,7 @@
- ⚙️ *Vehicle Setup*: Tailored configuration for *PX4* and *ArduPilot* platforms.
- 🔧 *Fully Open Source*: Customize and extend the software to suit your needs.
-🎯 Check out the latest updates in our [New Features and Release Notes](https://github.com/mavlink/qgroundcontrol/blob/master/ChangeLog.md).
+🎯 Check out the latest updates in our [New Features and Release Notes](https://github.com/mavlink/qgroundcontrol/blob/master/CHANGELOG.md).
---
diff --git a/cmake/Helpers.cmake b/cmake/Helpers.cmake
index 92b64621cf39..6c8f274881b2 100644
--- a/cmake/Helpers.cmake
+++ b/cmake/Helpers.cmake
@@ -54,16 +54,14 @@ function(qgc_config_caching)
string(TOLOWER "${_cache_tool}" _cache_tool)
if(_cache_tool STREQUAL "ccache")
- # set(ENV{CCACHE_CONFIGPATH} "${CMAKE_SOURCE_DIR}/tools/ccache.conf")
- # set(ENV{CCACHE_DIR} "${CMAKE_SOURCE_DIR}/.ccache")
+ set(ENV{CCACHE_DIR} "${CMAKE_SOURCE_DIR}/.cache/ccache")
set(ENV{CCACHE_BASEDIR} "${CMAKE_SOURCE_DIR}")
+ set(ENV{CCACHE_MAXSIZE} "2G")
set(ENV{CCACHE_COMPRESSLEVEL} "5")
set(ENV{CCACHE_SLOPPINESS} "pch_defines,time_macros,include_file_mtime,include_file_ctime")
- # set(ENV{CCACHE_NOHASHDIR} "true")
if(APPLE)
set(ENV{CCACHE_COMPILERCHECK} "content")
endif()
- # set(ENV{CCACHE_MAXSIZE} "1G")
elseif(_cache_tool STREQUAL "sccache")
# set(ENV{SCCACHE_PATH} "")
# set(ENV{SCCACHE_DIR} "")
diff --git a/cmake/find-modules/FindGStreamer.cmake b/cmake/find-modules/FindGStreamer.cmake
index af3a1c208231..5844f980cd8d 100644
--- a/cmake/find-modules/FindGStreamer.cmake
+++ b/cmake/find-modules/FindGStreamer.cmake
@@ -11,11 +11,16 @@
# ----------------------------------------------------------------------------
# Set default version based on platform
+# Android uses 1.22.12 with custom S3 package (full 1.24.x is 1.2GB and causes disk space issues)
+# Windows/macOS use 1.24.13 (latest stable release)
+# Linux uses system-installed version (minimum 1.20)
if(NOT DEFINED GStreamer_FIND_VERSION)
if(LINUX)
set(GStreamer_FIND_VERSION 1.20)
- else()
+ elseif(ANDROID)
set(GStreamer_FIND_VERSION 1.22.12)
+ else()
+ set(GStreamer_FIND_VERSION 1.24.13)
endif()
endif()
diff --git a/cmake/Coverage.cmake b/cmake/modules/Coverage.cmake
similarity index 100%
rename from cmake/Coverage.cmake
rename to cmake/modules/Coverage.cmake
diff --git a/cmake/presets/Linux.json b/cmake/presets/Linux.json
index 1e6e469877f5..c86feb318cf7 100644
--- a/cmake/presets/Linux.json
+++ b/cmake/presets/Linux.json
@@ -4,120 +4,175 @@
"configurePresets": [
{
"name": "Linux",
- "displayName": "Linux configuration using Qt6",
+ "displayName": "Linux Release",
"generator": "Ninja",
"binaryDir": "${sourceParentDir}/build/qt6-Linux",
"toolchainFile": "$penv{QT_ROOT_DIR}/lib/cmake/Qt6/qt.toolchain.cmake",
+ "inherits": ["release"],
"cacheVariables": {
- "CMAKE_BUILD_TYPE": "Release",
"QT_VERSION_MAJOR": "6"
}
},
+ {
+ "name": "Linux-debug",
+ "displayName": "Linux Debug",
+ "inherits": ["debug", "Linux"],
+ "binaryDir": "${sourceParentDir}/build/qt6-Linux-debug"
+ },
{
"name": "Linux-ccache",
- "displayName": "Linux configuration using Qt6 and ccache",
+ "displayName": "Linux Release with ccache",
"inherits": ["dev", "ccache", "Linux"]
+ },
+ {
+ "name": "Linux-debug-ccache",
+ "displayName": "Linux Debug with ccache",
+ "inherits": ["dev", "ccache", "Linux-debug"]
+ },
+ {
+ "name": "Linux-coverage",
+ "displayName": "Linux Coverage",
+ "inherits": ["coverage", "Linux"],
+ "binaryDir": "${sourceParentDir}/build/qt6-Linux-coverage"
+ },
+ {
+ "name": "Linux-minimal",
+ "displayName": "Linux Minimal (fast build)",
+ "inherits": ["minimal", "Linux-debug"],
+ "binaryDir": "${sourceParentDir}/build/qt6-Linux-minimal"
}
],
"buildPresets": [
{
"name": "Linux",
- "displayName": "Linux build using Qt6",
+ "displayName": "Linux Release",
"configurePreset": "Linux"
},
+ {
+ "name": "Linux-debug",
+ "displayName": "Linux Debug",
+ "configurePreset": "Linux-debug"
+ },
{
"name": "Linux-ccache",
- "displayName": "Linux build using Qt6 and ccache",
+ "displayName": "Linux Release with ccache",
"configurePreset": "Linux-ccache"
+ },
+ {
+ "name": "Linux-debug-ccache",
+ "displayName": "Linux Debug with ccache",
+ "configurePreset": "Linux-debug-ccache"
+ },
+ {
+ "name": "Linux-coverage",
+ "displayName": "Linux Coverage",
+ "configurePreset": "Linux-coverage"
+ },
+ {
+ "name": "Linux-minimal",
+ "displayName": "Linux Minimal",
+ "configurePreset": "Linux-minimal"
}
],
"testPresets": [
{
"name": "Linux",
- "displayName": "Linux tests using Qt6",
+ "displayName": "Linux Release tests",
"configurePreset": "Linux",
"inherits": ["default"]
},
+ {
+ "name": "Linux-debug",
+ "displayName": "Linux Debug tests",
+ "configurePreset": "Linux-debug",
+ "inherits": ["default"]
+ },
{
"name": "Linux-ccache",
- "displayName": "Linux tests using Qt6 and ccache",
+ "displayName": "Linux Release tests with ccache",
"configurePreset": "Linux-ccache",
"inherits": ["default"]
+ },
+ {
+ "name": "Linux-debug-ccache",
+ "displayName": "Linux Debug tests with ccache",
+ "configurePreset": "Linux-debug-ccache",
+ "inherits": ["default"]
+ },
+ {
+ "name": "Linux-coverage",
+ "displayName": "Linux Coverage tests",
+ "configurePreset": "Linux-coverage",
+ "inherits": ["default"]
}
],
"packagePresets": [
{
"name": "Linux",
- "displayName": "Linux package using Qt6",
+ "displayName": "Linux Release package",
"configurePreset": "Linux"
},
{
"name": "Linux-ccache",
- "displayName": "Linux package using Qt6 and ccache",
+ "displayName": "Linux Release package with ccache",
"configurePreset": "Linux-ccache"
}
],
"workflowPresets": [
{
"name": "Linux",
- "displayName": "Linux workflow using Qt6",
+ "displayName": "Linux Release workflow",
+ "steps": [
+ { "type": "configure", "name": "Linux" },
+ { "type": "build", "name": "Linux" },
+ { "type": "test", "name": "Linux" },
+ { "type": "package", "name": "Linux" }
+ ]
+ },
+ {
+ "name": "Linux-debug",
+ "displayName": "Linux Debug workflow",
"steps": [
- {
- "type": "configure",
- "name": "Linux"
- },
- {
- "type": "build",
- "name": "Linux"
- },
- {
- "type": "test",
- "name": "Linux"
- },
- {
- "type": "package",
- "name": "Linux"
- }
+ { "type": "configure", "name": "Linux-debug" },
+ { "type": "build", "name": "Linux-debug" },
+ { "type": "test", "name": "Linux-debug" }
]
},
{
"name": "Linux-ccache",
- "displayName": "Linux workflow using Qt6 and ccache",
+ "displayName": "Linux Release workflow with ccache",
+ "steps": [
+ { "type": "configure", "name": "Linux-ccache" },
+ { "type": "build", "name": "Linux-ccache" },
+ { "type": "test", "name": "Linux-ccache" },
+ { "type": "package", "name": "Linux-ccache" }
+ ]
+ },
+ {
+ "name": "Linux-debug-ccache",
+ "displayName": "Linux Debug workflow with ccache",
+ "steps": [
+ { "type": "configure", "name": "Linux-debug-ccache" },
+ { "type": "build", "name": "Linux-debug-ccache" },
+ { "type": "test", "name": "Linux-debug-ccache" }
+ ]
+ },
+ {
+ "name": "Linux-coverage",
+ "displayName": "Linux Coverage workflow",
"steps": [
- {
- "type": "configure",
- "name": "Linux-ccache"
- },
- {
- "type": "build",
- "name": "Linux-ccache"
- },
- {
- "type": "test",
- "name": "Linux-ccache"
- },
- {
- "type": "package",
- "name": "Linux-ccache"
- }
+ { "type": "configure", "name": "Linux-coverage" },
+ { "type": "build", "name": "Linux-coverage" },
+ { "type": "test", "name": "Linux-coverage" }
]
},
{
"name": "Linux-CI",
- "displayName": "Linux workflow using Qt6 and ccache for CI",
+ "displayName": "Linux CI workflow (Release)",
"steps": [
- {
- "type": "configure",
- "name": "Linux-ccache"
- },
- {
- "type": "build",
- "name": "Linux-ccache"
- },
- {
- "type": "package",
- "name": "Linux-ccache"
- }
+ { "type": "configure", "name": "Linux-ccache" },
+ { "type": "build", "name": "Linux-ccache" },
+ { "type": "package", "name": "Linux-ccache" }
]
}
]
diff --git a/cmake/presets/common.json b/cmake/presets/common.json
index ead509e34015..739a0969b2c2 100644
--- a/cmake/presets/common.json
+++ b/cmake/presets/common.json
@@ -23,6 +23,52 @@
"CMAKE_C_COMPILER_LAUNCHER": "ccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "ccache"
}
+ },
+ {
+ "name": "sccache",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_C_COMPILER_LAUNCHER": "sccache",
+ "CMAKE_CXX_COMPILER_LAUNCHER": "sccache"
+ }
+ },
+ {
+ "name": "debug",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "QGC_BUILD_TESTING": "ON",
+ "QGC_DEBUG_QML": "ON"
+ }
+ },
+ {
+ "name": "release",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release",
+ "QGC_BUILD_TESTING": "OFF",
+ "QGC_DEBUG_QML": "OFF",
+ "QGC_BUILD_INSTALLER": "ON"
+ }
+ },
+ {
+ "name": "coverage",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug",
+ "QGC_BUILD_TESTING": "ON",
+ "QGC_ENABLE_COVERAGE": "ON"
+ }
+ },
+ {
+ "name": "minimal",
+ "hidden": true,
+ "cacheVariables": {
+ "QGC_VIEWER3D": "OFF",
+ "QGC_ENABLE_BLUETOOTH": "OFF",
+ "QGC_ENABLE_GST_VIDEOSTREAMING": "OFF",
+ "QGC_ENABLE_UVC": "OFF"
+ }
}
],
"testPresets": [
diff --git a/deploy/docker/Dockerfile-build-ubuntu b/deploy/docker/Dockerfile-build-ubuntu
index 3558ec1063a1..4f4abb16b61d 100644
--- a/deploy/docker/Dockerfile-build-ubuntu
+++ b/deploy/docker/Dockerfile-build-ubuntu
@@ -31,14 +31,18 @@ ENV LANG=en_US.UTF-8 \
# ---------- Bring in the helper scripts ----------
# They must be in the build context next to this Dockerfile.
+COPY .github/build-config.json /tmp/qt/
+COPY tools/setup/read-config.sh /tmp/qt/
COPY tools/setup/install-dependencies-debian.sh /tmp/qt/
-COPY tools/setup/install-qt-debian.sh /tmp/qt/
+COPY tools/setup/install-qt-debian.sh /tmp/qt/
RUN chmod +x /tmp/qt/*.sh && \
/tmp/qt/install-dependencies-debian.sh && \
/tmp/qt/install-qt-debian.sh && \
rm -rf /tmp/qt # keep the image slim
-ENV QT_ROOT_DIR=/opt/Qt/6.10.1/gcc_64
+# Qt version passed from workflow (reads from .github/build-config.json)
+ARG QT_VERSION=6.10.1
+ENV QT_ROOT_DIR=/opt/Qt/${QT_VERSION}/gcc_64
ENV PATH=$QT_ROOT_DIR/bin:$PATH
# ---------- Git safe directory (avoids “detected dubious ownership”) ----------
diff --git a/docs/en/qgc-dev-guide/contribute/coding_style.md b/docs/en/qgc-dev-guide/contribute/coding_style.md
index 3d4d62420c1a..7e3e0d749620 100644
--- a/docs/en/qgc-dev-guide/contribute/coding_style.md
+++ b/docs/en/qgc-dev-guide/contribute/coding_style.md
@@ -1,12 +1,40 @@
# Coding Style
-High level style information:
+See the full **[Coding Style Guide](https://github.com/mavlink/qgroundcontrol/blob/master/CODING_STYLE.md)** for comprehensive documentation.
-- Tabs expanded to 4 spaces
-- Pascal/CamelCase naming conventions
+## Quick Reference
-The style itself is documents in the following example files:
+- **Indentation**: 4 spaces (no tabs)
+- **Naming**: PascalCase for classes, camelCase for methods/variables
+- **Private members**: Prefix with underscore (`_myVariable`)
+- **C++ Standard**: C++20
+- **Qt Version**: Qt 6.10+
-- [CodingStyle.cc](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.cc)
-- [CodingStyle.h](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.h)
-- [CodingStyle.qml](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.qml)
+## Example Files
+
+The coding style is demonstrated in these reference files:
+
+- [CodingStyle.h](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.h) - C++ header patterns
+- [CodingStyle.cc](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.cc) - C++ implementation patterns
+- [CodingStyle.qml](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.qml) - QML patterns
+
+## Key Guidelines
+
+### C++
+- Use `[[nodiscard]]` for functions with important return values
+- Use `std::span` instead of pointer + size parameters
+- Use defensive checks instead of `Q_ASSERT` (removed in release builds)
+- Use `QGC_LOGGING_CATEGORY` for logging
+
+### QML
+- No hardcoded sizes - use `ScreenTools`
+- No hardcoded colors - use `QGCPalette`
+- Use QGC controls (`QGCButton`, `QGCLabel`, etc.)
+- Use `qsTr()` for all user-visible strings
+- Use function syntax for Connections: `function onSignal() { }`
+
+### Common Pitfalls
+1. Assuming single vehicle - always null-check `activeVehicle()`
+2. Accessing Facts before `parametersReady` signal
+3. Using `Q_ASSERT` in production code
+4. Hardcoded sizes/colors in QML
diff --git a/justfile b/justfile
new file mode 100644
index 000000000000..ee33590642ef
--- /dev/null
+++ b/justfile
@@ -0,0 +1,50 @@
+# QGroundControl Development Commands
+# Wrapper for Makefile - install: cargo install just, brew install just, or apt install just
+
+# Default: show available commands
+default:
+ @just --list
+
+# Show Makefile help
+help:
+ @make help
+
+# Initialize git submodules
+submodules:
+ make submodules
+
+# Configure CMake build
+configure:
+ make configure
+
+# Build the project
+build:
+ make build
+
+# Run unit tests
+test:
+ make test
+
+# Clean build directory
+clean:
+ make clean
+
+# Run pre-commit checks
+lint:
+ make lint
+
+# Format C++ code
+format:
+ make format
+
+# Build with Docker (Ubuntu)
+docker:
+ make docker
+
+# Full rebuild
+rebuild: clean configure build
+
+# Override build type: just release-build
+release-build:
+ make configure BUILD_TYPE=Release
+ make build BUILD_TYPE=Release
diff --git a/pyrightconfig.json b/pyrightconfig.json
new file mode 100644
index 000000000000..0dcc292371d6
--- /dev/null
+++ b/pyrightconfig.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://json.schemastore.org/pyrightconfig.json",
+ "include": [
+ "tools/**/*.py",
+ "test/**/*.py"
+ ],
+ "exclude": [
+ "build",
+ "**/__pycache__"
+ ],
+ "typeCheckingMode": "basic",
+ "pythonVersion": "3.10",
+ "reportMissingImports": "warning",
+ "reportMissingTypeStubs": false,
+ "reportUnusedImport": "warning",
+ "reportUnusedVariable": "warning"
+}
diff --git a/tools/HeaderUpdater.py b/tools/HeaderUpdater.py
index 856a741e2b93..48f7b592a86d 100644
--- a/tools/HeaderUpdater.py
+++ b/tools/HeaderUpdater.py
@@ -1,11 +1,27 @@
-import os
+#!/usr/bin/env python3
+"""
+QGroundControl License Header Updater
+
+Updates or validates license headers in source files.
+
+Usage:
+ python HeaderUpdater.py # Update headers in src/
+ python HeaderUpdater.py --check # Check headers without modifying (CI mode)
+ python HeaderUpdater.py --check src/ test/ # Check specific directories
+ python HeaderUpdater.py src/ test/ # Update specific directories
+"""
+
+import argparse
import datetime
+import os
import re
+import sys
+from pathlib import Path
-# Define the file extensions you want to target
-TARGET_EXTENSIONS = [".cpp", ".cc", ".h", ".qml"] # Add other extensions as needed
+# File extensions to process
+TARGET_EXTENSIONS = {".cpp", ".cc", ".h", ".qml"}
-# Define the new header with a placeholder for the updated year
+# License header template
HEADER_TEMPLATE = """/****************************************************************************
*
* (c) 2009-{year} QGROUNDCONTROL PROJECT
@@ -16,45 +32,199 @@
****************************************************************************/
"""
+# Pattern to match QGC license header (with flexible year format)
+HEADER_PATTERN = re.compile(r"\(c\)\s*(2009-\d{4}|\d{4})\s+QGROUNDCONTROL")
+
+# Directories to skip
+SKIP_DIRS = {
+ "build",
+ "libs",
+ "deploy",
+ ".git",
+ "node_modules",
+ "__pycache__",
+ "cpm_modules",
+}
+
+
+def find_source_files(directories: list[str]) -> list[Path]:
+ """Find all source files in the given directories."""
+ files = []
+ for directory in directories:
+ path = Path(directory)
+ if not path.exists():
+ print(f"Warning: Directory not found: {directory}", file=sys.stderr)
+ continue
-def update_header_in_file(file_path, header_template):
- # Get the current year
+ for root, dirs, filenames in os.walk(path):
+ # Skip certain directories
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
+
+ for filename in filenames:
+ if any(filename.endswith(ext) for ext in TARGET_EXTENSIONS):
+ files.append(Path(root) / filename)
+
+ return sorted(files)
+
+
+def check_header(file_path: Path) -> tuple[bool, str]:
+ """
+ Check if a file has a valid QGC license header.
+
+ Returns:
+ (is_valid, message)
+ """
+ try:
+ content = file_path.read_text(encoding="utf-8", errors="replace")
+ except Exception as e:
+ return False, f"Error reading file: {e}"
+
+ # Check if header exists
+ match = HEADER_PATTERN.search(content[:1000]) # Only check first 1000 chars
+
+ if not match:
+ return False, "Missing QGC license header"
+
+ # Check if year is current
current_year = datetime.datetime.now().year
- new_header = header_template.format(year=current_year)
+ year_match = match.group(1)
+
+ if year_match == str(current_year):
+ return True, "Header OK (single year)"
+ elif year_match == f"2009-{current_year}":
+ return True, "Header OK"
+ elif year_match.startswith("2009-"):
+ return (
+ False,
+ f"Header year outdated: {year_match} (should be 2009-{current_year})",
+ )
+ else:
+ return False, f"Header year format incorrect: {year_match}"
+
+
+def update_header(file_path: Path, current_year: int) -> tuple[bool, str]:
+ """
+ Update the license header in a file.
- with open(file_path, "r") as file:
- content = file.read()
+ Returns:
+ (was_modified, message)
+ """
+ try:
+ content = file_path.read_text(encoding="utf-8", errors="replace")
+ except Exception as e:
+ return False, f"Error reading file: {e}"
- # Regex pattern to match both "2009-XXXX" and just "XXXX"
- header_pattern = re.compile(r"\(c\)\s*(2009-\d{4}|\d{4}) QGROUNDCONTROL")
+ original_content = content
- # Check if the header already exists
- match = header_pattern.search(content)
+ # Check if header exists
+ match = HEADER_PATTERN.search(content)
if match:
- # Update the header to include "2009-current_year" if it is in the wrong format
- updated_content = header_pattern.sub(
- f"(c) 2009-{current_year} QGROUNDCONTROL", content
- )
+ # Update existing header
+ content = HEADER_PATTERN.sub(f"(c) 2009-{current_year} QGROUNDCONTROL", content)
else:
- # Prepend the new header if it's missing
- updated_content = new_header + "\n" + content
+ # Prepend new header
+ new_header = HEADER_TEMPLATE.format(year=current_year)
+ content = new_header + "\n" + content
+
+ if content != original_content:
+ try:
+ file_path.write_text(content, encoding="utf-8")
+ return True, "Header updated"
+ except Exception as e:
+ return False, f"Error writing file: {e}"
+
+ return False, "Header already up to date"
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Update or check QGC license headers in source files.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+ parser.add_argument(
+ "directories",
+ nargs="*",
+ default=["src"],
+ help="Directories to process (default: src)",
+ )
+ parser.add_argument(
+ "--check",
+ action="store_true",
+ help="Check headers without modifying files (exit 1 if issues found)",
+ )
+ parser.add_argument(
+ "--verbose",
+ "-v",
+ action="store_true",
+ help="Show all files, not just those with issues",
+ )
+
+ args = parser.parse_args()
+
+ # Find repo root (look for COPYING.md)
+ script_dir = Path(__file__).parent
+ repo_root = script_dir.parent
- with open(file_path, "w") as file:
- file.write(updated_content)
+ if not (repo_root / "COPYING.md").exists():
+ print("Warning: COPYING.md not found in repo root", file=sys.stderr)
+ # Resolve directories relative to repo root
+ directories = [
+ str(repo_root / d) if not os.path.isabs(d) else d for d in args.directories
+ ]
-def process_directory(directory):
- for root, _, files in os.walk(directory):
- for file in files:
- if any(file.endswith(ext) for ext in TARGET_EXTENSIONS):
- file_path = os.path.join(root, file)
- update_header_in_file(file_path, HEADER_TEMPLATE)
+ files = find_source_files(directories)
+ current_year = datetime.datetime.now().year
+
+ if not files:
+ print("No source files found.")
+ return 0
+
+ print(f"Processing {len(files)} files...")
+
+ issues = []
+ updated = []
+
+ for file_path in files:
+ rel_path = (
+ file_path.relative_to(repo_root)
+ if file_path.is_relative_to(repo_root)
+ else file_path
+ )
+
+ if args.check:
+ is_valid, message = check_header(file_path)
+ if not is_valid:
+ issues.append((rel_path, message))
+ print(f" FAIL: {rel_path}: {message}")
+ elif args.verbose:
+ print(f" OK: {rel_path}")
+ else:
+ was_modified, message = update_header(file_path, current_year)
+ if was_modified:
+ updated.append(rel_path)
+ print(f" Updated: {rel_path}")
+ elif args.verbose:
+ print(f" Skipped: {rel_path}: {message}")
+
+ print()
+
+ if args.check:
+ if issues:
+ print(f"Found {len(issues)} file(s) with header issues.")
+ return 1
+ else:
+ print("All headers are valid.")
+ return 0
+ else:
+ if updated:
+ print(f"Updated {len(updated)} file(s).")
+ else:
+ print("No files needed updating.")
+ return 0
if __name__ == "__main__":
- current_directory = os.path.dirname(os.path.abspath(__file__))
- source_directory = current_directory + "/../src"
- print(source_directory)
- process_directory(source_directory)
- print("Headers updated.")
+ sys.exit(main())
diff --git a/tools/README.md b/tools/README.md
new file mode 100644
index 000000000000..d8a5c2921234
--- /dev/null
+++ b/tools/README.md
@@ -0,0 +1,311 @@
+# QGroundControl Tools
+
+This directory contains development tools, scripts, and configuration files for QGroundControl.
+
+## Directory Structure
+
+```
+tools/
+├── analyze.sh # Static analysis (clang-tidy, cppcheck)
+├── check-deps.sh # Check for outdated dependencies
+├── clean.sh # Clean build artifacts and caches
+├── coverage.sh # Code coverage reports
+├── format-check.sh # Check/apply clang-format
+├── generate-docs.sh # Generate API docs (Doxygen)
+├── param-docs.py # Generate parameter documentation
+├── profile.sh # Profiling (valgrind, perf)
+├── ccache.conf # ccache configuration
+├── HeaderUpdater.py # License header management
+├── qt6.natvis # Visual Studio debugger visualizers
+├── coding-style/ # Code style examples
+├── gdb-pretty-printers/ # GDB/LLDB Qt type formatters
+├── log-analyzer/ # QGC log analysis tools
+├── mock-mavlink/ # MAVLink vehicle simulator
+├── setup/ # Environment setup scripts
+└── translations/ # Translation tools
+```
+
+## Quick Start
+
+```bash
+# Format changed files
+./tools/format-check.sh
+
+# Run static analysis
+./tools/analyze.sh
+
+# Clean build
+./tools/clean.sh
+```
+
+## Development Scripts
+
+### format-check.sh
+
+Check or apply clang-format to source files.
+
+```bash
+./tools/format-check.sh # Format changed files (vs master)
+./tools/format-check.sh --check # Check only (for CI)
+./tools/format-check.sh --all # Format all source files
+./tools/format-check.sh src/Vehicle/ # Format specific directory
+```
+
+### analyze.sh
+
+Run static analysis on source code.
+
+```bash
+./tools/analyze.sh # Analyze changed files
+./tools/analyze.sh --all # Analyze all files
+./tools/analyze.sh --tool cppcheck # Use cppcheck instead of clang-tidy
+./tools/analyze.sh src/Vehicle/ # Analyze specific directory
+```
+
+### clean.sh
+
+Clean build artifacts and caches.
+
+```bash
+./tools/clean.sh # Clean build directory
+./tools/clean.sh --all # Clean everything (build, caches)
+./tools/clean.sh --cache # Clean only caches
+./tools/clean.sh --dry-run # Show what would be removed
+```
+
+### coverage.sh
+
+Generate code coverage reports.
+
+```bash
+./tools/coverage.sh # Build with coverage, run tests, generate report
+./tools/coverage.sh --report # Generate report only (after tests)
+./tools/coverage.sh --open # Generate and open in browser
+./tools/coverage.sh --clean # Clean coverage data
+```
+
+Requires: `lcov`, `genhtml`
+
+### profile.sh
+
+Profile QGC for performance and memory issues.
+
+```bash
+./tools/profile.sh # CPU profiling with perf
+./tools/profile.sh --memcheck # Memory leak detection (valgrind)
+./tools/profile.sh --callgrind # CPU profiling (valgrind)
+./tools/profile.sh --massif # Heap profiling (valgrind)
+./tools/profile.sh --heaptrack # Heap profiling (heaptrack)
+./tools/profile.sh --sanitize # Build with AddressSanitizer
+```
+
+### check-deps.sh
+
+Check for outdated dependencies and submodules.
+
+```bash
+./tools/check-deps.sh # Check all dependencies
+./tools/check-deps.sh --submodules # Check git submodules only
+./tools/check-deps.sh --qt # Check Qt version
+./tools/check-deps.sh --update # Update submodules to latest
+```
+
+### generate-docs.sh
+
+Generate API documentation using Doxygen.
+
+```bash
+./tools/generate-docs.sh # Generate HTML docs
+./tools/generate-docs.sh --open # Generate and open in browser
+./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX)
+./tools/generate-docs.sh --clean # Clean generated docs
+```
+
+Requires: `doxygen`, `graphviz`
+
+### param-docs.py
+
+Generate parameter documentation from FactMetaData JSON files.
+
+```bash
+./tools/param-docs.py # Generate markdown
+./tools/param-docs.py --format html # Generate HTML
+./tools/param-docs.py --format json # Generate JSON
+./tools/param-docs.py --group "Battery" # Filter by group
+./tools/param-docs.py --output params.md # Custom output file
+```
+
+## Setup Scripts
+
+Scripts in `setup/` help configure development environments. They read configuration from `.github/build-config.json` for consistent versioning.
+
+| Script | Platform | Description |
+|--------|----------|-------------|
+| `install-dependencies-debian.sh` | Linux | Install build dependencies via apt |
+| `install-dependencies-osx.sh` | macOS | Install dependencies via Homebrew + GStreamer |
+| `install-dependencies-windows.ps1` | Windows | Install GStreamer (Vulkan SDK optional) |
+| `install-qt-debian.sh` | Linux | Install Qt via aqtinstall |
+| `install-qt-macos.sh` | macOS | Install Qt via aqtinstall |
+| `install-qt-windows.ps1` | Windows | Install Qt via aqtinstall |
+| `build-gstreamer.sh` | Linux | Build GStreamer from source (optional) |
+| `read-config.sh` | All | Helper to read `.github/build-config.json` |
+
+### Usage Examples
+
+```bash
+# Linux: Install all dependencies
+sudo ./tools/setup/install-dependencies-debian.sh
+./tools/setup/install-qt-debian.sh
+
+# macOS: Install all dependencies
+./tools/setup/install-dependencies-osx.sh
+./tools/setup/install-qt-macos.sh
+
+# Windows (PowerShell as Admin):
+.\tools\setup\install-dependencies-windows.ps1
+.\tools\setup\install-qt-windows.ps1
+
+# Build GStreamer from source (Linux, optional)
+./tools/setup/build-gstreamer.sh -p /opt/gstreamer
+```
+
+## Debugging Tools
+
+### gdb-pretty-printers/
+
+GDB pretty printers for Qt 6 types. Makes debugging Qt containers and strings readable.
+
+```bash
+# In GDB:
+source tools/gdb-pretty-printers/qt6.py
+
+# Then:
+(gdb) print myQString
+$1 = "Hello, World!"
+```
+
+See [gdb-pretty-printers/README.md](gdb-pretty-printers/README.md) for setup instructions.
+
+### qt6.natvis
+
+Visual Studio debugger visualizers for Qt6 types. Automatically loaded by VS when debugging.
+
+## Testing Tools
+
+### mock-mavlink/
+
+Simple MAVLink vehicle simulator for testing QGC without hardware.
+
+```bash
+# Install dependency
+pip install pymavlink
+
+# Run mock vehicle (QGC connects to UDP 14550)
+./tools/mock-mavlink/mock_vehicle.py
+
+# Multiple vehicles
+./tools/mock-mavlink/mock_vehicle.py --system-id 1 --port 14550 &
+./tools/mock-mavlink/mock_vehicle.py --system-id 2 --port 14551 &
+```
+
+See [mock-mavlink/README.md](mock-mavlink/README.md) for details.
+
+### log-analyzer/
+
+Analyze QGC application logs and telemetry files.
+
+```bash
+# Analyze application log
+./tools/log-analyzer/analyze_log.py ~/.local/share/QGroundControl/Logs/QGCConsole.log
+
+# Show only errors
+./tools/log-analyzer/analyze_log.py --errors QGCConsole.log
+
+# Analyze MAVLink telemetry log
+./tools/log-analyzer/analyze_log.py flight.tlog
+
+# Show statistics
+./tools/log-analyzer/analyze_log.py --stats QGCConsole.log
+
+# Filter by component
+./tools/log-analyzer/analyze_log.py --component Vehicle QGCConsole.log
+```
+
+See [log-analyzer/README.md](log-analyzer/README.md) for details.
+
+## Translation Tools
+
+Scripts in `translations/` manage internationalization.
+
+| Script | Description |
+|--------|-------------|
+| `qgc-lupdate.sh` | Update Qt translation files (runs lupdate + JSON extractor) |
+| `qgc-lupdate-json.py` | Extract translatable strings from JSON files |
+
+```bash
+# From repository root:
+source tools/translations/qgc-lupdate.sh
+
+# Or run JSON extractor directly:
+python3 tools/translations/qgc-lupdate-json.py --verbose
+```
+
+See [translations/README.md](translations/README.md) for Crowdin integration.
+
+## Code Quality Tools
+
+### HeaderUpdater.py
+
+Updates or validates license headers in source files.
+
+```bash
+python3 tools/HeaderUpdater.py # Update headers
+python3 tools/HeaderUpdater.py --check # Check only (for CI)
+python3 tools/HeaderUpdater.py --check src/ # Check specific directory
+```
+
+### ccache.conf
+
+Configuration for [ccache](https://ccache.dev/) to speed up rebuilds. CMake automatically uses this when ccache is available.
+
+```bash
+# Manual use:
+export CCACHE_CONFIGPATH=/path/to/qgroundcontrol/tools/ccache.conf
+```
+
+### coding-style/
+
+Example files demonstrating QGC coding conventions:
+
+- `CodingStyle.h` - Header file conventions
+- `CodingStyle.cc` - Implementation file conventions
+- `CodingStyle.qml` - QML file conventions
+
+See [CODING_STYLE.md](../CODING_STYLE.md) for the full style guide.
+
+## VS Code Integration
+
+The repository includes VS Code configuration in `.vscode/`:
+
+- **settings.json** - Editor settings, CMake integration
+- **extensions.json** - Recommended extensions
+- **tasks.json** - Build, test, format tasks
+- **launch.json** - Debug configurations
+
+Open the repository in VS Code and install recommended extensions for the best experience.
+
+## Centralized Configuration
+
+Version numbers and build settings are centralized in `.github/build-config.json`:
+
+```json
+{
+ "qt_version": "6.10.1",
+ "qt_modules": "qtcharts qtlocation ...",
+ "gstreamer_version": "1.24.12",
+ "ndk_version": "r27c",
+ ...
+}
+```
+
+Scripts read from this file to ensure consistent versions across local development and CI.
diff --git a/tools/analyze.sh b/tools/analyze.sh
new file mode 100755
index 000000000000..222317071f62
--- /dev/null
+++ b/tools/analyze.sh
@@ -0,0 +1,177 @@
+#!/usr/bin/env bash
+#
+# Run static analysis on QGroundControl source code
+#
+# Usage:
+# ./tools/analyze.sh # Analyze changed files (vs master)
+# ./tools/analyze.sh --all # Analyze all source files
+# ./tools/analyze.sh src/Vehicle/ # Analyze specific directory
+# ./tools/analyze.sh --tool clang-tidy # Use specific tool
+#
+# Tools:
+# clang-tidy - Clang static analyzer (default, requires compile_commands.json)
+# cppcheck - Cppcheck static analyzer
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+# Defaults
+TOOL="clang-tidy"
+ANALYZE_ALL=false
+TARGET_PATH=""
+BUILD_DIR="$REPO_ROOT/build"
+COMPILE_COMMANDS="$BUILD_DIR/compile_commands.json"
+
+show_help() {
+ head -15 "$0" | tail -13
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ -a|--all)
+ ANALYZE_ALL=true
+ shift
+ ;;
+ -t|--tool)
+ TOOL="$2"
+ shift 2
+ ;;
+ -b|--build-dir)
+ BUILD_DIR="$2"
+ COMPILE_COMMANDS="$BUILD_DIR/compile_commands.json"
+ shift 2
+ ;;
+ *)
+ TARGET_PATH="$1"
+ shift
+ ;;
+ esac
+done
+
+# Get files to analyze
+get_files() {
+ if [[ -n "$TARGET_PATH" ]]; then
+ find "$REPO_ROOT/$TARGET_PATH" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null
+ elif [[ "$ANALYZE_ALL" == true ]]; then
+ find "$REPO_ROOT/src" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \)
+ else
+ # Only changed files vs master
+ git -C "$REPO_ROOT" diff --name-only master... -- '*.cc' '*.cpp' '*.h' '*.hpp' 2>/dev/null | \
+ xargs -I{} echo "$REPO_ROOT/{}" | \
+ while read -r f; do [[ -f "$f" ]] && echo "$f"; done
+ fi
+}
+
+run_clang_tidy() {
+ if [[ ! -f "$COMPILE_COMMANDS" ]]; then
+ log_error "compile_commands.json not found at $COMPILE_COMMANDS"
+ log_info "Run: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
+ exit 1
+ fi
+
+ if ! command -v clang-tidy &> /dev/null; then
+ log_error "clang-tidy not found. Install with: sudo apt install clang-tidy"
+ exit 1
+ fi
+
+ local files
+ files=$(get_files)
+
+ if [[ -z "$files" ]]; then
+ log_info "No files to analyze"
+ exit 0
+ fi
+
+ local file_count
+ file_count=$(echo "$files" | wc -l)
+ log_info "Running clang-tidy on $file_count files..."
+
+ local exit_code=0
+ echo "$files" | while read -r file; do
+ echo -n " Analyzing: ${file#"$REPO_ROOT"/}... "
+ if clang-tidy -p "$BUILD_DIR" "$file" 2>/dev/null; then
+ echo -e "${GREEN}OK${NC}"
+ else
+ echo -e "${RED}ISSUES${NC}"
+ exit_code=1
+ fi
+ done
+
+ return $exit_code
+}
+
+run_cppcheck() {
+ if ! command -v cppcheck &> /dev/null; then
+ log_error "cppcheck not found. Install with: sudo apt install cppcheck"
+ exit 1
+ fi
+
+ local files
+ files=$(get_files)
+
+ if [[ -z "$files" ]]; then
+ log_info "No files to analyze"
+ exit 0
+ fi
+
+ local file_count
+ file_count=$(echo "$files" | wc -l)
+ log_info "Running cppcheck on $file_count files..."
+
+ # Create file list
+ local filelist
+ filelist=$(mktemp)
+ echo "$files" > "$filelist"
+
+ cppcheck \
+ --enable=warning,style,performance,portability \
+ --std=c++20 \
+ --suppress=missingIncludeSystem \
+ --suppress=unmatchedSuppression \
+ --inline-suppr \
+ --file-list="$filelist" \
+ --error-exitcode=1 \
+ 2>&1
+
+ local exit_code=$?
+ rm -f "$filelist"
+ return $exit_code
+}
+
+# Main
+cd "$REPO_ROOT"
+
+case "$TOOL" in
+ clang-tidy)
+ run_clang_tidy
+ ;;
+ cppcheck)
+ run_cppcheck
+ ;;
+ *)
+ log_error "Unknown tool: $TOOL"
+ log_info "Available tools: clang-tidy, cppcheck"
+ exit 1
+ ;;
+esac
+
+log_ok "Static analysis complete"
diff --git a/tools/ccache.conf b/tools/ccache.conf
index e00b8710c6aa..30c1c95ae41a 100644
--- a/tools/ccache.conf
+++ b/tools/ccache.conf
@@ -1,2 +1,22 @@
+# ccache configuration for QGroundControl
+#
+# Note: CMake automatically configures ccache when building QGC.
+# This file documents the settings and can be used standalone:
+# export CCACHE_CONFIGPATH=/path/to/qgroundcontrol/tools/ccache.conf
+#
+# CMake sets CCACHE_DIR to .cache/ccache in the project root.
+
+# Maximum cache size
+max_size = 2G
+
+# Cache directory (when using this config standalone)
+# cache_dir = /path/to/qgroundcontrol/.cache/ccache
+
+# Compression level (1-19, higher = smaller but slower)
compression_level = 5
+
+# Allow ccache to work with precompiled headers and time-based macros
sloppiness = pch_defines,time_macros,include_file_mtime,include_file_ctime
+
+# Useful for debugging cache misses (uncomment to enable)
+# log_file = /tmp/ccache.log
diff --git a/tools/check-deps.sh b/tools/check-deps.sh
new file mode 100755
index 000000000000..d04e629170a1
--- /dev/null
+++ b/tools/check-deps.sh
@@ -0,0 +1,260 @@
+#!/usr/bin/env bash
+#
+# Check for outdated dependencies and submodules
+#
+# Usage:
+# ./tools/check-deps.sh # Check all dependencies
+# ./tools/check-deps.sh --submodules # Check only git submodules
+# ./tools/check-deps.sh --qt # Check Qt version
+# ./tools/check-deps.sh --update # Update submodules to latest
+#
+# Checks:
+# - Git submodules vs upstream
+# - Qt version vs latest
+# - GStreamer version vs latest
+# - Python dependencies
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+# Defaults
+CHECK_ALL=true
+CHECK_SUBMODULES=false
+CHECK_QT=false
+UPDATE_DEPS=false
+
+show_help() {
+ head -15 "$0" | tail -13
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ --submodules)
+ CHECK_ALL=false
+ CHECK_SUBMODULES=true
+ shift
+ ;;
+ --qt)
+ CHECK_ALL=false
+ CHECK_QT=true
+ shift
+ ;;
+ --update)
+ UPDATE_DEPS=true
+ shift
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+check_submodules() {
+ log_info "Checking git submodules..."
+
+ cd "$REPO_ROOT"
+
+ # Initialize submodules if needed
+ git submodule update --init --recursive 2>/dev/null || true
+
+ local outdated=0
+
+ while IFS= read -r line; do
+ if [[ -z "$line" ]]; then
+ continue
+ fi
+
+ # Parse submodule status (status and hash reserved for future use)
+ local _status="${line:0:1}"
+ local _hash="${line:1:40}"
+ local path
+ path=$(echo "$line" | awk '{print $2}')
+
+ if [[ ! -d "$REPO_ROOT/$path" ]]; then
+ continue
+ fi
+
+ cd "$REPO_ROOT/$path"
+
+ # Get current and remote info (reserved for future verbose output)
+ local _current_hash _branch
+ _current_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
+ _branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached")
+
+ # Fetch updates quietly
+ git fetch --quiet 2>/dev/null || true
+
+ # Check if behind
+ local behind=0
+ # shellcheck disable=SC1083 # @{u} is valid git syntax for upstream
+ if git rev-parse '@{u}' &>/dev/null; then
+ behind=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo "0")
+ fi
+
+ if [[ "$behind" -gt 0 ]]; then
+ log_warn "$path: $behind commits behind upstream"
+ ((outdated++)) || true
+ else
+ echo -e " ${GREEN}✓${NC} $path (up to date)"
+ fi
+
+ cd "$REPO_ROOT"
+ done < <(git submodule status --recursive 2>/dev/null)
+
+ if [[ "$outdated" -eq 0 ]]; then
+ log_ok "All submodules up to date"
+ else
+ log_warn "$outdated submodule(s) have updates available"
+ if [[ "$UPDATE_DEPS" == true ]]; then
+ log_info "Updating submodules..."
+ git submodule update --remote --merge
+ log_ok "Submodules updated"
+ else
+ log_info "Run with --update to update submodules"
+ fi
+ fi
+}
+
+check_qt_version() {
+ log_info "Checking Qt version..."
+
+ # Read current version from config
+ local config_file="$REPO_ROOT/.github/build-config.json"
+ if [[ ! -f "$config_file" ]]; then
+ log_warn "build-config.json not found"
+ return
+ fi
+
+ local current_version
+ current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('qt_version', 'unknown'))")
+
+ echo " Current: Qt $current_version"
+
+ # Check latest Qt version (from qt.io)
+ local latest_info
+ if command -v curl &> /dev/null; then
+ # Try to get latest version info
+ latest_info=$(curl -s "https://download.qt.io/official_releases/qt/" 2>/dev/null | \
+ grep -oP '6\.\d+' | sort -V | tail -1 || echo "")
+
+ if [[ -n "$latest_info" ]]; then
+ echo " Latest minor: Qt $latest_info.x"
+
+ local current_minor="${current_version%.*}"
+ if [[ "$current_minor" != "$latest_info" ]]; then
+ log_warn "Newer Qt minor version available: $latest_info"
+ else
+ log_ok "Using latest Qt minor version"
+ fi
+ fi
+ fi
+
+ # Check installed Qt
+ if command -v qmake &> /dev/null; then
+ local installed
+ installed=$(qmake --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "not found")
+ echo " Installed: Qt $installed"
+ elif [[ -n "${QT_ROOT_DIR:-}" ]]; then
+ echo " QT_ROOT_DIR: $QT_ROOT_DIR"
+ fi
+}
+
+check_gstreamer_version() {
+ log_info "Checking GStreamer version..."
+
+ # Read current version from config
+ local config_file="$REPO_ROOT/.github/build-config.json"
+ if [[ ! -f "$config_file" ]]; then
+ return
+ fi
+
+ local current_version
+ current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('gstreamer_version', 'unknown'))")
+
+ echo " Configured: GStreamer $current_version"
+
+ # Check installed version
+ if command -v gst-launch-1.0 &> /dev/null; then
+ local installed
+ installed=$(gst-launch-1.0 --version 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' || echo "not found")
+ echo " Installed: GStreamer $installed"
+ fi
+}
+
+check_python_deps() {
+ log_info "Checking Python dependencies..."
+
+ local req_files=("requirements.txt" "docs/requirements.txt")
+
+ for req in "${req_files[@]}"; do
+ if [[ -f "$REPO_ROOT/$req" ]]; then
+ echo " Checking $req..."
+ if command -v pip &> /dev/null; then
+ # Check for outdated packages
+ local outdated
+ outdated=$(pip list --outdated --format=columns 2>/dev/null | tail -n +3 || echo "")
+ if [[ -n "$outdated" ]]; then
+ echo "$outdated" | while read -r line; do
+ echo " $line"
+ done
+ fi
+ fi
+ fi
+ done
+}
+
+check_build_tools() {
+ log_info "Checking build tools..."
+
+ local tools=("cmake" "ninja" "ccache" "clang-format" "clang-tidy")
+
+ for tool in "${tools[@]}"; do
+ if command -v "$tool" &> /dev/null; then
+ local version
+ version=$("$tool" --version 2>/dev/null | head -1 || echo "unknown")
+ echo -e " ${GREEN}✓${NC} $tool: $version"
+ else
+ echo -e " ${YELLOW}✗${NC} $tool: not installed"
+ fi
+ done
+}
+
+# Main
+cd "$REPO_ROOT"
+
+if [[ "$CHECK_ALL" == true ]]; then
+ check_submodules
+ echo ""
+ check_qt_version
+ echo ""
+ check_gstreamer_version
+ echo ""
+ check_build_tools
+elif [[ "$CHECK_SUBMODULES" == true ]]; then
+ check_submodules
+elif [[ "$CHECK_QT" == true ]]; then
+ check_qt_version
+fi
+
+echo ""
+log_ok "Dependency check complete"
diff --git a/tools/clean.sh b/tools/clean.sh
new file mode 100755
index 000000000000..bf0a789b727d
--- /dev/null
+++ b/tools/clean.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+#
+# Clean build artifacts and caches
+#
+# Usage:
+# ./tools/clean.sh # Clean build directory
+# ./tools/clean.sh --all # Clean everything (build, caches, generated files)
+# ./tools/clean.sh --cache # Clean only caches (ccache, pip, etc.)
+#
+# This script removes:
+# - build/ CMake build directory
+# - .cache/ Local caches (ccache, clangd index)
+# - *.user Qt Creator user files
+# - CMakeUserPresets.json
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
+
+# Defaults
+CLEAN_ALL=false
+CLEAN_CACHE_ONLY=false
+DRY_RUN=false
+
+show_help() {
+ head -14 "$0" | tail -12
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ -a|--all)
+ CLEAN_ALL=true
+ shift
+ ;;
+ -c|--cache)
+ CLEAN_CACHE_ONLY=true
+ shift
+ ;;
+ -n|--dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ *)
+ echo "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+cd "$REPO_ROOT"
+
+remove_if_exists() {
+ local path="$1"
+ local desc="${2:-$path}"
+
+ if [[ -e "$path" ]]; then
+ if [[ "$DRY_RUN" == true ]]; then
+ log_info "Would remove: $desc"
+ else
+ log_info "Removing: $desc"
+ rm -rf "$path"
+ fi
+ fi
+}
+
+clean_build() {
+ remove_if_exists "build" "build directory"
+ remove_if_exists "CMakeUserPresets.json" "CMake user presets"
+
+ # Qt Creator user files
+ find . -maxdepth 1 -name "*.user" -type f 2>/dev/null | while read -r f; do
+ remove_if_exists "$f" "Qt Creator user file: $f"
+ done
+
+ # CMake generated files in source
+ find . -maxdepth 1 -name "CMakeFiles" -type d 2>/dev/null | while read -r d; do
+ remove_if_exists "$d" "CMake files: $d"
+ done
+}
+
+clean_cache() {
+ remove_if_exists ".cache" "local cache directory"
+
+ # clangd index
+ remove_if_exists ".clangd" "clangd index"
+
+ # ccache stats (not the cache itself, just local stats)
+ if command -v ccache &> /dev/null; then
+ if [[ "$DRY_RUN" == true ]]; then
+ log_info "Would clear ccache statistics"
+ else
+ log_info "Clearing ccache statistics"
+ ccache --zero-stats 2>/dev/null || true
+ fi
+ fi
+}
+
+clean_generated() {
+ # Compiled Python files
+ find . -type d -name "__pycache__" 2>/dev/null | while read -r d; do
+ remove_if_exists "$d" "Python cache: $d"
+ done
+
+ find . -name "*.pyc" -type f 2>/dev/null | while read -r f; do
+ remove_if_exists "$f" "Python bytecode: $f"
+ done
+
+ # Generated translation files (keep source .ts files)
+ # remove_if_exists "translations/*.qm" "compiled translation files"
+}
+
+# Main
+if [[ "$DRY_RUN" == true ]]; then
+ log_warn "Dry run mode - no files will be removed"
+fi
+
+if [[ "$CLEAN_CACHE_ONLY" == true ]]; then
+ clean_cache
+elif [[ "$CLEAN_ALL" == true ]]; then
+ clean_build
+ clean_cache
+ clean_generated
+else
+ clean_build
+fi
+
+log_ok "Clean complete"
+
+# Show disk space recovered
+if [[ "$DRY_RUN" != true ]]; then
+ echo ""
+ log_info "Disk usage: $(du -sh "$REPO_ROOT" 2>/dev/null | cut -f1)"
+fi
diff --git a/CodingStyle.cc b/tools/coding-style/CodingStyle.cc
similarity index 66%
rename from CodingStyle.cc
rename to tools/coding-style/CodingStyle.cc
index 762d255a8745..ff6989bc8f00 100644
--- a/CodingStyle.cc
+++ b/tools/coding-style/CodingStyle.cc
@@ -1,6 +1,6 @@
/****************************************************************************
*
- * (c) 2009-2024 QGROUNDCONTROL PROJECT
+ * (c) 2009-2025 QGROUNDCONTROL PROJECT
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
@@ -10,10 +10,14 @@
// This is an example class c++ file which is used to describe the QGroundControl
// coding style. In general almost everything in here has some coding style meaning.
// Not all style choices are explained.
+//
+// QGroundControl requires C++20. Use modern C++ features where appropriate.
#include "CodingStyle.h"
-#include
+#include
+#include
+#include
#include
#include
@@ -162,3 +166,77 @@ void CodingStyle::_methodWithManyArguments(
// This makes it clear the parameter is intentionally unused
// Implementation here...
}
+
+// =============================================================================
+// C++20 Features Examples
+// =============================================================================
+
+bool CodingStyle::validateInput(std::string_view input) const
+{
+ // C++20: std::string_view avoids allocations for read-only string operations
+ // Use when interfacing with non-Qt code or performance-critical paths
+ if (input.empty()) {
+ return false;
+ }
+
+ // C++20: Use std::ranges algorithms for cleaner code
+ return std::ranges::all_of(input, [](char c) {
+ return std::isalnum(static_cast(c)) || c == '_';
+ });
+}
+
+void CodingStyle::processData(std::span data)
+{
+ // C++20: std::span provides safe, bounds-checked view of contiguous data
+ // Replaces (int* ptr, size_t size) parameter pairs
+ if (data.empty()) {
+ qCDebug(CodingStyleLog) << "No data to process";
+ return;
+ }
+
+ // C++20: Range-based for with init-statement
+ for (int sum = 0; const int value : data) {
+ sum += value;
+ qCDebug(CodingStyleLog) << "Running sum:" << sum;
+ }
+
+ // C++20: Use ranges for transformations
+ // Example: filter and transform in a pipeline
+ auto positiveDoubled = data
+ | std::views::filter([](int n) { return n > 0; })
+ | std::views::transform([](int n) { return n * 2; });
+
+ for (const int value : positiveDoubled) {
+ qCDebug(CodingStyleLog) << "Positive doubled:" << value;
+ }
+}
+
+// C++20: Use designated initializers for aggregate types (defined in header or locally)
+namespace {
+ struct ConfigOptions {
+ int timeout = 30;
+ bool enabled = true;
+ int retryCount = 3;
+ };
+
+ void exampleDesignatedInitializers()
+ {
+ // C++20: Designated initializers make struct initialization clear
+ const ConfigOptions config{
+ .timeout = 60,
+ .enabled = true,
+ .retryCount = 5
+ };
+ Q_UNUSED(config);
+ }
+
+ // C++20: Concepts can constrain template parameters (use sparingly, prefer concrete types)
+ template
+ concept Numeric = std::integral || std::floating_point;
+
+ template
+ T clampValue(T value, T min, T max)
+ {
+ return std::clamp(value, min, max);
+ }
+} // anonymous namespace
diff --git a/CodingStyle.h b/tools/coding-style/CodingStyle.h
similarity index 84%
rename from CodingStyle.h
rename to tools/coding-style/CodingStyle.h
index c06255c0340e..dbd816443426 100644
--- a/CodingStyle.h
+++ b/tools/coding-style/CodingStyle.h
@@ -1,6 +1,6 @@
/****************************************************************************
*
- * (c) 2009-2024 QGROUNDCONTROL PROJECT
+ * (c) 2009-2025 QGROUNDCONTROL PROJECT
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
@@ -10,10 +10,14 @@
// This is an example class header file which is used to describe the QGroundControl
// coding style. In general almost everything in here has some coding style meaning.
// Not all style choices are explained.
+//
+// QGroundControl requires C++20. Use modern C++ features where appropriate.
#pragma once
#include
+#include
+#include
#include
#include
@@ -68,14 +72,21 @@ class CodingStyle : public QObject
/// Document public methods which are non-obvious in the header file
/// Use Q_INVOKABLE for methods callable from QML
- Q_INVOKABLE bool publicMethod1();
+ /// Use [[nodiscard]] for functions whose return value should not be ignored
+ [[nodiscard]] Q_INVOKABLE bool publicMethod1();
Q_INVOKABLE void performAction(const QString& param);
- // Public getters/setters
- int exampleProperty() const { return _exampleProperty; }
+ // C++20: Use std::string_view for read-only string parameters when Qt types aren't needed
+ [[nodiscard]] bool validateInput(std::string_view input) const;
+
+ // C++20: Use std::span for array-like parameters instead of pointer + size
+ void processData(std::span data);
+
+ // Public getters/setters - use [[nodiscard]] for getters
+ [[nodiscard]] int exampleProperty() const { return _exampleProperty; }
void setExampleProperty(int value);
- Vehicle* vehicle() const { return _vehicle; }
- bool readOnlyProperty() const { return _readOnlyProperty; }
+ [[nodiscard]] Vehicle* vehicle() const { return _vehicle; }
+ [[nodiscard]] bool readOnlyProperty() const { return _readOnlyProperty; }
signals:
/// Document signals which are non-obvious in the header file
diff --git a/CodingStyle.qml b/tools/coding-style/CodingStyle.qml
similarity index 99%
rename from CodingStyle.qml
rename to tools/coding-style/CodingStyle.qml
index cb34b1402888..8f904b88b894 100644
--- a/CodingStyle.qml
+++ b/tools/coding-style/CodingStyle.qml
@@ -1,6 +1,6 @@
/****************************************************************************
*
- * (c) 2009-2024 QGROUNDCONTROL PROJECT
+ * (c) 2009-2025 QGROUNDCONTROL PROJECT
*
* QGroundControl is licensed according to the terms in the file
* COPYING.md in the root of the source code directory.
diff --git a/tools/coverage.sh b/tools/coverage.sh
new file mode 100755
index 000000000000..48ef3549ccc0
--- /dev/null
+++ b/tools/coverage.sh
@@ -0,0 +1,226 @@
+#!/usr/bin/env bash
+#
+# Generate code coverage reports for QGroundControl
+#
+# Usage:
+# ./tools/coverage.sh # Build with coverage and run tests
+# ./tools/coverage.sh --report # Generate HTML report only (after tests)
+# ./tools/coverage.sh --open # Generate and open report in browser
+# ./tools/coverage.sh --clean # Clean coverage data
+#
+# Requirements:
+# - gcov (from GCC) or llvm-cov
+# - lcov and genhtml (for HTML reports)
+# - Optional: gcovr (for Cobertura XML output)
+#
+# The script configures CMake with coverage flags, runs tests, and generates reports.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+# Defaults
+BUILD_DIR="$REPO_ROOT/build-coverage"
+COVERAGE_DIR="$REPO_ROOT/coverage"
+REPORT_ONLY=false
+OPEN_REPORT=false
+CLEAN_ONLY=false
+TEST_FILTER=""
+
+show_help() {
+ head -16 "$0" | tail -14
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ -r|--report)
+ REPORT_ONLY=true
+ shift
+ ;;
+ -o|--open)
+ OPEN_REPORT=true
+ shift
+ ;;
+ -c|--clean)
+ CLEAN_ONLY=true
+ shift
+ ;;
+ -t|--test)
+ TEST_FILTER="$2"
+ shift 2
+ ;;
+ -b|--build-dir)
+ BUILD_DIR="$2"
+ shift 2
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+check_dependencies() {
+ local missing=()
+
+ if ! command -v lcov &> /dev/null; then
+ missing+=("lcov")
+ fi
+ if ! command -v genhtml &> /dev/null; then
+ missing+=("genhtml (part of lcov)")
+ fi
+
+ if [[ ${#missing[@]} -gt 0 ]]; then
+ log_error "Missing dependencies: ${missing[*]}"
+ log_info "Install with: sudo apt install lcov"
+ exit 1
+ fi
+}
+
+clean_coverage() {
+ log_info "Cleaning coverage data..."
+ rm -rf "$BUILD_DIR"
+ rm -rf "$COVERAGE_DIR"
+ find "$REPO_ROOT" -name "*.gcda" -delete 2>/dev/null || true
+ find "$REPO_ROOT" -name "*.gcno" -delete 2>/dev/null || true
+ log_ok "Coverage data cleaned"
+}
+
+configure_build() {
+ log_info "Configuring build with coverage..."
+
+ cmake -B "$BUILD_DIR" -S "$REPO_ROOT" \
+ -DCMAKE_BUILD_TYPE=Debug \
+ -DCMAKE_C_FLAGS="--coverage -fprofile-arcs -ftest-coverage" \
+ -DCMAKE_CXX_FLAGS="--coverage -fprofile-arcs -ftest-coverage" \
+ -DCMAKE_EXE_LINKER_FLAGS="--coverage" \
+ -DQGC_BUILD_TESTING=ON \
+ -G Ninja
+
+ log_ok "Build configured"
+}
+
+build_project() {
+ log_info "Building project..."
+ cmake --build "$BUILD_DIR" --parallel
+ log_ok "Build complete"
+}
+
+run_tests() {
+ log_info "Running tests..."
+
+ local test_args="--unittest"
+ if [[ -n "$TEST_FILTER" ]]; then
+ test_args="--unittest:$TEST_FILTER"
+ fi
+
+ # Run tests (continue even if some fail)
+ # shellcheck disable=SC2086 # Intentional word splitting for test args
+ "$BUILD_DIR/QGroundControl" $test_args || true
+
+ log_ok "Tests complete"
+}
+
+generate_report() {
+ log_info "Generating coverage report..."
+
+ mkdir -p "$COVERAGE_DIR"
+
+ # Capture coverage data
+ lcov --capture \
+ --directory "$BUILD_DIR" \
+ --output-file "$COVERAGE_DIR/coverage.info" \
+ --ignore-errors mismatch \
+ --rc lcov_branch_coverage=1
+
+ # Remove system headers and test files from coverage
+ lcov --remove "$COVERAGE_DIR/coverage.info" \
+ '/usr/*' \
+ '*/build/*' \
+ '*/test/*' \
+ '*/libs/*' \
+ '*/_deps/*' \
+ '*/Qt/*' \
+ --output-file "$COVERAGE_DIR/coverage.filtered.info" \
+ --rc lcov_branch_coverage=1
+
+ # Generate HTML report
+ genhtml "$COVERAGE_DIR/coverage.filtered.info" \
+ --output-directory "$COVERAGE_DIR/html" \
+ --title "QGroundControl Coverage Report" \
+ --legend \
+ --branch-coverage \
+ --highlight
+
+ # Print summary
+ echo ""
+ log_ok "Coverage report generated: $COVERAGE_DIR/html/index.html"
+
+ # Show summary statistics
+ lcov --summary "$COVERAGE_DIR/coverage.filtered.info" 2>&1 | grep -E "(lines|functions|branches)"
+}
+
+open_report() {
+ local report="$COVERAGE_DIR/html/index.html"
+ if [[ ! -f "$report" ]]; then
+ log_error "Report not found. Run coverage first."
+ exit 1
+ fi
+
+ log_info "Opening coverage report..."
+
+ if command -v xdg-open &> /dev/null; then
+ xdg-open "$report"
+ elif command -v open &> /dev/null; then
+ open "$report"
+ else
+ log_warn "Could not open browser. Report at: $report"
+ fi
+}
+
+# Main
+cd "$REPO_ROOT"
+check_dependencies
+
+if [[ "$CLEAN_ONLY" == true ]]; then
+ clean_coverage
+ exit 0
+fi
+
+if [[ "$REPORT_ONLY" == true ]]; then
+ generate_report
+ if [[ "$OPEN_REPORT" == true ]]; then
+ open_report
+ fi
+ exit 0
+fi
+
+# Full coverage workflow
+configure_build
+build_project
+run_tests
+generate_report
+
+if [[ "$OPEN_REPORT" == true ]]; then
+ open_report
+fi
+
+log_ok "Coverage complete!"
diff --git a/tools/format-check.sh b/tools/format-check.sh
new file mode 100755
index 000000000000..3dcbac0310bf
--- /dev/null
+++ b/tools/format-check.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env bash
+#
+# Check or apply clang-format to source files
+#
+# Usage:
+# ./tools/format-check.sh # Format changed files (vs master)
+# ./tools/format-check.sh --check # Check only, don't modify (for CI)
+# ./tools/format-check.sh --all # Format all source files
+# ./tools/format-check.sh src/Vehicle/ # Format specific directory
+#
+# Requires: clang-format (version 17+ recommended)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+# Defaults
+CHECK_ONLY=false
+FORMAT_ALL=false
+TARGET_PATH=""
+
+show_help() {
+ head -12 "$0" | tail -10
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ -c|--check)
+ CHECK_ONLY=true
+ shift
+ ;;
+ -a|--all)
+ FORMAT_ALL=true
+ shift
+ ;;
+ *)
+ TARGET_PATH="$1"
+ shift
+ ;;
+ esac
+done
+
+# Check for clang-format
+if ! command -v clang-format &> /dev/null; then
+ log_error "clang-format not found"
+ log_info "Install with: sudo apt install clang-format"
+ exit 1
+fi
+
+CLANG_FORMAT_VERSION=$(clang-format --version | grep -oP '\d+' | head -1)
+log_info "Using clang-format version $CLANG_FORMAT_VERSION"
+
+# Get files to format
+get_files() {
+ if [[ -n "$TARGET_PATH" ]]; then
+ find "$REPO_ROOT/$TARGET_PATH" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null
+ elif [[ "$FORMAT_ALL" == true ]]; then
+ find "$REPO_ROOT/src" "$REPO_ROOT/test" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null
+ else
+ # Only changed files vs master
+ git -C "$REPO_ROOT" diff --name-only master... -- '*.cc' '*.cpp' '*.h' '*.hpp' 2>/dev/null | \
+ xargs -I{} echo "$REPO_ROOT/{}" | \
+ while read -r f; do [[ -f "$f" ]] && echo "$f"; done
+ fi
+}
+
+cd "$REPO_ROOT"
+
+files=$(get_files)
+
+if [[ -z "$files" ]]; then
+ log_info "No files to format"
+ exit 0
+fi
+
+file_count=$(echo "$files" | wc -l)
+log_info "Processing $file_count files..."
+
+if [[ "$CHECK_ONLY" == true ]]; then
+ # Check mode - verify formatting without modifying
+ needs_format=()
+
+ while read -r file; do
+ if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then
+ needs_format+=("${file#"$REPO_ROOT"/}")
+ fi
+ done <<< "$files"
+
+ if [[ ${#needs_format[@]} -gt 0 ]]; then
+ log_error "The following files need formatting:"
+ for f in "${needs_format[@]}"; do
+ echo " $f"
+ done
+ echo ""
+ log_info "Run: ./tools/format-check.sh"
+ exit 1
+ fi
+
+ log_ok "All files properly formatted"
+else
+ # Format mode - modify files in place
+ formatted=0
+
+ while read -r file; do
+ if clang-format -i "$file"; then
+ ((formatted++))
+ fi
+ done <<< "$files"
+
+ log_ok "Formatted $formatted files"
+
+ # Show what changed
+ if git -C "$REPO_ROOT" diff --quiet; then
+ log_info "No formatting changes needed"
+ else
+ log_info "Files modified:"
+ git -C "$REPO_ROOT" diff --name-only
+ fi
+fi
diff --git a/tools/gdb-pretty-printers/README.md b/tools/gdb-pretty-printers/README.md
new file mode 100644
index 000000000000..8a61036d71da
--- /dev/null
+++ b/tools/gdb-pretty-printers/README.md
@@ -0,0 +1,65 @@
+# GDB Pretty Printers for Qt 6
+
+This directory contains GDB pretty printers for displaying Qt types in a human-readable format.
+
+## Setup
+
+### Quick Setup (per session)
+
+In GDB:
+```gdb
+source /path/to/qgroundcontrol/tools/gdb-pretty-printers/qt6.py
+```
+
+### Permanent Setup
+
+Add to your `~/.gdbinit`:
+```gdb
+python
+import sys
+sys.path.insert(0, '/path/to/qgroundcontrol/tools/gdb-pretty-printers')
+from qt6 import register_qt_printers
+register_qt_printers(None)
+end
+```
+
+### VS Code Setup
+
+The `.vscode/launch.json` in this repository is already configured to load these printers automatically.
+
+## Supported Types
+
+- **Strings**: QString, QByteArray
+- **Containers**: QList, QVector, QMap, QHash
+- **Geometry**: QPoint, QPointF, QSize, QSizeF, QRect, QRectF
+- **Other**: QVariant, QUrl, QSharedPointer
+
+## Example Output
+
+```
+(gdb) print myString
+$1 = "Hello, World!"
+
+(gdb) print myList
+$2 = QList (size=3)
+ [0] = 1
+ [1] = 2
+ [2] = 3
+
+(gdb) print myPoint
+$3 = (100, 200)
+
+(gdb) print myRect
+$4 = [(0, 0) 800x600]
+```
+
+## LLDB Support
+
+For LLDB (macOS), Qt provides built-in formatters. You can also use:
+
+```bash
+# In ~/.lldbinit
+command script import /path/to/qt6/plugins/lldb/qt6lldb.py
+```
+
+Or check if your Qt installation includes LLDB formatters in `$QT_ROOT/plugins/lldb/`.
diff --git a/tools/gdb-pretty-printers/qt6.py b/tools/gdb-pretty-printers/qt6.py
new file mode 100644
index 000000000000..7818b295ae44
--- /dev/null
+++ b/tools/gdb-pretty-printers/qt6.py
@@ -0,0 +1,357 @@
+"""
+GDB Pretty Printers for Qt 6 Types
+
+This module provides human-readable display of Qt types in GDB.
+
+Usage in GDB:
+ source /path/to/qgroundcontrol/tools/gdb-pretty-printers/qt6.py
+
+Usage in .gdbinit:
+ python
+ import sys
+ sys.path.insert(0, '/path/to/qgroundcontrol/tools/gdb-pretty-printers')
+ from qt6 import register_qt_printers
+ register_qt_printers(None)
+ end
+
+Based on KDE's Qt pretty printers, adapted for Qt 6.
+"""
+
+import gdb
+
+
+def _qstring_to_str(val):
+ """Convert a QString to a Python string."""
+ try:
+ d = val["d"]
+ if d == 0:
+ return '""'
+
+ # Qt 6 QString internal structure
+ size = int(d["size"])
+ if size == 0:
+ return '""'
+
+ # Data is stored inline after QStringData header in Qt 6
+ data_ptr = d["ptr"]
+ if data_ptr == 0:
+ return '""'
+
+ # Read UTF-16 data
+ inferior = gdb.selected_inferior()
+ mem = inferior.read_memory(int(data_ptr), size * 2)
+ return '"' + mem.tobytes().decode("utf-16-le", errors="replace") + '"'
+ except Exception as e:
+ return f""
+
+
+def _qbytearray_to_str(val):
+ """Convert a QByteArray to a Python string."""
+ try:
+ d = val["d"]
+ if d == 0:
+ return '""'
+
+ size = int(d["size"])
+ if size == 0:
+ return '""'
+
+ data_ptr = d["ptr"]
+ if data_ptr == 0:
+ return '""'
+
+ inferior = gdb.selected_inferior()
+ mem = inferior.read_memory(int(data_ptr), size)
+ return '"' + mem.tobytes().decode("utf-8", errors="replace") + '"'
+ except Exception as e:
+ return f""
+
+
+class QStringPrinter:
+ """Pretty printer for QString."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ return _qstring_to_str(self.val)
+
+ def display_hint(self):
+ return "string"
+
+
+class QByteArrayPrinter:
+ """Pretty printer for QByteArray."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ return _qbytearray_to_str(self.val)
+
+ def display_hint(self):
+ return "string"
+
+
+class QListPrinter:
+ """Pretty printer for QList."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ size = int(d["size"])
+ return f"QList<...> (size={size})"
+ except Exception:
+ return "QList<...>"
+
+ def children(self):
+ try:
+ d = self.val["d"]
+ size = int(d["size"])
+ ptr = d["ptr"]
+
+ # Get the element type
+ val_type = self.val.type.template_argument(0)
+
+ for i in range(min(size, 100)): # Limit to 100 elements
+ element = (ptr + i).dereference().cast(val_type)
+ yield f"[{i}]", element
+ except Exception:
+ pass
+
+ def display_hint(self):
+ return "array"
+
+
+class QVectorPrinter(QListPrinter):
+ """Pretty printer for QVector (alias of QList in Qt 6)."""
+
+ pass
+
+
+class QMapPrinter:
+ """Pretty printer for QMap."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ if d == 0:
+ return "QMap (empty)"
+ size = int(d["size"])
+ return f"QMap (size={size})"
+ except Exception:
+ return "QMap"
+
+ def display_hint(self):
+ return "map"
+
+
+class QHashPrinter:
+ """Pretty printer for QHash."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ if d == 0:
+ return "QHash (empty)"
+ size = int(d["size"])
+ return f"QHash (size={size})"
+ except Exception:
+ return "QHash"
+
+ def display_hint(self):
+ return "map"
+
+
+class QVariantPrinter:
+ """Pretty printer for QVariant."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ type_id = int(d["typeId"])
+
+ # Common type IDs in Qt 6
+ type_names = {
+ 0: "Invalid",
+ 1: "Bool",
+ 2: "Int",
+ 3: "UInt",
+ 4: "LongLong",
+ 5: "ULongLong",
+ 6: "Double",
+ 7: "Char",
+ 10: "QString",
+ 11: "QStringList",
+ 12: "QByteArray",
+ 13: "QBitArray",
+ 14: "QDate",
+ 15: "QTime",
+ 16: "QDateTime",
+ 17: "QUrl",
+ }
+
+ type_name = type_names.get(type_id, f"type={type_id}")
+ return f"QVariant({type_name})"
+ except Exception:
+ return "QVariant"
+
+
+class QPointPrinter:
+ """Pretty printer for QPoint/QPointF."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ x = self.val["xp"]
+ y = self.val["yp"]
+ return f"({x}, {y})"
+ except Exception:
+ return "QPoint"
+
+
+class QSizePrinter:
+ """Pretty printer for QSize/QSizeF."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ w = self.val["wd"]
+ h = self.val["ht"]
+ return f"{w}x{h}"
+ except Exception:
+ return "QSize"
+
+
+class QRectPrinter:
+ """Pretty printer for QRect/QRectF."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ x1 = self.val["x1"]
+ y1 = self.val["y1"]
+ x2 = self.val["x2"]
+ y2 = self.val["y2"]
+ return f"[({x1}, {y1}) - ({x2}, {y2})]"
+ except Exception:
+ try:
+ x = self.val["xp"]
+ y = self.val["yp"]
+ w = self.val["w"]
+ h = self.val["h"]
+ return f"[({x}, {y}) {w}x{h}]"
+ except Exception:
+ return "QRect"
+
+
+class QUrlPrinter:
+ """Pretty printer for QUrl."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ if d == 0:
+ return "QUrl(empty)"
+
+ # Try to get the full URL string
+ scheme = d["scheme"]
+ host = d["host"]
+ path = d["path"]
+
+ parts = []
+ if scheme:
+ parts.append(_qstring_to_str(scheme).strip('"'))
+ if host:
+ parts.append("://" + _qstring_to_str(host).strip('"'))
+ if path:
+ parts.append(_qstring_to_str(path).strip('"'))
+
+ return f"QUrl({(''.join(parts))})"
+ except Exception:
+ return "QUrl"
+
+
+class QSharedPointerPrinter:
+ """Pretty printer for QSharedPointer."""
+
+ def __init__(self, val):
+ self.val = val
+
+ def to_string(self):
+ try:
+ d = self.val["d"]
+ value = self.val["value"]
+
+ if value == 0:
+ return "QSharedPointer(nullptr)"
+
+ if d != 0:
+ strong = int(d["strongref"]["_q_value"])
+ weak = int(d["weakref"]["_q_value"])
+ return f"QSharedPointer(strong={strong}, weak={weak}) = {value}"
+
+ return f"QSharedPointer = {value}"
+ except Exception:
+ return "QSharedPointer"
+
+
+def build_qt_printer():
+ """Build and return the Qt pretty printer collection."""
+ pp = gdb.printing.RegexpCollectionPrettyPrinter("Qt6")
+
+ # Core types
+ pp.add_printer("QString", "^QString$", QStringPrinter)
+ pp.add_printer("QByteArray", "^QByteArray$", QByteArrayPrinter)
+ pp.add_printer("QVariant", "^QVariant$", QVariantPrinter)
+ pp.add_printer("QUrl", "^QUrl$", QUrlPrinter)
+
+ # Containers
+ pp.add_printer("QList", "^QList<.*>$", QListPrinter)
+ pp.add_printer("QVector", "^QVector<.*>$", QVectorPrinter)
+ pp.add_printer("QMap", "^QMap<.*>$", QMapPrinter)
+ pp.add_printer("QHash", "^QHash<.*>$", QHashPrinter)
+
+ # Geometry
+ pp.add_printer("QPoint", "^QPoint$", QPointPrinter)
+ pp.add_printer("QPointF", "^QPointF$", QPointPrinter)
+ pp.add_printer("QSize", "^QSize$", QSizePrinter)
+ pp.add_printer("QSizeF", "^QSizeF$", QSizePrinter)
+ pp.add_printer("QRect", "^QRect$", QRectPrinter)
+ pp.add_printer("QRectF", "^QRectF$", QRectPrinter)
+
+ # Smart pointers
+ pp.add_printer("QSharedPointer", "^QSharedPointer<.*>$", QSharedPointerPrinter)
+
+ return pp
+
+
+def register_qt_printers(obj):
+ """Register Qt pretty printers with GDB."""
+ gdb.printing.register_pretty_printer(obj, build_qt_printer(), replace=True)
+
+
+# Auto-register when loaded
+register_qt_printers(None)
+print("Qt 6 pretty printers loaded")
diff --git a/tools/generate-docs.sh b/tools/generate-docs.sh
new file mode 100755
index 000000000000..a879d9f8493a
--- /dev/null
+++ b/tools/generate-docs.sh
@@ -0,0 +1,239 @@
+#!/usr/bin/env bash
+#
+# Generate API documentation using Doxygen
+#
+# Usage:
+# ./tools/generate-docs.sh # Generate HTML docs
+# ./tools/generate-docs.sh --open # Generate and open in browser
+# ./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX)
+# ./tools/generate-docs.sh --clean # Clean generated docs
+#
+# Requirements:
+# - doxygen
+# - graphviz (for diagrams)
+# - Optional: texlive (for PDF output)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
+log_ok() { echo -e "${GREEN}[OK]${NC} $*"; }
+log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
+log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
+
+# Defaults
+OUTPUT_DIR="$REPO_ROOT/docs/api"
+DOXYFILE="$REPO_ROOT/Doxyfile"
+GENERATE_PDF=false
+OPEN_DOCS=false
+CLEAN_ONLY=false
+
+show_help() {
+ head -14 "$0" | tail -12
+ exit 0
+}
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--help)
+ show_help
+ ;;
+ -o|--open)
+ OPEN_DOCS=true
+ shift
+ ;;
+ --pdf)
+ GENERATE_PDF=true
+ shift
+ ;;
+ -c|--clean)
+ CLEAN_ONLY=true
+ shift
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+check_dependencies() {
+ if ! command -v doxygen &> /dev/null; then
+ log_error "doxygen not found. Install with: sudo apt install doxygen"
+ exit 1
+ fi
+
+ if ! command -v dot &> /dev/null; then
+ log_warn "graphviz not found. Diagrams will be disabled."
+ log_info "Install with: sudo apt install graphviz"
+ fi
+
+ if [[ "$GENERATE_PDF" == true ]] && ! command -v pdflatex &> /dev/null; then
+ log_error "pdflatex not found. Install with: sudo apt install texlive-latex-base"
+ exit 1
+ fi
+}
+
+clean_docs() {
+ log_info "Cleaning generated documentation..."
+ rm -rf "$OUTPUT_DIR"
+ log_ok "Documentation cleaned"
+}
+
+generate_doxyfile() {
+ log_info "Generating Doxyfile..."
+
+ cat > "$DOXYFILE" << 'DOXYFILE_CONTENT'
+# Doxyfile for QGroundControl
+
+PROJECT_NAME = "QGroundControl"
+PROJECT_BRIEF = "Ground Control Station for MAVLink Drones"
+PROJECT_NUMBER =
+OUTPUT_DIRECTORY = docs/api
+
+# Input
+INPUT = src
+INPUT_ENCODING = UTF-8
+FILE_PATTERNS = *.h *.cc *.cpp *.hpp *.qml *.md
+RECURSIVE = YES
+EXCLUDE_PATTERNS = */test/* */libs/* */_deps/* */build/*
+
+# Output formats
+GENERATE_HTML = YES
+HTML_OUTPUT = html
+GENERATE_LATEX = NO
+GENERATE_XML = NO
+
+# HTML settings
+HTML_COLORSTYLE_HUE = 220
+HTML_COLORSTYLE_SAT = 100
+HTML_COLORSTYLE_GAMMA = 80
+HTML_DYNAMIC_SECTIONS = YES
+DISABLE_INDEX = NO
+GENERATE_TREEVIEW = YES
+FULL_SIDEBAR = YES
+
+# Extraction settings
+EXTRACT_ALL = YES
+EXTRACT_PRIVATE = NO
+EXTRACT_STATIC = YES
+EXTRACT_LOCAL_CLASSES = YES
+
+# Source browser
+SOURCE_BROWSER = YES
+INLINE_SOURCES = NO
+STRIP_CODE_COMMENTS = YES
+REFERENCED_BY_RELATION = YES
+REFERENCES_RELATION = YES
+
+# Diagrams (requires graphviz)
+HAVE_DOT = YES
+DOT_NUM_THREADS = 0
+CLASS_DIAGRAMS = YES
+COLLABORATION_GRAPH = YES
+GROUP_GRAPHS = YES
+INCLUDE_GRAPH = YES
+INCLUDED_BY_GRAPH = YES
+CALL_GRAPH = NO
+CALLER_GRAPH = NO
+GRAPHICAL_HIERARCHY = YES
+DIRECTORY_GRAPH = YES
+DOT_IMAGE_FORMAT = svg
+INTERACTIVE_SVG = YES
+
+# Preprocessing
+ENABLE_PREPROCESSING = YES
+MACRO_EXPANSION = YES
+EXPAND_ONLY_PREDEF = NO
+PREDEFINED = Q_OBJECT Q_PROPERTY Q_INVOKABLE Q_SIGNAL Q_SLOT
+
+# Warnings
+QUIET = NO
+WARNINGS = YES
+WARN_IF_UNDOCUMENTED = NO
+WARN_IF_DOC_ERROR = YES
+
+# Other
+ALPHABETICAL_INDEX = YES
+GENERATE_TODOLIST = YES
+GENERATE_BUGLIST = YES
+SHOW_USED_FILES = YES
+SHOW_FILES = YES
+SHOW_NAMESPACES = YES
+DOXYFILE_CONTENT
+
+ log_ok "Doxyfile generated"
+}
+
+generate_docs() {
+ log_info "Generating documentation..."
+
+ mkdir -p "$OUTPUT_DIR"
+
+ # Generate Doxyfile if it doesn't exist
+ if [[ ! -f "$DOXYFILE" ]]; then
+ generate_doxyfile
+ fi
+
+ # Enable LaTeX if PDF requested
+ if [[ "$GENERATE_PDF" == true ]]; then
+ sed -i 's/GENERATE_LATEX.*= NO/GENERATE_LATEX = YES/' "$DOXYFILE"
+ fi
+
+ # Run doxygen
+ cd "$REPO_ROOT"
+ doxygen "$DOXYFILE"
+
+ log_ok "Documentation generated: $OUTPUT_DIR/html/index.html"
+
+ # Generate PDF if requested
+ if [[ "$GENERATE_PDF" == true ]]; then
+ log_info "Generating PDF..."
+ cd "$OUTPUT_DIR/latex"
+ make pdf
+ log_ok "PDF generated: $OUTPUT_DIR/latex/refman.pdf"
+ fi
+}
+
+open_docs() {
+ local index="$OUTPUT_DIR/html/index.html"
+ if [[ ! -f "$index" ]]; then
+ log_error "Documentation not found. Generate first."
+ exit 1
+ fi
+
+ log_info "Opening documentation..."
+
+ if command -v xdg-open &> /dev/null; then
+ xdg-open "$index"
+ elif command -v open &> /dev/null; then
+ open "$index"
+ else
+ log_warn "Could not open browser. Docs at: $index"
+ fi
+}
+
+# Main
+cd "$REPO_ROOT"
+
+if [[ "$CLEAN_ONLY" == true ]]; then
+ clean_docs
+ exit 0
+fi
+
+check_dependencies
+generate_docs
+
+if [[ "$OPEN_DOCS" == true ]]; then
+ open_docs
+fi
diff --git a/tools/log-analyzer/README.md b/tools/log-analyzer/README.md
new file mode 100644
index 000000000000..85332858de13
--- /dev/null
+++ b/tools/log-analyzer/README.md
@@ -0,0 +1,118 @@
+# QGC Log Analyzer
+
+Tools for analyzing QGroundControl logs and telemetry data.
+
+## Installation
+
+```bash
+# For basic log analysis
+# No dependencies required
+
+# For MAVLink telemetry logs (.tlog)
+pip install pymavlink
+```
+
+## Usage
+
+### Analyze Application Logs
+
+```bash
+# Show all entries
+./analyze_log.py ~/.local/share/QGroundControl/Logs/QGCConsole.log
+
+# Show only errors
+./analyze_log.py --errors QGCConsole.log
+
+# Show errors and warnings
+./analyze_log.py --warnings QGCConsole.log
+
+# Filter by component
+./analyze_log.py --component Vehicle QGCConsole.log
+
+# Filter by message pattern
+./analyze_log.py --message "parameter" QGCConsole.log
+
+# Show statistics
+./analyze_log.py --stats QGCConsole.log
+
+# Show timeline
+./analyze_log.py --timeline QGCConsole.log
+```
+
+### Analyze Telemetry Logs
+
+```bash
+# Analyze MAVLink telemetry log
+./analyze_log.py flight.tlog
+
+# Filter by MAVLink message type
+./analyze_log.py --message "HEARTBEAT" flight.tlog
+
+# Show statistics
+./analyze_log.py --stats flight.tlog
+```
+
+## Log Locations
+
+| Platform | Application Logs |
+|----------|-----------------|
+| Linux | `~/.local/share/QGroundControl/Logs/` |
+| macOS | `~/Library/Application Support/QGroundControl/Logs/` |
+| Windows | `%APPDATA%\QGroundControl\Logs\` |
+
+Telemetry logs (`.tlog`) are saved in the same directory.
+
+## Examples
+
+### Find Connection Issues
+
+```bash
+./analyze_log.py --message "connect|disconnect|timeout" QGCConsole.log
+```
+
+### Find Parameter Errors
+
+```bash
+./analyze_log.py --component parameter --errors QGCConsole.log
+```
+
+### Analyze Flight Session
+
+```bash
+# Get overview
+./analyze_log.py --stats flight.tlog
+
+# Find GPS issues
+./analyze_log.py --message "GPS|satellite" --warnings QGCConsole.log
+```
+
+## Output Formats
+
+### Default Output
+
+```
+[12:34:56.789] [INFO ] [qgc.vehicle] Connected to vehicle 1
+[12:34:57.123] [WARN ] [qgc.param] Parameter timeout
+[12:34:58.456] [ERROR] [qgc.link] Connection lost
+```
+
+### Statistics Output
+
+```
+=====================================
+LOG STATISTICS
+=====================================
+
+Total entries: 1234
+
+By level:
+ DEBUG: 500
+ INFO: 600
+ WARN: 100
+ ERROR: 34
+
+Top components:
+ qgc.vehicle: 300
+ qgc.mavlink: 250
+ qgc.param: 150
+```
diff --git a/tools/log-analyzer/analyze_log.py b/tools/log-analyzer/analyze_log.py
new file mode 100755
index 000000000000..1f3f689ffb47
--- /dev/null
+++ b/tools/log-analyzer/analyze_log.py
@@ -0,0 +1,371 @@
+#!/usr/bin/env python3
+"""
+QGroundControl Log Analyzer
+
+Analyzes QGC application logs and telemetry logs for debugging and diagnostics.
+
+Usage:
+ ./analyze_log.py # Analyze a log file
+ ./analyze_log.py --errors # Show only errors
+ ./analyze_log.py --warnings # Show errors and warnings
+ ./analyze_log.py --component Vehicle # Filter by component
+ ./analyze_log.py --timeline # Show timeline of events
+ ./analyze_log.py --stats # Show statistics
+
+Supports:
+ - QGC application logs
+ - MAVLink telemetry logs (.tlog)
+ - Console output logs
+
+Requirements:
+ pip install pymavlink # For .tlog files
+"""
+
+import argparse
+import os
+import re
+import sys
+from collections import Counter, defaultdict
+from datetime import datetime
+from pathlib import Path
+
+# Try to import pymavlink for tlog support
+try:
+ from pymavlink import mavutil
+
+ HAS_PYMAVLINK = True
+except ImportError:
+ HAS_PYMAVLINK = False
+
+
+class LogEntry:
+ """Represents a single log entry."""
+
+ def __init__(
+ self, timestamp=None, level=None, component=None, message=None, raw=None
+ ):
+ self.timestamp = timestamp
+ self.level = level or "INFO"
+ self.component = component or "unknown"
+ self.message = message or ""
+ self.raw = raw or ""
+
+ def __str__(self):
+ ts = (
+ self.timestamp.strftime("%H:%M:%S.%f")[:-3]
+ if self.timestamp
+ else "??:??:??"
+ )
+ return f"[{ts}] [{self.level:5}] [{self.component}] {self.message}"
+
+
+class QGCLogParser:
+ """Parser for QGC application logs."""
+
+ # Common QGC log patterns
+ PATTERNS = [
+ # Qt debug format: "qgc.component: message"
+ re.compile(r"^(?Pqgc\.[a-z.]+):\s*(?P.*)$", re.IGNORECASE),
+ # Timestamped format: "[HH:MM:SS.mmm] message"
+ re.compile(r"^\[(?P