diff --git a/.clang-format b/.clang-format index 658337b50c55..82bec87eac12 100644 --- a/.clang-format +++ b/.clang-format @@ -56,11 +56,11 @@ SpacesInSquareBrackets: false SortIncludes: CaseSensitive IncludeBlocks: Regroup IncludeCategories: - - Regex: '^' # Qt headers + - Regex: '^<[a-z].*>' # System/STL headers Priority: 1 - - Regex: '^<.*>' # System/STL headers + - Regex: '^' # Qt headers Priority: 2 - - Regex: '.*' # Project headers + - Regex: '.*' # Project headers Priority: 3 # Alignment diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe988a0c97ec..cb99286f0fda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,5 +11,8 @@ # *.qml @DonLakeFlyer # Build system -# CMakeLists.txt @HTRamsey -# /cmake/ @HTRamsey +CMakeLists.txt @HTRamsey +/cmake/ @HTRamsey + +# CI/CD +/.github/ @HTRamsey diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fd7ae9286829..58932ef48a3f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -89,7 +89,7 @@ QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for co ```bash git add . - git commit -m "Add feature: brief description" + git commit -m "feat: brief description" ``` 6. **Push to your fork** @@ -104,62 +104,7 @@ QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for co ## Coding Standards -For the complete coding style guide with examples, see [CODING_STYLE.md](../CODING_STYLE.md). - -### C++ Guidelines - -- **Standard**: C++20 -- **Framework**: Qt 6 guidelines -- **Naming Conventions**: - - Classes: `PascalCase` - - Methods/functions: `camelCase` - - Private members: `_leadingUnderscore` - - Constants: `ALL_CAPS` or `kPascalCase` - -- **Always use braces** for if/else/for/while statements - - ```cpp - // Good - if (condition) { - doSomething(); - } - - // Bad - if (condition) doSomething(); - ``` - -- **Defensive coding**: - - Always null-check pointers before use - - Validate all inputs - - Use Q_ASSERT for debug-build development checks only (compiled out in release builds) - - Always use defensive error handling in production code paths (never rely on Q_ASSERT) - - Handle errors gracefully in production code - -- **Code formatting**: - - Run `clang-format` before committing - - 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 - -- Follow Qt QML coding conventions -- Use type annotations -- Prefer declarative over imperative code -- See `src/QmlControls/QGCButton.qml` for examples - -### Logging - -Use Qt logging categories: - -```cpp -Q_DECLARE_LOGGING_CATEGORY(MyComponentLog) -QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name") - -qCDebug(MyComponentLog) << "Debug message:" << value; -qCWarning(MyComponentLog) << "Warning message"; -qCCritical(MyComponentLog) << "Critical error"; -``` +Follow [CODING_STYLE.md](../CODING_STYLE.md) for naming, formatting, C++20 features, QML style, and logging conventions. Run `clang-format` and `pre-commit run` before committing. ### Architecture Patterns diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a81ac38a42a5..4f9d9b726a9a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -79,7 +79,7 @@ body: label: System Information description: | Please include the following information: - - QGC Version: [e.g. 4.4.0] + - QGC Version: [e.g. 5.0.0] - QGC build: [e.g. daily, stable, self-built from source, etc...] - Operating System: [e.g. Windows 11, Ubuntu 22.04, macOS 15, iOS 17 ] - Flight Controller: [e.g. CubePilot Cube Orange, Pixhawk 6X, etc.] diff --git a/.github/actions/attest-and-upload/action.yml b/.github/actions/attest-and-upload/action.yml new file mode 100644 index 000000000000..1a86600caccc --- /dev/null +++ b/.github/actions/attest-and-upload/action.yml @@ -0,0 +1,59 @@ +name: Attest and Upload +description: Generate SBOM attestation and upload build artifact + +inputs: + artifact-name: + description: 'Artifact filename (e.g. QGroundControl.AppImage)' + required: true + package-name: + description: 'Package name for GitHub artifact' + required: true + scan-path: + description: 'Path to scan for SBOM (defaults to build dir)' + required: false + default: '' + subject-name: + description: 'SBOM subject name (defaults to package-name)' + required: false + default: '' + aws-role-arn: + description: 'AWS IAM role ARN for OIDC authentication' + required: false + default: '' + aws-key-id: + description: 'AWS access key ID' + required: false + default: '' + aws-secret-access-key: + description: 'AWS secret access key' + required: false + default: '' + aws-distribution-id: + description: 'AWS CloudFront distribution ID' + required: false + default: '' + upload-aws: + description: 'Upload artifact to AWS' + required: false + default: 'true' + +runs: + using: composite + steps: + - name: Attest Build with SBOM + uses: ./.github/actions/attest-sbom + with: + subject-path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + subject-name: ${{ inputs.subject-name || inputs.package-name }} + scan-path: ${{ inputs.scan-path || format('{0}/build', runner.temp) }} + + - name: Upload Build File + uses: ./.github/actions/upload + with: + artifact-name: ${{ inputs.artifact-name }} + package-name: ${{ inputs.package-name }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-distribution-id: ${{ inputs.aws-distribution-id }} + upload-aws: ${{ inputs.upload-aws }} diff --git a/.github/actions/attest-condition/action.yml b/.github/actions/attest-condition/action.yml deleted file mode 100644 index 50e34f613198..000000000000 --- a/.github/actions/attest-condition/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Attestation Condition Check -description: Check if attestation should be performed (shared logic for attest actions) - -inputs: - subject-path: - description: "Path to the artifact to attest" - required: true - condition: - description: "Additional condition (for example matrix.build_type == Release)" - required: false - default: "true" - -outputs: - skip: - description: "Whether to skip attestation (true/false)" - value: ${{ steps.check.outputs.skip }} - -runs: - using: composite - steps: - - name: Check attestation conditions - id: check - shell: bash - env: - EVENT_NAME: ${{ github.event_name }} - PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} - THIS_REPO: ${{ github.repository }} - CONDITION: ${{ inputs.condition }} - SUBJECT_PATH: ${{ inputs.subject-path }} - run: | - set -euo pipefail - - # Skip for external PRs (no repo permissions for attestations). - if [[ "${EVENT_NAME}" == "pull_request" ]]; then - if [[ "${PR_REPO}" != "${THIS_REPO}" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "Skipping attestation for external PR" - exit 0 - fi - fi - - if [[ "${CONDITION}" != "true" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "Skipping attestation: condition not met" - exit 0 - fi - - if [[ ! -e "${SUBJECT_PATH}" ]]; then - echo "skip=true" >> "${GITHUB_OUTPUT}" - echo "::warning::Artifact not found: ${SUBJECT_PATH}" - exit 0 - fi - - echo "skip=false" >> "${GITHUB_OUTPUT}" - echo "Will attest: ${SUBJECT_PATH}" diff --git a/.github/actions/attest-sbom/action.yml b/.github/actions/attest-sbom/action.yml index 28d16c03689b..4c480ef32bb6 100644 --- a/.github/actions/attest-sbom/action.yml +++ b/.github/actions/attest-sbom/action.yml @@ -25,52 +25,29 @@ outputs: runs: using: composite steps: - - name: Check attestation conditions + - name: Check attestation conditions and resolve paths id: check - uses: ./.github/actions/attest-condition - with: - subject-path: ${{ inputs.subject-path }} - - - name: Determine scan path - id: paths - if: steps.check.outputs.skip != 'true' shell: bash env: - SUBJECT_PATH: ${{ inputs.subject-path }} - SCAN_PATH: ${{ inputs.scan-path }} - SBOM_FORMAT: ${{ inputs.sbom-format }} - RUNNER_TEMP_DIR: ${{ runner.temp }} - SUBJECT_NAME: ${{ inputs.subject-name }} + EVENT_NAME: ${{ github.event_name }} + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} + THIS_REPO: ${{ github.repository }} run: | - set -euo pipefail - - subject_path="${SUBJECT_PATH}" - scan_path="${SCAN_PATH}" - - if [[ -z "${scan_path}" ]]; then - scan_path="$(dirname "${subject_path}")" - fi - - if [[ "${SBOM_FORMAT}" == "cyclonedx-json" ]]; then - suffix="cdx.json" - else - suffix="spdx.json" - fi - sbom_path="${RUNNER_TEMP_DIR}/${SUBJECT_NAME}.sbom.${suffix}" - - echo "scan-path=${scan_path}" >> "${GITHUB_OUTPUT}" - echo "sbom-path=${sbom_path}" >> "${GITHUB_OUTPUT}" - echo "Scan path: ${scan_path}" - echo "SBOM path: ${sbom_path}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/attest_helper.py" \ + --subject-path "${{ inputs.subject-path }}" \ + --subject-name "${{ inputs.subject-name }}" \ + --scan-path "${{ inputs.scan-path }}" \ + --sbom-format "${{ inputs.sbom-format }}" \ + --runner-temp "${{ runner.temp }}" - name: Generate SBOM id: sbom if: steps.check.outputs.skip != 'true' uses: anchore/sbom-action@v0 with: - path: ${{ steps.paths.outputs.scan-path }} + path: ${{ steps.check.outputs.scan-path }} format: ${{ inputs.sbom-format }} - output-file: ${{ steps.paths.outputs.sbom-path }} + output-file: ${{ steps.check.outputs.sbom-path }} upload-artifact: true upload-artifact-retention: 30 artifact-name: sbom-${{ inputs.subject-name }} @@ -86,4 +63,4 @@ runs: uses: actions/attest-sbom@v3 with: subject-path: ${{ inputs.subject-path }} - sbom-path: ${{ steps.paths.outputs.sbom-path }} + sbom-path: ${{ steps.check.outputs.sbom-path }} diff --git a/.github/actions/aws-upload/action.yml b/.github/actions/aws-upload/action.yml index 6aa7fa8de200..a6d2c33b6aa0 100644 --- a/.github/actions/aws-upload/action.yml +++ b/.github/actions/aws-upload/action.yml @@ -2,29 +2,29 @@ name: AWS Upload description: Upload release artifacts to AWS S3 and invalidate CloudFront cache inputs: - artifact_name: + artifact-name: description: Artifact filename to upload required: true - artifact_path: + artifact-path: description: Path to artifact file required: true - aws_key_id: + aws-key-id: description: AWS access key ID required: false - aws_secret_access_key: + aws-secret-access-key: description: AWS secret access key required: false - aws_role_arn: + aws-role-arn: description: AWS IAM role ARN for OIDC authentication (preferred over static credentials) required: false - aws_distribution_id: + aws-distribution-id: description: AWS CloudFront distribution ID (optional, for cache invalidation) required: false - aws_region: + aws-region: description: AWS region required: false default: 'us-west-2' - s3_bucket: + s3-bucket: description: S3 bucket name required: false default: 'qgroundcontrol' @@ -32,78 +32,62 @@ inputs: runs: using: composite steps: - - name: Validate credentials + - name: Validate credentials and artifact shell: bash env: - AWS_ROLE_ARN: ${{ inputs.aws_role_arn }} - AWS_KEY_ID: ${{ inputs.aws_key_id }} - AWS_SECRET_KEY: ${{ inputs.aws_secret_access_key }} + AWS_ROLE_ARN: ${{ inputs.aws-role-arn }} + AWS_KEY_ID: ${{ inputs.aws-key-id }} + AWS_SECRET_KEY: ${{ inputs.aws-secret-access-key }} run: | - if [[ -z "$AWS_ROLE_ARN" && ( -z "$AWS_KEY_ID" || -z "$AWS_SECRET_KEY" ) ]]; then - echo "::error::Either aws_role_arn (OIDC) or both aws_key_id and aws_secret_access_key (static credentials) must be provided" - exit 1 - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/aws_upload.py" validate \ + --role-arn "${AWS_ROLE_ARN:-}" \ + --key-id "${AWS_KEY_ID:-}" \ + --secret-key "${AWS_SECRET_KEY:-}" \ + --artifact-path "${{ inputs.artifact-path }}" \ + --artifact-name "${{ inputs.artifact-name }}" - name: Configure AWS Credentials (OIDC) - if: inputs.aws_role_arn != '' - uses: aws-actions/configure-aws-credentials@v4 + if: inputs.aws-role-arn != '' + uses: aws-actions/configure-aws-credentials@v6 with: - role-to-assume: ${{ inputs.aws_role_arn }} - aws-region: ${{ inputs.aws_region }} + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: ${{ inputs.aws-region }} - name: Configure AWS Credentials (Static) - if: inputs.aws_role_arn == '' && inputs.aws_key_id != '' - uses: aws-actions/configure-aws-credentials@v4 + if: inputs.aws-role-arn == '' && inputs.aws-key-id != '' + uses: aws-actions/configure-aws-credentials@v6 with: - aws-access-key-id: ${{ inputs.aws_key_id }} - aws-secret-access-key: ${{ inputs.aws_secret_access_key }} - aws-region: ${{ inputs.aws_region }} + aws-access-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: ${{ inputs.aws-region }} - name: Upload to S3 builds folder shell: bash - env: - ARTIFACT_PATH: ${{ inputs.artifact_path }} - S3_BUCKET: ${{ inputs.s3_bucket }} - REF_NAME: ${{ github.ref_name }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} run: | - # Validate artifact path exists and is a file - if [[ ! -f "$ARTIFACT_PATH" ]]; then - echo "Error: Artifact not found: $ARTIFACT_PATH" >&2 - exit 1 - fi - - # Validate artifact name contains only safe characters (no path separators) - if [[ "$ARTIFACT_NAME" =~ [/\\] ]] || [[ "$ARTIFACT_NAME" =~ \.\. ]]; then - echo "Error: Invalid artifact name (contains path separators): $ARTIFACT_NAME" >&2 - exit 1 - fi - - # Sanitize ref name - remove path traversal attempts and special chars - SAFE_REF_NAME=$(echo "$REF_NAME" | sed 's/\.\.//g' | sed 's/[^a-zA-Z0-9._-]/_/g') - # Intentional: public-read for distributing release builds to users. - aws s3 cp "$ARTIFACT_PATH" \ - "s3://${S3_BUCKET}/builds/${SAFE_REF_NAME}/${ARTIFACT_NAME}" \ - --acl public-read + python3 "${GITHUB_WORKSPACE}/.github/scripts/aws_upload.py" upload \ + --artifact-path "${{ inputs.artifact-path }}" \ + --artifact-name "${{ inputs.artifact-name }}" \ + --ref-name "${{ github.ref_name }}" \ + --s3-bucket "${{ inputs.s3-bucket }}" - name: Upload to S3 latest folder if: github.ref_type == 'tag' shell: bash env: - ARTIFACT_PATH: ${{ inputs.artifact_path }} - S3_BUCKET: ${{ inputs.s3_bucket }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} + ARTIFACT_PATH: ${{ inputs.artifact-path }} + S3_BUCKET: ${{ inputs.s3-bucket }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} run: | aws s3 cp "$ARTIFACT_PATH" \ "s3://${S3_BUCKET}/latest/${ARTIFACT_NAME}" \ --acl public-read - name: Invalidate CloudFront cache - if: github.ref_type == 'tag' && inputs.aws_distribution_id != '' + if: github.ref_type == 'tag' && inputs.aws-distribution-id != '' shell: bash env: - DISTRIBUTION_ID: ${{ inputs.aws_distribution_id }} - ARTIFACT_NAME: ${{ inputs.artifact_name }} + DISTRIBUTION_ID: ${{ inputs.aws-distribution-id }} + ARTIFACT_NAME: ${{ inputs.artifact-name }} run: | aws cloudfront create-invalidation \ --distribution-id "$DISTRIBUTION_ID" \ diff --git a/.github/actions/benchmark-runner/action.yml b/.github/actions/benchmark-runner/action.yml index 195659e38903..3afa6b55d995 100644 --- a/.github/actions/benchmark-runner/action.yml +++ b/.github/actions/benchmark-runner/action.yml @@ -26,16 +26,7 @@ runs: INPUT_OUTPUT_FILE: ${{ inputs.output-file }} INPUT_QT_PLATFORM: ${{ inputs.qt-platform }} run: | - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - - "${py_cmd}" "${GITHUB_WORKSPACE}/.github/scripts/benchmark_runner.py" \ + python3 "${GITHUB_WORKSPACE}/.github/scripts/benchmark_runner.py" \ --binary "$INPUT_BINARY_PATH" \ --filter "$INPUT_TEST_FILTER" \ --output "$INPUT_OUTPUT_FILE" \ diff --git a/.github/actions/build-config/action.yml b/.github/actions/build-config/action.yml index ecba81a47b8d..4fc098481acb 100644 --- a/.github/actions/build-config/action.yml +++ b/.github/actions/build-config/action.yml @@ -23,6 +23,9 @@ outputs: gstreamer_windows_version: description: GStreamer version (Windows) value: ${{ steps.read.outputs.gstreamer_windows_version }} + ndk_version: + description: Android NDK version (short form) + value: ${{ steps.read.outputs.ndk_version }} ndk_full_version: description: Android NDK version (full form for sdkmanager) value: ${{ steps.read.outputs.ndk_full_version }} @@ -50,6 +53,9 @@ outputs: ios_deployment_target: description: iOS deployment target version value: ${{ steps.read.outputs.ios_deployment_target }} + platform_workflows: + description: Comma-separated platform workflow names + value: ${{ steps.read.outputs.platform_workflows }} runs: using: composite diff --git a/.github/actions/build-setup/action.yml b/.github/actions/build-setup/action.yml index 70b310a61ecf..2d5299b2175d 100644 --- a/.github/actions/build-setup/action.yml +++ b/.github/actions/build-setup/action.yml @@ -25,14 +25,23 @@ inputs: required: false default: "Release" save-cache: - description: Whether to save cache (typically false for PRs) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: "false" + default: "auto" outputs: gstreamer_windows_version: description: GStreamer version for Windows value: ${{ steps.config.outputs.gstreamer_windows_version }} + qt_modules: + description: Qt modules used for this build + value: ${{ inputs.qt-modules || steps.config.outputs.qt_modules }} + qt_root_dir: + description: Installed Qt root directory + value: ${{ steps.qt.outputs.qt_root_dir }} + qt_version: + description: Resolved Qt version for this build + value: ${{ inputs.qt-version || steps.config.outputs.qt_version }} xcode_version: description: Xcode version for macOS value: ${{ steps.config.outputs.xcode_version }} @@ -61,6 +70,7 @@ runs: save-cache: ${{ inputs.save-cache }} - name: Install Qt + id: qt uses: ./.github/actions/qt-install with: version: ${{ inputs.qt-version || steps.config.outputs.qt_version }} diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index 2bc911cf30a3..7fbd95ce13f2 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -14,12 +14,28 @@ inputs: description: Path to CPM Modules required: false save-cache: - description: Save cache (set to false for PR builds to save storage) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: 'true' + default: 'auto' runs: using: composite steps: + - name: Resolve save-cache + id: cache-policy + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + PR_REPO: ${{ github.event.pull_request.head.repo.full_name }} + THIS_REPO: ${{ github.repository }} + run: | + save="$(python3 -c " + import os, sys + sys.path.insert(0, os.path.join(os.environ['GITHUB_WORKSPACE'], 'tools')) + from common.gh_actions import resolve_cache_policy + print(resolve_cache_policy('${{ inputs.save-cache }}')) + ")" + echo "save=${save}" >> "$GITHUB_OUTPUT" + - name: Determine cache scope id: cache-scope shell: bash @@ -27,65 +43,15 @@ runs: EVENT_NAME: ${{ github.event_name }} REF_NAME: ${{ github.ref_name }} PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - scope="shared" - case "${EVENT_NAME}" in - pull_request) - scope="pr-${PR_NUMBER:-unknown}" - ;; - workflow_dispatch) - scope="manual-${REF_NAME}" - ;; - push) - if [[ "${REF_NAME}" != "master" ]]; then - scope="branch-${REF_NAME}" - fi - ;; - *) - scope="${EVENT_NAME}-${REF_NAME}" - ;; - esac - scope="${scope//\//-}" - echo "scope=${scope}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" scope --event-name "${EVENT_NAME}" --ref-name "${REF_NAME}" --pr-number "${PR_NUMBER:-}" - name: Get ccache config id: ccache-config shell: bash - env: - INPUT_TARGET: ${{ inputs.target }} - WORKSPACE: ${{ github.workspace }} run: | - # Requires setup-python from common/action.yml - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - - # Pin ccache version for reproducibility and to avoid API calls - # Update periodically via Dependabot or manually - CCACHE_VERSION="4.12.3" - - # Validate version format (defense in depth) - if [[ ! "$CCACHE_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then - echo "Error: Invalid ccache version format: $CCACHE_VERSION" >&2 - exit 1 - fi - - if [[ "${INPUT_TARGET}" == "linux_gcc_arm64" ]]; then - arch="aarch64" - else - arch="x86_64" - fi - - "${PY_CMD}" "${WORKSPACE}/.github/scripts/install_ccache.py" \ - --version "${CCACHE_VERSION}" \ - --arch "${arch}" \ - --config "${WORKSPACE}/tools/configs/ccache.conf" \ - --output-only + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" config \ + --target "${{ inputs.target }}" \ + --conf "${GITHUB_WORKSPACE}/tools/configs/ccache.conf" - name: Cache ccache binary (Linux) if: runner.os == 'Linux' @@ -98,84 +64,59 @@ runs: - name: Install ccache (Linux) if: runner.os == 'Linux' && steps.ccache-binary.outputs.cache-hit != 'true' shell: bash - env: - WORKSPACE: ${{ github.workspace }} - CCACHE_VER: ${{ steps.ccache-config.outputs.version }} - CCACHE_ARCH: ${{ steps.ccache-config.outputs.arch }} run: | - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - "${PY_CMD}" "${WORKSPACE}/.github/scripts/install_ccache.py" \ - --version "${CCACHE_VER}" \ - --arch "${CCACHE_ARCH}" \ - --install + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" install \ + --version "${{ steps.ccache-config.outputs.version }}" \ + --arch "${{ steps.ccache-config.outputs.arch }}" - - name: Setup sccache (Windows x64) - if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' - uses: mozilla-actions/sccache-action@v0.0.9 + - name: Install ccache (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + if ! command -v ccache >/dev/null 2>&1; then + brew install ccache + fi + ccache --version - - name: Enable sccache GHA cache backend - if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' + - name: Determine Windows ccache arch + if: runner.os == 'Windows' + id: win-ccache shell: bash - run: echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" windows-config --host "${{ inputs.host }}" --target "${{ inputs.target }}" - - name: Cache ccache binary (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' - id: ccache-binary-win-arm64 + - name: Cache ccache binary (Windows) + if: runner.os == 'Windows' + id: ccache-binary-win uses: actions/cache@v5 with: - path: ${{ runner.temp }}/ccache-${{ steps.ccache-config.outputs.version }}-windows-aarch64/ccache.exe - key: ccache-binary-${{ steps.ccache-config.outputs.version }}-windows-aarch64 + path: ${{ runner.temp }}/ccache-${{ steps.ccache-config.outputs.version }}-windows-${{ steps.win-ccache.outputs.arch }}/ccache.exe + key: ccache-binary-${{ steps.ccache-config.outputs.version }}-windows-${{ steps.win-ccache.outputs.arch }} - - name: Install ccache (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' && steps.ccache-binary-win-arm64.outputs.cache-hit != 'true' + - name: Install ccache (Windows) + if: runner.os == 'Windows' && steps.ccache-binary-win.outputs.cache-hit != 'true' shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} + CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} + CCACHE_WIN_SHA256: ${{ steps.win-ccache.outputs.sha256 }} run: | - set -euo pipefail - ccache_url="https://github.com/ccache/ccache/releases/download/v${CCACHE_VERSION}/ccache-${CCACHE_VERSION}-windows-aarch64.zip" - temp_dir="$(mktemp -d)" - temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-aarch64" - - curl --fail --location --silent --show-error "$ccache_url" -o "${temp_dir}/ccache.zip" - unzip -q "${temp_dir}/ccache.zip" -d "$temp_dir" - mkdir -p "$install_dir" - cp "${temp_dir}/ccache-${CCACHE_VERSION}-windows-aarch64/ccache.exe" "${install_dir}/ccache.exe" + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" install-windows \ + --version "${CCACHE_VERSION}" \ + --arch "${CCACHE_ARCH}" \ + --sha256 "${CCACHE_WIN_SHA256}" \ + --runner-temp "${RUNNER_TEMP//\\//}" - - name: Add ccache to PATH (Windows ARM64) - if: runner.os == 'Windows' && inputs.host == 'windows_arm64' + - name: Add ccache to PATH (Windows) + if: runner.os == 'Windows' shell: bash env: CCACHE_VERSION: ${{ steps.ccache-config.outputs.version }} - run: | - set -euo pipefail - temp_root="${RUNNER_TEMP//\\//}" - install_dir="${temp_root}/ccache-${CCACHE_VERSION}-windows-aarch64" - if [ ! -f "${install_dir}/ccache.exe" ]; then - echo "Error: ccache.exe not found at ${install_dir}/ccache.exe" >&2 - exit 1 - fi - - echo "$install_dir" >> "$GITHUB_PATH" - "${install_dir}/ccache.exe" --version + CCACHE_ARCH: ${{ steps.win-ccache.outputs.arch }} + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" add-windows-path --version "${CCACHE_VERSION}" --arch "${CCACHE_ARCH}" --runner-temp "${RUNNER_TEMP//\\//}" - - name: Configure ccache environment (non-Windows) - if: runner.os != 'Windows' + - name: Configure ccache environment shell: bash - run: | - workspace_path="${GITHUB_WORKSPACE//\\//}" - echo "CCACHE_DIR=${workspace_path}/.ccache" >> "$GITHUB_ENV" - echo "CCACHE_BASEDIR=${workspace_path}" >> "$GITHUB_ENV" - echo "CCACHE_CONFIGPATH=${workspace_path}/tools/configs/ccache.conf" >> "$GITHUB_ENV" - mkdir -p "${workspace_path}/.ccache" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" configure-env --workspace "${GITHUB_WORKSPACE//\\//}" - name: Configure CPM source cache if: inputs.cpm-modules != '' @@ -183,96 +124,42 @@ runs: shell: bash env: CPM_MODULES_INPUT: ${{ inputs.cpm-modules }} - run: | - cpm_cache_path="${CPM_MODULES_INPUT//\\//}" - mkdir -p "${cpm_cache_path}" - echo "CPM_SOURCE_CACHE=${cpm_cache_path}" >> "$GITHUB_ENV" - echo "path=${cpm_cache_path}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" configure-cpm-cache --path "${CPM_MODULES_INPUT}" - name: Compute CPM dependency fingerprint if: inputs.cpm-modules != '' id: cpm-key shell: bash - run: | - set -euo pipefail - if command -v python3 >/dev/null 2>&1; then - PY_CMD="python3" - elif command -v python >/dev/null 2>&1; then - PY_CMD="python" - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - fingerprint="$("${PY_CMD}" - <<'PY' - import hashlib - import re - from pathlib import Path - - root = Path(".") - declaration_re = re.compile(r"CPM(Add|Find)Package|FetchContent_Declare") + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" fingerprint --root "${GITHUB_WORKSPACE}" - candidates: list[Path] = [] - for exact in [Path("CMakeLists.txt"), Path("cmake/modules/CPM.cmake"), Path(".github/build-config.json")]: - if exact.exists(): - candidates.append(exact) - - if (root / "cmake").exists(): - candidates.extend((root / "cmake").rglob("*.cmake")) - if (root / "src").exists(): - candidates.extend((root / "src").rglob("CMakeLists.txt")) - if (root / "test").exists(): - candidates.extend((root / "test").rglob("CMakeLists.txt")) - - dep_files: list[Path] = [] - for path in candidates: - if not path.is_file(): - continue - if path.name == "build-config.json": - dep_files.append(path) - continue - try: - text = path.read_text(encoding="utf-8", errors="ignore") - except OSError: - continue - if declaration_re.search(text): - dep_files.append(path) - - paths = sorted({str(path.as_posix()) for path in dep_files}) - h = hashlib.sha256() - for rel in paths: - path = Path(rel) - try: - content = path.read_bytes() - except OSError: - continue - h.update(rel.encode("utf-8")) - h.update(b"\0") - h.update(content) - h.update(b"\0") - - print(h.hexdigest()) - PY - )" - echo "fingerprint=${fingerprint}" >> "$GITHUB_OUTPUT" + - name: Restore Build Cache (ccache, save) + if: steps.cache-policy.outputs.save == 'true' + uses: actions/cache@v5 + with: + path: ${{ github.workspace }}/.ccache + key: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }}-${{ github.run_id }} + restore-keys: | + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - - name: Setup Build Cache (ccache) - if: "!(runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64')" - uses: hendrikmuhs/ccache-action@v1.2 + - name: Restore Build Cache (ccache, read-only) + if: steps.cache-policy.outputs.save != 'true' + uses: actions/cache/restore@v5 with: - key: ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }} + path: ${{ github.workspace }}/.ccache + key: ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}-${{ hashFiles('.github/build-config.json', 'tools/configs/ccache.conf') }}-${{ github.run_id }} restore-keys: | - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- - ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- - ${{ inputs.host }}-${{ inputs.target }}- - ${{ inputs.host }}- - max-size: ${{ steps.ccache-config.outputs.max_size }} - verbose: 2 - evict-old-files: job - save: ${{ inputs.save-cache == 'true' }} + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-${{ steps.cache-scope.outputs.scope }}- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}-shared- + ccache-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }}- + + - name: Initialize ccache stats + shell: bash + run: ccache -z - name: Cache CPM Modules (restore and save) - if: inputs.cpm-modules != '' && inputs.save-cache == 'true' + if: inputs.cpm-modules != '' && steps.cache-policy.outputs.save == 'true' uses: actions/cache@v5 with: path: ${{ steps.cpm-cache.outputs.path }} @@ -283,7 +170,7 @@ runs: cpm-modules- - name: Cache CPM Modules (restore only) - if: inputs.cpm-modules != '' && inputs.save-cache != 'true' + if: inputs.cpm-modules != '' && steps.cache-policy.outputs.save != 'true' uses: actions/cache/restore@v5 with: path: ${{ steps.cpm-cache.outputs.path }} diff --git a/.github/actions/cmake-build/action.yml b/.github/actions/cmake-build/action.yml index cdd66d14f3a5..5f05d3490aac 100644 --- a/.github/actions/cmake-build/action.yml +++ b/.github/actions/cmake-build/action.yml @@ -54,60 +54,15 @@ runs: CONTINUE: ${{ inputs.continue-on-error }} REVIEWDOG: ${{ inputs.reviewdog }} run: | - set -euo pipefail - - # Build command - cmd=(cmake --build .) - - # Add target - [[ -n "$TARGET" ]] && cmd+=(--target "$TARGET") - - # Add config for multi-config generators - [[ -n "$BUILD_TYPE" ]] && cmd+=(--config "$BUILD_TYPE") - - # Add parallel flag - if [[ "$PARALLEL" == "true" ]]; then - if [[ -n "$PARALLEL_JOBS" ]]; then - if [[ ! "$PARALLEL_JOBS" =~ ^[1-9][0-9]*$ ]]; then - echo "::error::parallel-jobs must be a positive integer, got '$PARALLEL_JOBS'" - exit 1 - fi - cmd+=(--parallel "$PARALLEL_JOBS") - else - cmd+=(--parallel) - fi - fi - - # When reviewdog is enabled, force output capture for warning analysis - if [[ "$REVIEWDOG" == "true" && -z "$OUTPUT_FILE" ]]; then - OUTPUT_FILE="${RUNNER_TEMP:-.}/build-output.log" - echo "REVIEWDOG_LOG=${OUTPUT_FILE}" >> "$GITHUB_ENV" - elif [[ "$REVIEWDOG" == "true" ]]; then - echo "REVIEWDOG_LOG=${OUTPUT_FILE}" >> "$GITHUB_ENV" - fi - - echo "Running: ${cmd[*]}" - start_time=$(date +%s) - - set +e - if [[ -n "$OUTPUT_FILE" ]]; then - "${cmd[@]}" 2>&1 | tee "$OUTPUT_FILE" - exit_code=${PIPESTATUS[0]} - else - "${cmd[@]}" - exit_code=$? - fi - set -e - - end_time=$(date +%s) - duration=$((end_time - start_time)) - - if [[ $exit_code -eq 0 ]]; then - echo "✓ Build completed in ${duration}s" - else - echo "::error::Build failed after ${duration}s" - [[ "$CONTINUE" != "true" ]] && exit $exit_code - fi + ARGS=(build) + [[ -n "$TARGET" ]] && ARGS+=(--target "$TARGET") + [[ -n "$BUILD_TYPE" ]] && ARGS+=(--build-type "$BUILD_TYPE") + [[ "$PARALLEL" == "true" ]] && ARGS+=(--parallel) + [[ -n "$PARALLEL_JOBS" ]] && ARGS+=(--parallel-jobs "$PARALLEL_JOBS") + [[ -n "$OUTPUT_FILE" ]] && ARGS+=(--output-file "$OUTPUT_FILE") + [[ "$CONTINUE" == "true" ]] && ARGS+=(--continue-on-error) + [[ "$REVIEWDOG" == "true" ]] && ARGS+=(--reviewdog) + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" "${ARGS[@]}" - name: Setup reviewdog if: inputs.reviewdog == 'true' && env.REVIEWDOG_LOG != '' @@ -132,11 +87,6 @@ runs: shell: bash run: | if command -v ccache >/dev/null 2>&1; then - echo "::group::ccache statistics" - ccache -s - echo "::endgroup::" - elif command -v sccache >/dev/null 2>&1; then - echo "::group::sccache statistics" - sccache --show-stats - echo "::endgroup::" + ccache -c + python3 "${GITHUB_WORKSPACE}/.github/scripts/ccache_helper.py" summary fi diff --git a/.github/actions/cmake-configure/action.yml b/.github/actions/cmake-configure/action.yml index 7aaa9b156dbd..27daf1755063 100644 --- a/.github/actions/cmake-configure/action.yml +++ b/.github/actions/cmake-configure/action.yml @@ -58,49 +58,19 @@ runs: - name: Configure shell: bash - env: - INPUT_SOURCE_DIR: ${{ inputs.source-dir }} - INPUT_BUILD_DIR: ${{ inputs.build-dir }} - INPUT_GENERATOR: ${{ inputs.generator }} - INPUT_BUILD_TYPE: ${{ inputs.build-type }} - INPUT_TESTING: ${{ inputs.testing }} - INPUT_COVERAGE: ${{ inputs.coverage }} - INPUT_STABLE: ${{ inputs.stable }} - INPUT_USE_QT_CMAKE: ${{ inputs.use-qt-cmake }} - INPUT_UNITY_BUILD: ${{ inputs.unity-build }} - INPUT_UNITY_BATCH_SIZE: ${{ inputs.unity-batch-size }} - INPUT_EXTRA_ARGS: ${{ inputs.extra-args }} - WORKSPACE: ${{ github.workspace }} run: | - args=( - -S "${INPUT_SOURCE_DIR}" - -B "${INPUT_BUILD_DIR}" - -G "${INPUT_GENERATOR}" - -t "${INPUT_BUILD_TYPE}" + ARGS=(configure + --source-dir "${{ inputs.source-dir }}" + --build-dir "${{ inputs.build-dir }}" + --generator "${{ inputs.generator }}" + --build-type "${{ inputs.build-type }}" ) - - [[ "${INPUT_TESTING}" == "true" ]] && args+=(--testing) - [[ "${INPUT_COVERAGE}" == "true" ]] && args+=(--coverage) - [[ "${INPUT_STABLE}" == "true" ]] && args+=(--stable) - [[ "${INPUT_USE_QT_CMAKE}" != "true" ]] && args+=(--no-qt-cmake) - - if [[ "${INPUT_UNITY_BUILD}" == "true" ]]; then - args+=(--unity --unity-batch "${INPUT_UNITY_BATCH_SIZE}") - fi - - # Add extra args if provided (after -- so argparse treats -D flags as positional) - if [[ -n "$INPUT_EXTRA_ARGS" ]]; then - read -ra extra <<< "$INPUT_EXTRA_ARGS" - args+=(--) - args+=("${extra[@]}") - fi - - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 + [[ "${{ inputs.testing }}" == "true" ]] && ARGS+=(--testing) + [[ "${{ inputs.coverage }}" == "true" ]] && ARGS+=(--coverage) + [[ "${{ inputs.stable }}" == "true" ]] && ARGS+=(--stable) + [[ "${{ inputs.use-qt-cmake }}" != "true" ]] && ARGS+=(--no-qt-cmake) + if [[ "${{ inputs.unity-build }}" == "true" ]]; then + ARGS+=(--unity-build --unity-batch-size "${{ inputs.unity-batch-size }}") fi - "${py_cmd}" "${WORKSPACE}/tools/configure.py" "${args[@]}" + [[ -n "${{ inputs.extra-args }}" ]] && ARGS+=("--extra-args=${{ inputs.extra-args }}") + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" "${ARGS[@]}" diff --git a/.github/actions/collect-artifact-sizes/action.yml b/.github/actions/collect-artifact-sizes/action.yml index 6f0e6882247f..761e977432b9 100644 --- a/.github/actions/collect-artifact-sizes/action.yml +++ b/.github/actions/collect-artifact-sizes/action.yml @@ -9,6 +9,10 @@ inputs: description: Comma-separated workflow names to collect artifact sizes for required: false default: Linux,Windows,MacOS,Android + event: + description: Optional workflow event name to filter runs by + required: false + default: '' output-file: description: Output JSON file path required: true @@ -34,15 +38,26 @@ runs: REPO: ${{ github.repository }} HEAD_SHA: ${{ inputs.head-sha }} WORKFLOWS: ${{ inputs.workflows }} + EVENT: ${{ inputs.event }} OUTPUT_FILE: ${{ inputs.output-file }} RUNS_FILE: ${{ inputs.runs-file }} ARTIFACTS_FILE: ${{ inputs.artifacts-file }} run: | - ARGS=(--repo "${REPO}" --head-sha "${HEAD_SHA}" --platform-workflows "${WORKFLOWS}" --output-file "${OUTPUT_FILE}") - if [[ -n "${RUNS_FILE}" && -f "${RUNS_FILE}" ]]; then - ARGS+=(--runs-file "${RUNS_FILE}") - fi - if [[ -n "${ARTIFACTS_FILE}" && -f "${ARTIFACTS_FILE}" ]]; then - ARGS+=(--artifacts-file "${ARTIFACTS_FILE}") - fi - python3 "${GITHUB_WORKSPACE}/.github/scripts/collect_artifact_sizes.py" "${ARGS[@]}" + python3 -c " + import os, subprocess, sys + args = ['${GITHUB_WORKSPACE}/.github/scripts/collect_artifact_sizes.py', + '--repo', os.environ['REPO'], + '--head-sha', os.environ['HEAD_SHA'], + '--platform-workflows', os.environ['WORKFLOWS'], + '--output-file', os.environ['OUTPUT_FILE']] + event = os.environ.get('EVENT', '') + if event: + args += ['--event', event] + runs = os.environ.get('RUNS_FILE', '') + if runs and os.path.isfile(runs): + args += ['--runs-file', runs] + artifacts = os.environ.get('ARTIFACTS_FILE', '') + if artifacts and os.path.isfile(artifacts): + args += ['--artifacts-file', artifacts] + sys.exit(subprocess.run([sys.executable] + args).returncode) + " diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml index 9b2e08a62774..b08732e7b106 100644 --- a/.github/actions/common/action.yml +++ b/.github/actions/common/action.yml @@ -15,11 +15,12 @@ runs: remove_packages_one_command: true rm_cmd: rmz - - uses: lukka/get-cmake@v4.2.1 + - uses: lukka/get-cmake@v4.2.3 - - uses: actions/setup-python@v6 + - uses: ./.github/actions/setup-python with: python-version: '3.12' + groups: scripts - name: Create build directory shell: bash diff --git a/.github/actions/coverage/action.yml b/.github/actions/coverage/action.yml index 20ab363c377b..098a69572a97 100644 --- a/.github/actions/coverage/action.yml +++ b/.github/actions/coverage/action.yml @@ -42,41 +42,13 @@ runs: using: composite steps: - name: Generate Coverage Report - working-directory: ${{ inputs.build-dir }} shell: bash - env: - INPUT_MODE: ${{ inputs.mode }} run: | - case "${INPUT_MODE}" in - full) target="coverage" ;; - report-only) target="coverage-report" ;; - *) - echo "::error::Invalid coverage mode '${INPUT_MODE}'. Use 'full' or 'report-only'." - exit 1 - ;; - esac - - cmake --build . --target "${target}" 2>&1 | tee coverage-output.txt - - # Extract coverage summary for GitHub Step Summary - { - echo "## 📊 Code Coverage" - echo "" - echo "Mode: \`${INPUT_MODE}\`" - echo "" - if grep -q "lines:" coverage-output.txt; then - LINE_COV=$(grep "lines:" coverage-output.txt | tail -1) - BRANCH_COV=$(grep "branches:" coverage-output.txt | tail -1 || echo "branches: N/A") - echo "| Metric | Coverage |" - echo "|--------|----------|" - echo "| Lines | ${LINE_COV#*: } |" - echo "| Branches | ${BRANCH_COV#*: } |" - else - echo "Coverage data not available" - fi - echo "" - echo "[View detailed report](coverage.html)" - } >> "$GITHUB_STEP_SUMMARY" + python3 "${GITHUB_WORKSPACE}/tools/coverage.py" \ + --mode "${{ inputs.mode }}" \ + --build-dir "${{ inputs.build-dir }}" \ + --log-file "${{ inputs.build-dir }}/coverage-output.txt" \ + --step-summary - name: Upload to Codecov uses: codecov/codecov-action@v5 diff --git a/.github/actions/deploy-docs/action.yml b/.github/actions/deploy-docs/action.yml new file mode 100644 index 000000000000..4611228167e6 --- /dev/null +++ b/.github/actions/deploy-docs/action.yml @@ -0,0 +1,50 @@ +name: Deploy Docs +description: Deploy built documentation to an external GitHub Pages repository + +inputs: + artifact-name: + description: Name of the uploaded artifact to download + required: true + target-repo: + description: 'Target repository in owner/repo format' + required: true + target-branch: + description: 'Branch to push to in target repo' + required: false + default: main + deploy-token: + description: 'GitHub token with push access to target repo' + required: true + source-branch: + description: 'Source branch name (used as subdirectory in target repo)' + required: true + commit-message: + description: 'Commit message prefix' + required: false + default: 'Docs update' + +runs: + using: composite + steps: + - name: Download artifact + uses: actions/download-artifact@v8 + with: + name: ${{ inputs.artifact-name }} + path: ~/_docs_build + + - name: Checkout target repo + uses: actions/checkout@v6 + with: + repository: ${{ inputs.target-repo }} + token: ${{ inputs.deploy-token }} + path: _deploy_target + + - name: Deploy docs + shell: bash + run: | + python3 "${GITHUB_WORKSPACE}/.github/scripts/deploy_docs.py" \ + --source-dir ~/_docs_build \ + --target-dir _deploy_target \ + --branch "${{ inputs.source-branch }}" \ + --target-branch "${{ inputs.target-branch }}" \ + --commit-message "${{ inputs.commit-message }}" diff --git a/.github/actions/detect-changes/action.yml b/.github/actions/detect-changes/action.yml index 11a67d8994c1..a88ba8788d47 100644 --- a/.github/actions/detect-changes/action.yml +++ b/.github/actions/detect-changes/action.yml @@ -3,7 +3,7 @@ description: Detect source, test, and CI changes for a platform inputs: platform: - description: "Platform (linux, windows, macos, android, ios)" + description: "Platform (linux, windows, macos, android, ios, docker-linux, docker-android)" required: true outputs: @@ -24,99 +24,4 @@ runs: PUSH_BEFORE_SHA: ${{ github.event.before || '' }} MERGE_BASE_SHA: ${{ github.event.merge_group.base_sha || '' }} CURRENT_SHA: ${{ github.sha }} - INPUT_PLATFORM: ${{ inputs.platform }} - run: | - set -euo pipefail - - platform="${INPUT_PLATFORM}" - changed="" - - # Ensure a specific commit object exists locally. With shallow checkouts - # we fetch only the SHAs needed for diff calculation. - ensure_commit() { - local sha="$1" - [[ -z "${sha}" ]] && return 0 - if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then - git fetch --no-tags --depth=1 origin "${sha}" >/dev/null 2>&1 || true - fi - git cat-file -e "${sha}^{commit}" 2>/dev/null - } - - force_all_true() { - echo "::warning::Unable to compute changed files reliably; forcing build for safety." - echo "any=true" >> "${GITHUB_OUTPUT}" - exit 0 - } - - if [[ "${EVENT_NAME}" == "pull_request" ]]; then - base_sha="${PR_BASE_SHA}" - head_sha="${PR_HEAD_SHA}" - - ensure_commit "${base_sha}" || force_all_true - ensure_commit "${head_sha}" || force_all_true - - changed="$(git diff --name-only "${base_sha}" "${head_sha}" 2>/dev/null)" || force_all_true - elif [[ "${EVENT_NAME}" == "push" ]]; then - before_sha="${PUSH_BEFORE_SHA}" - if [[ -n "${before_sha}" && "${before_sha}" != "0000000000000000000000000000000000000000" ]]; then - ensure_commit "${before_sha}" || force_all_true - ensure_commit "${CURRENT_SHA}" || force_all_true - changed="$(git diff --name-only "${before_sha}" "${CURRENT_SHA}" 2>/dev/null)" || force_all_true - else - changed="$(git diff-tree --no-commit-id --name-only -r "${CURRENT_SHA}")" - fi - elif [[ "${EVENT_NAME}" == "merge_group" && -n "${MERGE_BASE_SHA}" ]]; then - ensure_commit "${MERGE_BASE_SHA}" || force_all_true - ensure_commit "${CURRENT_SHA}" || force_all_true - changed="$(git diff --name-only "${MERGE_BASE_SHA}" "${CURRENT_SHA}" 2>/dev/null)" || force_all_true - else - changed="$(git diff-tree --no-commit-id --name-only -r "${CURRENT_SHA}")" - fi - - echo "Changed files:" - echo "${changed}" - - relevant_patterns=( - "^src/" - "^CMakeLists.txt$" - "^cmake/" - "^libs/" - "^translations/" - "^deploy/${platform}/" - "^test/" - "^\\.github/workflows/${platform}\\.yml$" - "^\\.github/actions/" - "^\\.github/scripts/" - "^\\.github/build-config\\.json$" - ) - - case "${platform}" in - android) relevant_patterns+=("^android/") ;; - esac - - case "${platform}" in - linux) relevant_patterns+=("^tools/setup/.*debian" "^tools/setup/install_dependencies\\.py$") ;; - windows) relevant_patterns+=("^tools/setup/.*windows") ;; - macos) relevant_patterns+=("^tools/setup/.*macos") ;; - android) relevant_patterns+=("^tools/setup/") ;; - ios) relevant_patterns+=("^tools/setup/.*ios" "^tools/setup/.*macos") ;; - esac - - any_changed="false" - - while IFS= read -r file; do - [[ -z "${file}" ]] && continue - - for pattern in "${relevant_patterns[@]}"; do - if [[ "${file}" =~ ${pattern} ]]; then - any_changed="true" - break - fi - done - - [[ "${any_changed}" == "true" ]] && break - done <<< "${changed}" - - echo "any=${any_changed}" >> "${GITHUB_OUTPUT}" - - echo "Results: any=${any_changed}" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/detect_changes.py" --platform "${{ inputs.platform }}" diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index 85afb4b3bdb0..17fdb7ebbda5 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -41,33 +41,10 @@ runs: - name: Validate inputs shell: bash - env: - DOCKERFILE: ${{ inputs.dockerfile }} - BUILD_TYPE: ${{ inputs.build-type }} run: | - # Validate dockerfile - case "$DOCKERFILE" in - Dockerfile-build-ubuntu|Dockerfile-build-android) - echo "Using dockerfile: $DOCKERFILE" - ;; - *) - echo "Error: Invalid dockerfile '$DOCKERFILE'" >&2 - echo "Allowed values: Dockerfile-build-ubuntu, Dockerfile-build-android" >&2 - exit 1 - ;; - esac - - # Validate build-type - case "$BUILD_TYPE" in - Release|Debug) - echo "Build type: $BUILD_TYPE" - ;; - *) - echo "Error: Invalid build-type '$BUILD_TYPE'" >&2 - echo "Allowed values: Release, Debug" >&2 - exit 1 - ;; - esac + python3 "${GITHUB_WORKSPACE}/.github/scripts/docker_helper.py" validate \ + --dockerfile "${{ inputs.dockerfile }}" \ + --build-type "${{ inputs.build-type }}" - name: Build Docker image if: contains(fromJSON('["Dockerfile-build-ubuntu", "Dockerfile-build-android"]'), inputs.dockerfile) @@ -84,24 +61,9 @@ runs: if: contains(fromJSON('["Release", "Debug"]'), inputs.build-type) shell: bash env: - SOURCE_DIR: ${{ github.workspace }} - BUILD_DIR: ${{ github.workspace }}/build - BUILD_TYPE: ${{ inputs.build-type }} ENABLE_FUSE: ${{ inputs.fuse }} run: | chmod +x ./deploy/docker/docker-run.sh - FUSE_FLAG="" - if [[ "$ENABLE_FUSE" == "true" ]]; then - FUSE_FLAG="--fuse" - fi - max_attempts=2 - attempt=1 - until ./deploy/docker/docker-run.sh ${FUSE_FLAG:+"$FUSE_FLAG"} qgc-builder:latest "$BUILD_TYPE"; do - if (( attempt >= max_attempts )); then - echo "Docker build failed after ${attempt} attempt(s)." >&2 - exit 1 - fi - echo "Docker build failed on attempt ${attempt}. Retrying in 30 seconds..." - sleep 30 - attempt=$((attempt + 1)) - done + ARGS=(run --build-type "${{ inputs.build-type }}") + [[ "$ENABLE_FUSE" == "true" ]] && ARGS+=(--fuse) + python3 "${GITHUB_WORKSPACE}/.github/scripts/docker_helper.py" "${ARGS[@]}" diff --git a/.github/actions/download-all-artifacts/action.yml b/.github/actions/download-all-artifacts/action.yml index 9a45306d2141..49303008326e 100644 --- a/.github/actions/download-all-artifacts/action.yml +++ b/.github/actions/download-all-artifacts/action.yml @@ -13,6 +13,10 @@ inputs: description: Comma-separated workflow names to download artifacts from required: false default: Linux,Windows,MacOS,Android + event: + description: Optional workflow event name to filter runs by + required: false + default: '' output-dir: description: Output directory for downloaded artifacts required: false @@ -43,17 +47,28 @@ runs: INPUT_HEAD_SHA: ${{ inputs.head-sha }} INPUT_OUTPUT_DIR: ${{ inputs.output-dir }} INPUT_WORKFLOWS: ${{ inputs.workflows }} + INPUT_EVENT: ${{ inputs.event }} INPUT_ARTIFACT_PREFIXES: ${{ inputs.artifact-prefixes }} INPUT_ARTIFACT_METADATA_FILE: ${{ inputs.artifact-metadata-file }} run: | - ARGS=(--repo "$INPUT_REPO" --head-sha "$INPUT_HEAD_SHA" --output-dir "$INPUT_OUTPUT_DIR" --workflows "$INPUT_WORKFLOWS") - if [[ -n "$RUNS_FILE" && -f "$RUNS_FILE" ]]; then - ARGS+=(--runs-file "$RUNS_FILE") - fi - if [[ -n "$INPUT_ARTIFACT_PREFIXES" ]]; then - ARGS+=(--artifact-prefixes "$INPUT_ARTIFACT_PREFIXES") - fi - if [[ -n "$INPUT_ARTIFACT_METADATA_FILE" ]]; then - ARGS+=(--artifact-metadata-out "$INPUT_ARTIFACT_METADATA_FILE") - fi - python3 "${GITHUB_WORKSPACE}/tools/setup/download_artifacts.py" "${ARGS[@]}" + python3 -c " + import os, subprocess, sys + args = ['${GITHUB_WORKSPACE}/.github/scripts/download_artifacts.py', + '--repo', os.environ['INPUT_REPO'], + '--head-sha', os.environ['INPUT_HEAD_SHA'], + '--output-dir', os.environ['INPUT_OUTPUT_DIR'], + '--workflows', os.environ['INPUT_WORKFLOWS']] + event = os.environ.get('INPUT_EVENT', '') + if event: + args += ['--event', event] + runs = os.environ.get('RUNS_FILE', '') + if runs and os.path.isfile(runs): + args += ['--runs-file', runs] + prefixes = os.environ.get('INPUT_ARTIFACT_PREFIXES', '') + if prefixes: + args += ['--artifact-prefixes', prefixes] + meta = os.environ.get('INPUT_ARTIFACT_METADATA_FILE', '') + if meta: + args += ['--artifact-metadata-out', meta] + sys.exit(subprocess.run([sys.executable] + args).returncode) + " diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index b5a13e44cc2b..cb4eaba5a23d 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -24,11 +24,14 @@ inputs: description: 'Upload to S3' required: false default: 'true' + aws-role-arn: + description: 'AWS IAM role ARN for OIDC authentication (preferred over static credentials)' + required: false aws-access-key-id: - description: 'AWS access key ID' + description: 'AWS access key ID (fallback if OIDC not available)' required: false aws-secret-access-key: - description: 'AWS secret access key' + description: 'AWS secret access key (fallback if OIDC not available)' required: false outputs: @@ -42,35 +45,7 @@ runs: - name: Read build config id: config shell: bash - env: - INPUT_VERSION: ${{ inputs.version }} - INPUT_PLATFORM: ${{ inputs.platform }} - INPUT_ARCH: ${{ inputs.arch }} - run: | - set -euo pipefail - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - echo "py_cmd=$py_cmd" >> "$GITHUB_OUTPUT" - - version="${INPUT_VERSION}" - if [[ -z "$version" ]]; then - case "${INPUT_PLATFORM}" in - macos) version_key="gstreamer_macos_version" ;; - windows) version_key="gstreamer_windows_version" ;; - android) version_key="gstreamer_android_version" ;; - *) version_key="gstreamer_version" ;; - esac - version=$("${py_cmd}" ./tools/setup/read_config.py --get "${version_key}") - fi - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "qt_version=$("${py_cmd}" ./tools/setup/read_config.py --get qt_version)" >> "$GITHUB_OUTPUT" - echo "Building GStreamer $version for ${INPUT_PLATFORM} (${INPUT_ARCH})" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/resolve_gstreamer_config.py" --platform "${{ inputs.platform }}" --version "${{ inputs.version }}" # ===== DEPENDENCIES ===== @@ -282,9 +257,27 @@ runs: Get-ChildItem (Join-Path $prefix 'bin') -Filter '*.exe' | Select-Object Name, Length | Format-Table $libCount = (Get-ChildItem (Join-Path $prefix 'lib') -Filter '*.lib' | Measure-Object).Count Write-Host "$libCount library files found" + $env:PATH = (Join-Path $prefix 'bin') + ";" + $env:PATH + $env:GST_PLUGIN_PATH = Join-Path $prefix 'lib' 'gstreamer-1.0' + & (Join-Path $prefix 'bin' 'gst-launch-1.0.exe') --version # ===== ARCHIVE & UPLOAD ===== + - name: Configure AWS Credentials (OIDC) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + role-to-assume: ${{ inputs.aws-role-arn }} + aws-region: us-west-2 + + - name: Configure AWS Credentials (Static) + if: inputs.upload-s3 == 'true' && inputs.aws-role-arn == '' && inputs.aws-access-key-id != '' + uses: aws-actions/configure-aws-credentials@v6 + with: + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-region: us-west-2 + - name: Create archive id: archive shell: bash @@ -297,8 +290,6 @@ runs: SIMULATOR: ${{ inputs.simulator }} UPLOAD_S3: ${{ inputs.upload-s3 }} WORKSPACE: ${{ github.workspace }} - AWS_ACCESS_KEY_ID: ${{ inputs.aws-access-key-id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws-secret-access-key }} AWS_DEFAULT_REGION: us-west-2 run: | set -euo pipefail @@ -309,12 +300,12 @@ runs: --output-dir "$OUTPUT_DIR" ) [[ "$SIMULATOR" == "true" ]] && args+=(--simulator) - [[ "$UPLOAD_S3" == "true" && -n "$AWS_ACCESS_KEY_ID" ]] && args+=(--upload-s3) + [[ "$UPLOAD_S3" == "true" ]] && args+=(--upload-s3) "${PY_CMD}" "${WORKSPACE}/.github/scripts/gstreamer_archive.py" "${args[@]}" - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: ${{ steps.archive.outputs.name }} path: ${{ steps.archive.outputs.path }} diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index cb49199176a7..b50415b04bbd 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -55,12 +55,7 @@ runs: - name: Enable universe repository (Linux) if: runner.os == 'Linux' shell: bash - run: | - if ! grep -rEq --include='*.list' --include='*.sources' '^(deb|Components:).*universe' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null; then - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 install -y -qq software-properties-common - sudo add-apt-repository -y universe - fi - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 update -y -qq + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" enable-universe - name: Cache and install apt packages (Linux) if: runner.os == 'Linux' @@ -72,71 +67,18 @@ runs: - name: Fix apt alternatives after cache restore (Linux) if: runner.os == 'Linux' shell: bash - run: | - # cache-apt-pkgs-action restores .deb tarballs but may not re-run - # postinst scripts that register update-alternatives entries. - # This leaves symlinks like libblas.so.3 missing even though the - # backing library (e.g. libopenblas) is installed. - if ! sudo dpkg --configure -a; then - echo "::warning::dpkg --configure -a failed; package state may be inconsistent" - fi - - multiarch="$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || true)" - blas_link="" - if [[ -n "${multiarch}" ]]; then - blas_link="/usr/lib/${multiarch}/libblas.so.3" - fi - - # Ensure BLAS SONAME is resolvable before AppImage dependency bundling. - if ! ldconfig -p | grep -qE '\blibblas\.so\.3\b'; then - echo "::warning::libblas.so.3 missing from linker cache; attempting repair" - sudo apt-get -o DPkg::Lock::Timeout=300 -o Acquire::Retries=3 install -y -qq --reinstall libblas3 libopenblas0 || true - if [[ -n "${multiarch}" ]]; then - sudo update-alternatives --auto "libblas.so.3-${multiarch}" || true - fi - - if [[ -n "${blas_link}" && ! -e "${blas_link}" ]]; then - candidate="$(find /usr/lib -type f -name 'libblas.so.3' 2>/dev/null | head -n1 || true)" - if [[ -n "${candidate}" ]]; then - echo "::warning::Creating compatibility symlink ${blas_link} -> ${candidate}" - sudo ln -sf "${candidate}" "${blas_link}" - fi - fi - sudo ldconfig - fi - - if ! ldconfig -p | grep -qE '\blibblas\.so\.3\b'; then - echo "::error::libblas.so.3 is still missing after repair attempt" - exit 1 - fi + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" fix-apt-alternatives - name: Install optional apt packages (Linux) if: runner.os == 'Linux' shell: bash - run: | - optional=$(python3 tools/setup/install_dependencies.py --platform debian --category gstreamer_optional --print-packages) - if [ -n "$optional" ]; then - available=() - for pkg in $optional; do - if apt-cache show "$pkg" >/dev/null 2>&1; then - available+=("$pkg") - else - echo "Skipping unavailable optional package: $pkg" - fi - done - if [ ${#available[@]} -gt 0 ]; then - echo "Installing optional packages: ${available[*]}" - sudo apt-get -o DPkg::Lock::Timeout=300 install -y -qq "${available[@]}" || true - fi - fi + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" install-optional - name: Detect Python version for pipx cache (Linux) if: runner.os == 'Linux' id: python-version shell: bash - run: | - py_minor="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" - echo "minor=${py_minor}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" detect-python-version - name: Cache pipx and pip downloads (Linux) if: runner.os == 'Linux' @@ -164,14 +106,9 @@ runs: timeout_minutes: 5 max_attempts: 3 command: | - for pkg in $EXTRA_PACKAGES; do - if [[ ! "$pkg" =~ ^[a-zA-Z0-9][a-zA-Z0-9.+-]*$ ]]; then - echo "Error: Invalid package name '$pkg'" >&2 - exit 1 - fi - done + validated="$(python3 tools/setup/install_dependencies.py --validate-extra-packages $EXTRA_PACKAGES)" # shellcheck disable=SC2086 - sudo apt-get install -y $EXTRA_PACKAGES + sudo apt-get install -y $validated - name: Install dependencies (macOS) if: runner.os == 'macOS' diff --git a/.github/actions/qt-android/action.yml b/.github/actions/qt-android/action.yml index a310d6206da5..04ca94010431 100644 --- a/.github/actions/qt-android/action.yml +++ b/.github/actions/qt-android/action.yml @@ -1,5 +1,12 @@ name: Android Qt description: Install Qt for Android +outputs: + host_qt_root_dir: + description: Desktop Qt root used for host tools + value: ${{ steps.qt-host.outputs.qt_root_dir }} + target_qt_root_dir: + description: Android Qt root used for target packages + value: ${{ steps.qt-target.outputs.qt_root_dir }} inputs: host: description: Host @@ -45,9 +52,9 @@ inputs: required: false default: '35.0.0' save-cache: - description: Save cache (set to false for PR builds) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: 'true' + default: 'auto' runs: using: composite steps: @@ -71,22 +78,9 @@ runs: - name: Update Android SDK / NDK / Tools shell: bash run: | - ANDROID_SDK_ROOT_UNIX="${ANDROID_SDK_ROOT//\\//}" - NDK_PATH="${ANDROID_SDK_ROOT_UNIX}/ndk/${{ inputs.ndk-full-version }}" - if [[ ! -d "$NDK_PATH" ]]; then - echo "::error::NDK path not found: $NDK_PATH" - exit 1 - fi - echo "ANDROID_NDK_ROOT=${NDK_PATH}" >> "$GITHUB_ENV" - echo "ANDROID_NDK_HOME=${NDK_PATH}" >> "$GITHUB_ENV" - echo "ANDROID_NDK=${NDK_PATH}" >> "$GITHUB_ENV" - if [[ "$RUNNER_OS" == "Windows" ]]; then - "${ANDROID_SDK_ROOT}"/cmdline-tools/latest/bin/sdkmanager.bat --update - "${GITHUB_WORKSPACE}"/android/gradlew.bat --version - else - sdkmanager --update - "${GITHUB_WORKSPACE}"/android/gradlew --version - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/android_sdk_helper.py" \ + --ndk-version "${{ inputs.ndk-full-version }}" \ + --workspace "${GITHUB_WORKSPACE}" - name: Setup Caching uses: ./.github/actions/cache @@ -98,7 +92,8 @@ runs: save-cache: ${{ inputs.save-cache }} - name: Install Qt for ${{ runner.os }} - uses: jurplel/install-qt-action@v4 + id: qt-host + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -106,12 +101,11 @@ runs: arch: ${{ inputs.arch }} dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true - name: Install Qt for Android (arm64_v8a) if: contains(format(';{0};', inputs.abis), ';arm64-v8a;') - uses: jurplel/install-qt-action@v4 + id: qt-android-arm64 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -119,12 +113,12 @@ runs: arch: android_arm64_v8a dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (armv7) if: contains(format(';{0};', inputs.abis), ';armeabi-v7a;') - uses: jurplel/install-qt-action@v4 + id: qt-android-armv7 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -132,12 +126,12 @@ runs: arch: android_armv7 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (x86_64) if: contains(format(';{0};', inputs.abis), ';x86_64;') - uses: jurplel/install-qt-action@v4 + id: qt-android-x86_64 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -145,12 +139,12 @@ runs: arch: android_x86_64 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' - name: Install Qt for Android (x86) if: contains(format(';{0};', inputs.abis), ';x86;') - uses: jurplel/install-qt-action@v4 + id: qt-android-x86 + uses: ./.github/actions/qt-install with: version: ${{ inputs.version }} host: ${{ inputs.host }} @@ -158,5 +152,15 @@ runs: arch: android_x86 dir: ${{ runner.temp }} modules: ${{ inputs.modules }} - setup-python: false - cache: true + export-env: 'false' + + - name: Resolve primary Android Qt root + id: qt-target + shell: bash + run: | + python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" resolve-android-root \ + --abis "${{ inputs.abis }}" \ + --arm64 "${{ steps.qt-android-arm64.outputs.qt_root_dir }}" \ + --armv7 "${{ steps.qt-android-armv7.outputs.qt_root_dir }}" \ + --x86-64 "${{ steps.qt-android-x86_64.outputs.qt_root_dir }}" \ + --x86 "${{ steps.qt-android-x86.outputs.qt_root_dir }}" diff --git a/.github/actions/qt-install/action.yml b/.github/actions/qt-install/action.yml index c74b755ade4e..853cc08b29c4 100644 --- a/.github/actions/qt-install/action.yml +++ b/.github/actions/qt-install/action.yml @@ -1,5 +1,15 @@ name: Install Qt -description: Install Qt with standard QGroundControl modules +description: Install Qt using aqtinstall with caching +outputs: + arch_dir: + description: Resolved Qt architecture directory name + value: ${{ steps.qt-meta.outputs.arch_dir }} + qt_root_dir: + description: Absolute Qt root directory + value: ${{ steps.qt-install.outputs.qt_root_dir || steps.qt-cached.outputs.qt_root_dir }} + qt_bin_dir: + description: Qt bin directory + value: ${{ steps.qt-install.outputs.qt_bin_dir || steps.qt-cached.outputs.qt_bin_dir }} inputs: version: description: Qt version to install @@ -26,43 +36,69 @@ inputs: description: Optional Qt archives subset to install (space-separated) required: false default: '' + export-env: + description: Export QT_ROOT_DIR and prepend the Qt bin directory to PATH + required: false + default: 'true' runs: using: composite steps: - - name: Compute Qt cache fingerprint - id: qt-cache-key + - name: Resolve arch directory and cache key + id: qt-meta shell: bash env: + QT_ARCH: ${{ inputs.arch }} QT_MODULES: ${{ inputs.modules }} QT_ARCHIVES: ${{ inputs.archives }} - run: | - set -euo pipefail - cache_input="${QT_MODULES}"$'\n'"${QT_ARCHIVES}" - if command -v sha256sum >/dev/null 2>&1; then - digest="$(printf '%s' "$cache_input" | sha256sum | cut -d' ' -f1)" - else - digest="$(printf '%s' "$cache_input" | shasum -a 256 | awk '{print $1}')" - fi - echo "digest=${digest}" >> "$GITHUB_OUTPUT" + run: python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" cache-key --arch "${QT_ARCH}" --modules "${QT_MODULES}" --archives "${QT_ARCHIVES}" - name: Restore extracted Qt cache + id: qt-cache uses: actions/cache@v5 with: path: ${{ inputs.dir }}/Qt/${{ inputs.version }} - key: qt-extracted-${{ runner.os }}-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.arch }}-${{ inputs.version }}-${{ steps.qt-cache-key.outputs.digest }} + key: qt-${{ runner.os }}-${{ inputs.host }}-${{ inputs.target }}-${{ inputs.arch }}-${{ inputs.version }}-${{ steps.qt-meta.outputs.digest }} - - name: Ensure pip available - shell: bash - run: python3 -m ensurepip 2>/dev/null || true - name: Install Qt ${{ inputs.version }} - uses: jurplel/install-qt-action@v4 - with: - version: ${{ inputs.version }} - host: ${{ inputs.host }} - target: ${{ inputs.target }} - arch: ${{ inputs.arch }} - dir: ${{ inputs.dir }} - modules: ${{ inputs.modules }} - archives: ${{ inputs.archives }} - setup-python: false - cache: true + if: steps.qt-cache.outputs.cache-hit != 'true' + id: qt-install + shell: bash + env: + QT_VERSION: ${{ inputs.version }} + QT_HOST: ${{ inputs.host }} + QT_TARGET: ${{ inputs.target }} + QT_ARCH: ${{ inputs.arch }} + QT_OUTDIR: ${{ inputs.dir }}/Qt + QT_MODULES: ${{ inputs.modules }} + QT_ARCHIVES: ${{ inputs.archives }} + run: | + python3 "${GITHUB_WORKSPACE}/tools/setup/install_qt.py" install \ + --version "${QT_VERSION}" \ + --host "${QT_HOST}" \ + --target "${QT_TARGET}" \ + --arch "${QT_ARCH}" \ + --outdir "${QT_OUTDIR}" \ + --modules "${QT_MODULES}" \ + --archives "${QT_ARCHIVES}" + + - name: Resolve Qt paths (cache hit) + if: steps.qt-cache.outputs.cache-hit == 'true' + id: qt-cached + shell: bash + env: + QT_OUTDIR: ${{ inputs.dir }}/Qt + QT_VERSION: ${{ inputs.version }} + ARCH_DIR: ${{ steps.qt-meta.outputs.arch_dir }} + run: | + qt_root="${QT_OUTDIR}/${QT_VERSION}/${ARCH_DIR}" + echo "qt_root_dir=${qt_root}" >> "$GITHUB_OUTPUT" + echo "qt_bin_dir=${qt_root}/bin" >> "$GITHUB_OUTPUT" + + - name: Set Qt environment + if: ${{ inputs.export-env == 'true' }} + shell: bash + run: | + qt_root="${{ steps.qt-install.outputs.qt_root_dir || steps.qt-cached.outputs.qt_root_dir }}" + echo "QT_ROOT_DIR=${qt_root}" >> "$GITHUB_ENV" + echo "${qt_root}/bin" >> "$GITHUB_PATH" + echo "Qt installed at ${qt_root}" diff --git a/.github/actions/qt-ios/action.yml b/.github/actions/qt-ios/action.yml index 05ca7b01315d..072210416da3 100644 --- a/.github/actions/qt-ios/action.yml +++ b/.github/actions/qt-ios/action.yml @@ -24,9 +24,9 @@ inputs: required: false default: Release save-cache: - description: Save cache (set to false for PR builds) + description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false - default: "true" + default: "auto" restore-timestamps: description: Restore file timestamps from git history (requires fetch-depth 0) required: false diff --git a/.github/actions/run-unit-tests/action.yml b/.github/actions/run-unit-tests/action.yml index 9db5a51bc936..eb56af76a104 100644 --- a/.github/actions/run-unit-tests/action.yml +++ b/.github/actions/run-unit-tests/action.yml @@ -32,52 +32,17 @@ runs: - name: Detect parallel jobs id: jobs shell: bash - env: - INPUT_PARALLEL: ${{ inputs.parallel }} run: | - if [[ "$INPUT_PARALLEL" != "auto" ]]; then - JOBS="$INPUT_PARALLEL" - elif command -v nproc >/dev/null 2>&1; then - JOBS="$(nproc)" - elif command -v sysctl >/dev/null 2>&1; then - JOBS="$(sysctl -n hw.ncpu)" - elif [[ -n "${NUMBER_OF_PROCESSORS:-}" ]]; then - JOBS="${NUMBER_OF_PROCESSORS}" - else - JOBS=2 - fi - - if [[ ! "$JOBS" =~ ^[0-9]+$ ]] || [[ "$JOBS" -lt 1 ]]; then - echo "::error::Invalid parallel job count: $JOBS" - exit 1 - fi - - echo "jobs=$JOBS" >> "$GITHUB_OUTPUT" + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" detect-jobs \ + --parallel "${{ inputs.parallel }}" - name: Run CTest shell: bash working-directory: ${{ inputs.build-dir }} - env: - INPUT_JUNIT_OUTPUT: ${{ inputs.junit-output }} - INPUT_CTEST_OUTPUT: ${{ inputs.ctest-output }} - INPUT_INCLUDE_LABELS: ${{ inputs.include-labels }} - INPUT_EXCLUDE_LABELS: ${{ inputs.exclude-labels }} - INPUT_JOBS: ${{ steps.jobs.outputs.jobs }} run: | - set -o pipefail - - CTEST_ARGS=( - --output-on-failure - --output-junit "$INPUT_JUNIT_OUTPUT" - --parallel "$INPUT_JOBS" - ) - - if [[ -n "$INPUT_INCLUDE_LABELS" ]]; then - CTEST_ARGS+=(-L "$INPUT_INCLUDE_LABELS") - fi - - if [[ -n "$INPUT_EXCLUDE_LABELS" ]]; then - CTEST_ARGS+=(-LE "$INPUT_EXCLUDE_LABELS") - fi - - ctest "${CTEST_ARGS[@]}" 2>&1 | tee "$INPUT_CTEST_OUTPUT" + python3 "${GITHUB_WORKSPACE}/.github/scripts/cmake_helper.py" ctest \ + --junit-output "${{ inputs.junit-output }}" \ + --ctest-output "${{ inputs.ctest-output }}" \ + --jobs "${{ steps.jobs.outputs.jobs }}" \ + --include-labels "${{ inputs.include-labels }}" \ + --exclude-labels "${{ inputs.exclude-labels }}" diff --git a/.github/actions/setup-python/action.yml b/.github/actions/setup-python/action.yml index 9f65b41f9a35..51b5f8a2b15a 100644 --- a/.github/actions/setup-python/action.yml +++ b/.github/actions/setup-python/action.yml @@ -3,7 +3,7 @@ description: Setup Python with uv for fast dependency installation inputs: groups: - description: 'Dependency groups to install (comma-separated: precommit, test, ci, qt, coverage, dev, lsp, all)' + description: 'Dependency groups to install (comma-separated: scripts, precommit, test, ci, qt, coverage, dev, lsp, all)' required: false default: 'ci' python-version: @@ -24,6 +24,8 @@ runs: with: enable-cache: true cache-dependency-glob: | + tools/pyproject.toml + tools/uv.lock tools/setup/install_python.py .pre-commit-config.yaml @@ -32,15 +34,7 @@ runs: env: GROUPS: ${{ inputs.groups }} run: | - if command -v python3 >/dev/null 2>&1; then - py_cmd=python3 - elif command -v python >/dev/null 2>&1; then - py_cmd=python - else - echo "Error: Python not found in PATH" >&2 - exit 1 - fi - "$py_cmd" tools/setup/install_python.py "$GROUPS" + python3 tools/setup/install_python.py "$GROUPS" - name: Add venv to PATH shell: bash diff --git a/.github/actions/size-analysis/action.yml b/.github/actions/size-analysis/action.yml index bfa4d04b370a..aa4afd411a7a 100644 --- a/.github/actions/size-analysis/action.yml +++ b/.github/actions/size-analysis/action.yml @@ -40,12 +40,4 @@ runs: ) [[ "$INPUT_INSTALL_BLOATY" == "true" ]] && args+=(--install-bloaty) - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - "${py_cmd}" "${GITHUB_WORKSPACE}/.github/scripts/size_analysis.py" "${args[@]}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/size_analysis.py" "${args[@]}" diff --git a/.github/actions/test-duration-report/action.yml b/.github/actions/test-duration-report/action.yml index 93f08b0578d6..eb30438d3881 100644 --- a/.github/actions/test-duration-report/action.yml +++ b/.github/actions/test-duration-report/action.yml @@ -48,160 +48,13 @@ runs: - name: Analyze test durations id: analyze shell: bash - env: - INPUT_JUNIT_PATH: ${{ inputs.junit-path }} - INPUT_BASELINE_PATH: ${{ inputs.baseline-path }} - INPUT_REPORT_JSON_PATH: ${{ inputs.report-json-path }} - INPUT_TOP_N: ${{ inputs.top-n }} - INPUT_SLOW_THRESHOLD_SECONDS: ${{ inputs.slow-threshold-seconds }} - INPUT_REGRESSION_FACTOR: ${{ inputs.regression-factor }} - INPUT_MIN_DELTA_SECONDS: ${{ inputs.min-delta-seconds }} - INPUT_FAIL_ON_REGRESSION: ${{ inputs.fail-on-regression }} run: | - python3 - <<'PY' - import json - import os - import sys - import xml.etree.ElementTree as ET - from pathlib import Path - - junit_path = Path(os.environ["INPUT_JUNIT_PATH"]) - baseline_path = Path(os.environ["INPUT_BASELINE_PATH"]) - report_json_path = Path(os.environ["INPUT_REPORT_JSON_PATH"]) - top_n = int(os.environ["INPUT_TOP_N"]) - slow_threshold = float(os.environ["INPUT_SLOW_THRESHOLD_SECONDS"]) - regression_factor = float(os.environ["INPUT_REGRESSION_FACTOR"]) - min_delta = float(os.environ["INPUT_MIN_DELTA_SECONDS"]) - fail_on_regression = os.environ["INPUT_FAIL_ON_REGRESSION"].lower() == "true" - - step_summary = Path(os.environ["GITHUB_STEP_SUMMARY"]) - github_output = Path(os.environ["GITHUB_OUTPUT"]) - - def test_key(elem: ET.Element) -> str: - classname = elem.attrib.get("classname", "").strip() - name = elem.attrib.get("name", "").strip() - if classname and name: - return f"{classname}::{name}" - return name or classname or "" - - def parse_time(value: str) -> float: - try: - return float(value) - except Exception: - return 0.0 - - if not junit_path.exists(): - msg = f"JUnit report not found at {junit_path}" - print(f"::warning::{msg}") - with step_summary.open("a", encoding="utf-8") as f: - f.write("## Test Duration Report\n\n") - f.write(f"- WARNING: {msg}\n") - with github_output.open("a", encoding="utf-8") as f: - f.write("regression_count=0\n") - f.write("slow_count=0\n") - sys.exit(0) - - tree = ET.parse(junit_path) - root = tree.getroot() - cases = [] - for testcase in root.iter("testcase"): - key = test_key(testcase) - secs = parse_time(testcase.attrib.get("time", "0")) - cases.append((key, secs)) - - total_seconds = sum(secs for _, secs in cases) - slowest = sorted(cases, key=lambda x: x[1], reverse=True) - slow_over_threshold = [(k, s) for k, s in slowest if s >= slow_threshold] - - baseline = {} - baseline_loaded = False - if baseline_path.exists(): - try: - payload = json.loads(baseline_path.read_text(encoding="utf-8")) - if isinstance(payload, dict) and "tests" in payload and isinstance(payload["tests"], dict): - raw = payload["tests"] - for key, value in raw.items(): - if isinstance(value, dict): - baseline[key] = float(value.get("seconds", 0.0)) - else: - baseline[key] = float(value) - baseline_loaded = True - elif isinstance(payload, dict): - for key, value in payload.items(): - if isinstance(value, (int, float)): - baseline[key] = float(value) - baseline_loaded = True - except Exception as e: - print(f"::warning::Failed to parse baseline file {baseline_path}: {e}") - - regressions = [] - if baseline: - for key, current in cases: - previous = baseline.get(key) - if previous is None or previous <= 0: - continue - if current >= previous * regression_factor and (current - previous) >= min_delta: - regressions.append((key, previous, current)) - - report = { - "junit_path": str(junit_path), - "total_tests": len(cases), - "total_seconds": total_seconds, - "slow_threshold_seconds": slow_threshold, - "slow_tests": [{"test": k, "seconds": s} for k, s in slow_over_threshold], - "regression_factor": regression_factor, - "min_delta_seconds": min_delta, - "regressions": [ - {"test": k, "baseline_seconds": b, "current_seconds": c, "delta_seconds": c - b} - for k, b, c in regressions - ], - } - report_json_path.parent.mkdir(parents=True, exist_ok=True) - report_json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8") - - with step_summary.open("a", encoding="utf-8") as f: - f.write("## Test Duration Report\n\n") - f.write(f"- JUnit: `{junit_path}`\n") - f.write(f"- Total tests: **{len(cases)}**\n") - f.write(f"- Total time: **{total_seconds:.1f}s**\n") - f.write(f"- Slow threshold: **{slow_threshold:.1f}s**\n") - if baseline_loaded: - f.write(f"- Baseline file: `{baseline_path}` ({len(baseline)} entries)\n") - else: - f.write(f"- Baseline file: `{baseline_path}` (not available)\n") - f.write("\n") - - if slowest: - f.write(f"### Top {min(top_n, len(slowest))} Slowest Tests\n\n") - f.write("| Test | Seconds |\n") - f.write("|---|---:|\n") - for key, secs in slowest[:top_n]: - f.write(f"| `{key}` | {secs:.3f} |\n") - f.write("\n") - - if regressions: - f.write("### Regressions vs Baseline\n\n") - f.write("| Test | Baseline (s) | Current (s) | Delta (s) |\n") - f.write("|---|---:|---:|---:|\n") - for key, base, cur in sorted(regressions, key=lambda x: x[2] - x[1], reverse=True)[:top_n]: - f.write(f"| `{key}` | {base:.3f} | {cur:.3f} | {cur - base:.3f} |\n") - f.write("\n") - else: - f.write("No baseline regressions detected.\n\n") - - for key, secs in slow_over_threshold[:top_n]: - print(f"::warning::Slow test (>={slow_threshold:.1f}s): {key} took {secs:.3f}s") - - for key, base, cur in sorted(regressions, key=lambda x: x[2] - x[1], reverse=True)[:top_n]: - print( - f"::warning::Timing regression: {key} baseline={base:.3f}s current={cur:.3f}s " - f"(+{(cur - base):.3f}s, x{(cur / base):.2f})" - ) - - with github_output.open("a", encoding="utf-8") as f: - f.write(f"regression_count={len(regressions)}\n") - f.write(f"slow_count={len(slow_over_threshold)}\n") - - if fail_on_regression and regressions: - sys.exit(1) - PY + python3 "${GITHUB_WORKSPACE}/.github/scripts/test_duration_report.py" \ + --junit-path "${{ inputs.junit-path }}" \ + --baseline-path "${{ inputs.baseline-path }}" \ + --report-json-path "${{ inputs.report-json-path }}" \ + --top-n "${{ inputs.top-n }}" \ + --slow-threshold-seconds "${{ inputs.slow-threshold-seconds }}" \ + --regression-factor "${{ inputs.regression-factor }}" \ + --min-delta-seconds "${{ inputs.min-delta-seconds }}" \ + ${{ inputs.fail-on-regression == 'true' && '--fail-on-regression' || '' }} diff --git a/.github/actions/upload/action.yml b/.github/actions/upload/action.yml index 0d8102c48165..1436575f9982 100644 --- a/.github/actions/upload/action.yml +++ b/.github/actions/upload/action.yml @@ -2,33 +2,33 @@ name: Upload Release description: Upload release artifact to GitHub and optionally to AWS inputs: - artifact_name: + artifact-name: description: Artifact filename required: true - package_name: + package-name: description: Package name (for GitHub artifact) required: true - aws_role_arn: + aws-role-arn: description: AWS IAM role ARN for OIDC authentication (preferred) required: false - aws_key_id: + aws-key-id: description: AWS access key ID required: false - aws_secret_access_key: + aws-secret-access-key: description: AWS secret access key required: false - aws_distribution_id: + aws-distribution-id: description: AWS CloudFront distribution ID required: false - upload_aws: + upload-aws: description: Upload artifact to AWS required: false default: 'true' - retention_days: + retention-days: description: Days to retain artifact (default 7 for PRs, 30 for pushes) required: false default: '' - compression_level: + compression-level: description: Compression level 0-9 (0=none, best for pre-compressed binaries) required: false default: '0' @@ -37,24 +37,24 @@ runs: using: composite steps: - name: Save artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: - name: ${{ inputs.package_name }} - path: ${{ runner.temp }}/build/${{ inputs.artifact_name }} - retention-days: ${{ inputs.retention_days != '' && inputs.retention_days || (github.event_name == 'pull_request' && '7' || '30') }} - compression-level: ${{ inputs.compression_level }} + name: ${{ inputs.package-name }} + path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + retention-days: ${{ inputs.retention-days != '' && inputs.retention-days || (github.event_name == 'pull_request' && '7' || '30') }} + compression-level: ${{ inputs.compression-level }} - name: Upload to AWS if: >- - fromJSON(inputs.upload_aws) && + fromJSON(inputs.upload-aws) && github.event_name == 'push' && github.repository_owner == 'mavlink' && - (inputs.aws_role_arn != '' || (inputs.aws_key_id != '' && inputs.aws_secret_access_key != '')) + (inputs.aws-role-arn != '' || (inputs.aws-key-id != '' && inputs.aws-secret-access-key != '')) uses: ./.github/actions/aws-upload with: - artifact_name: ${{ inputs.artifact_name }} - artifact_path: ${{ runner.temp }}/build/${{ inputs.artifact_name }} - aws_role_arn: ${{ inputs.aws_role_arn }} - aws_key_id: ${{ inputs.aws_key_id }} - aws_secret_access_key: ${{ inputs.aws_secret_access_key }} - aws_distribution_id: ${{ inputs.aws_distribution_id }} + artifact-name: ${{ inputs.artifact-name }} + artifact-path: ${{ runner.temp }}/build/${{ inputs.artifact-name }} + aws-role-arn: ${{ inputs.aws-role-arn }} + aws-key-id: ${{ inputs.aws-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-distribution-id: ${{ inputs.aws-distribution-id }} diff --git a/.github/actions/verify-executable/action.yml b/.github/actions/verify-executable/action.yml index 3052bca4ddd2..65025b1ad8a4 100644 --- a/.github/actions/verify-executable/action.yml +++ b/.github/actions/verify-executable/action.yml @@ -23,67 +23,9 @@ runs: steps: - name: Verify executable shell: bash - env: - INPUT_BINARY_PATH: ${{ inputs.binary-path }} - INPUT_WORKING_DIR: ${{ inputs.working-dir }} - INPUT_TYPE: ${{ inputs.type }} - INPUT_TIMEOUT: ${{ inputs.timeout }} run: | - set -euo pipefail - - binary_path="${INPUT_BINARY_PATH}" - - if [[ -n "${INPUT_WORKING_DIR}" ]]; then - work_dir="${INPUT_WORKING_DIR}" - else - work_dir="$(dirname "${binary_path}")" - fi - - binary_name="$(basename "${binary_path}")" - run_binary="${binary_name}" - run_headless="true" - - # AppImages typically don't ship the "offscreen" Qt platform plugin. - # Let run_tests.py use xvfb-run (Linux, no DISPLAY) instead. - if [[ "${INPUT_TYPE}" == "appimage" || "${binary_name}" == *.AppImage ]]; then - run_headless="false" - fi - - if [[ ! -d "${work_dir}" ]]; then - echo "::error::Working directory not found: ${work_dir}" - exit 1 - fi - - pushd "${work_dir}" >/dev/null - - if [[ "${RUNNER_OS}" != "Windows" ]]; then - chmod +x "${binary_name}" 2>/dev/null || true - run_binary="${PWD}/${binary_name}" - fi - - if command -v python3 >/dev/null 2>&1; then - py_cmd="python3" - elif command -v python >/dev/null 2>&1; then - py_cmd="python" - else - echo "Error: Python interpreter not found in PATH" >&2 - exit 1 - fi - - set +e - args=( - --binary "${run_binary}" - --timeout "${INPUT_TIMEOUT}" - --verify-only - -v - ) - if [[ "${run_headless}" == "true" ]]; then - args+=(--headless) - fi - "${py_cmd}" "${GITHUB_WORKSPACE}/tools/run_tests.py" "${args[@]}" - exit_code=$? - set -e - - popd >/dev/null - - exit "${exit_code}" + python3 "${GITHUB_WORKSPACE}/.github/scripts/verify_executable.py" \ + --binary-path "${{ inputs.binary-path }}" \ + --type "${{ inputs.type }}" \ + --working-dir "${{ inputs.working-dir }}" \ + --timeout "${{ inputs.timeout }}" diff --git a/.github/build-config.json b/.github/build-config.json index ee7cbb63d72d..599ce585893b 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -1,18 +1,21 @@ { "android_build_tools": "35.0.0", + "ccache_version": "4.13.1", "android_cmdline_tools": "13114758", "android_min_sdk": "28", "android_platform": "35", "cmake_minimum_version": "3.25", "gstreamer_android_version": "1.22.12", + "gstreamer_ios_version": "1.24.13", "gstreamer_macos_version": "1.24.13", - "gstreamer_minimum_version": "1.22.12", - "gstreamer_version": "1.24.13", + "gstreamer_minimum_version": "1.20", + "gstreamer_default_version": "1.24.13", "gstreamer_windows_version": "1.22.12", "ios_deployment_target": "14.0", "java_version": "17", "macos_deployment_target": "13.0", "ndk_full_version": "27.2.12479018", + "platform_workflows": "Linux,Windows,MacOS,Android", "ndk_version": "r27c", "qt_minimum_version": "6.10.0", "qt_modules": "qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml qtwebsockets qthttpserver", diff --git a/.github/codecov.yml b/.github/codecov.yml index cef4e70c46ed..e817c6eb2cd3 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,8 +1,8 @@ codecov: - require_ci_to_pass: yes + require_ci_to_pass: true notify: - after_n_builds: 2 # Wait for at least 2 builds (Debug + Release) - wait_for_ci: yes + after_n_builds: 1 # Only Linux Debug generates coverage + wait_for_ci: true coverage: precision: 2 @@ -26,9 +26,9 @@ coverage: comment: layout: "reach,diff,flags,tree,footer" behavior: default - require_changes: no - require_base: no - require_head: yes + require_changes: false + require_base: false + require_head: true ignore: - "test/**/*" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c31297820aa9..d9963c4a1597 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -39,6 +39,42 @@ src/ └── Settings/ # Persistent settings ``` +## CI Structure + +Platform workflows (`linux.yml`, `macos.yml`, `windows.yml`, `android.yml`, `ios.yml`) share logic via composite actions and reusable workflows. + +``` +.github/ +├── workflows/ +│ ├── linux.yml, macos.yml, ... # Platform build + test +│ ├── _detect-changes.yml # Reusable: skip builds on unrelated PRs +│ ├── build-results.yml # Aggregate PR comment (workflow_run trigger) +│ └── build-gstreamer.yml # GStreamer SDK builds +├── actions/ +│ ├── cmake-configure/ # CMake configure with consistent options +│ ├── cmake-build/ # Build with timing, reviewdog, ccache +│ ├── run-unit-tests/ # CTest runner with JUnit output +│ ├── detect-changes/ # Path-based change detection per platform +│ ├── attest-and-upload/ # SBOM attestation + artifact upload +│ ├── deploy-docs/ # Deploy built docs to external repo +│ ├── gstreamer/ # Build GStreamer from source +│ ├── setup-python/ # Python + uv + dependency installation +│ └── qt-install/ # Qt SDK installation with caching +├── scripts/ # Python scripts for CI jobs +│ ├── common/ # Shared modules (gh_actions, build_config) +│ ├── templates/ # Jinja2 templates for generated output +│ └── tests/ # Tests for CI scripts +└── build-config.json # Centralized version numbers +``` + +### CI Conventions + +- **Dependencies**: CI Python scripts use `httpx` for GitHub API access and `jinja2` for templating. Deps managed in `tools/pyproject.toml` under `[project.optional-dependencies] scripts`. +- **Shared helpers**: `gh_actions.py` provides GitHub API pagination (httpx) with `gh` CLI fallback. Import as `from common.gh_actions import ...`. +- **Bootstrap scripts** (`install_dependencies.py`, `ccache_helper.py`): Use stdlib only — they run before dependencies are installed. +- **Config**: Version numbers and build settings live in `.github/build-config.json`. Read via `common.build_config.get_build_config_value()`. +- **Outputs**: Use `common.gh_actions.write_github_output()` for `$GITHUB_OUTPUT` writes. + ## Quick Patterns ```cpp diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4ed07cc136b6..3bb6fbda7703 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,10 @@ # Dependabot configuration # To merge dependabot PRs, comment "@dependabot merge" after CI passes # See: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-pull-requests-for-dependency-updates +# +# Dependency management split: +# - Dependabot: GitHub Actions (this file) +# - Renovate: npm, git-submodules, pip, pre-commit (see renovate.json) version: 2 updates: @@ -17,4 +21,3 @@ updates: github-actions: patterns: - "*" - # npm dependencies managed by Renovate (see renovate.json) diff --git a/.github/renovate.json b/.github/renovate.json index 4c20d12f02dd..606c24bd13b1 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -14,6 +14,7 @@ "ignorePaths": [ ".github/workflows/**" ], + "description": "Manages npm, git-submodules, pip, pre-commit. GitHub Actions are handled by Dependabot (see dependabot.yml).", "enabledManagers": [ "npm", "git-submodules", diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 8f212c3dd0e5..1de04b1b0809 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -8,16 +8,19 @@ Python helper scripts used by workflows and composite actions in this repository |---|---| | `android_boot_test.py` | Android emulator boot smoke test | | `benchmark_runner.py` | Run benchmark binaries and summarize output | +| `ccache_helper.py` | Ccache CI helper: config output, binary install, build summary | | `check_baseline_ready.py` | Verify baseline-cache update readiness for platform workflows | +| `ci_bootstrap.py` | Bootstrap helper that makes `tools/common` imports work for CI scripts | | `collect_artifact_sizes.py` | Collect artifact sizes for latest successful platform workflow runs | | `collect_build_status.py` | Collect latest platform/pre-commit status for build-results comments | | `coverage_comment.py` | Build coverage report comments | | `find_binary.py` | Locate produced binaries/artifacts in build trees | | `generate_build_results_comment.py` | Generate consolidated PR build-results comment | | `gstreamer_archive.py` | Package GStreamer builds and optionally upload to S3 | -| `install_ccache.py` | Resolve ccache config and install pinned Linux binary | +| `plan_docker_builds.py` | Generate Docker workflow build matrices from changed files | +| `precommit_results.py` | Normalize pre-commit outputs into uploaded CI artifacts | | `size_analysis.py` | Analyze binary size changes | -| `workflow_runs.py` | Shared helpers for listing workflow runs via GitHub API | +| `test_duration_report.py` | Generate test-duration reports and regressions | ## Tests diff --git a/.github/scripts/android_sdk_helper.py b/.github/scripts/android_sdk_helper.py new file mode 100644 index 000000000000..affcd1a39881 --- /dev/null +++ b/.github/scripts/android_sdk_helper.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Android SDK/NDK setup helpers for CI.""" + +from __future__ import annotations + +import argparse +import os +import subprocess +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env # noqa: E402 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--ndk-version", required=True) + parser.add_argument("--workspace", default=os.environ.get("GITHUB_WORKSPACE", ".")) + args = parser.parse_args() + + sdk_root = os.environ.get("ANDROID_SDK_ROOT", "") + if not sdk_root: + print("::error::ANDROID_SDK_ROOT not set", file=sys.stderr) + sys.exit(1) + + sdk_root_unix = sdk_root.replace("\\", "/") + ndk_path = f"{sdk_root_unix}/ndk/{args.ndk_version}" + + if not Path(ndk_path).is_dir(): + print(f"::error::NDK path not found: {ndk_path}", file=sys.stderr) + sys.exit(1) + + append_github_env({ + "ANDROID_NDK_ROOT": ndk_path, + "ANDROID_NDK_HOME": ndk_path, + "ANDROID_NDK": ndk_path, + }) + + is_windows = os.environ.get("RUNNER_OS") == "Windows" + + if is_windows: + sdkmanager = os.path.join(sdk_root, "cmdline-tools", "latest", "bin", "sdkmanager.bat") + gradlew = os.path.join(args.workspace, "android", "gradlew.bat") + else: + sdkmanager = "sdkmanager" + gradlew = os.path.join(args.workspace, "android", "gradlew") + + subprocess.run([sdkmanager, "--update"], check=True) + subprocess.run([gradlew, "--version"], check=True) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/attest_helper.py b/.github/scripts/attest_helper.py new file mode 100644 index 000000000000..f59fdb791c10 --- /dev/null +++ b/.github/scripts/attest_helper.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Check attestation conditions and resolve SBOM paths.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import is_fork_pr, write_github_output # noqa: E402 + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--subject-path", required=True) + parser.add_argument("--subject-name", required=True) + parser.add_argument("--scan-path", default="") + parser.add_argument("--sbom-format", default="spdx-json") + parser.add_argument("--runner-temp", required=True) + args = parser.parse_args() + + subject = Path(args.subject_path) + + if is_fork_pr(): + print("Skipping attestation for external PR") + write_github_output({"skip": "true"}) + return + + if not subject.exists(): + print(f"::warning::Artifact not found: {subject}") + write_github_output({"skip": "true"}) + return + + scan_path = args.scan_path or str(subject.parent) + suffix = "cdx.json" if args.sbom_format == "cyclonedx-json" else "spdx.json" + sbom_path = str(Path(args.runner_temp) / f"{args.subject_name}.sbom.{suffix}") + + write_github_output({ + "skip": "false", + "scan-path": scan_path, + "sbom-path": sbom_path, + }) + print(f"Will attest: {subject}") + print(f"Scan path: {scan_path}") + print(f"SBOM path: {sbom_path}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/aws_upload.py b/.github/scripts/aws_upload.py new file mode 100644 index 000000000000..cc21cf4b2b80 --- /dev/null +++ b/.github/scripts/aws_upload.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Validate and upload artifacts to AWS S3. + +Subcommands: + validate Validate AWS credentials and artifact inputs + upload Upload artifact to S3 with path safety checks +""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys +from pathlib import Path + + +def validate_credentials(role_arn: str, key_id: str, secret_key: str) -> None: + """Ensure either OIDC role or static credentials are provided.""" + if not role_arn and (not key_id or not secret_key): + print( + "::error::Either aws_role_arn (OIDC) or both aws_key_id and " + "aws_secret_access_key (static credentials) must be provided", + file=sys.stderr, + ) + sys.exit(1) + + +def validate_artifact(artifact_path: str, artifact_name: str) -> None: + """Validate artifact exists and name is safe.""" + if not Path(artifact_path).is_file(): + print(f"::error::Artifact not found: {artifact_path}", file=sys.stderr) + sys.exit(1) + + if re.search(r"[/\\]", artifact_name) or ".." in artifact_name: + print( + f"::error::Invalid artifact name (contains path separators or ..): {artifact_name}", + file=sys.stderr, + ) + sys.exit(1) + + +def sanitize_ref(ref_name: str) -> str: + """Remove path traversal and unsafe characters from a git ref name.""" + safe = ref_name.replace("..", "") + return re.sub(r"[^a-zA-Z0-9._-]", "_", safe) + + +def cmd_validate(args: argparse.Namespace) -> None: + validate_credentials(args.role_arn, args.key_id, args.secret_key) + validate_artifact(args.artifact_path, args.artifact_name) + + +def cmd_upload(args: argparse.Namespace) -> None: + validate_artifact(args.artifact_path, args.artifact_name) + safe_ref = sanitize_ref(args.ref_name) + + s3_path = f"s3://{args.s3_bucket}/builds/{safe_ref}/{args.artifact_name}" + subprocess.run( + ["aws", "s3", "cp", args.artifact_path, s3_path, "--acl", "public-read"], + check=True, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + p_val = sub.add_parser("validate") + p_val.add_argument("--role-arn", default="") + p_val.add_argument("--key-id", default="") + p_val.add_argument("--secret-key", default="") + p_val.add_argument("--artifact-path", required=True) + p_val.add_argument("--artifact-name", required=True) + + p_up = sub.add_parser("upload") + p_up.add_argument("--artifact-path", required=True) + p_up.add_argument("--artifact-name", required=True) + p_up.add_argument("--ref-name", required=True) + p_up.add_argument("--s3-bucket", default="qgroundcontrol") + + args = parser.parse_args() + {"validate": cmd_validate, "upload": cmd_upload}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/benchmark_runner.py b/.github/scripts/benchmark_runner.py index 582669840e30..ccfc06184b9b 100755 --- a/.github/scripts/benchmark_runner.py +++ b/.github/scripts/benchmark_runner.py @@ -18,6 +18,15 @@ import time from pathlib import Path +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + def run_benchmarks(binary: Path, test_filter: str, platform: str) -> str: """Run Qt Test benchmarks and return output.""" @@ -106,33 +115,25 @@ def fallback_startup_benchmark(binary: Path, platform: str) -> list[dict]: def write_github_output(results: list[dict]) -> None: """Write outputs for GitHub Actions.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - with open(github_output, "a") as f: - f.write(f"has_results={'true' if results else 'false'}\n") - f.write(f"result_count={len(results)}\n") + _write_github_output({ + "has_results": "true" if results else "false", + "result_count": str(len(results)), + }) def write_summary(results: list[dict], output_file: Path) -> None: """Write GitHub step summary.""" - summary_file = os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_file: - return - - with open(summary_file, "a") as f: - f.write("## Runtime Benchmarks\n\n") - f.write( - "⚠️ **Note**: Runtime benchmarks on shared runners have ~3x variance.\n" - ) - f.write("Only significant regressions (>50%) trigger alerts.\n\n") - f.write("### Results\n\n") - - if results and output_file.exists(): - f.write("```json\n") - f.write(output_file.read_text()) - f.write("\n```\n") - else: - f.write("No benchmark results available\n") + parts = [ + "## Runtime Benchmarks\n", + "⚠️ **Note**: Runtime benchmarks on shared runners have ~3x variance.", + "Only significant regressions (>50%) trigger alerts.\n", + "### Results\n", + ] + if results and output_file.exists(): + parts.append(f"```json\n{output_file.read_text()}\n```\n") + else: + parts.append("No benchmark results available\n") + _write_step_summary("\n".join(parts)) def main() -> int: diff --git a/.github/scripts/ccache_helper.py b/.github/scripts/ccache_helper.py new file mode 100755 index 000000000000..952423fac421 --- /dev/null +++ b/.github/scripts/ccache_helper.py @@ -0,0 +1,851 @@ +#!/usr/bin/env python3 +""" +Ccache helper for CI: install, configure, and report. + +Subcommands: + config Output ccache configuration for GitHub Actions + install Download and install a pinned ccache binary (Linux only) + summary Write a build cache hit/miss summary to GitHub Step Summary + +Examples: + ccache_helper.py config --version 4.13.1 --arch x86_64 --conf ccache.conf + ccache_helper.py install --version 4.13.1 --arch x86_64 + ccache_helper.py summary +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time +import zipfile +from pathlib import Path +from typing import NamedTuple +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env, write_github_output as write_github_outputs # noqa: E402 + +def _download_file(url: str, dest: Path, *, timeout: int = 120) -> None: + """Download a URL to a local file using urllib (stdlib).""" + import urllib.request + + request = urllib.request.Request(url) + with urllib.request.urlopen(request, timeout=timeout) as resp: + with open(dest, "wb") as f: + while True: + chunk = resp.read(65536) + if not chunk: + break + f.write(chunk) + + +WINDOWS_BINARY_SHA256 = { + "x86_64": "1c78a0b816a3174d4b170b96294e016a21fb4a577dfd8361e7322f77f85c6348", + "aarch64": "5907c8f74dcfff920ee57df55a5cb98164630665926b2ba9085e9f5fb701c58f", +} + + +# --------------------------------------------------------------------------- +# Data classes +# --------------------------------------------------------------------------- + +class CcacheConfig(NamedTuple): + """Configuration values extracted for ccache setup.""" + + version: str + arch: str + max_size: str + + +# --------------------------------------------------------------------------- +# Installer +# --------------------------------------------------------------------------- + +class CcacheInstaller: + """Handles ccache installation with signature verification.""" + + DEFAULT_VERSION = "4.13.1" + DEFAULT_MAX_SIZE = "2G" + MINISIGN_VERSION = "0.11" + MINISIGN_KEY = "RWQX7yXbBedVfI4PNx6FLdFXu9GHUFsr28s4BVGxm4BeybtnX3P06saF" + + CCACHE_RELEASE_URL = "https://github.com/ccache/ccache/releases/download" + MINISIGN_RELEASE_URL = "https://github.com/jedisct1/minisign/releases/download" + MINISIGN_ARCHIVE_SHA256 = "f0a0954413df8531befed169e447a66da6868d79052ed7e892e50a4291af7ae0" + + def __init__( + self, + version: str = DEFAULT_VERSION, + arch: str | None = None, + config_path: Path | None = None, + prefix: Path | None = None, + max_retries: int = 3, + retry_delay: float = 5.0, + ) -> None: + self.version = version + self.arch = arch or self.detect_arch() + self.config_path = config_path + self.prefix = prefix or self._default_prefix() + self.max_retries = max_retries + self.retry_delay = retry_delay + self.max_size = self.DEFAULT_MAX_SIZE + + if config_path: + self.max_size = self.read_max_size(config_path) + + @staticmethod + def _default_prefix() -> Path: + """Get default installation prefix from env or standard location.""" + env_prefix = os.environ.get("CCACHE_PREFIX") + if env_prefix: + return Path(env_prefix) + return Path("/usr/local") + + @staticmethod + def validate_version(version: str) -> bool: + """Validate version format matches X.Y or X.Y.Z pattern.""" + pattern = r"^[0-9]+\.[0-9]+(\.[0-9]+)?$" + return bool(re.match(pattern, version)) + + @staticmethod + def detect_arch() -> str: + """Auto-detect CPU architecture, normalizing to ccache naming.""" + machine = platform.machine().lower() + arch_map = { + "x86_64": "x86_64", + "amd64": "x86_64", + "aarch64": "aarch64", + "arm64": "aarch64", + } + return arch_map.get(machine, "x86_64") + + def read_max_size(self, config_path: Path) -> str: + """Extract max_size value from ccache.conf file.""" + if not config_path.exists(): + print( + f"Warning: ccache config not found at {config_path}, using default max_size", + file=sys.stderr, + ) + return self.DEFAULT_MAX_SIZE + + pattern = re.compile(r"^max_size\s*=\s*(.+)$") + try: + with config_path.open() as f: + for line in f: + match = pattern.match(line.strip()) + if match: + return match.group(1).strip() + except OSError as e: + print(f"Warning: Failed to read config: {e}", file=sys.stderr) + + return self.DEFAULT_MAX_SIZE + + def download_with_retry(self, url: str, dest: Path) -> bool: + """Download file with retry logic.""" + from urllib.error import URLError + + for attempt in range(1, self.max_retries + 1): + try: + print(f"Downloading {url} (attempt {attempt}/{self.max_retries})") + _download_file(url, dest) + return True + except (URLError, OSError) as e: + print(f"Download failed: {e}", file=sys.stderr) + if attempt < self.max_retries: + print(f"Retrying in {self.retry_delay} seconds...") + time.sleep(self.retry_delay) + + return False + + def download_with_verify(self, temp_dir: Path) -> Path | None: + """Download ccache archive and verify its signature.""" + archive_name = f"ccache-{self.version}-linux-{self.arch}-glibc.tar.xz" + archive_path = temp_dir / archive_name + sig_path = temp_dir / f"{archive_name}.minisig" + + ccache_url = f"{self.CCACHE_RELEASE_URL}/v{self.version}/{archive_name}" + sig_url = f"{ccache_url}.minisig" + + if not self.download_with_retry(ccache_url, archive_path): + print("Error: Failed to download ccache archive", file=sys.stderr) + return None + + if not self.download_with_retry(sig_url, sig_path): + print("Error: Failed to download signature file", file=sys.stderr) + return None + + minisign_bin = self._setup_minisign(temp_dir) + if not minisign_bin: + print("Error: Failed to setup minisign", file=sys.stderr) + return None + + if not self.verify_signature(archive_path, sig_path, minisign_bin): + print("Error: Signature verification failed", file=sys.stderr) + return None + + return archive_path + + def _setup_minisign(self, temp_dir: Path) -> Path | None: + """Download and extract minisign binary.""" + minisign_archive = f"minisign-{self.MINISIGN_VERSION}-linux.tar.gz" + minisign_url = f"{self.MINISIGN_RELEASE_URL}/{self.MINISIGN_VERSION}/{minisign_archive}" + minisign_path = temp_dir / minisign_archive + + if not self.download_with_retry(minisign_url, minisign_path): + return None + + actual_hash = hashlib.sha256(minisign_path.read_bytes()).hexdigest() + if actual_hash != self.MINISIGN_ARCHIVE_SHA256: + print( + f"Error: minisign archive hash mismatch: {actual_hash} != {self.MINISIGN_ARCHIVE_SHA256}", + file=sys.stderr, + ) + return None + + try: + with tarfile.open(minisign_path, "r:gz") as tar: + tar.extractall(temp_dir, filter="data") + except tarfile.TarError as e: + print(f"Error extracting minisign: {e}", file=sys.stderr) + return None + + minisign_bin = temp_dir / "minisign-linux" / self.arch / "minisign" + if not minisign_bin.exists(): + print(f"Error: minisign binary not found at {minisign_bin}", file=sys.stderr) + return None + + minisign_bin.chmod(0o755) + return minisign_bin + + def verify_signature(self, archive: Path, sig_file: Path, minisign_bin: Path) -> bool: + """Verify archive signature using minisign.""" + try: + result = subprocess.run( + [str(minisign_bin), "-Vm", str(archive), "-P", self.MINISIGN_KEY], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(f"Signature verification failed: {result.stderr}", file=sys.stderr) + return False + print("Signature verified successfully") + return True + except OSError as e: + print(f"Error running minisign: {e}", file=sys.stderr) + return False + + def install(self, archive: Path) -> bool: + """Extract and install ccache binary.""" + temp_dir = archive.parent + extract_dir = temp_dir / f"ccache-{self.version}-linux-{self.arch}-glibc" + + try: + with tarfile.open(archive, "r:xz") as tar: + tar.extractall(temp_dir, filter="data") + except tarfile.TarError as e: + print(f"Error extracting ccache: {e}", file=sys.stderr) + return False + + ccache_bin = extract_dir / "ccache" + if not ccache_bin.exists(): + print(f"Error: ccache binary not found at {ccache_bin}", file=sys.stderr) + return False + + dest_bin = self.prefix / "bin" / "ccache" + + try: + result = subprocess.run( + ["sudo", "cp", str(ccache_bin), str(dest_bin)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(f"Error copying ccache: {result.stderr}", file=sys.stderr) + return False + + result = subprocess.run( + ["sudo", "chmod", "+x", str(dest_bin)], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + print(f"Error setting permissions: {result.stderr}", file=sys.stderr) + return False + + except OSError as e: + print(f"Error installing ccache: {e}", file=sys.stderr) + return False + + print(f"ccache {self.version} installed successfully to {dest_bin}") + return True + + def is_installed(self) -> bool: + """Check if requested ccache version is already installed.""" + ccache_path = shutil.which("ccache") + if not ccache_path: + return False + + try: + result = subprocess.run( + ["ccache", "--version"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return False + + version_match = re.search(r"[0-9]+\.[0-9]+(\.[0-9]+)?", result.stdout) + if version_match and version_match.group() == self.version: + return True + except OSError: + pass + + return False + + def get_config(self) -> CcacheConfig: + """Return current configuration as named tuple.""" + return CcacheConfig( + version=self.version, + arch=self.arch, + max_size=self.max_size, + ) + + def run_install(self) -> bool: + """Execute full installation workflow.""" + if platform.system() != "Linux": + print("Warning: Binary installation only supported on Linux", file=sys.stderr) + return False + + if self.is_installed(): + print(f"ccache {self.version} already installed") + return True + + print(f"Installing ccache {self.version}...") + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + archive = self.download_with_verify(temp_path) + if not archive: + return False + + if not self.install(archive): + return False + + self._print_version() + return True + + def _print_version(self) -> None: + """Print installed ccache version.""" + try: + result = subprocess.run( + ["ccache", "--version"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + print(result.stdout.strip()) + except OSError: + pass + + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- + +def get_ccache_json_stats() -> dict | None: + """Run ``ccache --print-stats --format=json`` and return parsed dict. + + Returns None when ccache is unavailable or too old (< 4.10). + """ + ccache_bin = shutil.which("ccache") + if not ccache_bin: + return None + + try: + result = subprocess.run( + [ccache_bin, "--print-stats", "--format=json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return None + return json.loads(result.stdout) + except (OSError, json.JSONDecodeError): + return None + + +def build_summary_markdown(stats: dict) -> str: + """Build a GitHub-flavoured markdown summary table from ccache JSON stats.""" + direct = int(stats.get("direct_cache_hit", 0)) + preprocessed = int(stats.get("preprocessed_cache_hit", 0)) + misses = int(stats.get("cache_miss", 0)) + hits = direct + preprocessed + total = hits + misses + pct = f"{hits / total * 100:.1f}" if total > 0 else "0.0" + + lines = [ + "### CCache Statistics", + "", + "| Metric | Value |", + "|--------|-------|", + f"| Cache hits | {hits} / {total} ({pct}%) |", + f"| Direct hits | {direct} |", + f"| Preprocessed hits | {preprocessed} |", + f"| Misses | {misses} |", + ] + return "\n".join(lines) + "\n" + + +def write_step_summary(markdown: str) -> bool: + """Append *markdown* to ``$GITHUB_STEP_SUMMARY``. + + Returns True on success, False when the env var is unset or write fails. + """ + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + print(markdown) + return True + + try: + with open(summary_path, "a") as f: + f.write(markdown) + return True + except OSError as e: + print(f"Warning: Failed to write step summary: {e}", file=sys.stderr) + return False + + +def get_ccache_verbose_stats() -> str | None: + """Run ``ccache -s -vv`` and return its output.""" + ccache_bin = shutil.which("ccache") + if not ccache_bin: + return None + + try: + result = subprocess.run( + [ccache_bin, "-s", "-vv"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return None + return result.stdout.strip() + except OSError: + return None + + +def run_summary() -> int: + """Collect ccache stats and write a job summary table.""" + parts: list[str] = [] + + stats = get_ccache_json_stats() + if stats is not None: + parts.append(build_summary_markdown(stats)) + else: + print("ccache JSON stats unavailable (ccache missing or < 4.10)", file=sys.stderr) + + verbose = get_ccache_verbose_stats() + if verbose: + parts.append( + "
\nVerbose stats\n\n" + f"```\n{verbose}\n```\n\n
\n" + ) + + if parts: + write_step_summary("\n".join(parts)) + return 0 + + +# --------------------------------------------------------------------------- +# GitHub Actions output helper +# --------------------------------------------------------------------------- + +def output_github_actions(config: CcacheConfig) -> None: + """Write outputs for GitHub Actions.""" + write_github_outputs({ + "version": config.version, + "arch": config.arch, + "max_size": config.max_size, + }) + + + +def append_github_path(path_entry: str) -> None: + """Append a path entry to ``$GITHUB_PATH`` when available.""" + github_path = os.environ.get("GITHUB_PATH") + if not github_path: + return + with open(github_path, "a", encoding="utf-8") as handle: + handle.write(f"{path_entry}\n") + + +def determine_cache_scope(event_name: str, ref_name: str, pr_number: str = "") -> str: + """Return the normalized cache scope used by CI.""" + scope = "shared" + if event_name == "pull_request": + scope = f"pr-{pr_number or 'unknown'}" + elif event_name == "workflow_dispatch": + scope = f"manual-{ref_name}" + elif event_name == "push": + if ref_name != "master": + scope = f"branch-{ref_name}" + else: + scope = f"{event_name}-{ref_name}" + return scope.replace("/", "-") + + +def compute_cpm_fingerprint(root: Path) -> str: + """Hash CMake files that declare CPM or FetchContent dependencies.""" + declaration_re = re.compile(r"CPM(Add|Find)Package|FetchContent_Declare") + candidates: list[Path] = [] + for exact in [ + root / "CMakeLists.txt", + root / "cmake/modules/CPM.cmake", + root / ".github/build-config.json", + ]: + if exact.exists(): + candidates.append(exact) + + for directory in ("cmake",): + base = root / directory + if base.exists(): + candidates.extend(base.rglob("*.cmake")) + + for directory in ("src", "test"): + base = root / directory + if base.exists(): + candidates.extend(base.rglob("CMakeLists.txt")) + + dep_files: list[Path] = [] + for path in candidates: + if not path.is_file(): + continue + if path.name == "build-config.json": + dep_files.append(path) + continue + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + if declaration_re.search(text): + dep_files.append(path) + + digest = hashlib.sha256() + rel_paths = sorted({path.relative_to(root).as_posix() for path in dep_files}) + for rel_path in rel_paths: + path = root / rel_path + try: + content = path.read_bytes() + except OSError: + continue + digest.update(rel_path.encode("utf-8")) + digest.update(b"\0") + digest.update(content) + digest.update(b"\0") + return digest.hexdigest() + + +def resolve_windows_binary_config(host: str, target: str) -> dict[str, str]: + """Return Windows ccache binary arch and checksum for the requested target.""" + arch = "aarch64" if host == "windows_arm64" else "x86_64" + if target != "android" and host != "windows_arm64": + arch = "x86_64" + return {"arch": arch, "sha256": WINDOWS_BINARY_SHA256[arch]} + + +def install_windows_binary(version: str, arch: str, sha256: str, runner_temp: Path) -> Path: + """Download and install the ccache Windows binary under ``runner_temp``.""" + ccache_url = ( + f"https://github.com/ccache/ccache/releases/download/v{version}/" + f"ccache-{version}-windows-{arch}.zip" + ) + install_dir = runner_temp / f"ccache-{version}-windows-{arch}" + install_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + archive_path = temp_path / "ccache.zip" + _download_file(ccache_url, archive_path) + actual = hashlib.sha256(archive_path.read_bytes()).hexdigest() + if actual != sha256: + raise RuntimeError(f"SHA256 mismatch: {actual} != {sha256}") + + with zipfile.ZipFile(archive_path) as archive: + archive.extractall(temp_path) + + source = temp_path / f"ccache-{version}-windows-{arch}" / "ccache.exe" + if not source.exists(): + raise FileNotFoundError(f"ccache.exe not found in downloaded archive for {arch}") + shutil.copy2(source, install_dir / "ccache.exe") + return install_dir + + +def add_windows_binary_to_path(version: str, arch: str, runner_temp: Path) -> Path: + """Add the installed Windows ccache binary directory to PATH.""" + install_dir = runner_temp / f"ccache-{version}-windows-{arch}" + binary = install_dir / "ccache.exe" + if not binary.exists(): + raise FileNotFoundError(f"ccache.exe not found at {binary}") + append_github_path(str(install_dir)) + result = subprocess.run([str(binary), "--version"], capture_output=True, text=True, check=False) + if result.stdout: + print(result.stdout.strip()) + return install_dir + + +def configure_ccache_environment(workspace: Path) -> Path: + """Configure standard ccache environment variables for GitHub Actions.""" + workspace_path = Path(str(workspace).replace("\\", "/")) + ccache_dir = workspace_path / ".ccache" + ccache_dir.mkdir(parents=True, exist_ok=True) + append_github_env( + { + "CCACHE_DIR": ccache_dir.as_posix(), + "CCACHE_BASEDIR": workspace_path.as_posix(), + "CCACHE_CONFIGPATH": (workspace_path / "tools" / "configs" / "ccache.conf").as_posix(), + "CCACHE_MAXSIZE": "2G", + } + ) + return ccache_dir + + +def configure_cpm_cache(path_value: str) -> Path: + """Normalize and create the CPM cache path and export it.""" + cache_path = Path(path_value.replace("\\", "/")) + cache_path.mkdir(parents=True, exist_ok=True) + posix_path = cache_path.as_posix() + append_github_env({"CPM_SOURCE_CACHE": posix_path}) + write_github_outputs({"path": posix_path}) + return cache_path + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +TARGET_ARCH_MAP: dict[str, str] = { + "linux_gcc_arm64": "aarch64", +} + + +def resolve_arch(args: argparse.Namespace) -> str | None: + """Return architecture from --arch, --target, or None for auto-detect.""" + if getattr(args, "arch", None): + return args.arch + target = getattr(args, "target", None) + if target: + return TARGET_ARCH_MAP.get(target) + return None + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Ccache helper for CI: install, configure, and report", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + sub = parser.add_subparsers(dest="command") + + # -- config -------------------------------------------------------- + cfg = sub.add_parser("config", help="Output ccache configuration for GitHub Actions") + cfg.add_argument( + "--version", + default=CcacheInstaller.DEFAULT_VERSION, + help=f"ccache version (default: {CcacheInstaller.DEFAULT_VERSION})", + ) + cfg_arch = cfg.add_mutually_exclusive_group() + cfg_arch.add_argument( + "--arch", + choices=["x86_64", "aarch64"], + help="Architecture (default: auto-detect)", + ) + cfg_arch.add_argument( + "--target", + metavar="CI_TARGET", + help="CI build target — resolved to arch (e.g. linux_gcc_arm64 → aarch64)", + ) + cfg.add_argument( + "--conf", + type=Path, + metavar="PATH", + help="Path to ccache.conf for max_size", + ) + + # -- install ------------------------------------------------------- + inst = sub.add_parser("install", help="Install a pinned ccache binary (Linux)") + inst.add_argument( + "--version", + default=CcacheInstaller.DEFAULT_VERSION, + help=f"ccache version (default: {CcacheInstaller.DEFAULT_VERSION})", + ) + inst_arch = inst.add_mutually_exclusive_group() + inst_arch.add_argument( + "--arch", + choices=["x86_64", "aarch64"], + help="Architecture (default: auto-detect)", + ) + inst_arch.add_argument( + "--target", + metavar="CI_TARGET", + help="CI build target — resolved to arch (e.g. linux_gcc_arm64 → aarch64)", + ) + inst.add_argument( + "--prefix", + type=Path, + default=None, + help="Installation prefix (default: $CCACHE_PREFIX or /usr/local)", + ) + + # -- summary ------------------------------------------------------- + sub.add_parser("summary", help="Write cache hit/miss summary to GitHub Step Summary") + + # -- scope --------------------------------------------------------- + scope = sub.add_parser("scope", help="Compute the workflow cache scope") + scope.add_argument("--event-name", required=True, help="GitHub event name") + scope.add_argument("--ref-name", required=True, help="Git ref name") + scope.add_argument("--pr-number", default="", help="Pull request number") + + # -- fingerprint --------------------------------------------------- + fingerprint = sub.add_parser("fingerprint", help="Compute CPM dependency fingerprint") + fingerprint.add_argument("--root", type=Path, default=Path("."), help="Repository root") + + # -- windows-config ------------------------------------------------ + windows_cfg = sub.add_parser("windows-config", help="Resolve Windows ccache binary metadata") + windows_cfg.add_argument("--host", required=True, help="Workflow host input") + windows_cfg.add_argument("--target", required=True, help="Workflow target input") + + install_win = sub.add_parser("install-windows", help="Download and install Windows ccache binary") + install_win.add_argument("--version", required=True) + install_win.add_argument("--arch", required=True, choices=["x86_64", "aarch64"]) + install_win.add_argument("--sha256", required=True) + install_win.add_argument("--runner-temp", type=Path, required=True) + + add_win = sub.add_parser("add-windows-path", help="Add Windows ccache install dir to PATH") + add_win.add_argument("--version", required=True) + add_win.add_argument("--arch", required=True, choices=["x86_64", "aarch64"]) + add_win.add_argument("--runner-temp", type=Path, required=True) + + env_cfg = sub.add_parser("configure-env", help="Configure ccache GitHub environment variables") + env_cfg.add_argument("--workspace", type=Path, required=True) + + cpm_cfg = sub.add_parser("configure-cpm-cache", help="Configure CPM source cache path") + cpm_cfg.add_argument("--path", required=True) + + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Main entry point.""" + args = parse_args(argv) + + if args.command == "config": + if not CcacheInstaller.validate_version(args.version): + print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) + return 1 + + installer = CcacheInstaller( + version=args.version, + arch=resolve_arch(args), + config_path=args.conf, + ) + config = installer.get_config() + + print("ccache configuration:") + print(f" Version: {config.version}") + print(f" Arch: {config.arch}") + print(f" Max Size: {config.max_size}") + + output_github_actions(config) + return 0 + + if args.command == "install": + if not CcacheInstaller.validate_version(args.version): + print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) + return 1 + + installer = CcacheInstaller( + version=args.version, + arch=resolve_arch(args), + prefix=args.prefix, + ) + + config = installer.get_config() + print("ccache configuration:") + print(f" Version: {config.version}") + print(f" Arch: {config.arch}") + print(f" Max Size: {config.max_size}") + + if not installer.run_install(): + return 1 + return 0 + + if args.command == "summary": + return run_summary() + + if args.command == "scope": + scope = determine_cache_scope(args.event_name, args.ref_name, args.pr_number) + print(scope) + write_github_outputs({"scope": scope}) + return 0 + + if args.command == "fingerprint": + fingerprint = compute_cpm_fingerprint(args.root.resolve()) + print(fingerprint) + write_github_outputs({"fingerprint": fingerprint}) + return 0 + + if args.command == "windows-config": + values = resolve_windows_binary_config(args.host, args.target) + print(f"arch={values['arch']}") + print(f"sha256={values['sha256']}") + write_github_outputs(values) + return 0 + + if args.command == "install-windows": + install_dir = install_windows_binary(args.version, args.arch, args.sha256, args.runner_temp) + print(install_dir) + write_github_outputs({"install_dir": str(install_dir)}) + return 0 + + if args.command == "add-windows-path": + install_dir = add_windows_binary_to_path(args.version, args.arch, args.runner_temp) + print(install_dir) + return 0 + + if args.command == "configure-env": + ccache_dir = configure_ccache_environment(args.workspace) + print(ccache_dir) + return 0 + + if args.command == "configure-cpm-cache": + cache_path = configure_cpm_cache(args.path) + print(cache_path) + return 0 + + print( + "Error: a subcommand is required (config, install, summary, scope, fingerprint, windows-config, install-windows, add-windows-path, configure-env, configure-cpm-cache)", + file=sys.stderr, + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/check_baseline_ready.py b/.github/scripts/check_baseline_ready.py index 0db71e473e3f..5d7c5c34a465 100644 --- a/.github/scripts/check_baseline_ready.py +++ b/.github/scripts/check_baseline_ready.py @@ -4,18 +4,15 @@ from __future__ import annotations import argparse -import hashlib import json import os -import sys -from pathlib import Path from typing import Any -_SCRIPT_DIR = Path(__file__).resolve().parent -if str(_SCRIPT_DIR) not in sys.path: - sys.path.insert(0, str(_SCRIPT_DIR)) +from ci_bootstrap import ensure_tools_dir -from workflow_runs import list_workflow_runs, parse_csv_list +ensure_tools_dir(__file__) + +from common.gh_actions import list_workflow_runs_for_sha, parse_csv_list, write_github_output def evaluate_readiness( @@ -53,21 +50,6 @@ def evaluate_readiness( return ready, missing, incomplete, failed -def write_output(key: str, value: str) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - if "\n" in value: - value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] - delim = f"EOF_{key}_{value_hash}" - while delim in value: - delim = f"{delim}_X" - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}<<{delim}\n{value}\n{delim}\n") - else: - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}={value}\n") - def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Check baseline readiness for build-results workflow.") @@ -94,7 +76,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv) platforms = parse_csv_list(args.platform_workflows) - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) if args.runs_cache: with open(args.runs_cache, "w", encoding="utf-8") as f: @@ -102,10 +84,12 @@ def main(argv: list[str] | None = None) -> int: ready, missing, incomplete, failed = evaluate_readiness(runs, platforms, args.event) - write_output("ready", "true" if ready else "false") - write_output("missing", ",".join(missing)) - write_output("incomplete", ",".join(incomplete)) - write_output("failed", ",".join(failed)) + write_github_output({ + "ready": "true" if ready else "false", + "missing": ",".join(missing), + "incomplete": ",".join(incomplete), + "failed": ",".join(failed), + }) if ready: print(f"Baseline ready for {args.head_sha}.") diff --git a/.github/scripts/ci_bootstrap.py b/.github/scripts/ci_bootstrap.py new file mode 100644 index 000000000000..a6e33b60138d --- /dev/null +++ b/.github/scripts/ci_bootstrap.py @@ -0,0 +1,16 @@ +"""Bootstrap helper for GitHub CI scripts that import from tools/.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def ensure_tools_dir(start: str | Path) -> Path: + """Ensure the repository's tools directory is importable.""" + path = Path(start).resolve() + repo_root = path.parents[2] + tools_dir = repo_root / "tools" + if str(tools_dir) not in sys.path: + sys.path.insert(0, str(tools_dir)) + return tools_dir diff --git a/.github/scripts/cmake_helper.py b/.github/scripts/cmake_helper.py new file mode 100644 index 000000000000..4a27cd0d98b3 --- /dev/null +++ b/.github/scripts/cmake_helper.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""CMake build and configure helpers for CI. + +Subcommands: + build Run cmake --build with CI-friendly output and timing + configure Run configure.py with standardized arguments + detect-jobs Detect number of parallel jobs for the current platform + ctest Run CTest with standardized arguments and timing +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import time + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import append_github_env, write_github_output # noqa: E402 + + +def detect_jobs(requested: str = "auto") -> int: + """Detect number of parallel jobs for the current platform.""" + if requested != "auto": + if not re.match(r"^[1-9]\d*$", requested): + print(f"::error::Invalid parallel job count: {requested}", file=sys.stderr) + sys.exit(1) + return int(requested) + + try: + return os.cpu_count() or 2 + except Exception: + return 2 + + +def cmd_detect_jobs(args: argparse.Namespace) -> None: + jobs = detect_jobs(args.parallel) + write_github_output({"jobs": str(jobs)}) + + +def cmd_build(args: argparse.Namespace) -> None: + """Run cmake --build with CI-friendly output.""" + cmd = ["cmake", "--build", "."] + + if args.target: + cmd += ["--target", args.target] + if args.build_type: + cmd += ["--config", args.build_type] + if args.parallel: + if args.parallel_jobs: + if not re.match(r"^[1-9]\d*$", args.parallel_jobs): + print(f"::error::parallel-jobs must be a positive integer, got '{args.parallel_jobs}'", + file=sys.stderr) + sys.exit(1) + cmd += ["--parallel", args.parallel_jobs] + else: + cmd.append("--parallel") + + output_file = args.output_file + if args.reviewdog and not output_file: + output_file = os.path.join(os.environ.get("RUNNER_TEMP", "."), "build-output.log") + + if output_file and args.reviewdog: + append_github_env({"REVIEWDOG_LOG": output_file}) + + print(f"Running: {' '.join(cmd)}") + start = time.monotonic() + + if output_file: + with open(output_file, "w") as log: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in proc.stdout: + sys.stdout.write(line) + log.write(line) + proc.wait() + exit_code = proc.returncode + else: + result = subprocess.run(cmd, check=False) + exit_code = result.returncode + + duration = int(time.monotonic() - start) + + if exit_code == 0: + print(f"::notice::Build completed in {duration}s") + else: + print(f"::error::Build failed after {duration}s") + if not args.continue_on_error: + sys.exit(exit_code) + + +def cmd_configure(args: argparse.Namespace) -> None: + """Run configure.py with standardized arguments.""" + workspace = os.environ.get("GITHUB_WORKSPACE", ".") + cmd = [ + sys.executable, os.path.join(workspace, "tools", "configure.py"), + "-S", args.source_dir, + "-B", args.build_dir, + "-G", args.generator, + "-t", args.build_type, + ] + if args.testing: + cmd.append("--testing") + if args.coverage: + cmd.append("--coverage") + if args.stable: + cmd.append("--stable") + if not args.use_qt_cmake: + cmd.append("--no-qt-cmake") + if args.unity_build: + cmd += ["--unity", "--unity-batch", args.unity_batch_size] + if args.extra_args: + cmd.append("--") + cmd.extend(args.extra_args.split()) + + start = time.monotonic() + result = subprocess.run(cmd, check=False) + duration = int(time.monotonic() - start) + print(f"::notice::Configure completed in {duration}s") + sys.exit(result.returncode) + + +def cmd_ctest(args: argparse.Namespace) -> None: + """Run CTest with standardized arguments and timing.""" + cmd = [ + "ctest", + "--output-on-failure", + "--output-junit", args.junit_output, + "--parallel", str(args.jobs), + ] + if args.include_labels: + cmd += ["-L", args.include_labels] + if args.exclude_labels: + cmd += ["-LE", args.exclude_labels] + + start = time.monotonic() + + with open(args.ctest_output, "w") as log: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + for line in proc.stdout: + sys.stdout.write(line) + log.write(line) + proc.wait() + exit_code = proc.returncode + + duration = int(time.monotonic() - start) + print(f"::notice::Tests completed in {duration}s") + sys.exit(exit_code) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + # detect-jobs + p_jobs = sub.add_parser("detect-jobs") + p_jobs.add_argument("--parallel", default="auto") + + # build + p_build = sub.add_parser("build") + p_build.add_argument("--target", default="") + p_build.add_argument("--build-type", default="") + p_build.add_argument("--parallel", action="store_true", default=False) + p_build.add_argument("--parallel-jobs", default="") + p_build.add_argument("--output-file", default="") + p_build.add_argument("--continue-on-error", action="store_true", default=False) + p_build.add_argument("--reviewdog", action="store_true", default=False) + + # configure + p_conf = sub.add_parser("configure") + p_conf.add_argument("--source-dir", required=True) + p_conf.add_argument("--build-dir", required=True) + p_conf.add_argument("--generator", default="Ninja") + p_conf.add_argument("--build-type", default="Release") + p_conf.add_argument("--testing", action="store_true", default=False) + p_conf.add_argument("--coverage", action="store_true", default=False) + p_conf.add_argument("--stable", action="store_true", default=False) + p_conf.add_argument("--use-qt-cmake", action="store_true", default=True) + p_conf.add_argument("--no-qt-cmake", action="store_false", dest="use_qt_cmake") + p_conf.add_argument("--unity-build", action="store_true", default=False) + p_conf.add_argument("--unity-batch-size", default="16") + p_conf.add_argument("--extra-args", default="") + + # ctest + p_ctest = sub.add_parser("ctest") + p_ctest.add_argument("--junit-output", required=True) + p_ctest.add_argument("--ctest-output", required=True) + p_ctest.add_argument("--jobs", type=int, required=True) + p_ctest.add_argument("--include-labels", default="") + p_ctest.add_argument("--exclude-labels", default="") + + args = parser.parse_args() + commands = { + "detect-jobs": cmd_detect_jobs, + "build": cmd_build, + "configure": cmd_configure, + "ctest": cmd_ctest, + } + commands[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/collect_artifact_sizes.py b/.github/scripts/collect_artifact_sizes.py index 64c1fbbce3f8..20d9baf98855 100644 --- a/.github/scripts/collect_artifact_sizes.py +++ b/.github/scripts/collect_artifact_sizes.py @@ -5,13 +5,17 @@ import argparse import concurrent.futures -from datetime import datetime import json -from pathlib import Path import sys +from pathlib import Path from typing import Any -from workflow_runs import list_run_artifacts, list_workflow_runs, parse_csv_list +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import list_run_artifacts, list_workflow_runs_for_sha, parse_csv_list +from common.github_runs import select_latest_runs_by_name _DISTRIBUTABLE_PREFIXES = ( @@ -39,48 +43,19 @@ def _is_distributable_artifact(name: str) -> bool: return False return any(name.startswith(prefix) for prefix in _DISTRIBUTABLE_PREFIXES) - -def _parse_created_at(created_at: Any) -> datetime | None: - value = str(created_at).strip() - if not value: - return None - if value.endswith("Z"): - value = f"{value[:-1]}+00:00" - try: - return datetime.fromisoformat(value) - except ValueError: - return None - - -def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: - candidate_created_at = str(candidate.get("created_at", "")) - existing_created_at = str(existing.get("created_at", "")) - candidate_dt = _parse_created_at(candidate_created_at) - existing_dt = _parse_created_at(existing_created_at) - if candidate_dt is not None and existing_dt is not None: - return candidate_dt > existing_dt - return candidate_created_at > existing_created_at - - def latest_successful_runs( runs: list[dict[str, Any]], platforms: list[str], + *, + event: str = "", ) -> dict[str, dict[str, Any]]: - target = set(platforms) - latest: dict[str, dict[str, Any]] = {} - - for run in runs: - name = str(run.get("name", "")) - if name not in target: - continue - if str(run.get("status", "")) != "completed" or str(run.get("conclusion", "")) != "success": - continue - - existing = latest.get(name) - if existing is None or _is_newer_run(run, existing): - latest[name] = run - - return latest + return select_latest_runs_by_name( + runs, + set(platforms), + event=event, + status="completed", + conclusion="success", + ) def format_size_human(size_bytes: int) -> str: @@ -167,6 +142,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: default="Linux,Windows,MacOS,Android", help="Comma-separated platform workflow names", ) + parser.add_argument( + "--event", + default="", + choices=["", "push", "pull_request", "workflow_dispatch", "schedule"], + help="Optional workflow event name to filter runs by", + ) parser.add_argument( "--output-file", default="artifact-sizes.json", @@ -188,6 +169,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: def main(argv: list[str] | None = None) -> int: args = parse_args(argv) platforms = parse_csv_list(args.platform_workflows) + event = str(args.event).strip() if args.runs_file: try: @@ -204,7 +186,7 @@ def main(argv: list[str] | None = None) -> int: return 1 runs = loaded_runs else: - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) artifacts_by_run_id: dict[int, list[dict[str, Any]]] = {} if args.artifacts_file: artifacts_path = Path(args.artifacts_file) @@ -225,7 +207,7 @@ def main(argv: list[str] | None = None) -> int: artifact for artifact in run_artifacts if isinstance(artifact, dict) ] - latest = latest_successful_runs(runs, platforms) + latest = latest_successful_runs(runs, platforms, event=event) artifacts = collect_artifacts( args.repo, latest, diff --git a/.github/scripts/collect_build_status.py b/.github/scripts/collect_build_status.py index 76a65cfd559e..fea4839964bc 100644 --- a/.github/scripts/collect_build_status.py +++ b/.github/scripts/collect_build_status.py @@ -4,12 +4,16 @@ from __future__ import annotations import argparse -import hashlib import json import os from typing import Any -from workflow_runs import list_workflow_runs, parse_csv_list +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import list_workflow_runs_for_sha, parse_csv_list, write_github_output +from common.github_runs import select_latest_runs_by_name def latest_runs_by_name( @@ -17,17 +21,7 @@ def latest_runs_by_name( names: set[str], event: str, ) -> dict[str, dict[str, Any]]: - latest: dict[str, dict[str, Any]] = {} - for run in runs: - name = str(run.get("name", "")) - if name not in names: - continue - if str(run.get("event", "")) != event: - continue - existing = latest.get(name) - if existing is None or str(run.get("created_at", "")) > str(existing.get("created_at", "")): - latest[name] = run - return latest + return select_latest_runs_by_name(runs, names, event=event) def platform_status( @@ -74,22 +68,6 @@ def render_table(platforms: list[str], states: dict[str, dict[str, str]]) -> str return "\n".join(lines) -def write_output(key: str, value: str) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - if "\n" in value: - value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] - delim = f"EOF_{key}_{value_hash}" - while delim in value: - delim = f"{delim}_X" - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}<<{delim}\n{value}\n{delim}\n") - else: - with open(github_output, "a", encoding="utf-8") as f: - f.write(f"{key}={value}\n") - def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser(description="Collect build status for PR build-results comment.") @@ -119,7 +97,7 @@ def main(argv: list[str] | None = None) -> int: target_names = set(platforms) target_names.add("pre-commit") - runs = list_workflow_runs(args.repo, args.head_sha) + runs = list_workflow_runs_for_sha(args.repo, args.head_sha) if args.runs_cache: with open(args.runs_cache, "w", encoding="utf-8") as f: @@ -138,13 +116,15 @@ def main(argv: list[str] | None = None) -> int: "Some builds failed." if all_complete else "Some builds still in progress." ) - write_output("table", table) - write_output("summary", summary) - write_output("all_complete", "true" if all_complete else "false") - write_output("precommit_status", precommit["status"]) - write_output("precommit_url", precommit["url"]) - write_output("precommit_conclusion", precommit["conclusion"]) - write_output("precommit_run_id", precommit["run_id"]) + write_github_output({ + "table": table, + "summary": summary, + "all_complete": "true" if all_complete else "false", + "precommit_status": precommit["status"], + "precommit_url": precommit["url"], + "precommit_conclusion": precommit["conclusion"], + "precommit_run_id": precommit["run_id"], + }) print(table) print(summary) diff --git a/.github/scripts/coverage_comment.py b/.github/scripts/coverage_comment.py index 4ee43e3aabdb..9c16e5430b88 100755 --- a/.github/scripts/coverage_comment.py +++ b/.github/scripts/coverage_comment.py @@ -6,24 +6,7 @@ import sys from pathlib import Path -try: - from defusedxml.ElementTree import ParseError as XMLParseError - from defusedxml.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = True -except ImportError: - from xml.etree.ElementTree import ParseError as XMLParseError - from xml.etree.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = False - - -def xml_parse(path: str): - if _USING_DEFUSEDXML: - return _xml_parse_impl(path) - # Harden stdlib fallback: reject XML with DTD/entities. - text = Path(path).read_text(encoding="utf-8", errors="replace") - if " dict | None: diff --git a/.github/scripts/deploy_docs.py b/.github/scripts/deploy_docs.py new file mode 100644 index 000000000000..2e19fd85e474 --- /dev/null +++ b/.github/scripts/deploy_docs.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Deploy built documentation to an external GitHub Pages repository.""" + +from __future__ import annotations + +import argparse +import datetime +import re +import shutil +import subprocess +import sys +from pathlib import Path + + +def sanitize_branch(name: str) -> str: + """Remove unsafe characters from branch name for use as a directory.""" + return re.sub(r"[^a-zA-Z0-9._-]", "_", name) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source-dir", required=True, help="Path to built docs") + parser.add_argument("--target-dir", required=True, help="Path to target repo checkout") + parser.add_argument("--branch", required=True, help="Source branch name (used as subdirectory)") + parser.add_argument("--target-branch", default="main", help="Branch to push to") + parser.add_argument("--commit-message", default="Docs update") + parser.add_argument("--author-email", default="bot@px4.io") + parser.add_argument("--author-name", default="PX4BuildBot") + args = parser.parse_args() + + safe_branch = sanitize_branch(args.branch) + target = Path(args.target_dir) + deploy_dir = target / safe_branch + + if deploy_dir.exists(): + shutil.rmtree(deploy_dir) + deploy_dir.mkdir(parents=True) + + source = Path(args.source_dir) + for item in source.iterdir(): + dest = deploy_dir / item.name + if item.is_dir(): + shutil.copytree(item, dest) + else: + shutil.copy2(item, dest) + + def git(*cmd: str) -> subprocess.CompletedProcess: + return subprocess.run(["git", *cmd], cwd=str(target), check=True) + + git("config", "user.email", args.author_email) + git("config", "user.name", args.author_name) + git("add", safe_branch) + + diff = subprocess.run( + ["git", "diff", "--cached", "--quiet"], + cwd=str(target), check=False, + ) + if diff.returncode == 0: + print("No documentation changes to deploy.") + return + + today = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d") + git("commit", "-m", f"{args.commit_message} {today}") + git("push", "origin", args.target_branch) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/detect_changes.py b/.github/scripts/detect_changes.py new file mode 100644 index 000000000000..aca58e2ed447 --- /dev/null +++ b/.github/scripts/detect_changes.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Detect whether a CI build is needed based on changed files and platform. + +Computes the git diff for the current event (pull_request, push, merge_group) +and matches changed files against platform-specific patterns. + +Usage: + python3 .github/scripts/detect_changes.py --platform linux +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from typing import Sequence + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output + +# Patterns that trigger a build for ANY platform +_COMMON_PATTERNS: list[str] = [ + r"^src/", + r"^CMakeLists\.txt$", + r"^cmake/", + r"^libs/", + r"^resources/", + r"^translations/", + r"^.*\.qrc$", + r"^test/", + r"^\.github/actions/", + r"^\.github/scripts/", + r"^\.github/build-config\.json$", +] + +# Per-platform additional patterns +_PLATFORM_PATTERNS: dict[str, list[str]] = { + "android": [r"^android/"], + "docker-linux": [ + r"^deploy/docker/Dockerfile-build-ubuntu$", + r"^deploy/linux/", + ], + "docker-android": [ + r"^deploy/docker/Dockerfile-build-android$", + r"^android/", + r"^deploy/android/", + ], +} + +# Per-platform tools/setup patterns +_SETUP_PATTERNS: dict[str, list[str]] = { + "linux": [r"^tools/setup/.*debian", r"^tools/setup/install_dependencies\.py$"], + "windows": [r"^tools/setup/.*windows"], + "macos": [r"^tools/setup/.*macos"], + "android": [r"^tools/setup/"], + "ios": [r"^tools/setup/.*ios", r"^tools/setup/.*macos"], + "docker-linux": [r"^tools/setup/"], + "docker-android": [r"^tools/setup/"], +} + + +def workflow_name_for_platform(platform: str) -> str: + """Map a platform to its workflow YAML filename (without extension).""" + if platform.startswith("docker-"): + return "docker" + return platform + + +def build_patterns(platform: str) -> list[re.Pattern[str]]: + """Build the list of compiled regex patterns for a platform.""" + wf = workflow_name_for_platform(platform) + raw: list[str] = list(_COMMON_PATTERNS) + raw.append(rf"^deploy/{re.escape(platform)}/") + raw.append(rf"^\.github/workflows/{re.escape(wf)}\.yml$") + raw.extend(_PLATFORM_PATTERNS.get(platform, [])) + raw.extend(_SETUP_PATTERNS.get(platform, [])) + return [re.compile(p) for p in raw] + + +def has_relevant_changes(files: Sequence[str], platform: str) -> bool: + """Check if any changed file matches the platform's patterns.""" + patterns = build_patterns(platform) + for f in files: + if not f: + continue + for p in patterns: + if p.search(f): + return True + return False + + +# --------------------------------------------------------------------------- +# Git helpers — these call out to git as subprocesses +# --------------------------------------------------------------------------- + +_NULL_SHA = "0" * 40 + + +def _run_git(*args: str, check: bool = True) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + capture_output=True, + text=True, + check=check, + ) + + +def _ensure_commit(sha: str) -> bool: + """Ensure a commit exists locally; fetch it shallowly if needed.""" + if not sha: + return True + r = _run_git("cat-file", "-e", f"{sha}^{{commit}}", check=False) + if r.returncode == 0: + return True + _run_git("fetch", "--no-tags", "--depth=1", "origin", sha, check=False) + return _run_git("cat-file", "-e", f"{sha}^{{commit}}", check=False).returncode == 0 + + +def _diff_names(sha_a: str, sha_b: str) -> list[str] | None: + """Return changed file names between two commits, or None on failure.""" + r = _run_git("diff", "--name-only", sha_a, sha_b, check=False) + if r.returncode != 0: + return None + return r.stdout.strip().splitlines() + + +def _tree_names(sha: str) -> list[str]: + """List files changed in a single commit.""" + r = _run_git("diff-tree", "--no-commit-id", "--name-only", "-r", sha, check=False) + return r.stdout.strip().splitlines() if r.returncode == 0 else [] + + +def get_changed_files() -> list[str] | None: + """Determine changed files from environment variables set by GitHub Actions. + + Returns None if the diff cannot be computed reliably (caller should force build). + """ + event = os.environ.get("EVENT_NAME", "") + + if event == "pull_request": + base = os.environ.get("PR_BASE_SHA", "") + head = os.environ.get("PR_HEAD_SHA", "") + if not _ensure_commit(base) or not _ensure_commit(head): + return None + return _diff_names(base, head) + + if event == "push": + before = os.environ.get("PUSH_BEFORE_SHA", "") + current = os.environ.get("CURRENT_SHA", "") + if before and before != _NULL_SHA: + if not _ensure_commit(before) or not _ensure_commit(current): + return None + return _diff_names(before, current) + return _tree_names(current) + + if event == "merge_group": + merge_base = os.environ.get("MERGE_BASE_SHA", "") + current = os.environ.get("CURRENT_SHA", "") + if merge_base: + if not _ensure_commit(merge_base) or not _ensure_commit(current): + return None + return _diff_names(merge_base, current) + + current = os.environ.get("CURRENT_SHA", "") + return _tree_names(current) if current else None + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Detect CI-relevant file changes for a platform.") + parser.add_argument("--platform", required=True, + help="Platform (linux, windows, macos, android, ios, docker-linux, docker-android)") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + platform = args.platform + + changed = get_changed_files() + if changed is None: + print("::warning::Unable to compute changed files reliably; forcing build for safety.") + write_github_output({"any": "true"}) + return 0 + + print("Changed files:") + for f in changed: + print(f" {f}") + + any_changed = has_relevant_changes(changed, platform) + write_github_output({"any": "true" if any_changed else "false"}) + print(f"Results: any={any_changed}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/docker_helper.py b/.github/scripts/docker_helper.py new file mode 100644 index 000000000000..e782519b1594 --- /dev/null +++ b/.github/scripts/docker_helper.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Docker build helpers for CI. + +Subcommands: + validate Validate dockerfile and build-type inputs + run Run docker build with retry logic +""" + +from __future__ import annotations + +import argparse +import subprocess +import sys +import time + +VALID_DOCKERFILES = {"Dockerfile-build-ubuntu", "Dockerfile-build-android"} +VALID_BUILD_TYPES = {"Release", "Debug"} + + +def cmd_validate(args: argparse.Namespace) -> None: + ok = True + if args.dockerfile not in VALID_DOCKERFILES: + print(f"::error::Invalid dockerfile '{args.dockerfile}'", file=sys.stderr) + print(f"Allowed values: {', '.join(sorted(VALID_DOCKERFILES))}", file=sys.stderr) + ok = False + if args.build_type not in VALID_BUILD_TYPES: + print(f"::error::Invalid build-type '{args.build_type}'", file=sys.stderr) + print(f"Allowed values: {', '.join(sorted(VALID_BUILD_TYPES))}", file=sys.stderr) + ok = False + if not ok: + sys.exit(1) + print(f"Using dockerfile: {args.dockerfile}") + print(f"Build type: {args.build_type}") + + +def cmd_run(args: argparse.Namespace) -> None: + cmd = ["./deploy/docker/docker-run.sh"] + if args.fuse: + cmd.append("--fuse") + cmd += [args.image, args.build_type] + + for attempt in range(1, args.max_attempts + 1): + result = subprocess.run(cmd, check=False) + if result.returncode == 0: + return + if attempt >= args.max_attempts: + print(f"::error::Docker build failed after {attempt} attempt(s).", file=sys.stderr) + sys.exit(1) + print(f"Docker build failed on attempt {attempt}. Retrying in {args.retry_delay} seconds...") + time.sleep(args.retry_delay) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + + p_val = sub.add_parser("validate") + p_val.add_argument("--dockerfile", required=True) + p_val.add_argument("--build-type", required=True) + + p_run = sub.add_parser("run") + p_run.add_argument("--image", default="qgc-builder:latest") + p_run.add_argument("--build-type", required=True) + p_run.add_argument("--fuse", action="store_true", default=False) + p_run.add_argument("--max-attempts", type=int, default=2) + p_run.add_argument("--retry-delay", type=int, default=30) + + args = parser.parse_args() + {"validate": cmd_validate, "run": cmd_run}[args.command](args) + + +if __name__ == "__main__": + main() diff --git a/tools/setup/download_artifacts.py b/.github/scripts/download_artifacts.py old mode 100755 new mode 100644 similarity index 79% rename from tools/setup/download_artifacts.py rename to .github/scripts/download_artifacts.py index 1a73d5b44765..812c4543aece --- a/tools/setup/download_artifacts.py +++ b/.github/scripts/download_artifacts.py @@ -5,7 +5,7 @@ from all successful platform builds for a given commit SHA. Usage: - python3 tools/setup/download_artifacts.py \\ + python3 .github/scripts/download_artifacts.py \\ --repo owner/repo \\ --head-sha abc123 \\ --output-dir artifacts \\ @@ -17,19 +17,17 @@ import argparse import concurrent.futures -from datetime import datetime import json -import subprocess import sys from pathlib import Path from typing import Any -REPO_ROOT = Path(__file__).resolve().parents[2] -COMMON_DIR = REPO_ROOT / "tools" / "common" -if str(COMMON_DIR) not in sys.path: - sys.path.insert(0, str(COMMON_DIR)) +from ci_bootstrap import ensure_tools_dir -import gh_actions as _gh_actions +ensure_tools_dir(__file__) + +from common.gh_actions import gh, list_workflow_runs_for_sha, list_run_artifacts +from common.github_runs import group_runs_by_name, select_latest_runs_by_name ARTIFACT_EXTENSIONS = ( @@ -45,51 +43,20 @@ ARTIFACT_EXTENSION_SET = frozenset(ARTIFACT_EXTENSIONS) -def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: - """Run a gh CLI command and return the result.""" - return _gh_actions.gh(*args, check=check) - - -def _parse_created_at(created_at: Any) -> datetime | None: - value = str(created_at).strip() - if not value: - return None - if value.endswith("Z"): - value = f"{value[:-1]}+00:00" - try: - return datetime.fromisoformat(value) - except ValueError: - return None - - -def _is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: - candidate_created_at = str(candidate.get("created_at", "")) - existing_created_at = str(existing.get("created_at", "")) - candidate_dt = _parse_created_at(candidate_created_at) - existing_dt = _parse_created_at(existing_created_at) - if candidate_dt is not None and existing_dt is not None: - return candidate_dt > existing_dt - return candidate_created_at > existing_created_at - - def select_latest_successful_runs( runs: list[dict[str, Any]], workflows: list[str], + *, + event: str = "", ) -> list[dict[str, Any]]: """Return latest completed/successful run per workflow name.""" - workflow_set = set(workflows) - latest_by_workflow: dict[str, dict[str, Any]] = {} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - existing = latest_by_workflow.get(name) - if existing is None or _is_newer_run(run, existing): - latest_by_workflow[name] = run - + latest_by_workflow = select_latest_runs_by_name( + runs, + set(workflows), + event=event, + status="completed", + conclusion="success", + ) order = {name: idx for idx, name in enumerate(workflows)} return sorted( latest_by_workflow.values(), @@ -100,40 +67,28 @@ def select_latest_successful_runs( def group_successful_runs_by_workflow( runs: list[dict[str, Any]], workflows: list[str], + *, + event: str = "", ) -> dict[str, list[dict[str, Any]]]: """Group successful runs by workflow name, newest first.""" - workflow_set = set(workflows) - grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in workflows} - - for run in runs: - name = str(run.get("name", "")) - if name not in workflow_set: - continue - if run.get("status") != "completed" or run.get("conclusion") != "success": - continue - grouped[name].append(run) - - for workflow_name in grouped: - grouped[workflow_name].sort( - key=lambda run: ( - _parse_created_at(run.get("created_at")) is not None, - str(run.get("created_at", "")), - ), - reverse=True, - ) - - return grouped + return group_runs_by_name( + runs, + workflows, + event=event, + status="completed", + conclusion="success", + ) -def get_workflow_runs(repo: str, head_sha: str, workflows: list[str]) -> list[dict]: +def get_workflow_runs(repo: str, head_sha: str, workflows: list[str], *, event: str = "") -> list[dict]: """Return completed successful runs for the given workflows and commit.""" - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) - return select_latest_successful_runs(all_runs, workflows) + all_runs = list_workflow_runs_for_sha(repo, head_sha) + return select_latest_successful_runs(all_runs, workflows, event=event) def select_artifact_names_for_run(repo: str, run_id: int, prefixes: list[str]) -> list[str]: """Return artifact names that match configured name prefixes.""" - return select_artifact_names(_gh_actions.list_run_artifacts(repo, run_id), prefixes) + return select_artifact_names(list_run_artifacts(repo, run_id), prefixes) def select_artifact_names(artifacts: list[dict[str, Any]], prefixes: list[str]) -> list[str]: @@ -213,6 +168,12 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: default="Linux,Windows,MacOS,Android", help="Comma-separated workflow names to download from (default: Linux,Windows,MacOS,Android)", ) + parser.add_argument( + "--event", + default="", + choices=["", "push", "pull_request", "workflow_dispatch", "schedule"], + help="Optional workflow event name to filter runs by", + ) parser.add_argument( "--runs-file", default="", @@ -241,6 +202,7 @@ def main(argv: list[str] | None = None) -> int: head_sha = args.head_sha output_dir = args.output_dir workflows = [w.strip() for w in args.workflows.split(",") if w.strip()] + event = str(args.event).strip() artifact_prefixes = [p.strip() for p in args.artifact_prefixes.split(",") if p.strip()] artifact_metadata_out = Path(args.artifact_metadata_out) if args.artifact_metadata_out else None @@ -265,18 +227,18 @@ def main(argv: list[str] | None = None) -> int: return 1 all_runs = loaded_runs else: - all_runs = _gh_actions.list_workflow_runs_for_sha(repo, head_sha) + all_runs = list_workflow_runs_for_sha(repo, head_sha) preloaded_artifacts: dict[int, list[dict[str, Any]]] = {} - had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows)) + had_successful_runs = bool(select_latest_successful_runs(all_runs, workflows, event=event)) if artifact_prefixes: runs = [] - grouped_runs = group_successful_runs_by_workflow(all_runs, workflows) + grouped_runs = group_successful_runs_by_workflow(all_runs, workflows, event=event) for workflow_name in workflows: candidates = grouped_runs.get(workflow_name, []) selected_run: dict[str, Any] | None = None for candidate in candidates: run_id = int(candidate["id"]) - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) preloaded_artifacts[run_id] = artifacts if select_artifact_names(artifacts, artifact_prefixes): selected_run = candidate @@ -284,7 +246,7 @@ def main(argv: list[str] | None = None) -> int: if selected_run is not None: runs.append(selected_run) else: - runs = select_latest_successful_runs(all_runs, workflows) + runs = select_latest_successful_runs(all_runs, workflows, event=event) if not runs: if artifact_prefixes and had_successful_runs: @@ -317,7 +279,7 @@ def main(argv: list[str] | None = None) -> int: if artifact_prefixes or artifact_metadata_out is not None: artifacts = preloaded_artifacts.get(run_id) if artifacts is None: - artifacts = _gh_actions.list_run_artifacts(repo, run_id) + artifacts = list_run_artifacts(repo, run_id) if artifact_prefixes: names = select_artifact_names(artifacts, artifact_prefixes) run_artifact_map[run_id] = names if names else None diff --git a/.github/scripts/find_binary.py b/.github/scripts/find_binary.py index 5ed8ab5cfd26..3799f90faadf 100644 --- a/.github/scripts/find_binary.py +++ b/.github/scripts/find_binary.py @@ -135,7 +135,7 @@ def output_github_actions(info: BinaryInfo) -> None: """Write outputs for GitHub Actions.""" github_output = os.environ.get("GITHUB_OUTPUT") if github_output: - with open(github_output, "a") as f: + with open(github_output, "a", encoding="utf-8") as f: f.write(f"binary_path={info.path}\n") f.write(f"binary_name={info.name}\n") f.write(f"binary_dir={info.directory}\n") diff --git a/.github/scripts/generate_build_results_comment.py b/.github/scripts/generate_build_results_comment.py index 1dc9c8febc99..b61d4ba2bf83 100644 --- a/.github/scripts/generate_build_results_comment.py +++ b/.github/scripts/generate_build_results_comment.py @@ -10,29 +10,16 @@ import re from datetime import UTC, datetime from pathlib import Path -from typing import Mapping +from typing import Any, Mapping from urllib.parse import quote, urlsplit, urlunsplit -try: - from defusedxml.ElementTree import ParseError as XMLParseError - from defusedxml.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = True -except ImportError: - from xml.etree.ElementTree import ParseError as XMLParseError - from xml.etree.ElementTree import parse as _xml_parse_impl - _USING_DEFUSEDXML = False +import jinja2 -logger = logging.getLogger(__name__) +from xml_utils import XMLParseError, xml_parse as _xml_parse +logger = logging.getLogger(__name__) -def _xml_parse(path: Path): - if _USING_DEFUSEDXML: - return _xml_parse_impl(path) - # Harden stdlib fallback: reject XML with DTD/entities. - text = path.read_text(encoding="utf-8", errors="replace") - if " str: @@ -121,101 +108,80 @@ def _failed_test_lines(content: str, limit: int = 20) -> list[str]: return lines -def _render_test_results(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _format_delta_mb(delta_bytes: int) -> str: + delta_mb = delta_bytes / 1024.0 / 1024.0 + if delta_bytes > 0: + return f"+{delta_mb:.2f} MB (increase)" + if delta_bytes < 0: + return f"{delta_mb:.2f} MB (decrease)" + return "No change" + + +def _format_size_human(size_bytes: int) -> str: + size_mb = size_bytes / 1024.0 / 1024.0 + if size_mb >= 1024: + return f"{(size_mb / 1024):.2f} GB" + return f"{size_mb:.2f} MB" + + +def _collect_test_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: pattern = _env(env, "TEST_RESULTS_GLOB", "artifacts/test-results-*/test-output.txt") files = sorted(base_dir.glob(pattern)) if not files: - return [] + return None - lines = ["### Test Results", ""] - total_passed = 0 - total_failed = 0 - total_skipped = 0 + platforms: list[dict[str, Any]] = [] + total_passed = total_failed = total_skipped = 0 has_failures = False for file in files: arch = file.parent.name.removeprefix("test-results-") content = file.read_text(encoding="utf-8", errors="ignore") passed, failed, skipped = _count_test_results(content) - total_passed += passed total_failed += failed total_skipped += skipped + entry: dict[str, Any] = {"arch": arch, "passed": passed, "failed": failed, "skipped": skipped} if failed > 0: has_failures = True - lines.append(f"**{arch}**: {passed} passed, {failed} failed, {skipped} skipped") - lines.append("") - lines.append("
Failed tests") - lines.append("") - lines.append("```") - lines.extend(_failed_test_lines(content)) - lines.append("```") - lines.append("
") - else: - lines.append(f"**{arch}**: {passed} passed, {skipped} skipped") - lines.append("") + entry["failed_lines"] = _failed_test_lines(content) + platforms.append(entry) - if has_failures: - lines.append(f"**Total: {total_passed} passed, {total_failed} failed, {total_skipped} skipped**") - else: - lines.append(f"**Total: {total_passed} passed, {total_skipped} skipped**") - lines.append("") - return lines + return { + "platforms": platforms, + "total_passed": total_passed, + "total_failed": total_failed, + "total_skipped": total_skipped, + "has_failures": has_failures, + } -def _render_coverage(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _collect_coverage_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: coverage_path = base_dir / _env(env, "COVERAGE_XML", "artifacts/coverage-report/coverage.xml") if not coverage_path.exists(): - return [] + return None - lines = ["### Code Coverage", ""] current = _parse_coverage_percent(coverage_path) baseline_path = base_dir / _env(env, "BASELINE_COVERAGE_XML", "baseline-coverage.xml") baseline = _parse_coverage_percent(baseline_path) - coverage_text = f"{current:.1f}%" if current is not None else "N/A" - + result: dict[str, Any] = {"current": current, "baseline": baseline} if baseline is not None and current is not None: - diff = current - baseline - lines.append("| Coverage | Baseline | Change |") - lines.append("|----------|----------|--------|") - lines.append(f"| {coverage_text} | {baseline:.1f}% | {diff:+.1f}% |") - else: - lines.append(f"Coverage: **{coverage_text}**") - lines.append("") - lines.append("*No baseline available for comparison*") - - lines.append("") - return lines - - -def _format_delta_mb(delta_bytes: int) -> str: - delta_mb = delta_bytes / 1024.0 / 1024.0 - if delta_bytes > 0: - return f"+{delta_mb:.2f} MB (increase)" - if delta_bytes < 0: - return f"{delta_mb:.2f} MB (decrease)" - return "No change" - - -def _format_size_human(size_bytes: int) -> str: - size_mb = size_bytes / 1024.0 / 1024.0 - if size_mb >= 1024: - return f"{(size_mb / 1024):.2f} GB" - return f"{size_mb:.2f} MB" + result["diff"] = current - baseline + return result -def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: +def _collect_artifact_data(base_dir: Path, env: Mapping[str, str]) -> dict[str, Any] | None: pr_sizes_path = base_dir / _env(env, "PR_SIZES_JSON", "pr-sizes.json") if not pr_sizes_path.exists(): - return [] + return None try: pr_data = json.loads(pr_sizes_path.read_text(encoding="utf-8")) except Exception: logger.debug("Failed to parse PR sizes from %s", pr_sizes_path, exc_info=True) - return [] + return None baseline_path = base_dir / _env(env, "BASELINE_SIZES_JSON", "baseline-sizes.json") baseline: dict[str, int] = {} @@ -236,19 +202,12 @@ def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: logger.debug("Failed to parse baseline sizes from %s", baseline_path, exc_info=True) baseline = {} - lines = ["### Artifact Sizes", ""] - if baseline: - lines.append("| Artifact | Size | Δ from master |") - lines.append("|----------|------|---------------|") - else: - lines.append("| Artifact | Size |") - lines.append("|----------|------|") - - total_delta = 0 artifacts = pr_data.get("artifacts", []) if not isinstance(artifacts, list): - return [] + return None + items: list[dict[str, Any]] = [] + total_delta = 0 for artifact in artifacts: if not isinstance(artifact, dict): continue @@ -261,21 +220,20 @@ def _render_artifact_sizes(base_dir: Path, env: Mapping[str, str]) -> list[str]: continue size_human = str(artifact.get("size_human", "")).strip() or _format_size_human(new_size) + entry: dict[str, Any] = {"name": name, "size_human": size_human} if baseline and name in baseline: - old_size = baseline[name] - delta = new_size - old_size + delta = new_size - baseline[name] total_delta += delta - lines.append(f"| {name} | {size_human} | {_format_delta_mb(delta)} |") - else: - lines.append(f"| {name} | {size_human} |") - - lines.append("") - if baseline and total_delta != 0: - direction = "increased" if total_delta > 0 else "decreased" - lines.append(f"**Total size {direction} by {abs(total_delta) / 1024.0 / 1024.0:.2f} MB**") - elif not baseline: - lines.append("*No baseline available for comparison*") - return lines + entry["delta"] = delta + entry["delta_human"] = _format_delta_mb(delta) + items.append(entry) + + return { + "entries": items, + "has_baseline": bool(baseline), + "total_delta": total_delta, + "total_delta_mb": abs(total_delta) / 1024.0 / 1024.0, + } def generate_comment(env: Mapping[str, str], base_dir: Path, now_utc: datetime | None = None) -> str: @@ -297,50 +255,41 @@ def generate_comment(env: Mapping[str, str], base_dir: Path, now_utc: datetime | if parsed_note: precommit_note = parsed_note - lines: list[str] = [ - "## Build Results", - "", - "### Platform Status", - "", - ] + now = now_utc or datetime.now(UTC) - if table: - lines.extend(table.strip().splitlines()) - else: - lines.extend(["| Platform | Status | Details |", "|----------|--------|--------|"]) - - lines.extend( - [ - "", - f"**{summary}**", - "", - "### Pre-commit", - "", - "| Check | Status | Details |", - "|-------|--------|---------|", - f"| pre-commit | {precommit_status} | {precommit_details} |", - "", - ] + jinja_env = jinja2.Environment( + loader=jinja2.FileSystemLoader(_TEMPLATE_DIR), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, ) - - if precommit_note: - lines.append(precommit_note) - lines.append("") - - lines.extend(_render_test_results(base_dir, env)) - lines.extend(_render_coverage(base_dir, env)) - lines.extend(_render_artifact_sizes(base_dir, env)) - - now = now_utc or datetime.now(UTC) - lines.extend( - [ - "", - "---", - f"Updated: {now.strftime('%Y-%m-%d %H:%M:%S UTC')} • Triggered by: {triggered_by}", - ] + template = jinja_env.get_template("build_results.md.j2") + + rendered = template.render( + table=table, + summary=summary, + precommit={"status": precommit_status, "details": precommit_details, "note": precommit_note}, + tests=_collect_test_data(base_dir, env), + coverage=_collect_coverage_data(base_dir, env), + artifacts=_collect_artifact_data(base_dir, env), + timestamp=now.strftime("%Y-%m-%d %H:%M:%S UTC"), + triggered_by=triggered_by, ) - return "\n".join(lines).rstrip() + "\n" + # Normalize: collapse 3+ blank lines to 2, strip trailing whitespace per line + lines = [line.rstrip() for line in rendered.splitlines()] + result: list[str] = [] + blank_count = 0 + for line in lines: + if not line: + blank_count += 1 + if blank_count <= 2: + result.append(line) + else: + blank_count = 0 + result.append(line) + + return "\n".join(result).rstrip() + "\n" def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/gstreamer_archive.py b/.github/scripts/gstreamer_archive.py index 84beaef7aa70..6a0c796208b2 100755 --- a/.github/scripts/gstreamer_archive.py +++ b/.github/scripts/gstreamer_archive.py @@ -20,6 +20,15 @@ from dataclasses import dataclass from pathlib import Path +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + @dataclass class ArchiveResult: @@ -258,18 +267,16 @@ def _install_aws_cli_macos(self) -> None: run_command(["sudo", "installer", "-pkg", str(installer_pkg), "-target", "/"]) def write_github_output(self) -> None: - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output or not self._archive_result: + if not self._archive_result: return - - with open(github_output, "a") as f: - f.write(f"name={self._archive_result.name}\n") - f.write(f"path={self._archive_result.path}\n") - f.write(f"ext={self._archive_result.extension}\n") + _write_github_output({ + "name": self._archive_result.name, + "path": str(self._archive_result.path), + "ext": self._archive_result.extension, + }) def write_github_summary(self, uploaded: bool = False) -> None: - summary_path = os.environ.get("GITHUB_STEP_SUMMARY") - if not summary_path or not self._archive_result: + if not self._archive_result: return arch_display = self._normalize_arch() @@ -294,8 +301,7 @@ def write_github_summary(self, uploaded: bool = False) -> None: f"| S3 Path | dependencies/gstreamer/{self.platform}/{self.version}/ |" ) - with open(summary_path, "a") as f: - f.write("\n".join(lines) + "\n") + _write_step_summary("\n".join(lines) + "\n") def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/install_ccache.py b/.github/scripts/install_ccache.py deleted file mode 100755 index 4e96ccd096b5..000000000000 --- a/.github/scripts/install_ccache.py +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/env python3 -""" -Install and configure ccache with signature verification. - -Usage: - install_ccache.py [--version VERSION] [--arch ARCH] [--config PATH] [--prefix PATH] - -Outputs (for GitHub Actions): - version, arch, max_size -""" - -from __future__ import annotations - -import argparse -import hashlib -import os -import platform -import re -import shutil -import subprocess -import sys -import tarfile -import tempfile -import time -from pathlib import Path -from typing import NamedTuple -from urllib.error import HTTPError, URLError -from urllib.request import urlopen, urlretrieve - - -class CcacheConfig(NamedTuple): - """Configuration values extracted for ccache setup.""" - - version: str - arch: str - max_size: str - - -class CcacheInstaller: - """Handles ccache installation with signature verification.""" - - DEFAULT_VERSION = "4.12.3" - DEFAULT_MAX_SIZE = "2G" - MINISIGN_VERSION = "0.11" - MINISIGN_KEY = "RWQX7yXbBedVfI4PNx6FLdFXu9GHUFsr28s4BVGxm4BeybtnX3P06saF" - - CCACHE_RELEASE_URL = "https://github.com/ccache/ccache/releases/download" - MINISIGN_RELEASE_URL = "https://github.com/jedisct1/minisign/releases/download" - MINISIGN_ARCHIVE_SHA256 = "f0a0954413df8531befed169e447a66da6868d79052ed7e892e50a4291af7ae0" - - def __init__( - self, - version: str = DEFAULT_VERSION, - arch: str | None = None, - config_path: Path | None = None, - prefix: Path | None = None, - max_retries: int = 3, - retry_delay: float = 5.0, - ) -> None: - self.version = version - self.arch = arch or self.detect_arch() - self.config_path = config_path - self.prefix = prefix or self._default_prefix() - self.max_retries = max_retries - self.retry_delay = retry_delay - self.max_size = self.DEFAULT_MAX_SIZE - - if config_path: - self.max_size = self.read_max_size(config_path) - - @staticmethod - def _default_prefix() -> Path: - """Get default installation prefix from env or standard location.""" - env_prefix = os.environ.get("CCACHE_PREFIX") - if env_prefix: - return Path(env_prefix) - return Path("/usr/local") - - @staticmethod - def validate_version(version: str) -> bool: - """Validate version format matches X.Y or X.Y.Z pattern.""" - pattern = r"^[0-9]+\.[0-9]+(\.[0-9]+)?$" - return bool(re.match(pattern, version)) - - @staticmethod - def detect_arch() -> str: - """Auto-detect CPU architecture, normalizing to ccache naming.""" - machine = platform.machine().lower() - arch_map = { - "x86_64": "x86_64", - "amd64": "x86_64", - "aarch64": "aarch64", - "arm64": "aarch64", - } - return arch_map.get(machine, "x86_64") - - def read_max_size(self, config_path: Path) -> str: - """Extract max_size value from ccache.conf file.""" - if not config_path.exists(): - print( - f"Warning: ccache config not found at {config_path}, using default max_size", - file=sys.stderr, - ) - return self.DEFAULT_MAX_SIZE - - pattern = re.compile(r"^max_size\s*=\s*(.+)$") - try: - with config_path.open() as f: - for line in f: - match = pattern.match(line.strip()) - if match: - return match.group(1).strip() - except OSError as e: - print(f"Warning: Failed to read config: {e}", file=sys.stderr) - - return self.DEFAULT_MAX_SIZE - - def download_with_retry(self, url: str, dest: Path) -> bool: - """Download file with retry logic.""" - for attempt in range(1, self.max_retries + 1): - try: - print(f"Downloading {url} (attempt {attempt}/{self.max_retries})") - urlretrieve(url, dest) - return True - except (HTTPError, URLError, OSError) as e: - print(f"Download failed: {e}", file=sys.stderr) - if attempt < self.max_retries: - print(f"Retrying in {self.retry_delay} seconds...") - time.sleep(self.retry_delay) - - return False - - def download_with_verify(self, temp_dir: Path) -> Path | None: - """Download ccache archive and verify its signature.""" - archive_name = f"ccache-{self.version}-linux-{self.arch}.tar.xz" - archive_path = temp_dir / archive_name - sig_path = temp_dir / f"{archive_name}.minisig" - - ccache_url = f"{self.CCACHE_RELEASE_URL}/v{self.version}/{archive_name}" - sig_url = f"{ccache_url}.minisig" - - if not self.download_with_retry(ccache_url, archive_path): - print("Error: Failed to download ccache archive", file=sys.stderr) - return None - - if not self.download_with_retry(sig_url, sig_path): - print("Error: Failed to download signature file", file=sys.stderr) - return None - - minisign_bin = self._setup_minisign(temp_dir) - if not minisign_bin: - print("Error: Failed to setup minisign", file=sys.stderr) - return None - - if not self.verify_signature(archive_path, sig_path, minisign_bin): - print("Error: Signature verification failed", file=sys.stderr) - return None - - return archive_path - - def _setup_minisign(self, temp_dir: Path) -> Path | None: - """Download and extract minisign binary.""" - minisign_archive = f"minisign-{self.MINISIGN_VERSION}-linux.tar.gz" - minisign_url = f"{self.MINISIGN_RELEASE_URL}/{self.MINISIGN_VERSION}/{minisign_archive}" - minisign_path = temp_dir / minisign_archive - - if not self.download_with_retry(minisign_url, minisign_path): - return None - - actual_hash = hashlib.sha256(minisign_path.read_bytes()).hexdigest() - if actual_hash != self.MINISIGN_ARCHIVE_SHA256: - print( - f"Error: minisign archive hash mismatch: {actual_hash} != {self.MINISIGN_ARCHIVE_SHA256}", - file=sys.stderr, - ) - return None - - try: - with tarfile.open(minisign_path, "r:gz") as tar: - tar.extractall(temp_dir, filter="data") - except tarfile.TarError as e: - print(f"Error extracting minisign: {e}", file=sys.stderr) - return None - - minisign_bin = temp_dir / "minisign-linux" / self.arch / "minisign" - if not minisign_bin.exists(): - print(f"Error: minisign binary not found at {minisign_bin}", file=sys.stderr) - return None - - minisign_bin.chmod(0o755) - return minisign_bin - - def verify_signature(self, archive: Path, sig_file: Path, minisign_bin: Path) -> bool: - """Verify archive signature using minisign.""" - try: - result = subprocess.run( - [str(minisign_bin), "-Vm", str(archive), "-P", self.MINISIGN_KEY], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - print(f"Signature verification failed: {result.stderr}", file=sys.stderr) - return False - print("Signature verified successfully") - return True - except OSError as e: - print(f"Error running minisign: {e}", file=sys.stderr) - return False - - def install(self, archive: Path) -> bool: - """Extract and install ccache binary.""" - temp_dir = archive.parent - extract_dir = temp_dir / f"ccache-{self.version}-linux-{self.arch}" - - try: - with tarfile.open(archive, "r:xz") as tar: - tar.extractall(temp_dir, filter="data") - except tarfile.TarError as e: - print(f"Error extracting ccache: {e}", file=sys.stderr) - return False - - ccache_bin = extract_dir / "ccache" - if not ccache_bin.exists(): - print(f"Error: ccache binary not found at {ccache_bin}", file=sys.stderr) - return False - - dest_bin = self.prefix / "bin" / "ccache" - - try: - result = subprocess.run( - ["sudo", "cp", str(ccache_bin), str(dest_bin)], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - print(f"Error copying ccache: {result.stderr}", file=sys.stderr) - return False - - result = subprocess.run( - ["sudo", "chmod", "+x", str(dest_bin)], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - print(f"Error setting permissions: {result.stderr}", file=sys.stderr) - return False - - except OSError as e: - print(f"Error installing ccache: {e}", file=sys.stderr) - return False - - print(f"ccache {self.version} installed successfully to {dest_bin}") - return True - - def is_installed(self) -> bool: - """Check if requested ccache version is already installed.""" - ccache_path = shutil.which("ccache") - if not ccache_path: - return False - - try: - result = subprocess.run( - ["ccache", "--version"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode != 0: - return False - - version_match = re.search(r"[0-9]+\.[0-9]+(\.[0-9]+)?", result.stdout) - if version_match and version_match.group() == self.version: - return True - except OSError: - pass - - return False - - def get_config(self) -> CcacheConfig: - """Return current configuration as named tuple.""" - return CcacheConfig( - version=self.version, - arch=self.arch, - max_size=self.max_size, - ) - - def run_install(self) -> bool: - """Execute full installation workflow.""" - if platform.system() != "Linux": - print("Warning: Binary installation only supported on Linux", file=sys.stderr) - return False - - if self.is_installed(): - print(f"ccache {self.version} already installed") - return True - - print(f"Installing ccache {self.version}...") - - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - archive = self.download_with_verify(temp_path) - if not archive: - return False - - if not self.install(archive): - return False - - self._print_version() - return True - - def _print_version(self) -> None: - """Print installed ccache version.""" - try: - result = subprocess.run( - ["ccache", "--version"], - capture_output=True, - text=True, - check=False, - ) - if result.returncode == 0: - print(result.stdout.strip()) - except OSError: - pass - - -def output_github_actions(config: CcacheConfig) -> None: - """Write outputs for GitHub Actions.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - try: - with open(github_output, "a") as f: - f.write(f"version={config.version}\n") - f.write(f"arch={config.arch}\n") - f.write(f"max_size={config.max_size}\n") - except OSError as e: - print(f"Warning: Failed to write GitHub output: {e}", file=sys.stderr) - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description="Install and configure ccache with signature verification", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - parser.add_argument( - "--version", - default=CcacheInstaller.DEFAULT_VERSION, - help=f"ccache version (default: {CcacheInstaller.DEFAULT_VERSION})", - ) - parser.add_argument( - "--arch", - choices=["x86_64", "aarch64"], - help="Architecture (default: auto-detect)", - ) - parser.add_argument( - "--config", - type=Path, - metavar="PATH", - help="Path to ccache.conf for max_size", - ) - parser.add_argument( - "--prefix", - type=Path, - default=None, - help="Installation prefix (default: $CCACHE_PREFIX or /usr/local)", - ) - parser.add_argument( - "--install", - action="store_true", - help="Install ccache binary (Linux only)", - ) - parser.add_argument( - "--output-only", - action="store_true", - help="Only output config, don't install", - ) - - return parser.parse_args() - - -def main() -> int: - """Main entry point.""" - args = parse_args() - - if not CcacheInstaller.validate_version(args.version): - print(f"Error: Invalid ccache version format: {args.version}", file=sys.stderr) - return 1 - - installer = CcacheInstaller( - version=args.version, - arch=args.arch, - config_path=args.config, - prefix=args.prefix, - ) - - config = installer.get_config() - - print("ccache configuration:") - print(f" Version: {config.version}") - print(f" Arch: {config.arch}") - print(f" Max Size: {config.max_size}") - - output_github_actions(config) - - if args.install and not args.output_only: - if not installer.run_install(): - return 1 - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/scripts/install_dependencies_helper.py b/.github/scripts/install_dependencies_helper.py new file mode 100644 index 000000000000..c48cd67e1f9e --- /dev/null +++ b/.github/scripts/install_dependencies_helper.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Post-install fixups for CI dependency caching on Linux. + +Handles: +- Enabling the Ubuntu universe repository +- Repairing apt alternatives after cache-apt-pkgs-action restore +- Installing optional packages +- Detecting Python version for pipx cache keys +""" + +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output # noqa: E402 + +APT_OPTS = ["-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3"] + + +def _run(cmd: list[str], *, check: bool = True, **kwargs) -> subprocess.CompletedProcess: + return subprocess.run(cmd, check=check, **kwargs) + + +def _sudo(cmd: list[str], **kwargs) -> subprocess.CompletedProcess: + return _run(["sudo", *cmd], **kwargs) + + +def _ldconfig_has_blas() -> bool: + result = _run(["ldconfig", "-p"], capture_output=True, text=True, check=False) + return bool(re.search(r"\blibblas\.so\.3\b", result.stdout)) + + +def _get_multiarch() -> str: + result = _run( + ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"], + capture_output=True, text=True, check=False, + ) + return result.stdout.strip() if result.returncode == 0 else "" + + +def enable_universe() -> None: + """Enable the Ubuntu universe repository if not already present.""" + sources_dirs = [Path("/etc/apt/sources.list"), Path("/etc/apt/sources.list.d")] + universe_found = False + + for source in sources_dirs: + if not source.exists(): + continue + paths = [source] if source.is_file() else list(source.glob("*.list")) + list(source.glob("*.sources")) + for p in paths: + try: + text = p.read_text(errors="replace") + if re.search(r"^(deb|Components:).*universe", text, re.MULTILINE): + universe_found = True + break + except OSError: + continue + if universe_found: + break + + if not universe_found: + _sudo(["apt-get", *APT_OPTS, "install", "-y", "-qq", "software-properties-common"]) + _sudo(["add-apt-repository", "-y", "universe"]) + + _sudo(["apt-get", *APT_OPTS, "update", "-y", "-qq"]) + + +def fix_apt_alternatives() -> None: + """Repair apt alternatives and BLAS symlinks after cache restore.""" + result = _sudo(["dpkg", "--configure", "-a"], check=False) + if result.returncode != 0: + print("::warning::dpkg --configure -a failed; package state may be inconsistent") + + if _ldconfig_has_blas(): + return + + print("::warning::libblas.so.3 missing from linker cache; attempting repair") + _sudo( + ["apt-get", *APT_OPTS, "install", "-y", "-qq", "--reinstall", "libblas3", "libopenblas0"], + check=False, + ) + + multiarch = _get_multiarch() + if multiarch: + _sudo(["update-alternatives", "--auto", f"libblas.so.3-{multiarch}"], check=False) + + blas_link = Path(f"/usr/lib/{multiarch}/libblas.so.3") + if not blas_link.exists(): + candidates = list(Path("/usr/lib").rglob("libblas.so.3")) + if candidates: + candidate = candidates[0] + print(f"::warning::Creating compatibility symlink {blas_link} -> {candidate}") + _sudo(["ln", "-sf", str(candidate), str(blas_link)]) + + _sudo(["ldconfig"]) + + if not _ldconfig_has_blas(): + print("::error::libblas.so.3 is still missing after repair attempt") + sys.exit(1) + + +def install_optional_packages() -> None: + """Install optional apt packages that are available.""" + result = _run( + [sys.executable, "tools/setup/install_dependencies.py", + "--platform", "debian", "--category", "gstreamer_optional", "--print-available-packages"], + capture_output=True, text=True, check=False, + ) + packages = result.stdout.strip() + if packages: + print(f"Installing optional packages: {packages}") + _sudo(["apt-get", *APT_OPTS, "install", "-y", "-qq", *packages.split()], check=False) + + +def detect_python_version() -> None: + """Write Python minor version to GITHUB_OUTPUT for cache keys.""" + minor = f"{sys.version_info.major}.{sys.version_info.minor}" + write_github_output({"minor": minor}) + + +def main() -> None: + import argparse + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="command", required=True) + sub.add_parser("enable-universe") + sub.add_parser("fix-apt-alternatives") + sub.add_parser("install-optional") + sub.add_parser("detect-python-version") + + args = parser.parse_args() + commands = { + "enable-universe": enable_universe, + "fix-apt-alternatives": fix_apt_alternatives, + "install-optional": install_optional_packages, + "detect-python-version": detect_python_version, + } + commands[args.command]() + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/plan_docker_builds.py b/.github/scripts/plan_docker_builds.py new file mode 100644 index 000000000000..723f2f5c9851 --- /dev/null +++ b/.github/scripts/plan_docker_builds.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Plan Docker build matrix entries for the Docker workflow.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output + + +def plan_builds(event_name: str, linux_changed: bool, android_changed: bool) -> dict[str, object]: + """Return workflow matrix and a has_jobs flag.""" + include: list[dict[str, object]] = [] + + linux_selected = event_name != "pull_request" or linux_changed + android_selected = event_name != "pull_request" or android_changed + + if linux_selected: + include.append( + { + "platform": "Linux", + "dockerfile": "Dockerfile-build-ubuntu", + "fuse": True, + "artifact_pattern": "*.AppImage", + } + ) + if android_selected: + include.append( + { + "platform": "Android", + "dockerfile": "Dockerfile-build-android", + "fuse": False, + "artifact_pattern": "*.apk", + } + ) + + return {"matrix": {"include": include}, "has_jobs": bool(include)} + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Plan Docker workflow builds.") + parser.add_argument("--event-name", default=os.environ.get("EVENT_NAME", "")) + parser.add_argument("--linux", default=os.environ.get("LINUX", "false")) + parser.add_argument("--android", default=os.environ.get("ANDROID", "false")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Compute the Docker build matrix and emit outputs.""" + args = parse_args(argv) + plan = plan_builds( + args.event_name, + args.linux == "true", + args.android == "true", + ) + matrix_json = json.dumps(plan["matrix"], separators=(",", ":")) + print(matrix_json) + + write_github_output({ + "matrix": matrix_json, + "has_jobs": "true" if plan["has_jobs"] else "false", + }) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/precommit_results.py b/.github/scripts/precommit_results.py new file mode 100644 index 000000000000..d75014b3d365 --- /dev/null +++ b/.github/scripts/precommit_results.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Prepare pre-commit result artifacts for CI.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import sys +from pathlib import Path + + +def write_results_json( + output_dir: Path, + *, + exit_code: str, + passed: str, + failed: str, + skipped: str, + run_url: str, +) -> Path: + """Write the summary JSON artifact.""" + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / "pre-commit-results.json" + output_path.write_text( + json.dumps( + { + "exit_code": exit_code, + "passed": passed, + "failed": failed, + "skipped": skipped, + "run_url": run_url, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + return output_path + + +def copy_output_file(source: Path | None, output_dir: Path) -> None: + """Copy the text output file into the artifact directory if it exists.""" + if source is None or not source.exists(): + return + shutil.copy2(source, output_dir / source.name) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Prepare pre-commit result artifacts.") + parser.add_argument("--output-dir", default="pre-commit-results") + parser.add_argument("--exit-code", default="1") + parser.add_argument("--passed", default="0") + parser.add_argument("--failed", default="0") + parser.add_argument("--skipped", default="0") + parser.add_argument("--run-url", default="") + parser.add_argument("--output-file", default="") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Generate the artifact directory for pre-commit results.""" + args = parse_args(argv) + output_dir = Path(args.output_dir) + write_results_json( + output_dir, + exit_code=args.exit_code, + passed=args.passed, + failed=args.failed, + skipped=args.skipped, + run_url=args.run_url, + ) + copy_output_file(Path(args.output_file) if args.output_file else None, output_dir) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/resolve_gstreamer_config.py b/.github/scripts/resolve_gstreamer_config.py new file mode 100644 index 000000000000..ef9fadf29cd7 --- /dev/null +++ b/.github/scripts/resolve_gstreamer_config.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Resolve GStreamer and Qt versions for GitHub Actions.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.build_config import get_build_config_value +from common.gh_actions import write_github_output + +VERSION_KEYS = { + "macos": "gstreamer_macos_version", + "windows": "gstreamer_windows_version", + "android": "gstreamer_android_version", + "ios": "gstreamer_ios_version", +} + + +def resolve_version(platform_name: str, requested_version: str) -> str: + """Return explicit version override or platform-specific config version.""" + if requested_version: + return requested_version + version_key = VERSION_KEYS.get(platform_name, "gstreamer_default_version") + return get_build_config_value(version_key) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Resolve GStreamer action config values.") + parser.add_argument("--platform", required=True, choices=["linux", "macos", "windows", "android", "ios"]) + parser.add_argument("--version", default="", help="Explicit GStreamer version override") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Resolve config values and emit GitHub outputs.""" + args = parse_args(argv) + outputs = { + "py_cmd": "python3", + "version": resolve_version(args.platform, args.version), + "qt_version": get_build_config_value("qt_version"), + } + write_github_output(outputs) + print(f"Building GStreamer {outputs['version']} for {args.platform}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/size_analysis.py b/.github/scripts/size_analysis.py index 4f31f2b6cd72..367d94c63e0b 100755 --- a/.github/scripts/size_analysis.py +++ b/.github/scripts/size_analysis.py @@ -21,6 +21,15 @@ from pathlib import Path from typing import Any +from ci_bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import ( # noqa: E402 + write_github_output as _write_github_output, + write_step_summary as _write_step_summary, +) + class BinaryAnalyzer: """Analyzes binary size and symbol information.""" @@ -215,20 +224,16 @@ def generate_summary( def write_github_output(binary_size: int, stripped_size: int, symbol_count: int) -> None: """Write outputs to GITHUB_OUTPUT file if available.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if github_output: - with open(github_output, "a") as f: - f.write(f"binary_size={binary_size}\n") - f.write(f"stripped_size={stripped_size}\n") - f.write(f"symbol_count={symbol_count}\n") + _write_github_output({ + "binary_size": str(binary_size), + "stripped_size": str(stripped_size), + "symbol_count": str(symbol_count), + }) def write_github_step_summary(summary: str) -> None: """Write summary to GITHUB_STEP_SUMMARY file if available.""" - step_summary = os.environ.get("GITHUB_STEP_SUMMARY") - if step_summary: - with open(step_summary, "a") as f: - f.write(summary) + _write_step_summary(summary) def parse_args() -> argparse.Namespace: diff --git a/.github/scripts/templates/build_results.md.j2 b/.github/scripts/templates/build_results.md.j2 new file mode 100644 index 000000000000..d100b4bf2c91 --- /dev/null +++ b/.github/scripts/templates/build_results.md.j2 @@ -0,0 +1,89 @@ +## Build Results + +### Platform Status + +{% if table -%} +{{ table }} +{% else -%} +| Platform | Status | Details | +|----------|--------|--------| +{% endif %} + +**{{ summary }}** + +### Pre-commit + +| Check | Status | Details | +|-------|--------|---------| +| pre-commit | {{ precommit.status }} | {{ precommit.details }} | + +{% if precommit.note -%} +{{ precommit.note }} + +{% endif -%} +{% if tests -%} +### Test Results + +{% for t in tests.platforms -%} +{% if t.failed > 0 -%} +**{{ t.arch }}**: {{ t.passed }} passed, {{ t.failed }} failed, {{ t.skipped }} skipped + +
Failed tests + +``` +{% for line in t.failed_lines -%} +{{ line }} +{% endfor -%} +``` +
+{% else -%} +**{{ t.arch }}**: {{ t.passed }} passed, {{ t.skipped }} skipped +{% endif %} +{% endfor -%} +{% if tests.has_failures -%} +**Total: {{ tests.total_passed }} passed, {{ tests.total_failed }} failed, {{ tests.total_skipped }} skipped** +{% else -%} +**Total: {{ tests.total_passed }} passed, {{ tests.total_skipped }} skipped** +{% endif %} +{% endif -%} +{% if coverage -%} +### Code Coverage + +{% if coverage.baseline is not none and coverage.current is not none -%} +| Coverage | Baseline | Change | +|----------|----------|--------| +| {{ "%.1f"|format(coverage.current) }}% | {{ "%.1f"|format(coverage.baseline) }}% | {{ "%+.1f"|format(coverage.diff) }}% | +{% else -%} +Coverage: **{% if coverage.current is not none %}{{ "%.1f"|format(coverage.current) }}%{% else %}N/A{% endif %}** + +*No baseline available for comparison* +{% endif %} +{% endif -%} +{% if artifacts -%} +### Artifact Sizes + +{% if artifacts.has_baseline -%} +| Artifact | Size | Δ from master | +|----------|------|---------------| +{% for a in artifacts.entries -%} +{% if a.delta is defined -%} +| {{ a.name }} | {{ a.size_human }} | {{ a.delta_human }} | +{% else -%} +| {{ a.name }} | {{ a.size_human }} | +{% endif -%} +{% endfor %} +{% else -%} +| Artifact | Size | +|----------|------| +{% for a in artifacts.entries -%} +| {{ a.name }} | {{ a.size_human }} | +{% endfor %} +{% endif -%} +{% if artifacts.has_baseline and artifacts.total_delta != 0 -%} +**Total size {{ "increased" if artifacts.total_delta > 0 else "decreased" }} by {{ "%.2f"|format(artifacts.total_delta_mb) }} MB** +{%- elif not artifacts.has_baseline -%} +*No baseline available for comparison* +{%- endif %} +{% endif %} +--- +Updated: {{ timestamp }} • Triggered by: {{ triggered_by }} diff --git a/.github/scripts/test_duration_report.py b/.github/scripts/test_duration_report.py new file mode 100644 index 000000000000..fcda4e756c57 --- /dev/null +++ b/.github/scripts/test_duration_report.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +"""Analyze JUnit test durations and emit CI summaries.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +from xml_utils import xml_parse + + +def test_key(elem: ET.Element) -> str: + """Return a stable identifier for a JUnit testcase element.""" + classname = elem.attrib.get("classname", "").strip() + name = elem.attrib.get("name", "").strip() + if classname and name: + return f"{classname}::{name}" + return name or classname or "" + + +def parse_time(value: str) -> float: + """Parse testcase duration, defaulting invalid values to 0.""" + try: + return float(value) + except Exception: + return 0.0 + + +def load_baseline(baseline_path: Path) -> tuple[dict[str, float], bool, str]: + """Load a baseline file into a simple test->seconds mapping.""" + if not baseline_path.exists(): + return {}, False, "" + + try: + payload = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline: dict[str, float] = {} + if isinstance(payload, dict) and "tests" in payload and isinstance(payload["tests"], dict): + for key, value in payload["tests"].items(): + if isinstance(value, dict): + baseline[key] = float(value.get("seconds", 0.0)) + else: + baseline[key] = float(value) + return baseline, True, "" + if isinstance(payload, dict): + for key, value in payload.items(): + if isinstance(value, (int, float)): + baseline[key] = float(value) + return baseline, True, "" + return {}, False, "baseline file has unsupported structure" + except Exception as exc: + return {}, False, str(exc) + + +def analyze_test_durations( + junit_path: Path, + baseline_path: Path, + *, + top_n: int, + slow_threshold: float, + regression_factor: float, + min_delta: float, +) -> dict[str, Any]: + """Return computed timing report data for a JUnit XML file.""" + tree = xml_parse(junit_path) + root = tree.getroot() + cases = [(test_key(testcase), parse_time(testcase.attrib.get("time", "0"))) for testcase in root.iter("testcase")] + + total_seconds = sum(secs for _, secs in cases) + slowest = sorted(cases, key=lambda item: item[1], reverse=True) + slow_over_threshold = [(key, secs) for key, secs in slowest if secs >= slow_threshold] + + baseline, baseline_loaded, baseline_error = load_baseline(baseline_path) + regressions: list[tuple[str, float, float]] = [] + if baseline: + for key, current in cases: + previous = baseline.get(key) + if previous is None or previous <= 0: + continue + if current >= previous * regression_factor and (current - previous) >= min_delta: + regressions.append((key, previous, current)) + + regressions.sort(key=lambda item: item[2] - item[1], reverse=True) + + return { + "junit_path": str(junit_path), + "total_tests": len(cases), + "total_seconds": total_seconds, + "slow_threshold_seconds": slow_threshold, + "slow_tests": [{"test": key, "seconds": secs} for key, secs in slow_over_threshold], + "regression_factor": regression_factor, + "min_delta_seconds": min_delta, + "regressions": [ + { + "test": key, + "baseline_seconds": baseline_secs, + "current_seconds": current_secs, + "delta_seconds": current_secs - baseline_secs, + } + for key, baseline_secs, current_secs in regressions + ], + "baseline_loaded": baseline_loaded, + "baseline_entries": len(baseline), + "baseline_error": baseline_error, + "top_slowest": [{"test": key, "seconds": secs} for key, secs in slowest[:top_n]], + "regression_count": len(regressions), + "slow_count": len(slow_over_threshold), + "regression_warnings": regressions[:top_n], + "slow_warnings": slow_over_threshold[:top_n], + } + + +def build_summary(report: dict[str, Any], baseline_path: Path, *, missing_junit: str = "") -> str: + """Render a markdown summary for GitHub Step Summary.""" + lines = ["## Test Duration Report", ""] + if missing_junit: + lines.append(f"- WARNING: {missing_junit}") + lines.append("") + return "\n".join(lines) + + lines.extend( + [ + f"- JUnit: `{report['junit_path']}`", + f"- Total tests: **{report['total_tests']}**", + f"- Total time: **{report['total_seconds']:.1f}s**", + f"- Slow threshold: **{report['slow_threshold_seconds']:.1f}s**", + ] + ) + if report["baseline_loaded"]: + lines.append(f"- Baseline file: `{baseline_path}` ({report['baseline_entries']} entries)") + else: + suffix = f" - {report['baseline_error']}" if report["baseline_error"] else "" + lines.append(f"- Baseline file: `{baseline_path}` (not available{suffix})") + lines.append("") + + top_slowest = report["top_slowest"] + if top_slowest: + lines.append(f"### Top {len(top_slowest)} Slowest Tests") + lines.append("") + lines.append("| Test | Seconds |") + lines.append("|---|---:|") + for item in top_slowest: + lines.append(f"| `{item['test']}` | {item['seconds']:.3f} |") + lines.append("") + + regressions = report["regressions"] + if regressions: + lines.append("### Regressions vs Baseline") + lines.append("") + lines.append("| Test | Baseline (s) | Current (s) | Delta (s) |") + lines.append("|---|---:|---:|---:|") + for item in regressions[: len(top_slowest) or len(regressions)]: + lines.append( + f"| `{item['test']}` | {item['baseline_seconds']:.3f} | {item['current_seconds']:.3f} | {item['delta_seconds']:.3f} |" + ) + lines.append("") + else: + lines.append("No baseline regressions detected.") + lines.append("") + + return "\n".join(lines) + + +def append_file(path: Path | None, content: str) -> None: + """Append content to a file if a path is provided.""" + if path is None: + return + with path.open("a", encoding="utf-8") as handle: + handle.write(content) + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Analyze JUnit test durations.") + parser.add_argument("--junit-path", required=True) + parser.add_argument("--baseline-path", default=".github/test-duration-baseline.json") + parser.add_argument("--report-json-path", default="test-duration-report.json") + parser.add_argument("--top-n", type=int, default=20) + parser.add_argument("--slow-threshold-seconds", type=float, default=60.0) + parser.add_argument("--regression-factor", type=float, default=1.5) + parser.add_argument("--min-delta-seconds", type=float, default=5.0) + parser.add_argument("--fail-on-regression", action="store_true") + parser.add_argument("--github-step-summary", default=os.environ.get("GITHUB_STEP_SUMMARY", "")) + parser.add_argument("--github-output", default=os.environ.get("GITHUB_OUTPUT", "")) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Run the report generation flow.""" + args = parse_args(argv) + junit_path = Path(args.junit_path) + baseline_path = Path(args.baseline_path) + report_json_path = Path(args.report_json_path) + step_summary = Path(args.github_step_summary) if args.github_step_summary else None + github_output = Path(args.github_output) if args.github_output else None + + if not junit_path.exists(): + message = f"JUnit report not found at {junit_path}" + print(f"::warning::{message}") + append_file(step_summary, build_summary({}, baseline_path, missing_junit=message)) + append_file(github_output, "regression_count=0\nslow_count=0\n") + return 0 + + report = analyze_test_durations( + junit_path, + baseline_path, + top_n=args.top_n, + slow_threshold=args.slow_threshold_seconds, + regression_factor=args.regression_factor, + min_delta=args.min_delta_seconds, + ) + report_json_path.parent.mkdir(parents=True, exist_ok=True) + report_json_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + append_file(step_summary, build_summary(report, baseline_path)) + append_file( + github_output, + f"regression_count={report['regression_count']}\nslow_count={report['slow_count']}\n", + ) + + if report["baseline_error"]: + print(f"::warning::Failed to parse baseline file {baseline_path}: {report['baseline_error']}") + + for key, secs in report["slow_warnings"]: + print(f"::warning::Slow test (>={args.slow_threshold_seconds:.1f}s): {key} took {secs:.3f}s") + for key, base, cur in report["regression_warnings"]: + print( + f"::warning::Timing regression: {key} baseline={base:.3f}s current={cur:.3f}s " + f"(+{(cur - base):.3f}s, x{(cur / base):.2f})" + ) + + return 1 if args.fail_on_regression and report["regression_count"] else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/tests/test_aws_upload.py b/.github/scripts/tests/test_aws_upload.py new file mode 100644 index 000000000000..5317897a81b5 --- /dev/null +++ b/.github/scripts/tests/test_aws_upload.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Tests for aws_upload.py.""" + +from __future__ import annotations + +import pytest + +from aws_upload import sanitize_ref, validate_artifact, validate_credentials + + +class TestSanitizeRef: + def test_simple_branch(self) -> None: + assert sanitize_ref("main") == "main" + + def test_slashes_replaced(self) -> None: + assert sanitize_ref("feature/foo") == "feature_foo" + + def test_dotdot_removed(self) -> None: + assert sanitize_ref("../../../etc/passwd") == "___etc_passwd" + + def test_special_chars(self) -> None: + assert sanitize_ref("v1.2.3-rc1") == "v1.2.3-rc1" + + def test_spaces_replaced(self) -> None: + assert sanitize_ref("my branch") == "my_branch" + + +class TestValidateCredentials: + def test_role_arn_sufficient(self) -> None: + validate_credentials("arn:aws:iam::123:role/test", "", "") + + def test_static_creds_sufficient(self) -> None: + validate_credentials("", "AKID", "secret") + + def test_missing_both_exits(self) -> None: + with pytest.raises(SystemExit): + validate_credentials("", "", "") + + def test_partial_static_exits(self) -> None: + with pytest.raises(SystemExit): + validate_credentials("", "AKID", "") + + +class TestValidateArtifact: + def test_path_traversal_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "../test.bin") + + def test_slash_in_name_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "path/test.bin") + + def test_backslash_rejected(self, tmp_path) -> None: + f = tmp_path / "test.bin" + f.touch() + with pytest.raises(SystemExit): + validate_artifact(str(f), "path\\test.bin") + + def test_missing_file_exits(self) -> None: + with pytest.raises(SystemExit): + validate_artifact("/nonexistent/file", "test.bin") + + def test_valid_artifact(self, tmp_path) -> None: + f = tmp_path / "QGroundControl.AppImage" + f.touch() + validate_artifact(str(f), "QGroundControl.AppImage") diff --git a/.github/scripts/tests/test_ccache_helper.py b/.github/scripts/tests/test_ccache_helper.py new file mode 100644 index 000000000000..de2c1b7ac7e2 --- /dev/null +++ b/.github/scripts/tests/test_ccache_helper.py @@ -0,0 +1,312 @@ +"""Tests for ccache_helper.py.""" + +from __future__ import annotations + +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from ccache_helper import ( + CcacheConfig, + CcacheInstaller, + build_summary_markdown, + configure_ccache_environment, + configure_cpm_cache, + compute_cpm_fingerprint, + determine_cache_scope, + main, + resolve_arch, + resolve_windows_binary_config, + parse_args, + write_step_summary, +) + + +class TestCcacheInstaller: + """Tests for CcacheInstaller class.""" + + def test_validate_version_valid(self): + """Test valid version formats.""" + assert CcacheInstaller.validate_version("4.13.1") + assert CcacheInstaller.validate_version("4.13") + assert CcacheInstaller.validate_version("5.0.0") + + def test_validate_version_invalid(self): + """Test invalid version formats.""" + assert not CcacheInstaller.validate_version("4.12.2.1") + assert not CcacheInstaller.validate_version("v4.12.2") + assert not CcacheInstaller.validate_version("latest") + assert not CcacheInstaller.validate_version("") + + def test_detect_arch_x86_64(self): + """Test x86_64 architecture detection.""" + with patch("platform.machine", return_value="x86_64"): + assert CcacheInstaller.detect_arch() == "x86_64" + + def test_detect_arch_amd64(self): + """Test amd64 (alias) architecture detection.""" + with patch("platform.machine", return_value="amd64"): + assert CcacheInstaller.detect_arch() == "x86_64" + + def test_detect_arch_arm64(self): + """Test arm64 architecture detection.""" + with patch("platform.machine", return_value="arm64"): + assert CcacheInstaller.detect_arch() == "aarch64" + + def test_detect_arch_aarch64(self): + """Test aarch64 architecture detection.""" + with patch("platform.machine", return_value="aarch64"): + assert CcacheInstaller.detect_arch() == "aarch64" + + def test_default_prefix_from_env(self): + """Test prefix from CCACHE_PREFIX environment variable.""" + with patch.dict(os.environ, {"CCACHE_PREFIX": "/custom/path"}): + assert CcacheInstaller._default_prefix() == Path("/custom/path") + + def test_default_prefix_fallback(self): + """Test default prefix fallback to /usr/local.""" + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("CCACHE_PREFIX", None) + assert CcacheInstaller._default_prefix() == Path("/usr/local") + + def test_read_max_size_from_config(self): + """Test reading max_size from ccache.conf.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f: + f.write("max_size = 5G\n") + f.write("compiler_check = content\n") + f.flush() + config_path = Path(f.name) + + try: + installer = CcacheInstaller(config_path=config_path) + assert installer.max_size == "5G" + finally: + config_path.unlink() + + def test_read_max_size_missing_file(self): + """Test max_size default when config file is missing.""" + installer = CcacheInstaller(config_path=Path("/nonexistent/path.conf")) + assert installer.max_size == CcacheInstaller.DEFAULT_MAX_SIZE + + def test_get_config(self): + """Test get_config returns correct named tuple.""" + installer = CcacheInstaller(version="4.10", arch="aarch64") + config = installer.get_config() + + assert isinstance(config, CcacheConfig) + assert config.version == "4.10" + assert config.arch == "aarch64" + assert config.max_size == CcacheInstaller.DEFAULT_MAX_SIZE + + def test_installer_with_custom_prefix(self): + """Test installer with custom prefix.""" + custom_prefix = Path("/opt/ccache") + installer = CcacheInstaller(prefix=custom_prefix) + assert installer.prefix == custom_prefix + + +class TestCcacheConfig: + """Tests for CcacheConfig named tuple.""" + + def test_config_creation(self): + """Test CcacheConfig creation.""" + config = CcacheConfig(version="4.13.1", arch="x86_64", max_size="2G") + assert config.version == "4.13.1" + assert config.arch == "x86_64" + assert config.max_size == "2G" + + def test_config_immutable(self): + """Test CcacheConfig is immutable.""" + config = CcacheConfig(version="4.13.1", arch="x86_64", max_size="2G") + with pytest.raises(AttributeError): + config.version = "5.0.0" + + +class TestBuildSummaryMarkdown: + """Tests for build_summary_markdown function.""" + + def test_typical_stats(self): + stats = { + "direct_cache_hit": 100, + "preprocessed_cache_hit": 20, + "cache_miss": 30, + } + md = build_summary_markdown(stats) + assert "120 / 150 (80.0%)" in md + assert "| Direct hits | 100 |" in md + assert "| Preprocessed hits | 20 |" in md + assert "| Misses | 30 |" in md + + def test_all_hits(self): + stats = {"direct_cache_hit": 50, "preprocessed_cache_hit": 0, "cache_miss": 0} + md = build_summary_markdown(stats) + assert "50 / 50 (100.0%)" in md + + def test_all_misses(self): + stats = {"direct_cache_hit": 0, "preprocessed_cache_hit": 0, "cache_miss": 42} + md = build_summary_markdown(stats) + assert "0 / 42 (0.0%)" in md + + def test_empty_stats(self): + md = build_summary_markdown({}) + assert "0 / 0 (0.0%)" in md + + def test_missing_fields_default_to_zero(self): + stats = {"direct_cache_hit": 5} + md = build_summary_markdown(stats) + assert "5 / 5 (100.0%)" in md + + +class TestWriteStepSummary: + """Tests for write_step_summary function.""" + + def test_writes_to_file(self, tmp_path): + summary_file = tmp_path / "summary.md" + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + assert write_step_summary("hello\n") + assert summary_file.read_text() == "hello\n" + + def test_appends_to_existing(self, tmp_path): + summary_file = tmp_path / "summary.md" + summary_file.write_text("existing\n") + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + write_step_summary("appended\n") + assert summary_file.read_text() == "existing\nappended\n" + + def test_prints_when_no_env(self, capsys): + with patch.dict(os.environ, {}, clear=True): + os.environ.pop("GITHUB_STEP_SUMMARY", None) + write_step_summary("fallback\n") + assert "fallback" in capsys.readouterr().out + + +class TestResolveArch: + """Tests for --target / --arch resolution.""" + + def test_arch_flag(self): + args = parse_args(["config", "--arch", "aarch64"]) + assert resolve_arch(args) == "aarch64" + + def test_target_arm64(self): + args = parse_args(["config", "--target", "linux_gcc_arm64"]) + assert resolve_arch(args) == "aarch64" + + def test_target_unknown_defaults_to_none(self): + args = parse_args(["config", "--target", "linux_gcc_64"]) + assert resolve_arch(args) is None + + def test_neither_returns_none(self): + args = parse_args(["config"]) + assert resolve_arch(args) is None + + def test_arch_and_target_mutually_exclusive(self): + with pytest.raises(SystemExit): + parse_args(["config", "--arch", "x86_64", "--target", "linux_gcc_arm64"]) + + def test_install_target(self): + args = parse_args(["install", "--target", "linux_gcc_arm64"]) + assert resolve_arch(args) == "aarch64" + + +class TestCLI: + """Tests for subcommand dispatch.""" + + def test_no_subcommand_returns_error(self): + assert main([]) == 1 + + def test_config_invalid_version(self): + assert main(["config", "--version", "bad"]) == 1 + + def test_config_valid(self, capsys): + assert main(["config", "--version", "4.13.1"]) == 0 + out = capsys.readouterr().out + assert "4.13.1" in out + + def test_config_with_target(self, capsys): + assert main(["config", "--target", "linux_gcc_arm64"]) == 0 + out = capsys.readouterr().out + assert "aarch64" in out + + def test_summary_without_ccache(self): + with patch("ccache_helper.get_ccache_json_stats", return_value=None), \ + patch("ccache_helper.get_ccache_verbose_stats", return_value=None): + assert main(["summary"]) == 0 + + def test_summary_with_stats(self, tmp_path): + stats = {"direct_cache_hit": 10, "preprocessed_cache_hit": 2, "cache_miss": 5} + verbose = "cache hit (direct): 10\ncache miss: 5" + summary_file = tmp_path / "summary.md" + with patch("ccache_helper.get_ccache_json_stats", return_value=stats), \ + patch("ccache_helper.get_ccache_verbose_stats", return_value=verbose), \ + patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(summary_file)}): + assert main(["summary"]) == 0 + content = summary_file.read_text() + assert "12 / 17" in content + assert "
" in content + assert "cache hit (direct): 10" in content + + +class TestCacheScope: + """Tests for workflow cache scope helper.""" + + def test_pull_request_scope(self): + assert determine_cache_scope("pull_request", "feature/test", "42") == "pr-42" + + def test_push_non_master_scope(self): + assert determine_cache_scope("push", "feature/test") == "branch-feature-test" + + +class TestFingerprint: + """Tests for CPM fingerprint helper.""" + + def test_fingerprint_changes_when_dependency_file_changes(self, tmp_path): + (tmp_path / "cmake/modules").mkdir(parents=True) + (tmp_path / ".github").mkdir() + (tmp_path / "CMakeLists.txt").write_text("project(QGC)\nCPMAddPackage(NAME foo)\n") + (tmp_path / "cmake/modules/CPM.cmake").write_text("# helper\n") + (tmp_path / ".github/build-config.json").write_text("{}\n") + + before = compute_cpm_fingerprint(tmp_path) + (tmp_path / "CMakeLists.txt").write_text("project(QGC)\nCPMAddPackage(NAME bar)\n") + after = compute_cpm_fingerprint(tmp_path) + + assert before != after + + +class TestWindowsConfig: + """Tests for Windows ccache binary resolution.""" + + def test_windows_arm64_uses_aarch64_binary(self): + values = resolve_windows_binary_config("windows_arm64", "windows") + assert values["arch"] == "aarch64" + + def test_android_windows_uses_x86_64_binary(self): + values = resolve_windows_binary_config("windows", "android") + assert values["arch"] == "x86_64" + + +class TestEnvironmentConfig: + """Tests for environment configuration helpers.""" + + def test_configure_ccache_environment_writes_env(self, tmp_path): + github_env = tmp_path / "env.txt" + workspace = tmp_path / "workspace" + with patch.dict(os.environ, {"GITHUB_ENV": str(github_env)}): + ccache_dir = configure_ccache_environment(workspace) + assert ccache_dir == workspace / ".ccache" + content = github_env.read_text() + assert "CCACHE_DIR=" in content + assert "CCACHE_CONFIGPATH=" in content + + def test_configure_cpm_cache_writes_outputs(self, tmp_path): + github_env = tmp_path / "env.txt" + github_output = tmp_path / "output.txt" + cache = tmp_path / "cpm-cache" + with patch.dict(os.environ, {"GITHUB_ENV": str(github_env), "GITHUB_OUTPUT": str(github_output)}): + configured = configure_cpm_cache(str(cache)) + assert configured == cache + assert "CPM_SOURCE_CACHE=" in github_env.read_text() + assert "path=" in github_output.read_text() diff --git a/.github/scripts/tests/test_check_baseline_ready.py b/.github/scripts/tests/test_check_baseline_ready.py index 65d8f20aeedb..b175a478e170 100644 --- a/.github/scripts/tests/test_check_baseline_ready.py +++ b/.github/scripts/tests/test_check_baseline_ready.py @@ -7,7 +7,7 @@ import pytest from check_baseline_ready import evaluate_readiness -from check_baseline_ready import write_output +from common.gh_actions import write_github_output PLATFORMS = ["Linux", "Windows", "MacOS", "Android"] @@ -84,7 +84,7 @@ def test_write_output_multiline_uses_collision_resistant_delimiter( monkeypatch.setenv("GITHUB_OUTPUT", str(out_path)) value = "line1\nEOF_missing_deadbeef\ntail" - write_output("missing", value) + write_github_output({"missing": value}) text = out_path.read_text(encoding="utf-8") assert "missing< None: + assert detect_jobs("4") == 4 + + def test_explicit_value_large(self) -> None: + assert detect_jobs("16") == 16 + + def test_auto_uses_cpu_count(self) -> None: + with patch("os.cpu_count", return_value=8): + assert detect_jobs("auto") == 8 + + def test_auto_fallback_none(self) -> None: + with patch("os.cpu_count", return_value=None): + assert detect_jobs("auto") == 2 + + def test_invalid_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("abc") + + def test_zero_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("0") + + def test_negative_exits(self) -> None: + import pytest + with pytest.raises(SystemExit): + detect_jobs("-1") diff --git a/.github/scripts/tests/test_collect_artifact_sizes.py b/.github/scripts/tests/test_collect_artifact_sizes.py index 25be628cd545..bd2f43e14845 100644 --- a/.github/scripts/tests/test_collect_artifact_sizes.py +++ b/.github/scripts/tests/test_collect_artifact_sizes.py @@ -15,6 +15,7 @@ def _run( created_at: str = "2026-02-24T00:00:00Z", status: str = "completed", conclusion: str = "success", + event: str = "pull_request", ) -> dict[str, object]: return { "id": run_id, @@ -22,6 +23,7 @@ def _run( "created_at": created_at, "status": status, "conclusion": conclusion, + "event": event, } @@ -51,6 +53,17 @@ def test_latest_successful_runs_handles_iso8601_offsets() -> None: assert latest["Linux"]["id"] == 2 +def test_latest_successful_runs_filters_by_event() -> None: + platforms = ["Linux"] + runs = [ + _run("Linux", run_id=1, created_at="2026-02-24T00:00:00Z", event="pull_request"), + _run("Linux", run_id=2, created_at="2026-02-24T01:00:00Z", event="push"), + ] + + latest = mod.latest_successful_runs(runs, platforms, event="pull_request") + assert latest["Linux"]["id"] == 1 + + def test_collect_artifacts_filters_non_product_artifacts() -> None: latest = {"Linux": {"id": 11}} @@ -210,7 +223,7 @@ def fake_list_run_artifacts(repo: str, run_id: int) -> list[dict[str, object]]: return [{"name": "QGroundControl-x86_64", "size_in_bytes": 2 * 1024 * 1024}] return [{"name": "QGroundControl-installer-AMD64", "size_in_bytes": 2 * 1024 * 1024}] - monkeypatch.setattr(mod, "list_workflow_runs", fake_list_workflow_runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", fake_list_workflow_runs) monkeypatch.setattr(mod, "list_run_artifacts", fake_list_run_artifacts) rc = mod.main( @@ -258,7 +271,7 @@ def fake_list_workflow_runs(repo: str, head_sha: str) -> list[dict[str, object]] def fail_list_run_artifacts(repo: str, run_id: int) -> list[dict[str, object]]: raise AssertionError("list_run_artifacts should not be called when artifacts file is provided") - monkeypatch.setattr(mod, "list_workflow_runs", fake_list_workflow_runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", fake_list_workflow_runs) monkeypatch.setattr(mod, "list_run_artifacts", fail_list_run_artifacts) rc = mod.main( diff --git a/.github/scripts/tests/test_collect_build_status.py b/.github/scripts/tests/test_collect_build_status.py index 5e3fb26de5fc..53c491e75e18 100644 --- a/.github/scripts/tests/test_collect_build_status.py +++ b/.github/scripts/tests/test_collect_build_status.py @@ -7,6 +7,7 @@ import pytest import collect_build_status as mod +from common.gh_actions import write_github_output def _run( @@ -55,7 +56,7 @@ def test_main_writes_expected_outputs(tmp_path: Path, monkeypatch: pytest.Monkey _run("Android", html_url="https://example.test/android"), _run("pre-commit", run_id=99, html_url="https://example.test/precommit"), ] - monkeypatch.setattr(mod, "list_workflow_runs", lambda repo, sha: runs) + monkeypatch.setattr(mod, "list_workflow_runs_for_sha", lambda repo, sha: runs) rc = mod.main( [ @@ -84,7 +85,7 @@ def test_write_output_uses_collision_resistant_delimiter(tmp_path: Path, monkeyp monkeypatch.setenv("GITHUB_OUTPUT", str(out_path)) value = "line1\nEOF_table_deadbeef\ntail" - mod.write_output("table", value) + write_github_output({"table": value}) text = out_path.read_text(encoding="utf-8") assert "table< None: + assert sanitize_branch("main") == "main" + + def test_slashes(self) -> None: + assert sanitize_branch("feature/docs-update") == "feature_docs-update" + + def test_special_chars(self) -> None: + assert sanitize_branch("v1.2.3-rc1") == "v1.2.3-rc1" + + def test_spaces(self) -> None: + assert sanitize_branch("my branch") == "my_branch" + + def test_preserves_dots_dashes_underscores(self) -> None: + assert sanitize_branch("release_v2.0-beta") == "release_v2.0-beta" diff --git a/.github/scripts/tests/test_detect_changes.py b/.github/scripts/tests/test_detect_changes.py new file mode 100644 index 000000000000..babdd2d3f29e --- /dev/null +++ b/.github/scripts/tests/test_detect_changes.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Tests for detect_changes.py.""" + +from __future__ import annotations + +import pytest + +from detect_changes import ( + build_patterns, + has_relevant_changes, + workflow_name_for_platform, +) + + +class TestWorkflowNameForPlatform: + def test_regular_platform(self) -> None: + assert workflow_name_for_platform("linux") == "linux" + assert workflow_name_for_platform("windows") == "windows" + assert workflow_name_for_platform("macos") == "macos" + + def test_docker_prefix(self) -> None: + assert workflow_name_for_platform("docker-linux") == "docker" + assert workflow_name_for_platform("docker-android") == "docker" + + +class TestBuildPatterns: + def test_common_patterns_present(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("^src/" in p for p in pattern_strs) + assert any("CMakeLists" in p for p in pattern_strs) + + def test_platform_workflow_pattern(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("linux\\.yml" in p for p in pattern_strs) + + def test_docker_workflow_pattern(self) -> None: + patterns = build_patterns("docker-linux") + pattern_strs = [p.pattern for p in patterns] + assert any("docker\\.yml" in p for p in pattern_strs) + + def test_android_extra_pattern(self) -> None: + patterns = build_patterns("android") + pattern_strs = [p.pattern for p in patterns] + assert any("^android/" in p for p in pattern_strs) + + def test_deploy_pattern_uses_platform(self) -> None: + patterns = build_patterns("linux") + pattern_strs = [p.pattern for p in patterns] + assert any("deploy/linux/" in p for p in pattern_strs) + + +class TestHasRelevantChanges: + def test_src_change_triggers_any_platform(self) -> None: + assert has_relevant_changes(["src/Vehicle.cc"], "linux") + assert has_relevant_changes(["src/Vehicle.cc"], "windows") + assert has_relevant_changes(["src/Vehicle.cc"], "android") + + def test_cmakelists_triggers(self) -> None: + assert has_relevant_changes(["CMakeLists.txt"], "linux") + + def test_cmake_dir_triggers(self) -> None: + assert has_relevant_changes(["cmake/FindFoo.cmake"], "macos") + + def test_qrc_triggers(self) -> None: + assert has_relevant_changes(["resources/qgc.qrc"], "linux") + + def test_unrelated_file_does_not_trigger(self) -> None: + assert not has_relevant_changes(["README.md"], "linux") + assert not has_relevant_changes(["docs/guide.md"], "windows") + + def test_workflow_file_triggers_own_platform(self) -> None: + assert has_relevant_changes([".github/workflows/linux.yml"], "linux") + assert not has_relevant_changes([".github/workflows/linux.yml"], "windows") + + def test_actions_dir_triggers_all(self) -> None: + assert has_relevant_changes([".github/actions/qt-install/action.yml"], "linux") + assert has_relevant_changes([".github/actions/qt-install/action.yml"], "android") + + def test_build_config_triggers_all(self) -> None: + assert has_relevant_changes([".github/build-config.json"], "ios") + + def test_android_dir_only_triggers_android(self) -> None: + assert has_relevant_changes(["android/AndroidManifest.xml"], "android") + assert not has_relevant_changes(["android/AndroidManifest.xml"], "linux") + + def test_docker_linux_patterns(self) -> None: + assert has_relevant_changes(["deploy/docker/Dockerfile-build-ubuntu"], "docker-linux") + assert has_relevant_changes(["deploy/linux/AppImage.sh"], "docker-linux") + assert not has_relevant_changes(["deploy/docker/Dockerfile-build-ubuntu"], "linux") + + def test_docker_android_patterns(self) -> None: + assert has_relevant_changes(["deploy/docker/Dockerfile-build-android"], "docker-android") + assert has_relevant_changes(["android/build.gradle"], "docker-android") + + def test_setup_patterns_linux(self) -> None: + assert has_relevant_changes(["tools/setup/install_dependencies.py"], "linux") + assert has_relevant_changes(["tools/setup/foo-debian.sh"], "linux") + assert not has_relevant_changes(["tools/setup/foo-windows.ps1"], "linux") + + def test_setup_patterns_windows(self) -> None: + assert has_relevant_changes(["tools/setup/foo-windows.ps1"], "windows") + assert not has_relevant_changes(["tools/setup/foo-debian.sh"], "windows") + + def test_setup_patterns_android_matches_all(self) -> None: + assert has_relevant_changes(["tools/setup/anything.py"], "android") + + def test_setup_patterns_ios(self) -> None: + assert has_relevant_changes(["tools/setup/foo-ios.sh"], "ios") + assert has_relevant_changes(["tools/setup/foo-macos.sh"], "ios") + assert not has_relevant_changes(["tools/setup/foo-debian.sh"], "ios") + + def test_empty_files_list(self) -> None: + assert not has_relevant_changes([], "linux") + + def test_empty_strings_ignored(self) -> None: + assert not has_relevant_changes(["", ""], "linux") + + def test_test_dir_triggers(self) -> None: + assert has_relevant_changes(["test/UnitTest.cc"], "linux") + + def test_deploy_platform_triggers(self) -> None: + assert has_relevant_changes(["deploy/macos/Info.plist"], "macos") + assert not has_relevant_changes(["deploy/macos/Info.plist"], "linux") + + def test_scripts_dir_triggers_all(self) -> None: + assert has_relevant_changes([".github/scripts/foo.py"], "windows") + + def test_docker_workflow_triggers_docker_platforms(self) -> None: + assert has_relevant_changes([".github/workflows/docker.yml"], "docker-linux") + assert has_relevant_changes([".github/workflows/docker.yml"], "docker-android") + assert not has_relevant_changes([".github/workflows/docker.yml"], "linux") diff --git a/.github/scripts/tests/test_docker_helper.py b/.github/scripts/tests/test_docker_helper.py new file mode 100644 index 000000000000..af855eb71bd5 --- /dev/null +++ b/.github/scripts/tests/test_docker_helper.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Tests for docker_helper.py.""" + +from __future__ import annotations + +import argparse + +import pytest + +from docker_helper import VALID_BUILD_TYPES, VALID_DOCKERFILES, cmd_validate + + +class TestValidate: + def test_valid_ubuntu_release(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-ubuntu", build_type="Release") + cmd_validate(args) + + def test_valid_android_debug(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-android", build_type="Debug") + cmd_validate(args) + + def test_invalid_dockerfile(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-bad", build_type="Release") + with pytest.raises(SystemExit): + cmd_validate(args) + + def test_invalid_build_type(self) -> None: + args = argparse.Namespace(dockerfile="Dockerfile-build-ubuntu", build_type="BadType") + with pytest.raises(SystemExit): + cmd_validate(args) diff --git a/tools/tests/test_download_artifacts.py b/.github/scripts/tests/test_download_artifacts.py similarity index 85% rename from tools/tests/test_download_artifacts.py rename to .github/scripts/tests/test_download_artifacts.py index 8201e6f7717d..98990f6ea1e7 100644 --- a/tools/tests/test_download_artifacts.py +++ b/.github/scripts/tests/test_download_artifacts.py @@ -1,18 +1,14 @@ #!/usr/bin/env python3 -"""Tests for tools/setup/download_artifacts.py.""" +"""Tests for download_artifacts.py.""" from __future__ import annotations import json import subprocess -import sys from pathlib import Path from unittest.mock import patch -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - -from setup import download_artifacts as mod +import download_artifacts as mod def _cp(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.CompletedProcess: @@ -25,6 +21,7 @@ def test_parse_args_defaults() -> None: assert args.head_sha == "abc123" assert args.output_dir == Path("artifacts") assert args.workflows == "Linux,Windows,MacOS,Android" + assert args.event == "" assert args.artifact_prefixes == "" assert args.artifact_metadata_out == "" @@ -37,7 +34,7 @@ def test_get_workflow_runs_filters_by_name_status_and_conclusion() -> None: {"id": 4, "name": "Other", "status": "completed", "conclusion": "success"}, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) assert len(runs) == 1 @@ -70,7 +67,7 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: }, ] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=all_runs): + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux", "Windows"]) by_name = {run["name"]: run["id"] for run in runs} @@ -78,6 +75,18 @@ def test_get_workflow_runs_keeps_latest_successful_run_per_workflow() -> None: assert by_name["Windows"] == 20 +def test_get_workflow_runs_filters_by_event() -> None: + all_runs = [ + {"id": 10, "name": "Linux", "status": "completed", "conclusion": "success", "event": "push"}, + {"id": 11, "name": "Linux", "status": "completed", "conclusion": "success", "event": "pull_request"}, + ] + + with patch.object(mod, "list_workflow_runs_for_sha", return_value=all_runs): + runs = mod.get_workflow_runs("owner/repo", "abc", ["Linux"], event="pull_request") + + assert [run["id"] for run in runs] == [11] + + def test_download_run_artifacts_failure_returns_false() -> None: with patch.object(mod, "gh", return_value=_cp(stderr="bad", returncode=1)): ok = mod.download_run_artifacts(123, "owner/repo", Path("artifacts")) @@ -107,7 +116,7 @@ def test_select_artifact_names_for_run_filters_by_prefix() -> None: {"name": "emulator-diagnostics-123"}, {"name": "QGroundControl-x86_64.AppImage"}, ] - with patch.object(mod._gh_actions, "list_run_artifacts", return_value=artifacts): + with patch.object(mod, "list_run_artifacts", return_value=artifacts): names = mod.select_artifact_names_for_run( "owner/repo", 42, @@ -129,7 +138,7 @@ def test_list_downloaded_artifacts_filters_extensions(tmp_path: Path) -> None: def test_main_returns_zero_when_no_runs_found() -> None: - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=[]), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=[]), patch.object( mod, "select_latest_successful_runs", return_value=[], ): rc = mod.main(["--repo", "owner/repo", "--head-sha", "abc123"]) @@ -138,7 +147,7 @@ def test_main_returns_zero_when_no_runs_found() -> None: def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( mod, "download_run_artifacts", return_value=False, @@ -156,10 +165,10 @@ def test_main_returns_one_when_downloads_fail_and_no_files(tmp_path: Path) -> No def test_main_returns_two_when_no_artifacts_match_prefixes(tmp_path: Path) -> None: runs = [{"id": 42, "name": "Linux", "status": "completed", "conclusion": "success"}] - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], + mod, "list_run_artifacts", return_value=[{"name": "unrelated-artifact", "size_in_bytes": 1}], ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( @@ -233,8 +242,8 @@ def _artifacts_for_run(repo: str, run_id: int) -> list[dict[str, object]]: downloaded = tmp_path / "coverage.xml" downloaded.write_text("", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( - mod._gh_actions, "list_run_artifacts", side_effect=_artifacts_for_run, + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( + mod, "list_run_artifacts", side_effect=_artifacts_for_run, ), patch.object( mod, "download_run_artifacts", return_value=True, ) as download_mock, patch.object( @@ -264,10 +273,10 @@ def test_main_writes_artifact_metadata_file(tmp_path: Path) -> None: downloaded = tmp_path / "dummy.txt" downloaded.write_text("x", encoding="utf-8") - with patch.object(mod._gh_actions, "list_workflow_runs_for_sha", return_value=runs), patch.object( + with patch.object(mod, "list_workflow_runs_for_sha", return_value=runs), patch.object( mod, "select_latest_successful_runs", return_value=runs, ), patch.object( - mod._gh_actions, "list_run_artifacts", return_value=artifacts, + mod, "list_run_artifacts", return_value=artifacts, ), patch.object( mod, "download_run_artifacts", return_value=True, ), patch.object( diff --git a/.github/scripts/tests/test_install_ccache.py b/.github/scripts/tests/test_install_ccache.py deleted file mode 100644 index 337000b143b9..000000000000 --- a/.github/scripts/tests/test_install_ccache.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Tests for install_ccache.py.""" - -from __future__ import annotations - -import os -import tempfile -from pathlib import Path -from unittest.mock import patch - -import pytest - -from install_ccache import CcacheConfig, CcacheInstaller - - -class TestCcacheInstaller: - """Tests for CcacheInstaller class.""" - - def test_validate_version_valid(self): - """Test valid version formats.""" - assert CcacheInstaller.validate_version("4.12.2") - assert CcacheInstaller.validate_version("4.12") - assert CcacheInstaller.validate_version("5.0.0") - - def test_validate_version_invalid(self): - """Test invalid version formats.""" - assert not CcacheInstaller.validate_version("4.12.2.1") - assert not CcacheInstaller.validate_version("v4.12.2") - assert not CcacheInstaller.validate_version("latest") - assert not CcacheInstaller.validate_version("") - - def test_detect_arch_x86_64(self): - """Test x86_64 architecture detection.""" - with patch("platform.machine", return_value="x86_64"): - assert CcacheInstaller.detect_arch() == "x86_64" - - def test_detect_arch_amd64(self): - """Test amd64 (alias) architecture detection.""" - with patch("platform.machine", return_value="amd64"): - assert CcacheInstaller.detect_arch() == "x86_64" - - def test_detect_arch_arm64(self): - """Test arm64 architecture detection.""" - with patch("platform.machine", return_value="arm64"): - assert CcacheInstaller.detect_arch() == "aarch64" - - def test_detect_arch_aarch64(self): - """Test aarch64 architecture detection.""" - with patch("platform.machine", return_value="aarch64"): - assert CcacheInstaller.detect_arch() == "aarch64" - - def test_default_prefix_from_env(self): - """Test prefix from CCACHE_PREFIX environment variable.""" - with patch.dict(os.environ, {"CCACHE_PREFIX": "/custom/path"}): - assert CcacheInstaller._default_prefix() == Path("/custom/path") - - def test_default_prefix_fallback(self): - """Test default prefix fallback to /usr/local.""" - with patch.dict(os.environ, {}, clear=True): - os.environ.pop("CCACHE_PREFIX", None) - assert CcacheInstaller._default_prefix() == Path("/usr/local") - - def test_read_max_size_from_config(self): - """Test reading max_size from ccache.conf.""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".conf", delete=False) as f: - f.write("max_size = 5G\n") - f.write("compiler_check = content\n") - f.flush() - config_path = Path(f.name) - - try: - installer = CcacheInstaller(config_path=config_path) - assert installer.max_size == "5G" - finally: - config_path.unlink() - - def test_read_max_size_missing_file(self): - """Test max_size default when config file is missing.""" - installer = CcacheInstaller(config_path=Path("/nonexistent/path.conf")) - assert installer.max_size == CcacheInstaller.DEFAULT_MAX_SIZE - - def test_get_config(self): - """Test get_config returns correct named tuple.""" - installer = CcacheInstaller(version="4.10", arch="aarch64") - config = installer.get_config() - - assert isinstance(config, CcacheConfig) - assert config.version == "4.10" - assert config.arch == "aarch64" - assert config.max_size == CcacheInstaller.DEFAULT_MAX_SIZE - - def test_installer_with_custom_prefix(self): - """Test installer with custom prefix.""" - custom_prefix = Path("/opt/ccache") - installer = CcacheInstaller(prefix=custom_prefix) - assert installer.prefix == custom_prefix - - -class TestCcacheConfig: - """Tests for CcacheConfig named tuple.""" - - def test_config_creation(self): - """Test CcacheConfig creation.""" - config = CcacheConfig(version="4.12.2", arch="x86_64", max_size="2G") - assert config.version == "4.12.2" - assert config.arch == "x86_64" - assert config.max_size == "2G" - - def test_config_immutable(self): - """Test CcacheConfig is immutable.""" - config = CcacheConfig(version="4.12.2", arch="x86_64", max_size="2G") - with pytest.raises(AttributeError): - config.version = "5.0.0" diff --git a/.github/scripts/tests/test_plan_docker_builds.py b/.github/scripts/tests/test_plan_docker_builds.py new file mode 100644 index 000000000000..7331e1f68a63 --- /dev/null +++ b/.github/scripts/tests/test_plan_docker_builds.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from plan_docker_builds import plan_builds + + +def test_plan_builds_pull_request_filters_by_changes(): + plan = plan_builds("pull_request", linux_changed=True, android_changed=False) + assert plan["has_jobs"] is True + assert plan["matrix"]["include"] == [ + { + "platform": "Linux", + "dockerfile": "Dockerfile-build-ubuntu", + "fuse": True, + "artifact_pattern": "*.AppImage", + } + ] + + +def test_plan_builds_push_includes_all(): + plan = plan_builds("push", linux_changed=False, android_changed=False) + assert len(plan["matrix"]["include"]) == 2 diff --git a/.github/scripts/tests/test_precommit_results.py b/.github/scripts/tests/test_precommit_results.py new file mode 100644 index 000000000000..a35b78215b27 --- /dev/null +++ b/.github/scripts/tests/test_precommit_results.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from pathlib import Path + +from precommit_results import main + + +def test_precommit_results_writes_json_and_copies_output(tmp_path): + output_file = tmp_path / "pre-commit-output.txt" + output_file.write_text("hook output\n", encoding="utf-8") + out_dir = tmp_path / "artifact" + + result = main( + [ + "--output-dir", + str(out_dir), + "--exit-code", + "1", + "--passed", + "10", + "--failed", + "2", + "--skipped", + "1", + "--run-url", + "https://example.test/run/1", + "--output-file", + str(output_file), + ] + ) + + assert result == 0 + assert (out_dir / "pre-commit-results.json").exists() + assert (out_dir / "pre-commit-output.txt").read_text(encoding="utf-8") == "hook output\n" diff --git a/.github/scripts/tests/test_resolve_gstreamer_config.py b/.github/scripts/tests/test_resolve_gstreamer_config.py new file mode 100644 index 000000000000..af1d56bad700 --- /dev/null +++ b/.github/scripts/tests/test_resolve_gstreamer_config.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Tests for resolve_gstreamer_config.py.""" + +from __future__ import annotations + +from unittest.mock import patch + +from resolve_gstreamer_config import resolve_version + + +def test_resolve_version_prefers_explicit_override() -> None: + assert resolve_version("linux", "1.2.3") == "1.2.3" + + +def test_resolve_version_uses_platform_specific_key() -> None: + with patch("resolve_gstreamer_config.get_build_config_value", return_value="9.9.9") as mock_get: + assert resolve_version("windows", "") == "9.9.9" + mock_get.assert_called_once() diff --git a/.github/scripts/tests/test_test_duration_report.py b/.github/scripts/tests/test_test_duration_report.py new file mode 100644 index 000000000000..b2b95bc16b23 --- /dev/null +++ b/.github/scripts/tests/test_test_duration_report.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import json + +from test_duration_report import analyze_test_durations, main + + +def _write_junit(path, xml: str) -> None: + path.write_text(xml, encoding="utf-8") + + +def test_analyze_test_durations_detects_regression(tmp_path): + junit = tmp_path / "junit.xml" + baseline = tmp_path / "baseline.json" + _write_junit( + junit, + """ + + + """, + ) + baseline.write_text(json.dumps({"tests": {"Suite::slow": {"seconds": 2.0}}}), encoding="utf-8") + + report = analyze_test_durations( + junit, + baseline, + top_n=5, + slow_threshold=5.0, + regression_factor=1.5, + min_delta=5.0, + ) + + assert report["slow_count"] == 1 + assert report["regression_count"] == 1 + + +def test_test_duration_report_main_missing_junit(tmp_path): + summary = tmp_path / "summary.md" + output = tmp_path / "output.txt" + result = main( + [ + "--junit-path", + str(tmp_path / "missing.xml"), + "--github-step-summary", + str(summary), + "--github-output", + str(output), + ] + ) + assert result == 0 + assert "regression_count=0" in output.read_text(encoding="utf-8") + assert "WARNING" in summary.read_text(encoding="utf-8") diff --git a/.github/scripts/tests/test_workflow_runs.py b/.github/scripts/tests/test_workflow_runs.py deleted file mode 100644 index 460fe9bbdb5c..000000000000 --- a/.github/scripts/tests/test_workflow_runs.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for workflow_runs.py.""" - -from __future__ import annotations - -import json - -import workflow_runs as mod - - -def test_parse_json_documents_handles_paginated_stream() -> None: - payload = ( - json.dumps({"workflow_runs": [{"id": 1, "name": "Linux"}]}) - + "\n" - + json.dumps({"workflow_runs": [{"id": 2, "name": "Windows"}]}) - ) - - docs = mod.parse_json_documents(payload) - assert len(docs) == 2 - assert docs[0]["workflow_runs"][0]["id"] == 1 - assert docs[1]["workflow_runs"][0]["id"] == 2 - - -def test_list_workflow_runs_delegates_to_shared_helper(monkeypatch) -> None: - def fake_list(repo: str, head_sha: str) -> list[dict[str, object]]: - assert repo == "owner/repo" - assert head_sha == "abc123" - return [{"id": 1, "name": "Linux"}] - - monkeypatch.setattr(mod, "_list_workflow_runs_for_sha", fake_list) - runs = mod.list_workflow_runs("owner/repo", "abc123") - assert runs == [{"id": 1, "name": "Linux"}] diff --git a/.github/scripts/verify_executable.py b/.github/scripts/verify_executable.py new file mode 100644 index 000000000000..efaf89dabe53 --- /dev/null +++ b/.github/scripts/verify_executable.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Verify QGroundControl executable with a boot test. + +Resolves paths, sets permissions, and delegates to run_tests.py --verify-only. +""" + +from __future__ import annotations + +import argparse +import os +import stat +import subprocess +import sys +from pathlib import Path + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary-path", required=True) + parser.add_argument("--working-dir", default="") + parser.add_argument("--type", default="binary", dest="exe_type") + parser.add_argument("--timeout", default="60") + args = parser.parse_args() + + binary_path = Path(args.binary_path) + work_dir = Path(args.working_dir) if args.working_dir else binary_path.parent + + if not work_dir.is_dir(): + print(f"::error::Working directory not found: {work_dir}", file=sys.stderr) + sys.exit(1) + + binary_name = binary_path.name + run_binary = binary_name + headless = True + + # AppImages typically don't ship the "offscreen" Qt platform plugin. + if args.exe_type == "appimage" or binary_name.endswith(".AppImage"): + headless = False + + if os.name != "nt": + exe = work_dir / binary_name + try: + exe.chmod(exe.stat().st_mode | stat.S_IEXEC) + except OSError: + pass + run_binary = str(exe.resolve()) + + workspace = os.environ.get("GITHUB_WORKSPACE", ".") + cmd = [ + sys.executable, os.path.join(workspace, "tools", "run_tests.py"), + "--binary", run_binary, + "--timeout", args.timeout, + "--verify-only", + "-v", + ] + if headless: + cmd.append("--headless") + + result = subprocess.run(cmd, cwd=str(work_dir), check=False) + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/workflow_runs.py b/.github/scripts/workflow_runs.py deleted file mode 100644 index 93efc8b64064..000000000000 --- a/.github/scripts/workflow_runs.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -"""Helpers for querying GitHub Actions workflow runs via gh CLI.""" - -from __future__ import annotations - -import sys -from pathlib import Path -from typing import Any - -# Ensure repository root is importable when scripts are executed by path. -_REPO_ROOT = Path(__file__).resolve().parents[2] -_COMMON_DIR = _REPO_ROOT / "tools" / "common" -if str(_COMMON_DIR) not in sys.path: - sys.path.insert(0, str(_COMMON_DIR)) - -import gh_actions as _gh_actions - -_gh = _gh_actions.gh -parse_json_documents = _gh_actions.parse_json_documents -list_run_artifacts = _gh_actions.list_run_artifacts -_list_workflow_runs_for_sha = _gh_actions.list_workflow_runs_for_sha - - -def parse_csv_list(value: str) -> list[str]: - """Parse comma-separated values into a trimmed non-empty list.""" - return [item.strip() for item in value.split(",") if item.strip()] - - -def list_workflow_runs(repo: str, head_sha: str) -> list[dict[str, Any]]: - """List workflow runs for a commit SHA across all pages.""" - return _list_workflow_runs_for_sha(repo, head_sha) diff --git a/.github/scripts/xml_utils.py b/.github/scripts/xml_utils.py new file mode 100644 index 000000000000..125d03dff2c1 --- /dev/null +++ b/.github/scripts/xml_utils.py @@ -0,0 +1,27 @@ +"""Safe XML parsing with defusedxml fallback.""" + +from pathlib import Path + +try: + from defusedxml.ElementTree import ParseError as XMLParseError + from defusedxml.ElementTree import parse as _xml_parse_impl + + _USING_DEFUSEDXML = True +except ImportError: + from xml.etree.ElementTree import ParseError as XMLParseError + from xml.etree.ElementTree import parse as _xml_parse_impl + + _USING_DEFUSEDXML = False + + +def xml_parse(path): + """Parse XML safely, rejecting DTD/ENTITY declarations when defusedxml is unavailable.""" + if _USING_DEFUSEDXML: + return _xml_parse_impl(path) + text = Path(path).read_text(encoding="utf-8", errors="replace") + if "- -DCMAKE_WARN_DEPRECATED=FALSE + -DCMAKE_TOOLCHAIN_FILE=${{ steps.qt-android.outputs.target_qt_root_dir }}/lib/cmake/Qt6/qt.toolchain.cmake + -DCMAKE_PREFIX_PATH=${{ steps.qt-android.outputs.target_qt_root_dir }} -DQT_ANDROID_ABIS=${{ env.QT_ANDROID_ABIS }} - -DQT_HOST_PATH=${{ env.QT_ROOT_DIR }}/../${{ matrix.qt_host_path }} + -DQT_HOST_PATH=${{ steps.qt-android.outputs.host_qt_root_dir }} -DQT_ANDROID_SIGN_APK=ON - name: Build @@ -353,32 +352,25 @@ jobs: - name: Upload Emulator Diagnostics if: ${{ failure() && matrix.emulator }} - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: emulator-diagnostics-${{ matrix.host }}-${{ github.run_id }}-${{ github.run_attempt }} path: ${{ runner.temp }}/emulator-diagnostics if-no-files-found: warn retention-days: 14 - - name: Attest Build with SBOM + - name: Attest and Upload if: ${{ !matrix.emulator }} - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk + artifact-name: ${{ env.PACKAGE }}.apk + package-name: ${{ matrix.primary && env.PACKAGE || format('{0}-{1}', env.PACKAGE, matrix.host) }} subject-name: ${{ env.PACKAGE }}-${{ matrix.host }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - if: ${{ !matrix.emulator }} - uses: ./.github/actions/upload - with: - artifact_name: ${{ env.PACKAGE }}.apk - package_name: ${{ matrix.primary && env.PACKAGE || format('{0}-{1}', env.PACKAGE, matrix.host) }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - 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: ${{ matrix.primary }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + 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: ${{ matrix.primary }} - name: Deploy to Play Store if: ${{ !matrix.emulator && matrix.primary && github.ref_type == 'tag' && env.HAS_PLAYSTORE_SECRET == 'true' }} diff --git a/.github/workflows/build-gstreamer.yml b/.github/workflows/build-gstreamer.yml index cb7c5c1a19a6..d1435da49971 100644 --- a/.github/workflows/build-gstreamer.yml +++ b/.github/workflows/build-gstreamer.yml @@ -57,6 +57,7 @@ concurrency: permissions: contents: read + id-token: write jobs: build: diff --git a/.github/workflows/build-results.yml b/.github/workflows/build-results.yml index 502036db47d0..c8c7c5c00847 100644 --- a/.github/workflows/build-results.yml +++ b/.github/workflows/build-results.yml @@ -56,23 +56,78 @@ jobs: const exact = prs.find(pr => pr.head?.sha === run.head_sha); return exact ? exact.number : null; - - name: Checkout + - name: Check if all platform workflows are complete + id: gate if: steps.pr.outputs.result != 'null' + uses: actions/github-script@v8 + env: + PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + with: + script: | + const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim())); + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: process.env.HEAD_SHA, + event: 'pull_request', + per_page: 100 + } + ); + const latest = new Map(); + for (const run of runs) { + if (!wanted.has(run.name)) continue; + const prev = latest.get(run.name); + if (!prev || new Date(run.created_at) > new Date(prev.created_at)) { + latest.set(run.name, run); + } + } + const missing = [...wanted].filter(n => !latest.has(n)); + const incomplete = [...latest.values()].filter(r => r.status !== 'completed'); + const ready = missing.length === 0 && incomplete.length === 0; + if (!ready) { + const reasons = []; + if (missing.length) reasons.push(`missing: ${missing.join(', ')}`); + if (incomplete.length) reasons.push(`still running: ${incomplete.map(r => r.name).join(', ')}`); + core.info(`Skipping early — ${reasons.join('; ')}`); + } + core.setOutput('ready', ready ? 'true' : 'false'); + + - name: Checkout + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: actions/checkout@v6 with: sparse-checkout: | .github/actions/download-all-artifacts .github/actions/collect-artifact-sizes + .github/actions/setup-python .github/scripts/collect_build_status.py .github/scripts/collect_artifact_sizes.py - .github/scripts/workflow_runs.py + .github/scripts/ci_bootstrap.py .github/scripts/generate_build_results_comment.py + .github/scripts/templates/build_results.md.j2 + .github/scripts/xml_utils.py + tools/common/build_config.py + tools/common/file_traversal.py tools/common/gh_actions.py - tools/setup/download_artifacts.py + tools/common/github_runs.py + tools/pyproject.toml + tools/uv.lock + tools/setup/setup_bootstrap.py + tools/setup/install_python.py + .github/scripts/download_artifacts.py sparse-checkout-cone-mode: false + - name: Setup Python dependencies + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' + uses: ./.github/actions/setup-python + with: + groups: scripts + - name: Collect build status - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' id: builds env: PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} @@ -94,6 +149,7 @@ jobs: with: head-sha: ${{ github.event.workflow_run.head_sha }} workflows: ${{ env.PLATFORM_WORKFLOWS }} + event: pull_request runs-file: workflow-runs-cache.json artifact-prefixes: coverage-report,test-results- artifact-metadata-file: workflow-artifacts.json @@ -104,6 +160,7 @@ jobs: with: head-sha: ${{ github.event.workflow_run.head_sha }} workflows: ${{ env.PLATFORM_WORKFLOWS }} + event: pull_request output-file: pr-sizes.json runs-file: workflow-runs-cache.json artifacts-file: workflow-artifacts.json @@ -111,7 +168,7 @@ jobs: - name: Download pre-commit results artifact if: steps.pr.outputs.result != 'null' && steps.builds.outputs.all_complete == 'true' && steps.builds.outputs.precommit_run_id != '' continue-on-error: true - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: run-id: ${{ steps.builds.outputs.precommit_run_id }} github-token: ${{ github.token }} @@ -135,7 +192,7 @@ jobs: key: coverage-baseline-latest - name: Generate combined report - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' env: BUILD_TABLE: ${{ steps.builds.outputs.table }} BUILD_SUMMARY: ${{ steps.builds.outputs.summary }} @@ -155,7 +212,7 @@ jobs: cat comment.md - name: Deduplicate prior build-results comments - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: actions/github-script@v8 env: PR_NUMBER: ${{ steps.pr.outputs.result }} @@ -190,7 +247,7 @@ jobs: } - name: Post combined comment - if: steps.pr.outputs.result != 'null' + if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true' uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.pr.outputs.result }} @@ -212,20 +269,74 @@ jobs: contents: read steps: + - name: Check if all platform workflows succeeded + id: gate + uses: actions/github-script@v8 + env: + PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + with: + script: | + const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim())); + const runs = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: process.env.HEAD_SHA, + event: 'push', + per_page: 100 + } + ); + const latest = new Map(); + for (const run of runs) { + if (!wanted.has(run.name)) continue; + const prev = latest.get(run.name); + if (!prev || new Date(run.created_at) > new Date(prev.created_at)) { + latest.set(run.name, run); + } + } + const missing = [...wanted].filter(n => !latest.has(n)); + const failed = [...latest.values()].filter(r => r.status !== 'completed' || r.conclusion !== 'success'); + const ready = missing.length === 0 && failed.length === 0; + if (!ready) { + const reasons = []; + if (missing.length) reasons.push(`missing: ${missing.join(', ')}`); + if (failed.length) reasons.push(`not successful: ${failed.map(r => `${r.name} (${r.status}/${r.conclusion})`).join(', ')}`); + core.info(`Skipping baseline save — ${reasons.join('; ')}`); + } + core.setOutput('ready', ready ? 'true' : 'false'); + - name: Checkout + if: steps.gate.outputs.ready == 'true' uses: actions/checkout@v6 with: sparse-checkout: | .github/actions/download-all-artifacts .github/actions/collect-artifact-sizes - .github/scripts/collect_artifact_sizes.py + .github/actions/setup-python .github/scripts/check_baseline_ready.py - .github/scripts/workflow_runs.py + .github/scripts/collect_artifact_sizes.py + .github/scripts/ci_bootstrap.py + tools/common/build_config.py + tools/common/file_traversal.py tools/common/gh_actions.py - tools/setup/download_artifacts.py + tools/common/github_runs.py + tools/pyproject.toml + tools/uv.lock + tools/setup/setup_bootstrap.py + tools/setup/install_python.py + .github/scripts/download_artifacts.py sparse-checkout-cone-mode: false + - name: Setup Python dependencies + if: steps.gate.outputs.ready == 'true' + uses: ./.github/actions/setup-python + with: + groups: scripts + - name: Verify all platform workflows succeeded for baseline SHA + if: steps.gate.outputs.ready == 'true' id: ready env: PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }} @@ -247,6 +358,7 @@ jobs: with: head-sha: ${{ github.event.workflow_run.head_sha }} workflows: ${{ env.PLATFORM_WORKFLOWS }} + event: push runs-file: workflow-runs-cache.json artifact-prefixes: coverage-report,test-results- artifact-metadata-file: workflow-artifacts.json @@ -257,6 +369,7 @@ jobs: with: head-sha: ${{ github.event.workflow_run.head_sha }} workflows: ${{ env.PLATFORM_WORKFLOWS }} + event: push output-file: baseline-sizes.json runs-file: workflow-runs-cache.json artifacts-file: workflow-artifacts.json diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index b7e4a0f1b664..3e759c532925 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -66,7 +66,7 @@ jobs: fail: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - name: Upload report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: link-check-report diff --git a/.github/workflows/ci-scripts.yml b/.github/workflows/ci-scripts.yml index 431942368dfd..2e3012dd3db5 100644 --- a/.github/workflows/ci-scripts.yml +++ b/.github/workflows/ci-scripts.yml @@ -9,6 +9,7 @@ on: - '.github/build-config.json' - 'tools/setup/**' - 'tools/*.py' + - 'tools/uv.lock' - 'tools/tests/**' - 'tools/common/**' - 'tools/generators/**' @@ -29,6 +30,7 @@ on: - '.github/build-config.json' - 'tools/setup/**' - 'tools/*.py' + - 'tools/uv.lock' - 'tools/tests/**' - 'tools/common/**' - 'tools/generators/**' @@ -96,6 +98,8 @@ jobs: .github/actions/setup-python .github/build-config.json .github/scripts + tools/pyproject.toml + tools/uv.lock tools/*.py tools/setup tools/tests @@ -112,7 +116,7 @@ jobs: - name: Setup Python uses: ./.github/actions/setup-python with: - groups: test + groups: scripts,test python-version: '3.12' - name: Python syntax smoke test diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 98c3485d6877..f147001c49fb 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -25,33 +25,45 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: - upload: - name: Upload Sources - if: >- - github.repository_owner == 'mavlink' && - ( - github.event_name == 'push' || - (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || - (github.event_name == 'workflow_dispatch' && inputs.action == 'upload') - ) + sync: + name: Crowdin Sync + if: github.repository_owner == 'mavlink' runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: read steps: + - name: Determine action + id: action + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_ACTION: ${{ inputs.action }} + PR_MERGED: ${{ github.event.pull_request.merged }} + run: | + if [[ "$EVENT_NAME" == "schedule" ]] || [[ "$EVENT_NAME" == "workflow_dispatch" && "$INPUT_ACTION" == "download" ]]; then + echo "mode=download" >> "$GITHUB_OUTPUT" + elif [[ "$EVENT_NAME" == "push" ]] || [[ "$EVENT_NAME" == "pull_request" && "$PR_MERGED" == "true" ]] || [[ "$EVENT_NAME" == "workflow_dispatch" && "$INPUT_ACTION" == "upload" ]]; then + echo "mode=upload" >> "$GITHUB_OUTPUT" + else + echo "mode=skip" >> "$GITHUB_OUTPUT" + fi + - name: Harden Runner + if: steps.action.outputs.mode != 'skip' uses: step-security/harden-runner@v2 with: egress-policy: audit - name: Checkout + if: steps.action.outputs.mode != 'skip' uses: actions/checkout@v6 - name: Upload sources to Crowdin + if: steps.action.outputs.mode == 'upload' uses: crowdin/github-action@v2 with: config: 'docs/crowdin_docs.yml' @@ -64,30 +76,8 @@ jobs: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_DOCS_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.PX4BUILDBOT_CROWDIN_PERSONAL_TOKEN }} - download: - name: Download Translations - if: >- - github.repository_owner == 'mavlink' && - ( - github.event_name == 'schedule' || - (github.event_name == 'workflow_dispatch' && inputs.action == 'download') - ) - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - contents: write - pull-requests: write - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - name: Download translations from Crowdin + if: steps.action.outputs.mode == 'download' uses: crowdin/github-action@v2 with: config: 'docs/crowdin_docs.yml' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 755b67166a7a..bebf5f1763cd 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -7,6 +7,12 @@ on: - 'android/**' - '**/*.gradle' - '**/*.gradle.kts' + - 'requirements*.txt' + - '**/requirements*.txt' + - 'pyproject.toml' + - '**/pyproject.toml' + - 'setup.py' + - '**/setup.py' concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8609f85bed1e..429fa750f1cd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,8 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths-ignore: - - 'docs/**' workflow_dispatch: concurrency: @@ -29,31 +27,26 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'pull_request' timeout-minutes: 5 + permissions: + contents: read outputs: - linux: ${{ steps.filter.outputs.linux }} - android: ${{ steps.filter.outputs.android }} - shared: ${{ steps.filter.outputs.shared }} + linux: ${{ steps.detect-linux.outputs.any }} + android: ${{ steps.detect-android.outputs.any }} steps: - - name: Detect changed paths - uses: dorny/paths-filter@v3 - id: filter + - name: Checkout repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Detect Linux changes + id: detect-linux + uses: ./.github/actions/detect-changes + with: + platform: docker-linux + - name: Detect Android changes + id: detect-android + uses: ./.github/actions/detect-changes with: - filters: | - shared: - - 'src/**' - - 'cmake/**' - - 'CMakeLists.txt' - - 'tools/setup/**' - - '.github/workflows/docker.yml' - - '.github/actions/docker/**' - - '.github/build-config.json' - linux: - - 'deploy/linux/**' - - 'deploy/docker/Dockerfile-build-ubuntu' - android: - - 'android/**' - - 'deploy/android/**' - - 'deploy/docker/Dockerfile-build-android' + platform: docker-android plan-builds: name: Plan Docker Builds @@ -65,63 +58,29 @@ jobs: matrix: ${{ steps.plan.outputs.matrix }} has_jobs: ${{ steps.plan.outputs.has_jobs }} steps: + - name: Checkout planner + uses: actions/checkout@v6 + with: + fetch-depth: 1 + sparse-checkout: | + .github/scripts/plan_docker_builds.py + .github/scripts/ci_bootstrap.py + tools/common/gh_actions.py + sparse-checkout-cone-mode: false + - name: Plan matrix id: plan env: EVENT_NAME: ${{ github.event_name }} - SHARED: ${{ needs.changes.outputs.shared }} LINUX: ${{ needs.changes.outputs.linux }} ANDROID: ${{ needs.changes.outputs.android }} - run: | - python3 - <<'PY' - import json - import os - from pathlib import Path - - event_name = os.environ.get("EVENT_NAME", "") - shared = os.environ.get("SHARED", "") == "true" - linux_changed = os.environ.get("LINUX", "") == "true" - android_changed = os.environ.get("ANDROID", "") == "true" - - include: list[dict[str, object]] = [] - - linux_selected = event_name != "pull_request" or shared or linux_changed - android_selected = event_name != "pull_request" or shared or android_changed - - if linux_selected: - include.append( - { - "platform": "Linux", - "dockerfile": "Dockerfile-build-ubuntu", - "fuse": True, - "artifact_pattern": "*.AppImage", - } - ) - if android_selected: - include.append( - { - "platform": "Android", - "dockerfile": "Dockerfile-build-android", - "fuse": False, - "artifact_pattern": "*.apk", - } - ) - - matrix = {"include": include} - has_jobs = "true" if include else "false" - matrix_json = json.dumps(matrix, separators=(",", ":")) - - output_path = Path(os.environ["GITHUB_OUTPUT"]) - with output_path.open("a", encoding="utf-8") as f: - f.write(f"matrix={matrix_json}\n") - f.write(f"has_jobs={has_jobs}\n") - PY + run: python3 "${GITHUB_WORKSPACE}/.github/scripts/plan_docker_builds.py" build: name: Docker ${{ matrix.platform }} runs-on: ubuntu-latest needs: [changes, plan-builds] - if: always() && !cancelled() && needs.plan-builds.outputs.has_jobs == 'true' + if: always() && !cancelled() && needs.plan-builds.outputs.has_jobs == 'true' && vars.DOCKER_BUILD_ENABLED == 'true' timeout-minutes: 120 strategy: @@ -213,7 +172,7 @@ jobs: - name: Scan artifact for vulnerabilities if: steps.artifact.outputs.found == 'true' - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.35.0 with: scan-type: 'fs' scan-ref: ${{ steps.artifact.outputs.path }} diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 9e10da04256e..47963303727d 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -14,6 +14,10 @@ on: - 'package*.json' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + permissions: contents: read @@ -48,7 +52,7 @@ jobs: touch docs/.vitepress/dist/.nojekyll - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: qgc_docs_build path: docs/.vitepress/dist/ @@ -66,35 +70,12 @@ jobs: with: egress-policy: audit - - name: Download Artifact - uses: actions/download-artifact@v7 - with: - name: qgc_docs_build - path: ~/_book - - - name: Checkout docs repo - uses: actions/checkout@v6 - with: - repository: mavlink/docs.qgroundcontrol.com - token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} - path: docs.qgroundcontrol.com - - name: Deploy docs - env: - BRANCH: ${{ env.BRANCH_NAME }} - run: | - # Sanitize branch name to prevent path traversal and shell injection - SAFE_BRANCH="${BRANCH//[^a-zA-Z0-9._-]/_}" - rm -rf "docs.qgroundcontrol.com/${SAFE_BRANCH}" - mkdir -p "docs.qgroundcontrol.com/${SAFE_BRANCH}" - cp -r ~/_book/* "docs.qgroundcontrol.com/${SAFE_BRANCH}/" - cd docs.qgroundcontrol.com - git config user.email "bot@px4.io" - git config user.name "PX4BuildBot" - git add "${SAFE_BRANCH}" - if git diff --cached --quiet; then - echo "No documentation changes to deploy." - exit 0 - fi - git commit -m "QGC docs build update $(date -u +%Y-%m-%d)" - git push origin master + uses: ./.github/actions/deploy-docs + with: + artifact-name: qgc_docs_build + target-repo: mavlink/docs.qgroundcontrol.com + target-branch: master + deploy-token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} + source-branch: ${{ env.BRANCH_NAME }} + commit-message: 'QGC docs build update' diff --git a/.github/workflows/doxygen_deploy.yml b/.github/workflows/doxygen_deploy.yml index df4dcd03c57d..0b470526bf1c 100644 --- a/.github/workflows/doxygen_deploy.yml +++ b/.github/workflows/doxygen_deploy.yml @@ -37,7 +37,7 @@ jobs: run: doxygen Doxyfile - name: Upload artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: doxygen_build path: docs/api/html/ @@ -55,34 +55,12 @@ jobs: with: egress-policy: audit - - name: Download Artifact - uses: actions/download-artifact@v7 - with: - name: doxygen_build - path: ~/_api_docs - - - name: Checkout API docs repo - uses: actions/checkout@v6 - with: - repository: mavlink/api.qgroundcontrol.com - token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} - path: api.qgroundcontrol.com - - name: Deploy docs - env: - BRANCH: ${{ env.BRANCH_NAME }} - run: | - SAFE_BRANCH="${BRANCH//[^a-zA-Z0-9._-]/_}" - rm -rf "api.qgroundcontrol.com/${SAFE_BRANCH}" - mkdir -p "api.qgroundcontrol.com/${SAFE_BRANCH}" - cp -r ~/_api_docs/* "api.qgroundcontrol.com/${SAFE_BRANCH}/" - cd api.qgroundcontrol.com - git config user.email "bot@px4.io" - git config user.name "PX4BuildBot" - git add "${SAFE_BRANCH}" - if git diff --cached --quiet; then - echo "No documentation changes to deploy." - exit 0 - fi - git commit -m "QGC Doxygen API docs update $(date -u +%Y-%m-%d)" - git push origin main + uses: ./.github/actions/deploy-docs + with: + artifact-name: doxygen_build + target-repo: mavlink/api.qgroundcontrol.com + target-branch: main + deploy-token: ${{ secrets.PX4BUILDBOT_ACCESSTOKEN }} + source-branch: ${{ env.BRANCH_NAME }} + commit-message: 'QGC Doxygen API docs update' diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index c8ec0f4c963d..14f5512c3e6b 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -34,7 +34,7 @@ jobs: cache-key: flatpak-builder-${{ hashFiles('deploy/linux/org.mavlink.qgroundcontrol.flatpak.yml') }} - name: Upload Flatpak - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: QGroundControl-flatpak path: QGroundControl.flatpak diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9f5d5e661cd5..3452977cf0ea 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -13,6 +13,7 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: build: @@ -58,7 +59,6 @@ jobs: xcode-version: ${{ steps.config.outputs.xcode_ios_version }} cpm-modules: ${{ runner.temp }}/build/cpm_modules build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Configure uses: ./.github/actions/cmake-configure @@ -74,17 +74,10 @@ jobs: build-dir: ${{ runner.temp }}/build build-type: ${{ matrix.build_type }} - - name: Attest Build with SBOM + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.app - subject-name: ${{ env.PACKAGE }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - uses: ./.github/actions/upload - with: - artifact_name: ${{ env.PACKAGE }}.app - package_name: ${{ env.PACKAGE }} - upload_aws: 'false' + artifact-name: ${{ env.PACKAGE }}.app + package-name: ${{ env.PACKAGE }} + upload-aws: 'false' diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 74ab664af963..43f49a58edf9 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -10,16 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/linux.yml' - - '.github/actions/**' - - 'deploy/linux/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'tools/setup/*debian*' - - 'tools/setup/install_dependencies.py' merge_group: workflow_dispatch: inputs: @@ -31,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -46,8 +35,15 @@ permissions: checks: write jobs: + changes: + uses: ./.github/workflows/_detect-changes.yml + with: + platform: linux + build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: ${{ matrix.os }} timeout-minutes: 120 @@ -77,6 +73,7 @@ jobs: steps: - name: Harden Runner + if: runner.arch != 'ARM64' uses: step-security/harden-runner@v2 with: egress-policy: audit @@ -93,7 +90,6 @@ jobs: qt-host: ${{ matrix.host }} qt-arch: ${{ matrix.arch }} build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install Dependencies uses: ./.github/actions/install-dependencies @@ -128,6 +124,29 @@ jobs: with: category: "/language:c-cpp" + - name: Run Unit Tests + if: matrix.build_type == 'Debug' + uses: ./.github/actions/run-unit-tests + with: + build-dir: ${{ runner.temp }}/build + junit-output: junit-results-linux-${{ matrix.arch }}.xml + ctest-output: test-output-linux-${{ matrix.arch }}.txt + include-labels: 'Unit|Integration' + exclude-labels: 'Flaky|Network' + + - name: Report Test Results + if: always() && !cancelled() && matrix.build_type == 'Debug' + uses: ./.github/actions/test-report + with: + name: Unit Tests (${{ matrix.arch }}) + build-dir: ${{ runner.temp }}/build + junit-file: junit-results-linux-${{ matrix.arch }}.xml + output-file: test-output-linux-${{ matrix.arch }}.txt + artifact-name: test-results-${{ matrix.arch }} + retention-days: 7 + trunk-org-slug: ${{ vars.TRUNK_ORG_SLUG }} + trunk-token: ${{ secrets.TRUNK_TOKEN }} + - name: Verify Executable if: matrix.build_type == 'Release' uses: ./.github/actions/verify-executable @@ -143,7 +162,7 @@ jobs: - name: Upload metrics artifact if: matrix.build_type == 'Release' && matrix.os == 'ubuntu-22.04' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: size-metrics path: ${{ runner.temp }}/build/metrics.json @@ -161,28 +180,21 @@ jobs: binary-path: ${{ runner.temp }}/build/${{ matrix.package }}.AppImage type: appimage - - name: Attest Build with SBOM + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom + uses: ./.github/actions/attest-and-upload with: - subject-path: ${{ runner.temp }}/build/${{ matrix.package }}.AppImage - subject-name: ${{ matrix.package }} - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File - if: matrix.build_type == 'Release' - uses: ./.github/actions/upload - with: - artifact_name: ${{ matrix.package }}.AppImage - package_name: ${{ matrix.package }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} + artifact-name: ${{ matrix.package }}.AppImage + package-name: ${{ matrix.package }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} debug-validation: name: ${{ matrix.job_name }} - if: ${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.build_type == 'Debug') }} + needs: changes + if: ${{ !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') && (github.event_name != 'workflow_dispatch' || inputs.build_type == 'Debug') }} runs-on: ubuntu-22.04 timeout-minutes: ${{ matrix.timeout_minutes }} @@ -190,11 +202,11 @@ jobs: fail-fast: false matrix: include: - - job_name: Test + Coverage linux_gcc_64 Debug - mode: coverage - timeout_minutes: 60 - fetch_depth: 0 - fetch_tags: false + # - job_name: Test + Coverage linux_gcc_64 Debug + # mode: coverage + # timeout_minutes: 60 + # fetch_depth: 0 + # fetch_tags: false - job_name: Sanitizers linux_gcc_64 Debug (ASan+UBSan) mode: sanitizers timeout_minutes: 120 @@ -218,7 +230,7 @@ jobs: fetch-tags: ${{ matrix.fetch_tags }} - name: Restore file timestamps - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: chetan/git-restore-mtime-action@v2 - name: Build Setup @@ -228,13 +240,12 @@ jobs: qt-host: linux qt-arch: linux_gcc_64 build-type: Debug - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install Dependencies uses: ./.github/actions/install-dependencies - name: Configure (Coverage) - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/cmake-configure with: build-dir: ${{ runner.temp }}/build @@ -244,7 +255,7 @@ jobs: stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} - name: Build (Coverage) - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/cmake-build with: build-dir: ${{ runner.temp }}/build @@ -268,7 +279,7 @@ jobs: build-type: Debug - name: Run Unit Tests - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/run-unit-tests with: build-dir: ${{ runner.temp }}/build @@ -296,7 +307,7 @@ jobs: parallel: '1' - name: Analyze Unit Test Durations - if: always() && !cancelled() && matrix.mode == 'coverage' + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/test-duration-report with: junit-path: ${{ runner.temp }}/build/junit-results-linux-linux_gcc_64.xml @@ -322,7 +333,7 @@ jobs: fail-on-regression: 'false' - name: Report Test Results - if: always() && !cancelled() && matrix.mode == 'coverage' + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/test-report with: name: Unit Tests (linux_gcc_64) @@ -335,8 +346,8 @@ jobs: trunk-token: ${{ secrets.TRUNK_TOKEN }} - name: Upload Duration Report - if: always() && !cancelled() && matrix.mode == 'coverage' - uses: actions/upload-artifact@v6 + if: always() && !cancelled() && matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' + uses: actions/upload-artifact@v7 with: name: test-duration-linux_gcc_64 path: ${{ runner.temp }}/build/test-duration-linux-linux_gcc_64.json @@ -344,7 +355,7 @@ jobs: - name: Upload Test Results (Sanitizers) if: always() && !cancelled() && matrix.mode == 'sanitizers' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: test-results-linux-sanitizers path: | @@ -354,21 +365,24 @@ jobs: retention-days: 7 - name: Ensure coverage tools are available - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' run: | - if ! command -v ccache >/dev/null 2>&1; then - sudo apt-get install -y --no-install-recommends ccache - fi - # Ensure ccache is at /usr/local/bin where the build's ccache-launcher expects it - if [ ! -f /usr/local/bin/ccache ] && command -v ccache >/dev/null 2>&1; then - sudo ln -s "$(command -v ccache)" /usr/local/bin/ccache + ccache_path="$(command -v ccache 2>/dev/null)" || { + echo "Error: ccache not found — expected pinned binary from cache action" >&2 + exit 1 + } + echo "ccache found at: ${ccache_path}" + if [[ "${ccache_path}" != "/usr/local/bin/ccache" ]]; then + echo "Warning: ccache resolved to ${ccache_path}, expected /usr/local/bin/ccache" >&2 fi + ccache_ver="$(ccache --version | head -1)" + echo "ccache version: ${ccache_ver}" if ! command -v gcovr >/dev/null 2>&1; then pipx install gcovr fi - name: Verify coverage data files exist - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' working-directory: ${{ runner.temp }}/build run: | gcda_count=$(find . -name '*.gcda' | wc -l | tr -d '[:space:]') @@ -379,14 +393,14 @@ jobs: fi - name: Coverage Report - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' uses: ./.github/actions/coverage with: build-dir: ${{ runner.temp }}/build mode: report-only - name: Verify Coverage Thresholds - if: matrix.mode == 'coverage' + if: matrix.mode == 'coverage' && vars.COVERAGE_ENABLED == 'true' working-directory: ${{ runner.temp }}/build run: | line_thresh=$(cmake -L -N . 2>/dev/null | sed -n 's/^QGC_COVERAGE_LINE_THRESHOLD:STRING=//p') @@ -395,10 +409,12 @@ jobs: branch_thresh="${branch_thresh:-20}" python3 - "$line_thresh" "$branch_thresh" <<'PY' - import sys - import xml.etree.ElementTree as ET + import os, sys from pathlib import Path + sys.path.insert(0, os.path.join(os.environ["GITHUB_WORKSPACE"], ".github", "scripts")) + from xml_utils import xml_parse + line_thresh = float(sys.argv[1]) branch_thresh = float(sys.argv[2]) @@ -406,7 +422,7 @@ jobs: print("::warning::coverage.xml not found, skipping threshold check") sys.exit(0) - tree = ET.parse("coverage.xml") + tree = xml_parse("coverage.xml") cov = tree.getroot() lines_valid = int(cov.get("lines-valid", 0)) line_rate = float(cov.get("line-rate", 0)) * 100 @@ -440,117 +456,3 @@ jobs: sys.exit(1 if failed else 0) PY - size-metrics: - name: Size Metrics - if: ${{ !cancelled() && (github.event_name != 'workflow_dispatch' || inputs.build_type != 'Debug') }} - runs-on: ubuntu-24.04 - timeout-minutes: 10 - needs: build - permissions: - actions: write - contents: read - pull-requests: write - - steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Download metrics artifact - uses: actions/download-artifact@v7 - with: - name: size-metrics - path: . - - - name: Save baseline - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - run: mkdir -p baseline && cp metrics.json baseline/metrics.json - - name: Delete stale baseline cache - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && hashFiles('baseline/metrics.json') != '' - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: gh actions-cache delete size-baseline-latest -R "${GH_REPO}" --confirm || true - - - name: Save baseline to cache - uses: actions/cache/save@v5 - if: github.event_name == 'push' && github.ref == 'refs/heads/master' && hashFiles('baseline/metrics.json') != '' - with: - path: baseline/metrics.json - key: size-baseline-latest - - - name: Restore baseline - if: github.event_name == 'pull_request' - id: baseline - uses: actions/cache/restore@v5 - with: - path: baseline/metrics.json - key: size-baseline-latest - - - name: Compare with baseline - if: github.event_name == 'pull_request' && hashFiles('baseline/metrics.json') != '' - uses: actions/github-script@v8 - with: - script: | - if (!context.issue.number) { - core.info('No PR number available; skipping size comparison comment.'); - return; - } - const fs = require('fs'); - const current = JSON.parse(fs.readFileSync('metrics.json', 'utf8')); - const baseline = JSON.parse(fs.readFileSync('baseline/metrics.json', 'utf8')); - - const find = (arr, name) => arr.find(m => m.name === name)?.value || 0; - const fmt = (b) => (b / 1048576).toFixed(2); - - const metrics = ['Binary Size', 'Stripped Size'].map(name => { - const cur = find(current, name); - const base = find(baseline, name); - const delta = base > 0 ? ((cur - base) / base * 100).toFixed(1) : 'N/A'; - return { name, current: cur, baseline: base, delta }; - }); - - const rows = metrics.map(m => - `| ${m.name} | ${fmt(m.baseline)} MB | ${fmt(m.current)} MB | ${m.delta === 'N/A' ? m.delta : m.delta + '%'} |` - ).join('\n'); - - const body = `\n` + - `## Binary Size Report\n\n` + - `| Metric | Baseline | PR | Change |\n` + - `|--------|----------|-----|--------|\n${rows}\n`; - - const threshold = 5; - const binaryDelta = metrics[0].delta; - if (binaryDelta !== 'N/A' && parseFloat(binaryDelta) > threshold) { - core.warning(`Binary size increased by ${binaryDelta}% (threshold: ${threshold}%)`); - } - - const comments = await github.paginate( - github.rest.issues.listComments, - { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - per_page: 100, - } - ); - const existing = comments.find(c => - c.user?.type === 'Bot' && c.body?.includes('') - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } diff --git a/.github/workflows/lupdate.yml b/.github/workflows/lupdate.yml index 5def9be2ddd3..86ce62f3da96 100644 --- a/.github/workflows/lupdate.yml +++ b/.github/workflows/lupdate.yml @@ -1,6 +1,8 @@ name: Update Translations on: + schedule: + - cron: '0 3 * * 0' # Every Sunday at 03:00 UTC workflow_dispatch: concurrency: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 69a7893b848b..8fcfa977afe9 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -10,14 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/macos.yml' - - '.github/actions/**' - - 'deploy/macos/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' merge_group: workflow_dispatch: inputs: @@ -29,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -40,13 +31,22 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: + changes: + uses: ./.github/workflows/_detect-changes.yml + with: + platform: macos + build: + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: macos-15 timeout-minutes: 120 strategy: + fail-fast: false matrix: build_type: ${{ inputs.build_type && fromJSON(format('["{0}"]', inputs.build_type)) || fromJSON('["Release"]') }} @@ -55,11 +55,6 @@ jobs: shell: bash steps: - - name: Harden Runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - name: Checkout repo uses: actions/checkout@v6 with: @@ -74,7 +69,6 @@ jobs: qt-host: mac qt-arch: clang_64 build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 @@ -141,21 +135,13 @@ jobs: with: binary-path: /Volumes/QGroundControl/QGroundControl.app/Contents/MacOS/QGroundControl - - name: Attest Build with SBOM - if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom - with: - subject-path: ${{ runner.temp }}/build/QGroundControl.dmg - subject-name: QGroundControl - scan-path: ${{ runner.temp }}/build - - - name: Upload Build File + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/upload + uses: ./.github/actions/attest-and-upload with: - artifact_name: QGroundControl.dmg - package_name: QGroundControl - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} + artifact-name: QGroundControl.dmg + package-name: QGroundControl + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + aws-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-distribution-id: ${{ secrets.AWS_DISTRIBUTION_ID }} diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 66cae5648af6..a204d7bcb416 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -65,7 +65,7 @@ jobs: run: python3 ./.github/scripts/find_binary.py --build-dir build --build-type Release - name: Upload binary - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: qgc-binary path: | @@ -95,6 +95,8 @@ jobs: .github/actions/build-config .github/actions/qt-install .github/scripts/benchmark_runner.py + tools/common/build_config.py + tools/setup/setup_bootstrap.py tools/setup/read_config.py .github/build-config.json sparse-checkout-cone-mode: false @@ -112,7 +114,7 @@ jobs: modules: ${{ steps.config.outputs.qt_modules }} - name: Download binary - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: qgc-binary path: build diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9aedaf436668..d9951b3f4500 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -5,15 +5,7 @@ on: branches: - master - 'Stable*' - paths: - - 'src/**' - - 'test/**' - - '.pre-commit-config.yaml' pull_request: - paths: - - 'src/**' - - 'test/**' - - '.pre-commit-config.yaml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -75,7 +67,7 @@ jobs: continue-on-error: true env: PRE_COMMIT_OUTPUT: pre-commit-output.txt - run: ./tools/pre-commit.sh --ci + run: python3 ./tools/pre_commit.py --ci - name: Prepare pre-commit results artifact if: github.event_name == 'pull_request' && always() @@ -86,42 +78,18 @@ jobs: SKIPPED: ${{ steps.pre-commit.outputs.skipped }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} run: | - mkdir -p pre-commit-results - - : "${EXIT_CODE:=1}" - : "${PASSED:=0}" - : "${FAILED:=0}" - : "${SKIPPED:=0}" - - python3 - <<'PY' - import json - import os - from pathlib import Path - - output_path = Path("pre-commit-results/pre-commit-results.json") - output_path.write_text( - json.dumps( - { - "exit_code": os.environ.get("EXIT_CODE", "1"), - "passed": os.environ.get("PASSED", "0"), - "failed": os.environ.get("FAILED", "0"), - "skipped": os.environ.get("SKIPPED", "0"), - "run_url": os.environ.get("RUN_URL", ""), - }, - indent=2, - ) - + "\n", - encoding="utf-8", - ) - PY - - if [[ -f pre-commit-output.txt ]]; then - cp pre-commit-output.txt pre-commit-results/pre-commit-output.txt - fi + python3 "${GITHUB_WORKSPACE}/.github/scripts/precommit_results.py" \ + --output-dir pre-commit-results \ + --exit-code "${EXIT_CODE:-1}" \ + --passed "${PASSED:-0}" \ + --failed "${FAILED:-0}" \ + --skipped "${SKIPPED:-0}" \ + --run-url "${RUN_URL}" \ + --output-file pre-commit-output.txt - name: Upload pre-commit results artifact if: github.event_name == 'pull_request' && always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: pre-commit-results path: pre-commit-results diff --git a/.github/workflows/px4-metadata.yml b/.github/workflows/px4-metadata.yml new file mode 100644 index 000000000000..b1ed35e09e10 --- /dev/null +++ b/.github/workflows/px4-metadata.yml @@ -0,0 +1,49 @@ +name: Update PX4 Metadata + +on: + schedule: + - cron: '0 6 * * *' # Daily at 6:00 AM UTC + workflow_dispatch: + +concurrency: + group: 'px4-metadata' + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + update-px4-metadata: + if: github.repository_owner == 'mavlink' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@v6 + + - name: Download PX4 airframe metadata + run: curl -fsSL https://artifacts.px4.io/Firmware/_general/airframes.xml -o src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml + + - name: Download PX4 parameter metadata + run: curl -fsSL https://artifacts.px4.io/Firmware/_general/parameters.xml -o src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + branch: update-px4-metadata + title: "chore: update PX4 metadata" + commit-message: "chore: update PX4 metadata" + body: | + This PR was automatically generated by GitHub Actions to update the bundled PX4 metadata files. + + Updated files: + - `src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml` (airframe definitions) + - `src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml` (parameter catalog) + + Source: https://artifacts.px4.io/Firmware/_general/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d89a0f0434ce..b54f1ee43e25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,11 @@ name: Release -# Disabled: semantic-release blocked by branch protection (can't push CHANGELOG.md) -# To fix: either remove @semantic-release/git plugin or use a PAT from allowed pusher +# TODO: Disabled — semantic-release blocked by branch protection (can't push CHANGELOG.md) +# Fix options: +# 1. Remove @semantic-release/git plugin (skip CHANGELOG commit) +# 2. Use a PAT from an allowed pusher +# 3. Use a GitHub App token with bypass permissions +# Until fixed, this workflow only runs via manual workflow_dispatch. on: workflow_dispatch: @@ -122,7 +126,7 @@ jobs: artifact-name: sbom-cyclonedx - name: Download artifacts - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: path: artifacts merge-multiple: true @@ -131,28 +135,28 @@ jobs: run: find artifacts -type f | head -50 || echo "No artifacts found" - name: Attest Linux AppImage - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl*.AppImage sbom-path: sbom.spdx.json continue-on-error: true - name: Attest macOS DMG - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl.dmg sbom-path: sbom.spdx.json continue-on-error: true - name: Attest Windows EXE - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl*.exe sbom-path: sbom.spdx.json continue-on-error: true - name: Attest Android APK - uses: actions/attest-sbom@v3 + uses: actions/attest-sbom@v4 with: subject-path: artifacts/**/QGroundControl.apk sbom-path: sbom.spdx.json diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 1eb2d135cf79..2e0cb75441fb 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -10,15 +10,6 @@ on: paths-ignore: - 'docs/**' pull_request: - paths: - - '.github/workflows/windows.yml' - - '.github/actions/**' - - 'deploy/windows/**' - - 'src/**' - - 'test/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'tools/setup/*windows*' merge_group: workflow_dispatch: inputs: @@ -30,7 +21,6 @@ on: options: - Release - Debug - workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -41,10 +31,18 @@ permissions: id-token: write attestations: write actions: read + security-events: write jobs: + changes: + uses: ./.github/workflows/_detect-changes.yml + with: + platform: windows + build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} + needs: changes + if: always() && !cancelled() && (needs.changes.outputs.should_build == 'true' || needs.changes.result == 'skipped') runs-on: ${{ matrix.os }} timeout-minutes: 120 @@ -81,10 +79,10 @@ jobs: shell: cmd steps: - # - name: Harden Runner - # uses: step-security/harden-runner@v2 - # with: - # egress-policy: audit + - name: Harden Runner + uses: step-security/harden-runner@v2 + with: + egress-policy: audit - name: Checkout repo uses: actions/checkout@v6 @@ -100,7 +98,6 @@ jobs: qt-arch: ${{ matrix.arch }} qt-version: ${{ matrix.qt_version || '' }} build-type: ${{ matrix.build_type }} - save-cache: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - name: Install GStreamer if: matrix.arch == 'win64_msvc2022_64' @@ -113,6 +110,17 @@ jobs: with: arch: ${{ (matrix.arch == 'win64_msvc2022_64' && 'x64') || (matrix.arch == 'win64_msvc2022_arm64' && 'arm64') || 'amd64_arm64' }} + - name: Install host Qt for ARM64 cross-compilation + if: matrix.arch == 'win64_msvc2022_arm64_cross_compiled' + id: qt-host + uses: ./.github/actions/qt-install + with: + version: ${{ steps.build-setup.outputs.qt_version }} + host: windows + arch: win64_msvc2022_64 + modules: ${{ steps.build-setup.outputs.qt_modules }} + export-env: 'false' + - name: Fix Rust linker resolution (ARM64 native) if: matrix.variant == 'arm64' shell: bash @@ -134,7 +142,7 @@ jobs: stable: ${{ (github.ref_type == 'tag' || contains(github.ref, 'Stable')) && 'true' || 'false' }} extra-args: >- -DQGC_ENABLE_GST_VIDEOSTREAMING=${{ matrix.arch == 'win64_msvc2022_64' && 'ON' || 'OFF' }} - ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && format('-DQT_HOST_PATH={0}/../msvc2022_64', env.QT_ROOT_DIR) || '' }} + ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && format('-DQT_HOST_PATH={0}', steps.qt-host.outputs.qt_root_dir) || '' }} - name: Build uses: ./.github/actions/cmake-build @@ -153,22 +161,14 @@ jobs: working-directory: ${{ runner.temp }}\build run: cmake --install . --config ${{ matrix.build_type }} - - name: Attest Build with SBOM - if: matrix.build_type == 'Release' - uses: ./.github/actions/attest-sbom - with: - subject-path: ${{ runner.temp }}/build/${{ matrix.package }}.exe - subject-name: ${{ matrix.package }} - scan-path: ${{ runner.temp }}/build - - - name: Upload artifact (installer) + - name: Attest and Upload if: matrix.build_type == 'Release' - uses: ./.github/actions/upload + uses: ./.github/actions/attest-and-upload with: - artifact_name: ${{ matrix.package }}.exe - package_name: ${{ matrix.package }} - aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} - 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: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }} + artifact-name: ${{ matrix.package }}.exe + package-name: ${{ matrix.package }} + aws-role-arn: ${{ secrets.AWS_ROLE_ARN }} + 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: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }} diff --git a/.gitignore b/.gitignore index feb50168cf3c..d8daa9afd380 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,7 @@ tags # ------------------------------------------------------------------------------ # Build System & CMake # ------------------------------------------------------------------------------ -build*/ -!.github/actions/build-setup/ -!.github/actions/build-setup/action.yml +/build*/ cmake-build-*/ out/ CMakeFiles @@ -79,7 +77,6 @@ bin/mac *.dll # Build tool artifacts -Makefile *.ninja .ninja_log build.ninja diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8fab6eae708a..0cf210641680 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,7 @@ repos: rev: v1.1.407 hooks: - id: pyright - files: ^(tools|test)/.*\.py$ + files: ^(tools|test|\.github/scripts)/.*\.py$ # Shell scripts - repo: https://github.com/shellcheck-py/shellcheck-py @@ -107,7 +107,6 @@ repos: rev: v1.37.1 hooks: - id: yamllint - args: ['-d', '{extends: default, rules: {line-length: {max: 200}, document-start: disable, truthy: {check-keys: false}, comments: {min-spaces-from-content: 1}}}'] # Markdown linting (report-only, doesn't auto-fix) - repo: https://github.com/igorshubovych/markdownlint-cli diff --git a/.qmlformat.ini b/.qmlformat.ini index 0ada5ddfb3d3..967e78da00c7 100644 --- a/.qmlformat.ini +++ b/.qmlformat.ini @@ -5,7 +5,7 @@ IndentWidth=4 UseTabs=false NormalizeOrder=true -NewlineType=native +NewlineType=unix ObjectsSpacing=true FunctionsSpacing=true SortImports=true diff --git a/.vscode/launch.default.json b/.vscode/launch.default.json index eed7731d54fc..e51a6123c663 100644 --- a/.vscode/launch.default.json +++ b/.vscode/launch.default.json @@ -17,18 +17,13 @@ "externalConsole": false, "name": "Launch QGroundControl", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "ignoreFailures": true, "text": "-enable-pretty-printing" - }, - { - "description": "Load Qt pretty printers", - "ignoreFailures": true, - "text": "source ${workspaceFolder}/tools/gdb-pretty-printers/qt6.py" } ], "stopAtEntry": false, @@ -43,7 +38,7 @@ }, "name": "Launch QGroundControl (LLDB)", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "type": "lldb" }, @@ -57,7 +52,7 @@ "externalConsole": false, "name": "Run Unit Tests (Debug)", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { @@ -79,7 +74,7 @@ "externalConsole": false, "name": "Debug Specific Test", "preLaunchTask": "CMake: Build", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "launch", "setupCommands": [ { @@ -95,7 +90,7 @@ "MIMode": "gdb", "name": "Attach to QGroundControl", "processId": "${command:pickProcess}", - "program": "${workspaceFolder}/build/QGroundControl", + "program": "${workspaceFolder}/build/Debug/QGroundControl", "request": "attach", "setupCommands": [ { diff --git a/.vscode/tasks.default.json b/.vscode/tasks.default.json index 37a5c4636786..15b183330245 100644 --- a/.vscode/tasks.default.json +++ b/.vscode/tasks.default.json @@ -9,10 +9,14 @@ "tasks": [ { "args": [ - "--preset", - "default" + "-B", + "build", + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Debug", + "-DQGC_BUILD_TESTING=ON" ], - "command": "cmake", + "command": "qt-cmake", "group": "build", "label": "CMake: Configure (Debug)", "problemMatcher": [], @@ -20,10 +24,14 @@ }, { "args": [ - "--preset", - "default-release" + "-B", + "build", + "-G", + "Ninja", + "-DCMAKE_BUILD_TYPE=Release", + "-DQGC_BUILD_TESTING=OFF" ], - "command": "cmake", + "command": "qt-cmake", "group": "build", "label": "CMake: Configure (Release)", "problemMatcher": [], @@ -59,9 +67,11 @@ }, { "args": [ - "--unittest" + "--test-dir", + "build", + "--output-on-failure" ], - "command": "${workspaceFolder}/build/QGroundControl", + "command": "ctest", "dependsOn": "CMake: Build", "group": { "isDefault": true, @@ -73,9 +83,13 @@ }, { "args": [ - "--unittest:${input:testClass}" + "--test-dir", + "build", + "--output-on-failure", + "-R", + "${input:testClass}" ], - "command": "${workspaceFolder}/build/QGroundControl", + "command": "ctest", "dependsOn": "CMake: Build", "group": "test", "label": "Run Specific Test", @@ -84,33 +98,47 @@ }, { "args": [ - "--check" + "./tools/analyze.py", + "--tool", + "clang-format" ], - "command": "${workspaceFolder}/tools/format-check.sh", + "command": "python3", "group": "none", "label": "Format: Check Changed Files", "problemMatcher": [], "type": "shell" }, { - "args": [], - "command": "${workspaceFolder}/tools/format-check.sh", + "args": [ + "./tools/analyze.py", + "--tool", + "clang-format", + "--fix" + ], + "command": "python3", "group": "none", "label": "Format: Fix Changed Files", "problemMatcher": [], "type": "shell" }, { - "command": "${workspaceFolder}/tools/analyze.sh", + "args": [ + "./tools/analyze.py" + ], + "command": "python3", "group": "none", "label": "Static Analysis", "problemMatcher": "$gcc", "type": "shell" }, { - "command": "${workspaceFolder}/tools/clean.sh", + "args": [ + "run", + "--all-files" + ], + "command": "pre-commit", "group": "none", - "label": "Clean All", + "label": "Pre-commit: Run All", "problemMatcher": [], "type": "shell" }, diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 000000000000..3283565553bc --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,12 @@ +extends: default + +rules: + document-start: disable + line-length: + max: 200 + comments-indentation: disable + truthy: + allowed-values: ["true", "false"] + check-keys: false + comments: + min-spaces-from-content: 1 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..71476374aa37 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,97 @@ +# AGENTS.md + +Instructions for AI coding agents (Codex, Claude Code, etc.) working on QGroundControl. + +## Quick References + +- [CODING_STYLE.md](CODING_STYLE.md) — Naming, formatting, C++20 features, QML style, logging +- [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md) — Architecture patterns (Fact System, Multi-Vehicle, FirmwarePlugin) +- [.github/copilot-instructions.md](.github/copilot-instructions.md) — Code structure, CI structure, quick patterns +- [tools/README.md](tools/README.md) — Development scripts and tooling + +## Build & Test Commands + +```bash +# Configure (Release) +cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + +# Build +cmake --build build --config Release --parallel + +# Run unit tests +cd build && ctest --output-on-failure -L Unit --parallel $(nproc) + +# Lint (pre-commit hooks) +pre-commit run --all-files + +# Format C++ +clang-format -i path/to/changed/files.cc + +# CI Python script tests +cd .github/scripts && PYTHONPATH=. python3 -m pytest tests/ -q + +# Tools Python tests +cd tools && uv run --extra scripts --extra test pytest tests/ -q +``` + +## Golden Rules + +1. **Fact System**: ALL vehicle parameters use Facts. Never create custom parameter storage. +2. **Multi-Vehicle**: ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()` +3. **Firmware Plugin**: Use `vehicle->firmwarePlugin()` for firmware-specific behavior. +4. **QML Sizing**: Use `ScreenTools.defaultFontPixelHeight/Width`, never hardcoded values. +5. **QML Colors**: Use `QGCPalette`, never hardcoded colors. +6. **Match existing style**: Follow conventions of surrounding code. See CODING_STYLE.md. + +## Project Layout + +``` +src/ # C++/QML application source +├── Vehicle/ # Vehicle state and communications +├── FactSystem/ # Parameter management (Fact, FactGroup, FactMetaData) +├── FirmwarePlugin/ # PX4/ArduPilot abstraction +├── MissionManager/ # Mission planning +├── MAVLink/ # Protocol handling +├── QmlControls/ # Reusable QML components +└── Settings/ # Persistent settings + +tools/ # Development scripts and shared Python modules +├── common/ # Shared Python modules (gh_actions, build_config, etc.) +├── setup/ # Environment setup scripts +├── analyzers/ # Static analysis tools +└── pyproject.toml # Python dependencies (uv) + +.github/ +├── workflows/ # CI workflows (linux, macos, windows, android, ios) +├── actions/ # Composite actions (cmake-build, run-unit-tests, etc.) +├── scripts/ # CI-specific Python scripts +│ ├── common/ # Shared CI modules +│ └── templates/ # Jinja2 templates +└── build-config.json # Centralized version numbers +``` + +## CI Conventions + +- Platform workflows share logic via composite actions and reusable workflows. +- CI Python scripts use `httpx` for GitHub API and `jinja2` for templating. +- Bootstrap scripts (`install_dependencies.py`, `ccache_helper.py`) use stdlib only. +- Version numbers live in `.github/build-config.json`. +- Use `common.gh_actions.write_github_output()` for `$GITHUB_OUTPUT` writes. + +## C++ Key Patterns + +```cpp +// Always null-check vehicle +Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle(); +if (!vehicle) return; + +// Access parameters via Fact System +Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME"); +if (param) param->setCookedValue(newValue); +``` + +```qml +// QML vehicle access +property var vehicle: QGroundControl.multiVehicleManager.activeVehicle +enabled: vehicle && vehicle.armed +``` diff --git a/CODING_STYLE.md b/CODING_STYLE.md index c86e0b9936d1..573c752363fa 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -130,8 +130,24 @@ Q_DECLARE_LOGGING_CATEGORY(MyComponentLog) QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name") // Use categorized logging + +// qCDebug is used for general logging. These logs only display when turned on. +// Do not use qCInfo, always use qCDebug. qCDebug(MyComponentLog) << "Debug message"; + +// qCWarning is used for logging of error flows which are handled but unusual. +// For example the vehicle failed to respond to a request. +// These logs will display even when the category is not enabled to display. qCWarning(MyComponentLog) << "Warning message"; + +// qCCritical is used to indicate a coding error. +// Example: An internal Fact is using an unsupported Fact type. +// These logs will cause unit tests to fail if they are hit. +// These logs will display even when the category is not enabled to display. +qCCritical(MyComponentLog) << "Internal Error: ..."; + +// Never use uncategorized logging +qDebug() << "..."; // This is wrong ``` ## Qt6 / QML Integration diff --git a/Makefile b/Makefile index f677f7c4f18d..d91993fdf59d 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ help: @echo " Qt version: $(QT_VERSION)" # Configuration - reads from centralized config -QT_VERSION := $(shell ./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1") +QT_VERSION := $(shell python3 ./tools/setup/read_config.py --get qt_version 2>/dev/null || echo "6.10.1") QT_DIR ?= $(HOME)/Qt/$(QT_VERSION)/gcc_64 BUILD_TYPE ?= Debug BUILD_DIR := build @@ -51,7 +51,7 @@ submodules: deps: @echo "Installing dependencies (requires sudo)..." - sudo ./tools/setup/install-dependencies-debian.sh + python3 ./tools/setup/install_dependencies.py --platform debian # Build targets configure: submodules @@ -81,22 +81,22 @@ lint: pre-commit run --all-files format: - ./tools/analyze.sh --tool clang-format + python3 ./tools/analyze.py --tool clang-format format-fix: - ./tools/analyze.sh --tool clang-format --fix + python3 ./tools/analyze.py --tool clang-format --fix analyze: - ./tools/analyze.sh + python3 ./tools/analyze.py coverage: - ./tools/coverage.sh + python3 ./tools/coverage.py check: lint test # Other targets run: - ./$(BUILD_DIR)/staging/QGroundControl + ./$(BUILD_DIR)/$(BUILD_TYPE)/QGroundControl docs: npm run docs:build diff --git a/README.md b/README.md index 78788ca279c6..1087445ee051 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Latest Release + Latest Release

diff --git a/THREAD_SAFETY_ANALYSIS.md b/THREAD_SAFETY_ANALYSIS.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/cmake/BuildConfig.cmake b/cmake/BuildConfig.cmake index 789d94ad7203..e5b9a553f732 100644 --- a/cmake/BuildConfig.cmake +++ b/cmake/BuildConfig.cmake @@ -22,8 +22,10 @@ endfunction() qgc_config_get_value(QGC_CONFIG_QT_VERSION "qt_version") qgc_config_get_value(QGC_CONFIG_QT_MINIMUM_VERSION "qt_minimum_version") -qgc_config_get_value(QGC_CONFIG_GSTREAMER_VERSION "gstreamer_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_VERSION "gstreamer_default_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_MIN_VERSION "gstreamer_minimum_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_ANDROID_VERSION "gstreamer_android_version") +qgc_config_get_value(QGC_CONFIG_GSTREAMER_IOS_VERSION "gstreamer_ios_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_MACOS_VERSION "gstreamer_macos_version") qgc_config_get_value(QGC_CONFIG_GSTREAMER_WIN_VERSION "gstreamer_windows_version") qgc_config_get_value(QGC_CONFIG_NDK_VERSION "ndk_version") @@ -35,4 +37,21 @@ qgc_config_get_value(QGC_CONFIG_CMAKE_MINIMUM "cmake_minimum_version") qgc_config_get_value(QGC_CONFIG_MACOS_DEPLOYMENT_TARGET "macos_deployment_target") qgc_config_get_value(QGC_CONFIG_IOS_DEPLOYMENT_TARGET "ios_deployment_target") +# Extract patch versions from platform strings (e.g., "1.22.12" -> QGC_GSTREAMER_PATCH_1_22=12) +foreach(_gst_ver_var QGC_CONFIG_GSTREAMER_ANDROID_VERSION QGC_CONFIG_GSTREAMER_IOS_VERSION QGC_CONFIG_GSTREAMER_MACOS_VERSION QGC_CONFIG_GSTREAMER_WIN_VERSION) + if(DEFINED ${_gst_ver_var}) + string(REPLACE "." ";" _ver_parts "${${_gst_ver_var}}") + list(LENGTH _ver_parts _ver_len) + if(_ver_len EQUAL 3) + list(GET _ver_parts 0 _major) + list(GET _ver_parts 1 _minor) + list(GET _ver_parts 2 _patch) + set(_patch_var "QGC_GSTREAMER_PATCH_${_major}_${_minor}") + if(NOT DEFINED ${_patch_var}) + set(${_patch_var} "${_patch}" CACHE STRING "GStreamer ${_major}.${_minor} patch version (from ${_gst_ver_var})") + endif() + endif() + endif() +endforeach() + message(STATUS "BuildConfig: Qt ${QGC_CONFIG_QT_VERSION}, GStreamer ${QGC_CONFIG_GSTREAMER_VERSION}, NDK ${QGC_CONFIG_NDK_VERSION}") diff --git a/cmake/CustomOptions.cmake b/cmake/CustomOptions.cmake index 84f86ea59216..481e989d4917 100644 --- a/cmake/CustomOptions.cmake +++ b/cmake/CustomOptions.cmake @@ -62,9 +62,6 @@ set(QGC_VALGRIND_TIMEOUT_MULTIPLIER 20 CACHE STRING "Timeout multiplier for Valg option(QGC_ENABLE_BZIP2 "Enable BZip2 decompression support" OFF) option(QGC_ENABLE_LZ4 "Enable LZ4 decompression support" OFF) -# MAVLink Inspector is disabled by default due to GPL licensing of QtCharts -# option(QGC_DISABLE_MAVLINK_INSPECTOR "Disable MAVLink Inspector" OFF) - # ============================================================================ # Communication Options # ============================================================================ @@ -78,7 +75,6 @@ option(QGC_NO_SERIAL_LINK "Disable serial port communication" OFF) option(QGC_ENABLE_UVC "Enable UVC (USB Video Class) device support" ON) option(QGC_ENABLE_GST_VIDEOSTREAMING "Enable GStreamer video backend" ON) -cmake_dependent_option(QGC_CUSTOM_GST_PACKAGE "Use QGC-provided GStreamer packages" OFF "QGC_ENABLE_GST_VIDEOSTREAMING" OFF) option(QGC_ENABLE_QT_VIDEOSTREAMING "Enable QtMultimedia video backend" OFF) # ============================================================================ diff --git a/cmake/find-modules/FindGStreamer.cmake b/cmake/find-modules/FindGStreamer.cmake deleted file mode 100644 index 200da8569dd9..000000000000 --- a/cmake/find-modules/FindGStreamer.cmake +++ /dev/null @@ -1,672 +0,0 @@ -# ============================================================================ -# FindGStreamer.cmake -# CMake find module for GStreamer multimedia framework -# -# Handles multiple platforms: Windows, Linux, macOS, Android, iOS -# Supports both static and dynamic linking -# ============================================================================ - -# ---------------------------------------------------------------------------- -# Default Configuration -# ---------------------------------------------------------------------------- - -# Set default version based on platform (from build-config.json) -if(NOT DEFINED GStreamer_FIND_VERSION) - if(LINUX) - set(GStreamer_FIND_VERSION 1.20) - elseif(WIN32) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_WIN_VERSION}) - elseif(ANDROID) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_ANDROID_VERSION}) - elseif(MACOS OR IOS) - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) - else() - set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_VERSION}) - endif() -endif() - -# Determine GStreamer root directory from various environment variables -if(NOT DEFINED GStreamer_ROOT_DIR) - if(DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) - elseif(DEFINED GStreamer_ROOT) - set(GStreamer_ROOT_DIR ${GStreamer_ROOT}) - endif() - - if(DEFINED GStreamer_ROOT_DIR AND NOT EXISTS "${GStreamer_ROOT_DIR}") - message(STATUS "GStreamer: User-provided directory does not exist: ${GStreamer_ROOT_DIR}") - endif() -endif() - -# Static vs dynamic linking preference -if(NOT DEFINED GStreamer_USE_STATIC_LIBS) - if(ANDROID OR IOS) - set(GStreamer_USE_STATIC_LIBS ON) - else() - set(GStreamer_USE_STATIC_LIBS OFF) - endif() -endif() - -# Framework usage (macOS/iOS only) -if(NOT DEFINED GStreamer_USE_FRAMEWORK) - if(APPLE) - set(GStreamer_USE_FRAMEWORK ON) - else() - set(GStreamer_USE_FRAMEWORK OFF) - endif() -endif() - -# ============================================================================ -# Platform-Specific Configuration -# ============================================================================ - -macro(_gst_normalize_and_validate_root) - cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "GStreamer: SDK not found at '${GStreamer_ROOT_DIR}' — " - "check installation or set GStreamer_ROOT_DIR") - endif() -endmacro() - -set(PKG_CONFIG_ARGN) - -# ---------------------------------------------------------------------------- -# Windows Platform -# ---------------------------------------------------------------------------- -if(WIN32) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(DEFINED ENV{GSTREAMER_1_0_ROOT_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_X86_64}") - elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") - elseif(MINGW AND DEFINED ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") - set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") - elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_x86_64") - set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_x86_64") - elseif(EXISTS "C:/gstreamer/1.0/msvc_x86_64") - set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_x86_64") - endif() - endif() - - _gst_normalize_and_validate_root() - - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config.exe") - # CACHE FORCE ensures find_package(PkgConfig) sees the bundled exe even when - # no system pkg-config is on PATH (common in CI containers). - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig;$ENV{PKG_CONFIG_PATH}") - list(APPEND PKG_CONFIG_ARGN - --dont-define-prefix - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# Linux Platform -# ---------------------------------------------------------------------------- -elseif(LINUX) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(EXISTS "/usr") - set(GStreamer_ROOT_DIR "/usr") - endif() - endif() - - _gst_normalize_and_validate_root() - - if((EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") - elseif((EXISTS "${GStreamer_ROOT_DIR}/lib64") AND (EXISTS "${GStreamer_ROOT_DIR}/lib64/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib64") - elseif((EXISTS "${GStreamer_ROOT_DIR}/lib") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/gstreamer-1.0")) - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - else() - message(FATAL_ERROR "Could not locate GStreamer libraries - check installation or set environment/cmake variables") - endif() - - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") - -# ---------------------------------------------------------------------------- -# Android Platform -# ---------------------------------------------------------------------------- -elseif(ANDROID) - set(_gst_freedesktop_url "https://gstreamer.freedesktop.org/data/pkg/android/${GStreamer_FIND_VERSION}/gstreamer-1.0-android-universal-${GStreamer_FIND_VERSION}.tar.xz") - set(_gst_s3_url "https://qgroundcontrol.s3.us-west-2.amazonaws.com/dependencies/gstreamer/android/qgc-android-gstreamer-${GStreamer_FIND_VERSION}.tar.xz") - - set(_gst_android_hash "") - - if(QGC_CUSTOM_GST_PACKAGE) - set(_gst_android_urls "${_gst_s3_url}") - set(_gst_source_variant "custom") - elseif(CMAKE_ANDROID_ARCH_ABI MATCHES "^(x86|x86_64)$") - # S3 custom package only contains ARM architectures; use freedesktop universal tarball for x86 - set(_gst_android_urls "${_gst_freedesktop_url}") - set(_gst_source_variant "universal") - # Fetch checksum only for freedesktop-only downloads where it can be verified - set(_gst_checksum_url "${_gst_freedesktop_url}.sha256sum") - if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(_gst_checksum_dir "$ENV{CPM_SOURCE_CACHE}") - else() - set(_gst_checksum_dir "${CMAKE_BINARY_DIR}/_deps") - endif() - set(_gst_checksum_file "${_gst_checksum_dir}/gstreamer-android-${GStreamer_FIND_VERSION}.sha256sum") - if(NOT EXISTS "${_gst_checksum_file}") - file(MAKE_DIRECTORY "${_gst_checksum_dir}") - file(DOWNLOAD "${_gst_checksum_url}" "${_gst_checksum_file}" - STATUS _checksum_status TIMEOUT 30 TLS_VERIFY ON) - list(GET _checksum_status 0 _checksum_code) - if(NOT _checksum_code EQUAL 0) - file(REMOVE "${_gst_checksum_file}") - endif() - endif() - if(EXISTS "${_gst_checksum_file}") - file(READ "${_gst_checksum_file}" _checksum_content) - string(STRIP "${_checksum_content}" _checksum_content) - string(REGEX MATCH "([0-9a-fA-F]{64})" _checksum_match "${_checksum_content}") - if(_checksum_match) - set(_gst_android_hash "SHA256=${CMAKE_MATCH_1}") - endif() - endif() - else() - set(_gst_android_urls "${_gst_freedesktop_url}" "${_gst_s3_url}") - set(_gst_source_variant "universal") - endif() - - # Download with retry before passing to CPM (avoids FetchContent timeout failures) - if(DEFINED ENV{CPM_SOURCE_CACHE}) - set(_gst_download_dir "$ENV{CPM_SOURCE_CACHE}/gstreamer-android") - else() - set(_gst_download_dir "${CMAKE_BINARY_DIR}/_deps/gstreamer-android") - endif() - set(_gst_archive_name "gstreamer-android-${_gst_source_variant}-${GStreamer_FIND_VERSION}.tar.xz") - set(_gst_archive_path "${_gst_download_dir}/${_gst_archive_name}") - - # Extract raw hash from "SHA256=" format - set(_gst_expected_hash "") - if(_gst_android_hash) - string(REGEX MATCH "=([0-9a-fA-F]+)$" _gst_hash_match "${_gst_android_hash}") - if(_gst_hash_match) - set(_gst_expected_hash "${CMAKE_MATCH_1}") - endif() - endif() - - # Verify cached archive integrity - set(_gst_need_download TRUE) - if(EXISTS "${_gst_archive_path}") - if(_gst_expected_hash) - file(SHA256 "${_gst_archive_path}" _gst_cached_hash) - if(_gst_cached_hash STREQUAL "${_gst_expected_hash}") - message(STATUS "GStreamer: Using cached ${_gst_archive_path} (SHA256 verified)") - set(_gst_need_download FALSE) - else() - message(STATUS "GStreamer: Cached archive failed SHA256 check, re-downloading") - file(REMOVE "${_gst_archive_path}") - endif() - else() - message(STATUS "GStreamer: Using cached ${_gst_archive_path}") - set(_gst_need_download FALSE) - endif() - endif() - - if(_gst_need_download) - file(MAKE_DIRECTORY "${_gst_download_dir}") - set(_gst_downloaded FALSE) - foreach(_gst_url IN LISTS _gst_android_urls) - message(STATUS "GStreamer: Downloading from ${_gst_url}") - file(DOWNLOAD "${_gst_url}" "${_gst_archive_path}.tmp" - STATUS _gst_dl_status - SHOW_PROGRESS - TIMEOUT 600 - INACTIVITY_TIMEOUT 60 - TLS_VERIFY ON - ) - list(GET _gst_dl_status 0 _gst_dl_code) - if(_gst_dl_code EQUAL 0) - if(_gst_expected_hash) - file(SHA256 "${_gst_archive_path}.tmp" _gst_actual_hash) - if(NOT _gst_actual_hash STREQUAL "${_gst_expected_hash}") - message(WARNING "GStreamer: SHA256 mismatch from ${_gst_url}, trying next URL") - file(REMOVE "${_gst_archive_path}.tmp") - continue() - endif() - endif() - file(RENAME "${_gst_archive_path}.tmp" "${_gst_archive_path}") - set(_gst_downloaded TRUE) - break() - endif() - list(GET _gst_dl_status 1 _gst_dl_error) - message(WARNING "GStreamer: Download failed from ${_gst_url}: ${_gst_dl_error}") - file(REMOVE "${_gst_archive_path}.tmp") - endforeach() - if(NOT _gst_downloaded) - message(FATAL_ERROR "GStreamer: All download URLs failed for ${_gst_archive_name}.\n" - "Tried: ${_gst_android_urls}\n" - "Set GStreamer_ROOT_DIR to use a local installation.") - endif() - endif() - - CPMAddPackage( - NAME gstreamer - VERSION ${GStreamer_FIND_VERSION} - URL "file://${_gst_archive_path}" - DOWNLOAD_EXTRACT_TIMESTAMP ON - ) - - if(NOT DEFINED GStreamer_ROOT_DIR) - if(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a") - set(_gst_arch "armv7") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") - set(_gst_arch "arm64") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") - set(_gst_arch "x86") - elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") - set(_gst_arch "x86_64") - else() - message(FATAL_ERROR "GStreamer: Unsupported Android ABI: ${CMAKE_ANDROID_ARCH_ABI}") - endif() - - set(GStreamer_ROOT_DIR "${gstreamer_SOURCE_DIR}/${_gst_arch}") - endif() - - cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - file(GLOB _gst_extracted_dirs LIST_DIRECTORIES true "${gstreamer_SOURCE_DIR}/*") - message(FATAL_ERROR - "Could not locate GStreamer for ${CMAKE_ANDROID_ARCH_ABI}\n" - " Expected: ${GStreamer_ROOT_DIR}\n" - " Source dir: ${gstreamer_SOURCE_DIR}\n" - " Contents: ${_gst_extracted_dirs}\n" - " Check that the GStreamer tarball contains an '${_gst_arch}' directory." - ) - endif() - - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - - # Clear host PKG_CONFIG_PATH to prevent host .pc files contaminating cross-builds. - # Use PKG_CONFIG_LIBDIR instead to restrict search to GStreamer SDK paths only. - set(ENV{PKG_CONFIG_PATH} "") - if(CMAKE_HOST_WIN32) - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/tools/windows/pkg-config.exe") - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") - list(APPEND PKG_CONFIG_ARGN --dont-define-prefix) - elseif(CMAKE_HOST_UNIX) - if(CMAKE_HOST_APPLE) - # Try to find pkg-config in common Homebrew locations and in PATH - find_program(PKG_CONFIG_EXECUTABLE - NAMES pkg-config - PATHS /opt/homebrew/bin /usr/local/bin - NO_DEFAULT_PATH - ) - if(NOT PKG_CONFIG_EXECUTABLE) - # Fallback to PATH search if not found in Homebrew locations - find_program(PKG_CONFIG_EXECUTABLE pkg-config) - endif() - if(NOT PKG_CONFIG_EXECUTABLE) - message(FATAL_ERROR "Could not find pkg-config. Please install pkg-config using tools/setup/install_dependencies.py --platform macos.") - endif() - endif() - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") - endif() - list(APPEND PKG_CONFIG_ARGN - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# macOS Platform -# ---------------------------------------------------------------------------- -elseif(MACOS) - if(NOT DEFINED GStreamer_ROOT_DIR) - if(EXISTS "/Library/Frameworks/GStreamer.framework") - set(GStreamer_ROOT_DIR "/Library/Frameworks/GStreamer.framework/Versions/1.0") - elseif(EXISTS "/opt/homebrew/opt/gstreamer") - set(GStreamer_ROOT_DIR "/opt/homebrew/opt/gstreamer") - set(GStreamer_USE_FRAMEWORK OFF) - elseif(EXISTS "/usr/local/opt/gstreamer") - set(GStreamer_ROOT_DIR "/usr/local/opt/gstreamer") - set(GStreamer_USE_FRAMEWORK OFF) - endif() - endif() - - _gst_normalize_and_validate_root() - - if(GStreamer_USE_FRAMEWORK AND NOT DEFINED GSTREAMER_FRAMEWORK_PATH) - set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") - cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) - endif() - - set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") - set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") - set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") - - if(GStreamer_USE_FRAMEWORK) - set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config") - set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) - set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") - else() - find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf - PATHS /opt/homebrew/bin /usr/local/bin - NO_DEFAULT_PATH - ) - if(NOT PKG_CONFIG_EXECUTABLE) - find_program(PKG_CONFIG_EXECUTABLE NAMES pkg-config pkgconf) - endif() - set(ENV{PKG_CONFIG_PATH} "") - set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") - endif() - list(APPEND PKG_CONFIG_ARGN - --define-variable=prefix=${GStreamer_ROOT_DIR} - --define-variable=libdir=${GSTREAMER_LIB_PATH} - --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} - ) - -# ---------------------------------------------------------------------------- -# iOS Platform (Currently Unsupported) -# ---------------------------------------------------------------------------- -elseif(IOS) - message(FATAL_ERROR "GStreamer for iOS is currently unsupported") - - CPMAddPackage( - NAME gstreamer - VERSION ${GStreamer_FIND_VERSION} - URL "https://gstreamer.freedesktop.org/data/pkg/ios/${GStreamer_FIND_VERSION}/gstreamer-1.0-devel-${GStreamer_FIND_VERSION}-ios-universal.pkg" - ) - - set(GST_PKG_FILE "${gstreamer_SOURCE_DIR}/gstreamer.pkg") - set(GST_EXPAND_DIR "${gstreamer_SOURCE_DIR}/gstreamer-pkg-expanded") - set(GST_PAYLOAD_DIR "${GST_EXPAND_DIR}/Payload") - - file(MAKE_DIRECTORY "${GST_EXPAND_DIR}") - execute_process( - COMMAND pkgutil --expand-full "${GST_PKG_FILE}" "${GST_EXPAND_DIR}" - RESULT_VARIABLE _pkgutil_rc - ) - if(NOT _pkgutil_rc EQUAL 0) - message(FATAL_ERROR "pkgutil failed to expand GStreamer .pkg") - endif() - - execute_process( - COMMAND xar -xf "${GST_EXPAND_DIR}/gstreamer-1.0-devel-${GStreamer_FIND_VERSION}-ios-universal.pkg/Payload" - --directory "${GST_PAYLOAD_DIR}" - RESULT_VARIABLE _xar_rc - ) - if(NOT _xar_rc EQUAL 0) - message(FATAL_ERROR "xar failed to extract GStreamer Payload") - endif() - - # set(GSTREAMER_FRAMEWORK_PATH "/Library/Developer/GStreamer/iPhone.sdk" CACHE PATH "Path of GStreamer.Framework") - set(GSTREAMER_FRAMEWORK_PATH "${GST_PAYLOAD_DIR}/usr/local/Frameworks/GStreamer.framework") - - set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") - - if(NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "Could not locate GStreamer - check installation or set environment/cmake variables") - endif() - - set(GSTREAMER_INCLUDE_PATH "${GSTREAMER_FRAMEWORK_PATH}/Headers") -endif() - -# ---------------------------------------------------------------------------- -# Validation -# ---------------------------------------------------------------------------- -if(NOT EXISTS "${GStreamer_ROOT_DIR}" OR NOT EXISTS "${GSTREAMER_LIB_PATH}" OR NOT EXISTS "${GSTREAMER_PLUGIN_PATH}" OR NOT EXISTS "${GSTREAMER_INCLUDE_PATH}") - message(FATAL_ERROR "GStreamer: Could not locate required directories - check installation or set GStreamer_ROOT_DIR") -endif() - -if(GStreamer_USE_FRAMEWORK AND NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") - message(FATAL_ERROR "GStreamer: Could not locate framework at ${GSTREAMER_FRAMEWORK_PATH}") -endif() - -# ============================================================================ -# Plugin & Dependency Configuration -# ============================================================================ - -if(GStreamer_USE_STATIC_LIBS) - set(GSTREAMER_EXTRA_DEPS - gstreamer-base-1.0 - gstreamer-video-1.0 - gstreamer-gl-1.0 - gstreamer-gl-prototypes-1.0 - gstreamer-rtsp-1.0 - # gstreamer-gl-egl-1.0 - # gstreamer-gl-wayland-1.0 - # gstreamer-gl-x11-1.0 - ) - - set(GSTREAMER_PLUGINS - coreelements - dav1d - isomp4 - libav - matroska - mpegtsdemux - opengl - openh264 - playback - rtp - rtpmanager - rtsp - sdpelem - tcp - typefindfunctions - udp - videoparsersbad - vpx - ) - if(ANDROID) - list(APPEND GSTREAMER_PLUGINS androidmedia) # vulkan - elseif(APPLE) - list(APPEND GSTREAMER_PLUGINS applemedia vulkan) - elseif(WIN32) - list(APPEND GSTREAMER_PLUGINS d3d d3d11 d3d12 dxva nvcodec) - elseif(LINUX) - list(APPEND GSTREAMER_PLUGINS nvcodec qsv va vulkan) # qml6 - GStreamer provided qml6 is xcb only - endif() -endif() - -if(ANDROID) - set(GStreamer_Mobile_MODULE_NAME gstreamer_android) - set(G_IO_MODULES openssl) - set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") - - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/") - set(GSTREAMER_ANDROID_MODULE_NAME gstreamer_android) - set(GSTREAMER_JAVA_SRC_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/src") - set(GSTREAMER_ASSETS_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/assets") - - configure_file( - "${GStreamer_NDK_BUILD_PATH}/gstreamer_android-1.0.c.in" - "${GStreamer_Mobile_MODULE_NAME}.c" - ) -endif() - -# ============================================================================ -# Package Discovery -# ============================================================================ - -set(GStreamer_ROOT_DIR "${GStreamer_ROOT_DIR}" CACHE PATH "GStreamer SDK root directory" FORCE) - -if(GStreamer_USE_FRAMEWORK) - list(APPEND CMAKE_FRAMEWORK_PATH "${GSTREAMER_FRAMEWORK_PATH}") -endif() - -if(GStreamer_USE_STATIC_LIBS) - list(APPEND PKG_CONFIG_ARGN "--static") -endif() - -find_package(PkgConfig REQUIRED QUIET) - -list(PREPEND CMAKE_PREFIX_PATH ${GStreamer_ROOT_DIR}) -if(LINUX) - pkg_check_modules(PC_GSTREAMER REQUIRED gstreamer-1.0>=${GStreamer_FIND_VERSION}) -else() - pkg_check_modules(PC_GSTREAMER REQUIRED gstreamer-1.0=${GStreamer_FIND_VERSION}) -endif() -set(GStreamer_VERSION "${PC_GSTREAMER_VERSION}") - -# ============================================================================ -# Component Discovery -# ============================================================================ - -# Helper function to find individual GStreamer components -function(find_gstreamer_component component pkgconfig_name) - set(target GStreamer::${component}) - - if(NOT TARGET ${target}) - string(TOUPPER ${component} upper) - pkg_check_modules(PC_GSTREAMER_${upper} IMPORTED_TARGET ${pkgconfig_name}) - if(TARGET PkgConfig::PC_GSTREAMER_${upper}) - qt_add_library(GStreamer::${component} INTERFACE IMPORTED) - target_link_libraries(GStreamer::${component} INTERFACE PkgConfig::PC_GSTREAMER_${upper}) - if("PC_GSTREAMER_${upper}" MATCHES "PC_GSTREAMER_GL") - get_target_property(_qt_incs PkgConfig::PC_GSTREAMER_GL INTERFACE_INCLUDE_DIRECTORIES) - set(__qt_fixed_incs) - foreach(path IN LISTS _qt_incs) - if(IS_DIRECTORY "${path}") - list(APPEND __qt_fixed_incs "${path}") - endif() - endforeach() - set_property(TARGET PkgConfig::PC_GSTREAMER_GL PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${__qt_fixed_incs}") - endif() - endif() - endif() - - if(TARGET ${target}) - set(GStreamer_${component}_FOUND TRUE PARENT_SCOPE) - endif() -endfunction() - -# ---------------------------------------------------------------------------- -# Find Core Components (Always Required) -# ---------------------------------------------------------------------------- -find_gstreamer_component(Core gstreamer-1.0) -find_gstreamer_component(Base gstreamer-base-1.0) -find_gstreamer_component(Video gstreamer-video-1.0) -find_gstreamer_component(Gl gstreamer-gl-1.0) -find_gstreamer_component(GlPrototypes gstreamer-gl-prototypes-1.0) -find_gstreamer_component(Rtsp gstreamer-rtsp-1.0) - -# ---------------------------------------------------------------------------- -# Find Optional Components (Based on FIND_COMPONENTS) -# ---------------------------------------------------------------------------- -if(GlEgl IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlEgl gstreamer-gl-egl-1.0) -endif() - -if(GlWayland IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlWayland gstreamer-gl-wayland-1.0) -endif() - -if(GlX11 IN_LIST GStreamer_FIND_COMPONENTS) - find_gstreamer_component(GlX11 gstreamer-gl-x11-1.0) -endif() - -# ============================================================================ -# Package Finalization -# ============================================================================ - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(GStreamer - VERSION_VAR GStreamer_VERSION - HANDLE_COMPONENTS -) - -# ---------------------------------------------------------------------------- -# Create Main GStreamer Target -# ---------------------------------------------------------------------------- -if(GStreamer_FOUND AND NOT TARGET GStreamer::GStreamer) - qt_add_library(GStreamer::GStreamer INTERFACE IMPORTED) - - if(GStreamer_USE_STATIC_LIBS) - target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_STATIC_BUILD) - endif() - - if(APPLE AND GStreamer_USE_FRAMEWORK) - set(CMAKE_FIND_FRAMEWORK ONLY) - find_library(GSTREAMER_FRAMEWORK GStreamer - PATHS - "${GSTREAMER_FRAMEWORK_PATH}" - "/Library/Frameworks" - "/usr/local/opt/gstreamer" - "/opt/homebrew/opt/gstreamer" - ) - unset(CMAKE_FIND_FRAMEWORK) - if(GSTREAMER_FRAMEWORK) - target_link_libraries(GStreamer::GStreamer INTERFACE ${GSTREAMER_FRAMEWORK}) - target_include_directories(GStreamer::GStreamer INTERFACE "${GSTREAMER_FRAMEWORK}/Headers") - if(MACOS) - target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_MACOS_FRAMEWORK) - endif() - return() - else() - message(FATAL_ERROR "GStreamer: Could not locate GStreamer.framework") - endif() - - # Android-specific link options - elseif(ANDROID) - target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-Bsymbolic") - if(CMAKE_SIZEOF_VOID_P EQUAL 4) - target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-z,notext") - endif() - endif() - - target_link_directories(GStreamer::GStreamer INTERFACE ${GSTREAMER_LIB_PATH}) - - target_link_libraries(GStreamer::GStreamer - INTERFACE - GStreamer::Core - GStreamer::Base - GStreamer::Video - GStreamer::Gl - GStreamer::GlPrototypes - GStreamer::Rtsp - ) - - foreach(component IN LISTS GStreamer_FIND_COMPONENTS) - if(GStreamer_${component}_FOUND) - target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) - endif() - endforeach() - -# ---------------------------------------------------------------------------- -# Static Plugin Linking (Static Builds Only) -# ---------------------------------------------------------------------------- - if(GStreamer_USE_STATIC_LIBS) - qt_add_library(GStreamer::Plugins INTERFACE IMPORTED) - target_link_directories(GStreamer::Plugins INTERFACE ${GSTREAMER_PLUGIN_PATH}) - - foreach(plugin IN LISTS GSTREAMER_PLUGINS) - pkg_check_modules(GST_PLUGIN_${plugin} QUIET IMPORTED_TARGET gst${plugin}) - if(GST_PLUGIN_${plugin}_FOUND) - target_link_libraries(GStreamer::Plugins INTERFACE PkgConfig::GST_PLUGIN_${plugin}) - else() - find_library(GST_PLUGIN_${plugin}_LIBRARY - NAMES gst${plugin} - PATHS - ${GSTREAMER_LIB_PATH} - ${GSTREAMER_PLUGIN_PATH} - ) - if(GST_PLUGIN_${plugin}_LIBRARY) - target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_LIBRARY}) - set(GST_PLUGIN_${plugin}_FOUND TRUE) - endif() - endif() - if(GST_PLUGIN_${plugin}_FOUND) - target_compile_definitions(GStreamer::Plugins INTERFACE GST_PLUGIN_${plugin}_FOUND) - endif() - endforeach() - - target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::Plugins) - endif() -endif() diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake new file mode 100644 index 000000000000..d267994b5b74 --- /dev/null +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -0,0 +1,595 @@ +if(NOT DEFINED GStreamer_FIND_VERSION) + if(LINUX) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MIN_VERSION}) + elseif(WIN32) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_WIN_VERSION}) + elseif(ANDROID) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_ANDROID_VERSION}) + elseif(IOS) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_IOS_VERSION}) + elseif(MACOS) + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) + else() + set(GStreamer_FIND_VERSION ${QGC_CONFIG_GSTREAMER_VERSION}) + endif() +endif() + +if(NOT GStreamer_FIND_VERSION) + message(FATAL_ERROR "GStreamer version not configured. Ensure BuildConfig.cmake has been included " + "and .github/build-config.json contains the appropriate gstreamer_*_version entry.") +endif() + +if(NOT DEFINED GStreamer_ROOT_DIR) + if(DEFINED GSTREAMER_ROOT) + set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) + elseif(DEFINED GStreamer_ROOT) + set(GStreamer_ROOT_DIR ${GStreamer_ROOT}) + endif() + + if(DEFINED GStreamer_ROOT_DIR AND NOT EXISTS "${GStreamer_ROOT_DIR}") + message(FATAL_ERROR "GStreamer: User-provided directory does not exist: ${GStreamer_ROOT_DIR}\n" + "Correct the path or unset GStreamer_ROOT_DIR.") + endif() +endif() + +if(NOT DEFINED GStreamer_USE_STATIC_LIBS) + if(ANDROID OR IOS) + set(GStreamer_USE_STATIC_LIBS ON) + else() + set(GStreamer_USE_STATIC_LIBS OFF) + endif() +endif() + +if(NOT DEFINED GStreamer_USE_FRAMEWORK) + if(APPLE) + set(GStreamer_USE_FRAMEWORK ON) + else() + set(GStreamer_USE_FRAMEWORK OFF) + endif() +endif() + +include(GStreamerHelpers) + +set(PKG_CONFIG_ARGN) + +function(_qgc_find_apple_pkg_config OUT_VAR) + find_program(_qgc_pkg_config + NAMES pkg-config pkgconf + PATHS /opt/homebrew/bin /usr/local/bin + NO_DEFAULT_PATH + ) + if(NOT _qgc_pkg_config) + find_program(_qgc_pkg_config NAMES pkg-config pkgconf) + endif() + if(NOT _qgc_pkg_config) + message(FATAL_ERROR + "Could not find pkg-config.\n" + "Install dependencies with: python3 tools/setup/install_dependencies.py --platform macos\n" + "or install pkg-config manually (for example: brew install pkg-config).") + endif() + set(${OUT_VAR} "${_qgc_pkg_config}" CACHE FILEPATH "pkg-config executable" FORCE) + unset(_qgc_pkg_config CACHE) +endfunction() + +function(_qgc_validate_expanded_pkg EXPANDED_DIR LABEL) + file(GLOB _payloads "${EXPANDED_DIR}/*.pkg/Payload") + if(NOT _payloads) + file(REMOVE_RECURSE "${EXPANDED_DIR}") + message(FATAL_ERROR + "pkgutil expanded GStreamer ${LABEL} package but no payloads were found in ${EXPANDED_DIR}") + endif() +endfunction() + +if(WIN32 AND NOT ANDROID) + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + set(_gst_win_arch "arm64") + else() + set(_gst_win_arch "x86_64") + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(_gst_win_arch STREQUAL "arm64") + if(DEFINED ENV{GSTREAMER_1_0_ROOT_ARM64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_ARM64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_ARM64}") + elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_ARM64}") + elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_arm64") + set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_arm64") + elseif(EXISTS "C:/gstreamer/1.0/msvc_arm64") + set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_arm64") + endif() + else() + if(DEFINED ENV{GSTREAMER_1_0_ROOT_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_X86_64}") + elseif(MSVC AND DEFINED ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MSVC_X86_64}") + elseif(MINGW AND DEFINED ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64} AND EXISTS "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") + set(GStreamer_ROOT_DIR "$ENV{GSTREAMER_1_0_ROOT_MINGW_X86_64}") + elseif(EXISTS "C:/Program Files/gstreamer/1.0/msvc_x86_64") + set(GStreamer_ROOT_DIR "C:/Program Files/gstreamer/1.0/msvc_x86_64") + elseif(EXISTS "C:/gstreamer/1.0/msvc_x86_64") + set(GStreamer_ROOT_DIR "C:/gstreamer/1.0/msvc_x86_64") + endif() + endif() + endif() + + _gst_normalize_and_validate_root() + + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config.exe") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN + --dont-define-prefix + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(LINUX AND NOT ANDROID) + if(NOT DEFINED GStreamer_ROOT_DIR) + set(GStreamer_ROOT_DIR "/usr") + endif() + + _gst_normalize_and_validate_root() + + if((EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib/${CMAKE_SYSTEM_PROCESSOR}-linux-gnu") + elseif((EXISTS "${GStreamer_ROOT_DIR}/lib64") AND (EXISTS "${GStreamer_ROOT_DIR}/lib64/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib64") + elseif((EXISTS "${GStreamer_ROOT_DIR}/lib") AND (EXISTS "${GStreamer_ROOT_DIR}/lib/gstreamer-1.0")) + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + else() + message(FATAL_ERROR "Could not locate GStreamer libraries - check installation or set environment/cmake variables") + endif() + + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") + +elseif(ANDROID) + gstreamer_get_package_url(android ${GStreamer_FIND_VERSION} _gst_android_url) + gstreamer_get_s3_mirror_url(android ${GStreamer_FIND_VERSION} _gst_android_s3_url) + gstreamer_fetch_checksum(android ${GStreamer_FIND_VERSION} _gst_android_hash) + + if(CPM_SOURCE_CACHE) + set(_gst_android_cache "${CPM_SOURCE_CACHE}/gstreamer-android") + else() + set(_gst_android_cache "${CMAKE_BINARY_DIR}/_deps/gstreamer-android") + endif() + gstreamer_resilient_download( + URLS "${_gst_android_url}" "${_gst_android_s3_url}" + FILENAME "gstreamer-android-${GStreamer_FIND_VERSION}.tar.xz" + DESTINATION_DIR "${_gst_android_cache}" + RESULT_VAR _gst_android_archive + EXPECTED_HASH "${_gst_android_hash}" + ) + + CPMAddPackage( + NAME gstreamer + VERSION ${GStreamer_FIND_VERSION} + URL "file://${_gst_android_archive}" + ) + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(CMAKE_ANDROID_ARCH_ABI STREQUAL "armeabi-v7a") + set(GStreamer_ABI_DIR "armv7") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") + set(GStreamer_ABI_DIR "arm64") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") + set(GStreamer_ABI_DIR "x86") + elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") + set(GStreamer_ABI_DIR "x86_64") + else() + message(FATAL_ERROR "Unsupported Android ABI: ${CMAKE_ANDROID_ARCH_ABI}") + endif() + set(GStreamer_ROOT_DIR "${gstreamer_SOURCE_DIR}/${GStreamer_ABI_DIR}") + set(GStreamer_AUTO_DOWNLOADED TRUE) + endif() + + _gst_normalize_and_validate_root() + + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + + set(ENV{PKG_CONFIG_PATH} "") + if(CMAKE_HOST_WIN32) + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build/tools/windows/pkg-config.exe") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig;${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN --dont-define-prefix) + elseif(CMAKE_HOST_UNIX) + if(CMAKE_HOST_APPLE) + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + endif() + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + endif() + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(MACOS AND NOT IOS) + if(NOT DEFINED GStreamer_ROOT_DIR) + if(EXISTS "/Library/Frameworks/GStreamer.framework") + set(GStreamer_ROOT_DIR "/Library/Frameworks/GStreamer.framework/Versions/1.0") + elseif(EXISTS "/opt/homebrew/opt/gstreamer") + set(GStreamer_ROOT_DIR "/opt/homebrew/opt/gstreamer") + set(GStreamer_USE_FRAMEWORK OFF) + elseif(EXISTS "/usr/local/opt/gstreamer") + set(GStreamer_ROOT_DIR "/usr/local/opt/gstreamer") + set(GStreamer_USE_FRAMEWORK OFF) + endif() + endif() + + _gst_normalize_and_validate_root() + + if(GStreamer_USE_FRAMEWORK AND NOT DEFINED GSTREAMER_FRAMEWORK_PATH) + set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") + cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) + endif() + + set(GSTREAMER_INCLUDE_PATH "${GStreamer_ROOT_DIR}/include") + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + + if(GStreamer_USE_FRAMEWORK) + set(ENV{PKG_CONFIG} "${GStreamer_ROOT_DIR}/bin/pkg-config") + set(PKG_CONFIG_EXECUTABLE "$ENV{PKG_CONFIG}" CACHE FILEPATH "pkg-config executable" FORCE) + set(ENV{PKG_CONFIG_PATH} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig:$ENV{PKG_CONFIG_PATH}") + else() + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + endif() + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) + +elseif(IOS) + if(NOT CMAKE_HOST_APPLE) + message(FATAL_ERROR "GStreamer for iOS can only be built on macOS") + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR) + if(EXISTS "/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GSTREAMER_FRAMEWORK_PATH "/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") + endif() + endif() + + if(NOT DEFINED GStreamer_ROOT_DIR OR NOT EXISTS "${GStreamer_ROOT_DIR}") + gstreamer_get_package_url(ios ${GStreamer_FIND_VERSION} _gst_ios_url) + gstreamer_get_s3_mirror_url(ios ${GStreamer_FIND_VERSION} _gst_ios_s3_url) + gstreamer_fetch_checksum(ios ${GStreamer_FIND_VERSION} _gst_ios_hash) + + set(_gst_ios_cache_dir "${CMAKE_BINARY_DIR}/_deps/gstreamer-ios-${GStreamer_FIND_VERSION}") + set(_gst_ios_expanded "${_gst_ios_cache_dir}/expanded") + + gstreamer_resilient_download( + URLS "${_gst_ios_url}" "${_gst_ios_s3_url}" + FILENAME "gstreamer-ios.pkg" + DESTINATION_DIR "${_gst_ios_cache_dir}" + RESULT_VAR _gst_ios_pkg + EXPECTED_HASH "${_gst_ios_hash}" + ) + + if(EXISTS "${_gst_ios_expanded}") + set(_gst_ios_complete FALSE) + file(GLOB _existing_pkg_dirs "${_gst_ios_expanded}/*.pkg") + foreach(_dir IN LISTS _existing_pkg_dirs) + if(EXISTS "${_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(_gst_ios_complete TRUE) + break() + endif() + endforeach() + if(NOT _gst_ios_complete) + message(STATUS "GStreamer: cached iOS expansion is incomplete; re-expanding") + file(REMOVE_RECURSE "${_gst_ios_expanded}") + endif() + endif() + + if(NOT EXISTS "${_gst_ios_expanded}") + message(STATUS "Expanding GStreamer iOS package...") + execute_process( + COMMAND pkgutil --expand-full "${_gst_ios_pkg}" "${_gst_ios_expanded}" + RESULT_VARIABLE _pkgutil_rc + ) + if(NOT _pkgutil_rc EQUAL 0) + file(REMOVE_RECURSE "${_gst_ios_expanded}") + message(FATAL_ERROR + "pkgutil failed to expand GStreamer iOS .pkg (exit code: ${_pkgutil_rc})") + endif() + _qgc_validate_expanded_pkg("${_gst_ios_expanded}" "iOS") + endif() + + file(GLOB _pkg_dirs "${_gst_ios_expanded}/*.pkg") + foreach(_pkg_dir IN LISTS _pkg_dirs) + if(EXISTS "${_pkg_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + set(GSTREAMER_FRAMEWORK_PATH "${_pkg_dir}/Payload/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework") + break() + endif() + endforeach() + + if(NOT GSTREAMER_FRAMEWORK_PATH) + message(FATAL_ERROR "Could not locate GStreamer.framework in downloaded iOS SDK") + endif() + + set(GStreamer_ROOT_DIR "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0") + set(GStreamer_AUTO_DOWNLOADED TRUE) + endif() + + _gst_normalize_and_validate_root() + + set(GStreamer_USE_FRAMEWORK ON) + if(NOT DEFINED GSTREAMER_FRAMEWORK_PATH) + set(GSTREAMER_FRAMEWORK_PATH "${GStreamer_ROOT_DIR}/../..") + cmake_path(NORMAL_PATH GSTREAMER_FRAMEWORK_PATH) + endif() + set(GSTREAMER_INCLUDE_PATH "${GSTREAMER_FRAMEWORK_PATH}/Headers") + set(GSTREAMER_LIB_PATH "${GStreamer_ROOT_DIR}/lib") + set(GSTREAMER_PLUGIN_PATH "${GSTREAMER_LIB_PATH}/gstreamer-1.0") + + _qgc_find_apple_pkg_config(PKG_CONFIG_EXECUTABLE) + + set(ENV{PKG_CONFIG_PATH} "") + set(ENV{PKG_CONFIG_LIBDIR} "${GSTREAMER_LIB_PATH}/pkgconfig:${GSTREAMER_PLUGIN_PATH}/pkgconfig") + list(APPEND PKG_CONFIG_ARGN + --define-variable=prefix=${GStreamer_ROOT_DIR} + --define-variable=libdir=${GSTREAMER_LIB_PATH} + --define-variable=includedir=${GSTREAMER_INCLUDE_PATH} + ) +endif() + +if(NOT EXISTS "${GStreamer_ROOT_DIR}" OR NOT EXISTS "${GSTREAMER_LIB_PATH}" OR NOT EXISTS "${GSTREAMER_PLUGIN_PATH}" OR NOT EXISTS "${GSTREAMER_INCLUDE_PATH}") + message(FATAL_ERROR "GStreamer: Could not locate required directories - check installation or set GStreamer_ROOT_DIR") +endif() + +if(GStreamer_USE_FRAMEWORK AND NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") + message(FATAL_ERROR "GStreamer: Could not locate framework at ${GSTREAMER_FRAMEWORK_PATH}") +endif() + +# Sub-libraries used directly by QGC source code. Plugin transitive deps +# are resolved automatically from each plugin's .pc file. +if(NOT DEFINED GSTREAMER_EXTRA_DEPS) + set(GSTREAMER_EXTRA_DEPS + gstreamer-base-1.0 + gstreamer-gl-1.0 + gstreamer-gl-prototypes-1.0 + gstreamer-rtsp-1.0 + gstreamer-video-1.0 + ) + if(WIN32) + list(APPEND GSTREAMER_EXTRA_DEPS graphene-1.0) + endif() + if(ANDROID OR IOS) + list(APPEND GSTREAMER_EXTRA_DEPS gio-2.0) + endif() + if(ANDROID) + list(APPEND GSTREAMER_EXTRA_DEPS gmodule-2.0 zlib) + endif() +endif() + +if(NOT DEFINED GSTREAMER_PLUGINS) + set(GSTREAMER_PLUGINS + coreelements + isomp4 + libav + matroska + mpegtsdemux + opengl + openh264 + playback + rtp + rtpmanager + rtsp + sdpelem + tcp + typefindfunctions + udp + videoconvertscale + videoparsersbad + vpx + ) + if(ANDROID) + list(APPEND GSTREAMER_PLUGINS androidmedia dav1d) + elseif(APPLE) + list(APPEND GSTREAMER_PLUGINS applemedia dav1d vulkan) + elseif(WIN32) + list(APPEND GSTREAMER_PLUGINS d3d d3d11 d3d12 dav1d dxva nvcodec) + elseif(LINUX) + list(APPEND GSTREAMER_PLUGINS dav1d nvcodec qsv va vulkan) + endif() +endif() + +if(ANDROID) + set(GStreamer_Mobile_MODULE_NAME gstreamer_android) + set(G_IO_MODULES openssl) + set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") + set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT_DIR}/share/gst-android/ndk-build") + set(GStreamer_JAVA_SRC_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/src") + set(GStreamer_ASSETS_DIR "${CMAKE_BINARY_DIR}/android-build-${CMAKE_PROJECT_NAME}/assets") +elseif(IOS) + set(GStreamer_Mobile_MODULE_NAME gstreamer_mobile) + set(G_IO_MODULES openssl) + set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") + set(GStreamer_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets") +endif() + +set(GStreamer_ROOT_DIR "${GStreamer_ROOT_DIR}" CACHE PATH "GStreamer SDK root directory" FORCE) + +if(GStreamer_USE_FRAMEWORK) + list(APPEND CMAKE_FRAMEWORK_PATH "${GSTREAMER_FRAMEWORK_PATH}") +endif() + +if(GStreamer_USE_STATIC_LIBS) + list(APPEND PKG_CONFIG_ARGN "--static") +endif() + +find_package(PkgConfig REQUIRED QUIET) + +list(PREPEND CMAKE_PREFIX_PATH ${GStreamer_ROOT_DIR}) +if(LINUX) + pkg_check_modules(PC_GSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0>=${GStreamer_FIND_VERSION}) +else() + pkg_check_modules(PC_GSTREAMER REQUIRED IMPORTED_TARGET gstreamer-1.0=${GStreamer_FIND_VERSION}) +endif() +set(GStreamer_VERSION "${PC_GSTREAMER_VERSION}") + +function(find_gstreamer_component component pkgconfig_name) + set(target GStreamer::${component}) + + if(NOT TARGET ${target}) + string(TOUPPER ${component} upper) + pkg_check_modules(PC_GSTREAMER_${upper} IMPORTED_TARGET ${pkgconfig_name}) + if(TARGET PkgConfig::PC_GSTREAMER_${upper}) + qt_add_library(GStreamer::${component} INTERFACE IMPORTED) + target_link_libraries(GStreamer::${component} INTERFACE PkgConfig::PC_GSTREAMER_${upper}) + if(upper STREQUAL "GL") + get_target_property(_qt_incs PkgConfig::PC_GSTREAMER_GL INTERFACE_INCLUDE_DIRECTORIES) + set(__qt_fixed_incs) + foreach(path IN LISTS _qt_incs) + if(IS_DIRECTORY "${path}") + list(APPEND __qt_fixed_incs "${path}") + endif() + endforeach() + set_property(TARGET PkgConfig::PC_GSTREAMER_GL PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${__qt_fixed_incs}") + endif() + endif() + endif() + + if(TARGET ${target}) + set(QGCGStreamer_${component}_FOUND TRUE PARENT_SCOPE) + set(GStreamer_${component}_FOUND TRUE PARENT_SCOPE) + endif() +endfunction() + +find_gstreamer_component(Core gstreamer-1.0) +find_gstreamer_component(Base gstreamer-base-1.0) +find_gstreamer_component(Video gstreamer-video-1.0) +find_gstreamer_component(Gl gstreamer-gl-1.0) +find_gstreamer_component(GlPrototypes gstreamer-gl-prototypes-1.0) +find_gstreamer_component(Rtsp gstreamer-rtsp-1.0) + +if(GlEgl IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlEgl gstreamer-gl-egl-1.0) +endif() + +if(GlWayland IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlWayland gstreamer-gl-wayland-1.0) +endif() + +if(GlX11 IN_LIST QGCGStreamer_FIND_COMPONENTS) + find_gstreamer_component(GlX11 gstreamer-gl-x11-1.0) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QGCGStreamer + REQUIRED_VARS GStreamer_ROOT_DIR GSTREAMER_LIB_PATH GSTREAMER_PLUGIN_PATH + VERSION_VAR GStreamer_VERSION + HANDLE_COMPONENTS +) + +if(QGCGStreamer_FOUND AND NOT TARGET GStreamer::GStreamer) + qt_add_library(GStreamer::GStreamer INTERFACE IMPORTED) + + if(GStreamer_USE_STATIC_LIBS) + target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_STATIC_BUILD) + endif() + + if(APPLE AND GStreamer_USE_FRAMEWORK) + set(_saved_find_framework "${CMAKE_FIND_FRAMEWORK}") + set(CMAKE_FIND_FRAMEWORK ONLY) + cmake_path(GET GSTREAMER_FRAMEWORK_PATH PARENT_PATH _gst_framework_parent) + find_library(GSTREAMER_FRAMEWORK GStreamer + PATHS + "${_gst_framework_parent}" + "${GSTREAMER_FRAMEWORK_PATH}" + "/Library/Frameworks" + "/usr/local/opt/gstreamer" + "/opt/homebrew/opt/gstreamer" + ) + set(CMAKE_FIND_FRAMEWORK "${_saved_find_framework}") + if(GSTREAMER_FRAMEWORK) + target_link_libraries(GStreamer::GStreamer INTERFACE ${GSTREAMER_FRAMEWORK}) + target_include_directories(GStreamer::GStreamer INTERFACE "${GSTREAMER_FRAMEWORK}/Headers") + if(MACOS) + target_compile_definitions(GStreamer::GStreamer INTERFACE QGC_GST_MACOS_FRAMEWORK) + endif() + else() + message(FATAL_ERROR "GStreamer: Could not locate GStreamer.framework") + endif() + + elseif(ANDROID) + target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-Bsymbolic") + if(CMAKE_SIZEOF_VOID_P EQUAL 4) + target_link_options(GStreamer::GStreamer INTERFACE "-Wl,-z,notext") + endif() + endif() + + target_link_directories(GStreamer::GStreamer INTERFACE ${GSTREAMER_LIB_PATH}) + + # On macOS with framework, the umbrella library already provides all component + # symbols. Linking individual component dylibs would add @rpath/libgst*.dylib + # load commands that cannot be resolved inside the app bundle. + if(NOT (MACOS AND GStreamer_USE_FRAMEWORK AND GSTREAMER_FRAMEWORK)) + target_link_libraries(GStreamer::GStreamer + INTERFACE + GStreamer::Core + GStreamer::Base + GStreamer::Video + GStreamer::Gl + GStreamer::GlPrototypes + GStreamer::Rtsp + ) + + foreach(component IN LISTS QGCGStreamer_FIND_COMPONENTS) + if(GStreamer_${component}_FOUND) + target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::${component}) + endif() + endforeach() + endif() + + if(GStreamer_USE_STATIC_LIBS) + qt_add_library(GStreamer::Plugins INTERFACE IMPORTED) + target_link_directories(GStreamer::Plugins INTERFACE ${GSTREAMER_PLUGIN_PATH}) + + foreach(plugin IN LISTS GSTREAMER_PLUGINS) + pkg_check_modules(GST_PLUGIN_${plugin} QUIET IMPORTED_TARGET gst${plugin}) + if(GST_PLUGIN_${plugin}_FOUND) + if(TARGET PkgConfig::GST_PLUGIN_${plugin}) + target_link_libraries(GStreamer::Plugins INTERFACE PkgConfig::GST_PLUGIN_${plugin}) + else() + target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_STATIC_LIBRARIES}) + target_link_directories(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_STATIC_LIBRARY_DIRS}) + endif() + else() + find_library(GST_PLUGIN_${plugin}_LIBRARY + NAMES gst${plugin} + PATHS + ${GSTREAMER_LIB_PATH} + ${GSTREAMER_PLUGIN_PATH} + NO_DEFAULT_PATH + ) + if(GST_PLUGIN_${plugin}_LIBRARY) + target_link_libraries(GStreamer::Plugins INTERFACE ${GST_PLUGIN_${plugin}_LIBRARY}) + set(GST_PLUGIN_${plugin}_FOUND TRUE) + endif() + endif() + if(GST_PLUGIN_${plugin}_FOUND) + target_compile_definitions(GStreamer::Plugins INTERFACE GST_PLUGIN_${plugin}_FOUND) + endif() + endforeach() + + target_link_libraries(GStreamer::GStreamer INTERFACE GStreamer::Plugins) + endif() +endif() diff --git a/cmake/find-modules/GStreamerHelpers.cmake b/cmake/find-modules/GStreamerHelpers.cmake new file mode 100644 index 000000000000..de472e0f47bd --- /dev/null +++ b/cmake/find-modules/GStreamerHelpers.cmake @@ -0,0 +1,385 @@ +# gstreamer_get_package_url( ) +# PLATFORM: android, ios, macos, macos_devel, windows_msvc_x64, windows_msvc_arm64, +# good_plugins, good_plugins_qt6, monorepo +function(gstreamer_get_package_url PLATFORM VERSION OUTPUT_VAR) + if(PLATFORM STREQUAL "android") + set(_url "https://gstreamer.freedesktop.org/data/pkg/android/${VERSION}/gstreamer-1.0-android-universal-${VERSION}.tar.xz") + elseif(PLATFORM STREQUAL "ios") + set(_url "https://gstreamer.freedesktop.org/data/pkg/ios/${VERSION}/gstreamer-1.0-devel-${VERSION}-ios-universal.pkg") + elseif(PLATFORM STREQUAL "macos") + set(_url "https://gstreamer.freedesktop.org/data/pkg/macos/${VERSION}/gstreamer-1.0-${VERSION}-universal.pkg") + elseif(PLATFORM STREQUAL "macos_devel") + set(_url "https://gstreamer.freedesktop.org/data/pkg/macos/${VERSION}/gstreamer-1.0-devel-${VERSION}-universal.pkg") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + if(VERSION VERSION_GREATER_EQUAL "1.28.0") + set(_ext "exe") + else() + set(_ext "msi") + endif() + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-x86_64-${VERSION}.${_ext}") + elseif(PLATFORM STREQUAL "windows_msvc_arm64") + if(VERSION VERSION_GREATER_EQUAL "1.28.0") + set(_ext "exe") + else() + set(_ext "msi") + endif() + set(_url "https://gstreamer.freedesktop.org/data/pkg/windows/${VERSION}/msvc/gstreamer-1.0-msvc-arm64-${VERSION}.${_ext}") + elseif(PLATFORM STREQUAL "good_plugins") + set(_url "https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${VERSION}.tar.xz") + elseif(PLATFORM STREQUAL "good_plugins_qt6") + set(_url "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/${VERSION}/gstreamer-${VERSION}.tar.gz?path=subprojects/gst-plugins-good/ext/qt6") + elseif(PLATFORM STREQUAL "monorepo") + set(_url "https://gitlab.freedesktop.org/gstreamer/gstreamer/-/archive/${VERSION}/gstreamer-${VERSION}.tar.gz") + else() + message(FATAL_ERROR "gstreamer_get_package_url: Unknown platform '${PLATFORM}'") + endif() + + set(${OUTPUT_VAR} "${_url}" PARENT_SCOPE) +endfunction() + +# gstreamer_get_s3_mirror_url( ) +# Derives an S3 mirror URL from the primary URL's filename. +# Returns empty string for platforms without an S3 mirror. +function(gstreamer_get_s3_mirror_url PLATFORM VERSION OUTPUT_VAR) + set(_s3_base "https://qgroundcontrol.s3.us-west-2.amazonaws.com/dependencies/gstreamer") + + if(PLATFORM STREQUAL "android") + set(_dir "android") + elseif(PLATFORM STREQUAL "ios") + set(_dir "ios") + elseif(PLATFORM STREQUAL "macos" OR PLATFORM STREQUAL "macos_devel") + set(_dir "macos") + elseif(PLATFORM STREQUAL "windows_msvc_x64" OR PLATFORM STREQUAL "windows_msvc_arm64") + set(_dir "windows") + else() + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + gstreamer_get_package_url(${PLATFORM} ${VERSION} _primary_url) + # Works for simple URLs without query strings (those platforms are excluded above) + cmake_path(GET _primary_url FILENAME _filename) + + set(${OUTPUT_VAR} "${_s3_base}/${_dir}/${_filename}" PARENT_SCOPE) +endfunction() + +# gstreamer_get_fallback_checksum( ) +# Returns a pinned checksum for known artifacts when sidecar checksum fetch fails. +function(gstreamer_get_fallback_checksum PLATFORM VERSION OUTPUT_VAR) + set(_checksum "") + + if(VERSION STREQUAL "1.28.0") + if(PLATFORM STREQUAL "android") + set(_checksum "SHA256=3315be90b32b96aea5339f161725b4298f939cc502ac299eff81b3819d4f5cc3") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + set(_checksum "SHA256=d6aca5785cae9d9ed447db491cc921163ff6af0c657eb733293c55416c8b71a6") + elseif(PLATFORM STREQUAL "windows_msvc_arm64") + set(_checksum "SHA256=99c41b2a4db08730cf17d89f17fed8eddb81e5da569285c83e00266a1f1b9357") + elseif(PLATFORM STREQUAL "macos") + set(_checksum "SHA256=9c252ae9d3ac5bc54505c4a4e93556c7d6e93218a18ad8060b30770d6db036a6") + elseif(PLATFORM STREQUAL "macos_devel") + set(_checksum "SHA256=592d6fe925799470a67da9cbbba6badf4d7ac63c89f6f4532baad7ec1daba5a4") + elseif(PLATFORM STREQUAL "ios") + set(_checksum "SHA256=c4e3365e37c3eae05d8acd470d338560c89a213fb58157c8773db151dc7ebcd7") + elseif(PLATFORM STREQUAL "good_plugins") + set(_checksum "SHA256=d97700f346fdf9ef5461c035e23ed1ce916ca7a31d6ddad987f774774361db77") + endif() + elseif(VERSION STREQUAL "1.22.12") + if(PLATFORM STREQUAL "android") + set(_checksum "SHA256=be92cf477d140c270b480bd8ba0e26b1e01c8db042c46b9e234d87352112e485") + elseif(PLATFORM STREQUAL "windows_msvc_x64") + set(_checksum "SHA256=e5cbc6fb9f40fc2850806163df4b9d92012f967c842dc000a2b254cbcd7901d6") + endif() + endif() + + set(${OUTPUT_VAR} "${_checksum}" PARENT_SCOPE) +endfunction() + +# gstreamer_fetch_checksum( ) +# Downloads the .sha256sum sidecar from freedesktop.org and returns "SHA256=". +# Returns empty string on failure (checksum unavailable or unparseable). +function(gstreamer_fetch_checksum PLATFORM VERSION OUTPUT_VAR) + gstreamer_get_package_url(${PLATFORM} ${VERSION} _pkg_url) + + # URLs with query strings (e.g. good_plugins_qt6) have no sidecar checksum + string(FIND "${_pkg_url}" "?" _qs_pos) + if(NOT _qs_pos EQUAL -1) + message(STATUS "GStreamer: Skipping checksum for ${PLATFORM} ${VERSION} (URL contains query string)") + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + + set(_checksum_url "${_pkg_url}.sha256sum") + + string(MD5 _url_hash "${_checksum_url}") + set(_checksum_dir "${CMAKE_BINARY_DIR}/_deps/checksums") + set(_checksum_file "${_checksum_dir}/${_url_hash}.sha256sum") + + foreach(_round RANGE 1 2) + if(NOT EXISTS "${_checksum_file}") + file(MAKE_DIRECTORY "${_checksum_dir}") + set(_checksum_tmp "${_checksum_file}.tmp") + set(_code 1) + foreach(_attempt RANGE 1 3) + file(DOWNLOAD "${_checksum_url}" "${_checksum_tmp}" + STATUS _status + TIMEOUT 30 + TLS_VERIFY ON + ) + list(GET _status 0 _code) + if(_code EQUAL 0) + break() + endif() + list(GET _status 1 _msg) + message(STATUS "GStreamer: Checksum download attempt ${_attempt}/3 failed: ${_msg}") + file(REMOVE "${_checksum_tmp}") + endforeach() + if(NOT _code EQUAL 0) + gstreamer_get_fallback_checksum(${PLATFORM} ${VERSION} _fallback_hash) + if(_fallback_hash) + message(WARNING + "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}; " + "using pinned fallback checksum.") + set(${OUTPUT_VAR} "${_fallback_hash}" PARENT_SCOPE) + return() + endif() + message(WARNING + "GStreamer: Checksum sidecar unavailable for ${PLATFORM} ${VERSION}; " + "continuing without integrity verification.") + set(${OUTPUT_VAR} "" PARENT_SCOPE) + return() + endif() + file(RENAME "${_checksum_tmp}" "${_checksum_file}") + endif() + + file(READ "${_checksum_file}" _content) + string(STRIP "${_content}" _content) + string(REGEX MATCH "([0-9a-fA-F]+)" _match "${_content}") + if(_match) + string(LENGTH "${CMAKE_MATCH_1}" _hash_len) + if(_hash_len EQUAL 64) + set(${OUTPUT_VAR} "SHA256=${CMAKE_MATCH_1}" PARENT_SCOPE) + return() + endif() + endif() + + # Parse failed — purge cached file and retry once + file(SIZE "${_checksum_file}" _file_size) + string(SUBSTRING "${_content}" 0 60 _content_preview) + string(REGEX REPLACE "[^ -~]" "?" _content_preview "${_content_preview}") + message(STATUS "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION} " + "(file size: ${_file_size}, preview: [${_content_preview}])") + file(REMOVE "${_checksum_file}") + endforeach() + + gstreamer_get_fallback_checksum(${PLATFORM} ${VERSION} _fallback_hash) + if(_fallback_hash) + message(WARNING + "GStreamer: Sidecar checksum content invalid/unparseable for ${PLATFORM} ${VERSION}; " + "using pinned fallback checksum.") + set(${OUTPUT_VAR} "${_fallback_hash}" PARENT_SCOPE) + return() + endif() + + message(WARNING "GStreamer: Could not parse checksum for ${PLATFORM} ${VERSION}; continuing without verification.") + set(${OUTPUT_VAR} "" PARENT_SCOPE) +endfunction() + +# gstreamer_resilient_download(URLS ... FILENAME DESTINATION_DIR +# RESULT_VAR [TIMEOUT ] [INACTIVITY_TIMEOUT ] [EXPECTED_HASH ] +# [ALLOW_FAILURE]) +# Downloads a file, trying each URL in order. Uses atomic temp-file rename to +# prevent partial-download ghosts. Skips if the destination file already exists. +macro(_gstreamer_parse_expected_hash _HASH_SPEC _OUT_ALGO _OUT_EXPECTED) + set(_gph_valid MD5 SHA1 SHA224 SHA256 SHA384 SHA512 SHA3_224 SHA3_256 SHA3_384 SHA3_512) + string(REGEX MATCH "^([A-Za-z0-9_]+)=(.+)$" _gph_match "${${_HASH_SPEC}}") + if(NOT _gph_match) + message(FATAL_ERROR "gstreamer_resilient_download: Invalid EXPECTED_HASH format '${${_HASH_SPEC}}' (expected ALGO=hex)") + endif() + set(${_OUT_ALGO} "${CMAKE_MATCH_1}") + set(${_OUT_EXPECTED} "${CMAKE_MATCH_2}") + list(FIND _gph_valid "${${_OUT_ALGO}}" _gph_idx) + if(_gph_idx EQUAL -1) + message(FATAL_ERROR "gstreamer_resilient_download: Unsupported hash algorithm '${${_OUT_ALGO}}'") + endif() +endmacro() + +function(gstreamer_resilient_download) + cmake_parse_arguments(ARG "ALLOW_FAILURE" "FILENAME;DESTINATION_DIR;RESULT_VAR;TIMEOUT;INACTIVITY_TIMEOUT;EXPECTED_HASH" "URLS" ${ARGN}) + + foreach(_required FILENAME DESTINATION_DIR RESULT_VAR) + if(NOT ARG_${_required}) + message(FATAL_ERROR "gstreamer_resilient_download: ${_required} is required") + endif() + endforeach() + if(NOT ARG_URLS) + message(FATAL_ERROR "gstreamer_resilient_download: at least one URL is required") + endif() + + if(NOT ARG_TIMEOUT) + set(ARG_TIMEOUT 120) + endif() + if(NOT ARG_INACTIVITY_TIMEOUT) + set(ARG_INACTIVITY_TIMEOUT 60) + endif() + + set(_dest "${ARG_DESTINATION_DIR}/${ARG_FILENAME}") + + if(EXISTS "${_dest}") + if(ARG_EXPECTED_HASH) + _gstreamer_parse_expected_hash(ARG_EXPECTED_HASH _hash_algo _expected) + file(${_hash_algo} "${_dest}" _cached_hash) + if(NOT _cached_hash STREQUAL "${_expected}") + message(STATUS "GStreamer: Cached ${ARG_FILENAME} failed ${_hash_algo} check, re-downloading") + file(REMOVE "${_dest}") + else() + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + else() + message(STATUS "GStreamer: Using cached ${ARG_FILENAME} (no checksum verification)") + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + endif() + + file(MAKE_DIRECTORY "${ARG_DESTINATION_DIR}") + set(_tmp "${_dest}.tmp") + set(_tried_urls "") + + foreach(_url IN LISTS ARG_URLS) + if(NOT _url) + continue() + endif() + message(STATUS "GStreamer: Downloading ${ARG_FILENAME} from ${_url}") + file(DOWNLOAD "${_url}" "${_tmp}" + STATUS _status + SHOW_PROGRESS + TIMEOUT ${ARG_TIMEOUT} + INACTIVITY_TIMEOUT ${ARG_INACTIVITY_TIMEOUT} + TLS_VERIFY ON + ) + list(GET _status 0 _code) + if(_code EQUAL 0) + if(ARG_EXPECTED_HASH) + _gstreamer_parse_expected_hash(ARG_EXPECTED_HASH _hash_algo _expected) + file(${_hash_algo} "${_tmp}" _actual_hash) + if(NOT _actual_hash STREQUAL "${_expected}") + message(WARNING "GStreamer: ${_hash_algo} mismatch for ${ARG_FILENAME} from ${_url}") + file(REMOVE "${_tmp}") + list(APPEND _tried_urls "${_url}") + continue() + endif() + endif() + file(RENAME "${_tmp}" "${_dest}") + set(${ARG_RESULT_VAR} "${_dest}" PARENT_SCOPE) + return() + endif() + list(GET _status 1 _error) + message(WARNING "GStreamer: Failed to download from ${_url}: ${_error}") + file(REMOVE "${_tmp}") + list(APPEND _tried_urls "${_url}") + endforeach() + + if(ARG_ALLOW_FAILURE) + message(STATUS "GStreamer: All download URLs failed for ${ARG_FILENAME} (allowing failure). Tried: ${_tried_urls}") + set(${ARG_RESULT_VAR} "" PARENT_SCOPE) + return() + endif() + + message(FATAL_ERROR "GStreamer: All download URLs failed for ${ARG_FILENAME}.\n" + "Tried: ${_tried_urls}\n" + "Install manually from https://gstreamer.freedesktop.org/download/ or set GStreamer_ROOT_DIR.") +endfunction() + +# gstreamer_get_recommended_version( []) +# Patch versions are auto-extracted from platform version strings in build-config.json +# via BuildConfig.cmake (e.g., gstreamer_android_version "1.22.12" -> QGC_GSTREAMER_PATCH_1_22=12). +function(gstreamer_get_recommended_version MAJOR MINOR OUTPUT_VAR) + set(_var_name "QGC_GSTREAMER_PATCH_${MAJOR}_${MINOR}") + if(DEFINED ${_var_name}) + set(_patch "${${_var_name}}") + elseif(ARGC GREATER 3 AND NOT "${ARGV3}" STREQUAL "") + set(_patch "${ARGV3}") + message(STATUS + "No patch mapping for GStreamer ${MAJOR}.${MINOR}; using detected patch ${_patch}.") + else() + set(_patch 0) + message(WARNING "No patch mapping for GStreamer ${MAJOR}.${MINOR} — defaulting to .0. " + "This version is not used by any platform target in .github/build-config.json.") + endif() + + set(${OUTPUT_VAR} "${MAJOR}.${MINOR}.${_patch}" PARENT_SCOPE) +endfunction() + +function(gstreamer_install_gio_modules) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_gio_modules: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + # Ship the full GIO module set from the selected runtime. Filtering can + # remove TLS/proxy helpers required by some deployments. + file(GLOB modules_to_install "${ARG_SOURCE_DIR}/*.${ARG_EXTENSION}*") + + if(modules_to_install) + install(FILES ${modules_to_install} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() + +function(gstreamer_install_plugins) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION;PREFIX" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_plugins: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + file(GLOB all_plugins "${ARG_SOURCE_DIR}/${ARG_PREFIX}*.${ARG_EXTENSION}*") + + # Install only plugins listed in GSTREAMER_PLUGINS (set by FindQGCGStreamer). + set(plugins_to_install "") + foreach(plugin_path IN LISTS all_plugins) + get_filename_component(plugin_name "${plugin_path}" NAME) + foreach(allowed IN LISTS GSTREAMER_PLUGINS) + if(plugin_name MATCHES "^${ARG_PREFIX}${allowed}([^a-zA-Z0-9]|$)") + list(APPEND plugins_to_install "${plugin_path}") + break() + endif() + endforeach() + endforeach() + + if(plugins_to_install) + install(FILES ${plugins_to_install} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() + +# _gst_normalize_and_validate_root() +# Normalize GStreamer_ROOT_DIR path separators and verify the directory exists. +# Must be called as a macro (not function) so it modifies the caller's scope. +macro(_gst_normalize_and_validate_root) + cmake_path(CONVERT "${GStreamer_ROOT_DIR}" TO_CMAKE_PATH_LIST GStreamer_ROOT_DIR NORMALIZE) + if(NOT EXISTS "${GStreamer_ROOT_DIR}") + message(FATAL_ERROR "GStreamer: SDK not found at '${GStreamer_ROOT_DIR}' — " + "check installation or set GStreamer_ROOT_DIR") + endif() +endmacro() + +# WARNING: This function copies ALL shared libraries from SOURCE_DIR without +# filtering. Only call it for auto-downloaded SDKs or known-clean directories. +# Do NOT point it at a shared system prefix (e.g. /usr/lib, Homebrew lib/). +function(gstreamer_install_libs) + cmake_parse_arguments(ARG "" "SOURCE_DIR;DEST_DIR;EXTENSION" "" ${ARGN}) + + if(NOT EXISTS "${ARG_SOURCE_DIR}") + message(WARNING "gstreamer_install_libs: SOURCE_DIR does not exist: ${ARG_SOURCE_DIR}") + return() + endif() + + file(GLOB all_libs "${ARG_SOURCE_DIR}/*.${ARG_EXTENSION}") + if(all_libs) + install(FILES ${all_libs} DESTINATION "${ARG_DEST_DIR}") + endif() +endfunction() diff --git a/cmake/find-modules/_FindGStreamer.cmake b/cmake/find-modules/_FindGStreamer.cmake deleted file mode 100644 index 8d858bed8238..000000000000 --- a/cmake/find-modules/_FindGStreamer.cmake +++ /dev/null @@ -1,456 +0,0 @@ -# SPDX-FileCopyrightText: 2024 L. E. Segovia -# SPDX-License-Ref: LGPL-2.1-or-later - -#[=======================================================================[.rst: -FindGStreamer -------- - -Finds the GStreamer library. Requires ``pkg-config`` to be installed. - -Configuration -^^^^^^^^^^^^^ - -This module can be configured with the following variables: - -``GStreamer_STATIC`` - Link against GStreamer statically (see below). - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` targets: - -``GStreamer::GStreamer`` - The GStreamer library. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``GStreamer_FOUND`` - True if the system has the GStreamer library. -``GStreamer_VERSION`` - The version of the GStreamer library which was found. -``GStreamer_INCLUDE_DIRS`` - Include directories needed to use GStreamer. -``GStreamer_LIBRARIES`` - Libraries needed to link to GStreamer. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``GStreamer_INCLUDE_DIR`` - The directory containing ``gst/gstversion.h``. -``GStreamer_LIBRARY`` - The path to the GStreamer library. - -Configuration Variables -^^^^^^^^^^^^^^^ - -Setting the following variables is required, depending on the operating system: - -``GStreamer_ROOT_DIR`` - Installation prefix of the GStreamer SDK. - -``GStreamer_USE_STATIC_LIBS` - Set to ON to force the use of the static libraries. Default is OFF. - -``GStreamer_EXTRA_DEPS`` - pkg-config names of the extra dependencies that will be included whenever linking against GStreamer. - -#]=======================================================================] - -if (GStreamer_FOUND) - return() -endif() - -##################### -# Setup variables # -##################### - -if (NOT DEFINED GStreamer_ROOT_DIR AND DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) -endif() - -if (NOT GStreamer_ROOT_DIR) - set(GStreamer_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../") -endif() - -if (NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "The directory GStreamer_ROOT_DIR=${GStreamer_ROOT_DIR} does not exist") -endif() - -if (NOT DEFINED GStreamer_USE_STATIC_LIBS) -set(GStreamer_USE_STATIC_LIBS OFF) -endif() - -# Set the environment for pkg-config -if (WIN32) - set(ENV{PKG_CONFIG_PATH} "${GStreamer_ROOT_DIR}/lib/pkgconfig;${GStreamer_ROOT_DIR}/lib/gstreamer-1.0/pkgconfig;${GStreamer_ROOT_DIR}/lib/gio/modules/pkgconfig") -else() - set(ENV{PKG_CONFIG_PATH} "${GStreamer_ROOT_DIR}/lib/pkgconfig:${GStreamer_ROOT_DIR}/lib/gstreamer-1.0/pkgconfig:${GStreamer_ROOT_DIR}/lib/gio/modules/pkgconfig") -endif() - -# Set the list of extra dependencies -if (NOT DEFINED GStreamer_EXTRA_DEPS) - set(GStreamer_EXTRA_DEPS) - if (DEFINED GSTREAMER_EXTRA_DEPS) - set(GStreamer_EXTRA_DEPS ${GSTREAMER_EXTRA_DEPS}) - endif() -endif() - -# Find libraries. This is meant to be used with static libraries -# (hence the reprioritization) but I've added a fallback to shared libraries -# and stub modules in case any are non-existent. -function(_gst_find_library LOCAL_LIB GST_LOCAL_LIB) - if (DEFINED ${GST_LOCAL_LIB}) - return() - endif() - - set(_gst_suffixes ${CMAKE_FIND_LIBRARY_SUFFIXES}) - set(_gst_prefixes ${CMAKE_FIND_LIBRARY_PREFIXES}) - if (APPLE) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".dylib" ".so" ".tbd") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - elseif (UNIX) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".so") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - else() - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a" ".lib") - set(CMAKE_FIND_LIBRARY_PREFIXES "" "lib") - endif() - - if ("${LOCAL_LIB}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - set(${GST_LOCAL_LIB} ${LOCAL_LIB} PARENT_SCOPE) - else() - find_library(${GST_LOCAL_LIB} - ${LOCAL_LIB} - HINTS ${ARGN} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED - ) - set(${GST_LOCAL_LIB} "${${GST_LOCAL_LIB}}" PARENT_SCOPE) - if (NOT ${GST_LOCAL_LIB}) - message(FATAL_ERROR "${LOCAL_LIB} was unexpectedly not found.") - endif() - endif() - - set(CMAKE_FIND_LIBRARY_SUFFIXES ${_gst_suffixes}) - set(CMAKE_FIND_LIBRARY_PREFIXES ${_gst_prefixes}) -endfunction() - -macro(_gst_apply_link_libraries HIDE PC_LIBRARIES PC_HINTS GST_TARGET) - if (APPLE AND ${HIDE}) - target_link_directories(${GST_TARGET} INTERFACE - ${${PC_HINTS}} - ) - endif() - foreach(LOCAL_LIB IN LISTS ${PC_LIBRARIES}) - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - if (NOT ${GST_LOCAL_LIB}) - _gst_find_library(${LOCAL_LIB} ${GST_LOCAL_LIB} ${${PC_HINTS}}) - endif() - if ("${${GST_LOCAL_LIB}}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - target_link_libraries(${GST_TARGET} INTERFACE - ${${GST_LOCAL_LIB}}) - elseif (APPLE AND ${HIDE}) - set(LOCAL_FILE) - get_filename_component(LOCAL_FILE ${${GST_LOCAL_LIB}} NAME) - target_link_libraries(${GST_TARGET} INTERFACE - "-hidden-l${LOCAL_FILE}") - elseif((UNIX OR ANDROID) AND ${HIDE}) - target_link_libraries(${GST_TARGET} INTERFACE - -Wl,--exclude-libs,${${GST_LOCAL_LIB}}) - else() - target_link_libraries(${GST_TARGET} INTERFACE - ${${GST_LOCAL_LIB}}) - endif() - endforeach() -endmacro() - -macro(_gst_filter_missing_directories GST_INCLUDE_DIRS) - set(_gst_include_dirs) - foreach(DIR IN LISTS ${GST_INCLUDE_DIRS}) - string(MAKE_C_IDENTIFIER "${DIR}" _gst_dir_id) - if (DEFINED _gst_exists_${_gst_dir_id}) - if (_gst_exists_${_gst_dir_id}) - list(APPEND _gst_include_dirs "${DIR}") - endif() - elseif (EXISTS "${DIR}") - list(APPEND _gst_include_dirs "${DIR}") - set(_gst_exists_${_gst_dir_id} TRUE) - else() - message(WARNING "Skipping missing include folder ${DIR}.") - set(_gst_exists_${_gst_dir_id} FALSE) - endif() - endforeach() - set(${GST_INCLUDE_DIRS} "${_gst_include_dirs}") -endmacro() - -macro(_gst_apply_frameworks PC_STATIC_LDFLAGS_OTHER GST_TARGET) - if (APPLE) - # LDFLAGS_OTHER may include framework linkage. Because CMake - # iterates over arguments separated by spaces, it doesn't realise - # that those arguments must not be split. - set(new_ldflags) - set(assemble_framework FALSE) - foreach(_arg IN LISTS ${PC_STATIC_LDFLAGS_OTHER}) - if (assemble_framework) - set(assemble_framework FALSE) - find_library(GST_${_arg}_LIB ${_arg} REQUIRED) - target_link_libraries(${GST_TARGET} - INTERFACE - "${GST_${_arg}_LIB}" - ) - elseif (_arg STREQUAL "-framework") - set(assemble_framework TRUE) - else() - set(assemble_framework FALSE) - list(APPEND new_ldflags "${_arg}") - endif() - endforeach() - set_target_properties(${GST_TARGET} PROPERTIES - INTERFACE_LINK_OPTIONS "${new_ldflags}" - ) - else() - set_target_properties(${TARGET} PROPERTIES - INTERFACE_LINK_OPTIONS "${${PC_STATIC_LDFLAGS_OTHER}}" - ) - endif() -endmacro() - -################################ -# Set up the targets # -################################ - -find_package(PkgConfig REQUIRED) - -# GStreamer's pkg-config modules are a MUST -- but we'll test them below -pkg_check_modules(PC_GStreamer gstreamer-1.0 ${GStreamer_EXTRA_DEPS}) -# Simulate the list that'll be wholearchive'd. -# Unfortunately, this uses an option only available with pkgconf. -# set(_old_pkg_config_executable "${PKG_CONFIG_EXECUTABLE}") -# set(PKG_CONFIG_EXECUTABLE ${PKG_CONFIG_EXECUTABLE} --maximum-traverse-depth=1) -# pkg_check_modules(PC_GStreamer_NoDeps QUIET REQUIRED gstreamer-1.0 ${GStreamer_EXTRA_DEPS}) -# set(PKG_CONFIG_EXECUTABLE "${_old_pkg_config_executable}") - -set(GStreamer_VERSION "${PC_GStreamer_VERSION}") - -# Test validity of the paths -# NOTE: only paths that must be considered are those provided by pkg-config -# NOTE 2: also exclude sysroots -find_path(GStreamer_INCLUDE_DIR - NAMES gst/gstversion.h - PATHS ${PC_GStreamer_INCLUDE_DIRS} - PATH_SUFFIXES gstreamer-1.0 - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED -) - -find_library(GStreamer_LIBRARY - NAMES gstreamer-1.0 - PATHS ${PC_GStreamer_LIBRARY_DIRS} - NO_DEFAULT_PATH - NO_CMAKE_FIND_ROOT_PATH - REQUIRED -) - -# Android: Ignore these libraries when constructing the IMPORTED_LOCATION -set(_gst_IGNORED_SYSTEM_LIBRARIES c c++ unwind m dl atomic) -if (ANDROID) - list(APPEND _gst_IGNORED_SYSTEM_LIBRARIES log GLESv2 EGL OpenSLES android vulkan) -elseif(APPLE) - list(APPEND _gst_IGNORED_SYSTEM_LIBRARIES iconv resolv System) -endif() - -# Normalize library flags coming from srt/haisrt -# https://github.com/Haivision/srt/commit/b90b64d26f850fb0efcc4cdd8b31cbf74bd4db0c -set(_gst_SRT_REGEX_PATCH "^:lib(.+)\\.(a|so|lib|dylib)$") - -if(PC_GStreamer_FOUND AND (NOT TARGET GStreamer::GStreamer)) - # This is not UNKNOWN but INTERFACE, as we only intend to - # make a target suitable for downstream consumption. - # FindPkgConfig already takes care of things, however it is totally unable - # to discern between shared and static libraries when populating - # xxx_STATIC_LINK_LIBRARIES, so we need to populate them manually. - add_library(GStreamer::GStreamer INTERFACE IMPORTED) - - if (GStreamer_USE_STATIC_LIBS) - _gst_filter_missing_directories(PC_GStreamer_STATIC_INCLUDE_DIRS) - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_STATIC_CFLAGS_OTHER}" - ) - if (PC_GStreamer_STATIC_INCLUDE_DIRS) - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_STATIC_INCLUDE_DIRS}" - ) - endif() - _gst_apply_frameworks(PC_GStreamer_STATIC_LDFLAGS_OTHER GStreamer::GStreamer) - else() - set_target_properties(GStreamer::GStreamer PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_CFLAGS_OTHER}" - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_INCLUDE_DIRS}" - INTERFACE_LINK_OPTIONS "${PC_GStreamer_LDFLAGS_OTHER}" - ) - endif() - - add_library(GStreamer::deps INTERFACE IMPORTED) - - if (NOT GStreamer_USE_STATIC_LIBS) - set_target_properties(GStreamer::deps PROPERTIES - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_LINK_LIBRARIES}" - ) - # We're done - else() - # Handle all libraries, even those specified with -l:libfoo.a (srt) - # Due to the unavailability of pkgconf's `--maximum-traverse-depth` - # on stock pkg-config, I attempt to simulate it through the shared - # libraries listing. - # If pkgconf is available, replace all PC_GStreamer_ entries with - # PC_GStreamer_NoDeps and uncomment the code block above. - foreach(LOCAL_LIB IN LISTS PC_GStreamer_LIBRARIES) - # list(TRANSFORM REPLACE) is of no use here - # https://gitlab.kitware.com/cmake/cmake/-/issues/16899 - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - if (NOT ${GST_LOCAL_LIB}) - _gst_find_library(${LOCAL_LIB} ${GST_LOCAL_LIB} ${PC_GStreamer_STATIC_LIBRARY_DIRS}) - endif() - target_link_libraries(GStreamer::GStreamer INTERFACE - "${${GST_LOCAL_LIB}}" - ) - endforeach() - - _gst_apply_link_libraries(ON PC_GStreamer_STATIC_LIBRARIES PC_GStreamer_STATIC_LIBRARY_DIRS GStreamer::deps) - endif() - - target_link_libraries(GStreamer::GStreamer - INTERFACE - GStreamer::deps - ) -endif() - -foreach(_gst_PLUGIN IN LISTS GSTREAMER_PLUGINS) - # Safety valve for the custom targets above - if ("${_gst_plugin}" IN_LIST _gst_CUSTOM_TARGETS) - continue() - endif() - - if (TARGET GStreamer::${_gst_PLUGIN}) - continue() - endif() - - if (GStreamer_FIND_REQUIRED_${_gst_PLUGIN}) - set(_gst_PLUGIN_REQUIRED REQUIRED) - else() - set(_gst_PLUGIN_REQUIRED) - endif() - - pkg_check_modules(PC_GStreamer_${_gst_PLUGIN} "gst${_gst_PLUGIN}") - - set(GStreamer_${_gst_PLUGIN}_FOUND "${PC_GStreamer_${_gst_PLUGIN}_FOUND}") - if (NOT GStreamer_${_gst_PLUGIN}_FOUND) - continue() - endif() - - add_library(GStreamer::${_gst_PLUGIN} INTERFACE IMPORTED) - _gst_filter_missing_directories(PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_CFLAGS_OTHER}" - ) - if (PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS}" - ) - endif() - if (GStreamer_USE_STATIC_LIBS) - _gst_apply_frameworks(PC_GStreamer_${_gst_PLUGIN}_STATIC_LDFLAGS_OTHER GStreamer::${_gst_PLUGIN}) - else() - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_${_gst_PLUGIN}_LINK_LIBRARIES}" - ) - # We're done - continue() - endif() - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - _gst_apply_link_libraries(OFF PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARIES PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARY_DIRS GStreamer::${_gst_PLUGIN}) -endforeach() - -foreach(_gst_PLUGIN IN LISTS GSTREAMER_APIS) - # Safety valve for the custom targets above - if ("${_gst_plugin}" IN_LIST _gst_CUSTOM_TARGETS) - continue() - endif() - - if (TARGET GStreamer::${_gst_PLUGIN}) - continue() - endif() - - if (GStreamer_FIND_REQUIRED_${_gst_PLUGIN}) - set(_gst_PLUGIN_REQUIRED REQUIRED) - else() - set(_gst_PLUGIN_REQUIRED) - endif() - - string(REGEX REPLACE "^api_(.+)" "\\1" _gst_PLUGIN_PC "${_gst_PLUGIN}") - string(REPLACE "_" "-" _gst_PLUGIN_PC "${_gst_PLUGIN_PC}") - - pkg_check_modules(PC_GStreamer_${_gst_PLUGIN} "gstreamer-${_gst_PLUGIN_PC}-1.0") - - set(GStreamer_${_gst_PLUGIN}_FOUND "${PC_GStreamer_${_gst_PLUGIN}_FOUND}") - if (NOT GStreamer_${_gst_PLUGIN}_FOUND) - continue() - endif() - - add_library(GStreamer::${_gst_PLUGIN} INTERFACE IMPORTED) - _gst_filter_missing_directories(PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_CFLAGS_OTHER}" - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - ) - if (PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS) - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_INCLUDE_DIRECTORIES "${PC_GStreamer_${_gst_PLUGIN}_INCLUDE_DIRS}" - ) - endif() - if (GStreamer_USE_STATIC_LIBS) - _gst_apply_frameworks(PC_GStreamer_${_gst_PLUGIN}_STATIC_LDFLAGS_OTHER GStreamer::${_gst_PLUGIN}) - else() - set_target_properties(GStreamer::${_gst_PLUGIN} PROPERTIES - INTERFACE_LINK_OPTIONS "${PC_GStreamer_${_gst_PLUGIN}_LDFLAGS_OTHER}" - INTERFACE_LINK_LIBRARIES "${PC_GStreamer_${_gst_PLUGIN}_LINK_LIBRARIES}" - ) - # We're done - continue() - endif() - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - _gst_apply_link_libraries(OFF PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARIES PC_GStreamer_${_gst_PLUGIN}_STATIC_LIBRARY_DIRS GStreamer::${_gst_PLUGIN}) -endforeach() - -# Perform final validation -include(FindPackageHandleStandardArgs) -set(_gst_handle_version_range) -if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.19.0") - set(_gst_handle_version_range "HANDLE_VERSION_RANGE") -endif() -find_package_handle_standard_args(GStreamer - REQUIRED_VARS - GStreamer_LIBRARY - GStreamer_INCLUDE_DIR - VERSION_VAR GStreamer_VERSION - ${_gst_handle_version_range} - HANDLE_COMPONENTS -) diff --git a/cmake/find-modules/_FindGStreamerMobile.cmake b/cmake/find-modules/_FindGStreamerMobile.cmake deleted file mode 100644 index 90311b27428e..000000000000 --- a/cmake/find-modules/_FindGStreamerMobile.cmake +++ /dev/null @@ -1,607 +0,0 @@ -# SPDX-FileCopyrightText: 2024 L. E. Segovia -# SPDX-License-Ref: LGPL-2.1-or-later - -#[=======================================================================[.rst: -FindGStreamerMobile -------- - -Creates additional mobile targets to install fonts and the CA certificate -bundle. Android and iOS only. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`INTERFACE` targets: - -``GStreamer::fonts`` - A target that will install GStreamer's default fonts into the app. - -``GStreamer::ca_certificates`` - A target that will install the NSS CA certificate bundle into the app. - -This module defines the following :prop_tgt:`SHARED` targets: - -``GStreamer::mobile`` - A target that will build the shared library consisting of GStreamer plus all the selected plugin components. (Android/iOS only) - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``GStreamer_Mobile_FOUND`` - ON if the system has the GStreamer library. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``GStreamer_CA_BUNDLE`` - Path to /etc/ssl/certs/ca-certificates.crt. -``GStreamer_UBUNTU_R_TTF`` - Path to the TrueType font Ubuntu R. -``GStreamer_FONTS_CONF`` - Path to /etc/fonts.conf. - -Configuration Variables -^^^^^^^^^^^^^^^ - -Like with the main GStreamer library, setting the following variables is -required, depending on the operating system: - -``GStreamer_ROOT_DIR`` - Installation prefix of the GStreamer SDK. - -``GStreamer_JAVA_SRC_DIR`` - Target directory for deploying the selected plugins' Java classfiles to. (Android only) - -``GStreamer_Mobile_MODULE_NAME`` - Name for the GStreamer::mobile shared library. Default is ``gstreamer_android`` (Android) or ``gstreamer_mobile`` (iOS). - -``GStreamer_ASSETS_DIR`` - Target directory for deploying assets to. - -``G_IO_MODULES`` - Set this to the GIO modules you need, additional to any GStreamer plugins. (Usually set to ``gnutls`` or ``openssl``) - -``G_IO_MODULES_PATH`` - Path for the static GIO modules. - -#]=======================================================================] - -if (GStreamer_Mobile_FOUND) - return() -endif() - -##################### -# Setup variables # -##################### - -if (NOT DEFINED GStreamer_ROOT_DIR AND DEFINED GSTREAMER_ROOT) - set(GStreamer_ROOT_DIR ${GSTREAMER_ROOT}) -endif() - -if (NOT GStreamer_ROOT_DIR) - set(GStreamer_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../") -endif() - -if (NOT EXISTS "${GStreamer_ROOT_DIR}") - message(FATAL_ERROR "The directory GStreamer_ROOT_DIR=${GStreamer_ROOT_DIR} does not exist") -endif() - -set(_gst_required_vars) - -if (ca_certificates IN_LIST GStreamerMobile_FIND_COMPONENTS) - # for setting the default GTlsDatabase - list(APPEND GStreamer_EXTRA_DEPS gio-2.0) -endif() - -if (ANDROID) - list(APPEND GStreamer_EXTRA_DEPS zlib) -endif() - -# Prepare Android hotfixes for x264 -if(ANDROID_ABI MATCHES "^armeabi") - set(NEEDS_NOTEXT_FIX TRUE) - set(NEEDS_BSYMBOLIC_FIX TRUE) -elseif(ANDROID_ABI STREQUAL "x86") - set(NEEDS_NOTEXT_FIX TRUE) - set(NEEDS_BSYMBOLIC_FIX TRUE) -# arm64: https://ffmpeg.org/pipermail/ffmpeg-devel/2022-July/298734.html -elseif(ANDROID_ABI STREQUAL "x86_64" OR ANDROID_ABI STREQUAL "arm64-v8a") - set(NEEDS_BSYMBOLIC_FIX TRUE) -endif() - -# Set up output variables for Android -if(ANDROID) - if (NOT DEFINED GStreamer_JAVA_SRC_DIR AND DEFINED GSTREAMER_JAVA_SRC_DIR) - set(GStreamer_JAVA_SRC_DIR ${GSTREAMER_JAVA_SRC_DIR}) - elseif(NOT DEFINED GStreamer_JAVA_SRC_DIR) - set(GStreamer_JAVA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src/") - else() - # Gradle does not let us access the root of the subproject - # so we implement the ndk-build assumption ourselves - set(GStreamer_JAVA_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_JAVA_SRC_DIR}") - endif() - - if(NOT DEFINED GStreamer_NDK_BUILD_PATH AND DEFINED GSTREAMER_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GSTREAMER_NDK_BUILD_PATH}") - elseif(NOT DEFINED GStreamer_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT}/share/gst-android/ndk-build/") - endif() -endif() - -if(NOT DEFINED GStreamer_Mobile_MODULE_NAME) - if (DEFINED GSTREAMER_ANDROID_MODULE_NAME) - set(GStreamer_Mobile_MODULE_NAME "${GSTREAMER_ANDROID_MODULE_NAME}") - elseif(ANDROID) - set(GStreamer_Mobile_MODULE_NAME gstreamer_android) - else() - set(GStreamer_Mobile_MODULE_NAME gstreamer_mobile) - endif() -endif() - -if(ANDROID) - if(NOT DEFINED GStreamer_ASSETS_DIR AND DEFINED GSTREAMER_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${GSTREAMER_ASSETS_DIR}") - elseif(NOT DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../src/assets/") - else() - # Same as above - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_ASSETS_DIR}") - endif() - - if(NOT DEFINED GStreamer_NDK_BUILD_PATH AND DEFINED GSTREAMER_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GSTREAMER_NDK_BUILD_PATH}") - elseif(NOT DEFINED GStreamer_NDK_BUILD_PATH) - set(GStreamer_NDK_BUILD_PATH "${GStreamer_ROOT}/share/gst-android/ndk-build/") - endif() -elseif(IOS) - if(NOT DEFINED GStreamer_ASSETS_DIR AND DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR ${GStreamer_ASSETS_DIR}) - elseif(NOT DEFINED GStreamer_ASSETS_DIR) - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets") - else() - # Same as above - set(GStreamer_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../${GStreamer_ASSETS_DIR}") - endif() -endif() - -if (ANDROID) - file(READ "${GStreamer_NDK_BUILD_PATH}/GStreamer.java" JAVA_INPUT) -endif() - -if (ANDROID OR APPLE) - set(GSTREAMER_IS_MOBILE ON) -else() - set(GSTREAMER_IS_MOBILE OFF) -endif() - -# Block shared GStreamer on mobile -if (GSTREAMER_IS_MOBILE) - if (NOT DEFINED GStreamer_USE_STATIC_LIBS) - set(GStreamer_USE_STATIC_LIBS ON) - endif() - if (NOT GStreamer_USE_STATIC_LIBS) - message(FATAL_ERROR "Shared library GStreamer is not supported on mobile platforms") - endif() -endif() - -# Now, let's set up targets for each of the components supplied -# These are the required plugins -set(_gst_plugins ${GStreamerMobile_FIND_COMPONENTS}) -# These are custom handled targets, and must be skipped from the loop -list(REMOVE_ITEM _gst_plugins fonts ca_certificates mobile) -list(REMOVE_DUPLICATES _gst_plugins) - -set(GSTREAMER_PLUGINS ${_gst_plugins}) -# These are the API packages -set(GSTREAMER_APIS ${GSTREAMER_PLUGINS}) -list(FILTER GSTREAMER_APIS INCLUDE REGEX "^api_") -# Filter them out, although they're handled the same -# they cannot be considered for the purposes of initialization -list(FILTER GSTREAMER_PLUGINS EXCLUDE REGEX "^api_") - -if (GSTREAMER_IS_MOBILE AND (NOT TARGET GStreamer::mobile)) - # Generate the plugins' declaration strings - # (don't append a semicolon, CMake does it as part of the list) - list(TRANSFORM GSTREAMER_PLUGINS - PREPEND "\nGST_PLUGIN_STATIC_DECLARE\(" - OUTPUT_VARIABLE PLUGINS_DECLARATION - ) - list(TRANSFORM PLUGINS_DECLARATION - APPEND "\)" - OUTPUT_VARIABLE PLUGINS_DECLARATION - ) - if(PLUGINS_DECLARATION) - set(PLUGINS_DECLARATION "${PLUGINS_DECLARATION};") - endif() - - # Generate the plugins' registration strings - list(TRANSFORM GSTREAMER_PLUGINS - PREPEND "\nGST_PLUGIN_STATIC_REGISTER\(" - OUTPUT_VARIABLE PLUGINS_REGISTRATION - ) - list(TRANSFORM PLUGINS_REGISTRATION - APPEND "\)" - OUTPUT_VARIABLE PLUGINS_REGISTRATION - ) - if(PLUGINS_REGISTRATION) - set(PLUGINS_REGISTRATION "${PLUGINS_REGISTRATION};") - endif() - - # Generate list of gio modules - if (NOT G_IO_MODULES) - set(G_IO_MODULES) - endif() - list(TRANSFORM G_IO_MODULES - PREPEND "gio" - OUTPUT_VARIABLE G_IO_MODULES_LIBS - ) - list(TRANSFORM G_IO_MODULES - PREPEND "\nGST_G_IO_MODULE_DECLARE\(" - OUTPUT_VARIABLE G_IO_MODULES_DECLARE - ) - list(TRANSFORM G_IO_MODULES_DECLARE - APPEND "\);" - OUTPUT_VARIABLE G_IO_MODULES_DECLARE - ) - if(G_IO_MODULES_DECLARE) - set(G_IO_MODULES_DECLARE "${G_IO_MODULES_DECLARE};") - endif() - list(TRANSFORM G_IO_MODULES - PREPEND "\nGST_G_IO_MODULE_LOAD\(" - OUTPUT_VARIABLE G_IO_MODULES_LOAD - ) - list(TRANSFORM G_IO_MODULES_LOAD - APPEND "\)" - OUTPUT_VARIABLE G_IO_MODULES_LOAD - ) - if(G_IO_MODULES_LOAD) - set(G_IO_MODULES_LOAD "${G_IO_MODULES_LOAD};") - endif() - - # Generates a source file that declares and registers all the required plugins - if (ANDROID) - configure_file( - "${CMAKE_CURRENT_LIST_DIR}/GStreamer/gstreamer_android-1.0.c.in" - "${GStreamer_Mobile_MODULE_NAME}.c" - ) - else() - configure_file( - "${CMAKE_CURRENT_LIST_DIR}/GStreamer/gst_ios_init.m.in" - "${GStreamer_Mobile_MODULE_NAME}.m" - ) - endif() - - # Creates a shared library including gstreamer, its plugins and all the dependencies - if (ANDROID) - add_library(GStreamerMobile - SHARED - "${GStreamer_Mobile_MODULE_NAME}.c" - ) - else() - add_library(GStreamerMobile SHARED) - enable_language(OBJC OBJCXX) - target_sources(GStreamerMobile - PRIVATE - "${GStreamer_Mobile_MODULE_NAME}.m" - ) - set_source_files_properties("${GStreamer_Mobile_MODULE_NAME}.m" - PROPERTIES - LANGUAGE OBJC - ) - find_library(Foundation_LIB Foundation REQUIRED) - target_link_libraries(GStreamerMobile - PRIVATE - ${Foundation_LIB} - ) - endif() - add_library(GStreamer::mobile ALIAS GStreamerMobile) - - # Assume it's C++ for the sake of gstsoundtouch - set_target_properties( - GStreamerMobile - PROPERTIES - LIBRARY_OUTPUT_NAME ${GStreamer_Mobile_MODULE_NAME} - ) - if (APPLE) - set_target_properties( - GStreamerMobile - PROPERTIES - LINKER_LANGUAGE OBJCXX - FRAMEWORK TRUE - FRAMEWORK_VERSION A - MACOSX_FRAMEWORK_IDENTIFIER org.gstreamer.GStreamerMobile - ) - else() - set_target_properties( - GStreamerMobile - PROPERTIES - LINKER_LANGUAGE CXX - ) - endif() -endif() - -if (GStreamerMobile_FIND_REQUIRED) -find_package(GStreamer COMPONENTS ${_gst_plugins} REQUIRED) -else() -find_package(GStreamer COMPONENTS ${_gst_plugins}) -endif() - -# Path for the static GIO modules -pkg_get_variable(G_IO_MODULES_PATH gio-2.0 giomoduledir) -if (NOT G_IO_MODULES_PATH) - set(G_IO_MODULES_PATH "${GStreamer_ROOT_DIR}/lib/gio/modules") -endif() - -if (GSTREAMER_IS_MOBILE) - set_target_properties( - GStreamerMobile - PROPERTIES - VERSION ${PC_GStreamer_VERSION} - SOVERSION ${PC_GStreamer_VERSION} - ) - - # Handle all libraries, even those specified with -l:libfoo.a (srt) - # Due to the unavailability of pkgconf's `--maximum-traverse-depth` - # on stock pkg-config, I attempt to simulate it through the shared - # libraries listing. - # If pkgconf is available, replace all PC_GStreamer_ entries with - # PC_GStreamer_NoDeps and uncomment the code block above. - foreach(LOCAL_LIB IN LISTS PC_GStreamer_LIBRARIES) - # list(TRANSFORM REPLACE) is of no use here - # https://gitlab.kitware.com/cmake/cmake/-/issues/16899 - if (LOCAL_LIB MATCHES "${_gst_SRT_REGEX_PATCH}") - string(REGEX REPLACE "${_gst_SRT_REGEX_PATCH}" "\\1" LOCAL_LIB "${LOCAL_LIB}") - endif() - string(MAKE_C_IDENTIFIER "_gst_${LOCAL_LIB}" GST_LOCAL_LIB) - # These have already been found by FindGStreamer - if ("${${GST_LOCAL_LIB}}" IN_LIST _gst_IGNORED_SYSTEM_LIBRARIES) - target_link_libraries(GStreamerMobile PRIVATE - "${${GST_LOCAL_LIB}}" - ) - elseif (MSVC) - target_link_libraries(GStreamerMobile PRIVATE - "/WHOLEARCHIVE:${${GST_LOCAL_LIB}}" - ) - elseif(APPLE) - target_link_libraries(GStreamerMobile PRIVATE - "-Wl,-force_load,${${GST_LOCAL_LIB}}" - ) - else() - target_link_libraries(GStreamerMobile PRIVATE - "-Wl,--whole-archive,${${GST_LOCAL_LIB}},--no-whole-archive" - ) - endif() - endforeach() - - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::deps - ) - - target_link_options( - GStreamerMobile - INTERFACE - $ - ) - - target_include_directories( - GStreamerMobile - INTERFACE - $ - ) - - # Text relocations are required for all 32-bit objects. We - # must disable the warning to allow linking with lld. Unlike gold, ld which - # will silently allow text relocations, lld support must be explicit. - # - # See https://crbug.com/911658#c19 for more information. See also - # https://trac.ffmpeg.org/ticket/7878 - if(DEFINED NEEDS_NOTEXT_FIX) - target_link_options( - GStreamerMobile - PRIVATE - "-Wl,-z,notext" - ) - endif() - - # resolve textrels in the x86 asm - if(DEFINED NEEDS_BSYMBOLIC_FIX) - target_link_options( - GStreamerMobile - PRIVATE - "-Wl,-Bsymbolic" - ) - endif() - - if (ANDROID) - # Collect all Java-based initializer classes - set(GSTREAMER_PLUGINS_CLASSES) - foreach(LOCAL_PLUGIN IN LISTS GSTREAMER_PLUGINS) - file(GLOB_RECURSE - LOCAL_PLUGIN_CLASS - FOLLOW_SYMLINKS - RELATIVE "${GStreamer_NDK_BUILD_PATH}" - "${GStreamer_NDK_BUILD_PATH}/${LOCAL_PLUGIN}/*.java" - ) - list(APPEND GSTREAMER_PLUGINS_CLASSES ${LOCAL_PLUGIN_CLASS}) - endforeach() - - # Same as above, but collect the plugins themselves - set(GSTREAMER_PLUGINS_WITH_CLASSES) - foreach(LOCAL_PLUGIN IN LISTS GSTREAMER_PLUGINS) - if(EXISTS "${GStreamer_NDK_BUILD_PATH}/${LOCAL_PLUGIN}/") - list(APPEND GSTREAMER_PLUGINS_WITH_CLASSES ${LOCAL_PLUGIN}) - endif() - endforeach() - - add_custom_target( - "copyjavasource_${ANDROID_ABI}" - ) - - foreach(LOCAL_FILE IN LISTS GSTREAMER_PLUGINS_CLASSES) - string(MAKE_C_IDENTIFIER "cp_${LOCAL_FILE}" COPYJAVASOURCE_TGT) - add_custom_target( - ${COPYJAVASOURCE_TGT} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_NDK_BUILD_PATH}/${LOCAL_FILE}" - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - BYPRODUCTS - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/${LOCAL_FILE}" - ) - add_dependencies(copyjavasource_${ANDROID_ABI} ${COPYJAVASOURCE_TGT}) - endforeach() - endif() - - # And, finally, set the GIO modules up - if (G_IO_MODULES_LIBS) - add_library(GStreamer::gio_modules INTERFACE IMPORTED) - - _gst_apply_link_libraries(OFF G_IO_MODULES_LIBS G_IO_MODULES_PATH GStreamer::gio_modules) - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::gio_modules - ) - endif() - set(GStreamer_mobile_FOUND TRUE) -endif() - -if(fonts IN_LIST GStreamerMobile_FIND_COMPONENTS) - set(GStreamer_UBUNTU_R_TTF "${GStreamer_NDK_BUILD_PATH}/fontconfig/fonts/Ubuntu-R.ttf" - CACHE FILEPATH "Path to Ubuntu-R.ttf" - ) - set(GStreamer_FONTS_CONF - "${GStreamer_NDK_BUILD_PATH}/fontconfig/fonts.conf" - CACHE FILEPATH "Path to fonts.conf" - ) - if (EXISTS "${GStreamer_UBUNTU_R_TTF}" AND EXISTS "${GStreamer_FONTS_CONF}") - set(GStreamerMobile_fonts_FOUND ON) - set(_gst_required_vars "GStreamer_UBUNTU_R_TTF GStreamer_FONTS_CONF ${gst_required_vars}") - - if (ANDROID) - string(REPLACE "//copyFonts" "copyFonts" JAVA_INPUT "${JAVA_INPUT}") - add_custom_target( - copyfontsres_${ANDROID_ABI} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_UBUNTU_R_TTF}" - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_FONTS_CONF}" - "${GStreamer_ASSETS_DIR}/fontconfig/" - BYPRODUCTS - "${GStreamer_ASSETS_DIR}/fontconfig/fonts/truetype/Ubuntu-R.ttf" - "${GStreamer_ASSETS_DIR}/fontconfig/fonts.conf" - ) - - if (TARGET GStreamerMobile) - add_dependencies(GStreamerMobile copyfontsres_${ANDROID_ABI}) - endif() - elseif(APPLE) - set(GStreamerMobile_fonts_FOUND ON) - list(APPEND GSTREAMER_RESOURCES "${GStreamer_FONTS_CONF}" "${GStreamer_UBUNTU_R_TTF}") - else() - message(FATAL_ERROR "No fonts assets available for this operating system.") - endif() - else() - set(GStreamerMobile_fonts_FOUND OFF) - endif() -endif() - -if(ca_certificates IN_LIST GStreamerMobile_FIND_COMPONENTS) - set(GStreamer_CA_BUNDLE "${GStreamer_ROOT_DIR}/etc/ssl/certs/ca-certificates.crt" - CACHE FILEPATH "Path to ca-certificates bundle" - ) - if (EXISTS "${GStreamer_CA_BUNDLE}") - set(GStreamerMobile_ca_certificates_FOUND ON) - string(REPLACE "//copyCaCertificates" "copyCaCertificates" JAVA_INPUT "${JAVA_INPUT}") - set(_gst_required_vars "GStreamer_CA_BUNDLE ${_gst_required_vars}") - - if (ANDROID) - add_custom_target( - copycacertificatesres_${ANDROID_ABI} - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_ASSETS_DIR}/ssl/certs/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${GStreamer_CA_BUNDLE}" - "${GStreamer_ASSETS_DIR}/ssl/certs/" - BYPRODUCTS "${GStreamer_ASSETS_DIR}/ssl/certs/ca-certificates.crt" - ) - - if (TARGET GStreamerMobile) - add_dependencies(GStreamerMobile copycacertificatesres_${ANDROID_ABI}) - target_compile_definitions(GStreamerMobile - PRIVATE - GSTREAMER_INCLUDE_CA_CERTIFICATES - ) - endif() - elseif (APPLE) - list(APPEND GSTREAMER_RESOURCES "${GStreamer_CA_BUNDLE}") - else() - message(FATAL_ERROR "No certificate bundle available for this operating system.") - endif() - else() - set(GStreamerMobile_ca_certificates_FOUND OFF) - endif() -endif() - -if (ANDROID) - file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/GStreamer.java" "${JAVA_INPUT}") - add_custom_target( - enable_includes_in_gstreamer_java - COMMAND - "${CMAKE_COMMAND}" -E make_directory - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/" - COMMAND - "${CMAKE_COMMAND}" -E copy - "${CMAKE_CURRENT_BINARY_DIR}/GStreamer.java" - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/GStreamer.java" - BYPRODUCTS - "${GStreamer_JAVA_SRC_DIR}/org/freedesktop/gstreamer/GStreamer.java" - ) - if (TARGET GStreamerMobile) - add_dependencies(copyjavasource_${ANDROID_ABI} enable_includes_in_gstreamer_java) - add_dependencies(GStreamerMobile copyjavasource_${ANDROID_ABI}) - endif() -endif() - -if (TARGET GStreamerMobile AND GSTREAMER_RESOURCES) - set_target_properties( - GStreamerMobile - PROPERTIES - RESOURCE "${GSTREAMER_RESOURCES}" - ) -endif() - -# Perform final validation -include(FindPackageHandleStandardArgs) -foreach(_gst_PLUGIN IN LISTS _gst_plugins) - set(GStreamerMobile_${_gst_PLUGIN}_FOUND "${GStreamer_${_gst_PLUGIN}_FOUND}") - - if (GStreamer_${_gst_PLUGIN}_FOUND) - target_link_libraries( - GStreamerMobile - PRIVATE - GStreamer::${_gst_PLUGIN} - ) - endif() -endforeach() -# FIXME: CMake does not tolerate interpolation of REQUIRED_VARS -find_package_handle_standard_args(GStreamerMobile - HANDLE_COMPONENTS -) diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh index 9514ce8243a0..2d8098cc8b68 100644 --- a/deploy/docker/entrypoint.sh +++ b/deploy/docker/entrypoint.sh @@ -27,7 +27,6 @@ if [[ -n "${ANDROID_SDK_ROOT:-}" ]]; then -DQT_HOST_PATH="${QT_HOST_PATH}" \ -DQT_ANDROID_ABIS="${ANDROID_ABIS}" \ -DANDROID_SDK_ROOT="${ANDROID_SDK_ROOT}" \ - -DQGC_CUSTOM_GST_PACKAGE=OFF \ -DQT_ANDROID_SIGN_APK=OFF cmake --build /project/build --target all --config "${BUILD_TYPE}" --parallel else diff --git a/deploy/linux/AppRun b/deploy/linux/AppRun index dc46bb7ca2db..c027c3834efb 100755 --- a/deploy/linux/AppRun +++ b/deploy/linux/AppRun @@ -25,9 +25,14 @@ for LIB_PATH in \ done # Preload system GLib 2.76+ for TTS compatibility (libspeechd.so.2 dependency) +# Must also preload system GIO to avoid ABI mismatch with bundled GStreamer GIO plugin if [ -n "$SYSTEM_GLIB" ]; then if nm -D "$SYSTEM_GLIB" 2>/dev/null | grep -qm1 g_string_free_and_steal; then - export LD_PRELOAD="${SYSTEM_GLIB}${LD_PRELOAD:+:$LD_PRELOAD}" + GLIB_PRELOAD="${SYSTEM_GLIB}" + if [ -f "$SYSTEM_GIO" ]; then + GLIB_PRELOAD="${GLIB_PRELOAD}:${SYSTEM_GIO}" + fi + export LD_PRELOAD="${GLIB_PRELOAD}${LD_PRELOAD:+:$LD_PRELOAD}" fi fi diff --git a/docs/assets/fly/toolbar/apm_support_indicator.png b/docs/assets/fly/toolbar/apm_support_indicator.png new file mode 100644 index 000000000000..d0f63475e9be Binary files /dev/null and b/docs/assets/fly/toolbar/apm_support_indicator.png differ diff --git a/docs/assets/fly/toolbar/battery_indicator.png b/docs/assets/fly/toolbar/battery_indicator.png index c26b0fce9e26..6f5b84ed3f27 100644 Binary files a/docs/assets/fly/toolbar/battery_indicator.png and b/docs/assets/fly/toolbar/battery_indicator.png differ diff --git a/docs/assets/fly/toolbar/esc_indicator.png b/docs/assets/fly/toolbar/esc_indicator.png new file mode 100644 index 000000000000..1576b67bf142 Binary files /dev/null and b/docs/assets/fly/toolbar/esc_indicator.png differ diff --git a/docs/assets/fly/toolbar/flight_modes_indicator.png b/docs/assets/fly/toolbar/flight_modes_indicator.png index e3481618ca87..821a72535077 100644 Binary files a/docs/assets/fly/toolbar/flight_modes_indicator.png and b/docs/assets/fly/toolbar/flight_modes_indicator.png differ diff --git a/docs/assets/fly/toolbar/gimbal_indicator.png b/docs/assets/fly/toolbar/gimbal_indicator.png new file mode 100644 index 000000000000..f73d2fb338bc Binary files /dev/null and b/docs/assets/fly/toolbar/gimbal_indicator.png differ diff --git a/docs/assets/fly/toolbar/gps_indicator.png b/docs/assets/fly/toolbar/gps_indicator.png index 4e2b76633fd1..71e813add254 100644 Binary files a/docs/assets/fly/toolbar/gps_indicator.png and b/docs/assets/fly/toolbar/gps_indicator.png differ diff --git a/docs/assets/fly/toolbar/joystick_indicator.png b/docs/assets/fly/toolbar/joystick_indicator.png new file mode 100644 index 000000000000..9b81227e7618 Binary files /dev/null and b/docs/assets/fly/toolbar/joystick_indicator.png differ diff --git a/docs/assets/fly/toolbar/main_status_indicator.png b/docs/assets/fly/toolbar/main_status_indicator.png new file mode 100644 index 000000000000..61b677ce34c5 Binary files /dev/null and b/docs/assets/fly/toolbar/main_status_indicator.png differ diff --git a/docs/assets/fly/toolbar/multi_vehicle_indicator.png b/docs/assets/fly/toolbar/multi_vehicle_indicator.png new file mode 100644 index 000000000000..71b5db858628 Binary files /dev/null and b/docs/assets/fly/toolbar/multi_vehicle_indicator.png differ diff --git a/docs/assets/fly/toolbar/rc_rssi_indicator.png b/docs/assets/fly/toolbar/rc_rssi_indicator.png new file mode 100644 index 000000000000..1c446b67aa2e Binary files /dev/null and b/docs/assets/fly/toolbar/rc_rssi_indicator.png differ diff --git a/docs/assets/fly/toolbar/remote_id_indicator.png b/docs/assets/fly/toolbar/remote_id_indicator.png new file mode 100644 index 000000000000..cf65d4c645a5 Binary files /dev/null and b/docs/assets/fly/toolbar/remote_id_indicator.png differ diff --git a/docs/assets/fly/toolbar/signing_indicator.jpg b/docs/assets/fly/toolbar/signing_indicator.jpg new file mode 100644 index 000000000000..c7f8dea46a4c Binary files /dev/null and b/docs/assets/fly/toolbar/signing_indicator.jpg differ diff --git a/docs/assets/fly/toolbar/telemetry_rssi_indicator.png b/docs/assets/fly/toolbar/telemetry_rssi_indicator.png new file mode 100644 index 000000000000..5a23ada7d5cd Binary files /dev/null and b/docs/assets/fly/toolbar/telemetry_rssi_indicator.png differ diff --git a/docs/assets/fly/toolbar/vtol_indicator.png b/docs/assets/fly/toolbar/vtol_indicator.png new file mode 100644 index 000000000000..d6f8221952fc Binary files /dev/null and b/docs/assets/fly/toolbar/vtol_indicator.png differ diff --git a/docs/assets/plan/plan_view_overview.jpg b/docs/assets/plan/plan_view_overview.jpg deleted file mode 100644 index c7de9d5f7d5b..000000000000 Binary files a/docs/assets/plan/plan_view_overview.jpg and /dev/null differ diff --git a/docs/assets/plan/plan_view_overview.png b/docs/assets/plan/plan_view_overview.png new file mode 100644 index 000000000000..e73b940e9ac6 Binary files /dev/null and b/docs/assets/plan/plan_view_overview.png differ diff --git a/docs/assets/settings/mavlink/overview_telemetry.jpg b/docs/assets/settings/mavlink/overview_telemetry.jpg new file mode 100644 index 000000000000..9631a67f226a Binary files /dev/null and b/docs/assets/settings/mavlink/overview_telemetry.jpg differ diff --git a/docs/en/SUMMARY.md b/docs/en/SUMMARY.md index 2bdb50744012..31e62fbaa89f 100644 --- a/docs/en/SUMMARY.md +++ b/docs/en/SUMMARY.md @@ -53,7 +53,7 @@ - [General](qgc-user-guide/settings_view/general.md) - [CSV Logging](qgc-user-guide/settings_view/csv.md) - [Offline Maps](qgc-user-guide/settings_view/offline_maps.md) - - [MAVLink](qgc-user-guide/settings_view/mavlink.md) + - [Telemetry](qgc-user-guide/settings_view/telemetry.md) - [Console Logging](qgc-user-guide/settings_view/console_logging.md) - [Virtual Joystick (PX4)](qgc-user-guide/settings_view/virtual_joystick.md) - [Analyze](qgc-user-guide/analyze_view/index.md) @@ -80,6 +80,7 @@ - [Getting Started with source & builds](qgc-dev-guide/getting_started/index.md) - [Build using Containers](qgc-dev-guide/getting_started/container.md) - [Navigating the Source Code](qgc-dev-guide/navigating_source.md) + - [API Reference (Doxygen)](https://api.qgroundcontrol.com/master/annotated.html) - [Class Hierarchy](qgc-dev-guide/classes/index.md) - [QGC Release/Branching Process](qgc-dev-guide/release_branching_process.md) - [Communication Flow](qgc-dev-guide/communication_flow.md) diff --git a/docs/en/qgc-dev-guide/classes/index.md b/docs/en/qgc-dev-guide/classes/index.md index 85f026674d99..9ce1d55bdfd6 100644 --- a/docs/en/qgc-dev-guide/classes/index.md +++ b/docs/en/qgc-dev-guide/classes/index.md @@ -1,5 +1,9 @@ # Class Hierarchy (high level) +::: tip +Doxygen documentation for classes can be found at . +::: + ## LinkManager, LinkInterface A "Link" in QGC is a specific type of communication pipe with the vehicle such as a serial port or UDP over WiFi. The base class for all links is LinkInterface. Each link runs on it's own thread and sends bytes to MAVLinkProtocol. diff --git a/docs/en/qgc-dev-guide/navigating_source.md b/docs/en/qgc-dev-guide/navigating_source.md index b2dec3c69e18..c04ffab06950 100644 --- a/docs/en/qgc-dev-guide/navigating_source.md +++ b/docs/en/qgc-dev-guide/navigating_source.md @@ -1,6 +1,13 @@ # Navigating QGC Source Code -QGC is a large code base. With that it can be daunting to find what your are looking for in the source. Below are listed some tips to help you find what you are looking for. +QGC is a large code base and it can be daunting to find what you are looking for in the source. + +Below are listed some tips to help you find what you are looking for. + +## Use the API Reference + +Doxygen documentation can be found at . +This can be easier to navigate and search than the source itself. ## Start from the top of the UI diff --git a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md index 50457b484cbe..3d6195ccd620 100644 --- a/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/en/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,49 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### MAVLink Signing MAVLink Signing indicator + +The MAVLink Signing indicator appears when signing keys have been configured (see [MAVLink 2 Signing](../settings_view/telemetry.md#signing)). +It shows a lock icon that indicates whether MAVLink 2 message signing is active on the current vehicle connection: + +- **Locked (green):** Signing is active — the vehicle's incoming packets matched a stored key, or a key was manually enabled. +- **Unlocked:** Signing is not active on the current connection. + +The dropdown shows the signing status, the name of the active key (if any), and the number of saved keys. +Expanding the indicator provides full key management: you can enable a key on the vehicle, disable the active key, delete unused keys, or add new keys. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -* Telemetry RSSI -* RC RSSI -* Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -* VTOL transitions -* Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/en/qgc-user-guide/plan_view/plan_view.md b/docs/en/qgc-user-guide/plan_view/plan_view.md index 648b34b4701a..f5f85af8e7f5 100644 --- a/docs/en/qgc-user-guide/plan_view/plan_view.md +++ b/docs/en/qgc-user-guide/plan_view/plan_view.md @@ -2,32 +2,32 @@ The _Plan View_ is used to plan _autonomous missions_ for your vehicle, and upload them to the vehicle. Once the mission is [planned](#plan_mission) and sent to the vehicle, you switch to the [Fly View](../fly_view/fly_view.md) to fly the mission. -It is also use to configure the [GeoFence](plan_geofence.md) and [Rally Points](plan_rally_points.md) if these are supported by the firmware. +It is also used to configure the [GeoFence](plan_geofence.md) and [Rally Points](plan_rally_points.md) if these are supported by the firmware. -![Plan View](../../../assets/plan/plan_view_overview.jpg) +![Plan View](../../../assets/plan/plan_view_overview.png) ## UI Overview {#ui_overview} -The [screenshot above](#plan_screenshot) shows a simple mission plan that starts with a takeoff at the [Planned Home](#planned_home) position (H), -flies through three waypoints, and then lands on the last waypoint (i.e. waypoint 3). +The [screenshot above](#plan_screenshot) shows the Plan View with an empty mission. +The map is centered on the [Planned Home](#planned_home) position (H). The main elements of the UI are: - **Map:** Displays the numbered indicators for the current mission, including the [Planned Home](#planned_home). Click on the indicators to select them (for editing) or drag them around to reposition them. -- **Plan Toolbar:** Status information for the currently selected waypoint relative to the previous waypoint, as well as statistics for the entire mission (e.g. horizontal distance and time for mission). - - `Max telem dist` is the distance between the [Planned Home](#planned_home) and the furthest waypoint. - - When connected to a vehicle it also shows an **Upload** button, can be used to upload the plan to the vehicle. -- **[Plan Tools](#plan_tools):** Used to create and manage missions. -- **[Mission Command List/Overlay](#mission_command_list):** Displays the current list of mission items (select items to [edit](#mission_command_editors)). -- **Terrain Altitude Overlay:** Shows the relative altitude of each mission command. - -It shows you information related to the currently selected waypoint as well as statistics for the entire mission. + Flight path lines and direction arrows show the planned route between waypoints. +- **Plan Toolbar:** Located at the top of the view with buttons for **Open**, **Save**, **Upload**, and **Clear**. + A hamburger menu (☰) provides additional options such as _Save as KML_ and _Download_ (load plan from vehicle). + The **Save** and **Upload** buttons are highlighted when there are unsaved or un-uploaded changes. +- **[Plan Tools](#plan_tools):** A vertical tool strip on the left side of the map used to add mission items (Takeoff, Waypoint, Pattern, ROI, Return/Land) and toggle the stats panel. +- **[Plan Editor Panel](#plan_editor_panel):** A collapsible tree view on the right side containing the plan file info, mission items, GeoFence, and rally point editors. +- **Layer Switcher:** Buttons in the top-right area for switching between **Mission**, **Geo-Fence**, and **Rally Point** editing layers. +- **Mission Stats / Terrain Panel:** A panel at the bottom of the map that can toggle between a terrain altitude profile chart (height AMSL vs. distance) and mission statistics (selected waypoint info, total distance, max telemetry distance, estimated flight time, and battery info). ## Planning a Mission {#plan_mission} -At very high level, the steps to create a mission are: +At a very high level, the steps to create a mission are: 1. Change to _Plan View_. 2. Add waypoints or commands to the mission and edit as needed. @@ -41,12 +41,8 @@ The following sections explain some of the details in the view. The _Planned Home_ shown in _Plan View_ is used to set the approximate start point when planning a mission (i.e. when a vehicle may not even be connected to QGC). It is used by QGC to estimate mission times and to draw waypoint lines. -![Planned Home Position](../../../assets/plan/mission/mission_settings_planned_home.jpg) - You should move/drag the planned home position to roughly the location where you plan to takeoff. -The altitude for the planned home position is set in the [Mission Settings](#mission_settings) panel. - - +The altitude for the planned home position is determined automatically from terrain data. :::tip The Fly View displays the _actual_ home position set by the vehicle firmware when it arms (this is where the vehicle will return in Return/RTL mode). @@ -54,132 +50,131 @@ The Fly View displays the _actual_ home position set by the vehicle firmware whe ## Plan Tools {#plan_tools} -The plan tools are used for adding individual waypoints, easing mission creation for complicated geometries, uploading/downloading/saving/restoring missions, and for navigating the map. The main tools are described below. - -::: info -**Center map**, **Zoom In**, **Zoom Out** tools help users better view and navigate the _Plan view_ map (they don't affect the mission commands sent to the vehicle). -::: +The plan tools are a vertical tool strip on the left side of the map, used for adding mission items. The tools are only visible when editing the Mission layer. The main tools (top to bottom) are described below. -### Add Waypoints +### Takeoff -Click on the **Add Waypoint** tool to activate it. While active, clicking on the map will add new mission waypoint at the clicked location. -The tool will stay active until you select it again. -Once you have added a waypoint, you can select it and drag it around to change its position. +Inserts a takeoff command into the mission. This tool is available for all vehicle types except rovers. -### File (Sync) {#file} - -The _File tools_ are used to move missions between the ground station and vehicle, and to save/restore them from files. -The tool displays an `!` to indicate that there are mission changes that you have not sent to the vehicle. - -::: info -Before you fly a mission you must upload it to the vehicle. -::: +### Pattern -The _File tools_ provide the following functionality: +The [Pattern](pattern.md) tool simplifies the creation of missions for flying complex geometries, including [surveys](../plan_view/pattern_survey.md) and [structure scans](../plan_view/pattern_structure_scan_v2.md). +If multiple pattern types are available for the vehicle, clicking the button opens a dropdown to select from Survey, Corridor Scan, Structure Scan, and landing patterns. -- Upload (Send to vehicle) -- Download (Load from vehicle) -- Save/Save as to File, including as KML file. -- Load from File -- Remove All (removes all mission waypoints from _Plan view_ and from vehicle) +### Waypoint -### Pattern +Click the **Waypoint** tool to toggle waypoint-on-click mode. While active, clicking on the map will add a new mission waypoint at the clicked location. +The tool stays active until you click it again to deactivate. +Once you have added a waypoint, you can select it and drag it around to reposition it. -The [Pattern](pattern.md) tool simplifies the creation of missions for flying complex geometries, including [surveys](../plan_view/pattern_survey.md) and [structure scans](../plan_view/pattern_structure_scan_v2.md). +### ROI -## Mission Command List {#mission_command_list} +Toggles Region of Interest mode. When active, clicking on the map sets an ROI location. +This tool is only visible if the vehicle firmware supports ROI mode. -Mission commands for the current mission are listed on the right side of the view. -At the top are a set of options to switch between editing the mission, GeoFence and rally points. -Within the list you can select individual mission items to edit their values. +### Return / Land -![Mission Command List](../../../assets/plan/mission/mission_command_list.jpg) +Adds a return or land command to the mission. The label varies by vehicle type: +- **Multicopters:** _Return_ +- **Fixed-wing:** _Land_ (or _Alt Land_ if a land item already exists in the mission) -### Mission Command Editors {#mission_command_editors} +### Stats -Click on a mission command in the list to display its editor (in which you can set/change the command attributes). +Toggles the [Mission Stats / Terrain panel](#stats_terrain) at the bottom of the map. Only visible when that panel is currently hidden. -You can change the **type** of the command by clicking on the command name (for example: _Waypoint_). -This will display the _Select Mission Command_ dialog shown below. -By default this just displays the "Basic Commands", but you can use the **Category** drop down menu to display more (e.g. choose **All commands** to see all the options). +## File Operations {#file} - +File operations are located in the **Plan Toolbar** at the top of the view: -To the right of each command name is a menu that you can click to access to additional options such as _Insert_ and _Delete_. +- **Open** — Load a plan from a file. +- **Save** — Save the current plan to a file. Highlighted when there are unsaved changes. +- **Upload** — Upload the plan to the vehicle. Highlighted when the plan has un-uploaded changes. +- **Clear** — Remove all mission items, geofence, and rally points. If connected to a vehicle, also clears them from the vehicle. +- **Hamburger menu** (☰) — Additional options: + - _Save as KML_ — Export the plan as a KML file. + - _Download_ — Download the current plan from the vehicle (only available when connected). ::: info -The list of available commands will depend on firmware and vehicle type. -Examples may include: Waypoint, Start image capture, Jump to item (to repeat mission) and other commands. +Before you fly a mission you must upload it to the vehicle. ::: -### Mission Settings {#mission_settings} +## Plan Editor Panel {#plan_editor_panel} -The _Mission Start_ panel is the first item that appears in the [mission command list](#mission_command_list). -It may be used to specify a number default settings that may affect the start or end of the mission. +The Plan Editor Panel is a collapsible tree view on the right side of the view. +The panel can be collapsed or expanded using the toggle button on its left edge. +It is organized into the following collapsible sections: **Plan Info**, **Defaults**, **Mission**, **GeoFence**, **Rally Points**, and **Transform**. -![Mission Command List - showing mission settings](../../../assets/plan/mission_start.png) +### Layer Switcher {#layer_switcher} -![Mission settings](../../../assets/plan/mission/mission_settings.png) +Buttons in the top-right area of the map allow switching between the **Mission**, **Geo-Fence**, and **Rally Point** editing layers. +The active layer button is always visible; the other layer buttons slide in briefly when toggled and auto-hide after a few seconds. -#### Mission Defaults +### Plan Info {#plan_info} -##### Waypoint alt +The Plan Info section contains general plan-level settings: -Set the default altitude for the first mission item added to a plan (subsequent items take an initial altitude from the previous item). -This can also be used to change the altitude of all items in a plan to the same value; you will be prompted if you change the value when there are items in a plan. +- **Plan File** — An editable name for the plan file. +- **Vehicle Info** — Firmware and vehicle type selectors. When connected to a vehicle these are determined automatically; when planning offline you must set them before adding any mission items so that the correct mission commands are available. +- **Expected Home Position** — The altitude (AMSL) for the planned home position is determined automatically from terrain data. A **Move To Map Center** button repositions the home marker to the center of the map. This is only the _planned_ home position for estimating mission times and drawing waypoint lines — the actual home position is set by the vehicle when it arms. -##### Flight speed +### Defaults {#mission_settings} -Set a flight speed for the mission that is different than the default mission speed. +The Defaults section sets plan-wide values that apply to new mission items: -#### Mission End +- **Altitude Frame** — Select the altitude reference frame for waypoints. +- **Waypoints Altitude** — The default altitude for the first mission item added (subsequent items take their initial altitude from the previous item). Changing this value when items already exist will prompt to update all items to the new altitude. +- **Flight Speed** — Set a flight speed that differs from the default mission speed. +- **Vehicle Speeds** — Cruise speed (fixed-wing) and/or hover speed (multi-rotor/VTOL), used for estimating mission time. -##### Return to Launch after mission end +### Mission Items {#mission_items} -Check this if you want your vehicle to Return/RTL after the final mission item. +The Mission section lists all mission items (waypoints, commands, patterns, etc.) in order. +Each item can be expanded to edit its parameters. -#### Planned Home Position - -The [Planned Home Position](#planned_home) section allows you to simulate the vehicle's home position while planning a mission. -This allows you to view the waypoint trajectory for your vehicle from takeoff to mission completion. - -![MissionSettings Planned Home Position Section](../../../assets/plan/mission/mission_settings_planned_home_position_section.jpg) +- Click an item to select it on the map and expand its editor. +- Click the **command name** dropdown to change the item type. A dialog shows "Basic Commands" by default; use the **Category** dropdown to see all available commands. +- Each item has a **hamburger menu** (☰) with options such as _Show all values_, _Move to vehicle position_, _Move to previous item position_, and _Edit position_. +- A **delete button** (trash icon) removes the item. +- Items that are incomplete or missing required values show a **?** status indicator. ::: info -This is only the _planned_ home position and you should place it where you plan to start the vehicle from. -It has no actual impact on flying the mission. -The actual home position of a vehicle is set by the vehicle itself when arming. +The list of available commands depends on firmware and vehicle type. +Examples include: Waypoint, Start image capture, Jump to item (to repeat mission), and other commands. ::: -This section allows you to set the **Altitude** and **Set Home to Map Centre** -(you can move it to another position by dragging it on the map). +#### Initial Camera Settings + +This allows you to set camera options before any mission items take place. +This includes camera actions (take photos by time/distance, start/stop video recording) and gimbal control. + +### GeoFence {#geofence} + +The GeoFence section allows you to define geofence boundaries: -#### Camera +- **Polygon Fences** — Add inclusion or exclusion polygon fences. Each polygon can be toggled between inclusion/exclusion, edited, or deleted. +- **Circular Fences** — Add inclusion or exclusion circular fences with a configurable radius. +- **Breach Return Point** — Optionally set a return point (with altitude) that the vehicle will fly to if it breaches the geofence. -The camera section allows you to specify a camera action to take, control the gimbal and set your camera into photo or video mode. +### Rally Points {#rally_points} -![MissionSettings Camera Section](../../../assets/plan/mission/mission_settings_camera_section.jpg) +The Rally Points section displays and manages rally points. When a rally point is selected, its position fields are shown for editing. -The available camera actions are: +### Transform {#transform} -- No change (continue current action) -- Take photos (time) -- Take photos (distance) -- Stop taking photos -- Start recording video -- Stop recording video +The Transform section provides tools to adjust the entire mission after it has been created: -#### Vehicle Info +- **Offset Mission** — Shift the mission by a specified distance in the East, North, and Up directions. Optionally include takeoff and/or landing items in the offset. +- **Reposition Mission** — Move the mission to a new location specified by geographic coordinates (Lat/Lon), UTM, MGRS, or the current vehicle position. +- **Rotate Mission** — Rotate the mission clockwise by a specified number of degrees. Optionally include takeoff and/or landing items in the rotation. -The appropriate mission commands for the vehicle depend on the firmware and vehicle type. +## Mission Stats / Terrain Panel {#stats_terrain} -If you are planning a mission while you are _connected to a vehicle_ the firmware and vehicle type will be determined from the vehicle. -This section allows you to specify the vehicle firmware/type when not connected to a vehicle. +A panel at the bottom of the map provides two toggleable views: -![MissionSettings VehicleInfoSection](../../../assets/plan/mission/mission_settings_vehicle_info_section.jpg) +- **Terrain Profile** — A chart showing height AMSL vs. distance from start across the mission. +- **Mission Statistics** — Displays details for the selected waypoint (altitude difference, azimuth, distance to previous waypoint, gradient, heading) and for the total mission (distance, max telemetry distance, estimated flight time). Battery information (batteries required and change point) is also shown when available. -The additional value that can be specified when planning a mission is the vehicle flight speed. -By specifying this value, total mission or survey times can be approximated even when not connected to a vehicle. +Toggle buttons on the left side of the panel let you switch between the terrain profile and mission statistics views, or collapse the panel entirely. ## Troubleshooting @@ -190,14 +185,14 @@ If a failure occurs you should see a status message in the QGC UI similar to: > Mission transfer failed. Retry transfer. Error: Mission write mission count failed, maximum retries exceeded. -The loss rate for your link can be viewed in [Settings View > MAVLink](../settings_view/mavlink.md). +The loss rate for your link can be viewed in [App Settings > Telemetry](../settings_view/telemetry.md). The loss rate should be in the low single digits (i.e. maximum of 2 or 3): - A loss rate in the high single digits can lead to intermittent failures. - Higher loss rates often lead to 100% failure. There is a much smaller possibility that issues are caused by bugs in either flight stack or QGC. -To analyse this possibility you can turn on [Console Logging](../settings_view/console_logging.md) for Plan upload/download and review the protocol message traffic. +To analyze this possibility you can turn on [Console Logging](../settings_view/console_logging.md) for Plan upload/download and review the protocol message traffic. ## Further Info diff --git a/docs/en/qgc-user-guide/settings_view/settings_view.md b/docs/en/qgc-user-guide/settings_view/settings_view.md index 78d3006666b3..b8b0c633fbe2 100644 --- a/docs/en/qgc-user-guide/settings_view/settings_view.md +++ b/docs/en/qgc-user-guide/settings_view/settings_view.md @@ -17,7 +17,7 @@ You can switch between the various settings options by clicking the buttons in t **[Offline Maps](offline_maps.md)**
Allows you to cache maps for use while you have no Internet connection. -**[MAVLink](mavlink.md)** +**[Telemetry](telemetry.md)**
Settings associated with the MAVLink connection to a vehicle. **[Console](console_logging.md)** diff --git a/docs/en/qgc-user-guide/settings_view/mavlink.md b/docs/en/qgc-user-guide/settings_view/telemetry.md similarity index 71% rename from docs/en/qgc-user-guide/settings_view/mavlink.md rename to docs/en/qgc-user-guide/settings_view/telemetry.md index 0ba6749f8327..758d9e752bb2 100644 --- a/docs/en/qgc-user-guide/settings_view/mavlink.md +++ b/docs/en/qgc-user-guide/settings_view/telemetry.md @@ -1,12 +1,10 @@ -# MAVLink Settings +# Telemetry Settings -The MAVLink settings (**SettingsView > MAVLink**) allow you to configure options and view information specific to MAVLink communications. +The Telemetry settings (**App Settings > Telemetry**) allow you to configure options and view information specific to MAVLink communications. This includes setting the MAVLink system ID for _QGroundControl_ and viewing link quality. The screen also allows you to manage [MAVLink 2 Log Streaming](#logging) (PX4 only), including _automating log upload to Flight Review_! -![MAVLink settings screen](../../../assets/settings/mavlink/overview.png) - ## Ground Station {#ground_station} This section sets the MAVLink properties and behaviour of _QGroundControl_. @@ -30,6 +28,29 @@ A high **Loss rate** may lead to protocol errors for things like parameter downl ![Link Status](../../../assets/settings/mavlink/link_status.jpg) +## MAVLink 2 Signing {#signing} + +The _MAVLink 2 Signing_ section (under **App Settings > Telemetry**) allows you to manage signing keys used for [MAVLink 2 message signing](https://mavlink.io/en/guide/message_signing.html). +When signing is enabled, all messages between QGroundControl and the vehicle are cryptographically authenticated, preventing unauthorized command injection. + +### Key Management + +- **Add Key:** Enter a friendly name and a passphrase. The passphrase is SHA-256 hashed to produce the 32-byte signing key. Only the hash is stored — the passphrase is not saved. +- **Enable:** Sends the signing key to the active vehicle via `SETUP_SIGNING`. The vehicle and QGroundControl will both begin signing messages. Only available when no key is currently active. +- **Disable:** Disables signing on the active vehicle by sending an all-zero key. Only shown for the currently active key. +- **Delete:** Removes a key from QGroundControl's key store. Not available for keys that are in use by any connected vehicle. A warning is shown because the vehicle may still have the key configured — deleting it locally means you can no longer communicate with that vehicle over a signed connection. + +### Auto-Detection + +When QGroundControl receives signed packets from a vehicle, it automatically tries each stored key to find a match. +If a match is found, signing is automatically configured on the link — no manual action is needed. +The detected key name is shown in the Signing toolbar indicator. + +::: warning +Signing keys should only be sent to the vehicle over secure (wired or encrypted) links. +Anyone who intercepts the key can sign messages and send commands to the vehicle. +::: + ## MAVLink 2 Logging (PX4 only) {#logging} The _MAVLink 2 Logging_ settings (PX4 only) configure real-time log streaming from PX4 to _QGroundControl_ and upload of logs to [Flight Review](https://logs.px4.io). diff --git a/docs/en/qgc-user-guide/troubleshooting/parameter_download.md b/docs/en/qgc-user-guide/troubleshooting/parameter_download.md index 547c9f1ae27e..f70d463b484c 100644 --- a/docs/en/qgc-user-guide/troubleshooting/parameter_download.md +++ b/docs/en/qgc-user-guide/troubleshooting/parameter_download.md @@ -7,9 +7,9 @@ At which point you will get an error stating that QGC was unable to retrieve the Although you can still fly the vehicle in this state it is not recommended. Also the vehicle setup pages will not be available. -You can see the loss rate for your link from the [Settings View > MAVLink](../settings_view/mavlink.md) page. +You can see the loss rate for your link from the [App Settings > Telemetry](../settings_view/telemetry.md) page. Even a loss rate in the high single digits can lead to intermittent failures of the plan protocols. -Higher loss rates could leads to 100% failure. +Higher loss rates could lead to 100% failure. There is also the more remote possibility of either firmware or QGC bugs. To see the details of the back and forth message traffic of the protocol you can turn on [Console Logging](../settings_view/console_logging.md) for the Parameter Protocol. diff --git a/docs/ko/initial-connect-state-machine-analysis.md b/docs/ko/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/ko/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/ko/qgc-dev-guide/command_line_options.md b/docs/ko/qgc-dev-guide/command_line_options.md index 040b0472cc93..6cdf89588a40 100644 --- a/docs/ko/qgc-dev-guide/command_line_options.md +++ b/docs/ko/qgc-dev-guide/command_line_options.md @@ -33,7 +33,7 @@ The options/command line arguments are listed in the table below. | Option | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | | `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | | `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | | `--unittest:name` | (Debug builds only) Runs the specified unit test. Leave off `:name` to run all tests. | diff --git a/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md index d4a3ea9ed6d4..0adbc5390dba 100644 --- a/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/ko/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,38 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -- Telemetry RSSI -- RC RSSI -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- VTOL transitions -- Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/ko/qgc-user-guide/getting_started/download_and_install.md b/docs/ko/qgc-user-guide/getting_started/download_and_install.md index f82f600f1eb3..85ccf71a9099 100644 --- a/docs/ko/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/ko/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` diff --git a/docs/ko/qgc-user-guide/plan_view/plan_view.md b/docs/ko/qgc-user-guide/plan_view/plan_view.md index 49a18d0547db..1d6d6c6b8a3e 100644 --- a/docs/ko/qgc-user-guide/plan_view/plan_view.md +++ b/docs/ko/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _계획 뷰_에서는 차량에 대한 _자율 임무_를 계획하고 차량에 펌웨어에서 지원하는 경우 [지오 펜스](plan_geofence.md)와 [랠리 포인트](plan_rally_points.md)를 설정할 수 있습니다. -![계획 뷰 ](../../../assets/plan/plan_view_overview.jpg) +![계획 뷰 ](../../../assets/plan/plan_view_overview.png) ## UI 개요 {#ui_overview} diff --git a/docs/tr/initial-connect-state-machine-analysis.md b/docs/tr/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/tr/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/tr/qgc-dev-guide/command_line_options.md b/docs/tr/qgc-dev-guide/command_line_options.md index 040b0472cc93..6cdf89588a40 100644 --- a/docs/tr/qgc-dev-guide/command_line_options.md +++ b/docs/tr/qgc-dev-guide/command_line_options.md @@ -33,7 +33,7 @@ The options/command line arguments are listed in the table below. | Option | Description | | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | | `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | | `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | | `--unittest:name` | (Debug builds only) Runs the specified unit test. Leave off `:name` to run all tests. | diff --git a/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md index d4a3ea9ed6d4..0adbc5390dba 100644 --- a/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/tr/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ The "Q" icon on the left of the toolbar allows you to select between additional ## Toolbar Indicators -Next are a multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. +Next are multiple toolbar indicators for vehicle status. The dropdowns for each toolbar indicator provide additional detail on status. You can also expand the indicators to show additional application and vehicle settings associated with the indicator. Press the ">" button to expand. ![Toolbar Indicator - expand button](../../../assets/fly/toolbar_indicator_expand.png) -### Flight Status +### Flight Status Flight Status indicator The Flight Status indicator shows you whether the vehicle is ready to fly or not. It can be in one of the following states: @@ -29,10 +29,10 @@ The Flight Status indicator shows you whether the vehicle is ready to fly or not - **Landing** - Vehicle is in the process of landing. - **Communication Lost** - QGroundControl has lost communication with the vehicle. -The Flight Status Indicator dropdown also gives you acess to: +The Flight Status indicator dropdown also gives you access to: - **Arm** - Arming a vehicle starts the motors in preparation for takeoff. You will only be able to arm the vehicle if it is safe and ready to fly. Generally you do not need to manually arm the vehicle. You can simply takeoff or start a mission and the vehicle will arm itself. -- **Disarm** - Disarming a vehicle is only availble when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. It will stop the motors. Generally you do not need to explicitly disarm as vehicles will disarm automatically after landing, or shortly after arming if you do not take off. - **Emergency Stop** - Emergency stop is used to disarm the vehicle while it is flying. For emergency use only, your vehicle will crash! In the cases of warnings or not ready state you can click the indicator to display the dropdown which will show the reason(s) why. The toggle on the right expands each error with additional information and possible solutions. @@ -41,7 +41,7 @@ In the cases of warnings or not ready state you can click the indicator to displ Once each issue is resolved it will disappear from the UI. When all issues blocking arming have been removed you should now be ready to fly. -## Flight Mode +### Flight Mode Flight Mode indicator The Flight Mode indicator shows you the current flight mode. The dropdown allows you to switch between flight modes. The expanded page allows you to: @@ -49,15 +49,23 @@ The Flight Mode indicator shows you the current flight mode. The dropdown allows - Set global geo-fence settings - Add/Remove flight modes from the displayed list -## Vehicle Messages +### Vehicle Messages Vehicle Messages indicator The Vehicle Messages indicator dropdown shows you messages which come from the vehicle. The indicator will turn red if there are important messages available. -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -The GPS indicator shows you the satellite count and the HDOP in the toolbar icon. The dropdown shows you additional GPS status. The expanded page give you access to RTK settings. +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## Battery +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator The Battery indicator shows you a configurable colored battery icon for remaining charge. It can also be configured to show percent remaining, voltage or both. The expanded page allows you to: @@ -65,14 +73,38 @@ The Battery indicator shows you a configurable colored battery icon for remainin - Configure the icon coloring - Set up the low battery failsafe -## Remote ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## Other Indicators +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -There are other indicators which only show in certain situations: +### APM Support Forwarding APM Support Forwarding indicator -- Telemetry RSSI -- RC RSSI -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- VTOL transitions -- Select from multiple connected vehicles +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/tr/qgc-user-guide/getting_started/download_and_install.md b/docs/tr/qgc-user-guide/getting_started/download_and_install.md index 7ca6f7148394..28289aabc6cc 100644 --- a/docs/tr/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/tr/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` diff --git a/docs/tr/qgc-user-guide/plan_view/plan_view.md b/docs/tr/qgc-user-guide/plan_view/plan_view.md index 77ab88f237cd..4d3a43fae8d3 100644 --- a/docs/tr/qgc-user-guide/plan_view/plan_view.md +++ b/docs/tr/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _Plan View_, aracınız için _ otonom görevler _ planlamak ve onları araca y Ayrıca eğer yazılım tarafından destekleniyorsa [GeoFence](plan_geofence.md) ve [Rally Points](plan_rally_points.md)'leri ayalarmak için kullanılır. -![Plan Ekranı](../../../assets/plan/plan_view_overview.jpg) +![Plan Ekranı](../../../assets/plan/plan_view_overview.png) ## Kullanıcı Arayüzü'ne Genel Bakış {#ui_overview} diff --git a/docs/zh/initial-connect-state-machine-analysis.md b/docs/zh/initial-connect-state-machine-analysis.md new file mode 100644 index 000000000000..bc2e4d216817 --- /dev/null +++ b/docs/zh/initial-connect-state-machine-analysis.md @@ -0,0 +1,374 @@ +# Initial Connect State Machine Analysis + +## Scope + +This document analyzes QGroundControl's vehicle initial connection pipeline implemented in: + +- `src/Vehicle/InitialConnectStateMachine.h` +- `src/Vehicle/InitialConnectStateMachine.cc` + +It also covers upstream/downstream dependencies that influence behavior: + +- `src/Vehicle/Vehicle.cc` +- `src/Vehicle/ComponentInformation/ComponentInformationManager.*` +- `src/Utilities/StateMachine/*` +- `src/FirmwarePlugin/FirmwarePlugin.cc` +- `src/FactSystem/ParameterManager.cc` +- `src/MissionManager/GeoFenceManager.cc` +- `src/MissionManager/RallyPointManager.cc` +- `src/API/QGCOptions.h` + +--- + +## 1. What the Initial Connect State Machine does + +At first online vehicle connection, QGC runs a linear state machine to gather critical vehicle metadata and synchronize mission-related state. + +### State order + +1. **Request AUTOPILOT_VERSION** (`RetryableRequestMessageState`) +2. **Request Standard Modes** (`AsyncFunctionState`) +3. **Request Component Information** (`AsyncFunctionState`) +4. **Request Parameters** (`AsyncFunctionState`) +5. **Request Mission** (`SkippableAsyncState`) +6. **Request Geofence** (`SkippableAsyncState`) +7. **Request Rally Points** (`SkippableAsyncState`) +8. **Signal Complete** (`RetryState` with zero retries) +9. **Final** (`QGCFinalState`) + +The state machine is started from `Vehicle` construction for normal vehicles. + +--- + +## 2. Progress model + +The machine uses weighted progress: + +- AutopilotVersion: 1 +- StandardModes: 1 +- ComponentInfo: 5 +- Parameters: 5 +- Mission: 2 +- GeoFence: 1 +- RallyPoints: 1 +- Complete: 1 + +This means parameter and component-info stages dominate progress reporting (as intended), while mission/fence/rally are lighter-weight contributors. + +Sub-progress is wired from managers during long operations: + +- `ComponentInformationManager::progressUpdate` +- `ParameterManager::loadProgressChanged` +- `MissionManager::progressPctChanged` +- `GeoFenceManager::progressPctChanged` +- `RallyPointManager::progressPctChanged` + +--- + +## 3. Retries/timeouts and failure behavior + +### Per-state retry policy + +`_maxRetries = 1` globally for initial connect states. + +Timeouts: + +- AUTOPILOT_VERSION: 5000 ms +- StandardModes: 5000 ms +- ComponentInfo: 30000 ms +- Parameters: 60000 ms +- Mission: 30000 ms +- GeoFence: 15000 ms +- RallyPoints: 15000 ms + +### Important semantic detail + +For all async states after AUTOPILOT_VERSION, timeout handling uses `RetryTransition`: + +- On first timeout: retry in-place (`restartWait()` + retry action), no state transition. +- After retries exhausted: transition to the next state. + +So timeout exhaustion is **graceful degradation** (advance), not hard stop, for this machine. + +### AUTOPILOT_VERSION state behavior + +AUTOPILOT_VERSION uses `RetryableRequestMessageState`, which also retries and then advances by default when retries are exhausted (`failOnMaxRetries` is not enabled here). It invokes a failure handler before advancing. + +--- + +## 4. Exactly what is skipped, and when + +## A) Entire machine skipped + +In unit tests, the machine is **not started** only for synthetic generic vehicles: + +- Condition: `qgcApp()->runningUnitTests() && _vehicleType == MAV_TYPE_GENERIC` + +All real vehicle types still run initial connect during tests. + +## B) AUTOPILOT_VERSION request skipped + +State skip predicate returns true if: + +1. No primary link exists, **or** +2. Primary link is high-latency (`LinkConfiguration::isHighLatency()`), **or** +3. Primary link is log replay (`LinkInterface::isLogReplay()` true, e.g. `LogReplayLink`) + +When skipped, the state completes immediately and transitions to StandardModes. + +## C) Mission load skipped + +Mission state is skipped if: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link + +## D) Geofence load skipped + +Geofence state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `GeoFenceManager::supported()` is false + - `supported()` is capability-driven: requires `MAV_PROTOCOL_CAPABILITY_MISSION_FENCE` + +## E) Rally points load skipped + +Rally state is skipped if any of: + +1. Link type should be skipped (`high latency` or `log replay`), **or** +2. No primary link, **or** +3. `RallyPointManager::supported()` is false + - `supported()` requires `MAV_PROTOCOL_CAPABILITY_MISSION_RALLY` + +When rally is skipped, QGC still marks: + +- `_initialPlanRequestComplete = true` +- emits `initialPlanRequestCompleteChanged(true)` + +so downstream UI logic does not stall waiting for rally load. + +## F) Component information sub-states skipped (inside ComponentInformationManager) + +`ComponentInformationManager` itself has internal skippable phases: + +- Parameter metadata skipped if `GENERAL` metadata says parameter metadata unsupported +- Events metadata skipped if unsupported +- Actuators metadata skipped if unsupported + +Support checks are based on `CompInfoGeneral::isMetaDataTypeSupported(type)`. + +--- + +## 5. What data is captured during initial connect, and why + +## A) AUTOPILOT_VERSION data captured + +On successful AUTOPILOT_VERSION response, QGC captures: + +1. **Vehicle UID** (`uid`) + - Used as stable identity and surfaced via `vehicleUIDChanged`. + +2. **Firmware board vendor/product IDs** + - Hardware identification for board-specific behavior and diagnostics. + +3. **Flight software semantic version** (`flight_sw_version` packed fields) + - Parsed into major/minor/patch/version-type and stored in `Vehicle`. + +4. **Firmware custom version / git hash** + - PX4: decoded from binary bytes (reverse-order formatting) + - APM: decoded as 8-char ASCII + - Exposed via `gitHashChanged`. + +5. **MAVLink capability bits** + - Stored via `_setCapabilities(...)` + - Drives feature gating such as mission-int, command-int, geofence, rally support. + +If AUTOPILOT_VERSION fails, QGC sets **assumed capabilities**: + +- Always assume MAVLink2 +- For PX4/APM, also assume mission-int/command-int/fence/rally support + +This allows connect flow to continue even without that message. + +## B) Standard modes captured + +QGC requests available standard modes one-by-one (`AVAILABLE_MODES`) and updates firmware plugin mode mapping. + +Purpose: + +- Provide accurate, vehicle-reported mode names/availability. +- Refresh flight mode UI mapping even if HEARTBEAT arrives earlier. + +## C) Component Information captured + +`ComponentInformationManager` requests metadata in this sequence: + +1. `GENERAL` metadata +2. URI update pass (maps/propagates discovered metadata URIs) +3. `PARAMETER` metadata (if supported) +4. `EVENTS` metadata (if supported) +5. `ACTUATORS` metadata (if supported) + +### Why this matters + +- **Parameter metadata** (`CompInfoParam`) feeds `ParameterManager` metadata lookups (`factMetaDataForName`), affecting parameter typing/metadata behavior. +- **Events metadata** supports event schema/decoding ecosystem. +- **Actuators metadata** is used by PX4 actuator configuration UX: + - If actuator metadata is absent or actuator conditions fail, PX4 plugin falls back to legacy Motor page. + - If present and valid (`show-ui-if` etc.), Actuators page is enabled. + +So component info is both a data-quality improvement and a feature-availability gate. + +## D) Parameters stage captured + +- Requests full parameter refresh (`refreshAllParameters()`) +- Waits for `parametersReadyChanged(true)` +- On ready: + - Sends GCS time to vehicle twice (reliability) + - Sets up auto-disarm signalling + +This stage is foundational for most setup/calibration UI and many capability/behavior branches. + +## E) Plan data captured + +- Mission items from vehicle +- Geofence data (if supported and not skipped) +- Rally points (if supported and not skipped) + +On rally completion (or rally skip), QGC sets initial plan load complete. + +--- + +## 6. Where "check latest stable firmware" fits + +The latest-stable firmware check is triggered as a **side-effect** of AUTOPILOT_VERSION success: + +- Condition: `QGCOptions::checkFirmwareVersion()` is true and `_checkLatestStableFWDone` is false. +- Action: `FirmwarePlugin::checkIfIsLatestStable(vehicle)`. + +This is asynchronous (downloads and parses a version file), and **does not gate** state machine transitions. Initial connect continues regardless of result. + +It is skipped during unit tests in `FirmwarePlugin::checkIfIsLatestStable`. + +--- + +## 7. Options/configurations that affect behavior + +1. **Link configuration high-latency flag** + - `LinkConfiguration.highLatency` + - Causes skipping of AUTOPILOT_VERSION and plan-load states. + +2. **Log replay link type** + - `LinkInterface::isLogReplay()` true + - Same skip effect as high-latency. + +3. **Firmware/version check option** + - `QGCOptions::checkFirmwareVersion()` (virtual, custom-build overridable) + - Controls only latest-stable check side-effect, not transition graph. + +4. **Capability bits** + - From AUTOPILOT_VERSION or assumed fallback. + - Gate geofence/rally support and therefore skip decisions. + +5. **Metadata support flags from CompInfoGeneral** + - Gate internal component-info sub-requests (param/events/actuators metadata). + +--- + +## 8. Practical implications + +- The machine is designed for **robust connect**, not strict fail-fast. +- Network/link quality and link type strongly influence which expensive operations are skipped. +- Component information is a key bridge between firmware-reported metadata and dynamic UI behavior (especially parameters and actuators). +- Missing AUTOPILOT_VERSION does not block connect; QGC degrades gracefully with assumptions. + +--- + +## 9. Quick skip matrix + +| State | Skip trigger(s) | Outcome | +| -------------------------------------- | --------------------------------------------- | ------------------------------------------------------------ | +| AUTOPILOT_VERSION | no primary link OR high-latency OR log replay | immediate complete/advance | +| StandardModes | none explicit | retry-on-timeout then advance | +| ComponentInfo | none explicit | retry-on-timeout then advance | +| Parameters | none explicit | retry-on-timeout then advance | +| Mission | high-latency OR log replay OR no primary link | skipped -> GeoFence | +| GeoFence | mission skip conditions OR !fence capability | skipped -> Rally | +| Rally | mission skip conditions OR !rally capability | skipped -> Complete (+mark plan complete) | +| Complete | never skipped | emits `initialConnectComplete` at machine finish | + +### Run matrix by (HighLatency, LogReplay, Flying) + +Assumptions for this matrix: + +- Primary link exists +- Geofence and rally are supported by capability bits +- State machine is started (not the `runningUnitTests && MAV_TYPE_GENERIC` bypass) + +Legend: + +- `Run` = state logic executes (it may still complete quickly) +- `Skip` = state is skipped by state skip predicate +- `Run*` = state executes but parameter download is short-circuited inside `ParameterManager` for high-latency/log-replay links + +| State | HL=0 LR=0 Fly=0 | HL=0 LR=0 Fly=1 | HL=1 LR=0 Fly=0 | HL=1 LR=0 Fly=1 | HL=0 LR=1 Fly=0 | HL=0 LR=1 Fly=1 | HL=1 LR=1 Fly=0 | HL=1 LR=1 Fly=1 | +| -------------------------------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | --------------- | +| AUTOPILOT_VERSION | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| StandardModes | Run | Run | Run | Run | Run | Run | Run | Run | +| ComponentInfo | Run | Run | Run | Run | Run | Run | Run | Run | +| Parameters | Run | Run | Run\* | Run\* | Run\* | Run\* | Run\* | Run\* | +| Mission | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| GeoFence | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Rally | Run | Run | Skip | Skip | Skip | Skip | Skip | Skip | +| Complete | Run | Run | Run | Run | Run | Run | Run | Run | + +`Flying` does not currently participate in InitialConnectStateMachine skip predicates. It does not change run/skip behavior in this machine. + +### Unit-test readable matrix (request expectations) + +This is the same compact matrix used by `_stateRunMatrix_data` in `InitialConnectTest`. + +```text ++----+----+-----+------------+------------+----------------+ +| HL | LR | Fly | AP_VERSION | AVAIL_MODS | PLAN_REQ_LISTS | ++----+----+-----+------------+------------+----------------+ +| 0 | 0 | 0 | Run | Run | Run | +| 0 | 0 | 1 | Run | Run | Run | +| 1 | 0 | 0 | Skip | Run | Skip | +| 1 | 0 | 1 | Skip | Run | Skip | +| 0 | 1 | 0 | Skip | Run | Skip | +| 0 | 1 | 1 | Skip | Run | Skip | +| 1 | 1 | 0 | Skip | Run | Skip | +| 1 | 1 | 1 | Skip | Run | Skip | ++----+----+-----+------------+------------+----------------+ +``` + +Where: + +- `HL` = HighLatency +- `LR` = LogReplay +- `Fly` = Flying +- `AP_VERSION` = `AUTOPILOT_VERSION` request expected +- `AVAIL_MODS` = `AVAILABLE_MODES` request expected +- `PLAN_REQ_LISTS` = mission/geofence/rally `MISSION_REQUEST_LIST` traffic expected + +Skip behavior basis: `skipForLinkType = (highLatency || logReplay)`. + +--- + +## 10. Notes on observability/debugging + +Useful log categories for tracing behavior: + +- `Vehicle.InitialConnectStateMachine` +- `ComponentInformation.ComponentInformationManager` +- `Vehicle.StandardModes` +- `Vehicle` (capability logs) +- `ActuatorsConfig` (for metadata-driven actuator UI decisions) + +This combination lets you determine: + +- Which states were entered/skipped/timed out +- Which metadata types were supported/downloaded +- Why actuator UI appeared or fell back to legacy motor UI diff --git a/docs/zh/qgc-dev-guide/command_line_options.md b/docs/zh/qgc-dev-guide/command_line_options.md index dd4bb55d774a..02cf2ea7862c 100644 --- a/docs/zh/qgc-dev-guide/command_line_options.md +++ b/docs/zh/qgc-dev-guide/command_line_options.md @@ -30,16 +30,16 @@ Linux终端: 选项/命令行参数列在下表中。 -| 选项 | 描述 | -| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | -| `--logging:full` | Turns on full logging. See [Console Logging](../../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | -| `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | -| `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | -| `--unittest:name` | (仅限调试版本)连续运行指定的单元测试次。 Leave off `:name` to run all tests. | -| `--unittest-stress:name` | (仅限调试版本)连续运行指定的单元测试20次。 Leave off :name to run all tests. | -| `--fake-mobile` | Simulates running on a mobile device. | -| `--test-high-dpi` | Simulates running _QGroundControl_ on a high DPI device. | +| 选项 | 描述 | +| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--clear-settings` | Clears the app settings (reverts _QGroundControl_ back to default settings). | +| `--logging:full` | Turns on full logging. See [Console Logging](../qgc-user-guide/settings_view/console_logging.md#logging-from-the-command-line). | +| `--logging:full,LinkManagerVerboseLog,ParameterLoaderLog` | Turns on full logging and turns off the following listed comma-separated logging options. | +| `--logging:LinkManagerLog,ParameterLoaderLog` | Turns on the specified comma separated logging options | +| `--unittest:name` | (仅限调试版本)连续运行指定的单元测试次。 Leave off `:name` to run all tests. | +| `--unittest-stress:name` | (仅限调试版本)连续运行指定的单元测试20次。 Leave off :name to run all tests. | +| `--fake-mobile` | Simulates running on a mobile device. | +| `--test-high-dpi` | Simulates running _QGroundControl_ on a high DPI device. | 笔记: diff --git a/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md b/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md index ba1854dcfc8a..9d3009f2d1ba 100644 --- a/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md +++ b/docs/zh/qgc-user-guide/fly_view/fly_view_toolbar.md @@ -13,11 +13,11 @@ ## 工具栏指示器 -接下来是关于车辆状况的多个工具栏指标。 每个工具栏指示器的下拉功能提供了关于状态的更多细节。 您也可以展开指示器以显示与指示器相关联的其他应用程序和载具设置。 按">"按钮来扩展。 +Next are multiple toolbar indicators for vehicle status. 每个工具栏指示器的下拉功能提供了关于状态的更多细节。 您也可以展开指示器以显示与指示器相关联的其他应用程序和载具设置。 按">"按钮来扩展。 ![工具栏指示器 - 展开按钮](../../../assets/fly/toolbar_indicator_expand.png) -### 飞行状态 +### Flight Status Flight Status indicator 飞行状态指示器显示载具是否可以飞行。 它可以是下列状态之一: @@ -29,10 +29,10 @@ - **着陆** - 载具正在着陆。 - **通信丢失** - QGroundControl已失去与载具的通信。 -飞行状态指示器下拉也让您可以: +The Flight Status indicator dropdown also gives you access to: - **解锁** - 解锁一辆载具开启发动机以准备起飞。 你只有在载具安全和准备飞行时才能解锁载具。 通常你不需要手动解锁载具。 你可以简单地起飞或开始执行任务,载具将解锁自己。 -- **锁定** - 只有当载具在地面时才能锁定载具。 它会关停电机。 一般来说,你无需明确进行锁定操作,因为飞行器会在着陆后自动锁定,或者如果在解锁后若未起飞,不久后也会自动锁定。 +- **Disarm** - Disarming a vehicle is only available when the vehicle is on the ground. 它会关停电机。 一般来说,你无需明确进行锁定操作,因为飞行器会在着陆后自动锁定,或者如果在解锁后若未起飞,不久后也会自动锁定。 - **紧急停机** - 在载具飞行时用紧急停机锁定载具。 仅供紧急情况使用,你的飞行器会坠毁! 在警告或尚未准备好状态的情况下,您可以点击指示器来显示下拉菜单,显示原因(s)。 右侧的切换按钮会展开每个错误,并显示更多信息及可能的解决方案。 @@ -41,7 +41,7 @@ 每个问题解决后,将从用户界面中消失。 当所有阻止解锁的问题被移除时,你现在应该可以准备飞行了。 -## 飞行模式 +### Flight Mode Flight Mode indicator 飞行模式指示器显示您当前的飞行模式。 下拉菜单允许您在飞行模式之间切换。 扩展页面允许您: @@ -49,15 +49,23 @@ - 设置全局地理栅栏设置 - 从显示列表中添加/删除飞行模式 -## 载具信息 +### Vehicle Messages Vehicle Messages indicator 车辆消息指示器下拉显示来自车辆的消息。 如果有重要信息,指示器将会变红。 -## GPS +### GPS / RTK GPS GPS / RTK GPS indicator -GPS指示器在工具栏图标中向您显示卫星计数和HDOP。 下拉菜单显示您额外的 GPS 状态。 扩展页面允许您访问RTK设置。 +The GPS/RTK GPS indicator shows satellite and GNSS status in the toolbar, and the dropdown provides additional GPS details. -## 电池 +With an active vehicle, the indicator shows vehicle GPS information (for example, satellite count and HDOP), and the expanded page provides access to RTK-related settings. + +When there is no active vehicle but RTK is connected, the indicator switches to RTK status so you can still monitor the correction link. + +### GPS Resilience + +The GPS Resilience indicator appears when the vehicle reports GPS resilience telemetry (authentication, spoofing, or jamming state). The dropdown provides summary status and per-GPS details when available. + +### Battery Battery indicator 电池指示器向您展示了一个可配置的充电图标。 它也可以配置为显示剩余的电压或两种电压。 扩展页面允许您: @@ -65,14 +73,38 @@ GPS指示器在工具栏图标中向您显示卫星计数和HDOP。 下拉菜单 - 配置图标着色 - 设置低电量故障安全 -## 远程 ID +### Remote ID Remote ID indicator + +The Remote ID indicator appears when Remote ID is available on the active vehicle. Its color indicates overall Remote ID health, and the dropdown shows Remote ID status details. + +### ESC ESC indicator + +The ESC indicator appears when ESC telemetry is available from the vehicle. It shows overall ESC health and online motor count, and opens a detailed ESC status page. + +### Joystick Joystick indicator + +The Joystick indicator appears when a joystick/gamepad is detected. The dropdown shows device status and connection details. + +### Telemetry RSSI Telemetry RSSI indicator + +The Telemetry RSSI indicator appears when telemetry signal information is available. It provides local/remote RSSI and additional radio link quality details. + +### RC RSSI RC RSSI indicator + +The RC RSSI indicator appears when RC signal information is available. It shows current RC link strength and opens a page with RC RSSI details. + +### Gimbal Gimbal indicator + +The Gimbal indicator is shown when the vehicle supports the [MAVLink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html). It displays active gimbal status and provides access to gimbal controls and settings. + +### VTOL Transitions VTOL indicator + +For VTOL vehicles, a VTOL transition status indicator is shown when applicable. It indicates the current VTOL mode/state and provides transition-related status information. + +### Multi-Vehicle Selector Multi-Vehicle indicator -## 其他指标 +The Multi-Vehicle selector appears when more than one vehicle is connected. It allows you to quickly switch the active vehicle from the toolbar. -还有其他指标只能在某些情况下显示: +### APM Support Forwarding APM Support Forwarding indicator -- 遥测信号强度指示 -- 遥控信号强度指示 -- Gimbal - Only displayed if the vehicle supports the [Mavlink Gimbal Protocol](https://mavlink.io/en/services/gimbal_v2.html) -- 垂直起降转换 -- 从多个连接的载具中选择 +On ArduPilot, an APM Support Forwarding indicator appears when MAVLink traffic forwarding to a support server is enabled. diff --git a/docs/zh/qgc-user-guide/getting_started/download_and_install.md b/docs/zh/qgc-user-guide/getting_started/download_and_install.md index 392a49da155b..057bfb4e2751 100644 --- a/docs/zh/qgc-user-guide/getting_started/download_and_install.md +++ b/docs/zh/qgc-user-guide/getting_started/download_and_install.md @@ -78,6 +78,7 @@ sudo apt remove --purge modemmanager ```sh sudo apt install gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-gl -y +sudo apt install python3-gi python3-gst-1.0 -y sudo apt install libfuse2 -y sudo apt install libxcb-xinerama0 libxkbcommon-x11-0 libxcb-cursor-dev -y ``` diff --git a/docs/zh/qgc-user-guide/plan_view/plan_view.md b/docs/zh/qgc-user-guide/plan_view/plan_view.md index a2f88ea5e35d..c9813fd5c341 100644 --- a/docs/zh/qgc-user-guide/plan_view/plan_view.md +++ b/docs/zh/qgc-user-guide/plan_view/plan_view.md @@ -5,7 +5,7 @@ _计划视图_ 用于为你的载具规 划自动化任务_ 并上传到载具 如果固件支持,它也可以用来配置 [地理围栏](plan_geofence.md) 和 [集结点](plan_rally_points.md)。 -![计划视图](../../../assets/plan/plan_view_overview.jpg) +![计划视图](../../../assets/plan/plan_view_overview.png) ## 界面概述{#ui_overview} diff --git a/justfile b/justfile index 61c011cf0230..d18c28cd9e04 100644 --- a/justfile +++ b/justfile @@ -2,7 +2,7 @@ # Install: cargo install just, brew install just, or apt install just # Configuration from build-config.json -qt_version := `./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1"` +qt_version := `python3 ./tools/setup/read_config.py --get qt_version 2>/dev/null || echo "6.10.1"` qt_dir := env_var_or_default("QT_DIR", home_directory() / "Qt" / qt_version / "gcc_64") build_type := env_var_or_default("BUILD_TYPE", "Debug") build_dir := "build" @@ -81,7 +81,7 @@ analyze: # Generate coverage report coverage: - ./tools/coverage.sh + python3 ./tools/coverage.py # Run lint + test check: lint test @@ -92,7 +92,7 @@ check: lint test # Launch QGroundControl run: - ./{{build_dir}}/staging/QGroundControl + ./{{build_dir}}/{{build_type}}/QGroundControl # Build documentation docs: @@ -115,7 +115,7 @@ info: # Check dependency versions check-deps: - ./tools/check-deps.sh + python3 ./tools/check_deps.py # Clean all caches and build artifacts distclean: clean diff --git a/package-lock.json b/package-lock.json index 85045011f70a..8f4b89ba6203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -721,247 +721,300 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.4.tgz", - "integrity": "sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.4.tgz", - "integrity": "sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.4.tgz", - "integrity": "sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.4.tgz", - "integrity": "sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.4.tgz", - "integrity": "sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.4.tgz", - "integrity": "sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.4.tgz", - "integrity": "sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.4.tgz", - "integrity": "sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz", - "integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.4.tgz", - "integrity": "sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.4.tgz", - "integrity": "sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.4.tgz", - "integrity": "sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.4.tgz", - "integrity": "sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.4.tgz", - "integrity": "sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz", - "integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.4.tgz", - "integrity": "sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.4.tgz", - "integrity": "sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.4.tgz", - "integrity": "sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], - "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.4.tgz", - "integrity": "sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -1047,10 +1100,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "license": "MIT" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "node_modules/@types/hast": { "version": "3.0.4", @@ -1878,12 +1930,11 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.4.tgz", - "integrity": "sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==", - "license": "MIT", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -1893,25 +1944,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.4", - "@rollup/rollup-android-arm64": "4.34.4", - "@rollup/rollup-darwin-arm64": "4.34.4", - "@rollup/rollup-darwin-x64": "4.34.4", - "@rollup/rollup-freebsd-arm64": "4.34.4", - "@rollup/rollup-freebsd-x64": "4.34.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.4", - "@rollup/rollup-linux-arm-musleabihf": "4.34.4", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-arm64-musl": "4.34.4", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.4", - "@rollup/rollup-linux-riscv64-gnu": "4.34.4", - "@rollup/rollup-linux-s390x-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-musl": "4.34.4", - "@rollup/rollup-win32-arm64-msvc": "4.34.4", - "@rollup/rollup-win32-ia32-msvc": "4.34.4", - "@rollup/rollup-win32-x64-msvc": "4.34.4", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, @@ -2603,117 +2660,153 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "@rollup/rollup-android-arm-eabi": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.4.tgz", - "integrity": "sha512-gGi5adZWvjtJU7Axs//CWaQbQd/vGy8KGcnEaCWiyCqxWYDxwIlAHFuSe6Guoxtd0SRvSfVTDMPd5H+4KE2kKA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.4.tgz", - "integrity": "sha512-1aRlh1gqtF7vNPMnlf1vJKk72Yshw5zknR/ZAVh7zycRAGF2XBMVDAHmFQz/Zws5k++nux3LOq/Ejj1WrDR6xg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.4.tgz", - "integrity": "sha512-drHl+4qhFj+PV/jrQ78p9ch6A0MfNVZScl/nBps5a7u01aGf/GuBRrHnRegA9bP222CBDfjYbFdjkIJ/FurvSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.4.tgz", - "integrity": "sha512-hQqq/8QALU6t1+fbNmm6dwYsa0PDD4L5r3TpHx9dNl+aSEMnIksHZkSO3AVH+hBMvZhpumIGrTFj8XCOGuIXjw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "optional": true }, "@rollup/rollup-freebsd-arm64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.4.tgz", - "integrity": "sha512-/L0LixBmbefkec1JTeAQJP0ETzGjFtNml2gpQXA8rpLo7Md+iXQzo9kwEgzyat5Q+OG/C//2B9Fx52UxsOXbzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "optional": true }, "@rollup/rollup-freebsd-x64": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.4.tgz", - "integrity": "sha512-6Rk3PLRK+b8L/M6m/x6Mfj60LhAUcLJ34oPaxufA+CfqkUrDoUPQYFdRrhqyOvtOKXLJZJwxlOLbQjNYQcRQfw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.4.tgz", - "integrity": "sha512-kmT3x0IPRuXY/tNoABp2nDvI9EvdiS2JZsd4I9yOcLCCViKsP0gB38mVHOhluzx+SSVnM1KNn9k6osyXZhLoCA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.4.tgz", - "integrity": "sha512-3iSA9tx+4PZcJH/Wnwsvx/BY4qHpit/u2YoZoXugWVfc36/4mRkgGEoRbRV7nzNBSCOgbWMeuQ27IQWgJ7tRzw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.4.tgz", - "integrity": "sha512-7CwSJW+sEhM9sESEk+pEREF2JL0BmyCro8UyTq0Kyh0nu1v0QPNY3yfLPFKChzVoUmaKj8zbdgBxUhBRR+xGxg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.4.tgz", - "integrity": "sha512-GZdafB41/4s12j8Ss2izofjeFXRAAM7sHCb+S4JsI9vaONX/zQ8cXd87B9MRU/igGAJkKvmFmJJBeeT9jJ5Cbw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", "optional": true }, - "@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.4.tgz", - "integrity": "sha512-uuphLuw1X6ur11675c2twC6YxbzyLSpWggvdawTUamlsoUv81aAXRMPBC1uvQllnBGls0Qt5Siw8reSIBnbdqQ==", + "@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "optional": true }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.4.tgz", - "integrity": "sha512-KvLEw1os2gSmD6k6QPCQMm2T9P2GYvsMZMRpMz78QpSoEevHbV/KOUbI/46/JRalhtSAYZBYLAnT9YE4i/l4vg==", + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.4.tgz", - "integrity": "sha512-wcpCLHGM9yv+3Dql/CI4zrY2mpQ4WFergD3c9cpRowltEh5I84pRT/EuHZsG0In4eBPPYthXnuR++HrFkeqwkA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.4.tgz", - "integrity": "sha512-nLbfQp2lbJYU8obhRQusXKbuiqm4jSJteLwfjnunDT5ugBKdxqw1X9KWwk8xp1OMC6P5d0WbzxzhWoznuVK6XA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.4.tgz", - "integrity": "sha512-JGejzEfVzqc/XNiCKZj14eb6s5w8DdWlnQ5tWUbs99kkdvfq9btxxVX97AaxiUX7xJTKFA0LwoS0KU8C2faZRg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.4.tgz", - "integrity": "sha512-/iFIbhzeyZZy49ozAWJ1ZR2KW6ZdYUbQXLT4O5n1cRZRoTpwExnHLjlurDXXPKEGxiAg0ujaR9JDYKljpr2fDg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.4.tgz", - "integrity": "sha512-qORc3UzoD5UUTneiP2Afg5n5Ti1GAW9Gp5vHPxzvAFFA3FBaum9WqGvYXGf+c7beFdOKNos31/41PRMUwh1tpA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.4.tgz", - "integrity": "sha512-5g7E2PHNK2uvoD5bASBD9aelm44nf1w4I5FEI7MPHLWcCSrR8JragXZWgKPXk5i2FU3JFfa6CGZLw2RrGBHs2Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.4.tgz", - "integrity": "sha512-p0scwGkR4kZ242xLPBuhSckrJ734frz6v9xZzD+kHVYRAkSUmdSLCIJRfql6H5//aF8Q10K+i7q8DiPfZp0b7A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "optional": true }, "@shikijs/core": { @@ -2788,9 +2881,9 @@ "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==" }, "@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "@types/hast": { "version": "3.0.4", @@ -3316,30 +3409,36 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "rollup": { - "version": "4.34.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.4.tgz", - "integrity": "sha512-spF66xoyD7rz3o08sHP7wogp1gZ6itSq22SGa/IZTcUDXDlOyrShwMwkVSB+BUxFRZZCUYqdb3KWDEOMVQZxuw==", - "requires": { - "@rollup/rollup-android-arm-eabi": "4.34.4", - "@rollup/rollup-android-arm64": "4.34.4", - "@rollup/rollup-darwin-arm64": "4.34.4", - "@rollup/rollup-darwin-x64": "4.34.4", - "@rollup/rollup-freebsd-arm64": "4.34.4", - "@rollup/rollup-freebsd-x64": "4.34.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.4", - "@rollup/rollup-linux-arm-musleabihf": "4.34.4", - "@rollup/rollup-linux-arm64-gnu": "4.34.4", - "@rollup/rollup-linux-arm64-musl": "4.34.4", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.4", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.4", - "@rollup/rollup-linux-riscv64-gnu": "4.34.4", - "@rollup/rollup-linux-s390x-gnu": "4.34.4", - "@rollup/rollup-linux-x64-gnu": "4.34.4", - "@rollup/rollup-linux-x64-musl": "4.34.4", - "@rollup/rollup-win32-arm64-msvc": "4.34.4", - "@rollup/rollup-win32-ia32-msvc": "4.34.4", - "@rollup/rollup-win32-x64-msvc": "4.34.4", - "@types/estree": "1.0.6", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "requires": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@types/estree": "1.0.8", "fsevents": "~2.3.2" } }, diff --git a/pyrightconfig.json b/pyrightconfig.json index fa8fa6295b4d..f59621a51177 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -6,9 +6,10 @@ ], "include": [ "tools/**/*.py", - "test/**/*.py" + "test/**/*.py", + ".github/scripts/**/*.py" ], - "pythonVersion": "3.10", + "pythonVersion": "3.12", "reportMissingImports": "warning", "reportMissingTypeStubs": false, "reportUnusedImport": "warning", diff --git a/qgcimages.qrc b/qgcimages.qrc index 6e6547bedd9a..cb92fc65167f 100644 --- a/qgcimages.qrc +++ b/qgcimages.qrc @@ -115,7 +115,7 @@ src/AutoPilotPlugins/PX4/Images/LandMode.svg src/AutoPilotPlugins/PX4/Images/LandModeCopter.svg src/AutoPilotPlugins/APM/Images/LightsComponentIcon.png - src/AnalyzeView/LogDownloadIcon.svg + src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg src/AutoPilotPlugins/PX4/Images/LowBattery.svg src/AutoPilotPlugins/PX4/Images/LowBatteryLight.svg src/FlightMap/Images/MapAddMission.svg @@ -130,8 +130,8 @@ src/FlightMap/Images/MapSyncChanged.svg src/FlightMap/Images/MapType.svg src/FlightMap/Images/MapTypeBlack.svg - src/AnalyzeView/MAVLinkConsoleIcon.svg - src/AnalyzeView/MAVLinkInspector.svg + src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg + src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg src/UI/toolbar/Images/Megaphone.svg src/AutoPilotPlugins/Common/Images/MotorComponentIcon.svg src/AutoPilotPlugins/PX4/Images/no-logging-light.svg @@ -212,7 +212,7 @@ src/AutoPilotPlugins/PX4/Images/VehicleTailDownRotate.png src/AutoPilotPlugins/PX4/Images/VehicleUpsideDown.png src/AutoPilotPlugins/PX4/Images/VehicleUpsideDownRotate.png - src/AnalyzeView/VibrationPageIcon.png + src/AnalyzeView/Vibration/VibrationPageIcon.png src/AutoPilotPlugins/Common/Images/wifi.svg src/UI/toolbar/Images/Yield.svg src/FlightMap/Images/ZoomMinus.svg diff --git a/qgcresources.qrc b/qgcresources.qrc index 400c52a8c067..1a9a8de2352d 100644 --- a/qgcresources.qrc +++ b/qgcresources.qrc @@ -28,6 +28,7 @@ resources/gear-black.svg resources/gear-white.svg resources/GearWithPaperPlane.svg + resources/GeoFence.svg resources/helicoptericon.svg resources/JoystickBezel.png resources/JoystickBezelLight.png @@ -52,6 +53,7 @@ resources/QGCLogoWhite.svg resources/QGCLogoArrow.svg resources/QGroundControlConnect.svg + resources/RallyPoint.svg resources/rtl.svg resources/sliders.svg resources/SaveToDisk.svg @@ -101,11 +103,4 @@ resources/gcscontrolIndicator/gcscontrol_gcs.svg resources/gcscontrolIndicator/gcscontrol_line.svg - - src/UI/AppSettings/NTRIPSettings.qml - src/UI/AppSettings/NTRIPSettings.qml - - - src/Settings/NTRIP.SettingsGroup.json - diff --git a/resources/GeoFence.svg b/resources/GeoFence.svg new file mode 100644 index 000000000000..5a61004cfdaf --- /dev/null +++ b/resources/GeoFence.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/RallyPoint.svg b/resources/RallyPoint.svg new file mode 100644 index 000000000000..28d2870297cd --- /dev/null +++ b/resources/RallyPoint.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ruff.toml b/ruff.toml index c417efa12b07..4a218d352410 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ # Ruff configuration for QGroundControl Python tools # https://docs.astral.sh/ruff/ -target-version = "py310" +target-version = "py312" line-length = 100 # Exclude build artifacts and external dependencies @@ -42,5 +42,5 @@ known-first-party = ["qgc"] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false -line-ending = "auto" +line-ending = "lf" docstring-code-format = true diff --git a/src/API/QGCCorePlugin.cc b/src/API/QGCCorePlugin.cc index 7b0883387895..90347d290b48 100644 --- a/src/API/QGCCorePlugin.cc +++ b/src/API/QGCCorePlugin.cc @@ -63,26 +63,24 @@ const QVariantList &QGCCorePlugin::analyzePages() { static const QVariantList analyzeList = { QVariant::fromValue(new QmlComponentInfo( - tr("Log Download"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/LogDownloadPage.qml")), - QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/LogDownloadIcon.svg")))), + tr("Onboard Logs"), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/OnboardLogs/OnboardLogPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/OnboardLogIcon.svg")))), QVariant::fromValue(new QmlComponentInfo( tr("GeoTag Images"), QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/GeoTag/GeoTagPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/GeoTag/GeoTagIcon.svg")))), QVariant::fromValue(new QmlComponentInfo( tr("MAVLink Console"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkConsolePage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkConsoleIcon.svg")))), -#ifndef QGC_DISABLE_MAVLINK_INSPECTOR QVariant::fromValue(new QmlComponentInfo( tr("MAVLink Inspector"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspectorPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/MAVLinkInspector.svg")))), -#endif QVariant::fromValue(new QmlComponentInfo( tr("Vibration"), - QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/VibrationPage.qml")), + QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AnalyzeView/Vibration/VibrationPage.qml")), QUrl::fromUserInput(QStringLiteral("qrc:/qmlimages/VibrationPageIcon")))), }; diff --git a/src/QmlControls/AnalyzePage.qml b/src/AnalyzeView/AnalyzePage.qml similarity index 91% rename from src/QmlControls/AnalyzePage.qml rename to src/AnalyzeView/AnalyzePage.qml index 7f1bdf30ec0d..92fd24873019 100644 --- a/src/QmlControls/AnalyzePage.qml +++ b/src/AnalyzeView/AnalyzePage.qml @@ -10,7 +10,6 @@ Item { anchors.margins: ScreenTools.defaultFontPixelWidth property alias pageComponent: pageLoader.sourceComponent - property alias pageName: pageNameLabel.text property alias pageDescription: pageDescriptionLabel.text property alias headerComponent: headerLoader.sourceComponent property real availableWidth: width - pageLoader.x @@ -40,11 +39,6 @@ Item { anchors.right: floatIcon.visible ? floatIcon.left : parent.right spacing: _margins visible: !ScreenTools.isShortScreen && headerLoader.sourceComponent === null - QGCLabel { - id: pageNameLabel - font.pointSize: ScreenTools.largeFontPointSize - visible: !popped - } QGCLabel { id: pageDescriptionLabel anchors.left: parent.left @@ -77,8 +71,8 @@ Item { fillMode: Image.PreserveAspectFit color: qgcPal.text visible: allowPopout && !popped && !ScreenTools.isMobile - MouseArea { - anchors.fill: parent + QGCMouseArea { + fillItem: floatIcon onClicked: popout() } } diff --git a/src/AnalyzeView/AnalyzeView.qml b/src/AnalyzeView/AnalyzeView.qml index 19f6538978dd..159c9e9c36c5 100644 --- a/src/AnalyzeView/AnalyzeView.qml +++ b/src/AnalyzeView/AnalyzeView.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Window import QtQuick.Controls import QGroundControl @@ -23,10 +22,6 @@ Rectangle { anchors.fill: parent } - GeoTagController { - id: geoController - } - QGCFlickable { id: buttonScroll width: buttonColumn.width @@ -44,37 +39,32 @@ Rectangle { width: _maxButtonWidth spacing: _defaultTextHeight / 2 - property real _maxButtonWidth: 0 - - Component.onCompleted: reflowWidths() - - // I don't know why this does not work - Connections { - target: QGroundControl.settingsManager.appSettings.appFontPointSize - function onValueChanged(value) { buttonColumn.reflowWidths() } - } - - function reflowWidths() { - buttonColumn._maxButtonWidth = 0 - for (var i = 0; i < children.length; i++) { - buttonColumn._maxButtonWidth = Math.max(buttonColumn._maxButtonWidth, children[i].width) - } - for (var j = 0; j < children.length; j++) { - children[j].width = buttonColumn._maxButtonWidth + property real _maxButtonWidth: { + var maxW = 0 + for (var i = 0; i < buttonRepeater.count; i++) { + var item = buttonRepeater.itemAt(i) + if (item) maxW = Math.max(maxW, item.implicitWidth) } + return maxW } Repeater { id: buttonRepeater model: QGroundControl.corePlugin ? QGroundControl.corePlugin.analyzePages : [] - Component.onCompleted: itemAt(0).checked = true + Component.onCompleted: { + if (count > 0) { + itemAt(0).checked = true + panelLoader.source = QGroundControl.corePlugin.analyzePages[0].url + panelLoader.title = QGroundControl.corePlugin.analyzePages[0].title + } + } SubMenuButton { - id: subMenu imageResource: modelData.icon autoExclusive: true text: modelData.title + width: buttonColumn._maxButtonWidth onClicked: { panelLoader.source = modelData.url @@ -108,13 +98,13 @@ Rectangle { anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - source: "LogDownloadPage.qml" + source: "" property string title Connections { target: panelLoader.item - function onPopout() { mainWindow.createrWindowedAnalyzePage(panelLoader.title, panelLoader.source) } + function onPopout() { mainWindow.createWindowedAnalyzePage(panelLoader.title, panelLoader.source) } } } } diff --git a/src/AnalyzeView/CMakeLists.txt b/src/AnalyzeView/CMakeLists.txt index 1f5bed0434bb..0969a76c349d 100644 --- a/src/AnalyzeView/CMakeLists.txt +++ b/src/AnalyzeView/CMakeLists.txt @@ -4,25 +4,12 @@ # ============================================================================ add_subdirectory(GeoTag) +add_subdirectory(MAVLinkConsole) +add_subdirectory(MAVLinkInspector) +add_subdirectory(OnboardLogs) target_sources(${CMAKE_PROJECT_NAME} PRIVATE - LogDownloadController.cc - LogDownloadController.h - LogEntry.cc - LogEntry.h - MAVLinkChartController.cc - MAVLinkChartController.h - MAVLinkConsoleController.cc - MAVLinkConsoleController.h - MAVLinkInspectorController.cc - MAVLinkInspectorController.h - MAVLinkMessage.cc - MAVLinkMessage.h - MAVLinkMessageField.cc - MAVLinkMessageField.h - MAVLinkSystem.cc - MAVLinkSystem.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -37,10 +24,13 @@ qt_add_qml_module(AnalyzeViewModule VERSION 1.0 RESOURCE_PREFIX /qml QML_FILES + AnalyzePage.qml AnalyzeView.qml - LogDownloadPage.qml - MAVLinkConsolePage.qml - MAVLinkInspectorPage.qml - VibrationPage.qml + MAVLinkConsole/MAVLinkConsolePage.qml + MAVLinkInspector/MAVLinkChart.qml + MAVLinkInspector/MAVLinkMessageButton.qml + MAVLinkInspector/MAVLinkInspectorPage.qml + OnboardLogs/OnboardLogPage.qml + Vibration/VibrationPage.qml NO_PLUGIN ) diff --git a/src/AnalyzeView/GeoTag/CMakeLists.txt b/src/AnalyzeView/GeoTag/CMakeLists.txt index 711107827fe1..07c1094a6baa 100644 --- a/src/AnalyzeView/GeoTag/CMakeLists.txt +++ b/src/AnalyzeView/GeoTag/CMakeLists.txt @@ -36,4 +36,5 @@ qt_add_qml_module(GeoTagModule NO_PLUGIN ) +target_link_libraries(GeoTagModule PRIVATE AnalyzeViewModule) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE GeoTagModule) diff --git a/src/AnalyzeView/GeoTag/GeoTagController.h b/src/AnalyzeView/GeoTag/GeoTagController.h index 5820bcc6565e..495f7b7791c2 100644 --- a/src/AnalyzeView/GeoTag/GeoTagController.h +++ b/src/AnalyzeView/GeoTag/GeoTagController.h @@ -49,6 +49,7 @@ class GeoTagController : public QObject { Q_OBJECT QML_ELEMENT + QML_SINGLETON Q_PROPERTY(QString logFile READ logFile WRITE setLogFile NOTIFY logFileChanged) Q_PROPERTY(QString imageDirectory READ imageDirectory WRITE setImageDirectory NOTIFY imageDirectoryChanged) diff --git a/src/AnalyzeView/GeoTag/GeoTagPage.qml b/src/AnalyzeView/GeoTag/GeoTagPage.qml index d6830f14283a..709973af61bc 100644 --- a/src/AnalyzeView/GeoTag/GeoTagPage.qml +++ b/src/AnalyzeView/GeoTag/GeoTagPage.qml @@ -3,6 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts import QGroundControl +import QGroundControl.AnalyzeView import QGroundControl.Controls AnalyzePage { @@ -27,7 +28,7 @@ AnalyzePage { Layout.preferredHeight: statusColumn.height + _margin * 2 color: qgcPal.windowShade radius: ScreenTools.defaultFontPixelWidth / 2 - visible: geoController.inProgress || geoController.errorMessage || geoController.taggedCount > 0 + visible: GeoTagController.inProgress || GeoTagController.errorMessage || GeoTagController.taggedCount > 0 ColumnLayout { id: statusColumn @@ -40,12 +41,12 @@ AnalyzePage { RowLayout { Layout.fillWidth: true spacing: _margin - visible: geoController.inProgress + visible: GeoTagController.inProgress BusyIndicator { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 2 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 2 - running: geoController.inProgress + running: GeoTagController.inProgress } ColumnLayout { @@ -61,32 +62,32 @@ AnalyzePage { Layout.fillWidth: true from: 0 to: 100 - value: geoController.progress + value: GeoTagController.progress } } } QGCLabel { Layout.fillWidth: true - text: geoController.errorMessage + text: GeoTagController.errorMessage color: qgcPal.colorRed font.bold: true wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter - visible: geoController.errorMessage && !geoController.inProgress + visible: GeoTagController.errorMessage && !GeoTagController.inProgress } QGCLabel { Layout.fillWidth: true text: { - if (geoController.taggedCount > 0 && !geoController.inProgress) { - let msg = qsTr("Successfully tagged %1 images").arg(geoController.taggedCount) + if (GeoTagController.taggedCount > 0 && !GeoTagController.inProgress) { + let msg = qsTr("Successfully tagged %1 images").arg(GeoTagController.taggedCount) let details = [] - if (geoController.skippedCount > 0) { - details.push(qsTr("%1 skipped").arg(geoController.skippedCount)) + if (GeoTagController.skippedCount > 0) { + details.push(qsTr("%1 skipped").arg(GeoTagController.skippedCount)) } - if (geoController.failedCount > 0) { - details.push(qsTr("%1 failed").arg(geoController.failedCount)) + if (GeoTagController.failedCount > 0) { + details.push(qsTr("%1 failed").arg(GeoTagController.failedCount)) } if (details.length > 0) { msg += " (" + details.join(", ") + ")" @@ -95,10 +96,10 @@ AnalyzePage { } return "" } - color: geoController.failedCount > 0 ? qgcPal.colorOrange : qgcPal.colorGreen + color: GeoTagController.failedCount > 0 ? qgcPal.colorOrange : qgcPal.colorGreen font.bold: true horizontalAlignment: Text.AlignHCenter - visible: geoController.taggedCount > 0 && !geoController.inProgress + visible: GeoTagController.taggedCount > 0 && !GeoTagController.inProgress } } } @@ -126,12 +127,12 @@ AnalyzePage { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5 radius: height / 2 - color: geoController.logFile ? qgcPal.colorGreen : qgcPal.button + color: GeoTagController.logFile ? qgcPal.colorGreen : qgcPal.button QGCLabel { anchors.centerIn: parent - text: geoController.logFile ? "\u2713" : "1" - color: geoController.logFile ? "white" : qgcPal.buttonText + text: GeoTagController.logFile ? "\u2713" : "1" + color: GeoTagController.logFile ? "white" : qgcPal.buttonText font.bold: true } } @@ -148,7 +149,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: openLogFile.openForLoad() QGCFileDialog { @@ -157,7 +158,7 @@ AnalyzePage { nameFilters: [qsTr("Flight logs (*.ulg *.bin)"), qsTr("ULog (*.ulg)"), qsTr("DataFlash (*.bin)"), qsTr("All Files (*)")] defaultSuffix: "ulg" onAcceptedForLoad: (file) => { - geoController.logFile = file + GeoTagController.logFile = file close() } } @@ -165,9 +166,9 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true - text: geoController.logFile ? geoController.logFile : qsTr("No file selected") + text: GeoTagController.logFile ? GeoTagController.logFile : qsTr("No file selected") elide: Text.ElideMiddle - opacity: geoController.logFile ? 1.0 : 0.5 + opacity: GeoTagController.logFile ? 1.0 : 0.5 } } } @@ -196,12 +197,12 @@ AnalyzePage { Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 1.5 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight * 1.5 radius: height / 2 - color: geoController.imageDirectory ? qgcPal.colorGreen : qgcPal.button + color: GeoTagController.imageDirectory ? qgcPal.colorGreen : qgcPal.button QGCLabel { anchors.centerIn: parent - text: geoController.imageDirectory ? "\u2713" : "2" - color: geoController.imageDirectory ? "white" : qgcPal.buttonText + text: GeoTagController.imageDirectory ? "\u2713" : "2" + color: GeoTagController.imageDirectory ? "white" : qgcPal.buttonText font.bold: true } } @@ -218,7 +219,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: selectImageDir.openForLoad() QGCFileDialog { @@ -226,7 +227,7 @@ AnalyzePage { title: qsTr("Select Image Folder") selectFolder: true onAcceptedForLoad: (file) => { - geoController.imageDirectory = file + GeoTagController.imageDirectory = file close() } } @@ -234,9 +235,9 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true - text: geoController.imageDirectory ? geoController.imageDirectory : qsTr("No folder selected") + text: GeoTagController.imageDirectory ? GeoTagController.imageDirectory : qsTr("No folder selected") elide: Text.ElideMiddle - opacity: geoController.imageDirectory ? 1.0 : 0.5 + opacity: GeoTagController.imageDirectory ? 1.0 : 0.5 } } } @@ -287,7 +288,7 @@ AnalyzePage { QGCButton { text: qsTr("Browse...") - enabled: !geoController.inProgress + enabled: !GeoTagController.inProgress onClicked: selectDestDir.openForLoad() QGCFileDialog { @@ -295,7 +296,7 @@ AnalyzePage { title: qsTr("Select Output Folder") selectFolder: true onAcceptedForLoad: (file) => { - geoController.saveDirectory = file + GeoTagController.saveDirectory = file close() } } @@ -304,15 +305,15 @@ AnalyzePage { QGCLabel { Layout.fillWidth: true text: { - if (geoController.saveDirectory) { - return geoController.saveDirectory - } else if (geoController.imageDirectory) { - return geoController.imageDirectory + "/TAGGED" + if (GeoTagController.saveDirectory) { + return GeoTagController.saveDirectory + } else if (GeoTagController.imageDirectory) { + return GeoTagController.imageDirectory + "/TAGGED" } return qsTr("Default: /TAGGED subfolder") } elide: Text.ElideMiddle - opacity: geoController.saveDirectory ? 1.0 : 0.5 + opacity: GeoTagController.saveDirectory ? 1.0 : 0.5 } } } @@ -349,11 +350,11 @@ AnalyzePage { QGCTextField { id: timeOffsetField Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 10 - text: geoController.timeOffsetSecs.toFixed(1) - enabled: !geoController.inProgress + text: GeoTagController.timeOffsetSecs.toFixed(1) + enabled: !GeoTagController.inProgress inputMethodHints: Qt.ImhFormattedNumbersOnly validator: DoubleValidator { bottom: -3600; top: 3600; decimals: 1 } - onEditingFinished: geoController.timeOffsetSecs = parseFloat(text) || 0 + onEditingFinished: GeoTagController.timeOffsetSecs = parseFloat(text) || 0 } QGCLabel { @@ -371,9 +372,9 @@ AnalyzePage { QGCCheckBox { id: previewCheckbox text: qsTr("Preview mode (don't write files)") - checked: geoController.previewMode - enabled: !geoController.inProgress - onClicked: geoController.previewMode = checked + checked: GeoTagController.previewMode + enabled: !GeoTagController.inProgress + onClicked: GeoTagController.previewMode = checked } QGCLabel { @@ -391,20 +392,20 @@ AnalyzePage { Layout.alignment: Qt.AlignHCenter Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 20 text: { - if (geoController.inProgress) { + if (GeoTagController.inProgress) { return qsTr("Cancel") - } else if (geoController.previewMode) { + } else if (GeoTagController.previewMode) { return qsTr("Preview") } else { return qsTr("Start Tagging") } } - enabled: (geoController.logFile && geoController.imageDirectory) || geoController.inProgress + enabled: (GeoTagController.logFile && GeoTagController.imageDirectory) || GeoTagController.inProgress onClicked: { - if (geoController.inProgress) { - geoController.cancelTagging() + if (GeoTagController.inProgress) { + GeoTagController.cancelTagging() } else { - geoController.startTagging() + GeoTagController.startTagging() } } } @@ -416,7 +417,7 @@ AnalyzePage { Layout.minimumHeight: ScreenTools.defaultFontPixelHeight * 10 color: qgcPal.windowShade radius: ScreenTools.defaultFontPixelWidth / 2 - visible: geoController.imageModel.count > 0 + visible: GeoTagController.imageModel.count > 0 ColumnLayout { anchors.fill: parent @@ -428,7 +429,7 @@ AnalyzePage { spacing: _margin QGCLabel { - text: qsTr("Images (%1)").arg(geoController.imageModel.count) + text: qsTr("Images (%1)").arg(GeoTagController.imageModel.count) font.bold: true } @@ -477,7 +478,7 @@ AnalyzePage { id: imageListView Layout.fillWidth: true Layout.fillHeight: true - model: geoController.imageModel + model: GeoTagController.imageModel delegate: Rectangle { width: imageListView.width diff --git a/src/AnalyzeView/LogEntry.cc b/src/AnalyzeView/LogEntry.cc deleted file mode 100644 index a50239cb54cc..000000000000 --- a/src/AnalyzeView/LogEntry.cc +++ /dev/null @@ -1,64 +0,0 @@ -#include "LogEntry.h" -#include "QGCApplication.h" -#include "QGCLoggingCategory.h" - -#include - -QGC_LOGGING_CATEGORY(LogEntryLog, "AnalyzeView.QGCLogEntry") - -LogDownloadData::LogDownloadData(QGCLogEntry * const logEntry) - : ID(logEntry->id()) - , entry(logEntry) -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -LogDownloadData::~LogDownloadData() -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -void LogDownloadData::advanceChunk() -{ - ++current_chunk; - chunk_table = QBitArray(chunkBins(), false); -} - -uint32_t LogDownloadData::chunkBins() const -{ - const qreal num = static_cast((entry->size() - (current_chunk * kChunkSize))) / static_cast(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); - return qMin(static_cast(qCeil(num)), kTableBins); -} - -uint32_t LogDownloadData::numChunks() const -{ - const qreal num = static_cast(entry->size()) / static_cast(kChunkSize); - return qCeil(num); -} - -bool LogDownloadData::chunkEquals(const bool val) const -{ - return (chunk_table == QBitArray(chunk_table.size(), val)); -} - -/*===========================================================================*/ - -QGCLogEntry::QGCLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) - : QObject(parent) - , _logID(logId) - , _logSize(logSize) - , _logTimeUTC(dateTime) - , _received(received) -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -QGCLogEntry::~QGCLogEntry() -{ - // qCDebug(LogEntryLog) << Q_FUNC_INFO << this; -} - -QString QGCLogEntry::sizeStr() const -{ - return qgcApp()->bigSizeToString(_logSize); -} diff --git a/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt b/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt new file mode 100644 index 000000000000..6cd4a2a3fe55 --- /dev/null +++ b/src/AnalyzeView/MAVLinkConsole/CMakeLists.txt @@ -0,0 +1,12 @@ +# ============================================================================ +# MAVLink Console Module +# MAVLink serial console controller +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + MAVLinkConsoleController.cc + MAVLinkConsoleController.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/AnalyzeView/MAVLinkConsoleController.cc b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleController.cc rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.cc diff --git a/src/AnalyzeView/MAVLinkConsoleController.h b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.h similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleController.h rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleController.h diff --git a/src/AnalyzeView/MAVLinkConsoleIcon.svg b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg similarity index 100% rename from src/AnalyzeView/MAVLinkConsoleIcon.svg rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsoleIcon.svg diff --git a/src/AnalyzeView/MAVLinkConsolePage.qml b/src/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml similarity index 100% rename from src/AnalyzeView/MAVLinkConsolePage.qml rename to src/AnalyzeView/MAVLinkConsole/MAVLinkConsolePage.qml diff --git a/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt b/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt new file mode 100644 index 000000000000..4dfd894f392f --- /dev/null +++ b/src/AnalyzeView/MAVLinkInspector/CMakeLists.txt @@ -0,0 +1,20 @@ +# ============================================================================ +# MAVLink Inspector Module +# MAVLink inspector controller and chart integration +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + MAVLinkChartController.cc + MAVLinkChartController.h + MAVLinkInspectorController.cc + MAVLinkInspectorController.h + MAVLinkMessage.cc + MAVLinkMessage.h + MAVLinkMessageField.cc + MAVLinkMessageField.h + MAVLinkSystem.cc + MAVLinkSystem.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/QmlControls/MAVLinkChart.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkChart.qml similarity index 100% rename from src/QmlControls/MAVLinkChart.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChart.qml diff --git a/src/AnalyzeView/MAVLinkChartController.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkChartController.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.cc diff --git a/src/AnalyzeView/MAVLinkChartController.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.h similarity index 100% rename from src/AnalyzeView/MAVLinkChartController.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkChartController.h diff --git a/src/AnalyzeView/MAVLinkInspector.svg b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg similarity index 100% rename from src/AnalyzeView/MAVLinkInspector.svg rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspector.svg diff --git a/src/AnalyzeView/MAVLinkInspectorController.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.cc similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorController.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.cc diff --git a/src/AnalyzeView/MAVLinkInspectorController.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.h similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorController.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorController.h diff --git a/src/AnalyzeView/MAVLinkInspectorPage.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml similarity index 100% rename from src/AnalyzeView/MAVLinkInspectorPage.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkInspectorPage.qml diff --git a/src/AnalyzeView/MAVLinkMessage.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.cc similarity index 100% rename from src/AnalyzeView/MAVLinkMessage.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.cc diff --git a/src/AnalyzeView/MAVLinkMessage.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.h similarity index 100% rename from src/AnalyzeView/MAVLinkMessage.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessage.h diff --git a/src/QmlControls/MAVLinkMessageButton.qml b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageButton.qml similarity index 100% rename from src/QmlControls/MAVLinkMessageButton.qml rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageButton.qml diff --git a/src/AnalyzeView/MAVLinkMessageField.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.cc similarity index 100% rename from src/AnalyzeView/MAVLinkMessageField.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.cc diff --git a/src/AnalyzeView/MAVLinkMessageField.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.h similarity index 100% rename from src/AnalyzeView/MAVLinkMessageField.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkMessageField.h diff --git a/src/AnalyzeView/MAVLinkSystem.cc b/src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.cc similarity index 100% rename from src/AnalyzeView/MAVLinkSystem.cc rename to src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.cc diff --git a/src/AnalyzeView/MAVLinkSystem.h b/src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.h similarity index 100% rename from src/AnalyzeView/MAVLinkSystem.h rename to src/AnalyzeView/MAVLinkInspector/MAVLinkSystem.h diff --git a/src/AnalyzeView/OnboardLogs/CMakeLists.txt b/src/AnalyzeView/OnboardLogs/CMakeLists.txt new file mode 100644 index 000000000000..f47679afa27c --- /dev/null +++ b/src/AnalyzeView/OnboardLogs/CMakeLists.txt @@ -0,0 +1,14 @@ +# ============================================================================ +# Onboard Logs Module +# Onboard log list and download functionality +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + OnboardLogController.cc + OnboardLogController.h + OnboardLogEntry.cc + OnboardLogEntry.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/AnalyzeView/LogDownloadController.cc b/src/AnalyzeView/OnboardLogs/OnboardLogController.cc similarity index 69% rename from src/AnalyzeView/LogDownloadController.cc rename to src/AnalyzeView/OnboardLogs/OnboardLogController.cc index 38b569e85b3d..f6b9638bb6b9 100644 --- a/src/AnalyzeView/LogDownloadController.cc +++ b/src/AnalyzeView/OnboardLogs/OnboardLogController.cc @@ -1,6 +1,6 @@ -#include "LogDownloadController.h" +#include "OnboardLogController.h" #include "AppSettings.h" -#include "LogEntry.h" +#include "OnboardLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" #include "ParameterManager.h" @@ -13,35 +13,35 @@ #include #include -QGC_LOGGING_CATEGORY(LogDownloadControllerLog, "AnalyzeView.LogDownloadController") +QGC_LOGGING_CATEGORY(OnboardLogControllerLog, "AnalyzeView.OnboardLogController") -LogDownloadController::LogDownloadController(QObject *parent) +OnboardLogController::OnboardLogController(QObject *parent) : QObject(parent) , _timer(new QTimer(this)) , _logEntriesModel(new QmlObjectListModel(this)) { - qCDebug(LogDownloadControllerLog) << this; + qCDebug(OnboardLogControllerLog) << this; - (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &LogDownloadController::_setActiveVehicle); - (void) connect(_timer, &QTimer::timeout, this, &LogDownloadController::_processDownload); + (void) connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &OnboardLogController::_setActiveVehicle); + (void) connect(_timer, &QTimer::timeout, this, &OnboardLogController::_processDownload); _timer->setSingleShot(false); _setActiveVehicle(MultiVehicleManager::instance()->activeVehicle()); } -LogDownloadController::~LogDownloadController() +OnboardLogController::~OnboardLogController() { - qCDebug(LogDownloadControllerLog) << this; + qCDebug(OnboardLogControllerLog) << this; } -void LogDownloadController::download(const QString &path) +void OnboardLogController::download(const QString &path) { const QString dir = path.isEmpty() ? SettingsManager::instance()->appSettings()->logSavePath() : path; _downloadToDirectory(dir); } -void LogDownloadController::_downloadToDirectory(const QString &dir) +void OnboardLogController::_downloadToDirectory(const QString &dir) { _receivedAllEntries(); @@ -56,7 +56,7 @@ void LogDownloadController::_downloadToDirectory(const QString &dir) _downloadPath += QDir::separator(); } - QGCLogEntry *const log = _getNextSelected(); + QGCOnboardLogEntry *const log = _getNextSelected(); if (log) { log->setStatus(tr("Waiting")); } @@ -65,7 +65,7 @@ void LogDownloadController::_downloadToDirectory(const QString &dir) _receivedAllData(); } -void LogDownloadController::_processDownload() +void OnboardLogController::_processDownload() { if (_requestingLogEntries) { _findMissingEntries(); @@ -74,13 +74,13 @@ void LogDownloadController::_processDownload() } } -void LogDownloadController::_findMissingEntries() +void OnboardLogController::_findMissingEntries() { const int num_logs = _logEntriesModel->count(); int start = -1; int end = -1; for (int i = 0; i < num_logs; i++) { - const QGCLogEntry *const entry = _logEntriesModel->value(i); + const QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -103,14 +103,14 @@ void LogDownloadController::_findMissingEntries() if (_retries++ > 2) { for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (entry && !entry->received()) { entry->setStatus(tr("Error")); } } _receivedAllEntries(); - qCWarning(LogDownloadControllerLog) << "Too many errors retreiving log list. Giving up."; + qCWarning(OnboardLogControllerLog) << "Too many errors retreiving log list. Giving up."; return; } @@ -124,7 +124,7 @@ void LogDownloadController::_findMissingEntries() _requestLogList(static_cast(start), static_cast(end)); } -void LogDownloadController::_setActiveVehicle(Vehicle *vehicle) +void OnboardLogController::_setActiveVehicle(Vehicle *vehicle) { if (vehicle == _vehicle) { return; @@ -132,19 +132,19 @@ void LogDownloadController::_setActiveVehicle(Vehicle *vehicle) if (_vehicle) { _logEntriesModel->clearAndDeleteContents(); - (void) disconnect(_vehicle, &Vehicle::logEntry, this, &LogDownloadController::_logEntry); - (void) disconnect(_vehicle, &Vehicle::logData, this, &LogDownloadController::_logData); + (void) disconnect(_vehicle, &Vehicle::logEntry, this, &OnboardLogController::_logEntry); + (void) disconnect(_vehicle, &Vehicle::logData, this, &OnboardLogController::_logData); } _vehicle = vehicle; if (_vehicle) { - (void) connect(_vehicle, &Vehicle::logEntry, this, &LogDownloadController::_logEntry); - (void) connect(_vehicle, &Vehicle::logData, this, &LogDownloadController::_logData); + (void) connect(_vehicle, &Vehicle::logEntry, this, &OnboardLogController::_logEntry); + (void) connect(_vehicle, &Vehicle::logData, this, &OnboardLogController::_logData); } } -void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) +void OnboardLogController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t id, uint16_t num_logs, uint16_t last_log_num) { Q_UNUSED(last_log_num); @@ -159,7 +159,7 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = new QGCLogEntry(i); + QGCOnboardLogEntry *const entry = new QGCOnboardLogEntry(i); _logEntriesModel->append(entry); } } @@ -168,13 +168,13 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t if ((size > 0) || (_vehicle->firmwareType() != MAV_AUTOPILOT_ARDUPILOTMEGA)) { id -= _apmOffset; if (id < _logEntriesModel->count()) { - QGCLogEntry *const entry = _logEntriesModel->value(id); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(id); entry->setSize(size); entry->setTime(QDateTime::fromSecsSinceEpoch(time_utc)); entry->setReceived(true); entry->setStatus(tr("Available")); } else { - qCWarning(LogDownloadControllerLog) << "Received log entry for out-of-bound index:" << id; + qCWarning(OnboardLogControllerLog) << "Received onboard log entry for out-of-bound index:" << id; } } } else { @@ -190,17 +190,17 @@ void LogDownloadController::_logEntry(uint32_t time_utc, uint32_t size, uint16_t } } -void LogDownloadController::_receivedAllEntries() +void OnboardLogController::_receivedAllEntries() { _timer->stop(); _setListing(false); } -bool LogDownloadController::_entriesComplete() const +bool OnboardLogController::_entriesComplete() const { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - const QGCLogEntry *const entry = _logEntriesModel->value(i); + const QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -213,7 +213,7 @@ bool LogDownloadController::_entriesComplete() const return true; } -void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) +void OnboardLogController::_logData(uint32_t ofs, uint16_t id, uint8_t count, const uint8_t *data) { if (!_downloadingLogs || !_downloadData) { return; @@ -221,34 +221,34 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c id -= _apmOffset; if (_downloadData->ID != id) { - qCWarning(LogDownloadControllerLog) << "Received log data for wrong log"; + qCWarning(OnboardLogControllerLog) << "Received log data for wrong log"; return; } if ((ofs % MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN) != 0) { - qCWarning(LogDownloadControllerLog) << "Ignored misaligned incoming packet @" << ofs; + qCWarning(OnboardLogControllerLog) << "Ignored misaligned incoming packet @" << ofs; return; } bool result = false; if (ofs <= _downloadData->entry->size()) { - const uint32_t chunk = ofs / LogDownloadData::kChunkSize; - // qCDebug(LogDownloadControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; + const uint32_t chunk = ofs / OnboardLogDownloadData::kChunkSize; + // qCDebug(OnboardLogControllerLog) << "Received data - Offset:" << ofs << "Chunk:" << chunk; if (chunk != _downloadData->current_chunk) { - qCWarning(LogDownloadControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; + qCWarning(OnboardLogControllerLog) << "Ignored packet for out of order chunk actual:expected" << chunk << _downloadData->current_chunk; return; } - const uint16_t bin = (ofs - (chunk * LogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; + const uint16_t bin = (ofs - (chunk * OnboardLogDownloadData::kChunkSize)) / MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; if (bin >= _downloadData->chunk_table.size()) { - qCWarning(LogDownloadControllerLog) << "Out of range bin received"; + qCWarning(OnboardLogControllerLog) << "Out of range bin received"; } else { _downloadData->chunk_table.setBit(bin); } if (_downloadData->file.pos() != ofs) { if (!_downloadData->file.seek(ofs)) { - qCWarning(LogDownloadControllerLog) << "Error while seeking log file offset"; + qCWarning(OnboardLogControllerLog) << "Error while seeking log file offset"; return; } } @@ -268,17 +268,17 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c } else if (_chunkComplete()) { _downloadData->advanceChunk(); _requestLogData(_downloadData->ID, - _downloadData->current_chunk * LogDownloadData::kChunkSize, + _downloadData->current_chunk * OnboardLogDownloadData::kChunkSize, _downloadData->chunk_table.size() * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); } else if ((bin < (_downloadData->chunk_table.size() - 1)) && _downloadData->chunk_table.at(bin + 1)) { // Likely to be grabbing fragments and got to the end of a gap _findMissingData(); } } else { - qCWarning(LogDownloadControllerLog) << "Error while writing log file chunk"; + qCWarning(OnboardLogControllerLog) << "Error while writing log file chunk"; } } else { - qCWarning(LogDownloadControllerLog) << "Received log offset greater than expected"; + qCWarning(OnboardLogControllerLog) << "Received log offset greater than expected"; } if (!result) { @@ -286,7 +286,7 @@ void LogDownloadController::_logData(uint32_t ofs, uint16_t id, uint8_t count, c } } -void LogDownloadController::_findMissingData() +void OnboardLogController::_findMissingData() { if (_logComplete()) { _receivedAllData(); @@ -315,12 +315,12 @@ void LogDownloadController::_findMissingData() } } - const uint32_t pos = (_downloadData->current_chunk * LogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + const uint32_t pos = (_downloadData->current_chunk * OnboardLogDownloadData::kChunkSize) + (start * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); const uint32_t len = (end - start) * MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN; _requestLogData(_downloadData->ID, pos, len, _retries); } -void LogDownloadController::_updateDataRate() +void OnboardLogController::_updateDataRate() { constexpr uint kSizeUpdateThreshold = 102400; // 0.1 MB const bool timeThresholdMet = _downloadData->elapsed.elapsed() >= kGUIRateMs; @@ -350,17 +350,17 @@ void LogDownloadController::_updateDataRate() _downloadData->last_status_written = _downloadData->written; } -bool LogDownloadController::_chunkComplete() const +bool OnboardLogController::_chunkComplete() const { return _downloadData->chunkEquals(true); } -bool LogDownloadController::_logComplete() const +bool OnboardLogController::_logComplete() const { return (_chunkComplete() && ((_downloadData->current_chunk + 1) == _downloadData->numChunks())); } -void LogDownloadController::_receivedAllData() +void OnboardLogController::_receivedAllData() { _timer->stop(); if (_prepareLogDownload()) { @@ -372,11 +372,11 @@ void LogDownloadController::_receivedAllData() } } -bool LogDownloadController::_prepareLogDownload() +bool OnboardLogController::_prepareLogDownload() { _downloadData.reset(); - QGCLogEntry *const entry = _getNextSelected(); + QGCOnboardLogEntry *const entry = _getNextSelected(); if (!entry) { return false; } @@ -386,7 +386,7 @@ bool LogDownloadController::_prepareLogDownload() const QString ftime = (entry->time().date().year() >= 2010) ? entry->time().toString(QStringLiteral("yyyy-M-d-hh-mm-ss")) : QStringLiteral("UnknownDate"); - _downloadData = std::make_unique(entry); + _downloadData = std::make_unique(entry); _downloadData->filename = QStringLiteral("log_") + QString::number(entry->id()) + "_" + ftime; if (_vehicle->firmwareType() == MAV_AUTOPILOT_PX4) { @@ -415,9 +415,9 @@ bool LogDownloadController::_prepareLogDownload() bool result = false; if (!_downloadData->file.open(QIODevice::WriteOnly)) { - qCWarning(LogDownloadControllerLog) << "Failed to create log file:" << _downloadData->filename; + qCWarning(OnboardLogControllerLog) << "Failed to create log file:" << _downloadData->filename; } else if (!_downloadData->file.resize(entry->size())) { - qCWarning(LogDownloadControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; + qCWarning(OnboardLogControllerLog) << "Failed to allocate space for log file:" << _downloadData->filename; } else { _downloadData->current_chunk = 0; _downloadData->chunk_table = QBitArray(_downloadData->chunkBins(), false); @@ -437,17 +437,17 @@ bool LogDownloadController::_prepareLogDownload() return result; } -void LogDownloadController::refresh() +void OnboardLogController::refresh() { _logEntriesModel->clearAndDeleteContents(); _requestLogList(0, 0xffff); } -QGCLogEntry *LogDownloadController::_getNextSelected() const +QGCOnboardLogEntry *OnboardLogController::_getNextSelected() const { const int numLogs = _logEntriesModel->count(); for (int i = 0; i < numLogs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -460,7 +460,7 @@ QGCLogEntry *LogDownloadController::_getNextSelected() const return nullptr; } -void LogDownloadController::cancel() +void OnboardLogController::cancel() { _requestLogEnd(); _receivedAllEntries(); @@ -478,11 +478,11 @@ void LogDownloadController::cancel() _setDownloading(false); } -void LogDownloadController::_resetSelection(bool canceled) +void OnboardLogController::_resetSelection(bool canceled) { const int num_logs = _logEntriesModel->count(); for (int i = 0; i < num_logs; i++) { - QGCLogEntry *const entry = _logEntriesModel->value(i); + QGCOnboardLogEntry *const entry = _logEntriesModel->value(i); if (!entry) { continue; } @@ -498,16 +498,16 @@ void LogDownloadController::_resetSelection(bool canceled) emit selectionChanged(); } -void LogDownloadController::eraseAll() +void OnboardLogController::eraseAll() { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -522,23 +522,23 @@ void LogDownloadController::eraseAll() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; return; } refresh(); } -void LogDownloadController::_requestLogList(uint32_t start, uint32_t end) +void OnboardLogController::_requestLogList(uint32_t start, uint32_t end) { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -555,30 +555,30 @@ void LogDownloadController::_requestLogList(uint32_t start, uint32_t end) ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; return; } - qCDebug(LogDownloadControllerLog) << "Request log entry list (" << start << "through" << end << ")"; + qCDebug(OnboardLogControllerLog) << "Request onboard log entry list (" << start << "through" << end << ")"; _setListing(true); _timer->start(kRequestLogListTimeoutMs); } -void LogDownloadController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) +void OnboardLogController::_requestLogData(uint16_t id, uint32_t offset, uint32_t count, int retryCount) { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } id += _apmOffset; - qCDebug(LogDownloadControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; + qCDebug(OnboardLogControllerLog) << "Request log data (id:" << id << "offset:" << offset << "size:" << count << "retryCount" << retryCount << ")"; mavlink_message_t msg{}; (void) mavlink_msg_log_request_data_pack_chan( @@ -594,20 +594,20 @@ void LogDownloadController::_requestLogData(uint16_t id, uint32_t offset, uint32 ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; } } -void LogDownloadController::_requestLogEnd() +void OnboardLogController::_requestLogEnd() { if (!_vehicle) { - qCWarning(LogDownloadControllerLog) << "Vehicle Unavailable"; + qCWarning(OnboardLogControllerLog) << "Vehicle Unavailable"; return; } SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { - qCWarning(LogDownloadControllerLog) << "Link Unavailable"; + qCWarning(OnboardLogControllerLog) << "Link Unavailable"; return; } @@ -622,11 +622,11 @@ void LogDownloadController::_requestLogEnd() ); if (!_vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg)) { - qCWarning(LogDownloadControllerLog) << "Failed to send"; + qCWarning(OnboardLogControllerLog) << "Failed to send"; } } -void LogDownloadController::_setDownloading(bool active) +void OnboardLogController::_setDownloading(bool active) { if (_downloadingLogs != active) { _downloadingLogs = active; @@ -635,7 +635,7 @@ void LogDownloadController::_setDownloading(bool active) } } -void LogDownloadController::_setListing(bool active) +void OnboardLogController::_setListing(bool active) { if (_requestingLogEntries != active) { _requestingLogEntries = active; @@ -644,7 +644,7 @@ void LogDownloadController::_setListing(bool active) } } -void LogDownloadController::setCompressLogs(bool compress) +void OnboardLogController::setCompressLogs(bool compress) { if (_compressLogs != compress) { _compressLogs = compress; @@ -652,25 +652,25 @@ void LogDownloadController::setCompressLogs(bool compress) } } -bool LogDownloadController::compressLogFile(const QString &logPath) +bool OnboardLogController::compressLogFile(const QString &logPath) { Q_UNUSED(logPath) - qCWarning(LogDownloadControllerLog) << "Log compression not yet implemented (decompression-only API)"; + qCWarning(OnboardLogControllerLog) << "Log compression not yet implemented (decompression-only API)"; return false; } -void LogDownloadController::cancelCompression() +void OnboardLogController::cancelCompression() { // Not implemented - compression API is decompression-only } -void LogDownloadController::_handleCompressionProgress(qreal progress) +void OnboardLogController::_handleCompressionProgress(qreal progress) { Q_UNUSED(progress) // Not implemented - compression API is decompression-only } -void LogDownloadController::_handleCompressionFinished(bool success) +void OnboardLogController::_handleCompressionFinished(bool success) { Q_UNUSED(success) // Not implemented - compression API is decompression-only diff --git a/src/AnalyzeView/LogDownloadController.h b/src/AnalyzeView/OnboardLogs/OnboardLogController.h similarity index 89% rename from src/AnalyzeView/LogDownloadController.h rename to src/AnalyzeView/OnboardLogs/OnboardLogController.h index b999f14601d6..0bf1cf74dd60 100644 --- a/src/AnalyzeView/LogDownloadController.h +++ b/src/AnalyzeView/OnboardLogs/OnboardLogController.h @@ -4,17 +4,17 @@ #include #include -Q_DECLARE_LOGGING_CATEGORY(LogDownloadControllerLog) +Q_DECLARE_LOGGING_CATEGORY(OnboardLogControllerLog) -struct LogDownloadData; -class QGCLogEntry; +struct OnboardLogDownloadData; +class QGCOnboardLogEntry; class QmlObjectListModel; class QTimer; class QThread; class Vehicle; -class LogDownloadTest; +class OnboardLogDownloadTest; -class LogDownloadController : public QObject +class OnboardLogController : public QObject { Q_OBJECT QML_ELEMENT @@ -28,11 +28,11 @@ class LogDownloadController : public QObject Q_PROPERTY(bool compressing READ compressing NOTIFY compressingChanged) Q_PROPERTY(float compressionProgress READ compressionProgress NOTIFY compressionProgressChanged) - friend class LogDownloadTest; + friend class OnboardLogDownloadTest; public: - explicit LogDownloadController(QObject *parent = nullptr); - ~LogDownloadController(); + explicit OnboardLogController(QObject *parent = nullptr); + ~OnboardLogController(); Q_INVOKABLE void refresh(); Q_INVOKABLE void download(const QString &path = QString()); @@ -89,7 +89,7 @@ private slots: void _setListing(bool active); void _updateDataRate(); - QGCLogEntry *_getNextSelected() const; + QGCOnboardLogEntry *_getNextSelected() const; QTimer *_timer = nullptr; QmlObjectListModel *_logEntriesModel = nullptr; @@ -98,7 +98,7 @@ private slots: bool _requestingLogEntries = false; int _apmOffset = 0; int _retries = 0; - std::unique_ptr _downloadData; + std::unique_ptr _downloadData; QString _downloadPath; Vehicle *_vehicle = nullptr; bool _compressLogs = false; diff --git a/src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc new file mode 100644 index 000000000000..e5125030dbb6 --- /dev/null +++ b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc @@ -0,0 +1,64 @@ +#include "OnboardLogEntry.h" +#include "QGCApplication.h" +#include "QGCLoggingCategory.h" + +#include + +QGC_LOGGING_CATEGORY(OnboardLogEntryLog, "AnalyzeView.QGCOnboardLogEntry") + +OnboardLogDownloadData::OnboardLogDownloadData(QGCOnboardLogEntry * const logEntry) + : ID(logEntry->id()) + , entry(logEntry) +{ + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; +} + +OnboardLogDownloadData::~OnboardLogDownloadData() +{ + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; +} + +void OnboardLogDownloadData::advanceChunk() +{ + ++current_chunk; + chunk_table = QBitArray(chunkBins(), false); +} + +uint32_t OnboardLogDownloadData::chunkBins() const +{ + const qreal num = static_cast((entry->size() - (current_chunk * kChunkSize))) / static_cast(MAVLINK_MSG_LOG_DATA_FIELD_DATA_LEN); + return qMin(static_cast(qCeil(num)), kTableBins); +} + +uint32_t OnboardLogDownloadData::numChunks() const +{ + const qreal num = static_cast(entry->size()) / static_cast(kChunkSize); + return qCeil(num); +} + +bool OnboardLogDownloadData::chunkEquals(const bool val) const +{ + return (chunk_table == QBitArray(chunk_table.size(), val)); +} + +/*===========================================================================*/ + +QGCOnboardLogEntry::QGCOnboardLogEntry(uint logId, const QDateTime &dateTime, uint logSize, bool received, QObject *parent) + : QObject(parent) + , _logID(logId) + , _logSize(logSize) + , _logTimeUTC(dateTime) + , _received(received) +{ + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; +} + +QGCOnboardLogEntry::~QGCOnboardLogEntry() +{ + // qCDebug(OnboardLogEntryLog) << Q_FUNC_INFO << this; +} + +QString QGCOnboardLogEntry::sizeStr() const +{ + return qgcApp()->bigSizeToString(_logSize); +} diff --git a/src/AnalyzeView/LogEntry.h b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.h similarity index 86% rename from src/AnalyzeView/LogEntry.h rename to src/AnalyzeView/OnboardLogs/OnboardLogEntry.h index 829ac87af423..567b26134654 100644 --- a/src/AnalyzeView/LogEntry.h +++ b/src/AnalyzeView/OnboardLogs/OnboardLogEntry.h @@ -10,14 +10,14 @@ #include "MAVLinkLib.h" -class QGCLogEntry; +class QGCOnboardLogEntry; -Q_DECLARE_LOGGING_CATEGORY(LogEntryLog) +Q_DECLARE_LOGGING_CATEGORY(OnboardLogEntryLog) -struct LogDownloadData +struct OnboardLogDownloadData { - explicit LogDownloadData(QGCLogEntry * const logEntry); - ~LogDownloadData(); + explicit OnboardLogDownloadData(QGCOnboardLogEntry * const logEntry); + ~OnboardLogDownloadData(); void advanceChunk(); @@ -31,7 +31,7 @@ struct LogDownloadData bool chunkEquals(const bool val) const; uint ID = 0; - QGCLogEntry *const entry = nullptr; + QGCOnboardLogEntry *const entry = nullptr; QBitArray chunk_table; uint32_t current_chunk = 0; @@ -49,7 +49,7 @@ struct LogDownloadData /*===========================================================================*/ -class QGCLogEntry : public QObject +class QGCOnboardLogEntry : public QObject { Q_OBJECT // QML_ELEMENT @@ -63,8 +63,8 @@ class QGCLogEntry : public QObject Q_PROPERTY(QString status READ status NOTIFY statusChanged) public: - explicit QGCLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); - ~QGCLogEntry(); + explicit QGCOnboardLogEntry(uint logId, const QDateTime &dateTime = QDateTime(), uint logSize = 0, bool received = false, QObject *parent = nullptr); + ~QGCOnboardLogEntry(); uint id() const { return _logID; } uint size() const { return _logSize; } diff --git a/src/AnalyzeView/LogDownloadIcon.svg b/src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg similarity index 100% rename from src/AnalyzeView/LogDownloadIcon.svg rename to src/AnalyzeView/OnboardLogs/OnboardLogIcon.svg diff --git a/src/AnalyzeView/LogDownloadPage.qml b/src/AnalyzeView/OnboardLogs/OnboardLogPage.qml similarity index 69% rename from src/AnalyzeView/LogDownloadPage.qml rename to src/AnalyzeView/OnboardLogs/OnboardLogPage.qml index bdd15f03c426..7f253f0ff152 100644 --- a/src/AnalyzeView/LogDownloadPage.qml +++ b/src/AnalyzeView/OnboardLogs/OnboardLogPage.qml @@ -7,9 +7,9 @@ import QGroundControl import QGroundControl.Controls AnalyzePage { - id: logDownloadPage + id: onboardLogPage pageComponent: pageComponent - pageDescription: qsTr("Log Download allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") + pageDescription: qsTr("Onboard Logs allows you to download binary log files from your vehicle. Click Refresh to get list of available logs.") Component { id: pageComponent @@ -18,7 +18,7 @@ AnalyzePage { width: availableWidth height: availableHeight - Component.onCompleted: LogDownloadController.refresh() + Component.onCompleted: OnboardLogController.refresh() QGCFlickable { Layout.fillWidth: true @@ -28,7 +28,7 @@ AnalyzePage { GridLayout { id: gridLayout - rows: LogDownloadController.model.count + 1 + rows: OnboardLogController.model.count + 1 columns: 5 flow: GridLayout.TopToBottom columnSpacing: ScreenTools.defaultFontPixelWidth @@ -40,7 +40,7 @@ AnalyzePage { } Repeater { - model: LogDownloadController.model + model: OnboardLogController.model QGCCheckBox { Binding on checkState { @@ -54,7 +54,7 @@ AnalyzePage { QGCLabel { text: qsTr("Id") } Repeater { - model: LogDownloadController.model + model: OnboardLogController.model QGCLabel { text: object.id } } @@ -62,7 +62,7 @@ AnalyzePage { QGCLabel { text: qsTr("Date") } Repeater { - model: LogDownloadController.model + model: OnboardLogController.model QGCLabel { text: { @@ -82,7 +82,7 @@ AnalyzePage { QGCLabel { text: qsTr("Size") } Repeater { - model: LogDownloadController.model + model: OnboardLogController.model QGCLabel { text: object.sizeStr } } @@ -90,7 +90,7 @@ AnalyzePage { QGCLabel { text: qsTr("Status") } Repeater { - model: LogDownloadController.model + model: OnboardLogController.model QGCLabel { text: object.status } } @@ -104,40 +104,40 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs text: qsTr("Refresh") onClicked: { if (!QGroundControl.multiVehicleManager.activeVehicle || QGroundControl.multiVehicleManager.activeVehicle.isOfflineEditingVehicle) { - QGroundControl.showMessageDialog(logDownloadPage, qsTr("Log Refresh"), qsTr("You must be connected to a vehicle in order to download logs.")) + QGroundControl.showMessageDialog(onboardLogPage, qsTr("Onboard Log Refresh"), qsTr("You must be connected to a vehicle in order to download onboard logs.")) return } - LogDownloadController.refresh() + OnboardLogController.refresh() } } QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs text: qsTr("Download") onClicked: { var logsSelected = false - for (var i = 0; i < LogDownloadController.model.count; i++) { - if (LogDownloadController.model.get(i).selected) { + for (var i = 0; i < OnboardLogController.model.count; i++) { + if (OnboardLogController.model.get(i).selected) { logsSelected = true break } } if (!logsSelected) { - QGroundControl.showMessageDialog(logDownloadPage, qsTr("Log Download"), qsTr("You must select at least one log file to download.")) + QGroundControl.showMessageDialog(onboardLogPage, qsTr("Onboard Log"), qsTr("You must select at least one onboard log file to download.")) return } if (ScreenTools.isMobile) { - LogDownloadController.download() + OnboardLogController.download() return } @@ -150,7 +150,7 @@ AnalyzePage { QGCFileDialog { id: fileDialog onAcceptedForLoad: (file) => { - LogDownloadController.download(file) + OnboardLogController.download(file) close() } } @@ -158,22 +158,22 @@ AnalyzePage { QGCButton { Layout.fillWidth: true - enabled: !LogDownloadController.requestingList && !LogDownloadController.downloadingLogs && (LogDownloadController.model.count > 0) + enabled: !OnboardLogController.requestingList && !OnboardLogController.downloadingLogs && (OnboardLogController.model.count > 0) text: qsTr("Erase All") onClicked: QGroundControl.showMessageDialog( - logDownloadPage, - qsTr("Delete All Log Files"), - qsTr("All log files will be erased permanently. Is this really what you want?"), + onboardLogPage, + qsTr("Delete All Onboard Log Files"), + qsTr("All onboard log files will be erased permanently. Is this really what you want?"), Dialog.Yes | Dialog.No, - function() { LogDownloadController.eraseAll() } + function() { OnboardLogController.eraseAll() } ) } QGCButton { Layout.fillWidth: true text: qsTr("Cancel") - enabled: LogDownloadController.requestingList || LogDownloadController.downloadingLogs - onClicked: LogDownloadController.cancel() + enabled: OnboardLogController.requestingList || OnboardLogController.downloadingLogs + onClicked: OnboardLogController.cancel() } } } diff --git a/src/AnalyzeView/VibrationPage.qml b/src/AnalyzeView/Vibration/VibrationPage.qml similarity index 100% rename from src/AnalyzeView/VibrationPage.qml rename to src/AnalyzeView/Vibration/VibrationPage.qml diff --git a/src/AnalyzeView/VibrationPageIcon.png b/src/AnalyzeView/Vibration/VibrationPageIcon.png similarity index 100% rename from src/AnalyzeView/VibrationPageIcon.png rename to src/AnalyzeView/Vibration/VibrationPageIcon.png diff --git a/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml b/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml index 790dabe2d7fe..48495cf8216b 100644 --- a/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMAirframeComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth APMAirframeComponentController {id: controller; } @@ -14,8 +17,9 @@ Item { property Fact _frameType: controller.getParameterFact(-1, "FRAME_TYPE", false) property bool _frameTypeAvailable: controller.parameterExists(-1, "FRAME_TYPE") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Frame Class") diff --git a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc index cfb238e42d25..be615cf0858d 100644 --- a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc +++ b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.cc @@ -5,6 +5,7 @@ #include "APMHeliComponent.h" #include "APMLightsComponent.h" #include "APMMotorComponent.h" +#include "APMServoComponent.h" #include "APMPowerComponent.h" #include "APMRadioComponent.h" #include "APMRemoteSupportComponent.h" @@ -19,6 +20,7 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "VehicleComponent.h" #ifdef QT_DEBUG #include "APMFollowComponent.h" @@ -55,7 +57,7 @@ const QVariantList &APMAutoPilotPlugin::vehicleComponents() _airframeComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_airframeComponent))); - if (_vehicle->supportsRadio()) { + if (_vehicle->supports()->radio()) { _radioComponent = new APMRadioComponent(_vehicle, this); _radioComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_radioComponent))); @@ -82,6 +84,12 @@ const QVariantList &APMAutoPilotPlugin::vehicleComponents() _components.append(QVariant::fromValue(qobject_cast(_motorComponent))); } + if (_vehicle->parameterManager()->parameterExists(-1, QStringLiteral("SERVO1_MIN"))) { + _servoComponent = new APMServoComponent(_vehicle, this); + _servoComponent->setupTriggerSignals(); + _components.append(QVariant::fromValue(qobject_cast(_servoComponent))); + } + _safetyComponent = new APMSafetyComponent(_vehicle, this); _safetyComponent->setupTriggerSignals(); _components.append(QVariant::fromValue(qobject_cast(_safetyComponent))); diff --git a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h index bfeda75863c3..f1d1c51a8dd9 100644 --- a/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h +++ b/src/AutoPilotPlugins/APM/APMAutoPilotPlugin.h @@ -14,6 +14,7 @@ class APMMotorComponent; class APMGimbalComponent; class APMLightsComponent; class APMSubFrameComponent; +class APMServoComponent; class ESP8266Component; class APMHeliComponent; class APMRemoteSupportComponent; @@ -43,6 +44,7 @@ class APMAutoPilotPlugin : public AutoPilotPlugin APMLightsComponent *_lightsComponent = nullptr; APMSubFrameComponent *_subFrameComponent = nullptr; APMFlightModesComponent *_flightModesComponent = nullptr; + APMServoComponent *_servoComponent = nullptr; APMPowerComponent *_powerComponent = nullptr; APMMotorComponent *_motorComponent = nullptr; APMRadioComponent *_radioComponent = nullptr; diff --git a/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml b/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml index dcb250d4cf70..cfcdf8cf7d61 100644 --- a/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml +++ b/src/AutoPilotPlugins/APM/APMFlightModesComponent.qml @@ -204,7 +204,7 @@ SetupPage { FactComboBox { id: optCombo width: ScreenTools.defaultFontPixelWidth * 15 - fact: controller.getParameterFact(-1, "r.RC" + index + "_OPTION") + fact: controller.getParameterFact(-1, "RC" + index + "_OPTION") indexModel: false } } diff --git a/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml b/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml index edfbf81327ac..109b1d930254 100644 --- a/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMFlightModesComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact flightMode5: controller.getParameterFact(-1, _roverFirmware ? "MODE5" : "FLTMODE5") property Fact flightMode6: controller.getParameterFact(-1, _roverFirmware ? "MODE6" : "FLTMODE6") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Flight Mode 1") diff --git a/src/AutoPilotPlugins/APM/APMFollowComponent.qml b/src/AutoPilotPlugins/APM/APMFollowComponent.qml index 346e2dbcc939..ef1c53eaf556 100644 --- a/src/AutoPilotPlugins/APM/APMFollowComponent.qml +++ b/src/AutoPilotPlugins/APM/APMFollowComponent.qml @@ -6,6 +6,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls +import QGroundControl.PlanView SetupPage { id: followPage @@ -367,7 +368,7 @@ SetupPage { transform: Rotation { origin.x: vehicleIcon.width / 2 origin.y: vehicleIcon.height / 2 - angle: _roverFirmware ? 0 : + angle: _roverFirmware || !_followYawBehavior ? 0 : (_followYawBehavior.rawValue == _followYawBehaviorNone ? 0 : (_followYawBehavior.rawValue == _followYawBehaviorFace ? diff --git a/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml b/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml index 22f0d36ea80b..9b253fe49b7f 100644 --- a/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMFollowComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller } @@ -28,8 +31,9 @@ Item { { label: qsTr("Yaw Behavior"), fact: getFact("FOLL_YAW_BEHAVE"), visible: followParamsAvailable } ] - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 Repeater { model: followItems diff --git a/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml b/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml index 3dd830eb7e1a..aecdf55cc397 100644 --- a/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMLightsComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -81,8 +84,9 @@ Item { property int lights2Function: _rcFunctionRCIN10 } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Lights Output 1") diff --git a/src/AutoPilotPlugins/APM/APMPowerComponent.qml b/src/AutoPilotPlugins/APM/APMPowerComponent.qml index 8452c5e5bce3..a989498aeeaf 100644 --- a/src/AutoPilotPlugins/APM/APMPowerComponent.qml +++ b/src/AutoPilotPlugins/APM/APMPowerComponent.qml @@ -111,8 +111,8 @@ SetupPage { anchors.left: parent.left sourceComponent: _batt1FullSettings.visible ? powerSetupComponent : undefined - property Fact armVoltMin: controller.getParameterFact(-1, "r.BATT_ARM_VOLT", false /* reportMissing */) - property Fact battAmpPerVolt: controller.getParameterFact(-1, "r.BATT_AMP_PERVLT", false /* reportMissing */) + property Fact armVoltMin: controller.getParameterFact(-1, "BATT_ARM_VOLT", false /* reportMissing */) + property Fact battAmpPerVolt: controller.getParameterFact(-1, "BATT_AMP_PERVLT", false /* reportMissing */) property Fact battAmpOffset: controller.getParameterFact(-1, "BATT_AMP_OFFSET", false /* reportMissing */) property Fact battCapacity: controller.getParameterFact(-1, "BATT_CAPACITY", false /* reportMissing */) property Fact battCurrPin: controller.getParameterFact(-1, "BATT_CURR_PIN", false /* reportMissing */) @@ -198,8 +198,8 @@ SetupPage { anchors.left: parent.left sourceComponent: batt2FullSettings.visible ? powerSetupComponent : undefined - property Fact armVoltMin: controller.getParameterFact(-1, "r.BATT2_ARM_VOLT", false /* reportMissing */) - property Fact battAmpPerVolt: controller.getParameterFact(-1, "r.BATT2_AMP_PERVLT", false /* reportMissing */) + property Fact armVoltMin: controller.getParameterFact(-1, "BATT2_ARM_VOLT", false /* reportMissing */) + property Fact battAmpPerVolt: controller.getParameterFact(-1, "BATT2_AMP_PERVLT", false /* reportMissing */) property Fact battAmpOffset: controller.getParameterFact(-1, "BATT2_AMP_OFFSET", false /* reportMissing */) property Fact battCapacity: controller.getParameterFact(-1, "BATT2_CAPACITY", false /* reportMissing */) property Fact battCurrPin: controller.getParameterFact(-1, "BATT2_CURR_PIN", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml b/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml index 6ad00799681f..dc6e21bb1d35 100644 --- a/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMPowerComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -19,8 +22,9 @@ Item { property Fact _batt2Capacity: controller.getParameterFact(-1, "BATT2_CAPACITY", false /* reportMissing */) property bool _battCapacityAvailable: controller.parameterExists(-1, "BATT_CAPACITY") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Batt1 monitor") diff --git a/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml b/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml index 4df3795f4fcb..6618b48ed8f6 100644 --- a/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMRadioComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -15,8 +18,9 @@ Item { property Fact mapYawFact: controller.getParameterFact(-1, "RCMAP_YAW") property Fact mapThrottleFact: controller.getParameterFact(-1, "RCMAP_THROTTLE") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Roll") diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponent.qml b/src/AutoPilotPlugins/APM/APMSafetyComponent.qml index 29ac36b6b2b9..cf65e704a688 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponent.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponent.qml @@ -325,9 +325,9 @@ SetupPage { spacing: _margins / 2 property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") - property Fact _failsafeBattLowAct: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false /* reportMissing */) - property Fact _failsafeBattMah: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false /* reportMissing */) - property Fact _failsafeBattVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false /* reportMissing */) + property Fact _failsafeBattLowAct: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false /* reportMissing */) + property Fact _failsafeBattMah: controller.getParameterFact(-1, "BATT_LOW_MAH", false /* reportMissing */) + property Fact _failsafeBattVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false /* reportMissing */) property Fact _failsafeThrEnable: controller.getParameterFact(-1, "FS_THR_ENABLE") property Fact _failsafeThrValue: controller.getParameterFact(-1, "FS_THR_VALUE") @@ -524,10 +524,12 @@ SetupPage { Column { spacing: _margins / 2 - property Fact _landSpeedFact: controller.getParameterFact(-1, "LAND_SPEED") - property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALT") + property Fact _landSpeedFact: controller.getParameterFact(-1, "LAND_SPD_MS") + property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALT_M") property Fact _rtlLoitTimeFact: controller.getParameterFact(-1, "RTL_LOIT_TIME") - property Fact _rtlAltFinalFact: controller.getParameterFact(-1, "RTL_ALT_FINAL") + property Fact _rtlAltFinalFact: controller.getParameterFact(-1, "RTL_ALT_FINAL_M") + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + property bool _rtlAltIsMeters: controller.parameterExists(-1, "noremap.RTL_ALT_M") QGCLabel { id: rtlLabel @@ -575,7 +577,8 @@ SetupPage { text: qsTr("Return at specified altitude:") checked: _rtlAltFact.value != 0 - onClicked: _rtlAltFact.value = 1500 + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + onClicked: _rtlAltFact.value = _rtlAltIsMeters ? 15 : 1500 } FactTextField { @@ -651,7 +654,7 @@ SetupPage { Column { spacing: _margins / 2 - property Fact _rtlAltFact: controller.getParameterFact(-1, "r.RTL_ALTITUDE") + property Fact _rtlAltFact: controller.getParameterFact(-1, "RTL_ALTITUDE") QGCLabel { text: qsTr("Return to Launch") diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml index d43c1f97b604..259373685437 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSub.qml @@ -24,7 +24,7 @@ SetupPage { property bool _firmware34: globals.activeVehicle.versionCompare(3, 5, 0) < 0 // Enable/Action parameters - property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false) + property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false) property Fact _failsafeEKFEnable: controller.getParameterFact(-1, "FS_EKF_ACTION") property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") property Fact _failsafeLeakEnable: controller.getParameterFact(-1, "FS_LEAK_ENABLE") @@ -39,9 +39,9 @@ SetupPage { property Fact _failsafeLeakPin: controller.getParameterFact(-1, "LEAK1_PIN") property Fact _failsafeLeakLogic: controller.getParameterFact(-1, "LEAK1_LOGIC") property Fact _failsafeEKFThreshold: controller.getParameterFact(-1, "FS_EKF_THRESH") - property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false) - property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false) - property bool _batteryDetected: controller.parameterExists(-1, "r.BATT_LOW_MAH") + property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false) + property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "BATT_LOW_MAH", false) + property bool _batteryDetected: controller.parameterExists(-1, "BATT_LOW_MAH") // Older firmwares use ARMING_CHECK. Newer firmwares use ARMING_SKIPCHK. property Fact _armingCheck: controller.getParameterFact(-1, "ARMING_CHECK", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml index bba18a214385..3c3583a83696 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,7 +23,7 @@ Item { property bool _batt1MonitorEnabled: _batt1Monitor.rawValue !== 0 property bool _batt2MonitorEnabled: _batt2MonitorAvailable && _batt2Monitor.rawValue !== 0 - property Fact _batt1FSLowAct: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false /* reportMissing */) + property Fact _batt1FSLowAct: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false /* reportMissing */) property Fact _batt1FSCritAct: controller.getParameterFact(-1, "BATT_FS_CRT_ACT", false /* reportMissing */) property Fact _batt2FSLowAct: controller.getParameterFact(-1, "BATT2_FS_LOW_ACT", false /* reportMissing */) property Fact _batt2FSCritAct: controller.getParameterFact(-1, "BATT2_FS_CRT_ACT", false /* reportMissing */) @@ -29,8 +32,9 @@ Item { property bool _roverFirmware: controller.parameterExists(-1, "MODE1") // This catches all usage of ArduRover firmware vehicle types: Rover, Boat... - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Arming Checks:") @@ -146,7 +150,7 @@ Item { valueText: fact ? (fact.value == 0 ? qsTr("current") : fact.valueString + " " + fact.units) : "" visible: controller.vehicle.multiRotor - property Fact fact: controller.getParameterFact(-1, "RTL_ALT", false /* reportMissing */) + property Fact fact: controller.getParameterFact(-1, "RTL_ALT_M", false /* reportMissing */) } VehicleSummaryRow { @@ -154,7 +158,7 @@ Item { valueText: fact ? (fact.value < 0 ? qsTr("current") : fact.valueString + " " + fact.units) : "" visible: controller.vehicle.fixedWing - property Fact fact: controller.getParameterFact(-1, "r.RTL_ALTITUDE", false /* reportMissing */) + property Fact fact: controller.getParameterFact(-1, "RTL_ALTITUDE", false /* reportMissing */) } } } diff --git a/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml b/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml index 9e914cda8409..2bd81dc5646e 100644 --- a/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml +++ b/src/AutoPilotPlugins/APM/APMSafetyComponentSummarySub.qml @@ -13,7 +13,7 @@ Item { FactPanelController { id: controller; } // Enable/Action parameters - property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "r.BATT_FS_LOW_ACT", false) + property Fact _failsafeBatteryEnable: controller.getParameterFact(-1, "BATT_FS_LOW_ACT", false) property Fact _failsafeEKFEnable: controller.getParameterFact(-1, "FS_EKF_ACTION") property Fact _failsafeGCSEnable: controller.getParameterFact(-1, "FS_GCS_ENABLE") property Fact _failsafeLeakEnable: controller.getParameterFact(-1, "FS_LEAK_ENABLE") @@ -28,8 +28,8 @@ Item { property Fact _failsafeLeakPin: controller.getParameterFact(-1, "LEAK1_PIN") property Fact _failsafeLeakLogic: controller.getParameterFact(-1, "LEAK1_LOGIC") property Fact _failsafeEKFThreshold: controller.getParameterFact(-1, "FS_EKF_THRESH") - property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "r.BATT_LOW_VOLT", false) - property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "r.BATT_LOW_MAH", false) + property Fact _failsafeBatteryVoltage: controller.getParameterFact(-1, "BATT_LOW_VOLT", false) + property Fact _failsafeBatteryCapacity: controller.getParameterFact(-1, "BATT_LOW_MAH", false) // Older firmwares use ARMING_CHECK. Newer firmwares use ARMING_SKIPCHK. property Fact _armingCheck: controller.getParameterFact(-1, "ARMING_CHECK", false /* reportMissing */) diff --git a/src/AutoPilotPlugins/APM/APMSensorsComponent.qml b/src/AutoPilotPlugins/APM/APMSensorsComponent.qml index 644bcd0f553b..a7af794599d8 100644 --- a/src/AutoPilotPlugins/APM/APMSensorsComponent.qml +++ b/src/AutoPilotPlugins/APM/APMSensorsComponent.qml @@ -738,7 +738,7 @@ SetupPage { QGCButton { width: _buttonWidth text: qsTr("CompassMot") - visible: globals.activeVehicle ? globals.activeVehicle.supportsMotorInterference : false + visible: globals.activeVehicle ? globals.activeVehicle.supports.motorInterference : false onClicked: compassMotDialogFactory.open() } diff --git a/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml index ed470b85f4a0..c4f07c75dc94 100644 --- a/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSensorsComponentSummary.qml @@ -11,7 +11,9 @@ import QGroundControl.Controls */ Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth APMSensorsComponentController { id: controller; } @@ -20,8 +22,9 @@ Item { factPanelController: controller } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Compasses:") @@ -77,7 +80,7 @@ Item { model: sensorParams.rgInsId.length APMSensorIdDecoder { fact: sensorParams.rgInsId[index] - anchors.right: parent.right + Layout.alignment: Qt.AlignRight } } @@ -90,7 +93,7 @@ Item { model: sensorParams.rgBaroId.length APMSensorIdDecoder { fact: sensorParams.rgBaroId[index] - anchors.right: parent.right + Layout.alignment: Qt.AlignRight } } } diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.cc b/src/AutoPilotPlugins/APM/APMServoComponent.cc new file mode 100644 index 000000000000..2b0601479fdc --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.cc @@ -0,0 +1,11 @@ +#include "APMServoComponent.h" + +APMServoComponent::APMServoComponent(Vehicle *vehicle, AutoPilotPlugin *autopilot, QObject *parent) + : VehicleComponent(vehicle, autopilot, AutoPilotPlugin::UnknownVehicleComponent, parent) +{ +} + +QUrl APMServoComponent::setupSource() const +{ + return QUrl::fromUserInput(QStringLiteral("qrc:/qml/QGroundControl/AutoPilotPlugins/APM/APMServoComponent.qml")); +} diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.h b/src/AutoPilotPlugins/APM/APMServoComponent.h new file mode 100644 index 000000000000..b3a43d6e2289 --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.h @@ -0,0 +1,24 @@ +#pragma once + +#include "VehicleComponent.h" + +class APMServoComponent : public VehicleComponent +{ + Q_OBJECT + +public: + explicit APMServoComponent(Vehicle *vehicle, AutoPilotPlugin *autopilot, QObject *parent = nullptr); + + // VehicleComponent overrides + QString name() const final { return _name; } + QString description() const final { return tr("Configure servo PWM limits, trim, direction, and function."); } + QString iconResource() const final { return QStringLiteral("/qmlimages/MotorComponentIcon.svg"); } + bool requiresSetup() const final { return false; } + bool setupComplete() const final { return true; } + QUrl setupSource() const final; + QUrl summaryQmlSource() const final { return QUrl(); } + QStringList setupCompleteChangedTriggerList() const final { return QStringList(); } + +private: + const QString _name = tr("Servo Outputs"); +}; diff --git a/src/AutoPilotPlugins/APM/APMServoComponent.qml b/src/AutoPilotPlugins/APM/APMServoComponent.qml new file mode 100644 index 000000000000..27285c506dba --- /dev/null +++ b/src/AutoPilotPlugins/APM/APMServoComponent.qml @@ -0,0 +1,398 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +SetupPage { + id: servoPage + pageComponent: pageComponent + showAdvanced: false + + readonly property int _maxServos: 16 + readonly property real _margins: ScreenTools.defaultFontPixelHeight * 0.5 + + FactPanelController { + id: controller + } + + ServoOutputMonitorController { + id: servoMonitor + } + + QGCPalette { + id: qgcPal + colorGroupEnabled: true + } + + // Keep the "Position (us)" bar a stable width so the table doesn't reflow. + readonly property int _positionBarWidth: ScreenTools.defaultFontPixelWidth * 10 + + function getFact(param) { + return controller.parameterExists(-1, param) + ? controller.getParameterFact(-1, param, false) + : null + } + + function servoExists(n) { + return controller.parameterExists(-1, "SERVO" + n + "_FUNCTION") + } + + Component { + id: pageComponent + + Column { + width: availableWidth + spacing: _margins + + QGCLabel { + text: qsTr("Configure ArduPilot servo outputs.") + wrapMode: Text.WordWrap + width: parent.width + } + + QGCGroupBox { + title: qsTr("Servo Outputs") + + GridLayout { + columns: 7 + rowSpacing: ScreenTools.defaultFontPixelHeight * 0.3 + columnSpacing: ScreenTools.defaultFontPixelWidth * 2 + + // --- Headers (all explicitly in row 0) ----------------- + QGCLabel { text: qsTr("Servo"); font.bold: true; Layout.row: 0; Layout.column: 0 } + QGCLabel { + text: qsTr("Position") + font.bold: true + Layout.row: 0 + Layout.column: 1 + Layout.preferredWidth: _positionBarWidth + Layout.fillWidth: false + } + QGCLabel { text: qsTr("Function"); font.bold: true; Layout.row: 0; Layout.column: 2 } + QGCLabel { text: qsTr("Min"); font.bold: true; Layout.row: 0; Layout.column: 3 } + QGCLabel { text: qsTr("Trim"); font.bold: true; Layout.row: 0; Layout.column: 4 } + QGCLabel { text: qsTr("Max"); font.bold: true; Layout.row: 0; Layout.column: 5 } + QGCLabel { text: qsTr("Reversed"); font.bold: true; Layout.row: 0; Layout.column: 6 } + + // --- Column 0: Servo number ---------------------------- + Repeater { + model: _maxServos + QGCLabel { + text: index + 1 + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 0 + } + } + + // --- Column 1: Position -------------------------------- + Repeater { + id: positionRepeater + model: _maxServos + Item { + readonly property int _servoIndex: index + 1 + readonly property var _minFact: getFact("SERVO" + _servoIndex + "_MIN") + readonly property var _maxFact: getFact("SERVO" + _servoIndex + "_MAX") + + property int pwmValue: servoMonitor.servoValue(index) + readonly property double _rawValue: pwmValue >= 0 ? pwmValue : NaN + readonly property double _minValue: _minFact ? _minFact.value : NaN + readonly property double _maxValue: _maxFact ? _maxFact.value : NaN + + readonly property double _range: _maxValue - _minValue + readonly property double _ratio: (_range > 0 && !isNaN(_rawValue) && !isNaN(_minValue)) + ? Math.max(0, Math.min(1, (_rawValue - _minValue) / _range)) + : 0 + + height: ScreenTools.defaultFontPixelHeight * 0.95 + Layout.preferredWidth: _positionBarWidth + Layout.fillWidth: false + width: _positionBarWidth + visible: servoExists(_servoIndex) + Layout.row: index + 1 + Layout.column: 1 + + readonly property color _trackColor: qgcPal.colorGrey + // Use a themed accent so the fill is always clearly distinct from the track + readonly property color _progressColor: qgcPal.colorGreen + + readonly property bool _hasValidValue: pwmValue >= 0 && !isNaN(_rawValue) && !isNaN(_minValue) && !isNaN(_maxValue) && _range > 0 + // Use the delegate width (cell width), not `parent.width` (Repeater/GridLayout width). + readonly property real _progressWidth: _hasValidValue ? Math.max(0, _ratio * width) : 0 + + // Keep the delegate's implicit width stable so GridLayout doesn't reflow + // based on changing label text lengths. + implicitWidth: 0 + + Rectangle { + anchors.fill: parent + id: track + color: _trackColor + opacity: 0.45 + border.width: 1 + border.color: qgcPal.text + radius: ScreenTools.defaultBorderRadius + } + + Rectangle { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + width: _progressWidth + color: _progressColor + radius: ScreenTools.defaultBorderRadius + } + + QGCLabel { + id: valueLabel + z: 1 + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: _hasValidValue ? Math.round(_rawValue) : "-" + font.bold: true + color: qgcPal.text + width: parent.width + } + } + } + + Connections { + target: servoMonitor + function onServoValueChanged(servo, pwmValue) { + const item = positionRepeater.itemAt(servo) + if (item) { + item.pwmValue = pwmValue + } + } + } + + // --- Column 2: Function -------------------------------- + Repeater { + model: _maxServos + FactComboBox { + fact: getFact("SERVO" + (index + 1) + "_FUNCTION") + indexModel: false + sizeToContents: true + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 2 + } + } + + // --- Column 3: Min --------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 3 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_MIN") + property int _repeatDir: 0 + + Timer { + id: repeatInitial + interval: 350 + repeat: false + running: false + onTriggered: repeatTimer.start() + } + + Timer { + id: repeatTimer + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitial.start() + } + onReleased: { + _repeatDir = 0 + repeatInitial.stop() + repeatTimer.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitial.start() + } + onReleased: { + _repeatDir = 0 + repeatInitial.stop() + repeatTimer.stop() + } + } + } + } + + // --- Column 4: Trim -------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 4 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_TRIM") + property int _repeatDir: 0 + + Timer { + id: repeatInitialTrim + interval: 350 + repeat: false + running: false + onTriggered: repeatTimerTrim.start() + } + + Timer { + id: repeatTimerTrim + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitialTrim.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialTrim.stop() + repeatTimerTrim.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitialTrim.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialTrim.stop() + repeatTimerTrim.stop() + } + } + } + } + + // --- Column 5: Max --------------------------------------- + Repeater { + model: _maxServos + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 5 + + readonly property var spFact: getFact("SERVO" + (index + 1) + "_MAX") + property int _repeatDir: 0 + + Timer { + id: repeatInitialMax + interval: 350 + repeat: false + running: false + onTriggered: repeatTimerMax.start() + } + + Timer { + id: repeatTimerMax + interval: 80 + repeat: true + running: false + onTriggered: if (spFact && _repeatDir !== 0) { spFact.value += _repeatDir } + } + + QGCButton { + text: "-" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = -1 + spFact.value -= 1 + repeatInitialMax.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialMax.stop() + repeatTimerMax.stop() + } + } + FactTextField { fact: spFact; showUnits: false; Layout.fillWidth: true } + QGCButton { + text: "+" + onPressed: { + if (!spFact) { + _repeatDir = 0 + return + } + _repeatDir = 1 + spFact.value += 1 + repeatInitialMax.start() + } + onReleased: { + _repeatDir = 0 + repeatInitialMax.stop() + repeatTimerMax.stop() + } + } + } + } + + // --- Column 6: Reversed --------------------------------- + Repeater { + model: _maxServos + FactCheckBox { + fact: getFact("SERVO" + (index + 1) + "_REVERSED") + visible: servoExists(index + 1) + Layout.row: index + 1 + Layout.column: 6 + } + } + } + } + } + } +} diff --git a/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml b/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml index c0261892f00f..8c871aec0eaa 100644 --- a/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml +++ b/src/AutoPilotPlugins/APM/APMSubFrameComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -35,8 +38,10 @@ Item { } } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { id: nameRow; labelText: qsTr("Frame Type") diff --git a/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml b/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml index cdcd618815c5..64f884d775a1 100644 --- a/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml +++ b/src/AutoPilotPlugins/APM/APMTuningComponentCopter.qml @@ -24,17 +24,17 @@ SetupPage { property Fact _rateRollI: controller.getParameterFact(-1, "ATC_RAT_RLL_I") property Fact _ratePitchP: controller.getParameterFact(-1, "ATC_RAT_PIT_P") property Fact _ratePitchI: controller.getParameterFact(-1, "ATC_RAT_PIT_I") - property Fact _rateClimbP: controller.getParameterFact(-1, "PSC_ACCZ_P") - property Fact _rateClimbI: controller.getParameterFact(-1, "PSC_ACCZ_I") + property Fact _rateClimbP: controller.getParameterFact(-1, "PSC_D_ACC_P") + property Fact _rateClimbI: controller.getParameterFact(-1, "PSC_D_ACC_I") property Fact _motSpinArm: controller.getParameterFact(-1, "MOT_SPIN_ARM") property Fact _motSpinMin: controller.getParameterFact(-1, "MOT_SPIN_MIN") - property Fact _ch7Opt: controller.getParameterFact(-1, "r.RC7_OPTION") - property Fact _ch8Opt: controller.getParameterFact(-1, "r.RC8_OPTION") - property Fact _ch9Opt: controller.getParameterFact(-1, "r.RC9_OPTION") - property Fact _ch10Opt: controller.getParameterFact(-1, "r.RC10_OPTION") - property Fact _ch11Opt: controller.getParameterFact(-1, "r.RC11_OPTION") - property Fact _ch12Opt: controller.getParameterFact(-1, "r.RC12_OPTION") + property Fact _ch7Opt: controller.getParameterFact(-1, "RC7_OPTION") + property Fact _ch8Opt: controller.getParameterFact(-1, "RC8_OPTION") + property Fact _ch9Opt: controller.getParameterFact(-1, "RC9_OPTION") + property Fact _ch10Opt: controller.getParameterFact(-1, "RC10_OPTION") + property Fact _ch11Opt: controller.getParameterFact(-1, "RC11_OPTION") + property Fact _ch12Opt: controller.getParameterFact(-1, "RC12_OPTION") readonly property int _firstOptionChannel: 7 readonly property int _lastOptionChannel: 12 @@ -59,7 +59,7 @@ SetupPage { function calcAutoTuneChannel() { _autoTuneSwitchChannelIndex = 0 for (var channel=_firstOptionChannel; channel<=_lastOptionChannel; channel++) { - var optionFact = controller.getParameterFact(-1, "r.RC" + channel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + channel + "_OPTION") if (optionFact.value == _autoTuneOption) { _autoTuneSwitchChannelIndex = channel - _firstOptionChannel + 1 break @@ -71,7 +71,7 @@ SetupPage { function setChannelAutoTuneOption(channel) { // First clear any previous settings for AutTune for (var optionChannel=_firstOptionChannel; optionChannel<=_lastOptionChannel; optionChannel++) { - var optionFact = controller.getParameterFact(-1, "r.RC" + optionChannel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + optionChannel + "_OPTION") if (optionFact.value == _autoTuneOption) { optionFact.value = 0 } @@ -79,7 +79,7 @@ SetupPage { // Now set the function into the new channel if (channel != 0) { - var optionFact = controller.getParameterFact(-1, "r.RC" + channel + "_OPTION") + var optionFact = controller.getParameterFact(-1, "RC" + channel + "_OPTION") optionFact.value = _autoTuneOption } } @@ -239,14 +239,14 @@ SetupPage { id: tuneMinField textField.validator: DoubleValidator {bottom: 0; top: 32767;} label: qsTr("Min:") - fact: controller.getParameterFact(-1, "r.TUNE_MIN") + fact: controller.getParameterFact(-1, "TUNE_MIN") } LabelledFactTextField { id: tuneMaxField textField.validator: DoubleValidator {bottom: 0; top: 32767;} label: qsTr("Max:") - fact: controller.getParameterFact(-1, "r.TUNE_MAX") + fact: controller.getParameterFact(-1, "TUNE_MAX") } } } diff --git a/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml b/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml index 2ba07fa15a6e..6e359012579a 100644 --- a/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml +++ b/src/AutoPilotPlugins/APM/APMTuningComponentSub.qml @@ -100,17 +100,20 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_D") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FILT") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_POSXY_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_POSZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELXY_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_VELZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_D") } + FactTextFieldSlider2 { + visible: controller.parameterExists(-1, "PSC_ACCZ_FILT") + fact: visible ? controller.getParameterFact(-1, "PSC_ACCZ_FILT") : null + } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_ACCZ_P") } } // Column - VEL parameters } @@ -123,19 +126,19 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_POSZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELXY_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_VELZ_P") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_D") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTD") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTE") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_FLTT") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_I") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_IMAX") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "r.PSC_ACCZ_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_POS_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_POS_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_NE_VEL_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_VEL_P") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_D") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTD") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTE") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_FLTT") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_I") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_IMAX") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "PSC_D_ACC_P") } } // Column - VEL parameters } @@ -191,12 +194,12 @@ SetupPage { anchors.top: parent.top spacing: _margins*1.5 - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_ACCEL") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_ACCEL_Z") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_RADIUS") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED_DN") } - FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WPNAV_SPEED_UP") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_ACC") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_ACC_Z") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_RADIUS_M") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD_DN") } + FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "WP_SPD_UP") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_SPEED") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_ACC_MAX") } FactTextFieldSlider2 { fact: controller.getParameterFact(-1, "LOIT_ANG_MAX") } diff --git a/src/AutoPilotPlugins/APM/CMakeLists.txt b/src/AutoPilotPlugins/APM/CMakeLists.txt index 7ce30f3f54dc..b213f76dc835 100644 --- a/src/AutoPilotPlugins/APM/CMakeLists.txt +++ b/src/AutoPilotPlugins/APM/CMakeLists.txt @@ -27,6 +27,8 @@ target_sources(${CMAKE_PROJECT_NAME} APMLightsComponent.h APMMotorComponent.cc APMMotorComponent.h + APMServoComponent.cc + APMServoComponent.h APMPowerComponent.cc APMPowerComponent.h APMRadioComponent.cc @@ -72,6 +74,7 @@ qt_add_qml_module(AutoPilotPluginsAPMModule APMLightsComponent.qml APMLightsComponentSummary.qml APMMotorComponent.qml + APMServoComponent.qml APMNotSupported.qml APMPowerComponent.qml APMPowerComponentSummary.qml diff --git a/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml b/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml index d49c407b7359..23485c204cf0 100644 --- a/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml +++ b/src/AutoPilotPlugins/Common/ESP8266ComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -21,8 +24,10 @@ Item { property Fact uartBaud: controller.getParameterFact(esp8266.componentID, "UART_BAUDRATE") property Fact wifiMode: controller.getParameterFact(esp8266.componentID, "WIFI_MODE", false) //-- Don't bitch if missing - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("Firmware Version") valueText: esp8266.version diff --git a/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml b/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml index b314c4471fd8..878a254e5c26 100644 --- a/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml +++ b/src/AutoPilotPlugins/Common/JoystickComponentSummary.qml @@ -1,17 +1,21 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth readonly property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle readonly property var _activeJoystick: joystickManager.activeJoystick - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Status") diff --git a/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml b/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml index 1df984dfab6d..c8e587012ee1 100644 --- a/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/AirframeComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth AirframeComponentController { id: controller; } @@ -15,8 +18,10 @@ Item { property bool autoStartSet: sysAutoStartFact ? (sysAutoStartFact.value !== 0) : false - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("System ID") valueText: sysIdFact ? sysIdFact.valueString : "" diff --git a/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml b/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml index d91b37a0046e..37ec0a18d685 100644 --- a/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml +++ b/src/AutoPilotPlugins/PX4/AirframeFactMetaData.xml @@ -175,7 +175,7 @@ Copter Beat Kueng <beat-kueng@gmx.net> Quadrotor x - https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini.html + https://docs.px4.io/main/en/frames_multicopter/holybro_qav250_pixhawk4_mini Copter @@ -270,6 +270,10 @@ + + Rover + Rover + Rover Rover @@ -277,7 +281,7 @@ Rover Rover - https://www.aionrobotics.com/r1 + https://docs.px4.io/main/en/complete_vehicles_rover/aion_r1 Rover @@ -288,23 +292,29 @@ Rover https://www.axialadventure.com/product/1-10-scx10-ii-trail-honcho-4wd-rock-crawler-brushed-rtr/AXID9059.html - + Rover Rover - + Rover - Rover - throttle - steering - - Rover - Katrin Moritz - Rover - Speed of left wheels - Steering servo + + + + Spacecraft + DISCOWER + Free-Flyer + https://atmos.discower.io + back left thruster, +x thrust + front left thruster, -x thrust + back right thruster, +x thrust + front right thruster, -x thrust + front left thruster, +y thrust + front right thruster, -y thrust + back left thruster, +y thrust + back right thruster, -y thrust @@ -343,12 +353,32 @@ elevon right elevon left + + VTOL + Simulation + MC motor front right + MC motor back left + MC motor front left + MC motor back right + Forward thrust motor + Ailerons (single channel) + Elevator + Rudder + VTOL Roman Bapst <roman@auterion.com> Standard VTOL + MC motor front right + MC motor back left + MC motor front left + MC motor back right + Forward thrust motor + Aileron + Elevator + Rudder VTOL @@ -372,10 +402,4 @@ VTOL Tiltrotor - - - Spacecraft - Free-Flyer - - diff --git a/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml b/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml index 815b5f1fe4bc..5fdd09f06bf3 100644 --- a/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/FlightModesComponentSummary.qml @@ -1,20 +1,25 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } property Fact _nullFact property Fact _rcMapFltmode: controller.parameterExists(-1, "RC_MAP_FLTMODE") ? controller.getParameterFact(-1, "RC_MAP_FLTMODE") : _nullFact - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 + VehicleSummaryRow { labelText: qsTr("Mode switch") valueText: _rcMapFltmode.value === 0 ? qsTr("Setup required") : _rcMapFltmode.enumStringValue diff --git a/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml b/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml index 73f62e09095a..3354dfc786a3 100644 --- a/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/PX4RadioComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -18,8 +21,9 @@ Item { property Fact mapAux1Fact: controller.getParameterFact(-1, "RC_MAP_AUX1") property Fact mapAux2Fact: controller.getParameterFact(-1, "RC_MAP_AUX2") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Roll") diff --git a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml index e02375611953..08b7e2751f4d 100644 --- a/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/PowerComponentSummary.qml @@ -1,44 +1,78 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth property string _naString: qsTr("N/A") FactPanelController { id: controller; } - BatteryParams { - id: battParams - controller: controller - batteryIndex: 1 + property int _indexedBatteryParamCount: { + var batteryIndex = 1 + while (controller.parameterExists(-1, "BAT" + batteryIndex + "_SOURCE")) { + batteryIndex++ + } + return batteryIndex - 1 } - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 - VehicleSummaryRow { - labelText: qsTr("Battery Source") - valueText: battParams.battSource.enumStringValue - } + Repeater { + model: _indexedBatteryParamCount - VehicleSummaryRow { - labelText: qsTr("Battery Full") - valueText: battParams.battHighVoltAvailable ? battParams.battHighVolt.valueString + " " + battParams.battHighVolt.units : _naString - } + Loader { + sourceComponent: batterySummaryComponent - VehicleSummaryRow { - labelText: qsTr("Battery Empty") - valueText: battParams.battLowVoltAvailable ? battParams.battLowVolt.valueString + " " + battParams.battLowVolt.units : _naString + property int batteryIndex: index + 1 + property bool showBatteryIndex: _indexedBatteryParamCount > 1 + } } + } + + Component { + id: batterySummaryComponent + + ColumnLayout { + spacing: 0 + + property var _controller: controller + property int _batteryIndex: batteryIndex + + BatteryParams { + id: battParams + controller: _controller + batteryIndex: _batteryIndex + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Source").arg(batteryIndex) : qsTr("Battery Source") + valueText: battParams.battSource.enumStringValue + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Full").arg(batteryIndex) : qsTr("Battery Full") + valueText: battParams.battHighVoltAvailable ? battParams.battHighVolt.valueString + " " + battParams.battHighVolt.units : _naString + } + + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Empty").arg(batteryIndex) : qsTr("Battery Empty") + valueText: battParams.battLowVoltAvailable ? battParams.battLowVolt.valueString + " " + battParams.battLowVolt.units : _naString + } - VehicleSummaryRow { - labelText: qsTr("Number of Cells") - valueText: battParams.battNumCellsAvailable ? battParams.battNumCells.valueString : _naString + VehicleSummaryRow { + labelText: showBatteryIndex ? qsTr("Battery %1 Number of Cells").arg(batteryIndex) : qsTr("Number of Cells") + valueText: battParams.battNumCellsAvailable ? battParams.battNumCells.valueString : _naString + } } } } diff --git a/src/AutoPilotPlugins/PX4/SafetyComponent.qml b/src/AutoPilotPlugins/PX4/SafetyComponent.qml index 8475e7f385a6..65956b0fd967 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponent.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponent.qml @@ -213,7 +213,7 @@ SetupPage { } QGCLabel { - text: qsTr("RC Loss Failsafe Trigger") + text: qsTr("RC/Joystick Loss Failsafe Trigger") } Rectangle { @@ -253,7 +253,7 @@ SetupPage { } QGCLabel { - text: qsTr("RC Loss Timeout:") + text: qsTr("RC/Joystick Loss Timeout:") Layout.fillWidth: true } FactTextField { diff --git a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml index 187726b64237..1284495ef77a 100644 --- a/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/SafetyComponentSummary.qml @@ -1,12 +1,15 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact _rtlLandDelayFact: controller.getParameterFact(-1, "RTL_LAND_DELAY") property int _rtlLandDelayValue: _rtlLandDelayFact.value - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Low Battery Failsafe") @@ -29,12 +33,12 @@ Item { } VehicleSummaryRow { - labelText: qsTr("RC Loss Failsafe") + labelText: qsTr("RC/Joystick Loss Failsafe") valueText: rcLossAction ? rcLossAction.enumStringValue : "" } VehicleSummaryRow { - labelText: qsTr("RC Loss Timeout") + labelText: qsTr("RC/Joystick Loss Timeout") valueText: commRCLossFact ? commRCLossFact.valueString + " " + commRCLossFact.units : "" } diff --git a/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml b/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml index e3b5a542c8db..3382b902dd1e 100644 --- a/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml +++ b/src/AutoPilotPlugins/PX4/SensorsComponentSummary.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls @@ -10,7 +11,9 @@ import QGroundControl.Controls */ Item { - anchors.fill: parent + implicitWidth: mainLayout.implicitWidth + implicitHeight: mainLayout.implicitHeight + width: parent.width // grows when Loader is wider than implicitWidth FactPanelController { id: controller; } @@ -20,8 +23,9 @@ Item { property Fact gyro0IdFact: controller.getParameterFact(-1, "CAL_GYRO0_ID") property Fact accel0IdFact: controller.getParameterFact(-1, "CAL_ACC0_ID") - Column { - anchors.fill: parent + ColumnLayout { + id: mainLayout + spacing: 0 VehicleSummaryRow { labelText: qsTr("Compass 0") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9c73a206fa69..6a6bd52091de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ add_subdirectory(Utilities) add_subdirectory(Joystick) add_subdirectory(MAVLink) add_subdirectory(MissionManager) +add_subdirectory(PlanView) add_subdirectory(PositionManager) add_subdirectory(QmlControls) add_subdirectory(Settings) @@ -130,6 +131,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME} FirstRunPromptDialogsModule FlyViewModule FlightMapModule + PlanViewModule QGroundControlControlsModule QGroundControlModule ToolbarModule diff --git a/src/Camera/CMakeLists.txt b/src/Camera/CMakeLists.txt index 8e973259bf1f..dc088982d619 100644 --- a/src/Camera/CMakeLists.txt +++ b/src/Camera/CMakeLists.txt @@ -7,8 +7,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE CameraMetaData.cc CameraMetaData.h - MavlinkCameraControl.cc - MavlinkCameraControl.h + MavlinkCameraControlInterface.cc + MavlinkCameraControlInterface.h QGCCameraIO.cc QGCCameraIO.h QGCCameraManager.cc diff --git a/src/Camera/MavlinkCameraControl.cc b/src/Camera/MavlinkCameraControlInterface.cc similarity index 77% rename from src/Camera/MavlinkCameraControl.cc rename to src/Camera/MavlinkCameraControlInterface.cc index 8624997c0a6e..44d1a9ec7cc4 100644 --- a/src/Camera/MavlinkCameraControl.cc +++ b/src/Camera/MavlinkCameraControlInterface.cc @@ -1,22 +1,22 @@ -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "QGCLoggingCategory.h" -QGC_LOGGING_CATEGORY(CameraControlLog, "Camera.MavlinkCameraControl") -QGC_LOGGING_CATEGORY(CameraControlVerboseLog, "Camera.MavlinkCameraControl:verbose") +QGC_LOGGING_CATEGORY(CameraControlLog, "Camera.MavlinkCameraControlInterface") +QGC_LOGGING_CATEGORY(CameraControlVerboseLog, "Camera.MavlinkCameraControlInterface:verbose") -MavlinkCameraControl::MavlinkCameraControl(Vehicle *vehicle, QObject *parent) +MavlinkCameraControlInterface::MavlinkCameraControlInterface(Vehicle *vehicle, QObject *parent) : FactGroup(0, parent, true /* ignore camel case */) , _vehicle(vehicle) { qCDebug(CameraControlLog) << this; } -MavlinkCameraControl::~MavlinkCameraControl() +MavlinkCameraControlInterface::~MavlinkCameraControlInterface() { qCDebug(CameraControlLog) << this; } -QString MavlinkCameraControl::captureImageStatusToStr(uint8_t image_status) +QString MavlinkCameraControlInterface::captureImageStatusToStr(uint8_t image_status) { switch (image_status) { case PHOTO_CAPTURE_IDLE: @@ -32,7 +32,7 @@ QString MavlinkCameraControl::captureImageStatusToStr(uint8_t image_status) } } -QString MavlinkCameraControl::captureVideoStatusToStr(uint8_t video_status) +QString MavlinkCameraControlInterface::captureVideoStatusToStr(uint8_t video_status) { switch (video_status) { case VIDEO_CAPTURE_STATUS_STOPPED: @@ -44,7 +44,7 @@ QString MavlinkCameraControl::captureVideoStatusToStr(uint8_t video_status) } } -QString MavlinkCameraControl::storageStatusToStr(uint8_t status) +QString MavlinkCameraControlInterface::storageStatusToStr(uint8_t status) { switch (status) { case STORAGE_STATUS_EMPTY: @@ -60,7 +60,7 @@ QString MavlinkCameraControl::storageStatusToStr(uint8_t status) } } -QString MavlinkCameraControl::cameraModeToStr(CameraMode mode) +QString MavlinkCameraControlInterface::cameraModeToStr(CameraMode mode) { switch (mode) { case CAM_MODE_UNDEFINED: diff --git a/src/Camera/MavlinkCameraControl.h b/src/Camera/MavlinkCameraControlInterface.h similarity index 90% rename from src/Camera/MavlinkCameraControl.h rename to src/Camera/MavlinkCameraControlInterface.h index 3fd9be8712e9..d393d31d332f 100644 --- a/src/Camera/MavlinkCameraControl.h +++ b/src/Camera/MavlinkCameraControlInterface.h @@ -19,7 +19,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraControlLog) Q_DECLARE_LOGGING_CATEGORY(CameraControlVerboseLog) /// Abstract base class for all camera controls: real and simulated -class MavlinkCameraControl : public FactGroup +class MavlinkCameraControlInterface : public FactGroup { Q_OBJECT QML_ELEMENT @@ -44,6 +44,8 @@ class MavlinkCameraControl : public FactGroup Q_PROPERTY(bool hasFocus READ hasFocus NOTIFY infoChanged) Q_PROPERTY(bool hasVideoStream READ hasVideoStream NOTIFY infoChanged) Q_PROPERTY(bool hasTracking READ hasTracking NOTIFY infoChanged) + Q_PROPERTY(bool supportsTrackingPoint READ supportsTrackingPoint NOTIFY infoChanged) + Q_PROPERTY(bool supportsTrackingRect READ supportsTrackingRect NOTIFY infoChanged) Q_PROPERTY(bool photosInVideoMode READ photosInVideoMode NOTIFY infoChanged) Q_PROPERTY(bool videoInPhotoMode READ videoInPhotoMode NOTIFY infoChanged) Q_PROPERTY(bool isBasic READ isBasic NOTIFY infoChanged) @@ -74,10 +76,14 @@ class MavlinkCameraControl : public FactGroup Q_PROPERTY(QStringList streamLabels READ streamLabels NOTIFY streamLabelsChanged) Q_PROPERTY(ThermalViewMode thermalMode READ thermalMode WRITE setThermalMode NOTIFY thermalModeChanged) Q_PROPERTY(double thermalOpacity READ thermalOpacity WRITE setThermalOpacity NOTIFY thermalOpacityChanged) + + // Camera tracking properties Q_PROPERTY(bool trackingEnabled READ trackingEnabled WRITE setTrackingEnabled NOTIFY trackingEnabledChanged) - Q_PROPERTY(TrackingStatus trackingStatus READ trackingStatus CONSTANT) - Q_PROPERTY(bool trackingImageStatus READ trackingImageStatus NOTIFY trackingImageStatusChanged) - Q_PROPERTY(QRectF trackingImageRect READ trackingImageRect NOTIFY trackingImageStatusChanged) + Q_PROPERTY(bool trackingImageIsActive READ trackingImageIsActive NOTIFY trackingImageIsActiveChanged) + Q_PROPERTY(bool trackingImageIsPoint READ trackingImageIsPoint NOTIFY trackingImageIsPointChanged) + Q_PROPERTY(QRectF trackingImageRect READ trackingImageRect NOTIFY trackingImageRectChanged) + Q_PROPERTY(QPointF trackingImagePoint READ trackingImagePoint NOTIFY trackingImagePointChanged) + Q_PROPERTY(qreal trackingImageRadius READ trackingImageRadius NOTIFY trackingImageRadiusChanged) // These properties are used to determine what controls to show in the UI and are based on both the camera capabilities as well as the video manager status. // They are not necessarily directly related to the MAVLink camera capabilities. @@ -93,8 +99,8 @@ class MavlinkCameraControl : public FactGroup friend class QGCCameraParamIO; public: - explicit MavlinkCameraControl(Vehicle *vehicle, QObject *parent = nullptr); - virtual ~MavlinkCameraControl(); + explicit MavlinkCameraControlInterface(Vehicle *vehicle, QObject *parent = nullptr); + virtual ~MavlinkCameraControlInterface(); enum CameraMode { CAM_MODE_UNDEFINED = -1, @@ -141,14 +147,6 @@ class MavlinkCameraControl : public FactGroup }; Q_ENUM(ThermalViewMode) - enum TrackingStatus { - TRACKING_UNKNOWN = 0, - TRACKING_SUPPORTED = 1, - TRACKING_ENABLED = 2, - TRACKING_RECTANGLE = 4, - TRACKING_POINT = 8 - }; - Q_ENUM(TrackingStatus) Q_INVOKABLE virtual void setCameraModeVideo() = 0; Q_INVOKABLE virtual void setCameraModePhoto() = 0; @@ -165,8 +163,8 @@ class MavlinkCameraControl : public FactGroup Q_INVOKABLE virtual void stopZoom() = 0; Q_INVOKABLE virtual void stopStream() = 0; Q_INVOKABLE virtual void resumeStream() = 0; - Q_INVOKABLE virtual void startTracking(QRectF rec) = 0; - Q_INVOKABLE virtual void startTracking(QPointF point, double radius) = 0; + Q_INVOKABLE virtual void startTrackingRect(QRectF rec) = 0; + Q_INVOKABLE virtual void startTrackingPoint(QPointF point, double radius) = 0; Q_INVOKABLE virtual void stopTracking() = 0; virtual int version() const = 0; @@ -182,6 +180,8 @@ class MavlinkCameraControl : public FactGroup virtual bool hasZoom() const = 0; virtual bool hasFocus() const = 0; virtual bool hasTracking() const = 0; + virtual bool supportsTrackingPoint() const = 0; + virtual bool supportsTrackingRect() const = 0; virtual bool hasVideoStream() const = 0; virtual bool photosInVideoMode() const = 0; virtual bool videoInPhotoMode() const = 0; @@ -239,10 +239,11 @@ class MavlinkCameraControl : public FactGroup virtual bool trackingEnabled() const = 0; virtual void setTrackingEnabled(bool set) = 0; - virtual TrackingStatus trackingStatus() const = 0; - - virtual bool trackingImageStatus() const = 0; + virtual bool trackingImageIsActive() const = 0; + virtual bool trackingImageIsPoint() const = 0; virtual QRectF trackingImageRect() const = 0; + virtual QPointF trackingImagePoint() const = 0; + virtual qreal trackingImageRadius() const = 0; virtual void factChanged(Fact *pFact) = 0; ///< Notify controller a parameter has changed virtual bool incomingParameter(Fact *pFact, QVariant &newValue) = 0; ///< Allow controller to modify or invalidate incoming parameter @@ -286,7 +287,11 @@ class MavlinkCameraControl : public FactGroup void recordTimeChanged(); void streamLabelsChanged(); void trackingEnabledChanged(); - void trackingImageStatusChanged(); + void trackingImageIsActiveChanged(); + void trackingImageIsPointChanged(); + void trackingImageRectChanged(); + void trackingImagePointChanged(); + void trackingImageRadiusChanged(); void thermalModeChanged(); void thermalOpacityChanged(); void storageStatusChanged(); diff --git a/src/Camera/QGCCameraIO.cc b/src/Camera/QGCCameraIO.cc index cda1587524cb..7339d99c5a6e 100644 --- a/src/Camera/QGCCameraIO.cc +++ b/src/Camera/QGCCameraIO.cc @@ -1,5 +1,5 @@ #include "QGCCameraIO.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "LinkInterface.h" #include "MAVLinkProtocol.h" #include "QGCLoggingCategory.h" @@ -12,7 +12,7 @@ namespace { constexpr int kMaxRetries = 3; } -QGCCameraParamIO::QGCCameraParamIO(MavlinkCameraControl *control, Fact *fact, Vehicle *vehicle) +QGCCameraParamIO::QGCCameraParamIO(MavlinkCameraControlInterface *control, Fact *fact, Vehicle *vehicle) : QObject(control) , _control(control) , _fact(fact) diff --git a/src/Camera/QGCCameraIO.h b/src/Camera/QGCCameraIO.h index 75a1f9d68340..3bcb0e74b22a 100644 --- a/src/Camera/QGCCameraIO.h +++ b/src/Camera/QGCCameraIO.h @@ -5,7 +5,7 @@ #include "MAVLinkLib.h" -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class Fact; class Vehicle; @@ -16,7 +16,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraIOLogVerbose) class QGCCameraParamIO : public QObject { public: - QGCCameraParamIO(MavlinkCameraControl *control, Fact *fact, Vehicle *vehicle); + QGCCameraParamIO(MavlinkCameraControlInterface *control, Fact *fact, Vehicle *vehicle); ~QGCCameraParamIO(); void handleParamAck(const mavlink_param_ext_ack_t &ack); @@ -36,7 +36,7 @@ private slots: void _sendParameter(); QVariant _valueFromMessage(const char *value, uint8_t param_type); - MavlinkCameraControl *_control = nullptr; + MavlinkCameraControlInterface *_control = nullptr; Fact *_fact = nullptr; Vehicle *_vehicle = nullptr; diff --git a/src/Camera/QGCCameraManager.cc b/src/Camera/QGCCameraManager.cc index b6ad8c5b80a5..363d655dd180 100644 --- a/src/Camera/QGCCameraManager.cc +++ b/src/Camera/QGCCameraManager.cc @@ -3,7 +3,7 @@ #include "FirmwarePlugin.h" #include "Joystick.h" #include "JoystickManager.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MultiVehicleManager.h" #include "QGCLoggingCategory.h" #include "QGCVideoStreamInfo.h" @@ -231,10 +231,10 @@ void QGCCameraManager::_handleHeartbeat(const mavlink_message_t &message) pInfo->lastHeartbeat.start(); } -MavlinkCameraControl *QGCCameraManager::currentCameraInstance() +MavlinkCameraControlInterface *QGCCameraManager::currentCameraInstance() { if ((_currentCameraIndex < _cameras.count()) && !_cameras.isEmpty()) { - MavlinkCameraControl *pCamera = qobject_cast(_cameras[_currentCameraIndex]); + MavlinkCameraControlInterface *pCamera = qobject_cast(_cameras[_currentCameraIndex]); return pCamera; } return nullptr; @@ -242,7 +242,7 @@ MavlinkCameraControl *QGCCameraManager::currentCameraInstance() QGCVideoStreamInfo *QGCCameraManager::currentStreamInstance() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { QGCVideoStreamInfo *pInfo = pCamera->currentStreamInstance(); return pInfo; @@ -252,7 +252,7 @@ QGCVideoStreamInfo *QGCCameraManager::currentStreamInstance() QGCVideoStreamInfo *QGCCameraManager::thermalStreamInstance() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { QGCVideoStreamInfo *pInfo = pCamera->thermalStreamInstance(); return pInfo; @@ -260,15 +260,15 @@ QGCVideoStreamInfo *QGCCameraManager::thermalStreamInstance() return nullptr; } -MavlinkCameraControl *QGCCameraManager::_findCamera(int id) +MavlinkCameraControlInterface *QGCCameraManager::_findCamera(int id) { for (int i = 0; i < _cameras.count(); i++) { if (!_cameras[i]) { continue; } - MavlinkCameraControl *pCamera = qobject_cast(_cameras[i]); + MavlinkCameraControlInterface *pCamera = qobject_cast(_cameras[i]); if (!pCamera) { - qCCritical(CameraManagerLog) << "Invalid MavlinkCameraControl instance"; + qCCritical(CameraManagerLog) << "Invalid MavlinkCameraControlInterface instance"; continue; } if (pCamera->compID() == id) { @@ -280,7 +280,7 @@ MavlinkCameraControl *QGCCameraManager::_findCamera(int id) return nullptr; } -void QGCCameraManager::_addCameraControlToLists(MavlinkCameraControl *cameraControl) +void QGCCameraManager::_addCameraControlToLists(MavlinkCameraControlInterface *cameraControl) { if (qobject_cast(cameraControl)) { qCDebug(CameraManagerLog) << "Adding simulated camera to list"; @@ -319,9 +319,9 @@ void QGCCameraManager::_handleCameraInfo(const mavlink_message_t& message) mavlink_msg_camera_information_decode(&message, &info); qCDebug(CameraManagerLog) << "Camera information received from" << QGCMAVLink::compIdToString(message.compid) << "Model:" << reinterpret_cast(info.model_name); - qCDebug(CameraManagerLog) << "Creating MavlinkCameraControl for camera"; + qCDebug(CameraManagerLog) << "Creating MavlinkCameraControlInterface for camera"; - MavlinkCameraControl *pCamera = _vehicle->firmwarePlugin()->createCameraControl(&info, _vehicle, message.compid, this); + MavlinkCameraControlInterface *pCamera = _vehicle->firmwarePlugin()->createCameraControl(&info, _vehicle, message.compid, this); if (pCamera) { _addCameraControlToLists(pCamera); @@ -362,7 +362,7 @@ void QGCCameraManager::_checkForLostCameras() continue; } - MavlinkCameraControl* pCamera = _findCamera(pInfo->compID); + MavlinkCameraControlInterface* pCamera = _findCamera(pInfo->compID); if (pCamera) { const int idx = _cameras.indexOf(pCamera); if (idx >= 0) { @@ -397,7 +397,7 @@ void QGCCameraManager::_checkForLostCameras() void QGCCameraManager::_handleCameraCaptureStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_camera_capture_status_t cap{}; mavlink_msg_camera_capture_status_decode(&message, &cap); @@ -407,7 +407,7 @@ void QGCCameraManager::_handleCameraCaptureStatus(const mavlink_message_t &messa void QGCCameraManager::_handleStorageInformation(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_storage_information_t st{}; mavlink_msg_storage_information_decode(&message, &st); @@ -441,7 +441,7 @@ void QGCCameraManager::_handleCameraSettings(const mavlink_message_t& message) void QGCCameraManager::_handleParamExtAck(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_param_ext_ack_t ack{}; mavlink_msg_param_ext_ack_decode(&message, &ack); @@ -451,7 +451,7 @@ void QGCCameraManager::_handleParamExtAck(const mavlink_message_t &message) void QGCCameraManager::_handleParamExtValue(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_param_ext_value_t value{}; mavlink_msg_param_ext_value_decode(&message, &value); @@ -461,7 +461,7 @@ void QGCCameraManager::_handleParamExtValue(const mavlink_message_t &message) void QGCCameraManager::_handleVideoStreamInformation(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_video_stream_information_t streamInfo{}; mavlink_msg_video_stream_information_decode(&message, &streamInfo); @@ -472,7 +472,7 @@ void QGCCameraManager::_handleVideoStreamInformation(const mavlink_message_t &me void QGCCameraManager::_handleVideoStreamStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_video_stream_status_t streamStatus{}; mavlink_msg_video_stream_status_decode(&message, &streamStatus); @@ -482,7 +482,7 @@ void QGCCameraManager::_handleVideoStreamStatus(const mavlink_message_t &message void QGCCameraManager::_handleBatteryStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_battery_status_t batteryStatus{}; mavlink_msg_battery_status_decode(&message, &batteryStatus); @@ -492,7 +492,7 @@ void QGCCameraManager::_handleBatteryStatus(const mavlink_message_t &message) void QGCCameraManager::_handleTrackingImageStatus(const mavlink_message_t &message) { - MavlinkCameraControl *pCamera = _findCamera(message.compid); + MavlinkCameraControlInterface *pCamera = _findCamera(message.compid); if (pCamera) { mavlink_camera_tracking_image_status_t tis{}; mavlink_msg_camera_tracking_image_status_decode(&message, &tis); @@ -640,7 +640,7 @@ void QGCCameraManager::_activeJoystickChanged(Joystick *joystick) void QGCCameraManager::_triggerCamera() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->takePhoto(); } @@ -648,7 +648,7 @@ void QGCCameraManager::_triggerCamera() void QGCCameraManager::_startVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->startVideoRecording(); } @@ -656,7 +656,7 @@ void QGCCameraManager::_startVideoRecording() void QGCCameraManager::_stopVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stopVideoRecording(); } @@ -664,7 +664,7 @@ void QGCCameraManager::_stopVideoRecording() void QGCCameraManager::_toggleVideoRecording() { - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->toggleVideoRecording(); } @@ -675,7 +675,7 @@ void QGCCameraManager::_stepZoom(int direction) if (_lastZoomChange.elapsed() > 40) { _lastZoomChange.start(); qCDebug(CameraManagerLog) << "Step Camera Zoom" << direction; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stepZoom(direction); } @@ -685,7 +685,7 @@ void QGCCameraManager::_stepZoom(int direction) void QGCCameraManager::_startZoom(int direction) { qCDebug(CameraManagerLog) << "Start Camera Zoom" << direction; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->startZoom(direction); } @@ -694,7 +694,7 @@ void QGCCameraManager::_startZoom(int direction) void QGCCameraManager::_stopZoom() { qCDebug(CameraManagerLog) << "Stop Camera Zoom"; - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { pCamera->stopZoom(); } @@ -719,7 +719,7 @@ void QGCCameraManager::_stepStream(int direction) { if (_lastCameraChange.elapsed() > 1000) { _lastCameraChange.start(); - MavlinkCameraControl *pCamera = currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = currentCameraInstance(); if (pCamera) { qCDebug(CameraManagerLog) << "Step Camera Stream" << direction; int stream = pCamera->currentStream() + direction; @@ -796,8 +796,8 @@ void QGCCameraManager::_handleCameraFovStatus(const mavlink_message_t& message) } auto* settings = SettingsManager::instance()->gimbalControllerSettings(); - settings->CameraHFov()->setRawValue(fov.hfov); - settings->CameraVFov()->setRawValue(vfovDeg); + settings->cameraHFov()->setRawValue(fov.hfov); + settings->cameraVFov()->setRawValue(vfovDeg); } void QGCCameraManager::_setCurrentZoomLevel(int level) diff --git a/src/Camera/QGCCameraManager.h b/src/Camera/QGCCameraManager.h index 6ce5a3a9bef1..5a7515d474e1 100644 --- a/src/Camera/QGCCameraManager.h +++ b/src/Camera/QGCCameraManager.h @@ -17,7 +17,7 @@ Q_DECLARE_LOGGING_CATEGORY(CameraManagerLog) class CameraMetaData; class Joystick; -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class QGCCameraManagerTest; class QGCVideoStreamInfo; class SimulatedCameraControl; @@ -29,11 +29,11 @@ class QGCCameraManager : public QObject QML_ELEMENT QML_UNCREATABLE("") Q_MOC_INCLUDE("Joystick.h") - Q_MOC_INCLUDE("MavlinkCameraControl.h") + Q_MOC_INCLUDE("MavlinkCameraControlInterface.h") Q_PROPERTY(QmlObjectListModel* cameras READ cameras NOTIFY camerasChanged) Q_PROPERTY(QStringList cameraLabels READ cameraLabels NOTIFY cameraLabelsChanged) - Q_PROPERTY(MavlinkCameraControl* currentCameraInstance READ currentCameraInstance NOTIFY currentCameraChanged) + Q_PROPERTY(MavlinkCameraControlInterface* currentCameraInstance READ currentCameraInstance NOTIFY currentCameraChanged) Q_PROPERTY(int currentCamera READ currentCamera WRITE setCurrentCamera NOTIFY currentCameraChanged) Q_PROPERTY(int currentZoomLevel READ currentZoomLevel NOTIFY currentZoomLevelChanged) @@ -65,7 +65,7 @@ class QGCCameraManager : public QObject const QmlObjectListModel* cameras() const { return &_cameras; } QStringList cameraLabels() const { return _cameraLabels; } int currentCamera() const { return _currentCameraIndex; } - MavlinkCameraControl* currentCameraInstance(); + MavlinkCameraControlInterface* currentCameraInstance(); void setCurrentCamera(int sel); QGCVideoStreamInfo* currentStreamInstance(); QGCVideoStreamInfo* thermalStreamInstance(); @@ -112,7 +112,7 @@ private slots: void _setCurrentZoomLevel(int level); private: - MavlinkCameraControl* _findCamera(int id); + MavlinkCameraControlInterface* _findCamera(int id); void _requestCameraInfo(CameraStruct* cameraInfo); void _handleHeartbeat(const mavlink_message_t& message); void _handleCameraInfo(const mavlink_message_t& message); @@ -125,7 +125,7 @@ private slots: void _handleVideoStreamStatus(const mavlink_message_t& message); void _handleBatteryStatus(const mavlink_message_t& message); void _handleTrackingImageStatus(const mavlink_message_t& message); - void _addCameraControlToLists(MavlinkCameraControl* cameraControl); + void _addCameraControlToLists(MavlinkCameraControlInterface* cameraControl); void _handleCameraFovStatus(const mavlink_message_t& message); QPointer _vehicle; diff --git a/src/Camera/SimulatedCameraControl.cc b/src/Camera/SimulatedCameraControl.cc index e0ea5b6cf541..352891ccb154 100644 --- a/src/Camera/SimulatedCameraControl.cc +++ b/src/Camera/SimulatedCameraControl.cc @@ -9,7 +9,7 @@ QGC_LOGGING_CATEGORY(SimulatedCameraControlLog, "Camera.SimulatedCameraControl") SimulatedCameraControl::SimulatedCameraControl(Vehicle *vehicle, QObject *parent) - : MavlinkCameraControl(vehicle, parent) + : MavlinkCameraControlInterface(vehicle, parent) { qCDebug(SimulatedCameraControlLog) << this; @@ -216,7 +216,7 @@ bool SimulatedCameraControl::hasVideoStream() const return VideoManager::instance()->decoding(); } -MavlinkCameraControl::CaptureVideoState SimulatedCameraControl::captureVideoState() const +MavlinkCameraControlInterface::CaptureVideoState SimulatedCameraControl::captureVideoState() const { if (!capturesVideo()) { return CaptureVideoStateDisabled; @@ -230,7 +230,7 @@ MavlinkCameraControl::CaptureVideoState SimulatedCameraControl::captureVideoStat return CaptureVideoStateIdle; } -MavlinkCameraControl::CapturePhotosState SimulatedCameraControl::capturePhotosState() const +MavlinkCameraControlInterface::CapturePhotosState SimulatedCameraControl::capturePhotosState() const { if (_photoCaptureStatus() == PHOTO_CAPTURE_IN_PROGRESS) { return CapturePhotosStateCapturingSinglePhoto; @@ -241,7 +241,7 @@ MavlinkCameraControl::CapturePhotosState SimulatedCameraControl::capturePhotosSt return capturesPhotos() ? CapturePhotosStateIdle : CapturePhotosStateDisabled; } -void SimulatedCameraControl::setPhotoCaptureMode(MavlinkCameraControl::PhotoCaptureMode photoCaptureMode) +void SimulatedCameraControl::setPhotoCaptureMode(MavlinkCameraControlInterface::PhotoCaptureMode photoCaptureMode) { if (photoCaptureMode == PHOTO_CAPTURE_TIMELAPSE) { qCWarning(CameraControlLog) << "Time lapse capture not supported by simulated camera"; diff --git a/src/Camera/SimulatedCameraControl.h b/src/Camera/SimulatedCameraControl.h index bc31fa970550..0cf1aad785f3 100644 --- a/src/Camera/SimulatedCameraControl.h +++ b/src/Camera/SimulatedCameraControl.h @@ -3,7 +3,7 @@ #include #include -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" class QGCVideoStreamInfo; class Vehicle; @@ -12,7 +12,7 @@ class Vehicle; /// Video record if a manual stream is available /// Photo capture using DO_DIGICAM_CONTROL if the setting is enabled /// It does not support time lapse capture -class SimulatedCameraControl : public MavlinkCameraControl +class SimulatedCameraControl : public MavlinkCameraControlInterface { Q_OBJECT @@ -35,8 +35,8 @@ class SimulatedCameraControl : public MavlinkCameraControl void stopStream() override {} bool stopTakePhoto() override { return false;} void resumeStream() override {} - void startTracking(QRectF /*rec*/) override {} - void startTracking(QPointF /*point*/, double /*radius*/) override {} + void startTrackingRect(QRectF /*rec*/) override {} + void startTrackingPoint(QPointF /*point*/, double /*radius*/) override {} void stopTracking() override {} int version() const override { return 0; } @@ -52,6 +52,8 @@ class SimulatedCameraControl : public MavlinkCameraControl bool hasZoom() const override { return false; } bool hasFocus() const override { return false; } bool hasTracking() const override { return false; } + bool supportsTrackingPoint() const override { return false; } + bool supportsTrackingRect() const override { return false; } bool hasVideoStream() const override; bool photosInVideoMode() const override { return true; } bool videoInPhotoMode() const override { return false; } @@ -105,10 +107,11 @@ class SimulatedCameraControl : public MavlinkCameraControl bool trackingEnabled() const override { return false; } void setTrackingEnabled(bool /*set*/) override {} - TrackingStatus trackingStatus() const override { return TRACKING_UNKNOWN; } - - bool trackingImageStatus() const override { return false; } + bool trackingImageIsActive() const override { return false; } + bool trackingImageIsPoint() const override { return false; } QRectF trackingImageRect() const override { return QRectF(); } + QPointF trackingImagePoint() const override { return QPointF(); } + qreal trackingImageRadius() const override { return 0.0; } void factChanged(Fact* /*pFact*/) override {}; bool incomingParameter(Fact* /*pFact*/, QVariant& /*newValue*/) override { return false; } diff --git a/src/Camera/VehicleCameraControl.cc b/src/Camera/VehicleCameraControl.cc index 489010131c66..a6b37709c847 100644 --- a/src/Camera/VehicleCameraControl.cc +++ b/src/Camera/VehicleCameraControl.cc @@ -17,6 +17,8 @@ #include #include + +#include #include #include #include @@ -97,7 +99,7 @@ static bool read_value(QDomNode& element, const char* tagName, QString& target) } VehicleCameraControl::VehicleCameraControl(const mavlink_camera_information_t *info, Vehicle* vehicle, int compID, QObject* parent) - : MavlinkCameraControl(vehicle, parent) + : MavlinkCameraControlInterface(vehicle, parent) , _compID(compID) { QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); @@ -130,15 +132,9 @@ VehicleCameraControl::VehicleCameraControl(const mavlink_camera_information_t *i _videoRecordTimeUpdateTimer.setInterval(333); connect(&_videoRecordTimeUpdateTimer, &QTimer::timeout, this, &VehicleCameraControl::_recTimerHandler); - //-- Tracking - if(_mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_RECTANGLE); - _trackingStatus = static_cast(_trackingStatus | TRACKING_SUPPORTED); - } - if(_mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_POINT); - _trackingStatus = static_cast(_trackingStatus | TRACKING_SUPPORTED); - } + //-- Tracking capabilities + _hasTrackingRectCapability = _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE; + _hasTrackingPointCapability = _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT; connect(this, &VehicleCameraControl::dataReady, this, &VehicleCameraControl::_dataReady); @@ -230,7 +226,7 @@ bool VehicleCameraControl::capturesPhotos() const return _mavlinkCameraInfo.flags & (CAMERA_CAP_FLAGS_CAPTURE_IMAGE | CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM); } -MavlinkCameraControl::CaptureVideoState VehicleCameraControl::captureVideoState() const +MavlinkCameraControlInterface::CaptureVideoState VehicleCameraControl::captureVideoState() const { if (_videoCaptureStatus() == VIDEO_CAPTURE_STATUS_RUNNING || VideoManager::instance()->recording()) { return CaptureVideoStateCapturing; @@ -246,7 +242,7 @@ MavlinkCameraControl::CaptureVideoState VehicleCameraControl::captureVideoState( return CaptureVideoStateDisabled; } -MavlinkCameraControl::CapturePhotosState VehicleCameraControl::capturePhotosState() const +MavlinkCameraControlInterface::CapturePhotosState VehicleCameraControl::capturePhotosState() const { if (_photoCaptureStatus() == PHOTO_CAPTURE_IN_PROGRESS) { return CapturePhotosStateCapturingSinglePhoto; @@ -1693,35 +1689,56 @@ void VehicleCameraControl::handleTrackingImageStatus(const mavlink_camera_tracki _trackingImageStatus = trackingImageStatus; - if (_trackingImageStatus.tracking_status == 0 || !trackingEnabled()) { - _trackingImageRect = {}; + const bool active = ((_trackingImageStatus.tracking_status & CAMERA_TRACKING_STATUS_FLAGS_ACTIVE) != 0) && trackingEnabled(); + const bool isPoint = active && (_trackingImageStatus.tracking_mode == CAMERA_TRACKING_MODE_POINT); + + if (!active) { qCDebug(CameraControlLog) << "Tracking off"; - } else { - if (_trackingImageStatus.tracking_mode == 2) { - _trackingImageRect = QRectF(QPointF(_trackingImageStatus.rec_top_x, _trackingImageStatus.rec_top_y), - QPointF(_trackingImageStatus.rec_bottom_x, _trackingImageStatus.rec_bottom_y)); + _trackingImageRect = {}; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + } else if (isPoint) { + const QPointF point(std::clamp(static_cast(_trackingImageStatus.point_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.point_y), 0.0, 1.0)); + qreal radius = static_cast(_trackingImageStatus.radius); + if (qIsNaN(radius) || radius <= 0) { + radius = 0.05; } else { - float r = _trackingImageStatus.radius; - if (qIsNaN(r) || r <= 0 ) { - r = 0.05f; - } - // Bottom is NAN so that we can draw perfect square using video aspect ratio - _trackingImageRect = QRectF(QPointF(_trackingImageStatus.point_x - r, _trackingImageStatus.point_y - r), - QPointF(_trackingImageStatus.point_x + r, NAN)); + radius = std::clamp(radius, 0.0, 1.0); + } + qCDebug(CameraControlLog) << "Tracking Point [" << point << "] radius:" << radius; + _trackingImageRect = {}; + if (_trackingImagePoint != point) { + _trackingImagePoint = point; + emit trackingImagePointChanged(); + } + if (!qFuzzyCompare(_trackingImageRadius, radius)) { + _trackingImageRadius = radius; + emit trackingImageRadiusChanged(); + } + } else { + // Rectangle tracking + const QRectF rect = QRectF(QPointF(std::clamp(static_cast(_trackingImageStatus.rec_top_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.rec_top_y), 0.0, 1.0)), + QPointF(std::clamp(static_cast(_trackingImageStatus.rec_bottom_x), 0.0, 1.0), + std::clamp(static_cast(_trackingImageStatus.rec_bottom_y), 0.0, 1.0))).normalized(); + qCDebug(CameraControlLog) << "Tracking Rect [" << rect << "]"; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + if (_trackingImageRect != rect) { + _trackingImageRect = rect; + emit trackingImageRectChanged(); } - // get rectangle into [0..1] boundaries - _trackingImageRect.setLeft(std::min(std::max(_trackingImageRect.left(), 0.0), 1.0)); - _trackingImageRect.setTop(std::min(std::max(_trackingImageRect.top(), 0.0), 1.0)); - _trackingImageRect.setRight(std::min(std::max(_trackingImageRect.right(), 0.0), 1.0)); - _trackingImageRect.setBottom(std::min(std::max(_trackingImageRect.bottom(), 0.0), 1.0)); - - qCDebug(CameraControlLog) << "Tracking Image Status [left:" << _trackingImageRect.left() - << "top:" << _trackingImageRect.top() - << "right:" << _trackingImageRect.right() - << "bottom:" << _trackingImageRect.bottom() << "]"; } - emit trackingImageStatusChanged(); + if (_trackingImageIsActive != active) { + _trackingImageIsActive = active; + emit trackingImageIsActiveChanged(); + } + if (_trackingImageIsPoint != isPoint) { + _trackingImageIsPoint = isPoint; + emit trackingImageIsPointChanged(); + } } void VehicleCameraControl::setCurrentStream(int stream) @@ -2323,59 +2340,60 @@ VehicleCameraControl::mode() void VehicleCameraControl::setTrackingEnabled(bool set) { - if(set) { - _trackingStatus = static_cast(_trackingStatus | TRACKING_ENABLED); - } else { - _trackingStatus = static_cast(_trackingStatus & ~TRACKING_ENABLED); + if (_trackingEnabled == set) { + return; + } + _trackingEnabled = set; + if (!set) { + stopTracking(); } emit trackingEnabledChanged(); } -void VehicleCameraControl::startTracking(QRectF rec) +void VehicleCameraControl::startTrackingRect(QRectF rec) { - if(_trackingMarquee != rec) { - _trackingMarquee = rec; + if (!_hasTrackingRectCapability) { + qCCritical(CameraControlLog) << "startTrackingRect called but camera does not have rectangle tracking capability"; + return; + } - qCDebug(CameraControlLog) << "Start Tracking (Rectangle: [" - << static_cast(rec.x()) << ", " - << static_cast(rec.y()) << "] - [" - << static_cast(rec.x() + rec.width()) << ", " - << static_cast(rec.y() + rec.height()) << "]"; + qCDebug(CameraControlLog) << "Start Tracking (Rectangle: [" + << static_cast(rec.x()) << ", " + << static_cast(rec.y()) << "] - [" + << static_cast(rec.x() + rec.width()) << ", " + << static_cast(rec.y() + rec.height()) << "]"; - _vehicle->sendMavCommand(_compID, - MAV_CMD_CAMERA_TRACK_RECTANGLE, - true, - static_cast(rec.x()), - static_cast(rec.y()), - static_cast(rec.x() + rec.width()), - static_cast(rec.y() + rec.height())); + _vehicle->sendMavCommand(_compID, + MAV_CMD_CAMERA_TRACK_RECTANGLE, + true, + static_cast(rec.x()), + static_cast(rec.y()), + static_cast(rec.x() + rec.width()), + static_cast(rec.y() + rec.height())); - // Request tracking status - _requestTrackingStatus(); - } + _requestTrackingStatus(); } -void VehicleCameraControl::startTracking(QPointF point, double radius) +void VehicleCameraControl::startTrackingPoint(QPointF point, double radius) { - if(_trackingPoint != point || _trackingRadius != radius) { - _trackingPoint = point; - _trackingRadius = radius; + if (!_hasTrackingPointCapability) { + qCCritical(CameraControlLog) << "startTrackingPoint called but camera does not have point tracking capability"; + return; + } - qCDebug(CameraControlLog) << "Start Tracking (Point: [" - << static_cast(point.x()) << ", " - << static_cast(point.y()) << "], Radius: " - << static_cast(radius); + qCDebug(CameraControlLog) << "Start Tracking (Point: [" + << static_cast(point.x()) << ", " + << static_cast(point.y()) << "], Radius: " + << static_cast(radius); - _vehicle->sendMavCommand(_compID, - MAV_CMD_CAMERA_TRACK_POINT, - true, - static_cast(point.x()), - static_cast(point.y()), - static_cast(radius)); + _vehicle->sendMavCommand(_compID, + MAV_CMD_CAMERA_TRACK_POINT, + true, + static_cast(point.x()), + static_cast(point.y()), + static_cast(radius)); - // Request tracking status - _requestTrackingStatus(); - } + _requestTrackingStatus(); } void VehicleCameraControl::stopTracking() @@ -2394,8 +2412,18 @@ void VehicleCameraControl::stopTracking() MAVLINK_MSG_ID_CAMERA_TRACKING_IMAGE_STATUS, -1); - // reset tracking image rectangle + // reset tracking state _trackingImageRect = {}; + _trackingImagePoint = {}; + _trackingImageRadius = 0.0; + if (_trackingImageIsActive) { + _trackingImageIsActive = false; + emit trackingImageIsActiveChanged(); + } + if (_trackingImageIsPoint) { + _trackingImageIsPoint = false; + emit trackingImageIsPointChanged(); + } } void VehicleCameraControl::_requestTrackingStatus() diff --git a/src/Camera/VehicleCameraControl.h b/src/Camera/VehicleCameraControl.h index a9cbe6a5a647..6ff3470eded6 100644 --- a/src/Camera/VehicleCameraControl.h +++ b/src/Camera/VehicleCameraControl.h @@ -1,6 +1,6 @@ #pragma once -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "QmlObjectListModel.h" class QGCVideoStreamInfo; @@ -35,7 +35,7 @@ class QGCCameraOptionRange : public QObject }; /// MAVLink Camera API controller - connected to a real mavlink v2 camera -class VehicleCameraControl : public MavlinkCameraControl +class VehicleCameraControl : public MavlinkCameraControlInterface { public: VehicleCameraControl(const mavlink_camera_information_t* info, Vehicle* vehicle, int compID, QObject* parent = nullptr); @@ -56,8 +56,8 @@ class VehicleCameraControl : public MavlinkCameraControl Q_INVOKABLE virtual void stopZoom (); Q_INVOKABLE virtual void stopStream (); Q_INVOKABLE virtual void resumeStream (); - Q_INVOKABLE virtual void startTracking (QRectF rec); - Q_INVOKABLE virtual void startTracking (QPointF point, double radius); + Q_INVOKABLE virtual void startTrackingRect (QRectF rec); + Q_INVOKABLE virtual void startTrackingPoint (QPointF point, double radius); Q_INVOKABLE virtual void stopTracking (); virtual int version () const { return _version; } @@ -72,7 +72,9 @@ class VehicleCameraControl : public MavlinkCameraControl virtual bool hasModes () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_MODES; } virtual bool hasZoom () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM; } virtual bool hasFocus () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_BASIC_FOCUS; } - virtual bool hasTracking () const { return _trackingStatus & TRACKING_SUPPORTED; } + virtual bool hasTracking () const { return _hasTrackingRectCapability || _hasTrackingPointCapability; } + virtual bool supportsTrackingPoint() const { return _hasTrackingPointCapability; } + virtual bool supportsTrackingRect () const { return _hasTrackingRectCapability; } virtual bool hasVideoStream () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM; } virtual bool photosInVideoMode () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_CAN_CAPTURE_IMAGE_IN_VIDEO_MODE; } virtual bool videoInPhotoMode () const { return _mavlinkCameraInfo.flags & CAMERA_CAP_FLAGS_CAN_CAPTURE_VIDEO_IN_IMAGE_MODE; } @@ -125,13 +127,14 @@ class VehicleCameraControl : public MavlinkCameraControl virtual void handleVideoStreamInformation(const mavlink_video_stream_information_t &videoStreamInformation); virtual void handleVideoStreamStatus(const mavlink_video_stream_status_t &videoStreamStatus); - virtual bool trackingEnabled () const { return _trackingStatus & TRACKING_ENABLED; } + virtual bool trackingEnabled () const { return _trackingEnabled; } virtual void setTrackingEnabled (bool set); - virtual TrackingStatus trackingStatus () const { return _trackingStatus; } - - virtual bool trackingImageStatus() const { return _trackingImageStatus.tracking_status == 1; } + virtual bool trackingImageIsActive() const { return _trackingImageIsActive; } + virtual bool trackingImageIsPoint() const { return _trackingImageIsPoint; } virtual QRectF trackingImageRect() const { return _trackingImageRect; } + virtual QPointF trackingImagePoint() const { return _trackingImagePoint; } + virtual qreal trackingImageRadius() const { return _trackingImageRadius; } virtual Fact* exposureMode (); virtual Fact* ev (); @@ -293,10 +296,13 @@ protected slots: QStringList _streamLabels; ThermalViewMode _thermalMode = THERMAL_BLEND; double _thermalOpacity = 85.0; - TrackingStatus _trackingStatus = TRACKING_UNKNOWN; - QRectF _trackingMarquee; - QPointF _trackingPoint; - double _trackingRadius = 0.0; - mavlink_camera_tracking_image_status_t _trackingImageStatus; + bool _hasTrackingRectCapability = false; + bool _hasTrackingPointCapability = false; + bool _trackingEnabled = false; + bool _trackingImageIsActive = false; + bool _trackingImageIsPoint = false; + mavlink_camera_tracking_image_status_t _trackingImageStatus{}; QRectF _trackingImageRect; + QPointF _trackingImagePoint; + qreal _trackingImageRadius = 0.0; }; diff --git a/src/Comms/LinkInterface.cc b/src/Comms/LinkInterface.cc index d2bcd60c3054..d967783dc1d0 100644 --- a/src/Comms/LinkInterface.cc +++ b/src/Comms/LinkInterface.cc @@ -3,8 +3,6 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "MAVLinkSigning.h" -#include "SettingsManager.h" -#include "MavlinkSettings.h" #include @@ -42,20 +40,16 @@ bool LinkInterface::mavlinkChannelIsSet() const bool LinkInterface::initMavlinkSigning() { - if (!isSecureConnection()) { - auto mavlinkSettings = SettingsManager::instance()->mavlinkSettings(); - const QByteArray signingKeyBytes = mavlinkSettings->mavlink2SigningKey()->rawValue().toByteArray(); - if (MAVLinkSigning::initSigning(static_cast(_mavlinkChannel), signingKeyBytes, MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)) { - if (signingKeyBytes.isEmpty()) { - qCDebug(LinkInterfaceLog) << "Signing disabled on channel" << _mavlinkChannel; - } else { - qCDebug(LinkInterfaceLog) << "Signing enabled on channel" << _mavlinkChannel; - } - } else { - qCWarning(LinkInterfaceLog) << "Failed To enable Signing on channel" << _mavlinkChannel; - // FIXME: What should we do here? - return false; - } + // Always clear any prior signing state on the channel to avoid stale + // mavlink_status_t::signing from a previous connection on this channel. + // For insecure connections the correct key will be auto-detected from + // incoming signed packets via MAVLinkSigning::tryDetectKey(). + if (MAVLinkSigning::initSigning(static_cast(_mavlinkChannel), QByteArrayView(), nullptr)) { + qCDebug(LinkInterfaceLog) << "Signing cleared on channel" << _mavlinkChannel + << (isSecureConnection() ? "(secure)" : "(will auto-detect)"); + } else { + qCWarning(LinkInterfaceLog) << "Failed to initialise signing on channel" << _mavlinkChannel; + return false; } return true; diff --git a/src/Comms/LinkManager.cc b/src/Comms/LinkManager.cc index 5c9aa6d9b08e..cd127a8ed76e 100644 --- a/src/Comms/LinkManager.cc +++ b/src/Comms/LinkManager.cc @@ -785,20 +785,6 @@ bool LinkManager::isLinkUSBDirect(const LinkInterface *link) return false; } -void LinkManager::resetMavlinkSigning() -{ - // Make a copy under mutex protection to avoid holding lock during signing initialization - QList links; - { - QMutexLocker locker(&_linksMutex); - links = _rgLinks; - } - - for (const SharedLinkInterfacePtr &sharedLink: links) { - sharedLink->initMavlinkSigning(); - } -} - #ifndef QGC_NO_SERIAL_LINK // Serial Only Functions void LinkManager::_filterCompositePorts(QList &portList) diff --git a/src/Comms/LinkManager.h b/src/Comms/LinkManager.h index 964b1ecfeb7d..69fa18eaadf8 100644 --- a/src/Comms/LinkManager.h +++ b/src/Comms/LinkManager.h @@ -88,9 +88,6 @@ class LinkManager : public QObject /// Returns pointer to the mavlink support forwarding link, or nullptr if it does not exist SharedLinkInterfacePtr mavlinkForwardingSupportLink(); - /// Re-initilize the mavlink signing for all links. Used when the signing key changes. - void resetMavlinkSigning(); - void disconnectAll(); /// Allocates a mavlink channel for use diff --git a/src/Comms/MockLink/MockConfiguration.cc b/src/Comms/MockLink/MockConfiguration.cc index 55052aea96ab..5b03cd0d91d6 100644 --- a/src/Comms/MockLink/MockConfiguration.cc +++ b/src/Comms/MockLink/MockConfiguration.cc @@ -92,8 +92,8 @@ void MockConfiguration::loadSettings(QSettings &settings, const QString &root) setCameraCanCaptureImageInVideoMode(settings.value(_cameraCanCaptureImageInVideoModeKey, true).toBool()); setCameraCanCaptureVideoInImageMode(settings.value(_cameraCanCaptureVideoInImageModeKey, false).toBool()); setCameraHasBasicZoom(settings.value(_cameraHasBasicZoomKey, true).toBool()); - setCameraHasTrackingPoint(settings.value(_cameraHasTrackingPointKey, false).toBool()); - setCameraHasTrackingRectangle(settings.value(_cameraHasTrackingRectangleKey, false).toBool()); + setCameraHasTrackingPoint(settings.value(_cameraHasTrackingPointKey, true).toBool()); + setCameraHasTrackingRectangle(settings.value(_cameraHasTrackingRectangleKey, true).toBool()); setGimbalHasRollAxis(settings.value(_gimbalHasRollAxisKey, true).toBool()); setGimbalHasPitchAxis(settings.value(_gimbalHasPitchAxisKey, true).toBool()); setGimbalHasYawAxis(settings.value(_gimbalHasYawAxisKey, true).toBool()); diff --git a/src/Comms/MockLink/MockConfiguration.h b/src/Comms/MockLink/MockConfiguration.h index 26057d8049c0..ec160a687a26 100644 --- a/src/Comms/MockLink/MockConfiguration.h +++ b/src/Comms/MockLink/MockConfiguration.h @@ -153,8 +153,8 @@ class MockConfiguration : public LinkConfiguration bool _cameraCanCaptureImageInVideoMode = true; bool _cameraCanCaptureVideoInImageMode = false; bool _cameraHasBasicZoom = true; - bool _cameraHasTrackingPoint = false; - bool _cameraHasTrackingRectangle = false; + bool _cameraHasTrackingPoint = true; + bool _cameraHasTrackingRectangle = true; // Gimbal capability flags (defaults - all enabled) bool _gimbalHasRollAxis = true; diff --git a/src/Comms/MockLink/MockLink.cc b/src/Comms/MockLink/MockLink.cc index 16c2349c9c38..bf5b07477766 100644 --- a/src/Comms/MockLink/MockLink.cc +++ b/src/Comms/MockLink/MockLink.cc @@ -7,6 +7,9 @@ #include "QGCApplication.h" #include "QGCLoggingCategory.h" #include "FirmwarePlugin.h" +#include "FactMetaData.h" +#include "ParameterManager.h" +#include "QGC.h" #include #include @@ -696,6 +699,9 @@ void MockLink::_handleIncomingMavlinkMsg(const mavlink_message_t &msg) case MAVLINK_MSG_ID_PARAM_MAP_RC: _handleParamMapRC(msg); break; + case MAVLINK_MSG_ID_SETUP_SIGNING: + _handleSetupSigning(msg); + break; default: break; } @@ -723,6 +729,28 @@ void MockLink::_handleParamMapRC(const mavlink_message_t &msg) } } +void MockLink::_handleSetupSigning(const mavlink_message_t &msg) +{ + mavlink_setup_signing_t setupSigning{}; + mavlink_msg_setup_signing_decode(&msg, &setupSigning); + + if (setupSigning.target_system != _vehicleSystemId) { + return; + } + + // All-zero key = disable signing + bool allZeroKey = true; + for (const uint8_t byte : setupSigning.secret_key) { + if (byte != 0) { + allZeroKey = false; + break; + } + } + + _signingEnabled = !allZeroKey; + qCDebug(MockLinkLog) << "Signing" << (_signingEnabled ? "enabled" : "disabled"); +} + void MockLink::_handleSetMode(const mavlink_message_t &msg) { mavlink_set_mode_t request{}; @@ -784,6 +812,13 @@ void MockLink::_setParamFloatUnionIntoMap(int componentId, const QString ¶mN _mapParamName2Value[componentId][paramName] = paramVariant; } +void MockLink::setMockParamValue(int componentId, const QString ¶mName, float value) +{ + mavlink_param_union_t valueUnion{}; + valueUnion.param_float = value; + _setParamFloatUnionIntoMap(componentId, paramName, valueUnion.param_float); +} + float MockLink::_floatUnionForParam(int componentId, const QString ¶mName) { Q_ASSERT(_mapParamName2Value.contains(componentId)); @@ -852,6 +887,37 @@ float MockLink::_floatUnionForParam(int componentId, const QString ¶mName) return valueUnion.param_float; } +uint32_t MockLink::_computeParamHash(int componentId) const +{ + // Volatile parameters are excluded from the hash, matching PX4 firmware and ParameterManager::_tryCacheHashLoad + static const QStringList volatileParams = { + QStringLiteral("COM_FLIGHT_UUID"), + QStringLiteral("EKF2_MAGBIAS_X"), + QStringLiteral("EKF2_MAGBIAS_Y"), + QStringLiteral("EKF2_MAGBIAS_Z"), + QStringLiteral("EKF2_MAG_DECL"), + QStringLiteral("LND_FLIGHT_T_HI"), + QStringLiteral("LND_FLIGHT_T_LO"), + QStringLiteral("SYS_RESTART_TYPE"), + }; + + uint32_t crc = 0; + const auto ¶ms = _mapParamName2Value[componentId]; + for (auto it = params.constBegin(); it != params.constEnd(); ++it) { + const QString &name = it.key(); + if (volatileParams.contains(name)) { + continue; + } + const QVariant &value = it.value(); + const MAV_PARAM_TYPE mavType = _mapParamName2MavParamType[componentId][name]; + const FactMetaData::ValueType_t factType = ParameterManager::mavTypeToFactType(mavType); + + crc = QGC::crc32(reinterpret_cast(qPrintable(name)), name.length(), crc); + crc = QGC::crc32(static_cast(value.constData()), FactMetaData::typeToSize(factType), crc); + } + return crc; +} + void MockLink::_handleParamRequestList(const mavlink_message_t &msg) { if (_failureMode == MockConfiguration::FailParamNoResponseToRequestList) { @@ -875,6 +941,7 @@ void MockLink::_handleParamRequestList(const mavlink_message_t &msg) // Start the worker routine _currentParamRequestListComponentIndex = 0; _currentParamRequestListParamIndex = 0; + _paramRequestListHashCheckSent = false; } void MockLink::_paramRequestListWorker() @@ -897,6 +964,37 @@ void MockLink::_paramRequestListWorker() const int cParameters = _paramRequestListParamNames.count(); if (_currentParamRequestListParamIndex >= cParameters) { + // All regular params sent — for PX4, append _HASH_CHECK as the last entry in the stream. + // Uses param_count=0, param_index=-1 (same as standalone response) so ParameterManager + // handles it via the _HASH_CHECK early-return path without affecting param count tracking. + if (_firmwareType == MAV_AUTOPILOT_PX4 && !_paramRequestListHashCheckSent) { + _paramRequestListHashCheckSent = true; + + mavlink_param_union_t valueUnion{}; + valueUnion.type = MAV_PARAM_TYPE_UINT32; + valueUnion.param_uint32 = _computeParamHash(componentId); + + char paramId[MAVLINK_MSG_ID_PARAM_VALUE_LEN]{}; + (void) strncpy(paramId, "_HASH_CHECK", MAVLINK_MSG_ID_PARAM_VALUE_LEN); + + qCDebug(MockLinkLog) << "Sending _HASH_CHECK in PARAM_REQUEST_LIST stream" << componentId << "hash:" << valueUnion.param_uint32; + + mavlink_message_t responseMsg{}; + (void) mavlink_msg_param_value_pack_chan( + _vehicleSystemId, + componentId, + mavlinkChannel(), + &responseMsg, + paramId, + valueUnion.param_float, + MAV_PARAM_TYPE_UINT32, + 0, // param_count: 0 to avoid affecting ParameterManager's count tracking + -1 // param_index: -1 signals this is a virtual/out-of-band parameter + ); + respondWithMavlinkMessage(responseMsg); + return; + } + // Move to next component if (++_currentParamRequestListComponentIndex >= _paramRequestListComponentIds.count()) { _currentParamRequestListComponentIndex = -1; @@ -906,6 +1004,7 @@ void MockLink::_paramRequestListWorker() // Cache param names for the new component _paramRequestListParamNames = _mapParamName2Value[_paramRequestListComponentIds.at(_currentParamRequestListComponentIndex)].keys(); _currentParamRequestListParamIndex = 0; + _paramRequestListHashCheckSent = false; } return; } @@ -961,6 +1060,18 @@ void MockLink::_handleParamSet(const mavlink_message_t &msg) qCDebug(MockLinkLog) << "_handleParamSet" << componentId << paramId << request.param_type; + // PX4 special case: _HASH_CHECK is a virtual parameter used by ParameterManager + // to signal cache-hit and stop parameter streaming. It is intentionally not part + // of the normal parameter maps. + if ((_firmwareType == MAV_AUTOPILOT_PX4) && (strncmp(paramId, "_HASH_CHECK", MAVLINK_MSG_PARAM_SET_FIELD_PARAM_ID_LEN) == 0)) { + QMutexLocker locker(&_paramRequestListMutex); + _currentParamRequestListComponentIndex = -1; + _paramRequestListComponentIds.clear(); + _paramRequestListParamNames.clear(); + qCDebug(MockLinkLog) << "Received _HASH_CHECK PARAM_SET, stopping parameter stream"; + return; + } + Q_ASSERT(_mapParamName2Value.contains(componentId)); Q_ASSERT(_mapParamName2MavParamType.contains(componentId)); Q_ASSERT(_mapParamName2Value[componentId].contains(paramId)); @@ -1005,15 +1116,23 @@ void MockLink::_handleParamRequestRead(const mavlink_message_t &msg) const QString paramName(QString::fromLocal8Bit(request.param_id, static_cast(strnlen(request.param_id, MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN)))); const int componentId = request.target_component; - // special case for magic _HASH_CHECK value - if ((request.target_component == MAV_COMP_ID_ALL) && (paramName == "_HASH_CHECK")) { + // special case for magic _HASH_CHECK value (PX4 only) + if ((_firmwareType == MAV_AUTOPILOT_PX4) && (paramName == "_HASH_CHECK")) { + _hashCheckRequestCount++; + if (_hashCheckNoResponse) { + return; + } + + const int hashComponentId = _mapParamName2Value.contains(MAV_COMP_ID_AUTOPILOT1) + ? MAV_COMP_ID_AUTOPILOT1 + : _mapParamName2Value.keys().first(); + mavlink_param_union_t valueUnion{}; valueUnion.type = MAV_PARAM_TYPE_UINT32; - valueUnion.param_uint32 = 0; - // Special case of magic hash check value + valueUnion.param_uint32 = _computeParamHash(hashComponentId); (void) mavlink_msg_param_value_pack_chan( _vehicleSystemId, - componentId, + hashComponentId, mavlinkChannel(), &responseMsg, request.param_id, @@ -1888,6 +2007,17 @@ void MockLink::_handleRequestMessage(const mavlink_command_long_t &request, bool accepted = false; noAck = false; + const uint32_t requestedMessageId = static_cast(request.param1); + + // Per-message-ID no-response injection: silently drop the request (no ACK, no message) + { + QMutexLocker locker(&_requestMessageNoResponseMutex); + if (_requestMessageNoResponseIds.contains(requestedMessageId)) { + noAck = true; + return; + } + } + switch (static_cast(request.param1)) { case MAVLINK_MSG_ID_AUTOPILOT_VERSION: _handleRequestMessageAutopilotVersion(request, accepted); diff --git a/src/Comms/MockLink/MockLink.h b/src/Comms/MockLink/MockLink.h index 6775137fdfe8..52d5259d0021 100644 --- a/src/Comms/MockLink/MockLink.h +++ b/src/Comms/MockLink/MockLink.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -51,6 +52,8 @@ class MockLink : public LinkInterface double vehicleLongitude() const { return _vehicleLongitude; } double vehicleAltitudeAMSL() const { return _vehicleAltitudeAMSL; } + bool signingEnabled() const { return _signingEnabled; } + /// Sends the specified mavlink message to QGC void respondWithMavlinkMessage(const mavlink_message_t &msg); @@ -84,7 +87,7 @@ class MockLink : public LinkInterface int receivedRequestMessageCount(int compId, int messageId) const { return _receivedRequestMessageByCompAndMsgCountMap.value(compId).value(messageId, 0); } void clearReceivedRequestMessageCounts() { _receivedRequestMessageCountMap.clear(); _receivedRequestMessageByCompAndMsgCountMap.clear(); } int receivedRequestMessageCount(uint32_t messageId) const { return _receivedRequestMessageCountMap.value(messageId, 0); } - void clearReceivedMavlinkMessageCounts() { _receivedMavlinkMessageCountMap.clear(); } + void clearReceivedMavlinkMessageCounts() { _receivedMavlinkMessageCountMap.clear(); _hashCheckRequestCount = 0; } int receivedMavlinkMessageCount(uint32_t messageId) const { return _receivedMavlinkMessageCountMap.value(messageId, 0); } enum RequestMessageFailureMode_t { @@ -95,6 +98,17 @@ class MockLink : public LinkInterface }; void setRequestMessageFailureMode(RequestMessageFailureMode_t failureMode) { _requestMessageFailureMode = failureMode; } + /// Block or unblock REQUEST_MESSAGE responses for a specific message ID. + /// When blocked, MockLink silently drops the request (no ACK, no message). + void setRequestMessageNoResponse(uint32_t messageId, bool noResponse = true) { + QMutexLocker locker(&_requestMessageNoResponseMutex); + if (noResponse) { + _requestMessageNoResponseIds.insert(messageId); + } else { + _requestMessageNoResponseIds.remove(messageId); + } + } + enum ParamSetFailureMode_t { FailParamSetNone, ///< Normal behavior FailParamSetNoAck, ///< Do not send PARAM_VALUE ack @@ -115,6 +129,14 @@ class MockLink : public LinkInterface _paramRequestReadFailureFirstAttemptPending = (mode == FailParamRequestReadFirstAttemptNoResponse); } + void setHashCheckNoResponse(bool noResponse) { _hashCheckNoResponse = noResponse; } + + /// Returns the number of standalone PARAM_REQUEST_READ requests for _HASH_CHECK received + int hashCheckRequestCount() const { return _hashCheckRequestCount; } + + /// Change a float parameter value directly on MockLink (for testing cache invalidation) + void setMockParamValue(int componentId, const QString ¶mName, float value); + static MockLink *startPX4MockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); static MockLink *startGenericMockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); static MockLink *startNoInitialConnectMockLink(bool sendStatusText, bool enableCamera, bool enableGimbal, MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); @@ -163,6 +185,7 @@ private slots: /// Convert from a parameter variant to the float value from mavlink_param_union_t float _floatUnionForParam(int componentId, const QString ¶mName); + uint32_t _computeParamHash(int componentId) const; void _setParamFloatUnionIntoMap(int componentId, const QString ¶mName, float paramFloat); /// Handle incoming bytes which are meant to be interpreted by the NuttX shell @@ -187,6 +210,7 @@ private slots: void _handleLogRequestList(const mavlink_message_t &msg); void _handleLogRequestData(const mavlink_message_t &msg); void _handleParamMapRC(const mavlink_message_t &msg); + void _handleSetupSigning(const mavlink_message_t &msg); void _handleRequestMessage(const mavlink_command_long_t &request, bool &accepted, bool &noAck); void _handleRequestMessageAutopilotVersion(const mavlink_command_long_t &request, bool &accepted); void _handleRequestMessageDebug(const mavlink_command_long_t &request, bool &accepted, bool &noAck); @@ -268,6 +292,7 @@ private slots: double _vehicleAltitudeAMSL = _defaultVehicleHomeAltitude; bool _commLost = false; + bool _signingEnabled = false; bool _highLatencyTransmissionEnabled = true; int _sendHomePositionDelayCount = 10; ///< No home position for 4 seconds @@ -299,10 +324,15 @@ private slots: QMutex _logDownloadMutex; RequestMessageFailureMode_t _requestMessageFailureMode = FailRequestMessageNone; + mutable QMutex _requestMessageNoResponseMutex; + QSet _requestMessageNoResponseIds; ParamSetFailureMode_t _paramSetFailureMode = FailParamSetNone; bool _paramSetFailureFirstAttemptPending = false; ParamRequestReadFailureMode_t _paramRequestReadFailureMode = FailParamRequestReadNone; bool _paramRequestReadFailureFirstAttemptPending = false; + bool _hashCheckNoResponse = false; + int _hashCheckRequestCount = 0; + bool _paramRequestListHashCheckSent = false; QMap _receivedMavCommandCountMap; QMap> _receivedMavCommandByCompCountMap; diff --git a/src/Comms/MockLink/MockLinkCamera.cc b/src/Comms/MockLink/MockLinkCamera.cc index feeb5db2ea36..2cc680e65ab5 100644 --- a/src/Comms/MockLink/MockLinkCamera.cc +++ b/src/Comms/MockLink/MockLinkCamera.cc @@ -5,6 +5,9 @@ #include #include +#include + +#include QGC_LOGGING_CATEGORY(MockLinkCameraLog, "Comms.MockLink.MockLinkCamera") @@ -105,6 +108,34 @@ void MockLinkCamera::run10HzTasks() _sendCameraImageCaptured(cam->compId); _sendCameraCaptureStatus(cam->compId); } + + // Send periodic tracking image status (with simulated drift) + if (cam->trackingMode != CAMERA_TRACKING_MODE_NONE && cam->trackingStatusIntervalUs > 0) { + const qint64 intervalMs = (cam->trackingStatusIntervalUs + 999) / 1000; + if (cam->trackingStatusLastSentMs == 0 || (now - cam->trackingStatusLastSentMs) >= intervalMs) { + // Drift the tracked target in a figure-8 pattern around its anchor + const double elapsed = static_cast(now - cam->trackingStartMs) / 1000.0; + const float driftX = 0.05f * static_cast(qSin(elapsed * 0.7)); + const float driftY = 0.05f * static_cast(qSin(elapsed * 1.1)); + + if (cam->trackingMode == CAMERA_TRACKING_MODE_POINT) { + cam->trackPointX = std::clamp(cam->trackAnchorX + driftX, 0.0f, 1.0f); + cam->trackPointY = std::clamp(cam->trackAnchorY + driftY, 0.0f, 1.0f); + } else { + const float halfW = (cam->trackRecBottomX - cam->trackRecTopX) / 2.0f; + const float halfH = (cam->trackRecBottomY - cam->trackRecTopY) / 2.0f; + const float cx = std::clamp(cam->trackAnchorX + driftX, halfW, 1.0f - halfW); + const float cy = std::clamp(cam->trackAnchorY + driftY, halfH, 1.0f - halfH); + cam->trackRecTopX = cx - halfW; + cam->trackRecTopY = cy - halfH; + cam->trackRecBottomX = cx + halfW; + cam->trackRecBottomY = cy + halfH; + } + + _sendCameraTrackingImageStatus(cam->compId); + cam->trackingStatusLastSentMs = now; + } + } } } @@ -344,12 +375,64 @@ bool MockLinkCamera::_handleCameraCommand(const mavlink_command_long_t &request, return true; case MAV_CMD_CAMERA_TRACK_POINT: + if (cam->capFlags & CAMERA_CAP_FLAGS_HAS_TRACKING_POINT) { + cam->trackingMode = CAMERA_TRACKING_MODE_POINT; + cam->trackPointX = request.param1; + cam->trackPointY = request.param2; + cam->trackRadius = request.param3; + cam->trackAnchorX = request.param1; + cam->trackAnchorY = request.param2; + cam->trackingStartMs = QDateTime::currentMSecsSinceEpoch(); + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking point" + << cam->trackPointX << cam->trackPointY << "radius" << cam->trackRadius; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + } else { + _sendCommandAck(targetCompId, request.command, MAV_RESULT_DENIED); + } + return true; + case MAV_CMD_CAMERA_TRACK_RECTANGLE: + if (cam->capFlags & CAMERA_CAP_FLAGS_HAS_TRACKING_RECTANGLE) { + cam->trackingMode = CAMERA_TRACKING_MODE_RECTANGLE; + cam->trackRecTopX = request.param1; + cam->trackRecTopY = request.param2; + cam->trackRecBottomX = request.param3; + cam->trackRecBottomY = request.param4; + cam->trackAnchorX = (request.param1 + request.param3) / 2.0f; + cam->trackAnchorY = (request.param2 + request.param4) / 2.0f; + cam->trackingStartMs = QDateTime::currentMSecsSinceEpoch(); + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking rectangle" + << cam->trackRecTopX << cam->trackRecTopY + << "->" << cam->trackRecBottomX << cam->trackRecBottomY; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + } else { + _sendCommandAck(targetCompId, request.command, MAV_RESULT_DENIED); + } + return true; + case MAV_CMD_CAMERA_STOP_TRACKING: - qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking command" << request.command; + cam->trackingMode = CAMERA_TRACKING_MODE_NONE; + cam->trackingStatusIntervalUs = -1; + cam->trackingStatusLastSentMs = 0; + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId << "tracking stopped"; _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); return true; + case MAV_CMD_SET_MESSAGE_INTERVAL: + { + const int msgId = static_cast(request.param1); + if (msgId == MAVLINK_MSG_ID_CAMERA_TRACKING_IMAGE_STATUS) { + cam->trackingStatusIntervalUs = static_cast(request.param2); + cam->trackingStatusLastSentMs = 0; + qCDebug(MockLinkCameraLog) << "Camera" << targetCompId + << "tracking status interval" << cam->trackingStatusIntervalUs << "us"; + _sendCommandAck(targetCompId, request.command, MAV_RESULT_ACCEPTED); + return true; + } + _sendCommandAck(targetCompId, request.command, MAV_RESULT_UNSUPPORTED); + return true; + } + default: break; } @@ -650,6 +733,33 @@ void MockLinkCamera::_sendVideoStreamStatus(uint8_t compId, uint8_t streamId) qCDebug(MockLinkCameraLog) << "Sent VIDEO_STREAM_STATUS for compId:" << compId << "stream:" << streamId; } +void MockLinkCamera::_sendCameraTrackingImageStatus(uint8_t compId) +{ + const CameraState *cam = _findCamera(compId); + if (!cam || cam->trackingMode == CAMERA_TRACKING_MODE_NONE) { + return; + } + + mavlink_message_t msg{}; + (void) mavlink_msg_camera_tracking_image_status_pack_chan( + _mockLink->vehicleId(), + compId, + _mockLink->mavlinkChannel(), + &msg, + CAMERA_TRACKING_STATUS_FLAGS_ACTIVE, // tracking_status + cam->trackingMode, // tracking_mode + CAMERA_TRACKING_TARGET_DATA_EMBEDDED, // target_data + cam->trackPointX, // point_x + cam->trackPointY, // point_y + cam->trackRadius, // radius + cam->trackRecTopX, // rec_top_x + cam->trackRecTopY, // rec_top_y + cam->trackRecBottomX, // rec_bottom_x + cam->trackRecBottomY, // rec_bottom_y + 0); // camera_device_id + _mockLink->respondWithMavlinkMessage(msg); +} + void MockLinkCamera::_sendCommandAck(uint8_t compId, uint16_t command, uint8_t result, int requestedMsgId) { mavlink_message_t msg{}; diff --git a/src/Comms/MockLink/MockLinkCamera.h b/src/Comms/MockLink/MockLinkCamera.h index f7e9a1eadbcf..d489449c02ad 100644 --- a/src/Comms/MockLink/MockLinkCamera.h +++ b/src/Comms/MockLink/MockLinkCamera.h @@ -57,6 +57,21 @@ class MockLinkCamera uint8_t image_status = ImageCaptureIdle; ///< ImageCaptureStatus enum float image_interval = 0.0f; ///< Interval between image captures (seconds) qint64 singleShotStartMs = 0; ///< Timestamp when single-shot capture started (0 = not active) + + // Tracking state + uint8_t trackingMode = CAMERA_TRACKING_MODE_NONE; ///< CAMERA_TRACKING_MODE enum + float trackPointX = 0.0f; + float trackPointY = 0.0f; + float trackRadius = 0.0f; + float trackRecTopX = 0.0f; + float trackRecTopY = 0.0f; + float trackRecBottomX = 0.0f; + float trackRecBottomY = 0.0f; + qint64 trackingStatusIntervalUs = -1; ///< Interval for CAMERA_TRACKING_IMAGE_STATUS (-1 = disabled) + qint64 trackingStatusLastSentMs = 0; ///< Timestamp of last tracking status message + qint64 trackingStartMs = 0; ///< Timestamp when tracking was started (for drift animation) + float trackAnchorX = 0.0f; ///< Original center X of tracked target + float trackAnchorY = 0.0f; ///< Original center Y of tracked target }; explicit MockLinkCamera(MockLink *mockLink, @@ -97,6 +112,7 @@ class MockLinkCamera void _sendCameraImageCaptured(uint8_t compId); void _sendVideoStreamInformation(uint8_t compId, uint8_t streamId); void _sendVideoStreamStatus(uint8_t compId, uint8_t streamId); + void _sendCameraTrackingImageStatus(uint8_t compId); void _sendCommandAck(uint8_t compId, uint16_t command, uint8_t result, int requestedMsgId = -1); CameraState *_findCamera(uint8_t compId); diff --git a/src/Comms/MockLink/PX4MockLink.params b/src/Comms/MockLink/PX4MockLink.params index 7dc95d5c9df3..556fb48875c6 100644 --- a/src/Comms/MockLink/PX4MockLink.params +++ b/src/Comms/MockLink/PX4MockLink.params @@ -21,6 +21,15 @@ 1 1 BAT1_V_EMPTY 3.400000095367431641 9 1 1 BAT1_V_LOAD_DROP 0.300000011920928955 9 1 1 BAT1_V_OFFS_CURR 0.000000000000000000 9 +1 1 BAT2_A_PER_V 15.391030311584472656 9 +1 1 BAT2_CAPACITY -1.000000000000000000 9 +1 1 BAT2_N_CELLS 3 6 +1 1 BAT2_R_INTERNAL -1.000000000000000000 9 +1 1 BAT2_SOURCE 0 6 +1 1 BAT2_V_CHARGED 4.050000190734863281 9 +1 1 BAT2_V_DIV 10.177939414978027344 9 +1 1 BAT2_V_EMPTY 3.400000095367431641 9 +1 1 BAT2_V_LOAD_DROP 0.300000011920928955 9 1 1 CAL_ACC0_EN 1 6 1 1 CAL_ACC0_ID 1376264 6 1 1 CAL_ACC0_XOFF 0.001000000047497451 9 diff --git a/src/Comms/QGCSerialPortInfo.cc b/src/Comms/QGCSerialPortInfo.cc index e66c4442c815..ad1c338cf756 100644 --- a/src/Comms/QGCSerialPortInfo.cc +++ b/src/Comms/QGCSerialPortInfo.cc @@ -110,8 +110,15 @@ bool QGCSerialPortInfo::_loadJsonData() return false; } + const QRegularExpression regExp(fallbackObject[_jsonRegExpKey].toString(), QRegularExpression::CaseInsensitiveOption); + if (!regExp.isValid()) { + qCWarning(QGCSerialPortInfoLog) << "Invalid regular expression in board description fallback:" + << regExp.errorString() + << "pattern:" << fallbackObject[_jsonRegExpKey].toString(); + return false; + } const BoardRegExpFallback_t boardFallback = { - fallbackObject[_jsonRegExpKey].toString(), + regExp, _boardClassStringToType(fallbackObject[_jsonBoardClassKey].toString()), fallbackObject[_jsonAndroidOnlyKey].toBool(false) }; @@ -136,8 +143,15 @@ bool QGCSerialPortInfo::_loadJsonData() return false; } + const QRegularExpression regExp(fallbackObject[_jsonRegExpKey].toString(), QRegularExpression::CaseInsensitiveOption); + if (!regExp.isValid()) { + qCWarning(QGCSerialPortInfoLog) << "Invalid regular expression in board manufacturer fallback:" + << regExp.errorString() + << "pattern:" << fallbackObject[_jsonRegExpKey].toString(); + return false; + } const BoardRegExpFallback_t boardFallback = { - fallbackObject[_jsonRegExpKey].toString(), + regExp, _boardClassStringToType(fallbackObject[_jsonBoardClassKey].toString()), fallbackObject[_jsonAndroidOnlyKey].toBool(false) }; @@ -195,7 +209,7 @@ bool QGCSerialPortInfo::getBoardInfo(QGCSerialPortInfo::BoardType_t &boardType, Q_ASSERT(boardType == BoardTypeUnknown); for (const BoardRegExpFallback_t &boardFallback : _boardDescriptionFallbackList) { - if (description().contains(QRegularExpression(boardFallback.regExp, QRegularExpression::CaseInsensitiveOption))) { + if (description().contains(boardFallback.regExp)) { #ifndef Q_OS_ANDROID if (boardFallback.androidOnly) { continue; @@ -208,7 +222,7 @@ bool QGCSerialPortInfo::getBoardInfo(QGCSerialPortInfo::BoardType_t &boardType, } for (const BoardRegExpFallback_t &boardFallback : _boardManufacturerFallbackList) { - if (manufacturer().contains(QRegularExpression(boardFallback.regExp, QRegularExpression::CaseInsensitiveOption))) { + if (manufacturer().contains(boardFallback.regExp)) { #ifndef Q_OS_ANDROID if (boardFallback.androidOnly) { continue; diff --git a/src/Comms/QGCSerialPortInfo.h b/src/Comms/QGCSerialPortInfo.h index 79c70966f9f1..500ff992053d 100644 --- a/src/Comms/QGCSerialPortInfo.h +++ b/src/Comms/QGCSerialPortInfo.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #ifdef Q_OS_ANDROID #include "qserialportinfo.h" @@ -70,7 +71,7 @@ class QGCSerialPortInfo : public QSerialPortInfo static QList _boardInfoList; struct BoardRegExpFallback_t { - QString regExp; + QRegularExpression regExp; BoardType_t boardType; bool androidOnly; }; diff --git a/src/Comms/USBBoardInfo.json b/src/Comms/USBBoardInfo.json index dc68a3ca6a92..3dccd3811a7a 100644 --- a/src/Comms/USBBoardInfo.json +++ b/src/Comms/USBBoardInfo.json @@ -35,6 +35,8 @@ { "vendorID": 12677, "productID": 57, "boardClass": "Pixhawk", "name": "ARK FMU V6X" }, { "vendorID": 12677, "productID": 58, "boardClass": "Pixhawk", "name": "ARK Pi6X" }, { "vendorID": 12677, "productID": 59, "boardClass": "Pixhawk", "name": "ARK FPV" }, + { "vendorID": 12677, "productID": 61, "boardClass": "Pixhawk", "name": "ARK FMU V6S" }, + { "vendorID": 12677, "productID": 62, "boardClass": "Pixhawk", "name": "ARK FMU V6XRT" }, { "vendorID": 1155, "productID": 22336, "boardClass": "Pixhawk", "name": "ArduPilot ChibiOS" }, { "vendorID": 4617, "productID": 22336, "boardClass": "Pixhawk", "name": "ArduPilot ChibiOS" }, @@ -118,6 +120,10 @@ { "regExp": "^ARK BL FPV.x$", "boardClass": "Pixhawk" }, { "regExp": "^ARK Pi6X.x$", "boardClass": "Pixhawk" }, { "regExp": "^ARK BL Pi6X.x$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK FMU v6S$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK BL FMU v6S$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK FMU v6XRT$", "boardClass": "Pixhawk" }, + { "regExp": "^ARK BL FMU v6XRT$", "boardClass": "Pixhawk" }, { "regExp": "^PX4 TROPIC Community","boardClass": "Pixhawk" }, { "regExp": "^PX4 MR-TROPIC", "boardClass": "Pixhawk" }, { "regExp": "^PX4 BL MR-TROPIC", "boardClass": "Pixhawk" }, diff --git a/src/FactSystem/Fact.cc b/src/FactSystem/Fact.cc index 3044a6f6d391..35e661abfcc0 100644 --- a/src/FactSystem/Fact.cc +++ b/src/FactSystem/Fact.cc @@ -50,7 +50,6 @@ Fact::Fact(const QString& settingsGroup, FactMetaData *metaData, QObject *parent const QVariant defaultValue = metaData->rawDefaultValue(); QMutexLocker locker(&_rawValueMutex); _rawValue = defaultValue; - _rawValueIsNotSet = false; } } @@ -89,7 +88,6 @@ const Fact &Fact::operator=(const Fact& other) _name = other._name; _componentId = other._componentId; _rawValue = other._rawValue; - _rawValueIsNotSet = other._rawValueIsNotSet; _type = other._type; _sendValueChangedSignals = other._sendValueChangedSignals; _deferredValueChangeSignal = other._deferredValueChangeSignal; @@ -113,7 +111,6 @@ void Fact::forceSetRawValue(const QVariant &value) { QMutexLocker locker(&_rawValueMutex); _rawValue = typedValue; - _rawValueIsNotSet = false; } const QVariant cooked = _metaData->rawTranslator()(typedValue); @@ -139,7 +136,6 @@ void Fact::setRawValue(const QVariant &value) QMutexLocker locker(&_rawValueMutex); if (typedValue != _rawValue) { _rawValue = typedValue; - _rawValueIsNotSet = false; changed = true; } } @@ -201,7 +197,6 @@ void Fact::containerSetRawValue(const QVariant &value) QMutexLocker locker(&_rawValueMutex); if (_rawValue != value) { _rawValue = value; - _rawValueIsNotSet = false; changed = true; } currentRaw = _rawValue; @@ -357,12 +352,6 @@ QStringList Fact::selectedBitmaskStrings() const QString Fact::_variantToString(const QVariant &variant, int decimalPlaces) const { - QMutexLocker locker(&_rawValueMutex); - if (_rawValueIsNotSet) { - return invalidValueString(decimalPlaces); - } - locker.unlock(); - QString valueString; const auto stripNegativeZero = [](QString &candidate) { diff --git a/src/FactSystem/Fact.h b/src/FactSystem/Fact.h index 484ba722aa87..67598eb08f63 100644 --- a/src/FactSystem/Fact.h +++ b/src/FactSystem/Fact.h @@ -202,7 +202,6 @@ class Fact : public QObject QString _name; int _componentId = -1; QVariant _rawValue{0}; - bool _rawValueIsNotSet = true; mutable QRecursiveMutex _rawValueMutex; FactMetaData::ValueType_t _type = FactMetaData::valueTypeInt32; FactMetaData *_metaData = nullptr; diff --git a/src/FactSystem/FactControls/AltitudeFactTextField.qml b/src/FactSystem/FactControls/AltitudeFactTextField.qml index b6796650919f..8015855aceae 100644 --- a/src/FactSystem/FactControls/AltitudeFactTextField.qml +++ b/src/FactSystem/FactControls/AltitudeFactTextField.qml @@ -7,17 +7,17 @@ import QGroundControl.Controls FactTextField { unitsLabel: fact ? fact.units : "" - extraUnitsLabel: fact ? _altitudeModeExtraUnits : "" + extraUnitsLabel: fact ? qsTr("%1").arg(_altitudeFrameExtraUnits) : "" showUnits: true showHelp: true - property int altitudeMode: QGroundControl.AltitudeModeNone + property int altitudeFrame: QGroundControl.AltitudeFrameNone - property string _altitudeModeExtraUnits + property string _altitudeFrameExtraUnits - onAltitudeModeChanged: updateAltitudeModeExtraUnits() + onAltitudeFrameChanged: updateAltitudeFrameExtraUnits() - function updateAltitudeModeExtraUnits() { - _altitudeModeExtraUnits = QGroundControl.altitudeModeExtraUnits(altitudeMode); + function updateAltitudeFrameExtraUnits() { + _altitudeFrameExtraUnits = QGroundControl.altitudeFrameExtraUnits(altitudeFrame); } } diff --git a/src/FactSystem/ParameterManager.cc b/src/FactSystem/ParameterManager.cc index 5878c59a4398..46a2d4e57969 100644 --- a/src/FactSystem/ParameterManager.cc +++ b/src/FactSystem/ParameterManager.cc @@ -41,10 +41,13 @@ ParameterManager::ParameterManager(Vehicle *vehicle) qCDebug(ParameterManagerLog) << this << "In log replay mode"; } - _initialRequestTimeoutTimer.setSingleShot(true); - // Use much shorter timeouts in unit tests since MockLink responds instantly - _initialRequestTimeoutTimer.setInterval(qgcApp()->runningUnitTests() ? 500 : 5000); - (void) connect(&_initialRequestTimeoutTimer, &QTimer::timeout, this, &ParameterManager::_initialRequestTimeout); + _hashCheckTimer.setSingleShot(true); + _hashCheckTimer.setInterval(kHashCheckTimeoutMs); + (void) connect(&_hashCheckTimer, &QTimer::timeout, this, &ParameterManager::_hashCheckTimeout); + + _paramRequestListTimer.setSingleShot(true); + _paramRequestListTimer.setInterval(qgcApp()->runningUnitTests() ? kTestInitialRequestIntervalMs : kParamRequestListTimeoutMs); + (void) connect(&_paramRequestListTimer, &QTimer::timeout, this, &ParameterManager::_paramRequestListTimeout); _waitingParamTimeoutTimer.setSingleShot(true); _waitingParamTimeoutTimer.setInterval(qgcApp()->runningUnitTests() ? 500 : 3000); @@ -123,14 +126,13 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met // ArduPilot has this strange behavior of streaming parameters that we didn't ask for. This even happens before it responds to the // PARAM_REQUEST_LIST. We disregard any of this until the initial request is responded to. - if ((parameterIndex == 65535) && (parameterName != QStringLiteral("_HASH_CHECK")) && _initialRequestTimeoutTimer.isActive()) { + if ((parameterIndex == 65535) && (parameterName != QStringLiteral("_HASH_CHECK")) && _paramRequestListTimer.isActive()) { qCDebug(ParameterManagerLog) << "Disregarding unrequested param prior to initial list response" << parameterName; return; } - _initialRequestTimeoutTimer.stop(); - if (_vehicle->px4Firmware() && (parameterName == "_HASH_CHECK")) { + _hashCheckTimer.stop(); if (!_initialLoadComplete && !_logReplay) { /* we received a cache hash, potentially load from cache */ _tryCacheHashLoad(_vehicle->id(), componentId, parameterValue); @@ -138,6 +140,8 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met return; } + _paramRequestListTimer.stop(); + // Used to debug cache crc misses (turn on ParameterManagerDebugCacheFailureLog) if (!_initialLoadComplete && !_logReplay && _debugCacheCRC.contains(componentId) && _debugCacheCRC[componentId]) { if (_debugCacheMap[componentId].contains(parameterName)) { @@ -155,7 +159,6 @@ void ParameterManager::_handleParamValue(int componentId, const QString ¶met } } - _initialRequestTimeoutTimer.stop(); _waitingParamTimeoutTimer.stop(); // Update our total parameter counts @@ -521,12 +524,12 @@ void ParameterManager::_ftpDownloadComplete(const QString &fileName, const QStri * can immediately response with the conventional parameter download request, because * we have no indication of communication link congestion.*/ if (immediateRetry) { - _initialRequestTimeout(); + _paramRequestListTimeout(); } else { - _initialRequestTimeoutTimer.start(); + _paramRequestListTimer.start(); } } else { - _initialRequestTimeoutTimer.start(); + _paramRequestListTimer.start(); } } @@ -535,7 +538,7 @@ void ParameterManager::_ftpDownloadProgress(float progress) qCDebug(ParameterManagerVerbose1Log) << "ParameterManager::_ftpDownloadProgress:" << progress; _setLoadProgress(static_cast(progress)); if (progress > 0.001) { - _initialRequestTimeoutTimer.stop(); + _paramRequestListTimer.stop(); } } @@ -556,11 +559,10 @@ void ParameterManager::refreshAllParameters(uint8_t componentId) emit missingParametersChanged(_missingParameters); } - if (!_initialLoadComplete) { - _initialRequestTimeoutTimer.start(); - } - if (_tryftp && ((componentId == MAV_COMP_ID_ALL) || (componentId == MAV_COMP_ID_AUTOPILOT1))) { + if (!_initialLoadComplete) { + _paramRequestListTimer.start(); + } FTPManager *const ftpManager = _vehicle->ftpManager(); (void) connect(ftpManager, &FTPManager::downloadComplete, this, &ParameterManager::_ftpDownloadComplete); _waitingParamTimeoutTimer.stop(); @@ -574,7 +576,19 @@ void ParameterManager::refreshAllParameters(uint8_t componentId) qCWarning(ParameterManagerLog) << "ParameterManager::refreshallParameters FTPManager::download returned failure"; (void) disconnect(ftpManager, &FTPManager::downloadComplete, this, &ParameterManager::_ftpDownloadComplete); } + } else if (_vehicle->px4Firmware() && !_initialLoadComplete && !_hashCheckDone) { + // PX4: Try _HASH_CHECK first to see if we can load from cache without a full parameter stream + _hashCheckTimer.start(); + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "Requesting _HASH_CHECK before full parameter list"; + const uint8_t hashCheckCompId = (componentId == MAV_COMP_ID_ALL) + ? static_cast(MAV_COMP_ID_AUTOPILOT1) + : componentId; + _requestHashCheck(hashCheckCompId); } else { + if (!_initialLoadComplete) { + _paramRequestListTimer.start(); + } + // Reset index wait lists for (int cid: _paramCountMap.keys()) { // Add/Update all indices to the wait list, parameter index is 0-based @@ -753,6 +767,32 @@ void ParameterManager::_waitingParamTimeout() } } +void ParameterManager::_requestHashCheck(uint8_t componentId) +{ + const SharedLinkInterfacePtr sharedLink = _vehicle->vehicleLinkManager()->primaryLink().lock(); + if (!sharedLink) { + return; + } + + qCDebug(ParameterManagerLog) << _logVehiclePrefix(componentId) << "Sending PARAM_REQUEST_READ for _HASH_CHECK"; + + char paramId[MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN + 1] = {}; + (void) strncpy(paramId, "_HASH_CHECK", MAVLINK_MSG_PARAM_REQUEST_READ_FIELD_PARAM_ID_LEN); + + mavlink_message_t msg{}; + (void) mavlink_msg_param_request_read_pack_chan( + MAVLinkProtocol::instance()->getSystemId(), + MAVLinkProtocol::getComponentId(), + sharedLink->mavlinkChannel(), + &msg, + static_cast(_vehicle->id()), + componentId, + paramId, + -1); + + (void) _vehicle->sendMessageOnLinkThreadSafe(sharedLink.get(), msg); +} + void ParameterManager::_mavlinkParamRequestRead(int componentId, const QString ¶mName, int paramIndex, bool notifyFailure) { auto paramRequestReadEncoder = [this, componentId, paramName, paramIndex](uint8_t /*systemId*/, uint8_t channel, mavlink_message_t *message) -> void { @@ -881,13 +921,19 @@ QString ParameterManager::parameterCacheFile(int vehicleId, int componentId) void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const QVariant &hashValue) { - qCInfo(ParameterManagerLog) << "Attemping load from cache"; + qCDebug(ParameterManagerLog) << "Attemping load from cache"; /* The datastructure of the cache table */ CacheMapName2ParamTypeVal cacheMap; QFile cacheFile(parameterCacheFile(vehicleId, componentId)); if (!cacheFile.exists()) { - /* no local cache, just wait for them to come in*/ + qCDebug(ParameterManagerLog) << "No parameter cache file"; + if (!_hashCheckDone) { + // Standalone hash check path — fall back to PARAM_REQUEST_LIST + _hashCheckDone = true; + refreshAllParameters(); + } + // If already in PARAM_REQUEST_LIST flow, just let the stream continue return; } (void) cacheFile.open(QIODevice::ReadOnly); @@ -915,7 +961,9 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q /* if the two param set hashes match, just load from the disk */ if (crc32_value == hashValue.toUInt()) { - qCInfo(ParameterManagerLog) << "Parameters loaded from cache" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); + _hashCheckDone = true; + _paramRequestListTimer.stop(); + qCDebug(ParameterManagerLog) << "Parameters loaded from cache" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); const int count = cacheMap.count(); int index = 0; @@ -961,14 +1009,14 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q // Hide 500ms after animation finishes connect(ani, &QVariantAnimation::finished, this, [this] { - QTimer::singleShot(500, [this] { + QTimer::singleShot(500, this, [this] { _setLoadProgress(0); }); }); ani->start(QAbstractAnimation::DeleteWhenStopped); } else { - qCInfo(ParameterManagerLog) << "Parameters cache match failed" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); + qCDebug(ParameterManagerLog) << "Parameters cache match failed" << qPrintable(QFileInfo(cacheFile).absoluteFilePath()); if (ParameterManagerDebugCacheFailureLog().isDebugEnabled()) { _debugCacheCRC[componentId] = true; _debugCacheMap[componentId] = cacheMap; @@ -977,6 +1025,12 @@ void ParameterManager::_tryCacheHashLoad(int vehicleId, int componentId, const Q } qgcApp()->showAppMessage(tr("Parameter cache CRC match failed")); } + if (!_hashCheckDone) { + // Standalone hash check path — fall back to PARAM_REQUEST_LIST + _hashCheckDone = true; + refreshAllParameters(); + } + // If already in PARAM_REQUEST_LIST flow, just let the stream continue } } @@ -1195,11 +1249,18 @@ void ParameterManager::_checkInitialLoadComplete() emit missingParametersChanged(_missingParameters); } -void ParameterManager::_initialRequestTimeout() +void ParameterManager::_hashCheckTimeout() +{ + _hashCheckDone = true; + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_HASH_CHECK timed out, falling back to PARAM_REQUEST_LIST"; + refreshAllParameters(); +} + +void ParameterManager::_paramRequestListTimeout() { if (_logReplay) { // Signal load complete - qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_initialRequestTimeout (log replay): Signalling load complete"; + qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "_paramRequestListTimeout (log replay): Signalling load complete"; _initialLoadComplete = true; _missingParameters = false; _parametersReady = true; @@ -1212,7 +1273,6 @@ void ParameterManager::_initialRequestTimeout() if (!_disableAllRetries && (++_initialRequestRetryCount <= _maxInitialRequestListRetry)) { qCDebug(ParameterManagerLog) << _logVehiclePrefix(-1) << "Retrying initial parameter request list"; refreshAllParameters(); - _initialRequestTimeoutTimer.start(); } else if (!_vehicle->genericFirmware()) { const QString errorMsg = tr("Vehicle %1 did not respond to request for parameters. " "This will cause %2 to be unable to display its full user interface.").arg(_vehicle->id()).arg(QCoreApplication::applicationName()); @@ -1223,9 +1283,9 @@ void ParameterManager::_initialRequestTimeout() QString ParameterManager::_remapParamNameToVersion(const QString ¶mName) const { - if (!paramName.startsWith(QStringLiteral("r."))) { - // No version mapping wanted - return paramName; + static const QString noRemapPrefix = QStringLiteral("noremap."); + if (paramName.startsWith(noRemapPrefix)) { + return paramName.mid(noRemapPrefix.length()); } const int majorVersion = _vehicle->firmwareMajorVersion(); @@ -1233,21 +1293,21 @@ QString ParameterManager::_remapParamNameToVersion(const QString ¶mName) con qCDebug(ParameterManagerLog) << "_remapParamNameToVersion" << paramName << majorVersion << minorVersion; - QString mappedParamName = paramName.right(paramName.length() - 2); if (majorVersion == Vehicle::versionNotSetValue) { // Vehicle version unknown - return mappedParamName; + return paramName; } const FirmwarePlugin::remapParamNameMajorVersionMap_t &majorVersionRemap = _vehicle->firmwarePlugin()->paramNameRemapMajorVersionMap(); if (!majorVersionRemap.contains(majorVersion)) { // No mapping for this major version qCDebug(ParameterManagerLog) << "_remapParamNameToVersion: no major version mapping"; - return mappedParamName; + return paramName; } const FirmwarePlugin::remapParamNameMinorVersionRemapMap_t &remapMinorVersion = majorVersionRemap[majorVersion]; // We must map backwards from the highest known minor version to one above the vehicle's minor version + QString mappedParamName = paramName; for (int currentMinorVersion = _vehicle->firmwarePlugin()->remapParamNameHigestMinorVersionNumber(majorVersion); currentMinorVersion>minorVersion; currentMinorVersion--) { if (remapMinorVersion.contains(currentMinorVersion)) { const FirmwarePlugin::remapParamNameMap_t &remap = remapMinorVersion[currentMinorVersion]; diff --git a/src/FactSystem/ParameterManager.h b/src/FactSystem/ParameterManager.h index 34c3d9546ba0..1fef68d92096 100644 --- a/src/FactSystem/ParameterManager.h +++ b/src/FactSystem/ParameterManager.h @@ -94,6 +94,8 @@ class ParameterManager : public QObject static constexpr int kParamRequestReadRetryCount = 2; ///< Number of retries for PARAM_REQUEST_READ static constexpr int kWaitForParamValueAckMs = 1000; ///< Time to wait for param value ack after set param static constexpr int kMaxInitialRequestListRetry = 4; ///< Maximum retries for initial parameter request list + static constexpr int kHashCheckTimeoutMs = 1000; ///< Timeout for standalone _HASH_CHECK request + static constexpr int kParamRequestListTimeoutMs = 5000; ///< Timeout for PARAM_REQUEST_LIST response static constexpr int kTestInitialRequestIntervalMs = 500; ///< Timer interval for initial request in test mode /// Maximum time to wait for initial request retries to exhaust in tests static constexpr int kTestMaxInitialRequestTimeMs = (kMaxInitialRequestListRetry + 1) * kTestInitialRequestIntervalMs + 1000; @@ -121,15 +123,24 @@ private slots: void _mavlinkParamSet(int componentId, const QString &name, FactMetaData::ValueType_t valueType, const QVariant &rawValue); void _waitingParamTimeout(); void _tryCacheLookup(); - void _initialRequestTimeout(); + void _hashCheckTimeout(); + void _paramRequestListTimeout(); /// Translates ParameterManager::defaultComponentId to real component id if needed int _actualComponentId(int componentId) const; void _mavlinkParamRequestRead(int componentId, const QString ¶mName, int paramIndex, bool notifyFailure); + void _requestHashCheck(uint8_t componentId); void _writeLocalParamCache(int vehicleId, int componentId); void _tryCacheHashLoad(int vehicleId, int componentId, const QVariant &hashValue); void _loadMetaData(); void _clearMetaData(); - /// Remap a parameter from one firmware version to another + /// Remap a parameter name from the newest firmware version to the version running on the vehicle. + /// All parameter names are walked backwards through the FirmwarePlugin remap tables from the + /// highest known minor version down to the vehicle's actual version. Names not found in any + /// remap table pass through unchanged. + /// + /// Names prefixed with "noremap." bypass remapping entirely — the prefix is stripped and the + /// bare name is used as-is. This is needed when code must distinguish old vs new parameter + /// names for unit conversion (e.g. checking whether WPNAV_SPEED exists vs WP_SPD). QString _remapParamNameToVersion(const QString ¶mName) const; bool _fillMavlinkParamUnion(FactMetaData::ValueType_t valueType, const QVariant &rawValue, mavlink_param_union_t ¶mUnion) const; bool _mavlinkParamUnionToVariant(const mavlink_param_union_t ¶mUnion, QVariant &outValue) const; @@ -165,6 +176,7 @@ private slots: bool _waitingForDefaultComponent = false; ///< true: last chance wait for default component params bool _metaDataAddedToFacts = false; ///< true: FactMetaData has been adde to the default component facts bool _logReplay = false; ///< true: running with log replay link + bool _hashCheckDone = false; ///< true: _HASH_CHECK has been attempted, go straight to PARAM_REQUEST_LIST typedef QPair ParamTypeVal; typedef QMap CacheMapName2ParamTypeVal; @@ -193,7 +205,8 @@ private slots: int _totalParamCount = 0; ///< Number of parameters across all components int _pendingWritesCount = 0; ///< Number of parameters with pending writes - QTimer _initialRequestTimeoutTimer; + QTimer _hashCheckTimer; + QTimer _paramRequestListTimer; QTimer _waitingParamTimeoutTimer; Fact _defaultFact; ///< Used to return default fact, when parameter not found diff --git a/src/FactSystem/SettingsFact.cc b/src/FactSystem/SettingsFact.cc index 5efffae1629a..c3152d807fec 100644 --- a/src/FactSystem/SettingsFact.cc +++ b/src/FactSystem/SettingsFact.cc @@ -49,7 +49,6 @@ SettingsFact::SettingsFact(const QString &settingsGroup, FactMetaData *metaData, QMutexLocker locker(&_rawValueMutex); _rawValue = resolvedValue; - _rawValueIsNotSet = false; } (void) connect(this, &Fact::rawValueChanged, this, &SettingsFact::_rawValueChanged); diff --git a/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json b/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json index b106da0495ff..6176998d4f4a 100644 --- a/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json +++ b/src/FirmwarePlugin/APM/APM-MavCmdInfoCommon.json @@ -90,6 +90,11 @@ "enumValues": "1,0", "default": 1 } + }, + { + "id": 2500, + "comment": "MAV_CMD_VIDEO_START_CAPTURE", + "paramRemove": "2" } ] } diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc index 7e521c67b859..0458b3bb6c1a 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.cc @@ -555,6 +555,7 @@ QList APMFirmwarePlugin::supportedMissionCommands(QGCMAVLink::VehicleCl MAV_CMD_DO_MOUNT_CONTROL, MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW, MAV_CMD_DO_SET_CAM_TRIGG_DIST, + MAV_CMD_IMAGE_START_CAPTURE, MAV_CMD_IMAGE_STOP_CAPTURE, MAV_CMD_VIDEO_START_CAPTURE, MAV_CMD_VIDEO_STOP_CAPTURE, MAV_CMD_DO_FENCE_ENABLE, MAV_CMD_DO_PARACHUTE, MAV_CMD_DO_INVERTED_FLIGHT, @@ -792,11 +793,11 @@ static void _MAV_CMD_DO_REPOSITION_ResultHandler(void *resultHandlerData, int /* delete data; } -void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const +bool APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const { if (qIsNaN(vehicle->altitudeRelative()->rawValue().toDouble())) { qgcApp()->showAppMessage(QStringLiteral("Unable to go to location, vehicle position not known.")); - return; + return false; } // attempt to use MAV_CMD_DO_REPOSITION to move vehicle. If that @@ -845,7 +846,7 @@ void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoord } if (instanceData->MAV_CMD_DO_REPOSITION_supported) { // no need to fall back - return; + return true; } } @@ -854,6 +855,8 @@ void APMFirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoord QGeoCoordinate coordWithAltitude = gotoCoord; coordWithAltitude.setAltitude(vehicle->altitudeRelative()->rawValue().toDouble()); vehicle->missionManager()->writeArduPilotGuidedMissionItem(coordWithAltitude, false /* altChangeOnly */); + + return true; } void APMFirmwarePlugin::guidedModeRTL(Vehicle *vehicle, bool smartRTL) const @@ -909,18 +912,28 @@ void APMFirmwarePlugin::guidedModeChangeAltitude(Vehicle *vehicle, double altitu bool APMFirmwarePlugin::mulirotorSpeedLimitsAvailable(Vehicle *vehicle) const { - return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "WPNAV_SPEED"); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. + return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD")) + || vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED")); } -double APMFirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle *vehicle) const +double APMFirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(Vehicle *vehicle) const { - const QString speedParam("WPNAV_SPEED"); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. - if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, speedParam)) { - return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, speedParam)->rawValue().toDouble() * 0.01; // note cm/s -> m/s + // 4.7+: WP_SPD is in m/s + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD"))) { + return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.WP_SPD"))->rawValue().toDouble(); } - return FirmwarePlugin::maximumHorizontalSpeedMultirotor(vehicle); + // pre-4.7: WPNAV_SPEED is in cm/s + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED"))) { + return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.WPNAV_SPEED"))->rawValue().toDouble() * 0.01; + } + + return FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(vehicle); } void APMFirmwarePlugin::guidedModeChangeGroundSpeedMetersSecond(Vehicle *vehicle, double groundspeed) const @@ -982,11 +995,28 @@ void APMFirmwarePlugin::guidedModeChangeHeading(Vehicle *vehicle, const QGeoCoor double APMFirmwarePlugin::minimumTakeoffAltitudeMeters(Vehicle* vehicle) const { double minTakeoffAlt = 0; - const QString takeoffAltParam(vehicle->vtol() ? QStringLiteral("Q_RTL_ALT") : QStringLiteral("PILOT_TKOFF_ALT")); - const float paramDivisor = vehicle->vtol() ? 1.0 : 100.0; // PILOT_TAKEOFF_ALT is in centimeters - if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, takeoffAltParam)) { - minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, takeoffAltParam)->rawValue().toDouble() / static_cast(paramDivisor); + // Use noremap. to bypass remap and check for specific parameter names directly, + // since the old and new parameters have different units. + + if (vehicle->vtol()) { + // 4.7+: Q_PILOT_TKO_ALT_M (meters) + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKO_ALT_M"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKO_ALT_M"))->rawValue().toDouble(); + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKOFF_ALT"))) { + // pre-4.7: Q_PILOT_TKOFF_ALT (centimeters) + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.Q_PILOT_TKOFF_ALT"))->rawValue().toDouble() / 100.0; + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("Q_RTL_ALT"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("Q_RTL_ALT"))->rawValue().toDouble(); + } + } else { + // 4.7+: PILOT_TKO_ALT_M (meters) + if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKO_ALT_M"))) { + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKO_ALT_M"))->rawValue().toDouble(); + } else if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKOFF_ALT"))) { + // pre-4.7: PILOT_TKOFF_ALT (centimeters) + minTakeoffAlt = vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, QStringLiteral("noremap.PILOT_TKOFF_ALT"))->rawValue().toDouble() / 100.0; + } } if (minTakeoffAlt == 0) { @@ -1229,7 +1259,7 @@ QMutex &APMFirmwarePlugin::_reencodeMavlinkChannelMutex() double APMFirmwarePlugin::maximumEquivalentAirspeed(Vehicle *vehicle) const { - const QString airspeedMax("r.AIRSPEED_MAX"); + const QString airspeedMax("AIRSPEED_MAX"); if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, airspeedMax)) { return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, airspeedMax)->rawValue().toDouble(); @@ -1240,7 +1270,7 @@ double APMFirmwarePlugin::maximumEquivalentAirspeed(Vehicle *vehicle) const double APMFirmwarePlugin::minimumEquivalentAirspeed(Vehicle *vehicle) const { - const QString airspeedMin("r.AIRSPEED_MIN"); + const QString airspeedMin("AIRSPEED_MIN"); if (vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, airspeedMin)) { return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, airspeedMin)->rawValue().toDouble(); @@ -1251,8 +1281,8 @@ double APMFirmwarePlugin::minimumEquivalentAirspeed(Vehicle *vehicle) const bool APMFirmwarePlugin::fixedWingAirSpeedLimitsAvailable(Vehicle *vehicle) const { - return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "r.AIRSPEED_MIN") && - vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "r.AIRSPEED_MAX"); + return vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "AIRSPEED_MIN") && + vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "AIRSPEED_MAX"); } void APMFirmwarePlugin::guidedModeChangeEquivalentAirspeedMetersSecond(Vehicle *vehicle, double airspeed_equiv) const diff --git a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h index 196236ee3f60..fc0dab63600b 100644 --- a/src/FirmwarePlugin/APM/APMFirmwarePlugin.h +++ b/src/FirmwarePlugin/APM/APMFirmwarePlugin.h @@ -31,7 +31,7 @@ class APMFirmwarePlugin : public FirmwarePlugin bool isCapable(const Vehicle *vehicle, FirmwareCapabilities capabilities) const override; void setGuidedMode(Vehicle *vehicle, bool guidedMode) const override; void guidedModeTakeoff(Vehicle *vehicle, double altitudeRel) const override; - void guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; + bool guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; double minimumTakeoffAltitudeMeters(Vehicle *vehicle) const override; void startTakeoff(Vehicle *vehicle) const override; void startMission(Vehicle *vehicle) const override; @@ -73,7 +73,7 @@ class APMFirmwarePlugin : public FirmwarePlugin // support for changing speed in Copter guide mode: bool mulirotorSpeedLimitsAvailable(Vehicle *vehicle) const override; - double maximumHorizontalSpeedMultirotor(Vehicle *vehicle) const override; + double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle *vehicle) const override; void guidedModeChangeGroundSpeedMetersSecond(Vehicle *vehicle, double speed) const override; static QPair startCompensatingBaro(Vehicle *vehicle); diff --git a/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml b/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml index 3e1087e5ab5f..b016864d806b 100644 --- a/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml +++ b/src/FirmwarePlugin/APM/APMFlightModeIndicator.qml @@ -12,7 +12,9 @@ SettingsGroupLayout { visible: activeVehicle.multiRotor property var activeVehicle: QGroundControl.multiVehicleManager.activeVehicle - property Fact rtlAltFact: controller.getParameterFact(-1, "RTL_ALT") + property Fact rtlAltFact: controller.getParameterFact(-1, "RTL_ALT_M") + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + property bool _rtlAltIsMeters: controller.parameterExists(-1, "noremap.RTL_ALT_M") FactPanelController { id: controller } @@ -45,7 +47,8 @@ SettingsGroupLayout { if (index === 0) { rtlAltFact.rawValue = 0 } else { - rtlAltFact.rawValue = 1500 + // RTL_ALT_M (4.7+) is in meters, RTL_ALT (pre-4.7) is in centimeters + rtlAltFact.rawValue = _rtlAltIsMeters ? 15 : 1500 } } diff --git a/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc index fcd94fc59dbf..fb2b5c9d03e5 100644 --- a/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduCopterFirmwarePlugin.cc @@ -74,6 +74,92 @@ ArduCopterFirmwarePlugin::ArduCopterFirmwarePlugin(QObject *parent) remapV4_0["TUNE_MIN"] = QStringLiteral("TUNE_LOW"); remapV4_0["TUNE_MAX"] = QStringLiteral("TUNE_HIGH"); + // ArduPilot 4.7: massive parameter rename and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Position controller: PSC_VELXY_* -> PSC_NE_VEL_* + remapV4_7["PSC_NE_VEL_P"] = QStringLiteral("PSC_VELXY_P"); + remapV4_7["PSC_NE_VEL_I"] = QStringLiteral("PSC_VELXY_I"); + remapV4_7["PSC_NE_VEL_D"] = QStringLiteral("PSC_VELXY_D"); + remapV4_7["PSC_NE_VEL_IMAX"] = QStringLiteral("PSC_VELXY_IMAX"); + remapV4_7["PSC_NE_VEL_FLTE"] = QStringLiteral("PSC_VELXY_FLTE"); + remapV4_7["PSC_NE_VEL_FLTD"] = QStringLiteral("PSC_VELXY_FLTD"); + remapV4_7["PSC_NE_VEL_FF"] = QStringLiteral("PSC_VELXY_FF"); + + // Position controller: PSC_VELZ_* -> PSC_D_VEL_* + remapV4_7["PSC_D_VEL_P"] = QStringLiteral("PSC_VELZ_P"); + remapV4_7["PSC_D_VEL_I"] = QStringLiteral("PSC_VELZ_I"); + remapV4_7["PSC_D_VEL_D"] = QStringLiteral("PSC_VELZ_D"); + remapV4_7["PSC_D_VEL_IMAX"] = QStringLiteral("PSC_VELZ_IMAX"); + remapV4_7["PSC_D_VEL_FLTE"] = QStringLiteral("PSC_VELZ_FLTE"); + remapV4_7["PSC_D_VEL_FF"] = QStringLiteral("PSC_VELZ_FF"); + + // Position controller: PSC_ACCZ_* -> PSC_D_ACC_* + remapV4_7["PSC_D_ACC_P"] = QStringLiteral("PSC_ACCZ_P"); + remapV4_7["PSC_D_ACC_I"] = QStringLiteral("PSC_ACCZ_I"); + remapV4_7["PSC_D_ACC_D"] = QStringLiteral("PSC_ACCZ_D"); + remapV4_7["PSC_D_ACC_IMAX"] = QStringLiteral("PSC_ACCZ_IMAX"); + remapV4_7["PSC_D_ACC_FLTD"] = QStringLiteral("PSC_ACCZ_FLTD"); + remapV4_7["PSC_D_ACC_FLTE"] = QStringLiteral("PSC_ACCZ_FLTE"); + remapV4_7["PSC_D_ACC_FLTT"] = QStringLiteral("PSC_ACCZ_FLTT"); + remapV4_7["PSC_D_ACC_FF"] = QStringLiteral("PSC_ACCZ_FF"); + remapV4_7["PSC_D_ACC_SMAX"] = QStringLiteral("PSC_ACCZ_SMAX"); + + // Position controller: PSC_POSXY_P -> PSC_NE_POS_P (simply renamed) + remapV4_7["PSC_NE_POS_P"] = QStringLiteral("PSC_POSXY_P"); + + // Position controller: PSC_POSZ_P -> PSC_D_POS_P (simply renamed) + remapV4_7["PSC_D_POS_P"] = QStringLiteral("PSC_POSZ_P"); + + // Waypoint navigation: WPNAV_* -> WP_* + remapV4_7["WP_ACC"] = QStringLiteral("WPNAV_ACCEL"); + remapV4_7["WP_ACC_CNR"] = QStringLiteral("WPNAV_ACCEL_C"); + remapV4_7["WP_ACC_Z"] = QStringLiteral("WPNAV_ACCEL_Z"); + remapV4_7["WP_RADIUS_M"] = QStringLiteral("WPNAV_RADIUS"); + remapV4_7["WP_SPD"] = QStringLiteral("WPNAV_SPEED"); + remapV4_7["WP_SPD_DN"] = QStringLiteral("WPNAV_SPEED_DN"); + remapV4_7["WP_SPD_UP"] = QStringLiteral("WPNAV_SPEED_UP"); + + // RTL parameters + remapV4_7["RTL_ALT_M"] = QStringLiteral("RTL_ALT"); + remapV4_7["RTL_SPEED_MS"] = QStringLiteral("RTL_SPEED"); + remapV4_7["RTL_ALT_FINAL_M"] = QStringLiteral("RTL_ALT_FINAL"); + remapV4_7["RTL_CLIMB_MIN_M"] = QStringLiteral("RTL_CLIMB_MIN"); + + // Landing parameters + remapV4_7["LAND_SPD_MS"] = QStringLiteral("LAND_SPEED"); + remapV4_7["LAND_SPD_HIGH_MS"]= QStringLiteral("LAND_SPEED_HIGH"); + remapV4_7["LAND_ALT_LOW_M"] = QStringLiteral("LAND_ALT_LOW"); + + // Loiter parameters + remapV4_7["LOIT_SPEED_MS"] = QStringLiteral("LOIT_SPEED"); + remapV4_7["LOIT_ACC_MAX_M"] = QStringLiteral("LOIT_ACC_MAX"); + remapV4_7["LOIT_BRK_ACC_M"] = QStringLiteral("LOIT_BRK_ACCEL"); + remapV4_7["LOIT_BRK_JRK_M"] = QStringLiteral("LOIT_BRK_JERK"); + + // Pilot parameters + remapV4_7["PILOT_ACC_Z"] = QStringLiteral("PILOT_ACCEL_Z"); + remapV4_7["PILOT_SPD_UP"] = QStringLiteral("PILOT_SPEED_UP"); + remapV4_7["PILOT_SPD_DN"] = QStringLiteral("PILOT_SPEED_DN"); + remapV4_7["PILOT_TKO_ALT_M"] = QStringLiteral("PILOT_TKOFF_ALT"); + + // Attitude controller + remapV4_7["ATC_ANGLE_MAX"] = QStringLiteral("ANGLE_MAX"); + remapV4_7["ATC_ACC_R_MAX"] = QStringLiteral("ATC_ACCEL_R_MAX"); + remapV4_7["ATC_ACC_P_MAX"] = QStringLiteral("ATC_ACCEL_P_MAX"); + remapV4_7["ATC_ACC_Y_MAX"] = QStringLiteral("ATC_ACCEL_Y_MAX"); + remapV4_7["ATC_RATE_WPY_MAX"]= QStringLiteral("ATC_SLEW_YAW"); + + // Circle + remapV4_7["CIRCLE_RADIUS_M"] = QStringLiteral("CIRCLE_RADIUS"); + + // PosHold + remapV4_7["PHLD_BRK_ANGLE"] = QStringLiteral("PHLD_BRAKE_ANGLE"); + remapV4_7["PHLD_BRK_RATE"] = QStringLiteral("PHLD_BRAKE_RATE"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + _remapParamNameIntialized = true; } } @@ -85,7 +171,7 @@ ArduCopterFirmwarePlugin::~ArduCopterFirmwarePlugin() int ArduCopterFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - return ((majorVersionNumber == 4) ? 0 : Vehicle::versionNotSetValue); + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } bool ArduCopterFirmwarePlugin::multiRotorXConfig(Vehicle *vehicle) const diff --git a/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc index f1bdfc591d8c..d5469bdb30ea 100644 --- a/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduPlaneFirmwarePlugin.cc @@ -76,6 +76,70 @@ ArduPlaneFirmwarePlugin::ArduPlaneFirmwarePlugin(QObject *parent) remapV4_5["RTL_ALTITUDE"] = QStringLiteral("ALT_HOLD_RTL"); // LAND_SPEED is only used in a Copter component + // ArduPilot 4.7: QuadPlane parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Attitude controller + remapV4_7["Q_A_ANGLE_MAX"] = QStringLiteral("Q_ANGLE_MAX"); + remapV4_7["Q_A_ACC_R_MAX"] = QStringLiteral("Q_A_ACCEL_R_MAX"); + remapV4_7["Q_A_ACC_P_MAX"] = QStringLiteral("Q_A_ACCEL_P_MAX"); + remapV4_7["Q_A_ACC_Y_MAX"] = QStringLiteral("Q_A_ACCEL_Y_MAX"); + remapV4_7["Q_A_RATE_WPY_MAX"] = QStringLiteral("Q_A_SLEW_YAW"); + + // Loiter + remapV4_7["Q_LOIT_SPEED_MS"] = QStringLiteral("Q_LOIT_SPEED"); + remapV4_7["Q_LOIT_ACC_MAX_M"] = QStringLiteral("Q_LOIT_ACC_MAX"); + remapV4_7["Q_LOIT_BRK_ACC_M"] = QStringLiteral("Q_LOIT_BRK_ACCEL"); + remapV4_7["Q_LOIT_BRK_JRK_M"] = QStringLiteral("Q_LOIT_BRK_JERK"); + + // Pilot + remapV4_7["Q_PILOT_SPD_UP"] = QStringLiteral("Q_PILOT_SPEED_UP"); + remapV4_7["Q_PILOT_SPD_DN"] = QStringLiteral("Q_PILOT_SPEED_DN"); + remapV4_7["Q_PILOT_TKO_ALT_M"] = QStringLiteral("Q_PILOT_TKOFF_ALT"); + + // Position controller: Q_P_VELXY_* -> Q_P_NE_VEL_* + remapV4_7["Q_P_NE_VEL_P"] = QStringLiteral("Q_P_VELXY_P"); + remapV4_7["Q_P_NE_VEL_I"] = QStringLiteral("Q_P_VELXY_I"); + remapV4_7["Q_P_NE_VEL_D"] = QStringLiteral("Q_P_VELXY_D"); + remapV4_7["Q_P_NE_VEL_IMAX"] = QStringLiteral("Q_P_VELXY_IMAX"); + remapV4_7["Q_P_NE_VEL_FLTE"] = QStringLiteral("Q_P_VELXY_FLTE"); + remapV4_7["Q_P_NE_VEL_FLTD"] = QStringLiteral("Q_P_VELXY_FLTD"); + remapV4_7["Q_P_NE_VEL_FF"] = QStringLiteral("Q_P_VELXY_FF"); + + // Position controller: Q_P_VELZ_* -> Q_P_D_VEL_* + remapV4_7["Q_P_D_VEL_P"] = QStringLiteral("Q_P_VELZ_P"); + remapV4_7["Q_P_D_VEL_I"] = QStringLiteral("Q_P_VELZ_I"); + remapV4_7["Q_P_D_VEL_D"] = QStringLiteral("Q_P_VELZ_D"); + remapV4_7["Q_P_D_VEL_IMAX"] = QStringLiteral("Q_P_VELZ_IMAX"); + remapV4_7["Q_P_D_VEL_FLTE"] = QStringLiteral("Q_P_VELZ_FLTE"); + remapV4_7["Q_P_D_VEL_FF"] = QStringLiteral("Q_P_VELZ_FF"); + + // Position controller: Q_P_ACCZ_* -> Q_P_D_ACC_* + remapV4_7["Q_P_D_ACC_P"] = QStringLiteral("Q_P_ACCZ_P"); + remapV4_7["Q_P_D_ACC_I"] = QStringLiteral("Q_P_ACCZ_I"); + remapV4_7["Q_P_D_ACC_D"] = QStringLiteral("Q_P_ACCZ_D"); + remapV4_7["Q_P_D_ACC_IMAX"] = QStringLiteral("Q_P_ACCZ_IMAX"); + remapV4_7["Q_P_D_ACC_FLTD"] = QStringLiteral("Q_P_ACCZ_FLTD"); + remapV4_7["Q_P_D_ACC_FLTE"] = QStringLiteral("Q_P_ACCZ_FLTE"); + remapV4_7["Q_P_D_ACC_FLTT"] = QStringLiteral("Q_P_ACCZ_FLTT"); + remapV4_7["Q_P_D_ACC_FF"] = QStringLiteral("Q_P_ACCZ_FF"); + remapV4_7["Q_P_D_ACC_SMAX"] = QStringLiteral("Q_P_ACCZ_SMAX"); + + // Waypoint navigation + remapV4_7["Q_WP_ACC"] = QStringLiteral("Q_WP_ACCEL"); + remapV4_7["Q_WP_ACC_CNR"] = QStringLiteral("Q_WP_ACCEL_C"); + remapV4_7["Q_WP_ACC_Z"] = QStringLiteral("Q_WP_ACCEL_Z"); + remapV4_7["Q_WP_RADIUS_M"] = QStringLiteral("Q_WP_RADIUS"); + remapV4_7["Q_WP_SPD"] = QStringLiteral("Q_WP_SPEED"); + remapV4_7["Q_WP_SPD_DN"] = QStringLiteral("Q_WP_SPEED_DN"); + remapV4_7["Q_WP_SPD_UP"] = QStringLiteral("Q_WP_SPEED_UP"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } } @@ -87,8 +151,7 @@ ArduPlaneFirmwarePlugin::~ArduPlaneFirmwarePlugin() int ArduPlaneFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping supports up to 4.5 - return ((majorVersionNumber == 4) ? 5 : Vehicle::versionNotSetValue); + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } QString ArduPlaneFirmwarePlugin::takeOffFlightMode() const diff --git a/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc index eab875cab42e..496bc382c980 100644 --- a/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduRoverFirmwarePlugin.cc @@ -47,6 +47,15 @@ ArduRoverFirmwarePlugin::ArduRoverFirmwarePlugin(QObject *parent) updateAvailableFlightModes(availableFlightModes); if (!_remapParamNameIntialized) { + // ArduPilot 4.7: parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } } @@ -56,10 +65,9 @@ ArduRoverFirmwarePlugin::~ArduRoverFirmwarePlugin() } -int ArduRoverFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const +int ArduRoverFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping not supported - return Vehicle::versionNotSetValue; + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } void ArduRoverFirmwarePlugin::guidedModeChangeAltitude(Vehicle* /*vehicle*/, double /*altitudeChange*/, bool /*pauseVehicle*/) diff --git a/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc b/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc index 6e275974c24f..ab066cb578e9 100644 --- a/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc +++ b/src/FirmwarePlugin/APM/ArduSubFirmwarePlugin.cc @@ -125,6 +125,67 @@ ArduSubFirmwarePlugin::ArduSubFirmwarePlugin(QObject *parent) updateAvailableFlightModes(availableFlightModes); if (!_remapParamNameIntialized) { + // ArduPilot 4.7: parameter renames and SI unit conversion + FirmwarePlugin::remapParamNameMap_t &remapV4_7 = _remapParamName[4][7]; + + // Position controller: PSC_VELXY_* -> PSC_NE_VEL_* + remapV4_7["PSC_NE_VEL_P"] = QStringLiteral("PSC_VELXY_P"); + remapV4_7["PSC_NE_VEL_I"] = QStringLiteral("PSC_VELXY_I"); + remapV4_7["PSC_NE_VEL_D"] = QStringLiteral("PSC_VELXY_D"); + remapV4_7["PSC_NE_VEL_IMAX"] = QStringLiteral("PSC_VELXY_IMAX"); + remapV4_7["PSC_NE_VEL_FLTE"] = QStringLiteral("PSC_VELXY_FLTE"); + remapV4_7["PSC_NE_VEL_FLTD"] = QStringLiteral("PSC_VELXY_FLTD"); + remapV4_7["PSC_NE_VEL_FF"] = QStringLiteral("PSC_VELXY_FF"); + + // Position controller: PSC_VELZ_* -> PSC_D_VEL_* + remapV4_7["PSC_D_VEL_P"] = QStringLiteral("PSC_VELZ_P"); + remapV4_7["PSC_D_VEL_I"] = QStringLiteral("PSC_VELZ_I"); + remapV4_7["PSC_D_VEL_D"] = QStringLiteral("PSC_VELZ_D"); + remapV4_7["PSC_D_VEL_IMAX"] = QStringLiteral("PSC_VELZ_IMAX"); + remapV4_7["PSC_D_VEL_FLTE"] = QStringLiteral("PSC_VELZ_FLTE"); + remapV4_7["PSC_D_VEL_FF"] = QStringLiteral("PSC_VELZ_FF"); + + // Position controller: PSC_ACCZ_* -> PSC_D_ACC_* + remapV4_7["PSC_D_ACC_P"] = QStringLiteral("PSC_ACCZ_P"); + remapV4_7["PSC_D_ACC_I"] = QStringLiteral("PSC_ACCZ_I"); + remapV4_7["PSC_D_ACC_D"] = QStringLiteral("PSC_ACCZ_D"); + remapV4_7["PSC_D_ACC_IMAX"] = QStringLiteral("PSC_ACCZ_IMAX"); + remapV4_7["PSC_D_ACC_FLTD"] = QStringLiteral("PSC_ACCZ_FLTD"); + remapV4_7["PSC_D_ACC_FLTE"] = QStringLiteral("PSC_ACCZ_FLTE"); + remapV4_7["PSC_D_ACC_FLTT"] = QStringLiteral("PSC_ACCZ_FLTT"); + remapV4_7["PSC_D_ACC_FF"] = QStringLiteral("PSC_ACCZ_FF"); + remapV4_7["PSC_D_ACC_SMAX"] = QStringLiteral("PSC_ACCZ_SMAX"); + + // Position controller: PSC_POSXY_P -> PSC_NE_POS_P + remapV4_7["PSC_NE_POS_P"] = QStringLiteral("PSC_POSXY_P"); + + // Position controller: PSC_POSZ_P -> PSC_D_POS_P + remapV4_7["PSC_D_POS_P"] = QStringLiteral("PSC_POSZ_P"); + + // Waypoint navigation: WPNAV_* -> WP_* + remapV4_7["WP_ACC"] = QStringLiteral("WPNAV_ACCEL"); + remapV4_7["WP_ACC_CNR"] = QStringLiteral("WPNAV_ACCEL_C"); + remapV4_7["WP_ACC_Z"] = QStringLiteral("WPNAV_ACCEL_Z"); + remapV4_7["WP_RADIUS_M"] = QStringLiteral("WPNAV_RADIUS"); + remapV4_7["WP_SPD"] = QStringLiteral("WPNAV_SPEED"); + remapV4_7["WP_SPD_DN"] = QStringLiteral("WPNAV_SPEED_DN"); + remapV4_7["WP_SPD_UP"] = QStringLiteral("WPNAV_SPEED_UP"); + + // Attitude controller + remapV4_7["ATC_ACC_R_MAX"] = QStringLiteral("ATC_ACCEL_R_MAX"); + remapV4_7["ATC_ACC_P_MAX"] = QStringLiteral("ATC_ACCEL_P_MAX"); + remapV4_7["ATC_ACC_Y_MAX"] = QStringLiteral("ATC_ACCEL_Y_MAX"); + remapV4_7["ATC_RATE_WPY_MAX"]= QStringLiteral("ATC_SLEW_YAW"); + + // Circle + remapV4_7["CIRCLE_RADIUS_M"] = QStringLiteral("CIRCLE_RADIUS"); + + // EKF + remapV4_7["EK3_FLOW_MAX"] = QStringLiteral("EK3_MAX_FLOW"); + + // Common + remapV4_7["ARMING_SKIPCHK"] = QStringLiteral("ARMING_CHECK"); + _remapParamNameIntialized = true; } @@ -142,10 +203,9 @@ ArduSubFirmwarePlugin::~ArduSubFirmwarePlugin() } -int ArduSubFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const +int ArduSubFirmwarePlugin::remapParamNameHigestMinorVersionNumber(int majorVersionNumber) const { - // Remapping not supported - return Vehicle::versionNotSetValue; + return ((majorVersionNumber == 4) ? 7 : Vehicle::versionNotSetValue); } void ArduSubFirmwarePlugin::initializeStreamRates(Vehicle *vehicle) diff --git a/src/FirmwarePlugin/FirmwarePlugin.cc b/src/FirmwarePlugin/FirmwarePlugin.cc index 813214343f2f..e0f9cd78ca5d 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.cc +++ b/src/FirmwarePlugin/FirmwarePlugin.cc @@ -141,12 +141,13 @@ void FirmwarePlugin::guidedModeTakeoff(Vehicle *vehicle, double takeoffAltRel) c qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); } -void FirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const +bool FirmwarePlugin::guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius) const { Q_UNUSED(vehicle); Q_UNUSED(gotoCoord); Q_UNUSED(forwardFlightLoiterRadius); qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); + return false; } void FirmwarePlugin::guidedModeChangeAltitude(Vehicle*, double, bool pauseVehicle) @@ -198,11 +199,12 @@ const QVariantList &FirmwarePlugin::toolIndicators(const Vehicle*) QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GPSResilienceIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/TelemetryRSSIIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/RCRSSIIndicator.qml")), - QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Controls/BatteryIndicator.qml")), + QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/BatteryIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/RemoteIDIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/GimbalIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/EscIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/JoystickIndicator.qml")), + QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/SigningIndicator.qml")), QVariant::fromValue(QUrl::fromUserInput("qrc:/qml/QGroundControl/Toolbar/MultiVehicleSelector.qml")), #ifdef QT_DEBUG // ControlIndicator is only available in debug builds for the moment @@ -291,7 +293,7 @@ QGCCameraManager *FirmwarePlugin::createCameraManager(Vehicle *vehicle) const return new QGCCameraManager(vehicle); } -MavlinkCameraControl *FirmwarePlugin::createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent) const +MavlinkCameraControlInterface *FirmwarePlugin::createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent) const { return new VehicleCameraControl(info, vehicle, compID, parent); } diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index 4e0dd535d030..56f850495852 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -8,11 +8,11 @@ #include "QGCMAVLink.h" #include "FollowMe.h" #include "FactMetaData.h" +#include "Vehicle.h" class VehicleComponent; class AutoPilotPlugin; -class Vehicle; -class MavlinkCameraControl; +class MavlinkCameraControlInterface; class QGCCameraManager; class Autotune; class LinkInterface; @@ -82,14 +82,27 @@ class FirmwarePlugin : public QObject GuidedTakeoffCapability = 1 << 7, ///< Vehicle supports guided takeoff }; - /// Maps from on parameter name to another - /// key: parameter name to translate from - /// value: mapped parameter name + /// Parameter name remapping support: + /// When firmware renames a parameter across versions, callers should use the *newest* (current) + /// parameter name. ParameterManager::_remapParamNameToVersion() walks the remap tables backwards + /// from the highest known minor version down to the vehicle's actual firmware version, translating + /// new names to old names at each step. If the name is not found in any remap table it passes + /// through unchanged, so remapping is always safe to run. + /// + /// To bypass remapping (e.g. when checking parameterExists for a specific old or new name to + /// decide which unit conversion to apply), prefix the name with "noremap.". + /// + /// Remap table entries map new_name -> old_name for the version in which the rename occurred. + /// For example: remapV4_0["TUNE_MIN"] = "TUNE_LOW" means TUNE_LOW was renamed to TUNE_MIN in 4.0. + + /// Maps from one parameter name to another (new_name -> old_name for a given version) + /// key: current (new) parameter name + /// value: previous (old) parameter name typedef QMap remapParamNameMap_t; /// Maps from firmware minor version to remapParamNameMap_t entry - /// key: firmware minor version - /// value: remapParamNameMap_t entry + /// key: firmware minor version in which the rename(s) occurred + /// value: remapParamNameMap_t with new->old mappings for that version typedef QMap remapParamNameMinorVersionRemapMap_t; /// Maps from firmware major version number to remapParamNameMinorVersionRemapMap_t entry @@ -184,7 +197,7 @@ class FirmwarePlugin : public QObject virtual double minimumTakeoffAltitudeMeters(Vehicle* /*vehicle*/) const { return 3.048; } /// @return The maximum horizontal groundspeed for a multirotor. - virtual double maximumHorizontalSpeedMultirotor(Vehicle* /*vehicle*/) const { return NAN; } + virtual double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* /*vehicle*/) const { return NAN; } /// @return The maximum equivalent airspeed setpoint. virtual double maximumEquivalentAirspeed(Vehicle* /*vehicle*/) const { return NAN; } @@ -207,8 +220,9 @@ class FirmwarePlugin : public QObject /// Command the vehicle to start the mission virtual void startMission(Vehicle *vehicle) const; - /// Command vehicle to move to specified location (altitude is included and relative) - virtual void guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius = 0.0) const; + /// Command vehicle to move to specified location (altitude is ignored, vehicle uses current altitude) + /// @return true: goto command accepted, false: goto failed (vehicle not moved) + virtual bool guidedModeGotoLocation(Vehicle *vehicle, const QGeoCoordinate &gotoCoord, double forwardFlightLoiterRadius = 0.0) const; /// Command vehicle to change altitude /// @param altitudeChange If > 0, go up by amount specified, if < 0, go down by amount specified @@ -305,10 +319,13 @@ class FirmwarePlugin : public QObject virtual QString missionCommandOverrides(QGCMAVLink::VehicleClass_t vehicleClass) const; /// Returns the mapping structure which is used to map from one parameter name to another based on firmware version. + /// See remapParamNameMap_t for details on how remapping works. virtual const remapParamNameMajorVersionMap_t ¶mNameRemapMajorVersionMap() const; - /// Returns the highest major version number that is known to the remap for this specified major version. - virtual int remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const { return 0; } + /// Returns the highest minor version number that has remap entries for the specified major version. + /// The remap logic iterates backwards from this version down to the vehicle's actual minor version. + /// Return Vehicle::versionNotSetValue if remapping is not supported for the given major version. + virtual int remapParamNameHigestMinorVersionNumber(int /*majorVersionNumber*/) const { return Vehicle::versionNotSetValue; } /// @return true: Motors are coaxial like an X8 config, false: Quadcopter for example virtual bool multiRotorCoaxialMotors(Vehicle* /*vehicle*/) const { return false; } @@ -342,7 +359,7 @@ class FirmwarePlugin : public QObject virtual QGCCameraManager *createCameraManager(Vehicle *vehicle) const; /// Camera control. - virtual MavlinkCameraControl *createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent = nullptr) const; + virtual MavlinkCameraControlInterface *createCameraControl(const mavlink_camera_information_t *info, Vehicle *vehicle, int compID, QObject *parent = nullptr) const; /// Returns a pointer to a dictionary of firmware-specific FactGroups virtual QMap *factGroups() { return nullptr; } diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index 96ab48cb1052..9bd3349adb37 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -2,16 +2,18 @@ #include "PX4ParameterMetaData.h" #include "QGCApplication.h" #include "PX4AutoPilotPlugin.h" +#include "QGCLoggingCategory.h" #include "SettingsManager.h" #include "PlanViewSettings.h" #include "ParameterManager.h" #include "Vehicle.h" -#include #include #include "px4_custom_mode.h" +QGC_LOGGING_CATEGORY(PX4FirmwarePluginLog, "FirmwarePlugin.PX4FirmwarePlugin") + PX4FirmwarePluginInstanceData::PX4FirmwarePluginInstanceData(QObject* parent) : FirmwarePluginInstanceData(parent) , versionNotified(false) @@ -141,7 +143,7 @@ bool PX4FirmwarePlugin::setFlightMode(const QString& flightMode, uint8_t* base_m } if (!found) { - qWarning() << "Unknown flight Mode" << flightMode; + qCWarning(PX4FirmwarePluginLog) << "Unknown flight Mode" << flightMode; } return found; @@ -182,7 +184,7 @@ FactMetaData* PX4FirmwarePlugin::_getMetaDataForFact(QObject* parameterMetaData, if (px4MetaData) { return px4MetaData->getMetaDataForFact(name, vehicleType, type); } else { - qWarning() << "Internal error: pointer passed to PX4FirmwarePlugin::getMetaDataForFact not PX4ParameterMetaData"; + qCWarning(PX4FirmwarePluginLog) << "Internal error: pointer passed to PX4FirmwarePlugin::getMetaDataForFact not PX4ParameterMetaData"; } return nullptr; @@ -260,7 +262,7 @@ QString PX4FirmwarePlugin::missionCommandOverrides(QGCMAVLink::VehicleClass_t ve case QGCMAVLink::VehicleClassRoverBoat: return QStringLiteral(":/json/PX4-MavCmdInfoRover.json"); default: - qWarning() << "PX4FirmwarePlugin::missionCommandOverrides called with bad VehicleClass_t:" << vehicleClass; + qCWarning(PX4FirmwarePluginLog) << "PX4FirmwarePlugin::missionCommandOverrides called with bad VehicleClass_t:" << vehicleClass; return QString(); } } @@ -305,7 +307,7 @@ void PX4FirmwarePlugin::_mavCommandResult(int vehicleId, int component, int comm auto* vehicle = qobject_cast(sender()); if (!vehicle) { - qWarning() << "Dynamic cast failed!"; + qCWarning(PX4FirmwarePluginLog) << "Dynamic cast failed!"; return; } @@ -341,7 +343,7 @@ void PX4FirmwarePlugin::guidedModeTakeoff(Vehicle* vehicle, double takeoffAltRel static_cast(takeoffAltAMSL)); // AMSL altitude } -double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle* vehicle) const +double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* vehicle) const { QString speedParam("MPC_XY_VEL_MAX"); @@ -349,7 +351,7 @@ double PX4FirmwarePlugin::maximumHorizontalSpeedMultirotor(Vehicle* vehicle) con return vehicle->parameterManager()->getParameter(ParameterManager::defaultComponentId, speedParam)->rawValue().toDouble(); } - return FirmwarePlugin::maximumHorizontalSpeedMultirotor(vehicle); + return FirmwarePlugin::maximumHorizontalSpeedMultirotorMetersSecond(vehicle); } double PX4FirmwarePlugin::maximumEquivalentAirspeed(Vehicle* vehicle) const @@ -385,7 +387,7 @@ bool PX4FirmwarePlugin::fixedWingAirSpeedLimitsAvailable(Vehicle* vehicle) const vehicle->parameterManager()->parameterExists(ParameterManager::defaultComponentId, "FW_AIRSPD_MAX"); } -void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const +bool PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const { // PX4 doesn't support setting the forward flight loiter radius of // MAV_CMD_DO_REPOSITION @@ -393,7 +395,7 @@ void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoord if (qIsNaN(vehicle->altitudeAMSL()->rawValue().toDouble())) { qgcApp()->showAppMessage(tr("Unable to go to location, vehicle position not known.")); - return; + return false; } if (vehicle->capabilityBits() & MAV_PROTOCOL_CAPABILITY_COMMAND_INT) { @@ -420,6 +422,8 @@ void PX4FirmwarePlugin::guidedModeGotoLocation(Vehicle* vehicle, const QGeoCoord static_cast(gotoCoord.longitude()), vehicle->altitudeAMSL()->rawValue().toFloat()); } + + return true; } typedef struct { @@ -433,13 +437,13 @@ static void _pauseVehicleThenChangeAltResultHandler(void* resultHandlerData, int if (ack.result != MAV_RESULT_ACCEPTED) { switch (failureCode) { case Vehicle::MavCmdResultCommandResultOnly: - qDebug() << QStringLiteral("MAV_CMD_DO_REPOSITION error(%1)").arg(ack.result); + qCDebug(PX4FirmwarePluginLog) << QStringLiteral("MAV_CMD_DO_REPOSITION error(%1)").arg(ack.result); break; case Vehicle::MavCmdResultFailureNoResponseToCommand: - qDebug() << "MAV_CMD_DO_REPOSITION no response from vehicle"; + qCDebug(PX4FirmwarePluginLog) << "MAV_CMD_DO_REPOSITION no response from vehicle"; break; case Vehicle::MavCmdResultFailureDuplicateCommand: - qDebug() << "Internal Error: MAV_CMD_DO_REPOSITION could not be sent due to duplicate command"; + qCDebug(PX4FirmwarePluginLog) << "Internal Error: MAV_CMD_DO_REPOSITION could not be sent due to duplicate command"; break; } } diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h index 2713af1fb481..438c3c53b62a 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h @@ -37,12 +37,12 @@ class PX4FirmwarePlugin : public FirmwarePlugin void guidedModeRTL (Vehicle* vehicle, bool smartRTL) const override; void guidedModeLand (Vehicle* vehicle) const override; void guidedModeTakeoff (Vehicle* vehicle, double takeoffAltRel) const override; - double maximumHorizontalSpeedMultirotor(Vehicle* vehicle) const override; + double maximumHorizontalSpeedMultirotorMetersSecond(Vehicle* vehicle) const override; double maximumEquivalentAirspeed(Vehicle* vehicle) const override; double minimumEquivalentAirspeed(Vehicle* vehicle) const override; bool mulirotorSpeedLimitsAvailable(Vehicle* vehicle) const override; bool fixedWingAirSpeedLimitsAvailable(Vehicle* vehicle) const override; - void guidedModeGotoLocation (Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; + bool guidedModeGotoLocation (Vehicle* vehicle, const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) const override; void guidedModeChangeAltitude (Vehicle* vehicle, double altitudeRel, bool pauseVehicle) override; void guidedModeChangeGroundSpeedMetersSecond(Vehicle* vehicle, double groundspeed) const override; void guidedModeChangeEquivalentAirspeedMetersSecond(Vehicle* vehicle, double airspeed_equiv) const override; diff --git a/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml b/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml index 66a3ff1bf333..9c2f190c5a9e 100644 --- a/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml +++ b/src/FirmwarePlugin/PX4/PX4ParameterFactMetaData.xml @@ -2,209 +2,17 @@ 3 1 15 - - - Speed controller bandwidth - Speed controller bandwidth, in Hz. Higher values result in faster speed and current rise times, but may result in overshoot and higher current consumption. For fixed-wing aircraft, this value should be less than 50 Hz; for multirotors, values up to 100 Hz may provide improvements in responsiveness. - 10 - 250 - Hz - - - Reverse direction - Motor spin direction as detected during initial enumeration. Use 0 or 1 to reverse direction. - 0 - 1 - - - Speed (RPM) controller gain - Speed (RPM) controller gain. Determines controller - aggressiveness; units are amp-seconds per radian. Systems with - higher rotational inertia (large props) will need gain increased; - systems with low rotational inertia (small props) may need gain - decreased. Higher values result in faster response, but may result - in oscillation and excessive overshoot. Lower values result in a - slower, smoother response. - 0.00 - 1.00 - C/rad - 3 - - - Idle speed (e Hz) - Idle speed (e Hz) - 0.0 - 100.0 - Hz - 3 - - - Spin-up rate (e Hz/s) - Spin-up rate (e Hz/s) - 5 - 1000 - 1/s^2 - - - Index of this ESC in throttle command messages. - Index of this ESC in throttle command messages. - 0 - 15 - - - Extended status ID - Extended status ID - 1 - 1000000 - - - Extended status interval (µs) - Extended status interval (µs) - 0 - 1000000 - us - - - ESC status interval (µs) - ESC status interval (µs) - 1000000 - us - - - Motor current limit in amps - Motor current limit in amps. This determines the maximum - current controller setpoint, as well as the maximum allowable - current setpoint slew rate. This value should generally be set to - the continuous current rating listed in the motor’s specification - sheet, or set equal to the motor’s specified continuous power - divided by the motor voltage limit. - 1 - 80 - A - 3 - - - Motor Kv in RPM per volt - Motor Kv in RPM per volt. This can be taken from the motor’s - specification sheet; accuracy will help control performance but - some deviation from the specified value is acceptable. - 0 - 4000 - rpm/V - - - READ ONLY: Motor inductance in henries. - READ ONLY: Motor inductance in henries. This is measured on start-up. - H - 3 - - - Number of motor poles. - Number of motor poles. Used to convert mechanical speeds to - electrical speeds. This number should be taken from the motor’s - specification sheet. - 2 - 40 - - - READ ONLY: Motor resistance in ohms - READ ONLY: Motor resistance in ohms. This is measured on start-up. When - tuning a new motor, check that this value is approximately equal - to the value shown in the motor’s specification sheet. - Ohm - 3 - - - Acceleration limit (V) - Acceleration limit (V) - 0.01 - 1.00 - V - 3 - - - Motor voltage limit in volts - Motor voltage limit in volts. The current controller’s - commanded voltage will never exceed this value. Note that this may - safely be above the nominal voltage of the motor; to determine the - actual motor voltage limit, divide the motor’s rated power by the - motor current limit. - 0 - V - 3 - - - - - GNSS dynamic model - Dynamic model used in the GNSS positioning engine. 0 – - Automotive, 1 – Sea, 2 – Airborne. - - 0 - 2 - - Automotive - Sea - Airborne - - - - Broadcast old GNSS fix message - Broadcast the old (deprecated) GNSS fix message - uavcan.equipment.gnss.Fix alongside the new alternative - uavcan.equipment.gnss.Fix2. It is recommended to - disable this feature to reduce the CAN bus traffic. - - 0 - 1 - - Fix2 - Fix and Fix2 - - - - device health warning - Set the device health to Warning if the dimensionality of - the GNSS solution is less than this value. 3 for the full (3D) - solution, 2 for planar (2D) solution, 1 for time-only solution, - 0 disables the feature. - - 0 - 3 - - disables the feature - time-only solution - planar (2D) solution - full (3D) solution - - - - - Set the device health to Warning if the number of satellites - used in the GNSS solution is below this threshold. Zero - disables the feature - - - - - Set the device health to Warning if the number of satellites - used in the GNSS solution is below this threshold. Zero - disables the feature - - 0 - 1000000 - us - - First 4 characters of CALLSIGN - Sets first 4 characters of a total of 8. Valid characters are A-Z, 0-9, " ". Example "PX4 " -> 1347957792 For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. + Sets first 4 characters of a total of 8. Valid characters are A-Z, 0-9, " ". Example "PX4 " -> 1347957792 +For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. true Second 4 characters of CALLSIGN - Sets second 4 characters of a total of 8. Valid characters are A-Z, 0-9, " " only. Example "TEST" -> 1413829460 For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. + Sets second 4 characters of a total of 8. Valid characters are A-Z, 0-9, " " only. Example "TEST" -> 1413829460 +For CALLSIGN shorter than 8 characters use the null terminator at the end '\0'. true @@ -356,225 +164,15 @@ - - PCA9685 Output Channel 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 10 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 11 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 12 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 13 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 14 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 15 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 16 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PCA9685 Output Channel 9 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - Put the selected channels into Duty-Cycle output mode - The driver will output standard pulse-width encoded signal without this bit set. To make PCA9685 output in duty-cycle fashion, please enable the corresponding channel bit here and adjusting standard params to suit your need. The driver will have 12bits resolution for duty-cycle output. That means to achieve 0% to 100% output range on one channel, the corresponding params MIN and MAX for the channel should be set to 0 and 4096. Other standard params follows the same rule. - 0 - 65535 - - Put CH1 to Duty-Cycle mode - Put CH2 to Duty-Cycle mode - Put CH3 to Duty-Cycle mode - Put CH4 to Duty-Cycle mode - Put CH5 to Duty-Cycle mode - Put CH6 to Duty-Cycle mode - Put CH7 to Duty-Cycle mode - Put CH8 to Duty-Cycle mode - Put CH9 to Duty-Cycle mode - Put CH10 to Duty-Cycle mode - Put CH11 to Duty-Cycle mode - Put CH12 to Duty-Cycle mode - Put CH13 to Duty-Cycle mode - Put CH14 to Duty-Cycle mode - Put CH15 to Duty-Cycle mode - Put CH16 to Duty-Cycle mode - - - - PCA9685 Output Channel 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC1). - -1 - 2200 - - - PCA9685 Output Channel 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC10). - -1 - 2200 - - - PCA9685 Output Channel 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC11). - -1 - 2200 - - - PCA9685 Output Channel 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC12). - -1 - 2200 - - - PCA9685 Output Channel 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC13). - -1 - 2200 - - - PCA9685 Output Channel 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC14). - -1 - 2200 - - - PCA9685 Output Channel 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC15). - -1 - 2200 - - - PCA9685 Output Channel 16 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC16). - -1 - 2200 - - - PCA9685 Output Channel 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC2). - -1 - 2200 - - - PCA9685 Output Channel 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC3). - -1 - 2200 - - - PCA9685 Output Channel 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC4). - -1 - 2200 - - - PCA9685 Output Channel 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC5). - -1 - 2200 - - - PCA9685 Output Channel 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC6). - -1 - 2200 - - - PCA9685 Output Channel 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC7). - -1 - 2200 - - - PCA9685 Output Channel 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC8). - -1 - 2200 - - - PCA9685 Output Channel 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PCA9685_FUNC9). - -1 - 2200 - - - PCA9685 Output Channel 1 Output Function - Select what should be output on PCA9685 Output Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 1 Output Function + Select what should be output on SIM Channel 1. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -623,11 +221,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 10 Output Function - Select what should be output on PCA9685 Output Channel 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 10 Output Function + Select what should be output on SIM Channel 10. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -676,11 +284,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 11 Output Function - Select what should be output on PCA9685 Output Channel 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 11 Output Function + Select what should be output on SIM Channel 11. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -729,11 +347,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 12 Output Function - Select what should be output on PCA9685 Output Channel 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 12 Output Function + Select what should be output on SIM Channel 12. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -782,11 +410,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 13 Output Function - Select what should be output on PCA9685 Output Channel 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 13 Output Function + Select what should be output on SIM Channel 13. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -835,11 +473,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 14 Output Function - Select what should be output on PCA9685 Output Channel 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 14 Output Function + Select what should be output on SIM Channel 14. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -888,11 +536,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 15 Output Function - Select what should be output on PCA9685 Output Channel 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 15 Output Function + Select what should be output on SIM Channel 15. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -941,11 +599,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 16 Output Function - Select what should be output on PCA9685 Output Channel 16. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 16 Output Function + Select what should be output on SIM Channel 16. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -994,11 +662,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 2 Output Function - Select what should be output on PCA9685 Output Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 2 Output Function + Select what should be output on SIM Channel 2. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1047,11 +725,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 3 Output Function - Select what should be output on PCA9685 Output Channel 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 3 Output Function + Select what should be output on SIM Channel 3. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1100,11 +788,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 4 Output Function - Select what should be output on PCA9685 Output Channel 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 4 Output Function + Select what should be output on SIM Channel 4. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1153,11 +851,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 5 Output Function - Select what should be output on PCA9685 Output Channel 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 5 Output Function + Select what should be output on SIM Channel 5. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1206,11 +914,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 6 Output Function - Select what should be output on PCA9685 Output Channel 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 6 Output Function + Select what should be output on SIM Channel 6. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1259,11 +977,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 7 Output Function - Select what should be output on PCA9685 Output Channel 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 7 Output Function + Select what should be output on SIM Channel 7. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1312,11 +1040,21 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 8 Output Function - Select what should be output on PCA9685 Output Channel 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + + SIM Channel 8 Output Function + Select what should be output on SIM Channel 8. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1365,11 +1103,21 @@ Gimbal Yaw Gripper Landing Gear Wheel - - - - PCA9685 Output Channel 9 Output Function - Select what should be output on PCA9685 Output Channel 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter + + + + SIM Channel 9 Output Function + Select what should be output on SIM Channel 9. +The default failsafe value is set according to the selected function: +- 'Min' for ConstantMin +- 'Max' for ConstantMax +- 'Max' for Parachute +- ('Max'+'Min')/2 for Servos +- 'Disarmed' for the rest Disabled Constant Min @@ -1418,8173 +1166,93 @@ Gimbal Yaw Gripper Landing Gear Wheel + IC Engine Ignition + IC Engine Throttle + IC Engine Choke + IC Engine Starter - - PCA9685 Output Channel 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 10 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 11 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 12 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 13 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 14 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 15 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 16 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 9 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PCA9685 Output Channel 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 10 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 11 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 12 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 13 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 14 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 15 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 16 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PCA9685 Output Channel 9 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM cycle frequency - Controls the PWM frequency at timing perspective. This is independent from PWM update frequency, as PCA9685 is capable to output without being continuously commanded by FC. Higher frequency leads to more accurate pulse width, but some ESCs and servos may not support it. This parameter should be set to the same value as PWM update rate in most case. This parameter MUST NOT exceed upper limit of 400.0, if any outputs as generic 1000~2000us pulse width is desired. Frequency higher than 400 only makes sense in duty-cycle mode. - 23.8 - 1525.87 - 2 - - - Reverse Output Range for PCA9685 Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. + + Reverse Output Range for SIM + Allows to reverse the output range for each channel. +Note: this is only useful for servos. 0 65535 - PCA9685 Output Channel 1 - PCA9685 Output Channel 2 - PCA9685 Output Channel 3 - PCA9685 Output Channel 4 - PCA9685 Output Channel 5 - PCA9685 Output Channel 6 - PCA9685 Output Channel 7 - PCA9685 Output Channel 8 - PCA9685 Output Channel 9 - PCA9685 Output Channel 10 - PCA9685 Output Channel 11 - PCA9685 Output Channel 12 - PCA9685 Output Channel 13 - PCA9685 Output Channel 14 - PCA9685 Output Channel 15 - PCA9685 Output Channel 16 + SIM Channel 1 + SIM Channel 2 + SIM Channel 3 + SIM Channel 4 + SIM Channel 5 + SIM Channel 6 + SIM Channel 7 + SIM Channel 8 + SIM Channel 9 + SIM Channel 10 + SIM Channel 11 + SIM Channel 12 + SIM Channel 13 + SIM Channel 14 + SIM Channel 15 + SIM Channel 16 - - PWM update rate - Controls the update rate of PWM output. Flight Controller will inform those numbers of update events in a second, to PCA9685. Higher update rate will consume more I2C bandwidth, which may even lead to worse output latency, or completely block I2C bus. - 50.0 - 400.0 - 2 - - - PWM Aux 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + + + Gate size for sideslip angle fusion + Sets the number of standard deviations used by the innovation consistency test. + 1 + 5 + SD - - PWM Capture 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Wind estimator sideslip measurement noise + Sideslip measurement noise of the internal wind estimator(s) of the airspeed selector. + 0 + 1 + rad + 3 - - PWM Capture 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Enable checks on airspeed sensors + Controls which checks are run to check airspeed data for validity. Only applied if ASPD_PRIMARY > 0. +Note: The missing data check (bit 0) is implicitly always enabled when ASPD_DO_CHECKS > 0, even if bit 0 is not explicitly set. + 0 + 31 + + Only data missing check (triggers if more than 1s no data) + Data stuck (triggers if data is exactly constant for 2s in FW mode) + Innovation check (see ASPD_FS_INNOV) + Load factor check (triggers if measurement is below stall speed) + First principle check (airspeed change vs. throttle and pitch) + - - PWM Aux 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 + + Fallback options + + Fallback only to other airspeed sensors + Fallback to groundspeed-minus-windspeed airspeed estimation + Fallback to thrust based airspeed estimation + - - PWM Aux 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Capture 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - PWM Aux 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC1). - -1 - 2200 - - - PWM Capture 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC2). - -1 - 2200 - - - PWM Capture 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC3). - -1 - 2200 - - - PWM Aux 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC2). - -1 - 2200 - - - PWM Aux 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC3). - -1 - 2200 - - - PWM Aux 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC4). - -1 - 2200 - - - PWM Aux 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC5). - -1 - 2200 - - - PWM Aux 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC6). - -1 - 2200 - - - PWM Aux 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC7). - -1 - 2200 - - - PWM Aux 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC8). - -1 - 2200 - - - PWM Capture 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_AUX_FUNC1). - -1 - 2200 - - - PWM Aux 1 Output Function - Select what should be output on PWM Aux 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 2 Output Function - Select what should be output on PWM Capture 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 3 Output Function - Select what should be output on PWM Capture 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 2 Output Function - Select what should be output on PWM Aux 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 3 Output Function - Select what should be output on PWM Aux 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 4 Output Function - Select what should be output on PWM Aux 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 5 Output Function - Select what should be output on PWM Aux 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 6 Output Function - Select what should be output on PWM Aux 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 7 Output Function - Select what should be output on PWM Aux 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 8 Output Function - Select what should be output on PWM Aux 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Capture 1 Output Function - Select what should be output on PWM Capture 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - Camera Trigger - Camera Capture - PPS Input - - - - PWM Aux 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Capture 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - PWM Aux 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Aux 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - PWM Capture 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - Reverse Output Range for PWM AUX - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 2047 - - PWM Aux 1 - PWM Aux 2 - PWM Aux 3 - PWM Aux 4 - PWM Aux 5 - PWM Aux 6 - PWM Aux 7 - PWM Aux 8 - PWM Capture 1 - PWM Capture 2 - PWM Capture 3 - - - - Output Protocol Configuration for PWM Aux 1-4 - Select which Output Protocol to use for outputs PWM Aux 1-4. Custom PWM rates can be used by directly setting any value >0. - True - - DShot150 - DShot300 - DShot600 - DShot1200 - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Aux 5-6 - Select which Output Protocol to use for outputs PWM Aux 5-6. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Aux 7-8 - Select which Output Protocol to use for outputs PWM Aux 7-8. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for PWM Capture 1-3 - Select which Output Protocol to use for outputs PWM Capture 1-3. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - MAIN 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 800 - 2200 - - - MAIN 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC1). - -1 - 2200 - - - MAIN 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC2). - -1 - 2200 - - - MAIN 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC3). - -1 - 2200 - - - MAIN 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC4). - -1 - 2200 - - - MAIN 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC5). - -1 - 2200 - - - MAIN 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC6). - -1 - 2200 - - - MAIN 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC7). - -1 - 2200 - - - MAIN 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see PWM_MAIN_FUNC8). - -1 - 2200 - - - MAIN 1 Output Function - Select what should be output on MAIN 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 2 Output Function - Select what should be output on MAIN 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 3 Output Function - Select what should be output on MAIN 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 4 Output Function - Select what should be output on MAIN 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 5 Output Function - Select what should be output on MAIN 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 6 Output Function - Select what should be output on MAIN 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 7 Output Function - Select what should be output on MAIN 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 8 Output Function - Select what should be output on MAIN 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - MAIN 1 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 2 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 3 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 4 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 5 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 6 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 7 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 8 Maximum Value - Maxmimum output value (when not disarmed). - 1600 - 2200 - - - MAIN 1 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 2 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 3 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 4 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 5 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 6 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 7 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - MAIN 8 Minimum Value - Minimum output value (when not disarmed). - 800 - 1400 - - - Reverse Output Range for PWM MAIN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - MAIN 1 - MAIN 2 - MAIN 3 - MAIN 4 - MAIN 5 - MAIN 6 - MAIN 7 - MAIN 8 - - - - Output Protocol Configuration for MAIN 1-2 - Select which Output Protocol to use for outputs MAIN 1-2. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for MAIN 3-4 - Select which Output Protocol to use for outputs MAIN 3-4. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Output Protocol Configuration for MAIN 5-8 - Select which Output Protocol to use for outputs MAIN 5-8. Custom PWM rates can be used by directly setting any value >0. - True - - OneShot - PWM 50 Hz - PWM 100 Hz - PWM 200 Hz - PWM 400 Hz - - - - Roboclaw Driver Channel 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 128 - 128 - - - Roboclaw Driver Channel 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 128 - 128 - - - Roboclaw Driver Channel 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see RBCLW_FUNC1). - -1 - 257 - - - Roboclaw Driver Channel 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see RBCLW_FUNC2). - -1 - 257 - - - Roboclaw Driver Channel 1 Output Function - Select what should be output on Roboclaw Driver Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Roboclaw Driver Channel 2 Output Function - Select what should be output on Roboclaw Driver Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Roboclaw Driver Channel 1 Maximum Value - Maxmimum output value (when not disarmed). - 128 - 256 - - - Roboclaw Driver Channel 2 Maximum Value - Maxmimum output value (when not disarmed). - 128 - 256 - - - Roboclaw Driver Channel 1 Minimum Value - Minimum output value (when not disarmed). - 1 - 128 - - - Roboclaw Driver Channel 2 Minimum Value - Minimum output value (when not disarmed). - 1 - 128 - - - Reverse Output Range for Roboclaw Driver - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 3 - - Roboclaw Driver Channel 1 - Roboclaw Driver Channel 2 - - - - SIM_GZ ESC 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC1). - -1 - 1000 - - - SIM_GZ ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC2). - -1 - 1000 - - - SIM_GZ ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC3). - -1 - 1000 - - - SIM_GZ ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC4). - -1 - 1000 - - - SIM_GZ ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC5). - -1 - 1000 - - - SIM_GZ ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC6). - -1 - 1000 - - - SIM_GZ ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC7). - -1 - 1000 - - - SIM_GZ ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_EC_FUNC8). - -1 - 1000 - - - SIM_GZ ESC 1 Output Function - Select what should be output on SIM_GZ ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 2 Output Function - Select what should be output on SIM_GZ ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 3 Output Function - Select what should be output on SIM_GZ ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 4 Output Function - Select what should be output on SIM_GZ ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 5 Output Function - Select what should be output on SIM_GZ ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 6 Output Function - Select what should be output on SIM_GZ ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 7 Output Function - Select what should be output on SIM_GZ ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 8 Output Function - Select what should be output on SIM_GZ ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - SIM_GZ ESC 1 - SIM_GZ ESC 2 - SIM_GZ ESC 3 - SIM_GZ ESC 4 - SIM_GZ ESC 5 - SIM_GZ ESC 6 - SIM_GZ ESC 7 - SIM_GZ ESC 8 - - - - SIM_GZ Servo 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - SIM_GZ Servo 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC1). - -1 - 1000 - - - SIM_GZ Servo 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC2). - -1 - 1000 - - - SIM_GZ Servo 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC3). - -1 - 1000 - - - SIM_GZ Servo 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC4). - -1 - 1000 - - - SIM_GZ Servo 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC5). - -1 - 1000 - - - SIM_GZ Servo 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC6). - -1 - 1000 - - - SIM_GZ Servo 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC7). - -1 - 1000 - - - SIM_GZ Servo 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_SV_FUNC8). - -1 - 1000 - - - SIM_GZ Servo 1 Output Function - Select what should be output on SIM_GZ Servo 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 2 Output Function - Select what should be output on SIM_GZ Servo 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 3 Output Function - Select what should be output on SIM_GZ Servo 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 4 Output Function - Select what should be output on SIM_GZ Servo 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 5 Output Function - Select what should be output on SIM_GZ Servo 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 6 Output Function - Select what should be output on SIM_GZ Servo 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 7 Output Function - Select what should be output on SIM_GZ Servo 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 8 Output Function - Select what should be output on SIM_GZ Servo 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Servo 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - SIM_GZ Servo 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - SIM_GZ Servo 1 - SIM_GZ Servo 2 - SIM_GZ Servo 3 - SIM_GZ Servo 4 - SIM_GZ Servo 5 - SIM_GZ Servo 6 - SIM_GZ Servo 7 - SIM_GZ Servo 8 - - - - SIM_GZ Wheels 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 200 - - - SIM_GZ Wheels 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC1). - -1 - 200 - - - SIM_GZ Wheels 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC2). - -1 - 200 - - - SIM_GZ Wheels 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC3). - -1 - 200 - - - SIM_GZ Wheels 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see SIM_GZ_WH_FUNC4). - -1 - 200 - - - SIM_GZ Wheels 1 Output Function - Select what should be output on SIM_GZ Wheels 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 2 Output Function - Select what should be output on SIM_GZ Wheels 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 3 Output Function - Select what should be output on SIM_GZ Wheels 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 4 Output Function - Select what should be output on SIM_GZ Wheels 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - SIM_GZ Wheels 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - SIM_GZ Wheels 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 200 - - - Reverse Output Range for SIM_GZ - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - SIM_GZ Wheels 1 - SIM_GZ Wheels 2 - SIM_GZ Wheels 3 - SIM_GZ Wheels 4 - - - - TAP ESC Output ESC 1 Output Function - Select what should be output on TAP ESC Output ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 2 Output Function - Select what should be output on TAP ESC Output ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 3 Output Function - Select what should be output on TAP ESC Output ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 4 Output Function - Select what should be output on TAP ESC Output ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 5 Output Function - Select what should be output on TAP ESC Output ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 6 Output Function - Select what should be output on TAP ESC Output ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 7 Output Function - Select what should be output on TAP ESC Output ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - TAP ESC Output ESC 8 Output Function - Select what should be output on TAP ESC Output ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for TAP ESC Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - TAP ESC Output ESC 1 - TAP ESC Output ESC 2 - TAP ESC Output ESC 3 - TAP ESC Output ESC 4 - TAP ESC Output ESC 5 - TAP ESC Output ESC 6 - TAP ESC Output ESC 7 - TAP ESC Output ESC 8 - - - - UAVCAN ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC1). - -1 - 8191 - - - UAVCAN ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC2). - -1 - 8191 - - - UAVCAN ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC3). - -1 - 8191 - - - UAVCAN ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC4). - -1 - 8191 - - - UAVCAN ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC5). - -1 - 8191 - - - UAVCAN ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC6). - -1 - 8191 - - - UAVCAN ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC7). - -1 - 8191 - - - UAVCAN ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_EC_FUNC8). - -1 - 8191 - - - UAVCAN ESC 1 Output Function - Select what should be output on UAVCAN ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 2 Output Function - Select what should be output on UAVCAN ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 3 Output Function - Select what should be output on UAVCAN ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 4 Output Function - Select what should be output on UAVCAN ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 5 Output Function - Select what should be output on UAVCAN ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 6 Output Function - Select what should be output on UAVCAN ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 7 Output Function - Select what should be output on UAVCAN ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 8 Output Function - Select what should be output on UAVCAN ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCAN ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - Reverse Output Range for UAVCAN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - UAVCAN ESC 1 - UAVCAN ESC 2 - UAVCAN ESC 3 - UAVCAN ESC 4 - UAVCAN ESC 5 - UAVCAN ESC 6 - UAVCAN ESC 7 - UAVCAN ESC 8 - - - - UAVCAN Servo 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 1000 - - - UAVCAN Servo 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC1). - -1 - 1000 - - - UAVCAN Servo 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC2). - -1 - 1000 - - - UAVCAN Servo 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC3). - -1 - 1000 - - - UAVCAN Servo 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC4). - -1 - 1000 - - - UAVCAN Servo 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC5). - -1 - 1000 - - - UAVCAN Servo 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC6). - -1 - 1000 - - - UAVCAN Servo 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC7). - -1 - 1000 - - - UAVCAN Servo 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UAVCAN_SV_FUNC8). - -1 - 1000 - - - UAVCAN Servo 1 Output Function - Select what should be output on UAVCAN Servo 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 2 Output Function - Select what should be output on UAVCAN Servo 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 3 Output Function - Select what should be output on UAVCAN Servo 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 4 Output Function - Select what should be output on UAVCAN Servo 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 5 Output Function - Select what should be output on UAVCAN Servo 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 6 Output Function - Select what should be output on UAVCAN Servo 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 7 Output Function - Select what should be output on UAVCAN Servo 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 8 Output Function - Select what should be output on UAVCAN Servo 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCAN Servo 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - UAVCAN Servo 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 1000 - - - Reverse Output Range for UAVCAN - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 255 - - UAVCAN Servo 1 - UAVCAN Servo 2 - UAVCAN Servo 3 - UAVCAN Servo 4 - UAVCAN Servo 5 - UAVCAN Servo 6 - UAVCAN Servo 7 - UAVCAN Servo 8 - - - - UAVCANv1 ESC 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC1). - -1 - 8191 - - - UAVCANv1 ESC 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC10). - -1 - 8191 - - - UAVCANv1 ESC 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC11). - -1 - 8191 - - - UAVCANv1 ESC 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC12). - -1 - 8191 - - - UAVCANv1 ESC 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC13). - -1 - 8191 - - - UAVCANv1 ESC 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC14). - -1 - 8191 - - - UAVCANv1 ESC 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC15). - -1 - 8191 - - - UAVCANv1 ESC 16 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC16). - -1 - 8191 - - - UAVCANv1 ESC 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC2). - -1 - 8191 - - - UAVCANv1 ESC 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC3). - -1 - 8191 - - - UAVCANv1 ESC 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC4). - -1 - 8191 - - - UAVCANv1 ESC 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC5). - -1 - 8191 - - - UAVCANv1 ESC 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC6). - -1 - 8191 - - - UAVCANv1 ESC 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC7). - -1 - 8191 - - - UAVCANv1 ESC 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC8). - -1 - 8191 - - - UAVCANv1 ESC 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see UCAN1_ESC_FUNC9). - -1 - 8191 - - - UAVCANv1 ESC 1 Output Function - Select what should be output on UAVCANv1 ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 10 Output Function - Select what should be output on UAVCANv1 ESC 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 11 Output Function - Select what should be output on UAVCANv1 ESC 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 12 Output Function - Select what should be output on UAVCANv1 ESC 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 13 Output Function - Select what should be output on UAVCANv1 ESC 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 14 Output Function - Select what should be output on UAVCANv1 ESC 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 15 Output Function - Select what should be output on UAVCANv1 ESC 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 16 Output Function - Select what should be output on UAVCANv1 ESC 16. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 2 Output Function - Select what should be output on UAVCANv1 ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 3 Output Function - Select what should be output on UAVCANv1 ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 4 Output Function - Select what should be output on UAVCANv1 ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 5 Output Function - Select what should be output on UAVCANv1 ESC 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 6 Output Function - Select what should be output on UAVCANv1 ESC 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 7 Output Function - Select what should be output on UAVCANv1 ESC 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 8 Output Function - Select what should be output on UAVCANv1 ESC 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 9 Output Function - Select what should be output on UAVCANv1 ESC 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - UAVCANv1 ESC 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 10 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 11 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 12 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 13 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 14 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 15 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 16 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 9 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 10 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 11 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 12 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 13 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 14 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 15 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 16 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - UAVCANv1 ESC 9 Minimum Value - Minimum output value (when not disarmed). - 0 - 8191 - - - Reverse Output Range for UAVCANv1 - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 65535 - - UAVCANv1 ESC 1 - UAVCANv1 ESC 2 - UAVCANv1 ESC 3 - UAVCANv1 ESC 4 - UAVCANv1 ESC 5 - UAVCANv1 ESC 6 - UAVCANv1 ESC 7 - UAVCANv1 ESC 8 - UAVCANv1 ESC 9 - UAVCANv1 ESC 10 - UAVCANv1 ESC 11 - UAVCANv1 ESC 12 - UAVCANv1 ESC 13 - UAVCANv1 ESC 14 - UAVCANv1 ESC 15 - UAVCANv1 ESC 16 - - - - VOXL2 IO Output PWM Channel 1 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 2 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 3 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL2 IO Output PWM Channel 4 Output Function - Select what should be output on VOXL2 IO Output PWM Channel 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for VOXL2 IO Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - VOXL2 IO Output PWM Channel 1 - VOXL2 IO Output PWM Channel 2 - VOXL2 IO Output PWM Channel 3 - VOXL2 IO Output PWM Channel 4 - - - - VOXL ESC Output ESC 1 Output Function - Select what should be output on VOXL ESC Output ESC 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 2 Output Function - Select what should be output on VOXL ESC Output ESC 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 3 Output Function - Select what should be output on VOXL ESC Output ESC 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - VOXL ESC Output ESC 4 Output Function - Select what should be output on VOXL ESC Output ESC 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Reverse Output Range for VOXL ESC Output - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 15 - - VOXL ESC Output ESC 1 - VOXL ESC Output ESC 2 - VOXL ESC Output ESC 3 - VOXL ESC Output ESC 4 - - - - Vertiq IO CVI 0 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 1 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 10 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 11 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 12 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 13 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 14 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 15 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 2 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 3 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 4 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 5 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 6 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 7 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 8 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 9 Disarmed Value - This is the output value that is set when not armed. Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set. - 0 - 65535 - - - Vertiq IO CVI 0 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC0). - -1 - 500 - - - Vertiq IO CVI 1 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC1). - -1 - 500 - - - Vertiq IO CVI 10 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC10). - -1 - 500 - - - Vertiq IO CVI 11 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC11). - -1 - 500 - - - Vertiq IO CVI 12 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC12). - -1 - 500 - - - Vertiq IO CVI 13 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC13). - -1 - 500 - - - Vertiq IO CVI 14 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC14). - -1 - 500 - - - Vertiq IO CVI 15 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC15). - -1 - 500 - - - Vertiq IO CVI 2 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC2). - -1 - 500 - - - Vertiq IO CVI 3 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC3). - -1 - 500 - - - Vertiq IO CVI 4 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC4). - -1 - 500 - - - Vertiq IO CVI 5 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC5). - -1 - 500 - - - Vertiq IO CVI 6 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC6). - -1 - 500 - - - Vertiq IO CVI 7 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC7). - -1 - 500 - - - Vertiq IO CVI 8 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC8). - -1 - 500 - - - Vertiq IO CVI 9 Failsafe Value - This is the output value that is set when in failsafe mode. When set to -1 (default), the value depends on the function (see VTQ_IO_FUNC9). - -1 - 500 - - - Vertiq IO CVI 0 Output Function - Select what should be output on Vertiq IO CVI 0. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 1 Output Function - Select what should be output on Vertiq IO CVI 1. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 10 Output Function - Select what should be output on Vertiq IO CVI 10. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 11 Output Function - Select what should be output on Vertiq IO CVI 11. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 12 Output Function - Select what should be output on Vertiq IO CVI 12. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 13 Output Function - Select what should be output on Vertiq IO CVI 13. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 14 Output Function - Select what should be output on Vertiq IO CVI 14. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 15 Output Function - Select what should be output on Vertiq IO CVI 15. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 2 Output Function - Select what should be output on Vertiq IO CVI 2. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 3 Output Function - Select what should be output on Vertiq IO CVI 3. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 4 Output Function - Select what should be output on Vertiq IO CVI 4. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 5 Output Function - Select what should be output on Vertiq IO CVI 5. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 6 Output Function - Select what should be output on Vertiq IO CVI 6. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 7 Output Function - Select what should be output on Vertiq IO CVI 7. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 8 Output Function - Select what should be output on Vertiq IO CVI 8. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 9 Output Function - Select what should be output on Vertiq IO CVI 9. The default failsafe value is set according to the selected function: - 'Min' for ConstantMin - 'Max' for ConstantMax - 'Max' for Parachute - ('Max'+'Min')/2 for Servos - 'Disarmed' for the rest - - Disabled - Constant Min - Constant Max - Motor 1 - Motor 2 - Motor 3 - Motor 4 - Motor 5 - Motor 6 - Motor 7 - Motor 8 - Motor 9 - Motor 10 - Motor 11 - Motor 12 - Servo 1 - Servo 2 - Servo 3 - Servo 4 - Servo 5 - Servo 6 - Servo 7 - Servo 8 - Peripheral via Actuator Set 1 - Peripheral via Actuator Set 2 - Peripheral via Actuator Set 3 - Peripheral via Actuator Set 4 - Peripheral via Actuator Set 5 - Peripheral via Actuator Set 6 - Landing Gear - Parachute - RC Roll - RC Pitch - RC Throttle - RC Yaw - RC Flaps - RC AUX 1 - RC AUX 2 - RC AUX 3 - RC AUX 4 - RC AUX 5 - RC AUX 6 - Gimbal Roll - Gimbal Pitch - Gimbal Yaw - Gripper - Landing Gear Wheel - - - - Vertiq IO CVI 0 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 1 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 10 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 11 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 12 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 13 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 14 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 15 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 2 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 3 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 4 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 5 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 6 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 7 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 8 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 9 Maximum Value - Maxmimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 0 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 1 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 10 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 11 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 12 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 13 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 14 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 15 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 2 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 3 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 4 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 5 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 6 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 7 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 8 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Vertiq IO CVI 9 Minimum Value - Minimum output value (when not disarmed). - 0 - 65535 - - - Reverse Output Range for Vertiq IO - Allows to reverse the output range for each channel. Note: this is only useful for servos. - 0 - 32767 - - Vertiq IO CVI 0 - Vertiq IO CVI 1 - Vertiq IO CVI 2 - Vertiq IO CVI 3 - Vertiq IO CVI 4 - Vertiq IO CVI 5 - Vertiq IO CVI 6 - Vertiq IO CVI 7 - Vertiq IO CVI 8 - Vertiq IO CVI 9 - Vertiq IO CVI 10 - Vertiq IO CVI 11 - Vertiq IO CVI 12 - Vertiq IO CVI 13 - Vertiq IO CVI 14 - Vertiq IO CVI 15 - - - - - - Gate size for sideslip angle fusion - Sets the number of standard deviations used by the innovation consistency test. - 1 - 5 - SD - - - Wind estimator sideslip measurement noise - Sideslip measurement noise of the internal wind estimator(s) of the airspeed selector. - 0 - 1 - rad - 3 - - - Enable checks on airspeed sensors - Controls which checks are run to check airspeed data for validity. Only applied if ASPD_PRIMARY > 0. - 0 - 31 - - Only data missing check (triggers if more than 1s no data) - Data stuck (triggers if data is exactly constant for 2s in FW mode) - Innovation check (see ASPD_FS_INNOV) - Load factor check (triggers if measurement is below stall speed) - First principle check (airspeed change vs. throttle and pitch) - - - - Enable fallback to sensor-less airspeed estimation - If set to true and airspeed checks are enabled, it will use a sensor-less airspeed estimation based on groundspeed minus windspeed if no other airspeed sensor available to fall back to. - - Disable fallback to sensor-less estimation - Enable fallback to sensor-less estimation - - - - First principle airspeed check time window - Window for comparing airspeed change to throttle and pitch change. Triggers when the airspeed change within this window is negative while throttle increases and the vehicle pitches down. Is meant to catch degrading airspeed blockages as can happen when flying through icing conditions. Relies on FW_THR_TRIM being set accurately. - 0 - s - 1 + + First principle airspeed check time window + Window for comparing airspeed change to throttle and pitch change. +Triggers when the airspeed change within this window is negative while throttle increases +and the vehicle pitches down. +Is meant to catch degrading airspeed blockages as can happen when flying through icing conditions. +Relies on FW_THR_TRIM being set accurately. + 0 + s + 1 Airspeed failure innovation threshold - This specifies the minimum airspeed innovation required to trigger a failsafe. Larger values make the check less sensitive, smaller values make it more sensitive. Large innovations indicate an inconsistency between predicted (groundspeed - windspeeed) and measured airspeed. The time required to detect a fault when the threshold is exceeded depends on the size of the exceedance and is controlled by the ASPD_FS_INTEG parameter. + This specifies the minimum airspeed innovation required to trigger a failsafe. Larger values make the check less sensitive, +smaller values make it more sensitive. Large innovations indicate an inconsistency between predicted (groundspeed - windspeeed) +and measured airspeed. +The time required to detect a fault when the threshold is exceeded depends on the size of the exceedance and is controlled by the ASPD_FS_INTEG parameter. 0.5 10.0 m/s @@ -9592,7 +1260,8 @@ Airspeed failure innovation integral threshold - This sets the time integral of airspeed innovation exceedance above ASPD_FS_INNOV required to trigger a failsafe. Larger values make the check less sensitive, smaller positive values make it more sensitive. + This sets the time integral of airspeed innovation exceedance above ASPD_FS_INNOV required to trigger a failsafe. +Larger values make the check less sensitive, smaller positive values make it more sensitive. 0.0 50.0 m @@ -9600,7 +1269,8 @@ Airspeed failsafe start delay - Delay before switching back to using airspeed sensor if checks indicate sensor is good. Set to a negative value to disable the re-enabling in flight. + Delay before switching back to using airspeed sensor if checks indicate sensor is good. +Set to a negative value to disable the re-enabling in flight. -1.0 s 1 @@ -9620,6 +1290,7 @@ First airspeed sensor Second airspeed sensor Third airspeed sensor + Thrust based airspeed @@ -9628,7 +1299,6 @@ 0.5 2.0 2 - true Scale of airspeed sensor 2 @@ -9636,7 +1306,6 @@ 0.5 2.0 2 - true Scale of airspeed sensor 3 @@ -9644,7 +1313,6 @@ 0.5 2.0 2 - true Controls when to apply the new estimated airspeed scale(s) @@ -9656,7 +1324,8 @@ Wind estimator true airspeed scale process noise spectral density - Airspeed scale process noise of the internal wind estimator(s) of the airspeed selector. When unaided, the scale uncertainty (1-sigma, unitless) increases by this amount every second. + Airspeed scale process noise of the internal wind estimator(s) of the airspeed selector. +When unaided, the scale uncertainty (1-sigma, unitless) increases by this amount every second. 0 0.1 1/s/sqrt(Hz) @@ -9677,17 +1346,19 @@ m/s 1 - - Horizontal wind uncertainty threshold for synthetic airspeed - The synthetic airspeed estimate (from groundspeed and heading) will be declared valid as soon and as long the horizontal wind uncertainty is below this value. - 0.001 + + Horizontal wind uncertainty threshold for valid ground-minus-wind + The airspeed alternative derived from groundspeed and heading will be declared valid +as soon and as long the horizontal wind uncertainty is below this value. + 0.01 5 m/s - 3 + 2 Wind estimator wind process noise spectral density - Wind process noise of the internal wind estimator(s) of the airspeed selector. When unaided, the wind estimate uncertainty (1-sigma, in m/s) increases by this amount every second. + Wind process noise of the internal wind estimator(s) of the airspeed selector. +When unaided, the wind estimate uncertainty (1-sigma, in m/s) increases by this amount every second. 0 1 m/s^2/sqrt(Hz) @@ -9711,7 +1382,8 @@ External heading usage mode (from Motion capture/Vision) - Set to 1 to use heading estimate from vision. Set to 2 to use heading from motion capture. + Set to 1 to use heading estimate from vision. +Set to 2 to use heading from motion capture. 0 2 @@ -9722,7 +1394,9 @@ Magnetic declination, in degrees - This parameter is not used in normal operation, as the declination is looked up based on the GPS coordinates of the vehicle. + This parameter is not used in normal operation, +as the declination is looked up based on the +GPS coordinates of the vehicle. deg 2 @@ -9757,7 +1431,9 @@ Controls when to apply the new gains - After the auto-tuning sequence is completed, a new set of gains is available and can be applied immediately or after landing. + After the auto-tuning sequence is completed, +a new set of gains is available and can be applied +immediately or after landing. Do not apply the new gains (logging only) Apply the new gains after disarm @@ -9766,7 +1442,11 @@ Tuning axes selection - Defines which axes will be tuned during the auto-tuning sequence Set bits in the following positions to enable: 0 : Roll 1 : Pitch 2 : Yaw + Defines which axes will be tuned during the auto-tuning sequence +Set bits in the following positions to enable: +0 : Roll +1 : Pitch +2 : Yaw 1 7 @@ -9776,8 +1456,8 @@ - Enable/disable auto tuning using an RC AUX input - Defines which RC_MAP_AUXn parameter maps the RC channel used to enable/disable auto tuning. + Enable/disable auto tuning using a manual control AUX input + Defines which RC_MAP_AUXn parameter maps the manual control channel used to enable/disable auto tuning. 0 6 @@ -9790,17 +1470,6 @@ Aux6 - - Start the autotuning sequence - WARNING: this will inject steps to the rate controller and can be dangerous. Only activate if you know what you are doing, and in a safe environment. Any motion of the remote stick will abort the signal injection and reset this parameter Best is to perform the identification in position or hold mode. Increase the amplitude of the injected signal using FW_AT_SYSID_AMP for more signal/noise ratio - - - Amplitude of the injected signal - This parameter scales the signal sent to the rate controller during system identification. - 0.1 - 6.0 - 1 - Start frequency of the injected signal Can be set lower or higher than the end frequency @@ -9809,7 +1478,7 @@ Hz 1 - + End frequency of the injected signal Can be set lower or higher than the start frequency 0.1 @@ -9825,7 +1494,7 @@ s 0 - + Input signal type Type of signal used during system identification to excite the system. @@ -9836,7 +1505,12 @@ Controls when to apply the new gains - After the auto-tuning sequence is completed, a new set of gains is available and can be applied immediately or after landing. WARNING Applying the gains in air is dangerous as there is no guarantee that those new gains will be able to stabilize the drone properly. + After the auto-tuning sequence is completed, +a new set of gains is available and can be applied +immediately or after landing. +WARNING Applying the gains in air is dangerous as there is no +guarantee that those new gains will be able to stabilize +the drone properly. Do not apply the new gains (logging only) Apply the new gains after disarm @@ -9853,10 +1527,6 @@ s 2 - - Start the autotuning sequence - WARNING: this will inject steps to the rate controller and can be dangerous. Only activate if you know what you are doing, and in a safe environment. Any motion of the remote stick will abort the signal injection and reset this parameter Best is to perform the identification in position or hold mode. Increase the amplitude of the injected signal using MC_AT_SYSID_AMP for more signal/noise ratio - Amplitude of the injected signal 0.1 @@ -9865,12 +1535,6 @@ - - Battery 1 current per volt (A/V) - The voltage seen by the ADC multiplied by this factor will determine the battery current. A value of -1 means to use the board default. - 8 - True - Battery 1 capacity Defines the capacity of battery 1 in mAh. @@ -9881,17 +1545,6 @@ 50 True - - Battery 1 Current ADC Channel - This parameter specifies the ADC channel used to monitor current of main power battery. A value of -1 means to use the board default. - True - - - Battery 1 idle current overwrite - This parameter allows to overwrite the current measured during idle (unarmed) state with a user-defined constant value (expressed in amperes). When the system is armed, the measured current is used. This is useful because on certain ESCs current measurements are inaccurate in case of no load. Negative values are ignored and will cause the measured current to be used. The default value of 0 disables the overwrite, in which case the measured value is always used. - 8 - True - Number of cells for battery 1 Defines the number of cells the attached battery consists of. @@ -9927,48 +1580,44 @@ Battery 1 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs - - Battery 1 Voltage ADC Channel - This parameter specifies the ADC channel used to monitor voltage of main power battery. A value of -1 means to use the board default. - True - Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 True - - Battery 1 voltage divider (V divider) - This is the divider from battery 1 voltage to ADC voltage. If using e.g. Mauch power modules the value from the datasheet can be applied straight here. A value of -1 means to use the board default. - 8 - True - Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 True - - Battery 2 current per volt (A/V) - The voltage seen by the ADC multiplied by this factor will determine the battery current. A value of -1 means to use the board default. - 8 - True - Battery 2 capacity Defines the capacity of battery 2 in mAh. @@ -9979,17 +1628,6 @@ 50 True - - Battery 2 Current ADC Channel - This parameter specifies the ADC channel used to monitor current of main power battery. A value of -1 means to use the board default. - True - - - Battery 2 idle current overwrite - This parameter allows to overwrite the current measured during idle (unarmed) state with a user-defined constant value (expressed in amperes). When the system is armed, the measured current is used. This is useful because on certain ESCs current measurements are inaccurate in case of no load. Negative values are ignored and will cause the measured current to be used. The default value of 0 disables the overwrite, in which case the measured value is always used. - 8 - True - Number of cells for battery 2 Defines the number of cells the attached battery consists of. @@ -10025,37 +1663,39 @@ Battery 2 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs - - Battery 2 Voltage ADC Channel - This parameter specifies the ADC channel used to monitor voltage of main power battery. A value of -1 means to use the board default. - True - Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 True - - Battery 2 voltage divider (V divider) - This is the divider from battery 2 voltage to ADC voltage. If using e.g. Mauch power modules the value from the datasheet can be applied straight here. A value of -1 means to use the board default. - 8 - True - Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 @@ -10106,18 +1746,28 @@ Battery 3 monitoring source - This parameter controls the source of battery data. The value 'Power Module' means that measurements are expected to come from a power module. If the value is set to 'External' then the system expects to receive mavlink battery status messages. If the value is set to 'ESCs', the battery information are taken from the esc_status message. This requires the ESC to provide both voltage as well as current. + This parameter controls the source of battery data. The value 'Power Module / Analog' +means that measurements are expected to come from either analog (ADC) inputs +or an I2C power monitor (e.g. INA226). Analog inputs are voltage and current +measurements read from the board's ADC channels, typically from an onboard +voltage divider and current shunt, or an external analog power module. +I2C power monitors are digital sensors on the I2C bus. +If the value is set to 'External' then the system expects to receive MAVLink +or CAN battery status messages, or the battery data is published by an external driver. +If the value is set to 'ESCs', the battery information are taken from the esc_status message. +This requires the ESC to provide both voltage as well as current (via ESC telemetry). True Disabled - Power Module + Power Module / Analog External ESCs Full cell voltage - Defines the voltage where a single cell of the battery is considered full. For a more accurate estimate set this below the nominal voltage of e.g. 4.2V + Defines the voltage where a single cell of the battery is considered full. +For a more accurate estimate set this below the nominal voltage of e.g. 4.2V V 2 0.01 @@ -10125,18 +1775,19 @@ Empty cell voltage - Defines the voltage where a single cell of the battery is considered empty. The voltage should be chosen above the steep dropoff at 3.5V. A typical lithium battery can only be discharged under high load down to 10% before it drops off to a voltage level damaging the cells. + Defines the voltage where a single cell of the battery is considered empty. +The voltage should be chosen above the steep dropoff at 3.5V. A typical +lithium battery can only be discharged under high load down to 10% before +it drops off to a voltage level damaging the cells. V 2 0.01 True - - This parameter is deprecated. Please use BAT1_I_CHANNEL - Expected battery current in flight - This value is used to initialize the in-flight average current estimation, which in turn is used for estimating remaining flight time and RTL triggering. + This value is used to initialize the in-flight average current estimation, +which in turn is used for estimating remaining flight time and RTL triggering. 0 500 A @@ -10145,7 +1796,9 @@ Critical threshold - Sets the threshold when the battery will be reported as critically low. This has to be lower than the low threshold. This threshold commonly will trigger RTL. + Sets the threshold when the battery will be reported as critically low. +This has to be lower than the low threshold. This threshold commonly +will trigger RTL. 0.05 0.5 norm @@ -10154,7 +1807,9 @@ Emergency threshold - Sets the threshold when the battery will be reported as dangerously low. This has to be lower than the critical threshold. This threshold commonly will trigger landing. + Sets the threshold when the battery will be reported as dangerously low. +This has to be lower than the critical threshold. This threshold commonly +will trigger landing. 0.03 0.5 norm @@ -10163,83 +1818,14 @@ Low threshold - Sets the threshold when the battery will be reported as low. This has to be higher than the critical threshold. + Sets the threshold when the battery will be reported as low. +This has to be higher than the critical threshold. 0.12 0.5 norm 2 0.01 - - Offset in volt as seen by the ADC input of the current sensor - This offset will be subtracted before calculating the battery current based on the voltage. - 8 - - - - - Enable USB autostart - true - - Disabled - Auto-detect - MAVLink - - - - Specify USB MAVLink mode - true - - normal - custom - onboard - osd - magic - config - iridium - minimal - extvision - extvisionmin - gimbal - onboard_low_bandwidth - uavionix - - - - - - Camera strobe delay - This parameter sets the delay between image integration start and strobe firing - 0.0 - 100.0 - ms - 1 - - - - - Camera capture edge - true - - Falling edge - Rising edge - - - - Camera capture feedback - Enables camera capture feedback - true - - - Camera capture timestamping mode - Change time measurement - true - - Get absolute timestamp - Get timestamp of mid exposure (active high) - Get timestamp of mid exposure (active low) - - @@ -10282,7 +1868,8 @@ Minimum camera trigger interval - This parameter sets the minimum time between two consecutive trigger events the specific camera setup is supporting. + This parameter sets the minimum time between two consecutive trigger events +the specific camera setup is supporting. 1.0 10000.0 ms @@ -10331,39 +1918,54 @@ Circuit breaker for disabling buzzer - Setting this parameter to 782097 will disable the buzzer audio notification. Setting this parameter to 782090 will disable the startup tune, while keeping all others enabled. + Setting this parameter to 782097 will disable the buzzer audio notification. +Setting this parameter to 782090 will disable the startup tune, while keeping +all others enabled. 0 782097 true Circuit breaker for flight termination - Setting this parameter to 121212 will disable the flight termination action if triggered by the FailureDetector logic or if FMU is lost. This circuit breaker does not affect the RC loss, data link loss, geofence, and takeoff failure detection safety logic. + Setting this parameter to 121212 will disable the flight termination action if triggered +by the FailureDetector logic or if FMU is lost. +This circuit breaker does not affect the RC loss, data link loss, geofence, +and takeoff failure detection safety logic. 0 121212 true Circuit breaker for IO safety - Setting this parameter to 22027 will disable IO safety. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 22027 will disable IO safety. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 22027 Circuit breaker for power supply check - Setting this parameter to 894281 will disable the power valid checks in the commander. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 894281 will disable the power valid +checks in the commander. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 894281 Circuit breaker for USB link check - Setting this parameter to 197848 will disable the USB connected checks in the commander, setting it to 0 keeps them enabled (recommended). We are generally recommending to not fly with the USB link connected and production vehicles should set this parameter to zero to prevent users from flying USB powered. However, for R&D purposes it has proven over the years to work just fine. + Setting this parameter to 197848 will disable the USB connected +checks in the commander, setting it to 0 keeps them enabled (recommended). +We are generally recommending to not fly with the USB link +connected and production vehicles should set this parameter to +zero to prevent users from flying USB powered. However, for R&D purposes +it has proven over the years to work just fine. 0 197848 Circuit breaker for arming in fixed-wing mode check - Setting this parameter to 159753 will enable arming in fixed-wing mode for VTOLs. WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK + Setting this parameter to 159753 will enable arming in fixed-wing +mode for VTOLs. +WARNING: ENABLING THIS CIRCUIT BREAKER IS AT OWN RISK 0 159753 @@ -10371,7 +1973,8 @@ Set the actuator failure failsafe mode - Note: actuator failure needs to be enabled and configured via FD_ACT_* parameters. + Note: actuator failure needs to be enabled and configured via FD_ACT_* +parameters. 0 3 @@ -10396,7 +1999,11 @@ Arm authorization method - Methods: - one arm: request authorization and arm when authorization is received - two step arm: 1st arm command request an authorization and 2nd arm command arm the drone if authorized Used if arm authorization is requested by COM_ARM_AUTH_REQ. + Methods: +- one arm: request authorization and arm when authorization is received +- two step arm: 1st arm command request an authorization and +2nd arm command arm the drone if authorized +Used if arm authorization is requested by COM_ARM_AUTH_REQ. one arm two step arm @@ -10408,14 +2015,16 @@ Arm authorization timeout - Timeout for authorizer answer. Used if arm authorization is requested by COM_ARM_AUTH_REQ. + Timeout for authorizer answer. +Used if arm authorization is requested by COM_ARM_AUTH_REQ. s 1 0.1 Minimum battery level for arming - Threshold for battery percentage below arming is prohibited. A negative value means BAT_CRIT_THR is the threshold. + Threshold for battery percentage below arming is prohibited. +A negative value means BAT_CRIT_THR is the threshold. -1 0.9 norm @@ -10424,11 +2033,13 @@ Enable checks on ESCs that report telemetry - If this parameter is set, the system will check ESC's online status and failures. This param is specific for ESCs reporting status. It shall be used only if ESCs support telemetry. + If this parameter is set, the system will check ESC's online status and failures. +This param is specific for ESCs reporting status. It shall be used only if ESCs support telemetry. - Enable FMU SD card hardfault detection check - This check detects if there are hardfault files present on the SD card. If so, and the parameter is enabled, arming is prevented. + Enable FMU SD card hardfault / watchdog detection check + This check detects if there are hardfault / watchdog files present on the +SD card. If so, and the parameter is enabled, arming is prevented. Maximum accelerometer inconsistency between IMU units that will allow arming @@ -10455,7 +2066,8 @@ Enable mag strength preflight check - Check if the estimator detects a strong magnetic disturbance (check enabled by EKF2_MAG_CHECK) + Check if the estimator detects a strong magnetic +disturbance (check enabled by EKF2_MAG_CHECK) Disabled Deny arming @@ -10468,7 +2080,9 @@ Enable Drone ID system detection and health check - This check detects if the Open Drone ID system is missing. Depending on the value of the parameter, the check can be disabled, warn only or deny arming. + This check detects if the Open Drone ID system is missing. +Depending on the value of the parameter, the check can be +disabled, warn only or deny arming. Disabled Warning only @@ -10477,7 +2091,9 @@ Enable FMU SD card detection check - This check detects if the FMU SD card is missing. Depending on the value of the parameter, the check can be disabled, warn only or deny arming. + This check detects if the FMU SD card is missing. +Depending on the value of the parameter, the check can be +disabled, warn only or deny arming. Disabled Warning only @@ -10486,20 +2102,36 @@ Arm switch is a momentary button - 0: Arming/disarming triggers on switch transition. 1: Arming/disarming triggers when holding the momentary button down for COM_RC_ARM_HYST like the stick gesture. + 0: Arming/disarming triggers on switch transition. +1: Arming/disarming triggers when holding the momentary button down like the stick gesture. + + + Enable Traffic Avoidance system detection check + This check detects if a traffic avoidance system (ADSB/FLARM transponder) +is missing. Depending on the value of the parameter, the check can be +disabled, warn only, or deny arming. + + Disabled + Warning only + Enforce for all modes + Enforce for mission modes only + - GPS preflight check - Measures taken when a check defined by EKF2_GPS_CHECK is failing. + Arming without GNSS configuration + Configures whether arming is allowed without GNSS, for modes that require a global position +(specifically, in those modes when a check defined by EKF2_GPS_CHECK fails). +The settings deny arming and warn, allow arming and warn, or silently allow arming. Deny arming - Warning only - Disabled + Allow arming (with warning) + Allow arming (no warning) Maximum allowed CPU load to still arm - The check fails if the CPU load is above this threshold for 2s. A negative value disables the check. + The check fails if the CPU load is above this threshold for 2s. +A negative value disables the check. -1 100 % @@ -10507,22 +2139,44 @@ Time-out for auto disarm after landing - A non-zero, positive value specifies the time-out period in seconds after which the vehicle will be automatically disarmed in case a landing situation has been detected during this period. A zero or negative value means that automatic disarming triggered by landing detection is disabled. + A non-zero, positive value specifies the time-out period in seconds after which the vehicle will be +automatically disarmed in case a landing situation has been detected during this period. +A zero or negative value means that automatic disarming triggered by landing detection is disabled. s 1 0.1 Allow disarming via switch/stick/button on multicopters in manual thrust modes - 0: Disallow disarming when not landed 1: Allow disarming in multicopter flight in modes where the thrust is directly controlled by the throttle stick e.g. Stabilized, Acro + 0: Disallow disarming when not landed +1: Allow disarming in multicopter flight in modes where +the thrust is directly controlled by thr throttle stick +e.g. Stabilized, Acro Time-out for auto disarm if not taking off - A non-zero, positive value specifies the time in seconds, within which the vehicle is expected to take off after arming. In case the vehicle didn't takeoff within the timeout it disarms again. A negative value disables autmoatic disarming triggered by a pre-takeoff timeout. + A non-zero, positive value specifies the time in seconds, within which the +vehicle is expected to take off after arming. In case the vehicle didn't takeoff +within the timeout it disarms again. +A negative value disables autmoatic disarming triggered by a pre-takeoff timeout. s 1 0.1 + + Datalink loss exceptions + Specify modes in which ground control station connection loss is ignored and no failsafe action is triggered. +See also COM_RCL_EXCEPT. + 0 + 31 + + Mission + Auto modes + Offboard + External Mode + Altitude Cruise + + GCS connection loss time threshold After this amount of seconds without datalink, the GCS connection lost mode triggers @@ -10534,7 +2188,11 @@ Delay between failsafe condition triggered and failsafe reaction - Before entering failsafe (RTL, Land, Hold), wait COM_FAIL_ACT_T seconds in Hold mode for the user to realize. During that time the user cannot take over control via the stick override feature (see COM_RC_OVERRIDE). Afterwards the configured failsafe action is triggered and the user may use stick override. A zero value disables the delay and the user cannot take over via stick movements (switching modes is still allowed). + Before entering failsafe (RTL, Land, Hold), wait COM_FAIL_ACT_T seconds in Hold mode +for the user to realize. +During that time the user can switch modes, but cannot take over control via the stick override feature (see COM_RC_OVERRIDE). +Afterwards the configured failsafe action is triggered and the user may use stick override. +A zero value disables the delay. 0.0 25.0 s @@ -10542,12 +2200,15 @@ Next flight UUID - This number is incremented automatically after every flight on disarming in order to remember the next flight UUID. The first flight is 0. + This number is incremented automatically after every flight on +disarming in order to remember the next flight UUID. +The first flight is 0. 0 Mode slot 1 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10564,6 +2225,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10576,7 +2238,8 @@ Mode slot 2 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10593,6 +2256,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10605,7 +2269,8 @@ Mode slot 3 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10622,6 +2287,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10634,7 +2300,8 @@ Mode slot 4 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10651,6 +2318,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10663,7 +2331,8 @@ Mode slot 5 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10680,6 +2349,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10692,7 +2362,8 @@ Mode slot 6 - If the main switch channel is in this range the selected flight mode will be applied. + If the main switch channel is in this range the +selected flight mode will be applied. Unassigned Manual @@ -10709,6 +2380,7 @@ Land Follow Me Precision Land + Altitude Cruise External Mode 1 External Mode 2 External Mode 3 @@ -10719,9 +2391,10 @@ External Mode 8 - + Remaining flight time low failsafe - Action the system takes when the remaining flight time is below the estimated time it takes to reach the RTL destination. + Action the system takes when the remaining flight time is below +the estimated time it takes to reach the RTL destination. 1 None @@ -10731,7 +2404,9 @@ User Flight Profile - Describes the intended use of the vehicle. Can be used by ground control software or log post processing. This param does not influence the behavior within the firmware. This means for example the control logic is independent of the setting of this param (but depends on other params). + Describes the intended use of the vehicle. +Can be used by ground control software or log post processing. +This param does not influence the behavior within the firmware. This means for example the control logic is independent of the setting of this param (but depends on other params). Default Pro User @@ -10741,7 +2416,13 @@ Maximum allowed flight time - The vehicle aborts the current operation and returns to launch when the time since takeoff is above this value. It is not possible to resume the mission or switch to any auto mode other than RTL or Land. Taking over in any manual mode is still possible. Starting from 90% of the maximum flight time, a warning message will be sent every 1 minute with the remaining time until automatic RTL. Set to -1 to disable. + The vehicle aborts the current operation and returns to launch when +the time since takeoff is above this value. It is not possible to resume the +mission or switch to any auto mode other than RTL or Land. Taking over in any manual +mode is still possible. +Starting from 90% of the maximum flight time, a warning message will be sent +every 1 minute with the remaining time until automatic RTL. +Set to -1 to disable. -1 s @@ -10758,23 +2439,30 @@ High Latency Datalink regain time threshold - After a data link loss: after this number of seconds with a healthy datalink the 'datalink loss' flag is set back to false + After a data link loss: after this number of seconds with a healthy datalink the 'datalink loss' +flag is set back to false 0 60 s Home position enabled - Set home position automatically if possible. + Set home position automatically if possible. +During missions, the latitude/longitude of the home position is locked and will not reset during intermediate landings. +It will only update once the mission is complete or landed outside of a mission. +However, the altitude is still being adjusted to correct for GNSS vertical drift in the first 2 minutes after takeoff. true Allows setting the home position after takeoff - If set to true, the autopilot is allowed to set its home position after takeoff The true home position is back-computed if a local position is estimate if available. If no local position is available, home is set to the current position. + If set to true, the autopilot is allowed to set its home position after takeoff +The true home position is back-computed if a local position is estimate if available. +If no local position is available, home is set to the current position. Imbalanced propeller failsafe mode - Action the system takes when an imbalanced propeller is detected by the failure detector. See also FD_IMB_PROP_THR to set the failure threshold. + Action the system takes when an imbalanced propeller is detected by the failure detector. +See also FD_IMB_PROP_THR to set the failure threshold. 1 Disabled @@ -10785,6 +2473,7 @@ Timeout value for disarming when kill switch is engaged + Use RC_MAP_KILL_SW to map a kill switch. 0.0 30.0 s @@ -10792,7 +2481,10 @@ Timeout for detecting a failure after takeoff - A non-zero, positive value specifies the timeframe in seconds within failure detector is allowed to disarm the vehicle if attitude exceeds the limits defined in FD_FAIL_P and FD_FAIL_R. The check is not executed for flight modes that do support acrobatic maneuvers, e.g: Acro (MC/FW) and Manual (FW). A zero or negative value means that the check is disabled. + A non-zero, positive value specifies the timeframe in seconds within failure detector is allowed to disarm the vehicle +if attitude exceeds the limits defined in FD_FAIL_P and FD_FAIL_R. +The check is not executed for flight modes that do support acrobatic maneuvers, e.g: Acro (MC/FW) and Manual (FW). +A zero or negative value means that the check is disabled. -1.0 5.0 s @@ -10800,7 +2492,8 @@ Battery failsafe mode - Action the system takes at critical battery. See also BAT_CRIT_THR and BAT_EMERGEN_THR for definition of battery states. + Action the system takes at critical battery. See also BAT_CRIT_THR and BAT_EMERGEN_THR +for definition of battery states. Warning Land mode @@ -10809,39 +2502,60 @@ External mode identifier 0 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 1 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 2 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 3 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 4 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 5 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 6 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. External mode identifier 7 - This parameter is automatically set to identify external modes. It ensures that modes get assigned to the same index independent from their startup order, which is required when mapping an external mode to an RC switch. + This parameter is automatically set to identify external modes. It ensures that modes +get assigned to the same index independent from their startup order, +which is required when mapping an external mode to an RC switch. + + + Allow external mode registration while armed + By default disabled for safety reasons Enable Actuator Testing - If set, enables the actuator test interface via MAVLink (ACTUATOR_TEST), that allows spinning the motors and moving the servos for testing purposes. + If set, enables the actuator test interface via MAVLink (ACTUATOR_TEST), that +allows spinning the motors and moving the servos for testing purposes. Time-out to wait when onboard computer connection is lost before warning about loss connection @@ -10852,7 +2566,8 @@ Set offboard loss failsafe mode - The offboard loss failsafe will only be entered after a timeout, set by COM_OF_LOSS_T in seconds. + The offboard loss failsafe will only be entered after a timeout, +set by COM_OF_LOSS_T in seconds. Position mode Altitude mode @@ -10873,19 +2588,16 @@ 0.01 - Expect and require a healthy MAVLink parachute system - - - Position mode navigation loss response - This sets the flight mode that will be used if navigation accuracy is no longer adequate for position control in manual Position mode. - - Altitude mode - Land mode (descend) - + Require MAVLink parachute system to be present and healthy - Horizontal position error threshold - This is the horizontal position error (EPH) threshold that will trigger a failsafe. The default is appropriate for a multicopter. Can be increased for a fixed-wing. If the previous position error was below this threshold, there is an additional factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). Set to -1 to disable. + Horizontal position error threshold for hovering systems + This is the horizontal position error (EPH) threshold that will trigger a failsafe. +If the previous position error was below this threshold, there is an additional +factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). +Only used for multicopters and VTOLs in hover mode. +Independent from estimator positioning data timeout threshold (see EKF2_NOAID_TOUT). +Set to -1 to disable. -1 400 m @@ -10893,7 +2605,10 @@ Low position accuracy action - Action the system takes when the estimated position has an accuracy below the specified threshold. See COM_POS_LOW_EPH to set the failsafe threshold. The failsafe action is only executed if the vehicle is in auto mission or auto loiter mode, otherwise it is only a warning. + Action the system takes when the estimated position has an accuracy below the specified threshold. +See COM_POS_LOW_EPH to set the failsafe threshold. +The failsafe action is only executed if the vehicle is in auto mission or auto loiter mode, +otherwise it is only a warning. 1 None @@ -10906,20 +2621,25 @@ Low position accuracy failsafe threshold - This triggers the action specified in COM_POS_LOW_ACT if the estimated position accuracy is below this threshold. Local position has to be still declared valid, which requires some kind of velocity aiding or large dead-reckoning time (EKF2_NOAID_TOUT), and a high failsafe threshold (COM_POS_FS_EPH). Set to -1 to disable. + This triggers the action specified in COM_POS_LOW_ACT if the estimated position accuracy is below this threshold. +Local position has to be still declared valid, which requires some kind of velocity aiding or large dead-reckoning time (EKF2_NOAID_TOUT), +and a high failsafe threshold (COM_POS_FS_EPH). +Set to -1 to disable. -1 1000 m Required number of redundant power modules - This configures a check to verify the expected number of 5V rail power supplies are present. By default only one is expected. Note: CBRK_SUPPLY_CHK disables all power checks including this one. + This configures a check to verify the expected number of 5V rail power supplies are present. By default only one is expected. +Note: CBRK_SUPPLY_CHK disables all power checks including this one. 0 4 Condition to enter prearmed mode - Condition to enter the prearmed state, an intermediate state between disarmed and armed in which non-throttling actuators are active. + Condition to enter the prearmed state, an intermediate state between disarmed and armed +in which non-throttling actuators are active. Disabled Safety button @@ -10927,7 +2647,7 @@ - Set command after a quadchute + Set action after a quadchute Warning only Return mode @@ -10937,46 +2657,61 @@ Maximum allowed RAM usage to pass checks - The check fails if the RAM usage is above this threshold. A negative value disables the check. + The check fails if the RAM usage is above this threshold. +A negative value disables the check. -1 100 % 1 - RC loss exceptions - Specify modes in which RC loss is ignored and the failsafe action not triggered. + Manual control loss exceptions + Specify modes in which stick input is ignored and no failsafe action is triggered. +External modes requiring stick input will still failsafe. +Auto modes are: Hold, Takeoff, Land, RTL, Descend, Follow Target, Precland, Orbit. 0 31 Mission - Hold + Auto modes Offboard + External Mode + Altitude Cruise - - RC input arm/disarm command duration - The default value of 1000 requires the stick to be held in the arm or disarm position for 1 second. - 100 - 1500 - ms - - RC control input mode - A value of 0 enables RC transmitter control (only). A valid RC transmitter calibration is required. A value of 1 allows joystick control only. RC input handling and the associated checks are disabled. A value of 2 allows either RC Transmitter or Joystick input. The first valid input is used, will fallback to other sources if the input stream becomes invalid. A value of 3 allows either input from RC or joystick. The first available source is selected and used until reboot. A value of 4 ignores any stick input. + Manual control input source configuration + Selects stick input selection behavior: +either a traditional remote control receiver (RC) or a MAVLink joystick (MANUAL_CONTROL message) +Priority sources are immediately switched to whenever they get valid. +0 RC only. Requires valid RC calibration. +1 MAVLink only. RC and related checks are disabled. +2 Switches only if current source becomes invalid. +3 Locks to the first valid source until reboot. +4 Ignores all sources. +5 RC priority, then MAVLink (lower instance before higher) +6 MAVLink priority (lower instance before higher), then RC +7 RC priority, then MAVLink (higher instance before lower) +8 MAVLink priority (higher instance before lower), then RC 0 - 4 + 8 - RC Transmitter only - Joystick only - RC and Joystick with fallback - RC or Joystick keep first - Stick input disabled + RC only + MAVLink only + RC or MAVLink with fallback + RC or MAVLink keep first + Disable manual control + Prio: RC > MAVL 1 > MAVL 2 + Prio: MAVL 1 > MAVL 2 > RC + Prio: RC > MAVL 2 > MAVL 1 + Prio: MAVL 2 > MAVL 1 > RC Manual control loss timeout - The time in seconds without a new setpoint from RC or Joystick, after which the connection is considered lost. This must be kept short as the vehicle will use the last supplied setpoint until the timeout triggers. + The time in seconds without a new setpoint from RC or Joystick, after which the connection is considered lost. +This must be kept short as the vehicle will use the last supplied setpoint until the timeout triggers. +Ensure the value is not set lower than the update interval of the RC or Joystick. 0 35 s @@ -10984,8 +2719,11 @@ 0.1 - Enable RC stick override of auto and/or offboard modes - When RC stick override is enabled, moving the RC sticks more than COM_RC_STICK_OV immediately gives control back to the pilot by switching to Position mode and if position is unavailable Altitude mode. Note: Only has an effect on multicopters, and VTOLs in multicopter mode. + Enable manual control stick override + When enabled, moving the sticks more than COM_RC_STICK_OV +immediately gives control back to the pilot by switching to Position mode and +if position is unavailable Altitude mode. +Note: Only has an effect on multicopters, and VTOLs in multicopter mode. 0 3 @@ -10994,8 +2732,9 @@ - RC stick override threshold - If COM_RC_OVERRIDE is enabled and the joystick input is moved more than this threshold the autopilot the pilot takes over control. + Stick override threshold + If COM_RC_OVERRIDE is enabled and the joystick input is moved more than this threshold +the autopilot the pilot takes over control. 5 80 % @@ -11004,7 +2743,11 @@ Enforced delay between arming and further navigation - The minimal time from arming the motors until moving the vehicle is possible is COM_SPOOLUP_TIME seconds. Goal: - Motors and propellers spool up to idle speed before getting commanded to spin faster - Timeout for ESCs and smart batteries to successfulyy do failure checks e.g. for stuck rotors before the vehicle is off the ground + The minimal time from arming the motors until moving the vehicle is possible is COM_SPOOLUP_TIME seconds. +Goal: +- Motors and propellers spool up to idle speed before getting commanded to spin faster +- Timeout for ESCs and smart batteries to successfulyy do failure checks +e.g. for stuck rotors before the vehicle is off the ground 0 30 s @@ -11025,7 +2768,10 @@ Minimum speed for the throw start - When the throw launch is enabled, the drone will only allow motors to spin after this speed is exceeded before detecting the freefall. This is a safety feature to ensure the drone does not turn on after accidental drop or a rapid movement before the throw. Set to 0 to disable. + When the throw launch is enabled, the drone will only allow motors to spin after this speed +is exceeded before detecting the freefall. This is a safety feature to ensure the drone does +not turn on after accidental drop or a rapid movement before the throw. +Set to 0 to disable. 0 m/s 1 @@ -11033,14 +2779,18 @@ Horizontal velocity error threshold - This is the horizontal velocity error (EVH) threshold that will trigger a failsafe. The default is appropriate for a multicopter. Can be increased for a fixed-wing. If the previous velocity error was below this threshold, there is an additional factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). + This is the horizontal velocity error (EVH) threshold that will trigger a failsafe. +The default is appropriate for a multicopter. Can be increased for a fixed-wing. +If the previous velocity error was below this threshold, there is an additional +factor of 2.5 applied (threshold for invalidation 2.5 times the one for validation). 0 m/s 1 High wind speed failsafe threshold - Wind speed threshold above which an automatic failsafe action is triggered. Failsafe action can be specified with COM_WIND_MAX_ACT. + Wind speed threshold above which an automatic failsafe action is triggered. +Failsafe action can be specified with COM_WIND_MAX_ACT. -1 m/s 1 @@ -11048,7 +2798,11 @@ High wind failsafe mode - Action the system takes when a wind speed above the specified threshold is detected. See COM_WIND_MAX to set the failsafe threshold. If enabled, it is not possible to resume the mission or switch to any auto mode other than RTL or Land if this threshold is exceeded. Taking over in any manual mode is still possible. + Action the system takes when a wind speed above the specified threshold is detected. +See COM_WIND_MAX to set the failsafe threshold. +If enabled, it is not possible to resume the mission or switch to any auto mode other than +RTL or Land if this threshold is exceeded. Taking over in any manual +mode is still possible. 1 None @@ -11061,7 +2815,9 @@ Wind speed warning threshold - A warning is triggered if the currently estimated wind speed is above this value. Warning is sent periodically (every 1 minute). Set to -1 to disable. + A warning is triggered if the currently estimated wind speed is above this value. +Warning is sent periodically (every 1 minute). +Set to -1 to disable. -1 m/s 1 @@ -11069,7 +2825,9 @@ Set GCS connection loss failsafe mode - The GCS connection loss failsafe will only be entered after a timeout, set by COM_DL_LOSS_T in seconds. Once the timeout occurs the selected action will be executed. + The GCS connection loss failsafe will only be entered after a timeout, +set by COM_DL_LOSS_T in seconds. Once the timeout occurs the selected +action will be executed. 0 6 @@ -11082,8 +2840,9 @@ - Set RC loss failsafe mode - The RC loss failsafe will only be entered after a timeout, set by COM_RC_LOSS_T in seconds. If RC input checks have been disabled by setting the COM_RC_IN_MODE param it will not be triggered. + Set manual control loss failsafe mode + The manual control loss failsafe will only be entered after a timeout, +set by COM_RC_LOSS_T in seconds. 1 6 @@ -11095,192 +2854,6 @@ - - - UAVCAN/CAN v1 bus bitrate - 20000 - 1000000 - bit/s - true - - - Cyphal - 0 - Cyphal disabled. 1 - Enables Cyphal - true - - - Cyphal Node ID - Read the specs at http://uavcan.org to learn more about Node ID. - -1 - 125 - true - - - actuator_outputs uORB over Cyphal publication port ID - -1 - 6143 - - - UDRAL battery parameters subscription port ID - -1 - 6143 - - - UDRAL battery status subscription port ID - -1 - 6143 - - - UDRAL battery energy source subscription port ID - -1 - 6143 - - - ESC 0 subscription port ID - -1 - 6143 - - - Cyphal ESC publication port ID - -1 - 6143 - - - Cyphal ESC 0 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 1 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 2 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 3 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 4 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 5 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 6 zubax feedback port ID - -1 - 6143 - - - Cyphal ESC 7 zubax feedback port ID - -1 - 6143 - - - GPS 0 subscription port ID - -1 - 6143 - - - GPS 1 subscription port ID - -1 - 6143 - - - Cyphal GPS publication port ID - -1 - 6143 - - - Cyphal legacy battery port ID - -1 - 6143 - - - Cyphal ESC readiness port ID - -1 - 6143 - - - Cyphal Servo publication port ID - -1 - 6143 - - - sensor_gps uORB over Cyphal subscription port ID - -1 - 6143 - - - sensor_gps uORB over Cyphal publication port ID - -1 - 6143 - - - - - DSHOT 3D deadband high - When the actuator_output is between DSHOT_3D_DEAD_L and DSHOT_3D_DEAD_H, motor will not spin. This value is with respect to the mixer_module range (0-1999), not the DSHOT values. - 1000 - 1999 - - - DSHOT 3D deadband low - When the actuator_output is between DSHOT_3D_DEAD_L and DSHOT_3D_DEAD_H, motor will not spin. This value is with respect to the mixer_module range (0-1999), not the DSHOT values. - 0 - 1000 - - - Allows for 3d mode when using DShot and suitable mixer - WARNING: ESC must be configured for 3D mode, and DSHOT_MIN set to 0. This splits the throttle ranges in two. Direction 1) 48 is the slowest, 1047 is the fastest. Direction 2) 1049 is the slowest, 2047 is the fastest. When mixer outputs 1000 or value inside DSHOT 3D deadband, DShot 0 is sent. - - - Enable bidirectional DShot - This parameter enables bidirectional DShot which provides RPM feedback. Note that this requires ESCs that support bidirectional DSHot, e.g. BlHeli32. This is not the same as DShot telemetry which requires an additional serial connection. - True - - - Minimum DShot Motor Output - Minimum Output Value for DShot in percent. The value depends on the ESC. Make sure to set this high enough so that the motors are always spinning while armed. - 0 - 1 - % - 2 - 0.01 - - - Serial Configuration for DShot Driver - Configure on which serial port to run DShot Driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Number of magnetic poles of the motors - Specify the number of magnetic poles of the motors. It is required to compute the RPM value from the eRPM returned with the ESC telemetry. Either get the number from the motor spec sheet or count the magnets on the bell of the motor (not the stator magnets). Typical motors for 5 inch props have 14 poles. - - 1-sigma IMU accelerometer switch-on bias @@ -11314,30 +2887,168 @@ m/s^2 2 - - Accel bias learning inhibit time constant - The vector magnitude of angular rate and acceleration used to check if learning should be inhibited has a peak hold filter applied to it with an exponential decay. This parameter controls the time constant of the decay. - 0.1 - 1.0 - s - 2 + + Accel bias learning inhibit time constant + The vector magnitude of angular rate and acceleration used to check if learning should be inhibited has a peak hold filter applied to it with an exponential decay. This parameter controls the time constant of the decay. + 0.1 + 1.0 + s + 2 + + + Process noise for IMU accelerometer bias prediction + 0.0 + 0.01 + m/s^3 + 6 + + + Accelerometer noise for covariance prediction + 0.01 + 1.0 + m/s^2 + 2 + + + Auxiliary global position sensor 0 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 0 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 0 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 + + + Auxiliary global position sensor 0 ID + Sensor ID for slot 0. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 0 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 0 + Used to lower bound or replace the uncertainty included in the message + 0.01 + m + 2 + + + Auxiliary global position sensor 1 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 1 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 1 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 + + + Auxiliary global position sensor 1 ID + Sensor ID for slot 1. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 1 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 1 + Used to lower bound or replace the uncertainty included in the message + 0.01 + m + 2 + + + Auxiliary global position sensor 2 aiding + Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion + 0 + 3 + + Horizontal position + Vertical position + + + + Auxiliary global position sensor 2 delay (to IMU) + 0 + 1000 + ms + 1 + True + + + Gate size for auxiliary global position sensor 2 fusion + Sets the number of standard deviations used by the innovation consistency test. + 1.0 + SD + 1 - - Process noise for IMU accelerometer bias prediction - 0.0 - 0.01 - m/s^3 - 6 + + Auxiliary global position sensor 2 ID + Sensor ID for slot 2. Set to 0 to disable this slot. + 0 + 255 - - Accelerometer noise for covariance prediction + + Fusion reset mode for sensor 2 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 2 + Used to lower bound or replace the uncertainty included in the message 0.01 - 1.0 - m/s^2 + m 2 - - Aux global position (AGP) sensor aiding + + Auxiliary global position sensor 3 aiding Set bits in the following positions to enable: 0 : Horizontal position fusion 1 : Vertical position fusion 0 3 @@ -11346,23 +3057,37 @@ Vertical position - - Aux global position estimator delay relative to IMU measurements + + Auxiliary global position sensor 3 delay (to IMU) 0 - 300 + 1000 ms 1 True - - Gate size for aux global position fusion + + Gate size for auxiliary global position sensor 3 fusion Sets the number of standard deviations used by the innovation consistency test. 1.0 SD 1 - - Measurement noise for aux global position measurements + + Auxiliary global position sensor 3 ID + Sensor ID for slot 3. Set to 0 to disable this slot. + 0 + 255 + + + Fusion reset mode for sensor 3 + Automatic: reset on fusion timeout if no other source of position is available Dead-reckoning: reset on fusion timeout if no source of velocity is available + + Automatic + Dead-reckoning + + + + Measurement noise for auxiliary global position sensor 3 Used to lower bound or replace the uncertainty included in the message 0.01 m @@ -11504,6 +3229,10 @@ EKF2 enable + + Enable constant position fusion during engine warmup + When enabled, constant position fusion is enabled when the vehicle is landed and armed. This is intended for IC engine warmup (e.g., fuel engines on catapult) to allow mode transitions to auto/takeoff despite vibrations from running engines. + Measurement noise for vision angle measurements Used to lower bound or replace the uncertainty included in the message @@ -11594,7 +3323,7 @@ Enable synthetic sideslip fusion - For reliable wind estimation both sideslip and airspeed fusion (see EKF2_ARSP_THR) should be enabled. Only applies to fixed-wing vehicles (or VTOLs in fixed-wing mode). Note: side slip fusion is currently not supported for tailsitters. + For reliable wind estimation both sideslip and airspeed fusion (see EKF2_ARSP_THR) should be enabled. Only applies to vehicles in fixed-wing mode or with airspeed fusion active. Note: side slip fusion is currently not supported for tailsitters. 1-sigma IMU gyro switch-on bias @@ -11620,11 +3349,11 @@ m 1 - + Integer bitmask controlling GPS checks Each threshold value is defined by the parameter indicated next to the check. Drift and offset checks only run when the vehicle is on ground and stationary. 0 - 1023 + 4095 Sat count (EKF2_REQ_NSATS) PDOP (EKF2_REQ_PDOP) @@ -11636,6 +3365,8 @@ Horizontal speed offset (EKF2_REQ_HDRIFT) Vertical speed offset (EKF2_REQ_VDRIFT) Spoofing + GPS fix type (EKF2_REQ_FIX) + Jamming @@ -11651,28 +3382,37 @@ - GPS measurement delay relative to IMU measurements + GPS measurement delay relative to IMU measurement + GPS measurement delay relative to IMU measurement if PPS time correction is not available/enabled (PPS_CAP_ENABLE). 0 300 ms 1 True + + Fusion reset mode + Automatic: reset on fusion timeout if no other source of position is available. Dead-reckoning: reset on fusion timeout if no source of velocity is available. + + Automatic + Dead-reckoning + + X position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Forward (roll) axis with origin relative to vehicle centre of gravity m 3 Y position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Right (pitch) axis with origin relative to vehicle centre of gravity m 3 Z position of GPS antenna in body frame - Forward axis with origin relative to vehicle centre of gravity + Down (yaw) axis with origin relative to vehicle centre of gravity m 3 @@ -11899,7 +3639,7 @@ 1/s 2 - + Expected range finder reading when on ground If the vehicle is on ground, is not moving as determined by the motion test and the range finder is returning invalid or no data, then an assumed range value of EKF2_MIN_RNG will be used by the terrain estimator so that a terrain height estimate is available at the start of flight in situations where the range finder may be inside its minimum measurements distance when on ground. 0.01 @@ -12062,6 +3802,19 @@ m 1 + + Required GPS fix + Minimum GPS fix type required for GPS usage. + + No fix required + 2D fix + 3D fix + RTCM code differential + RTK float + RTK fixed + Extrapolated + + Required GPS health time on startup Minimum continuous period without GPS failure required to mark a healthy GPS status. It can be reduced to speed up initialization, but it's recommended to keep this unchanged for a vehicle. @@ -12109,13 +3862,6 @@ 10.0 m - - Gate size used for innovation consistency checks for range aid fusion - A lower value means HAGL needs to be more stable in order to use range finder for height estimation in range aid mode - 0.1 - 5.0 - SD - Maximum horizontal velocity allowed for conditional range aid mode If the vehicle horizontal speed exceeds this value then the estimator will not fuse range measurements to estimate its height. This only applies when conditional range aid mode is activated (EKF2_RNG_CTRL = 1). @@ -12140,7 +3886,7 @@ 1 True - + Maximum distance at which the range finder could detect fog (m) Limit for fog detection. If the range finder measures a distance greater than this value, the measurement is considered to not be blocked by fog or rain. If there's a jump from larger than RNG_FOG to smaller than EKF2_RNG_FOG, the measurement may gets rejected. 0 - disabled 0.0 @@ -12194,7 +3940,7 @@ 3 - Minimum range validity period + Minumum range validity period Minimum duration during which the reported range finder signal quality needs to be non-zero in order to be declared valid (s) 0.1 5 @@ -12283,32 +4029,23 @@ 3 - - - Required esc bootloader version - 0 - 65535 - - - Required esc firmware version - 0 - 65535 - - - Required esc hardware version - 0 - 65535 - - RC Loss Alarm - Enable/disable event task for RC Loss. When enabled, an alarm tune will be played via buzzer or ESCs, if supported. The alarm will sound after a disarm, if the vehicle was previously armed and only if the vehicle had RC signal at some point. Particularly useful for locating crashed drones without a GPS sensor. + Enable/disable event task for RC Loss. When enabled, an alarm tune will be +played via buzzer or ESCs, if supported. The alarm will sound after a disarm, +if the vehicle was previously armed and only if the vehicle had RC signal at +some point. Particularly useful for locating crashed drones without a GPS +sensor. true Status Display - Enable/disable event task for displaying the vehicle status using arm-mounted LEDs. When enabled and if the vehicle supports it, LEDs will flash indicating various vehicle status changes. Currently PX4 has not implemented any specific status events. - + Enable/disable event task for displaying the vehicle status using arm-mounted +LEDs. When enabled and if the vehicle supports it, LEDs will flash +indicating various vehicle status changes. Currently PX4 has not implemented +any specific status events. +- true @@ -12333,7 +4070,8 @@ Maximum manually added yaw rate - This is the maximally added yaw rate setpoint from the yaw stick in any attitude controlled flight mode. It is added to the yaw rate setpoint generated by the controller for turn coordination. + This is the maximally added yaw rate setpoint from the yaw stick in any attitude controlled flight mode. +It is added to the yaw rate setpoint generated by the controller for turn coordination. 0 deg/s 1 @@ -12341,7 +4079,9 @@ Pitch setpoint offset (pitch at level flight) - An airframe specific offset of the pitch setpoint in degrees, the value is added to the pitch setpoint and should correspond to the pitch at typical cruise speed of the airframe. + An airframe specific offset of the pitch setpoint in degrees, the value is +added to the pitch setpoint and should correspond to the pitch at +typical cruise speed of the airframe. -90.0 90.0 deg @@ -12366,7 +4106,8 @@ Attitude pitch time constant - This defines the latency between a pitch step input and the achieved setpoint (inverse to a P gain). Smaller systems may require smaller values. + This defines the latency between a pitch step input and the achieved setpoint +(inverse to a P gain). Smaller systems may require smaller values. 0.2 1.0 s @@ -12383,21 +4124,14 @@ Attitude Roll Time Constant - This defines the latency between a roll step input and the achieved setpoint (inverse to a P gain). Smaller systems may require smaller values. + This defines the latency between a roll step input and the achieved setpoint +(inverse to a P gain). Smaller systems may require smaller values. 0.2 1.0 s 2 0.05 - - Spoiler landing setting - 0.0 - 1.0 - norm - 2 - 0.01 - Wheel steering rate feed forward 0.0 @@ -12408,7 +4142,8 @@ Wheel steering rate integrator gain - This gain defines how much control response will result out of a steady state error. It trims any constant error. + This gain defines how much control response will result out of a steady +state error. It trims any constant error. 0.0 10 %/rad @@ -12424,7 +4159,8 @@ Wheel steering rate proportional gain - This defines how much the wheel steering input will be commanded depending on the current body angular rate error. + This defines how much the wheel steering input will be commanded depending on the +current body angular rate error. 0.0 10 %/rad/s @@ -12433,11 +4169,13 @@ Enable wheel steering controller - Only enabled during automatic runway takeoff and landing. In all manual modes the wheel is directly controlled with yaw stick. + Only enabled during automatic runway takeoff and landing. +In all manual modes the wheel is directly controlled with yaw stick. Maximum wheel steering rate - This limits the maximum wheel steering rate the controller will output (in degrees per second). + This limits the maximum wheel steering rate the controller will output (in degrees per +second). 0.0 90.0 deg/s @@ -12454,9 +4192,25 @@ + + Flaps setting during landing + Sets a fraction of full flaps during landing. +Also applies to flaperons if enabled in the mixer/allocation. + 0.0 + 1.0 + norm + 2 + 0.01 + Bit mask to set the automatic landing abort conditions - Terrain estimation: bit 0: Abort if terrain is not found bit 1: Abort if terrain times out (after a first successful measurement) The last estimate is always used as ground, whether the last valid measurement or the land waypoint, depending on the selected abort criteria, until an abort condition is entered. If FW_LND_USETER == 0, these bits are ignored. TODO: Extend automatic abort conditions e.g. glide slope tracking error (horizontal and vertical) + Terrain estimation: +bit 0: Abort if terrain is not found +bit 1: Abort if terrain times out (after a first successful measurement) +The last estimate is always used as ground, whether the last valid measurement or the land waypoint, depending on the +selected abort criteria, until an abort condition is entered. If FW_LND_USETER == 0, these bits are ignored. +TODO: Extend automatic abort conditions +e.g. glide slope tracking error (horizontal and vertical) 0 3 @@ -12466,7 +4220,8 @@ Landing airspeed - The calibrated airspeed setpoint during landing. If set <= 0, landing airspeed = FW_AIRSPD_MIN by default. + The calibrated airspeed setpoint during landing. +If set <= 0, landing airspeed = FW_AIRSPD_MIN by default. -1.0 m/s 1 @@ -12474,16 +4229,19 @@ Maximum landing slope angle - Typically the desired landing slope angle when landing configuration (flaps, airspeed) is enabled. Set this value within the vehicle's performance limits. + Typically the desired landing slope angle when landing configuration (flaps, airspeed) is enabled. +Set this value within the vehicle's performance limits. 1.0 - 15.0 + 45.0 deg 1 0.5 Early landing configuration deployment - Allows to deploy the landing configuration (flaps, landing airspeed, etc.) already in the loiter-down waypoint before the final approach. Otherwise is enabled only in the final approach. + Allows to deploy the landing configuration (flaps, landing airspeed, etc.) already in +the loiter-down waypoint before the final approach. +Otherwise is enabled only in the final approach. Landing flare altitude (relative to landing altitude) @@ -12522,7 +4280,9 @@ Landing flare time - Multiplied by the descent rate to calculate a dynamic altitude at which to trigger the flare. NOTE: max(FW_LND_FLALT, FW_LND_FL_TIME * descent rate) is taken as the flare altitude + Multiplied by the descent rate to calculate a dynamic altitude at which +to trigger the flare. +NOTE: max(FW_LND_FLALT, FW_LND_FL_TIME * descent rate) is taken as the flare altitude 0.1 5.0 s @@ -12531,7 +4291,11 @@ Landing touchdown nudging option - Approach angle nudging: shifts the touchdown point laterally while keeping the approach entrance point constant Approach path nudging: shifts the touchdown point laterally along with the entire approach path This is useful for manually adjusting the landing point in real time when map or GNSS errors cause an offset from the desired landing vector. Nudging is done with yaw stick, constrained to FW_LND_TD_OFF (in meters) and the direction is relative to the vehicle heading (stick deflection to the right = land point moves to the right as seen by the vehicle). + Approach angle nudging: shifts the touchdown point laterally while keeping the approach entrance point constant +Approach path nudging: shifts the touchdown point laterally along with the entire approach path +This is useful for manually adjusting the landing point in real time when map or GNSS errors cause an offset from the +desired landing vector. Nudging is done with yaw stick, constrained to FW_LND_TD_OFF (in meters) and the direction is +relative to the vehicle heading (stick deflection to the right = land point moves to the right as seen by the vehicle). 0 2 @@ -12540,48 +4304,247 @@ Nudge approach path - - Maximum lateral position offset for the touchdown point - 0.0 - 10.0 - m - 1 - 1 + + Maximum lateral position offset for the touchdown point + 0.0 + 10.0 + m + 1 + 1 + + + Landing touchdown time (since flare start) + This is the time after the start of flaring that we expect the vehicle to touch the runway. +At this time, a 0.5s clamp down ramp will engage, constraining the pitch setpoint to RWTO_PSP. +If enabled, ensure that RWTO_PSP is configured appropriately for full gear contact on ground roll. +Set to -1.0 to disable touchdown clamping. E.g. it may not be desirable to clamp on belly landings. +The touchdown time will be constrained to be greater than or equal to the flare time (FW_LND_FL_TIME). + -1.0 + 5.0 + s + 1 + 0.1 + + + Altitude time constant factor for landing and low-height flight + The TECS altitude time constant (FW_T_ALT_TC) is multiplied by this value. + 0.2 + 1.0 + + 1 + 0.1 + + + Use terrain estimation during landing + This is critical for detecting when to flare, and should be enabled if possible. +If enabled and no measurement is found within a given timeout, the landing waypoint altitude will be used OR the landing +will be aborted, depending on the criteria set in FW_LND_ABORT. +If disabled, FW_LND_ABORT terrain based criteria are ignored. + 0 + 2 + + Disable the terrain estimate + Use the terrain estimate to trigger the flare (only) + Calculate landing glide slope relative to the terrain estimate + + + + Spoiler landing setting + 0.0 + 1.0 + norm + 2 + 0.01 + + + + + Flaps setting during take-off + Sets a fraction of full flaps during take-off. +Also applies to flaperons if enabled in the mixer/allocation. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Trigger time + Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + 0.0 + 5.0 + s + 2 + 0.05 + + + Trigger acceleration threshold + Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + 0 + m/s^2 + 1 + 0.5 + + + Control surface launch delay + Locks control surfaces during pre-launch (armed) and until this time since launch has passed. +Only affects control surfaces that have corresponding flag set, and not active for runway takeoff. +Set to 0 to disable any surface locking after arming. + 0.0 + s + 1 + 0.1 + + + Fixed-wing launch detection + Enables automatic launch detection based on measured acceleration. Use for hand- or catapult-launched vehicles. +Not compatible with runway takeoff. + + + Motor delay + Start the motor(s) this amount of seconds after launch is detected. + 0.0 + 10.0 + s + 1 + 0.5 + + + Takeoff Airspeed + The calibrated airspeed setpoint during the takeoff climbout. +If set <= 0, FW_AIRSPD_MIN will be set by default. + -1.0 + m/s + 1 + 0.1 + + + Minimum pitch during takeoff + -5.0 + 80.0 + deg + 1 + 0.5 + + + + + GPS failure loiter time + The time the system should do open loop loiter and wait for GPS recovery +before it starts descending. Set to 0 to disable. Roll angle is set to FW_GPSF_R. +Does only apply for fixed-wing vehicles or VTOLs with NAV_FORCE_VT set to 0. + 0 + s + + + GPS failure fixed roll angle + Roll angle in GPS failure loiter mode. + 0.0 + 60.0 + deg + 1 + 0.5 + + + Custom stick configuration + Applies in manual Position and Altitude flight modes. + 0 + 3 + + Alternative stick configuration (height rate on throttle stick, airspeed on pitch stick) + Enable airspeed setpoint via sticks in altitude and position flight mode + + + + Maximum pitch angle setpoint + Applies in any altitude controlled flight mode. + 0.0 + 80.0 + deg + 1 + 0.5 + + + Minimum pitch angle setpoint + Applies in any altitude controlled flight mode. + -60.0 + 0.0 + deg + 1 + 0.5 + + + Maximum roll angle setpoint + Applies in any altitude controlled flight mode. + 35.0 + 75.0 + deg + 1 + 0.5 + + + Idle throttle + This is the minimum throttle while on the ground ("landed") in auto modes. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Throttle limit max + Applies in any altitude controlled flight mode. +Should be set accordingly to achieve FW_T_CLMB_MAX. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Throttle limit min + Applies in any altitude controlled flight mode. +Usually set to 0 but can be increased to prevent the motor from stopping when +descending, which can increase achievable descent rates. + 0.0 + 1.0 + norm + 2 + 0.01 + + + Default target climbrate + In auto modes: default climb rate output by controller to achieve altitude setpoints. +In manual modes: maximum climb rate setpoint. + 0.1 + m/s + 2 + 0.01 - - Landing touchdown time (since flare start) - This is the time after the start of flaring that we expect the vehicle to touch the runway. At this time, a 0.5s clamp down ramp will engage, constraining the pitch setpoint to RWTO_PSP. If enabled, ensure that RWTO_PSP is configured appropriately for full gear contact on ground roll. Set to -1.0 to disable touchdown clamping. E.g. it may not be desirable to clamp on belly landings. The touchdown time will be constrained to be greater than or equal to the flare time (FW_LND_FL_TIME). - -1.0 - 5.0 - s - 1 - 0.1 + + Default target sinkrate + In auto modes: default sink rate output by controller to achieve altitude setpoints. +In manual modes: maximum sink rate setpoint. + 0.1 + m/s + 2 + 0.01 - - Altitude time constant factor for landing and low-height flight - The TECS altitude time constant (FW_T_ALT_TC) is multiplied by this value. - 0.2 - 1.0 - + + Speed <--> Altitude weight + Adjusts the amount of weighting that the pitch control +applies to speed vs height errors. +0 -> control height only +2 -> control speed only (gliders) + 0.0 + 2.0 1 - 0.1 - - - Use terrain estimation during landing - This is critical for detecting when to flare, and should be enabled if possible. If enabled and no measurement is found within a given timeout, the landing waypoint altitude will be used OR the landing will be aborted, depending on the criteria set in FW_LND_ABORT. If disabled, FW_LND_ABORT terrain based criteria are ignored. - 0 - 2 - - Disable the terrain estimate - Use the terrain estimate to trigger the flare (only) - Calculate landing glide slope relative to the terrain estimate - + 1.0 - - Height (AGL) of the wings when the aircraft is on the ground - This is used to constrain a minimum altitude below which we keep wings level to avoid wing tip strike. It's safer to give a slight margin here (> 0m) + This is used to constrain a minimum altitude below which we keep wings level to avoid wing tip strike. It's safer +to give a slight margin here (> 0m) 0.0 m 1 @@ -12596,37 +4559,188 @@ 0.1 - - - Trigger time - Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. + + + Path navigation roll slew rate limit + Maximum change in roll angle setpoint per second. +Applied in all Auto modes, plus manual Position & Altitude modes. + 0 + deg/s + 0 + 1 + + + + + Minimum groundspeed + The controller will increase the commanded airspeed to maintain +this minimum groundspeed to the next waypoint. 0.0 - 5.0 - s + 40 + m/s + 1 + 0.5 + + + Throttle max slew rate + Maximum slew rate for the commanded throttle + 0.0 + 1.0 + 2 + 0.01 + + + Altitude error time constant + 2.0 + 2 + 0.5 + + + Fast descend: minimum altitude error + Minimum altitude error needed to descend with max airspeed and minimal throttle. +A negative value disables fast descend. + -1.0 + 0 + + + Height rate feed forward + 0.0 + 1.0 2 0.05 - - Trigger acceleration threshold - Launch is detected when acceleration in body forward direction is above FW_LAUN_AC_THLD for FW_LAUN_AC_T seconds. - 0 - m/s^2 + + Integrator gain pitch + Increase it to trim out speed and height offsets faster, +with the downside of possible overshoots and oscillations. + 0.0 + 2.0 + 2 + 0.05 + + + Pitch damping gain + 0.0 + 2.0 + 2 + 0.1 + + + Roll -> Throttle feedforward + Is used to compensate for the additional drag created by turning. +Increase this gain if the aircraft initially loses energy in turns +and reduce if the aircraft initially gains energy in turns. + 0.0 + 20.0 1 0.5 - - Fixed-wing launch detection - Enables automatic launch detection based on measured acceleration. Use for hand- or catapult-launched vehicles. Not compatible with runway takeoff. + + Specific total energy balance rate feedforward gain + 0.5 + 3 + 2 + 0.01 - - Motor delay - Start the motor(s) this amount of seconds after launch is detected. + + Maximum descent rate + 1.0 + 15.0 + m/s + 1 + 0.5 + + + Airspeed rate measurement standard deviation + For the airspeed filter in TECS. + 0.01 + 10.0 + m/s^2 + 2 + 0.1 + + + Process noise standard deviation for the airspeed rate + This is defining the noise in the airspeed rate for the constant airspeed rate model +of the TECS airspeed filter. + 0.01 + 10.0 + m/s^2 + 2 + 0.1 + + + Airspeed measurement standard deviation + For the airspeed filter in TECS. + 0.01 + 10.0 + m/s + 2 + 0.1 + + + Specific total energy rate first order filter time constant + This filter is applied to the specific total energy rate used for throttle damping. + 0.0 + 2 + 2 + 0.01 + + + True airspeed error time constant + 2.0 + 2 + 0.5 + + + Throttle damping factor + This is the damping gain for the throttle demand loop. + 0.0 + 1.0 + 3 + 0.01 + + + Integrator gain throttle + Increase it to trim out speed and height offsets faster, +with the downside of possible overshoots and oscillations. 0.0 + 1.0 + 3 + 0.005 + + + Low-height threshold for tighter altitude tracking + Height above ground threshold below which tighter altitude +tracking gets enabled (see FW_LND_THRTC_SC). Below this height, TECS smoothly +(1 sec / sec) transitions the altitude tracking time constant from FW_T_ALT_TC +to FW_LND_THRTC_SC*FW_T_ALT_TC. +-1 to disable. + -1 + m + 0 + 1 + + + Maximum vertical acceleration + This is the maximum vertical acceleration +either up or down that the controller will use to correct speed +or height errors. + 1.0 10.0 - s + m/s^2 1 0.5 + + Wind-based airspeed scaling factor + Multiplying this factor with the current absolute wind estimate gives the airspeed offset +added to the minimum airspeed setpoint limit. This helps to make the +system more robust against disturbances (turbulence) in high wind. + 0 + 2 + 0.01 + @@ -12637,21 +4751,10 @@ 2 0.01 - - Enable minimum forward ground speed maintaining excess wind handling logic - - - Maximum, minimum forward ground speed for track keeping in excess wind - The maximum value of the minimum forward ground speed that may be commanded by the track keeping excess wind handling logic. Commanded in full at the normalized track error fraction of the track error boundary and reduced to zero on track. - 0.0 - 10.0 - m/s - 1 - 0.5 - Enable automatic lower bound on the NPFG period - Avoids limit cycling from a too aggressively tuned period/damping combination. If false, also disables upper bound NPFG_PERIOD_UB. + Avoids limit cycling from a too aggressively tuned period/damping combination. +If false, also disables upper bound NPFG_PERIOD_UB. NPFG period @@ -12664,7 +4767,8 @@ Period safety factor - Multiplied by period for conservative minimum period bounding (when period lower bounding is enabled). 1.0 bounds at marginal stability. + Multiplied by period for conservative minimum period bounding (when period lower +bounding is enabled). 1.0 bounds at marginal stability. 1.0 10.0 1 @@ -12672,7 +4776,8 @@ Roll time constant - Time constant of roll controller command / response, modeled as first order delay. Used to determine lower period bound. Setting zero disables automatic period bounding. + Time constant of roll controller command / response, modeled as first order delay. +Used to determine lower period bound. Setting zero disables automatic period bounding. 0.00 2.00 s @@ -12681,62 +4786,27 @@ NPFG switch distance multiplier - Multiplied by the track error boundary to determine when the aircraft switches to the next waypoint and/or path segment. Should be less than 1. + Multiplied by the track error boundary to determine when the aircraft switches +to the next waypoint and/or path segment. Should be less than 1. 0.1 1.0 - 2 - 0.01 - - - Enable track keeping excess wind handling logic - - - Enable automatic upper bound on the NPFG period - Adapts period to maintain track keeping in variable winds and path curvature. - - - Enable wind excess regulation - Disabling this parameter further disables all other airspeed incrementation options. - - - - - Path navigation roll slew rate limit - Maximum change in roll angle setpoint per second. Applied in all Auto modes, plus manual Position & Altitude modes. - 0 - deg/s - 0 - 1 - - - Custom stick configuration - Applies in manual Position and Altitude flight modes. - 0 - 3 - - Alternative stick configuration (height rate on throttle stick, airspeed on pitch stick) - Enable airspeed setpoint via sticks in altitude and position flight mode - - - - Maximum roll angle setpoint - Applies in any altitude controlled flight mode. - 35.0 - 65.0 - deg - 1 - 0.5 - - - Minimum pitch during takeoff - -5.0 - 30.0 - deg - 1 - 0.5 + 2 + 0.01 + + + Enable automatic upper bound on the NPFG period + Adapts period to maintain track keeping in variable winds and path curvature. + + Airspeed scale with full flaps + Factor applied to the minimum and stall airspeed when flaps are fully deployed. + 0.5 + 1 + 2 + 0.01 + Maximum Airspeed (CAS) The maximal airspeed (calibrated airspeed) the user is able to command. @@ -12747,7 +4817,13 @@ Minimum Airspeed (CAS) - The minimal airspeed (calibrated airspeed) the user is able to command. Further, if the airspeed falls below this value, the TECS controller will try to increase airspeed more aggressively. Has to be set according to the vehicle's stall speed (which should be set in FW_AIRSPD_STALL), with some margin between the stall speed and minimum airspeed. This value corresponds to the desired minimum speed with the default load factor (level flight, default weight), and is automatically adpated to the current load factor (calculated from roll setpoint and WEIGHT_GROSS/WEIGHT_BASE). + The minimal airspeed (calibrated airspeed) the user is able to command. +Further, if the airspeed falls below this value, the TECS controller will try to +increase airspeed more aggressively. +Has to be set according to the vehicle's stall speed (which should be set in FW_AIRSPD_STALL), +with some margin between the stall speed and minimum airspeed. +This value corresponds to the desired minimum speed with the default load factor (level flight, default weight), +and is automatically adpated to the current load factor (calculated from roll setpoint and WEIGHT_GROSS/WEIGHT_BASE). 0.5 m/s 1 @@ -12755,7 +4831,9 @@ Stall Airspeed (CAS) - The stall airspeed (calibrated airspeed) of the vehicle. It is used for airspeed sensor failure detection and for the control surface scaling airspeed limits. + The stall airspeed (calibrated airspeed) of the vehicle. +It is used for airspeed sensor failure detection and for the control +surface scaling airspeed limits. 0.5 m/s 1 @@ -12763,7 +4841,9 @@ Trim (Cruise) Airspeed - The trim CAS (calibrated airspeed) of the vehicle. If an airspeed controller is active, this is the default airspeed setpoint that the controller will try to achieve. This value corresponds to the trim airspeed with the default load factor (level flight, default weight). + The trim CAS (calibrated airspeed) of the vehicle. If an airspeed controller is active, +this is the default airspeed setpoint that the controller will try to achieve. +This value corresponds to the trim airspeed with the default load factor (level flight, default weight). 0.5 m/s 1 @@ -12771,7 +4851,9 @@ Service ceiling - Altitude in standard atmosphere at which the vehicle in normal configuration (WEIGHT_BASE) is still able to achieve a maximum climb rate of 0.5m/s at maximum throttle (FW_THR_MAX). Used to compensate for air density in FW_T_CLMB_MAX. Set negative to disable. + Altitude in standard atmosphere at which the vehicle in normal configuration (WEIGHT_BASE) is still able to achieve a maximum climb rate of +0.5m/s at maximum throttle (FW_THR_MAX). Used to compensate for air density in FW_T_CLMB_MAX. +Set negative to disable. -1.0 m 0 @@ -12779,7 +4861,8 @@ Throttle at max airspeed - Required throttle (at sea level, standard atmosphere) for level flight at maximum airspeed FW_AIRSPD_MAX Set to 0 to disable mapping of airspeed to trim throttle. + Required throttle (at sea level, standard atmosphere) for level flight at maximum airspeed FW_AIRSPD_MAX +Set to 0 to disable mapping of airspeed to trim throttle. 0 1 2 @@ -12787,7 +4870,8 @@ Throttle at min airspeed - Required throttle (at sea level, standard atmosphere) for level flight at minimum airspeed FW_AIRSPD_MIN Set to 0 to disable mapping of airspeed to trim throttle below FW_AIRSPD_TRIM. + Required throttle (at sea level, standard atmosphere) for level flight at minimum airspeed FW_AIRSPD_MIN +Set to 0 to disable mapping of airspeed to trim throttle below FW_AIRSPD_TRIM. 0 1 2 @@ -12804,32 +4888,38 @@ Maximum climb rate - This is the maximum calibrated climb rate that the aircraft can achieve with the throttle set to FW_THR_MAX and the airspeed set to the trim value. For electric aircraft make sure this number can be achieved towards the end of flight when the battery voltage has reduced. + This is the maximum calibrated climb rate that the aircraft can achieve with +the throttle set to FW_THR_MAX and the airspeed set to the +trim value. For electric aircraft make sure this number can be +achieved towards the end of flight when the battery voltage has reduced. 1.0 - 15.0 m/s 1 0.5 Minimum descent rate - This is the minimum calibrated sink rate of the aircraft with the throttle set to THR_MIN and flown at the same airspeed as used to measure FW_T_CLMB_MAX. + This is the minimum calibrated sink rate of the aircraft with the throttle +set to THR_MIN and flown at the same airspeed as used +to measure FW_T_CLMB_MAX. 1.0 - 5.0 m/s 1 0.5 Vehicle base weight - This is the weight of the vehicle at which it's performance limits were derived. A zero or negative value disables trim throttle and minimum airspeed compensation based on weight. + This is the weight of the vehicle at which it's performance limits were derived. A zero or negative value +disables trim throttle and minimum airspeed compensation based on weight. kg 1 0.5 Vehicle gross weight - This is the actual weight of the vehicle at any time. This value will differ from WEIGHT_BASE in case weight was added or removed from the base weight. Examples are the addition of payloads or larger batteries. A zero or negative value disables trim throttle and minimum airspeed compensation based on weight. + This is the actual weight of the vehicle at any time. This value will differ from WEIGHT_BASE in case weight was added +or removed from the base weight. Examples are the addition of payloads or larger batteries. A zero or negative value +disables trim throttle and minimum airspeed compensation based on weight. kg 1 0.1 @@ -12844,7 +4934,10 @@ Enable yaw rate controller in Acro - If this parameter is set to 1, the yaw rate controller is enabled in Fixed-wing Acro mode. Otherwise the pilot commands directly the yaw actuator. It is disabled by default because an active yaw rate controller will fight against the natural turn coordination of the plane. + If this parameter is set to 1, the yaw rate controller is enabled in Fixed-wing Acro mode. +Otherwise the pilot commands directly the yaw actuator. +It is disabled by default because an active yaw rate controller will fight against the +natural turn coordination of the plane. Acro body pitch max rate setpoint @@ -12860,11 +4953,16 @@ Enable airspeed scaling - This enables a logic that automatically adjusts the output of the rate controller to take into account the real torque produced by an aerodynamic control surface given the current deviation from the trim airspeed (FW_AIRSPD_TRIM). Enable when using aerodynamic control surfaces (e.g.: plane) Disable when using rotor wings (e.g.: autogyro) + This enables a logic that automatically adjusts the output of the rate controller to take +into account the real torque produced by an aerodynamic control surface given +the current deviation from the trim airspeed (FW_AIRSPD_TRIM). +Enable when using aerodynamic control surfaces (e.g.: plane) +Disable when using rotor wings (e.g.: autogyro) Enable throttle scale by battery level - This compensates for voltage drop of the battery over time by attempting to normalize performance across the operating range of the battery. + This compensates for voltage drop of the battery over time by attempting to +normalize performance across the operating range of the battery. Pitch trim increment at maximum airspeed @@ -12914,27 +5012,21 @@ 2 0.01 - - Flaps setting during landing - Sets a fraction of full flaps during landing. Also applies to flaperons if enabled in the mixer/allocation. - 0.0 - 1.0 - norm - 2 - 0.01 + + Enable rate gain compression - - Flaps setting during take-off - Sets a fraction of full flaps during take-off. Also applies to flaperons if enabled in the mixer/allocation. + + Compression gain lower limit + The range of the compression gain is between this parameter and 1.0 0.0 1.0 - norm 2 0.01 Manual pitch scale - Scale factor applied to the desired pitch actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired pitch actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 norm 2 @@ -12942,7 +5034,8 @@ Manual roll scale - Scale factor applied to the desired roll actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired roll actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 1.0 norm @@ -12951,7 +5044,8 @@ Manual yaw scale - Scale factor applied to the desired yaw actuator command in full manual mode. This parameter allows to adjust the throws of the control surfaces. + Scale factor applied to the desired yaw actuator command in full manual mode. This parameter allows +to adjust the throws of the control surfaces. 0.0 norm 2 @@ -13000,7 +5094,9 @@ Roll control to yaw control feedforward gain - This gain can be used to counteract the "adverse yaw" effect for fixed wings. When the plane enters a roll it will tend to yaw the nose out of the turn. This gain enables the use of a yaw actuator to counteract this effect. + This gain can be used to counteract the "adverse yaw" effect for fixed wings. +When the plane enters a roll it will tend to yaw the nose out of the turn. +This gain enables the use of a yaw actuator to counteract this effect. 0.0 1 0.01 @@ -13056,7 +5152,11 @@ Use airspeed for control - If set to 1, the airspeed measurement data, if valid, is used in the following controllers: - Rate controller: output scaling - Attitude controller: coordinated turn controller - Position controller: airspeed setpoint tracking, takeoff logic - VTOL: transition logic + If set to 1, the airspeed measurement data, if valid, is used in the following controllers: +- Rate controller: output scaling +- Attitude controller: coordinated turn controller +- Position controller: airspeed setpoint tracking, takeoff logic +- VTOL: transition logic Yaw rate derivative gain @@ -13099,251 +5199,37 @@ 0.005 - - - Minimum groundspeed - The controller will increase the commanded airspeed to maintain this minimum groundspeed to the next waypoint. - 0.0 - 40 - m/s - 1 - 0.5 - - - Maximum pitch angle setpoint - Applies in any altitude controlled flight mode. - 0.0 - 60.0 - deg - 1 - 0.5 - - - Minimum pitch angle setpoint - Applies in any altitude controlled flight mode. - -60.0 - 0.0 - deg - 1 - 0.5 - - - Idle throttle - This is the minimum throttle while on the ground ("landed") in auto modes. - 0.0 - 0.4 - norm - 2 - 0.01 - - - Throttle limit max - Applies in any altitude controlled flight mode. Should be set accordingly to achieve FW_T_CLMB_MAX. - 0.0 - 1.0 - norm - 2 - 0.01 - - - Throttle limit min - Applies in any altitude controlled flight mode. Usually set to 0 but can be increased to prevent the motor from stopping when descending, which can increase achievable descent rates. - 0.0 - 1.0 - norm - 2 - 0.01 - - - Throttle max slew rate - Maximum slew rate for the commanded throttle - 0.0 - 1.0 - 2 - 0.01 - - - Takeoff Airspeed - The calibrated airspeed setpoint during the takeoff climbout. If set <= 0, FW_AIRSPD_MIN will be set by default. - -1.0 - m/s - 1 - 0.1 - - - Altitude error time constant - 2.0 - 2 - 0.5 - - - Default target climbrate - In auto modes: default climb rate output by controller to achieve altitude setpoints. In manual modes: maximum climb rate setpoint. - 0.5 - 15 - m/s - 2 - 0.01 - - - Fast descend: minimum altitude error - Minimum altitude error needed to descend with max airspeed and minimal throttle. A negative value disables fast descend. - -1.0 - 0 - - - Height rate feed forward - 0.0 - 1.0 - 2 - 0.05 - - - Integrator gain pitch - Increase it to trim out speed and height offsets faster, with the downside of possible overshoots and oscillations. - 0.0 - 2.0 - 2 - 0.05 - - - Pitch damping gain - 0.0 - 2.0 - 2 - 0.1 - - - Roll -> Throttle feedforward - Is used to compensate for the additional drag created by turning. Increase this gain if the aircraft initially loses energy in turns and reduce if the aircraft initially gains energy in turns. - 0.0 - 20.0 - 1 - 0.5 - - - Specific total energy balance rate feedforward gain - 0.5 - 3 - 2 - 0.01 - - - Maximum descent rate - 1.0 - 15.0 - m/s - 1 - 0.5 - - - Default target sinkrate - In auto modes: default sink rate output by controller to achieve altitude setpoints. In manual modes: maximum sink rate setpoint. - 0.5 - 15 - m/s - 2 - 0.01 - - - Speed <--> Altitude weight - Adjusts the amount of weighting that the pitch control applies to speed vs height errors. 0 -> control height only 2 -> control speed only (gliders) - 0.0 - 2.0 - 1 - 1.0 - - - Airspeed rate measurement standard deviation - For the airspeed filter in TECS. - 0.01 - 10.0 - m/s^2 - 2 - 0.1 - - - Process noise standard deviation for the airspeed rate - This is defining the noise in the airspeed rate for the constant airspeed rate model of the TECS airspeed filter. - 0.01 - 10.0 - m/s^2 - 2 - 0.1 - - - Airspeed measurement standard deviation - For the airspeed filter in TECS. - 0.01 - 10.0 - m/s - 2 - 0.1 - - - Specific total energy rate first order filter time constant - This filter is applied to the specific total energy rate used for throttle damping. - 0.0 - 2 - 2 - 0.01 + + + Enable Actuator Failure check + If enabled, failure detector will verify that for motors, a minimum amount of ESC current per throttle +level is being consumed. +Otherwise this indicates an motor failure. + true - - True airspeed error time constant - 2.0 + + Overcurrent motor failure limit offset + threshold = FD_ACT_MOT_C2T * thrust + FD_ACT_HIGH_OFF + 0 + 30 + A 2 - 0.5 - - - Throttle damping factor - This is the damping gain for the throttle demand loop. - 0.0 - 1.0 - 3 - 0.01 - - - Integrator gain throttle - Increase it to trim out speed and height offsets faster, with the downside of possible overshoots and oscillations. - 0.0 - 1.0 - 3 - 0.005 - - - Low-height threshold for tighter altitude tracking - Height above ground threshold below which tighter altitude tracking gets enabled (see FW_LND_THRTC_SC). Below this height, TECS smoothly (1 sec / sec) transitions the altitude tracking time constant from FW_T_ALT_TC to FW_LND_THRTC_SC*FW_T_ALT_TC. -1 to disable. - -1 - m - 0 1 - - Maximum vertical acceleration - This is the maximum vertical acceleration either up or down that the controller will use to correct speed or height errors. - 1.0 - 10.0 - m/s^2 - 1 - 0.5 - - - Wind-based airspeed scaling factor - Multiplying this factor with the current absolute wind estimate gives the airspeed offset added to the minimum airspeed setpoint limit. This helps to make the system more robust against disturbances (turbulence) in high wind. + + Undercurrent motor failure limit offset + threshold = FD_ACT_MOT_C2T * thrust - FD_ACT_LOW_OFF 0 + 30 + A 2 - 0.01 - - - - - Enable Actuator Failure check - If enabled, failure detector will verify that for motors, a minimum amount of ESC current per throttle level is being consumed. Otherwise this indicates an motor failure. - true + 1 - - Motor Failure Current/Throttle Threshold - Motor failure triggers only below this current value + + Motor Failure Current/Throttle Scale + Determines the slope between expected steady state current and linearized, normalized thrust command. +E.g. FD_ACT_MOT_C2T A represents the expected steady state current at 100%. +FD_ACT_LOW_OFF and FD_ACT_HIGH_OFF offset the threshold from that slope. 0.0 50.0 A/% @@ -13351,17 +5237,18 @@ 1 - Motor Failure Throttle Threshold - Motor failure triggers only above this throttle value. + Motor Failure Thrust Threshold + Failure detection per motor only triggers above this thrust value. +Set to 1 to disable the detection. 0.0 1.0 norm 2 0.01 - - Motor Failure Time Threshold - Motor failure triggers only if the throttle threshold and the current to throttle threshold are violated for this time. + + Motor Failure Hysteresis Time + Motor failure only triggers after current thresholds are exceeded for this time. 10 10000 ms @@ -13369,11 +5256,13 @@ Enable checks on ESCs that report their arming state - If enabled, failure detector will verify that all the ESCs have successfully armed when the vehicle has transitioned to the armed state. Timeout for receiving an acknowledgement from the ESCs is 0.3s, if no feedback is received the failure detector will auto disarm the vehicle. + If enabled, failure detector will verify that all the ESCs have successfully armed when the vehicle has transitioned to the armed state. +Timeout for receiving an acknowledgement from the ESCs is 0.3s, if no feedback is received the failure detector will auto disarm the vehicle. Enable PWM input on for engaging failsafe from an external automatic trigger system (ATS) - Enabled on either AUX5 or MAIN5 depending on board. External ATS is required by ASTM F3322-18. + Enabled on either AUX5 or MAIN5 depending on board. +External ATS is required by ASTM F3322-18. true @@ -13384,7 +5273,12 @@ FailureDetector Max Pitch - Maximum pitch angle before FailureDetector triggers the attitude_failure flag. The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), which sets outputs to their failsafe values. On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), which disarms motors but does not set outputs to failsafe values. Setting this parameter to 0 disables the check + Maximum pitch angle before FailureDetector triggers the attitude_failure flag. +The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), +which sets outputs to their failsafe values. +On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), +which disarms motors but does not set outputs to failsafe values. +Setting this parameter to 0 disables the check 0 180 deg @@ -13399,7 +5293,12 @@ FailureDetector Max Roll - Maximum roll angle before FailureDetector triggers the attitude_failure flag. The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), which sets outputs to their failsafe values. On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), which disarms motors but does not set outputs to failsafe values. Setting this parameter to 0 disables the check + Maximum roll angle before FailureDetector triggers the attitude_failure flag. +The flag triggers flight termination (if @CBRK_FLIGHTTERM = 0), +which sets outputs to their failsafe values. +On takeoff the flag triggers lockdown (irrespective of @CBRK_FLIGHTTERM), +which disarms motors but does not set outputs to failsafe values. +Setting this parameter to 0 disables the check 0 180 deg @@ -13414,7 +5313,9 @@ Imbalanced propeller check threshold - Value at which the imbalanced propeller metric (based on horizontal and vertical acceleration variance) triggers a failure Setting this value to 0 disables the feature. + Value at which the imbalanced propeller metric (based on horizontal and +vertical acceleration variance) triggers a failure +Setting this value to 0 disables the feature. 0 1000 1 @@ -13436,14 +5337,17 @@ Hold Initial Heading Uncontrolled Hold Front Tangent to Circle - RC Controlled + Manually (yaw stick) Controlled Altitude control mode - Maintain altitude or track target's altitude. When maintaining the altitude, the drone can crash into terrain when the target moves uphill. When tracking the target's altitude, the follow altitude FLW_TGT_HT should be high enough to prevent terrain collisions due to GPS inaccuracies of the target. + Maintain altitude or track target's altitude. When maintaining the altitude, +the drone can crash into terrain when the target moves uphill. When tracking +the target's altitude, the follow altitude FLW_TGT_HT should be high enough +to prevent terrain collisions due to GPS inaccuracies of the target. 2D Tracking: Maintain constant altitude relative to home and track XY position only 2D + Terrain: Maintain constant altitude relative to terrain below and track XY position @@ -13458,7 +5362,11 @@ Follow Angle setting in degrees - Angle to follow the target from. 0.0 Equals straight in front of the target's course (direction of motion) and the angle increases in clockwise direction, meaning Right-side would be 90.0 degrees while Left-side is -90.0 degrees Note: When the user force sets the angle out of the min/max range, it will be wrapped (e.g. 480 -> 120) in the range to gracefully handle the out of range. + Angle to follow the target from. 0.0 Equals straight in front of the target's +course (direction of motion) and the angle increases in clockwise direction, +meaning Right-side would be 90.0 degrees while Left-side is -90.0 degrees +Note: When the user force sets the angle out of the min/max range, it will be +wrapped (e.g. 480 -> 120) in the range to gracefully handle the out of range. -180.0 180.0 @@ -13470,7 +5378,8 @@ Maximum tangential velocity setting for generating the follow orbit trajectory - This is the maximum tangential velocity the drone will circle around the target whenever an orbit angle setpoint changes. Higher value means more aggressive follow behavior. + This is the maximum tangential velocity the drone will circle around the target whenever +an orbit angle setpoint changes. Higher value means more aggressive follow behavior. 0.0 20.0 1 @@ -13482,30 +5391,21 @@ 1.0 2 - - - - Serial Configuration for Main GPS - Configure on which serial port to run Main GPS. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - + + GNSS Systems for Primary GPS (integer bitmask) - This integer bitmask controls the set of GNSS systems used by the receiver. Check your receiver's documentation on how many systems are supported to be used in parallel. Currently this functionality is just implemented for u-blox receivers. When no bits are set, the receiver's default configuration should be used. Set bits true to enable: 0 : Use GPS (with QZSS) 1 : Use SBAS (multiple GPS augmentation systems) 2 : Use Galileo 3 : Use BeiDou 4 : Use GLONASS 5 : Use NAVIC + This integer bitmask controls the set of GNSS systems used by the receiver. Check your +receiver's documentation on how many systems are supported to be used in parallel. +Currently this functionality is just implemented for u-blox receivers. +When no bits are set, the receiver's default configuration should be used. +Set bits true to enable: +0 : Use GPS (with QZSS) +1 : Use SBAS (multiple GPS augmentation systems) +2 : Use Galileo +3 : Use BeiDou +4 : Use GLONASS +5 : Use NAVIC 0 63 true @@ -13520,7 +5420,8 @@ Protocol for Main GPS - Select the GPS protocol over serial. Auto-detection will probe all protocols, and thus is a bit slower. + Select the GPS protocol over serial. +Auto-detection will probe all protocols, and thus is a bit slower. 0 7 true @@ -13534,28 +5435,19 @@ NMEA (generic) - - Serial Configuration for Secondary GPS - Configure on which serial port to run Secondary GPS. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - GNSS Systems for Secondary GPS (integer bitmask) - This integer bitmask controls the set of GNSS systems used by the receiver. Check your receiver's documentation on how many systems are supported to be used in parallel. Currently this functionality is just implemented for u-blox receivers. When no bits are set, the receiver's default configuration should be used. Set bits true to enable: 0 : Use GPS (with QZSS) 1 : Use SBAS (multiple GPS augmentation systems) 2 : Use Galileo 3 : Use BeiDou 4 : Use GLONASS 5 : Use NAVIC + This integer bitmask controls the set of GNSS systems used by the receiver. Check your +receiver's documentation on how many systems are supported to be used in parallel. +Currently this functionality is just implemented for u-blox receivers. +When no bits are set, the receiver's default configuration should be used. +Set bits true to enable: +0 : Use GPS (with QZSS) +1 : Use SBAS (multiple GPS augmentation systems) +2 : Use Galileo +3 : Use BeiDou +4 : Use GLONASS +5 : Use NAVIC 0 63 true @@ -13570,7 +5462,8 @@ Protocol for Secondary GPS - Select the GPS protocol over serial. Auto-detection will probe all protocols, and thus is a bit slower. + Select the GPS protocol over serial. +Auto-detection will probe all protocols, and thus is a bit slower. 0 6 true @@ -13584,9 +5477,21 @@ NMEA (generic) + + Wipes the flash config of UBX modules + Some UBX modules have a FLASH that allows to store persistent configuration that will be loaded on start. +PX4 does override all configuration parameters it needs in RAM, which takes precedence over the values in FLASH. +However, configuration parameters that are not overriden by PX4 can still cause unexpected problems during flight. +To avoid these kind of problems a clean config can be reached by wiping the FLASH on boot. +Note: Currently only supported on UBX. + true + Log GPS communication data - If this is set to 1, all GPS communication data will be published via uORB, and written to the log file as gps_dump message. If this is set to 2, the main GPS is configured to output RTCM data, which is then logged as gps_dump and can be used for PPK. + If this is set to 1, all GPS communication data will be published via uORB, +and written to the log file as gps_dump message. +If this is set to 2, the main GPS is configured to output RTCM data, +which is then logged as gps_dump and can be used for PPK. 0 2 @@ -13597,12 +5502,15 @@ Enable sat info (if available) - Enable publication of satellite info (ORB_ID(satellite_info)) if possible. Not available on MTK. + Enable publication of satellite info (ORB_ID(satellite_info)) if possible. +Not available on MTK. true u-blox F9P UART2 Baudrate - Select a baudrate for the F9P's UART2 port. In GPS_UBX_MODE 1, 2, and 3, the F9P's UART2 port is configured to send/receive RTCM corrections. Set this to 57600 if you want to attach a telemetry radio on UART2. + Select a baudrate for the F9P's UART2 port. +In GPS_UBX_MODE 1, 2, and 3, the F9P's UART2 port is configured to send/receive RTCM corrections. +Set this to 57600 if you want to attach a telemetry radio on UART2. 0 B/s true @@ -13621,9 +5529,18 @@ Enable I2C output protocol RTCM3X + + u-blox GPS DGNSS timeout + When set to 0 (default), default DGNSS timeout set by u-blox will be used. + 0 + 255 + s + true + u-blox GPS dynamic platform model - u-blox receivers support different dynamic platform models to adjust the navigation engine to the expected application environment. + u-blox receivers support different dynamic platform models to adjust the navigation engine to +the expected application environment. 0 9 true @@ -13635,9 +5552,42 @@ airborne with <4g acceleration + + u-blox GPS jamming detection high sensitivity mode + Enables or disables the high sensitivity mode for the u-blox jamming detection +(CFG-SEC-JAMDET_SENSITIVITY_HI). When enabled, the receiver uses a +more sensitive algorithm to detect jamming. Disabling this may reduce false +positives in electrically noisy environments. + true + + + u-blox GPS minimum satellite signal level for navigation + When set to 0 (default), default minimum satellite signal level set by u-blox wll be used. + 0 + 255 + dBHz + true + + + u-blox GPS minimum elevation for a GNSS satellite to be used in navigation + When set to 0 (default), default minimum elevation set by u-blox will be used. + 0 + 127 + deg + true + u-blox GPS Mode - Select the u-blox configuration setup. Most setups will use the default, including RTK and dual GPS without heading. If rover has RTCM corrections from a static base (or other static correction source) coming in on UART2, then select Mode 5. The Heading mode requires 2 F9P devices to be attached. The main GPS will act as rover and output heading information, whereas the secondary will act as moving base. Modes 1 and 2 require each F9P UART1 to be connected to the Autopilot. In addition, UART2 on the F9P units are connected to each other. Modes 3 and 4 only require UART1 on each F9P connected to the Autopilot or Can Node. UART RX DMA is required. RTK is still possible with this setup. + Select the u-blox configuration setup. Most setups will use the default, including RTK and +dual GPS without heading. +If rover has RTCM corrections from a static base (or other static correction source) coming in on UART2, then select Mode 5. +The Heading mode requires 2 F9P devices to be attached. The main GPS will act as rover and output +heading information, whereas the secondary will act as moving base. +Modes 1 and 2 require each F9P UART1 to be connected to the Autopilot. In addition, UART2 on the +F9P units are connected to each other. +Modes 3 and 4 only require UART1 on each F9P connected to the Autopilot or Can Node. UART RX DMA is required. +RTK is still possible with this setup. +Mode 6 is intended for use with a ground control station (not necessarily an RTK correction base). 0 1 true @@ -13648,27 +5598,49 @@ Heading (Rover With Moving Base UART1 Connected to Autopilot Or Can Node At 921600) Moving Base (Moving Base UART1 Connected to Autopilot Or Can Node At 921600) Rover with Static Base on UART2 (similar to Default, except coming in on UART2) + Ground Control Station (UART2 outputs NMEA) + + Enable MSM7 message output for PPK workflow + true + + + u-blox GPS output rate + Configure the output rate of u-blox GPS receivers (protocol v27+). +When set to 0, automatic rate selection is used based on the receiver model. +Default rates: M9N=8Hz, F9P L1L2=5Hz, F9P L1L5=5Hz, Others=10Hz. +Note: Higher rates reduce satellite count (e.g., >8Hz limits to 16 SVs on M9N). +Max rates vary by model and RTK mode: F9P L1L2=5-7Hz, F9P L1L5=7-8Hz, X20=25Hz. +High rates at 115200 baud may cause dropouts. + 0 + 25 + Hz + true + Heading/Yaw offset for dual antenna GPS - Heading offset angle for dual antenna GPS setups that support heading estimation. Set this to 0 if the antennas are parallel to the forward-facing direction of the vehicle and the rover (or Unicore primary) antenna is in front. The offset angle increases clockwise. Set this to 90 if the rover (or Unicore primary, or Septentrio Mosaic Aux) antenna is placed on the right side of the vehicle and the moving base antenna is on the left side. (Note: the Unicore primary antenna is the one connected on the right as seen from the top). + Heading offset angle for dual antenna GPS setups that support heading estimation. +Set this to 0 if the antennas are parallel to the forward-facing direction +of the vehicle and the rover (or Unicore primary) antenna is in front. +The offset angle increases clockwise. +Set this to 90 if the rover (or Unicore primary, or Septentrio Mosaic Aux) +antenna is placed on the right side of the vehicle and the moving base +antenna is on the left side. +(Note: the Unicore primary antenna is the one connected on the right as seen +from the top). 0 360 deg 3 true - - PPS Capture Enable - Enables the PPS capture module. This switches mode of FMU channel 7 to be the PPS input channel. - true - Geofence violation action - Note: Setting this value to 4 enables flight termination, which will kill the vehicle on violation of the fence. + Note: Setting this value to 4 enables flight termination, +which will kill the vehicle on violation of the fence. 0 5 @@ -13682,7 +5654,8 @@ Max horizontal distance from Home - Maximum horizontal distance in meters the vehicle can be from Home before triggering a geofence action. Disabled if 0. + Maximum horizontal distance in meters the vehicle can be from Home before triggering a geofence action. +Disabled if 0. 0 10000 m @@ -13690,7 +5663,8 @@ Max vertical distance from Home - Maximum vertical distance in meters the vehicle can be from Home before triggering a geofence action. Disabled if 0. + Maximum vertical distance in meters the vehicle can be from Home before triggering a geofence action. +Disabled if 0. 0 10000 m @@ -13698,11 +5672,16 @@ [EXPERIMENTAL] Use Pre-emptive geofence triggering - WARNING: This experimental feature may cause flyaways. Use at your own risk. Predict the motion of the vehicle and trigger the breach if it is determined that the current trajectory would result in a breach happening before the vehicle can make evasive maneuvers. The vehicle is then re-routed to a safe hold position (stop for multirotor, loiter for fixed wing). + WARNING: This experimental feature may cause flyaways. Use at your own risk. +Predict the motion of the vehicle and trigger the breach if it is determined that the current trajectory +would result in a breach happening before the vehicle can make evasive maneuvers. +The vehicle is then re-routed to a safe hold position (stop for multirotor, loiter for fixed wing). Geofence source - Select which position source should be used. Selecting GPS instead of global position makes sure that there is no dependence on the position estimator 0 = global position, 1 = GPS + Select which position source should be used. Selecting GPS instead of global position makes sure that there is +no dependence on the position estimator +0 = global position, 1 = GPS 0 1 @@ -13714,7 +5693,9 @@ Airframe selection - Defines which mixer implementation to use. Some are generic, while others are specifically fit to a certain vehicle with a fixed set of actuators. 'Custom' should only be used if noting else can be used. + Defines which mixer implementation to use. +Some are generic, while others are specifically fit to a certain vehicle with a fixed set of actuators. +'Custom' should only be used if noting else can be used. Multirotor Fixed-wing @@ -13730,11 +5711,30 @@ Helicopter (tail Servo) Helicopter (Coaxial) Rover (Mecanum) + Spacecraft 2D + Spacecraft 3D + + Control surface launch lock enabled + If actuator launch lock is enabled, this surface is kept at the disarmed value. + 0 + 255 + + Control Surface 1 + Control Surface 2 + Control Surface 3 + Control Surface 4 + Control Surface 5 + Control Surface 6 + Control Surface 7 + Control Surface 8 + + Motor failure handling mode - This is used to specify how to handle motor failures reported by failure detector. + This is used to specify how to handle motor failures +reported by failure detector. Ignore Remove first failed motor from effectiveness @@ -13742,7 +5742,8 @@ Collective pitch curve at position 0 - Defines the collective pitch at the interval position 0 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 0 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13750,7 +5751,8 @@ Collective pitch curve at position 1 - Defines the collective pitch at the interval position 1 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 1 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13758,7 +5760,8 @@ Collective pitch curve at position 2 - Defines the collective pitch at the interval position 2 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 2 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13766,7 +5769,8 @@ Collective pitch curve at position 3 - Defines the collective pitch at the interval position 3 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 3 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 @@ -13774,12 +5778,38 @@ Collective pitch curve at position 4 - Defines the collective pitch at the interval position 4 for a given thrust setpoint. Use negative values if the swash plate needs to move down to provide upwards thrust. + Defines the collective pitch at the interval position 4 for a given thrust setpoint. +Use negative values if the swash plate needs to move down to provide upwards thrust. -1 1 3 0.1 + + Integral gain for rpm control + Same definition as the proportional gain but for integral. + 0 + 10 + 3 + 0.1 + + + Proportional gain for rpm control + Ratio between rpm error devided by 1000 to how much normalized output gets added to correct for it. +motor_command = throttle_curve + CA_HELI_RPM_P * (rpm_setpoint - rpm_measurement) / 1000 + 0 + 10 + 3 + 0.1 + + + Setpoint for main rotor rpm + Requires rpm feedback for the controller. + 100 + 10000 + 0 + 1 + Throttle curve at position 0 Defines the output throttle at the interval position 0. @@ -13822,11 +5852,19 @@ Main rotor turns counter-clockwise - Default configuration is for a clockwise turning main rotor and positive thrust of the tail rotor is expected to rotate the vehicle clockwise. Set this parameter to true if the tail rotor provides thrust in counter-clockwise direction which is mostly the case when the main rotor turns counter-clockwise. + Default configuration is for a clockwise turning main rotor and +positive thrust of the tail rotor is expected to rotate the vehicle clockwise. +Set this parameter to true if the tail rotor provides thrust in counter-clockwise direction +which is mostly the case when the main rotor turns counter-clockwise. Offset for yaw compensation based on collective pitch - This allows to specify which collective pitch command results in the least amount of rotor drag. This is used to increase the accuracy of the yaw drag torque compensation based on collective pitch by aligning the lowest rotor drag with zero compensation. For symmetric profile blades this is the command that results in exactly 0° collective blade angle. For lift profile blades this is typically a command resulting in slightly negative collective blade angle. tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) + This allows to specify which collective pitch command results in the least amount of rotor drag. +This is used to increase the accuracy of the yaw drag torque compensation based on collective pitch +by aligning the lowest rotor drag with zero compensation. +For symmetric profile blades this is the command that results in exactly 0° collective blade angle. +For lift profile blades this is typically a command resulting in slightly negative collective blade angle. +tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) -2 2 3 @@ -13834,7 +5872,9 @@ Scale for yaw compensation based on collective pitch - This allows to add a proportional factor of the collective pitch command to the yaw command. A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) + This allows to add a proportional factor of the collective pitch command to the yaw command. +A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. +tail_output += CA_HELI_YAW_CP_S * abs(collective_pitch - CA_HELI_YAW_CP_O) -2 2 3 @@ -13842,15 +5882,39 @@ Scale for yaw compensation based on throttle - This allows to add a proportional factor of the throttle command to the yaw command. A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. tail_output += CA_HELI_YAW_TH_S * throttle + This allows to add a proportional factor of the throttle command to the yaw command. +A negative value is needed when positive thrust of the tail rotor rotates the vehicle opposite to the main rotor turn direction. +tail_output += CA_HELI_YAW_TH_S * throttle -2 2 3 0.1 + + Ice shedding cycle period + Ice shedding prevents ice buildup in VTOL aircraft motors by periodically spinning inactive rotors. +When enabled (period > 0), every cycle lasts for the defined period and includes a 2-second spin at 0.01 motor output. +If period <= 0, the feature is disabled. + 0.0 + s + 1 + 0.1 + + + Throw angle of swashplate servo at maximum commands for linearization + Used to linearize mechanical output of swashplate servos to avoid axis coupling and binding with 4 servo redundancy. +This requires a symmetric setup where the servo horn is exactly centered with a 0 command. +Setting to zero disables feature. + 0 + 75 + deg + 1 + 0.1 + Control allocation method - Selects the algorithm and desaturation method. If set to Automtic, the selection is based on the airframe (CA_AIRFRAME). + Selects the algorithm and desaturation method. +If set to Automatic, the selection is based on the airframe (CA_AIRFRAME). Pseudo-inverse with output clipping Pseudo-inverse with sequential desaturation technique @@ -13859,97 +5923,133 @@ Motor 0 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 10 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 11 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 1 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 2 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 3 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 4 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 5 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 6 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 7 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 8 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 Motor 9 slew rate limit - Minimum time allowed for the motor input signal to pass through the full output range. A value x means that the motor signal can only go from 0 to 1 in minimum x seconds (in case of reversible motors, the range is -1 to 1). Zero means that slew rate limiting is disabled. + Forces the motor output signal to take at least the configured time (in seconds) +to traverse its full range (normally [0, 1], or if reversible [-1, 1]). +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.01 @@ -13979,7 +6079,9 @@ Thrust coefficient of rotor 0 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -13987,7 +6089,9 @@ Moment coefficient of rotor 0 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14054,7 +6158,9 @@ Thrust coefficient of rotor 10 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14062,7 +6168,9 @@ Moment coefficient of rotor 10 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14129,7 +6237,9 @@ Thrust coefficient of rotor 11 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14137,7 +6247,9 @@ Moment coefficient of rotor 11 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14204,7 +6316,9 @@ Thrust coefficient of rotor 1 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14212,7 +6326,9 @@ Moment coefficient of rotor 1 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14279,7 +6395,9 @@ Thrust coefficient of rotor 2 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14287,7 +6405,9 @@ Moment coefficient of rotor 2 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14354,7 +6474,9 @@ Thrust coefficient of rotor 3 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14362,7 +6484,9 @@ Moment coefficient of rotor 3 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14429,7 +6553,9 @@ Thrust coefficient of rotor 4 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14437,7 +6563,9 @@ Moment coefficient of rotor 4 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14504,7 +6632,9 @@ Thrust coefficient of rotor 5 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14512,7 +6642,9 @@ Moment coefficient of rotor 5 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14579,7 +6711,9 @@ Thrust coefficient of rotor 6 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14587,7 +6721,9 @@ Moment coefficient of rotor 6 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14654,7 +6790,9 @@ Thrust coefficient of rotor 7 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14662,7 +6800,9 @@ Moment coefficient of rotor 7 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14729,7 +6869,9 @@ Thrust coefficient of rotor 8 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14737,7 +6879,9 @@ Moment coefficient of rotor 8 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14804,7 +6948,9 @@ Thrust coefficient of rotor 9 - The thrust coefficient if defined as Thrust = CT * u^2, where u (with value between actuator minimum and maximum) is the output signal sent to the motor controller. + The thrust coefficient if defined as Thrust = CT * u^2, +where u (with value between actuator minimum and maximum) +is the output signal sent to the motor controller. 0 100 1 @@ -14812,7 +6958,9 @@ Moment coefficient of rotor 9 - The moment coefficient if defined as Torque = KM * Thrust. Use a positive value for a rotor with CCW rotation. Use a negative value for a rotor with CW rotation. + The moment coefficient if defined as Torque = KM * Thrust. +Use a positive value for a rotor with CCW rotation. +Use a negative value for a rotor with CW rotation. -1 1 3 @@ -14969,65 +7117,89 @@ Servo 0 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 1 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 2 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 3 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 4 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 5 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 6 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 Servo 7 slew rate limit - Minimum time allowed for the servo input signal to pass through the full output range. A value x means that the servo signal can only go from -1 to 1 in minimum x seconds. Zero means that slew rate limiting is disabled. + Forces the servo output signal to take at least the configured time (in seconds) +to traverse its full range [-100%, 100%]. +Zero means that slew rate limiting is disabled. 0 10 + s 2 0.05 @@ -15045,7 +7217,9 @@ Control Surface 0 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15100,7 +7274,9 @@ Control Surface 1 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15155,7 +7331,9 @@ Control Surface 2 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15210,7 +7388,9 @@ Control Surface 3 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15265,7 +7445,9 @@ Control Surface 4 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15320,7 +7502,9 @@ Control Surface 5 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15375,7 +7559,9 @@ Control Surface 6 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15430,7 +7616,9 @@ Control Surface 7 trim - Can be used to add an offset to the servo control. + Can be used to add an offset to the servo control. +NOTE: Do not use for PWM servos. Use the PWM CENTER parameters instead (e.g., PWM_MAIN_CENT, PWM_AUX_CENT) instead. +This parameter can only be set if all PWM Center parameters are set to default. -1.0 1.0 2 @@ -15485,6 +7673,12 @@ 8 + + Control Surface slew rate for normalized flaps setpoint + 0.0 + 5.0 + 1 + Tilt 0 is used for control Define if this servo is used for additional control. @@ -15497,7 +7691,8 @@ Tilt Servo 0 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15505,7 +7700,8 @@ Tilt Servo 0 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15513,7 +7709,9 @@ Tilt Servo 0 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15533,7 +7731,8 @@ Tilt Servo 1 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15541,7 +7740,8 @@ Tilt Servo 1 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15549,7 +7749,9 @@ Tilt Servo 1 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15569,7 +7771,8 @@ Tilt Servo 2 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15577,7 +7780,8 @@ Tilt Servo 2 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15585,7 +7789,9 @@ Tilt Servo 2 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15605,7 +7811,8 @@ Tilt Servo 3 Tilt Angle at Maximum - Defines the tilt angle when the servo is at the maximum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the maximum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15613,7 +7820,8 @@ Tilt Servo 3 Tilt Angle at Minimum - Defines the tilt angle when the servo is at the minimum. An angle of zero means upwards. + Defines the tilt angle when the servo is at the minimum. +An angle of zero means upwards. -90.0 90.0 deg @@ -15621,7 +7829,9 @@ Tilt Servo 3 Tilt Direction - Defines the direction the servo tilts towards when moving towards the maximum tilt angle. For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. + Defines the direction the servo tilts towards when moving towards the maximum tilt angle. +For example if the minimum tilt angle is -90, the maximum 90, and the direction 'Towards Front', +the motor axis aligns with the XZ-plane, points towards -X at the minimum and +X at the maximum tilt. 0 359 @@ -15643,7 +7853,8 @@ Gate size for acceleration fusion - Sets the number of standard deviations used by the innovation consistency test. + Sets the number of standard deviations used +by the innovation consistency test. 1.0 10.0 SD @@ -15651,7 +7862,8 @@ 1-sigma initial hover thrust uncertainty - Sets the number of standard deviations used by the innovation consistency test. + Sets the number of standard deviations used +by the innovation consistency test. 0.0 1.0 normalized_thrust @@ -15659,7 +7871,9 @@ Hover thrust process noise - Reduce to make the hover thrust estimate more stable, increase if the real hover thrust is expected to change quickly over time. + Reduce to make the hover thrust estimate +more stable, increase if the real hover thrust +is expected to change quickly over time. 0.0001 1.0 normalized_thrust/s @@ -15667,7 +7881,10 @@ Max deviation from MPC_THR_HOVER - Defines the range of the hover thrust estimate around MPC_THR_HOVER. A value of 0.2 with MPC_THR_HOVER at 0.5 results in a range of [0.3, 0.7]. Set to a large value if the vehicle operates in varying physical conditions that affect the required hover thrust strongly (e.g. differently sized payloads). + Defines the range of the hover thrust estimate around MPC_THR_HOVER. +A value of 0.2 with MPC_THR_HOVER at 0.5 results in a range of [0.3, 0.7]. +Set to a large value if the vehicle operates in varying physical conditions that +affect the required hover thrust strongly (e.g. differently sized payloads). 0.01 0.4 normalized_thrust @@ -15675,7 +7892,9 @@ Horizontal velocity threshold for sensitivity reduction - Above this speed, the measurement noise is linearly increased to reduce the sensitivity of the estimator from biased measurement. Set to a low value on vehicles with large lifting surfaces. + Above this speed, the measurement noise is linearly increased +to reduce the sensitivity of the estimator from biased measurement. +Set to a low value on vehicles with large lifting surfaces. 1.0 20.0 m/s @@ -15683,62 +7902,31 @@ Vertical velocity threshold for sensitivity reduction - Above this speed, the measurement noise is linearly increased to reduce the sensitivity of the estimator from biased measurement. Set to a low value on vehicles affected by air drag when climbing or descending. + Above this speed, the measurement noise is linearly increased +to reduce the sensitivity of the estimator from biased measurement. +Set to a low value on vehicles affected by air drag when climbing or descending. 1.0 10.0 m/s 1 - - - Serial Configuration for Iridium (with MAVLink) - Configure on which serial port to run Iridium (with MAVLink). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Satellite radio read interval. Only required to be nonzero if data is not sent using a ring call - 0 - 5000 - s - - - Iridium SBD session timeout - 0 - 300 - s - - - Time the Iridium driver will wait for additional mavlink messages to combine them into one SBD message - Value 0 turns the functionality off - 0 - 500 - ms - - Fixed-wing land detector: Max airspeed Maximum airspeed allowed in the landed state 2 - 20 + 30 m/s 1 + + Fixed-wing land detector: max rotational speed + Maximum allowed norm of the angular velocity in the landed state. +Only used if neither airspeed nor groundspeed can be used for landing detection. + deg/s + 1 + Fixed-wing land detection trigger time Time the land conditions (speeds and acceleration) have to be satisfied to detect a landing. @@ -15749,9 +7937,11 @@ Fixed-wing land detector: Max horizontal velocity threshold - Maximum horizontal velocity allowed in the landed state. A factor of 0.7 is applied in case of airspeed-less flying (either because no sensor is present or sensor data got invalid in flight). + Maximum horizontal velocity allowed in the landed state. +A factor of 0.7 is applied in case of airspeed-less flying +(either because no sensor is present or sensor data got invalid in flight). 0.5 - 10 + 20 m/s 1 @@ -15767,31 +7957,24 @@ Fixed-wing land detector: Max horizontal acceleration Maximum horizontal (x,y body axes) acceleration allowed in the landed state 2 - 15 + 30 m/s^2 1 Ground effect altitude for multicopters - The height above ground below which ground effect creates barometric altitude errors. A negative value indicates no ground effect. + The height above ground below which ground effect creates barometric altitude errors. +A negative value indicates no ground effect. -1 m 2 - Multicopter max rotation - Maximum allowed angular velocity around each axis allowed in the landed state. + Multicopter max rotational speed + Maximum allowed norm of the angular velocity (roll, pitch) in the landed state. deg/s 1 - - Multicopter land detection trigger time - Total time it takes to go through all three land detection stages: ground contact, maybe landed, landed when all necessary conditions are constantly met. - 0.1 - 10.0 - s - 1 - Multicopter max horizontal velocity Maximum horizontal velocity allowed in the landed state @@ -15800,39 +7983,48 @@ Multicopter vertical velocity threshold - Vertical velocity threshold to detect landing. Has to be set lower than the expected minimal speed for landing, which is either MPC_LAND_SPEED or MPC_LAND_CRWL. This is enforced by an automatic check. + Vertical velocity threshold to detect landing. +Has to be set lower than the expected minimal speed for landing, +which is either MPC_LAND_SPEED or MPC_LAND_CRWL. +This is enforced by an automatic check. 0 m/s 2 Total flight time in microseconds - Total flight time of this autopilot. Higher 32 bits of the value. Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. + Total flight time of this autopilot. Higher 32 bits of the value. +Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. 0 Total flight time in microseconds - Total flight time of this autopilot. Lower 32 bits of the value. Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. + Total flight time of this autopilot. Lower 32 bits of the value. +Flight time in microseconds = (LND_FLIGHT_T_HI << 32) | LND_FLIGHT_T_LO. 0 Acceleration uncertainty - Variance of acceleration measurement used for landing target position prediction. Higher values results in tighter following of the measurements and more lenient outlier rejection + Variance of acceleration measurement used for landing target position prediction. +Higher values results in tighter following of the measurements and more lenient outlier rejection 0.01 (m/s^2)^2 2 Landing target measurement uncertainty - Variance of the landing target measurement from the driver. Higher values result in less aggressive following of the measurement and a smoother output as well as fewer rejected measurements. + Variance of the landing target measurement from the driver. +Higher values result in less aggressive following of the measurement and a smoother output as well as fewer rejected measurements. tan(rad)^2 4 Landing target mode - Configure the mode of the landing target. Depending on the mode, the landing target observations are used differently to aid position estimation. Mode Moving: The landing target may be moving around while in the field of view of the vehicle. Landing target measurements are not used to aid positioning. Mode Stationary: The landing target is stationary. Measured velocity w.r.t. the landing target is used to aid velocity estimation. + Configure the mode of the landing target. Depending on the mode, the landing target observations are used differently to aid position estimation. +Mode Moving: The landing target may be moving around while in the field of view of the vehicle. Landing target measurements are not used to aid positioning. +Mode Stationary: The landing target is stationary. Measured velocity w.r.t. the landing target is used to aid velocity estimation. 0 1 @@ -15905,7 +8097,8 @@ Accelerometer xy noise density - Data sheet noise density = 150ug/sqrt(Hz) = 0.0015 m/s^2/sqrt(Hz) Larger than data sheet to account for tilt error. + Data sheet noise density = 150ug/sqrt(Hz) = 0.0015 m/s^2/sqrt(Hz) +Larger than data sheet to account for tilt error. 0.00001 2 m/s^2/sqrt(Hz) @@ -15991,7 +8184,16 @@ Integer bitmask controlling data fusion - Set bits in the following positions to enable: 0 : Set to true to fuse GPS data if available, also requires GPS for altitude init 1 : Set to true to fuse optical flow data if available 2 : Set to true to fuse vision position 3 : Set to true to enable landing target 4 : Set to true to fuse land detector 5 : Set to true to publish AGL as local position down component 6 : Set to true to enable flow gyro compensation 7 : Set to true to enable baro fusion default (145 - GPS, baro, land detector) + Set bits in the following positions to enable: +0 : Set to true to fuse GPS data if available, also requires GPS for altitude init +1 : Set to true to fuse optical flow data if available +2 : Set to true to fuse vision position +3 : Set to true to enable landing target +4 : Set to true to fuse land detector +5 : Set to true to publish AGL as local position down component +6 : Set to true to enable flow gyro compensation +7 : Set to true to enable baro fusion +default (145 - GPS, baro, land detector) 0 255 @@ -16099,7 +8301,8 @@ Position propagation noise density - Increase to trust measurements more. Decrease to trust model more. + Increase to trust measurements more. +Decrease to trust model more. 0 1 m/s/sqrt(Hz) @@ -16114,7 +8317,8 @@ Velocity propagation noise density - Increase to trust measurements more. Decrease to trust model more. + Increase to trust measurements more. +Decrease to trust model more. 0 1 m/s^2/sqrt(Hz) @@ -16196,36 +8400,18 @@ Broadcast heartbeats on local network for MAVLink instance 0 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 0) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 0 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16235,12 +8421,17 @@ Enable MAVLink Message forwarding for instance 0 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 0 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 0, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 0, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16250,7 +8441,8 @@ MAVLink Mode for instance 0 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16264,62 +8456,54 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 0 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 0 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 0 - If ethernet enabled and selected as configuration for MAVLink instance 0, selected remote port will be set and used in MAVLink instance 0. + If ethernet enabled and selected as configuration for MAVLink instance 0, +selected remote port will be set and used in MAVLink instance 0. True MAVLink Network Port for instance 0 - If ethernet enabled and selected as configuration for MAVLink instance 0, selected udp port will be set and used in MAVLink instance 0. + If ethernet enabled and selected as configuration for MAVLink instance 0, +selected udp port will be set and used in MAVLink instance 0. True Broadcast heartbeats on local network for MAVLink instance 1 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 1) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 1 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16329,12 +8513,17 @@ Enable MAVLink Message forwarding for instance 1 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 1 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 1, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 1, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16344,7 +8533,8 @@ MAVLink Mode for instance 1 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16358,62 +8548,54 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 1 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 1 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 1 - If ethernet enabled and selected as configuration for MAVLink instance 1, selected remote port will be set and used in MAVLink instance 1. + If ethernet enabled and selected as configuration for MAVLink instance 1, +selected remote port will be set and used in MAVLink instance 1. True MAVLink Network Port for instance 1 - If ethernet enabled and selected as configuration for MAVLink instance 1, selected udp port will be set and used in MAVLink instance 1. + If ethernet enabled and selected as configuration for MAVLink instance 1, +selected udp port will be set and used in MAVLink instance 1. True Broadcast heartbeats on local network for MAVLink instance 2 - This allows a ground control station to automatically find the drone on the local network. + This allows a ground control station to automatically find the drone +on the local network. Never broadcast Always broadcast Only multicast - - Serial Configuration for MAVLink (instance 2) - Configure on which serial port to run MAVLink. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - Enable serial flow control for instance 2 - This is used to force flow control on or off for the the mavlink instance. By default it is auto detected. Use when auto detection fails. + This is used to force flow control on or off for the the mavlink +instance. By default it is auto detected. Use when auto detection fails. True Force off @@ -16423,12 +8605,17 @@ Enable MAVLink Message forwarding for instance 2 - If enabled, forward incoming MAVLink messages to other MAVLink ports if the message is either broadcast or the target is not the autopilot. This allows for example a GCS to talk to a camera that is connected to the autopilot via MAVLink (on a different link than the GCS). + If enabled, forward incoming MAVLink messages to other MAVLink ports if the +message is either broadcast or the target is not the autopilot. +This allows for example a GCS to talk to a camera that is connected to the +autopilot via MAVLink (on a different link than the GCS). True Configures the frequency of HIGH_LATENCY2 stream for instance 2 - Positive real value that configures the transmission frequency of the HIGH_LATENCY2 stream for instance 2, configured in iridium mode. This parameter has no effect if the instance mode is different from iridium. + Positive real value that configures the transmission frequency of the +HIGH_LATENCY2 stream for instance 2, configured in iridium mode. +This parameter has no effect if the instance mode is different from iridium. 0.0 50.0 Hz @@ -16438,7 +8625,8 @@ MAVLink Mode for instance 2 - The MAVLink Mode defines the set of streamed messages (for example the vehicle's attitude) and their sending rates. + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. True Normal @@ -16452,28 +8640,38 @@ Gimbal Onboard Low Bandwidth uAvionix + Low Bandwidth Enable software throttling of mavlink on instance 2 - If enabled, MAVLink messages will be throttled according to `txbuf` field reported by radio_status. Requires a radio to send the mavlink message RADIO_STATUS. + If enabled, MAVLink messages will be throttled according to +`txbuf` field reported by radio_status. +Requires a radio to send the mavlink message RADIO_STATUS. True Maximum MAVLink sending rate for instance 2 - Configure the maximum sending rate for the MAVLink streams in Bytes/sec. If the configured streams exceed the maximum rate, the sending rate of each stream is automatically decreased. If this is set to 0 a value of half of the theoretical maximum bandwidth is used. This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on 8N1-configured links). + Configure the maximum sending rate for the MAVLink streams in Bytes/sec. +If the configured streams exceed the maximum rate, the sending rate of +each stream is automatically decreased. +If this is set to 0 a value of half of the theoretical maximum bandwidth is used. +This corresponds to baudrate/20 Bytes/s (baudrate/10 = maximum data rate on +8N1-configured links). 0 B/s True MAVLink Remote Port for instance 2 - If ethernet enabled and selected as configuration for MAVLink instance 2, selected remote port will be set and used in MAVLink instance 2. + If ethernet enabled and selected as configuration for MAVLink instance 2, +selected remote port will be set and used in MAVLink instance 2. True MAVLink Network Port for instance 2 - If ethernet enabled and selected as configuration for MAVLink instance 2, selected udp port will be set and used in MAVLink instance 2. + If ethernet enabled and selected as configuration for MAVLink instance 2, +selected udp port will be set and used in MAVLink instance 2. True @@ -16484,34 +8682,49 @@ Forward external setpoint messages - If set to 1 incoming external setpoint messages will be directly forwarded to the controllers if in offboard control mode + If set to 1 incoming external setpoint messages will be directly forwarded +to the controllers if in offboard control mode Parameter hash check - Disabling the parameter hash check functionality will make the mavlink instance stream parameters continuously. + Disabling the parameter hash check functionality will make the mavlink instance +stream parameters continuously. Heartbeat message forwarding - The mavlink heartbeat message will not be forwarded if this parameter is set to 'disabled'. The main reason for disabling heartbeats to be forwarded is because they confuse dronekit. + The mavlink heartbeat message will not be forwarded if this parameter is set to 'disabled'. +The main reason for disabling heartbeats to be forwarded is because they confuse dronekit. - + MAVLink protocol version - Default to 1, switch to 2 if GCS sends version 2 - Always use version 1 - Always use version 2 + Version 1 with auto-upgrade to v2 if detected + Version 2 Timeout in seconds for the RADIO_STATUS reports coming in - If the connected radio stops reporting RADIO_STATUS for a certain time, a warning is triggered and, if MAV_X_RADIO_CTL is enabled, the software-flow control is reset. + If the connected radio stops reporting RADIO_STATUS for a certain time, +a warning is triggered and, if MAV_X_RADIO_CTL is enabled, the software-flow +control is reset. 1 250 s + + MAVLink protocol signing + + Message signing disabled + Signing enabled except on USB + Signing always enabled + + MAVLink SiK Radio ID - When non-zero the MAVLink app will attempt to configure the SiK radio to this ID and re-set the parameter to 0. If the value is negative it will reset the complete radio config to factory defaults. Only applies if this mavlink instance is going through a SiK radio + When non-zero the MAVLink app will attempt to configure the +SiK radio to this ID and re-set the parameter to 0. If the value +is negative it will reset the complete radio config to +factory defaults. Only applies if this mavlink instance is going through a SiK radio -1 240 @@ -16521,6 +8734,25 @@ 250 true + + Enable MAVLink forwarding on TELEM2 + TELEM2 on Skynode only. + True + + + MAVLink Mode for SOM to FMU communication channel + The MAVLink Mode defines the set of streamed messages (for example the +vehicle's attitude) and their sending rates. + True + + Normal + Onboard + Config + Minimal + Onboard Low Bandwidth + Low Bandwidth + + MAVLink airframe type 0 @@ -16551,50 +8783,17 @@ If set to 1 incoming HIL GPS messages are parsed. - - - BMM350 data averaging - Defines which averaging mode to use during data polling. - True - - No averaging - 2 sample averaging - 4 sample averaging - 8 sample averaging - - - - BMM350 pad drive strength setting - This setting helps avoid signal problems like overshoot or undershoot. - 0 - 7 - - - BMM350 ODR rate - Defines which ODR rate to use during data polling. - True - - 400 Hz - 200 Hz - 100 Hz - 50 Hz - 25 Hz - 12.5 Hz - 6.25 Hz - 3.125 Hz - 1.5625 Hz - - - Enable online mag bias calibration - This enables continuous calibration of the magnetometers before takeoff using gyro data. + This enables continuous calibration of the magnetometers +before takeoff using gyro data. true Mag bias estimator learning gain - Increase to make the estimator more responsive Decrease to make the estimator more robust to noise + Increase to make the estimator more responsive +Decrease to make the estimator more robust to noise 0.1 100 1 @@ -16604,11 +8803,26 @@ Enable arm/disarm stick gesture - This determines if moving the left stick to the lower right arms and to the lower left disarms the vehicle. + This determines if moving the left stick to the lower right +arms and to the lower left disarms the vehicle. + + + Deadzone for sticks (only specific use cases) + Range around stick center ignored to prevent +vehicle drift from stick hardware inaccuracy. +Does not apply to any precise constant input like +throttle and attitude or rate piloting. + 0 + 1 + 2 + 0.01 Trigger time for kill stick gesture - The timeout for holding the left stick to the lower left and the right stick to the lower right at the same time until the gesture kills the actuators one-way. A negative value disables the feature. + The timeout for holding the left stick to the lower left +and the right stick to the lower right at the same time until the gesture +kills the actuators one-way. +A negative value disables the feature. -1 15 s @@ -16616,32 +8830,24 @@ - - GPS failure loiter time - The time the system should do open loop loiter and wait for GPS recovery before it starts descending. Set to 0 to disable. Roll angle is set to FW_GPSF_R. Does only apply for fixed-wing vehicles or VTOLs with NAV_FORCE_VT set to 0. - 0 - 3600 - s - - - GPS failure fixed roll angle - Roll angle in GPS failure loiter mode. - 0.0 - 30.0 - deg - 1 - 0.5 - Timeout to allow the payload to execute the mission command - Ensure: gripper: NAV_CMD_DO_GRIPPER has released before continuing mission. winch: CMD_DO_WINCH has delivered before continuing mission. gimbal: CMD_DO_GIMBAL_MANAGER_PITCHYAW has reached the commanded orientation before beginning to take pictures. + Ensure: +gripper: NAV_CMD_DO_GRIPPER +has released before continuing mission. +winch: CMD_DO_WINCH +has delivered before continuing mission. +gimbal: CMD_DO_GIMBAL_MANAGER_PITCHYAW +has reached the commanded orientation before beginning to take pictures. 0 s 1 Maximal horizontal distance from Home to first waypoint - There will be a warning message if the current waypoint is more distant than MIS_DIST_1WP from Home. Has no effect on mission validity. Set a value of zero or less to disable. + There will be a warning message if the current waypoint is more distant than MIS_DIST_1WP from Home. +Has no effect on mission validity. +Set a value of zero or less to disable. -1 100000 m @@ -16650,13 +8856,16 @@ Landing abort min altitude - Minimum altitude above landing point that the vehicle will climb to after an aborted landing. Then vehicle will loiter in this altitude until further command is received. Only applies to fixed-wing vehicles. + Minimum altitude above landing point that the vehicle will climb to after an aborted landing. +Then vehicle will loiter in this altitude until further command is received. +Only applies to fixed-wing vehicles. 0 m Enable yaw control of the mount. (Only affects multicopters and ROI mission items) - If enabled, yaw commands will be sent to the mount and the vehicle will follow its heading towards the flight direction. If disabled, the vehicle will yaw towards the ROI. + If enabled, yaw commands will be sent to the mount and the vehicle will follow its heading towards the flight direction. +If disabled, the vehicle will yaw towards the ROI. 0 1 @@ -16666,7 +8875,8 @@ Default take-off altitude - This is the relative altitude the system will take off to if not otherwise specified. + This is the relative altitude the system will take off to +if not otherwise specified. 0 m 1 @@ -16674,7 +8884,8 @@ Mission takeoff/landing required - Specifies if a mission has to contain a takeoff and/or mission landing. Validity of configured takeoffs/landings is checked independently of the setting here. + Specifies if a mission has to contain a takeoff and/or mission landing. +Validity of configured takeoffs/landings is checked independently of the setting here. No requirements Require a takeoff @@ -16694,7 +8905,10 @@ Time in seconds we wait on reaching target heading at a waypoint if it is forced - If set > 0 it will ignore the target heading for normal waypoint acceptance. If the waypoint forces the heading the timeout will matter. For example on VTOL forwards transition. Mainly useful for VTOLs that have less yaw authority and might not reach target yaw in wind. Disabled by default. + If set > 0 it will ignore the target heading for normal waypoint acceptance. If the +waypoint forces the heading the timeout will matter. For example on VTOL forwards transition. +Mainly useful for VTOLs that have less yaw authority and might not reach target +yaw in wind. Disabled by default. -1 20 s @@ -16716,7 +8930,8 @@ Acceptance Radius - Default acceptance radius, overridden by acceptance radius of waypoint if set. For fixed wing the npfg switch distance is used for horizontal acceptance. + Default acceptance radius, overridden by acceptance radius of waypoint if set. +For fixed wing the npfg switch distance is used for horizontal acceptance. 0.05 200.0 m @@ -16728,7 +8943,8 @@ FW Altitude Acceptance Radius before a landing - Altitude acceptance used for the last waypoint before a fixed-wing landing. This is usually smaller than the standard vertical acceptance because close to the ground higher accuracy is required. + Altitude acceptance used for the last waypoint before a fixed-wing landing. This is usually smaller +than the standard vertical acceptance because close to the ground higher accuracy is required. 0.05 200.0 m @@ -16745,9 +8961,11 @@ Loiter radius (FW only) - Default value of loiter radius in FW mode (e.g. for Loiter mode). - 25 - 1000 + Default value of loiter radius in fixed-wing mode (e.g. for Loiter mode). +The direction of the loiter can be set via the sign: A positive value for +clockwise, negative for counter-clockwise. + -10000 + 10000 m 1 0.5 @@ -16763,7 +8981,11 @@ Minimum height above ground during Mission and RTL - Minimum height above ground the vehicle is allowed to descend to during Mission and RTL, excluding landing commands. Requires a distance sensor to be set up. Note: only prevents the vehicle from descending further, but does not force it to climb. Set to a negative value to disable. + Minimum height above ground the vehicle is allowed to descend to during Mission and RTL, +excluding landing commands. +Requires a distance sensor to be set up. +Note: only prevents the vehicle from descending further, but does not force it to climb. +Set to a negative value to disable. -1 m 1 @@ -16771,7 +8993,10 @@ Minimum Loiter altitude - This is the minimum altitude above Home the system will always obey in Loiter (Hold) mode if switched into this mode without specifying an altitude (e.g. through Loiter switch on RC). Doesn't affect Loiters that are part of Missions or that are entered through a reposition setpoint ("Go to"). Set to a negative value to disable. + This is the minimum altitude above Home the system will always obey in Loiter (Hold) mode if switched into this +mode without specifying an altitude (e.g. through Loiter switch on RC). +Doesn't affect Loiters that are part of Missions or that are entered through a reposition setpoint ("Go to"). +Set to a negative value to disable. -1 m 1 @@ -16779,7 +9004,8 @@ Set traffic avoidance mode - Enabling this will allow the system to respond to transponder data from e.g. ADSB transponders + Enabling this will allow the system to respond +to transponder data from e.g. ADSB transponders Disabled Warn only @@ -16802,7 +9028,8 @@ Estimated time until collision - Minimum acceptable time until collsion. Assumes constant speed over 3d distance. + Minimum acceptable time until collsion. +Assumes constant speed over 3d distance. 1 900000000 s @@ -16811,7 +9038,11 @@ Multicopter air-mode - The air-mode enables the mixer to increase the total thrust of the multirotor in order to keep attitude and rate control even at low and high throttle. This function should be disabled during tuning as it will help the controller to diverge if the closed-loop is unstable (i.e. the vehicle is not tuned yet). Enabling air-mode for yaw requires the use of an arming switch. + The air-mode enables the mixer to increase the total thrust of the multirotor +in order to keep attitude and rate control even at low and high throttle. +This function should be disabled during tuning as it will help the controller +to diverge if the closed-loop is unstable (i.e. the vehicle is not tuned yet). +Enabling air-mode for yaw requires the use of an arming switch. Disabled Roll/Pitch @@ -16819,23 +9050,17 @@ - - - Custom configuration for ModalAI drones - This can be set to indicate that drone behavior needs to be changed to match a custom setting - True - - Stabilize the mount - Set to true for servo gimbal, false for passthrough. This is required for a gimbal which is not capable of stabilizing itself and relies on the IMU's attitude estimation. - 0 - 2 + Set to true for servo gimbal, false for passthrough. +This is required for a gimbal which is not capable of stabilizing itself +and relies on the IMU's attitude estimation. Disable Stabilize all axis Stabilize yaw for absolute/lock mode. + Stabilize pitch for absolute/lock mode. @@ -16902,9 +9127,23 @@ Mavlink System ID of the mount If MNT_MODE_OUT is MAVLink gimbal protocol v1, mount configure/control commands will be sent with this target ID. + + Max positive angle of pitch setpoint (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). + deg + 1 + + + Min negative angle of pitch setpoint (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). + deg + 1 + Mount input mode - This is the protocol used between the ground station and the autopilot. Recommended is Auto, RC only or MAVLink gimbal protocol v2. The rest will be deprecated. + This is the protocol used between the ground station and the autopilot. +Recommended is Auto, RC only or MAVLink gimbal protocol v2. +The rest will be deprecated. -1 4 true @@ -16919,7 +9158,8 @@ Mount output mode - This is the protocol used between the autopilot and a connected gimbal. Recommended is the MAVLink gimbal protocol v2 if the gimbal supports it. + This is the protocol used between the autopilot and a connected gimbal. +Recommended is the MAVLink gimbal protocol v2 if the gimbal supports it. 0 2 true @@ -16929,43 +9169,17 @@ MAVLink gimbal protocol v2 - - Offset for pitch channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Offset for roll channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Offset for yaw channel output in degrees - -360.0 - 360.0 - deg - 1 - - - Range of pitch channel output in degrees (only in AUX output mode) - 1.0 - 720.0 - deg - 1 - - Range of roll channel output in degrees (only in AUX output mode) + Range of roll channel output (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). Note that only symmetric angular ranges are supported. 1.0 720.0 deg 1 - Range of yaw channel output in degrees (only in AUX output mode) + Range of yaw channel output (only in MNT_MODE_OUT=AUX) + Use output driver settings to calibrate (e.g. PWM_CENT/_MIN/_MAX). Note that only symmetric angular ranges are supported. 1.0 720.0 deg @@ -16994,18 +9208,29 @@ Angular rate + + Alpha filter time constant defining the convergence rate (in seconds) for open-loop AUX mount control + Use when no angular position feedback is available. +With MNT_MODE_OUT set to AUX, the mount operates in open-loop and directly commands the servo output. +Parameters must be tuned for the specific servo to approximate its speed and response. + 0.0 + Acro mode roll, pitch expo factor - Exponential factor for tuning the input curve shape. 0 Purely linear input curve 1 Purely cubic input curve + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 1 2 Acro mode yaw expo factor - Exponential factor for tuning the input curve shape. 0 Purely linear input curve 1 Purely cubic input curve + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 1 2 @@ -17030,14 +9255,20 @@ Acro mode roll, pitch super expo factor - "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO. 0 Pure Expo function 0.7 reasonable shape enhancement for intuitive stick feel 0.95 very strong bent input curve only near maxima have effect + "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 0.95 2 Acro mode yaw super expo factor - "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO_Y. 0 Pure Expo function 0.7 reasonable shape enhancement for intuitive stick feel 0.95 very strong bent input curve only near maxima have effect + "Superexponential" factor for refining the input curve shape tuned using MC_ACRO_EXPO_Y. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 0.95 2 @@ -17055,14 +9286,18 @@ Max pitch rate - Limit for pitch rate in manual and auto modes (except acro). Has effect for large rotations in autonomous mode, to avoid large control output and mixer saturation. This is not only limited by the vehicle's properties, but also by the maximum measurement rate of the gyro. + Limit for pitch rate in manual and auto modes (except acro). +Has effect for large rotations in autonomous mode, to avoid large control +output and mixer saturation. +This is not only limited by the vehicle's properties, but also by the maximum +measurement rate of the gyro. 0.0 1800.0 deg/s 1 5 - + Pitch P gain Pitch proportional gain, i.e. desired angular speed in rad/s for error 1 rad. 0.0 @@ -17072,14 +9307,18 @@ Max roll rate - Limit for roll rate in manual and auto modes (except acro). Has effect for large rotations in autonomous mode, to avoid large control output and mixer saturation. This is not only limited by the vehicle's properties, but also by the maximum measurement rate of the gyro. + Limit for roll rate in manual and auto modes (except acro). +Has effect for large rotations in autonomous mode, to avoid large control +output and mixer saturation. +This is not only limited by the vehicle's properties, but also by the maximum +measurement rate of the gyro. 0.0 1800.0 deg/s 1 5 - + Roll P gain Roll proportional gain, i.e. desired angular speed in rad/s for error 1 rad. 0.0 @@ -17105,24 +9344,30 @@ Yaw weight - A fraction [0,1] deprioritizing yaw compared to roll and pitch in non-linear attitude control. Deprioritizing yaw is necessary because multicopters have much less control authority in yaw compared to the other axes and it makes sense because yaw is not critical for stable hovering or 3D navigation. For yaw control tuning use MC_YAW_P. This ratio has no impact on the yaw gain. + A fraction [0,1] deprioritizing yaw compared to roll and pitch in non-linear attitude control. +Deprioritizing yaw is necessary because multicopters have much less control authority +in yaw compared to the other axes and it makes sense because yaw is not critical for +stable hovering or 3D navigation. +For yaw control tuning use MC_YAW_P. This ratio has no impact on the yaw gain. 0.0 1.0 2 0.1 - + Maximum yaw acceleration in autonomous modes - Limits the acceleration of the yaw setpoint to avoid large control output and mixer saturation. + Limits the acceleration of the yaw setpoint to avoid large +control output and mixer saturation. 5 360 deg/s^2 0 5 - + Maximum yaw rate in autonomous modes - Limits the rate of change of the yaw setpoint to avoid large control output and mixer saturation. + Limits the rate of change of the yaw setpoint to avoid large +control output and mixer saturation. 5 360 deg/s @@ -17166,7 +9411,9 @@ Acceleration to tilt coupling - Set to decouple tilt from vertical acceleration. This provides smoother flight but slightly worse tracking in position and auto modes. Unset if accurate position tracking during dynamic maneuvers is more important than a smooth flight. + Set to decouple tilt from vertical acceleration. +This provides smoother flight but slightly worse tracking in position and auto modes. +Unset if accurate position tracking during dynamic maneuvers is more important than a smooth flight. Maximum downwards acceleration in climb rate controlled modes @@ -17187,7 +9434,9 @@ Maximum horizontal acceleration - MPC_POS_MODE 1 just deceleration 4 not used, use MPC_ACC_HOR instead + MPC_POS_MODE +1 just deceleration +4 not used, use MPC_ACC_HOR instead 2 15 m/s^2 @@ -17204,7 +9453,13 @@ Altitude reference mode - Set to 0 to control height relative to the earth frame origin. This origin may move up and down in flight due to sensor drift. Set to 1 to control height relative to estimated distance to ground. The vehicle will move up and down with terrain height variation. Requires a distance to ground sensor. The height controller will revert to using height above origin if the distance to ground estimate becomes invalid as indicated by the local_position.distance_bottom_valid message being false. Set to 2 to control height relative to ground (requires a distance sensor) when stationary and relative to earth frame origin when moving horizontally. The speed threshold is controlled by the MPC_HOLD_MAX_XY parameter. + Control height +0: relative earth frame origin which may drift due to sensors +1: relative to ground (requires distance sensor) which changes with terrain variation. +It will revert to relative earth frame if the distance to ground estimate becomes invalid. +2: relative to ground (requires distance sensor) when stationary +and relative to earth frame when moving horizontally. +The speed threshold is MPC_HOLD_MAX_XY 0 2 @@ -17213,14 +9468,6 @@ Terrain hold - - Deadzone for sticks in manual piloted modes - Does not apply to manual throttle and direct attitude piloting by stick. - 0 - 1 - 2 - 0.01 - Maximum horizontal velocity for which position hold is enabled (use 0 to disable check) Only used with MPC_POS_MODE Direct velocity or MPC_ALT_MODE 2 @@ -17239,7 +9486,8 @@ Jerk limit in autonomous modes - Limit the maximum jerk of the vehicle (how fast the acceleration can change). A lower value leads to smoother vehicle motions but also limited agility. + Limit the maximum jerk of the vehicle (how fast the acceleration can change). +A lower value leads to smoother vehicle motions but also limited agility. 1 80 m/s^3 @@ -17248,7 +9496,10 @@ Maximum horizontal and vertical jerk in Position/Altitude mode - Limit the maximum jerk of the vehicle (how fast the acceleration can change). A lower value leads to smoother motions but limits agility (how fast it can change directions or break). Setting this to the maximum value essentially disables the limit. Only used with MPC_POS_MODE Acceleration based. + Limit the maximum jerk (acceleration change) of the vehicle. +A lower value leads to smoother motions but limits agility. +Setting this to the maximum value essentially disables the limit. +Only used with MPC_POS_MODE Acceleration based. 0.5 500 m/s^3 @@ -17257,7 +9508,9 @@ Altitude for 1. step of slow landing (descend) - Below this altitude descending velocity gets limited to a value between "MPC_Z_VEL_MAX_DN" (or "MPC_Z_V_AUTO_DN") and "MPC_LAND_SPEED" Value needs to be higher than "MPC_LAND_ALT2" + Below this altitude descending velocity gets limited to a value +between "MPC_Z_VEL_MAX_DN" (or "MPC_Z_V_AUTO_DN") and "MPC_LAND_SPEED" +Value needs to be higher than "MPC_LAND_ALT2" 0 122 m @@ -17265,7 +9518,9 @@ Altitude for 2. step of slow landing (landing) - Below this altitude descending velocity gets limited to "MPC_LAND_SPEED" Value needs to be lower than "MPC_LAND_ALT1" + Below this altitude descending velocity gets +limited to "MPC_LAND_SPEED" +Value needs to be lower than "MPC_LAND_ALT1" 0 122 m @@ -17273,7 +9528,8 @@ Altitude for 3. step of slow landing - Below this altitude descending velocity gets limited to "MPC_LAND_CRWL", if LIDAR available. No effect if LIDAR not available + If a valid distance sensor measurement to the ground is available, +limit descending velocity to "MPC_LAND_CRWL" below this altitude. 0 122 m @@ -17286,17 +9542,26 @@ m/s 1 - + User assisted landing radius - When nudging is enabled (see MPC_LAND_RC_HELP), this controls the maximum allowed horizontal displacement from the original landing point. - 0 + When nudging is enabled (see MPC_LAND_RC_HELP), this defines the maximum +allowed horizontal displacement from the original landing point. +- If inside of the radius, only allow nudging inputs that do not move the vehicle outside of it. +- If outside of the radius, only allow nudging inputs that move the vehicle back towards it. +Set it to -1 for infinite radius. + -1 m 0 1 Enable nudging based on user input during autonomous land routine - Using stick input the vehicle can be moved horizontally and yawed. The descend speed is amended: stick full up - 0 stick centered - MPC_LAND_SPEED stick full down - 2 * MPC_LAND_SPEED Manual override during auto modes has to be disabled to use this feature (see COM_RC_OVERRIDE). + Using stick input the vehicle can be moved horizontally and yawed. +The descend speed is amended: +stick full up - 0 +stick centered - MPC_LAND_SPEED +stick full down - 2 * MPC_LAND_SPEED +Manual override during auto modes has to be disabled to use this feature (see COM_RC_OVERRIDE). 0 1 @@ -17312,7 +9577,9 @@ Minimum collective thrust in Stabilized mode - The value is mapped to the lowest throttle stick position in Stabilized mode. Too low collective thrust leads to loss of roll/pitch/yaw torque control authority. Airmode is used to keep torque authority with zero thrust (see MC_AIRMODE). + The value is mapped to the lowest throttle stick position in Stabilized mode. +Too low collective thrust leads to loss of roll/pitch/yaw torque control authority. +Airmode is used to keep torque authority with zero thrust (see MC_AIRMODE). 0 1 norm @@ -17320,7 +9587,7 @@ 0.01 - Maximal tilt angle in Stabilized or Altitude mode + Maximal tilt angle in Stabilized, Altitude and Altitude Cruise mode 1 70 deg @@ -17337,7 +9604,8 @@ Manual yaw rate input filter time constant - Not used in Stabilized mode Setting this parameter to 0 disables the filter + Not used in Stabilized mode +Setting this parameter to 0 disables the filter 0 5 s @@ -17346,7 +9614,13 @@ Position/Altitude mode variant - The supported sub-modes are: - "Direct velocity": Sticks directly map to velocity setpoints without smoothing. Also applies to vertical direction and Altitude mode. Useful for velocity control tuning. - "Acceleration based": Sticks map to acceleration and there's a virtual brake drag + The supported sub-modes are: +Direct velocity: +Sticks directly map to velocity setpoints without smoothing. +Also applies to vertical direction and Altitude mode. +Useful for velocity control tuning. +Acceleration based: +Sticks map to acceleration and there's a virtual brake drag Direct velocity Acceleration based @@ -17354,15 +9628,27 @@ Thrust curve mapping in Stabilized Mode - This parameter defines how the throttle stick input is mapped to collective thrust in Stabilized mode. In case the default is used ('Rescale to hover thrust'), the stick input is linearly rescaled, such that a centered stick corresponds to the hover throttle (see MPC_THR_HOVER). Select 'No Rescale' to directly map the stick 1:1 to the output. This can be useful in case the hover thrust is very low and the default would lead to too much distortion (e.g. if hover thrust is set to 20%, then 80% of the upper thrust range is squeezed into the upper half of the stick range). Note: In case MPC_THR_HOVER is set to 50%, the modes 0 and 1 are the same. + Defines how the throttle stick is mapped to collective thrust in Stabilized mode. +Rescale to hover thrust estimate: +Stick input is linearly rescaled, such that a centered throttle stick corresponds to the hover thrust estimator's output. +No rescale: +Directly map the stick 1:1 to the output. +Can be useful with very low hover thrust which leads to much distortion and the upper half getting sensitive. +Rescale to hover thrust parameter: +Similar to rescaling to the hover thrust estimate, but it uses the hover thrust parameter value (see MPC_THR_HOVER) instead of estimated value. +With MPC_THR_HOVER 0.5 it's equivalent to No rescale. - Rescale to hover thrust - No Rescale + Rescale to estimate + No rescale + Rescale to parameter Vertical thrust required to hover - Mapped to center throttle stick in Stabilized mode (see MPC_THR_CURVE). Used for initialization of the hover thrust estimator (see MPC_USE_HTE). The estimated hover thrust is used as base for zero vertical acceleration in altitude control. The hover thrust is important for land detection to work correctly. + Mapped to center throttle stick in Stabilized mode (see MPC_THR_CURVE). +Used for initialization of the hover thrust estimator. +The estimated hover thrust is used as base for zero vertical acceleration in altitude control. +The hover thrust is important for land detection to work correctly. 0.1 0.8 norm @@ -17380,7 +9666,9 @@ Minimum collective thrust in climb rate controlled modes - Too low thrust leads to loss of roll/pitch/yaw torque control authority. With airmode enabled this parameters can be set to 0 while still keeping torque authority (see MC_AIRMODE). + Too low thrust leads to loss of roll/pitch/yaw torque control authority. +With airmode enabled this parameters can be set to 0 +while still keeping torque authority (see MC_AIRMODE). 0.05 0.5 norm @@ -17389,7 +9677,8 @@ Horizontal thrust margin - Margin that is kept for horizontal control when higher priority vertical thrust is saturated. To avoid completely starving horizontal control with high vertical error. + Margin that is kept for horizontal control when higher priority vertical thrust is saturated. +To avoid completely starving horizontal control with high vertical error. 0 0.5 norm @@ -17398,7 +9687,8 @@ Maximum tilt angle in air - Absolute maximum for all velocity or acceleration controlled modes. Any higher value is truncated. + Absolute maximum for all velocity or acceleration controlled modes. +Any higher value is truncated. 20 89 deg @@ -17416,7 +9706,9 @@ Smooth takeoff ramp time constant - Increasing this value will make climb rate controlled takeoff slower. If it's too slow the drone might scratch the ground and tip over. A time constant of 0 disables the ramp + Increasing this value will make climb rate controlled takeoff slower. +If it's too slow the drone might scratch the ground and tip over. +A time constant of 0 disables the ramp 0 5 s @@ -17428,10 +9720,6 @@ m/s 2 - - Hover thrust estimator - Disable to use the fixed parameter MPC_THR_HOVER Enable to use the hover thrust estimator - Velocity derivative low pass cutoff frequency A value of 0 disables the filter. @@ -17452,7 +9740,9 @@ Maximum horizontal velocity setpoint in Position mode - Must be smaller than MPC_XY_VEL_MAX. The maximum sideways and backward speed can be set differently using MPC_VEL_MAN_SIDE and MPC_VEL_MAN_BACK, respectively. + Must be smaller than MPC_XY_VEL_MAX. +The maximum sideways and backward speed can be set differently +using MPC_VEL_MAN_SIDE and MPC_VEL_MAN_BACK, respectively. 3 20 m/s @@ -17461,7 +9751,8 @@ Maximum backward velocity in Position mode - If set to a negative value or larger than MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. + If set to a negative value or larger than +MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. -1 20 m/s @@ -17470,7 +9761,8 @@ Maximum sideways velocity in Position mode - If set to a negative value or larger than MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. + If set to a negative value or larger than +MPC_VEL_MANUAL then MPC_VEL_MANUAL is used. -1 20 m/s @@ -17488,7 +9780,8 @@ Velocity notch filter frequency - The center frequency for the 2nd order notch filter on the velocity. A value of 0 disables the filter. + The center frequency for the 2nd order notch filter on the velocity. +A value of 0 disables the filter. 0 50 Hz @@ -17506,20 +9799,17 @@ Maximum horizontal error allowed by the trajectory generator - The integration speed of the trajectory setpoint is linearly reduced with the horizontal position tracking error. When the error is above this parameter, the integration of the trajectory is stopped to wait for the drone. This value can be adjusted depending on the tracking capabilities of the vehicle. + The integration speed of the trajectory setpoint is linearly +reduced with the horizontal position tracking error. When the +error is above this parameter, the integration of the +trajectory is stopped to wait for the drone. +This value can be adjusted depending on the tracking +capabilities of the vehicle. 0.1 10 1 1 - - Manual position control stick exponential curve sensitivity - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - Proportional gain for horizontal position error Defined as corrective velocity in m/s per m position error @@ -17537,7 +9827,9 @@ Overall Horizontal Velocity Limit - If set to a value greater than zero, other parameters are automatically set (such as MPC_XY_VEL_MAX or MPC_VEL_MANUAL). If set to a negative value, the existing individual parameters are used. + If set to a value greater than zero, other parameters are automatically set (such as +MPC_XY_VEL_MAX or MPC_VEL_MANUAL). +If set to a negative value, the existing individual parameters are used. -20 20 1 @@ -17553,7 +9845,8 @@ Integral gain for horizontal velocity error - Defined as correction acceleration in m/s^2 per m velocity integral Allows to eliminate steady state errors in disturbances like wind. + Defined as correction acceleration in m/s^2 per m velocity integral +Allows to eliminate steady state errors in disturbances like wind. 0 60 2 @@ -17561,7 +9854,8 @@ Maximum horizontal velocity - Absolute maximum for all velocity controlled modes. Any higher value is truncated. + Absolute maximum for all velocity controlled modes. +Any higher value is truncated. 0 20 m/s @@ -17576,22 +9870,6 @@ 2 0.1 - - Manual control stick yaw rotation exponential curve - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - - - Manual control stick vertical exponential curve - The higher the value the less sensitivity the stick has around zero while still reaching the maximum value with full stick deflection. 0 Purely linear input curve 1 Purely cubic input curve - 0 - 1 - 2 - 0.01 - Proportional gain for vertical position error Defined as corrective velocity in m/s per m position error @@ -17602,7 +9880,9 @@ Overall Vertical Velocity Limit - If set to a value greater than zero, other parameters are automatically set (such as MPC_Z_VEL_MAX_UP or MPC_LAND_SPEED). If set to a negative value, the existing individual parameters are used. + If set to a value greater than zero, other parameters are automatically set (such as +MPC_Z_VEL_MAX_UP or MPC_LAND_SPEED). +If set to a negative value, the existing individual parameters are used. -3 8 1 @@ -17626,7 +9906,9 @@ Maximum descent velocity - Absolute maximum for all climb rate controlled modes. In manually piloted modes full stick deflection commands this velocity. For default autonomous velocity see MPC_Z_V_AUTO_UP + Absolute maximum for all climb rate controlled modes. +In manually piloted modes full stick deflection commands this velocity. +For default autonomous velocity see MPC_Z_V_AUTO_UP 0.5 4 m/s @@ -17635,7 +9917,9 @@ Maximum ascent velocity - Absolute maximum for all climb rate controlled modes. In manually piloted modes full stick deflection commands this velocity. For default autonomous velocity see MPC_Z_V_AUTO_UP + Absolute maximum for all climb rate controlled modes. +In manually piloted modes full stick deflection commands this velocity. +For default autonomous velocity see MPC_Z_V_AUTO_UP 0.5 8 m/s @@ -17670,7 +9954,11 @@ Responsiveness - Changes the overall responsiveness of the vehicle. The higher the value, the faster the vehicle will react. If set to a value greater than zero, other parameters are automatically set (such as the acceleration or jerk limits). If set to a negative value, the existing individual parameters are used. + Changes the overall responsiveness of the vehicle. +The higher the value, the faster the vehicle will react. +If set to a value greater than zero, other parameters are automatically set (such as +the acceleration or jerk limits). +If set to a negative value, the existing individual parameters are used. -1 1 2 @@ -17695,7 +9983,9 @@ Default horizontal velocity limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 0.1 m/s 2 @@ -17703,7 +9993,9 @@ Default vertical velocity limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 0.1 m/s 2 @@ -17711,7 +10003,9 @@ Default yaw rate limit - This value is used in slow mode if no aux channel is mapped and no limit is commanded through MAVLink. + This value is used in slow mode if +no aux channel is mapped and +no limit is commanded through MAVLink. 1 deg/s 0 @@ -17729,6 +10023,18 @@ AUX6 + + RC_MAP_AUX{N} to allow for gimbal pitch rate control in position slow mode + + No pitch rate input + AUX1 + AUX2 + AUX3 + AUX4 + AUX5 + AUX6 + + Manual input mapped to scale vertical velocity in position slow mode @@ -17781,7 +10087,11 @@ Battery power level scaler - This compensates for voltage drop of the battery over time by attempting to normalize performance across the operating range of the battery. The copter should constantly behave as if it was fully charged with reduced max acceleration at lower battery percentages. i.e. if hover is at 0.5 throttle at 100% battery, it will still be 0.5 at 60% battery. + This compensates for voltage drop of the battery over time by attempting to +normalize performance across the operating range of the battery. The copter +should constantly behave as if it was fully charged with reduced max acceleration +at lower battery percentages. i.e. if hover is at 0.5 throttle at 100% battery, +it will still be 0.5 at 60% battery. Pitch rate D gain @@ -17805,7 +10115,13 @@ Pitch rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_PITCHRATE_K * (MC_PITCHRATE_P * error + MC_PITCHRATE_I * error_integral + MC_PITCHRATE_D * error_derivative) Set MC_PITCHRATE_P=1 to implement a PID in the ideal form. Set MC_PITCHRATE_K=1 to implement a PID in the parallel form. + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_PITCHRATE_K * (MC_PITCHRATE_P * error ++ MC_PITCHRATE_I * error_integral ++ MC_PITCHRATE_D * error_derivative) +Set MC_PITCHRATE_P=1 to implement a PID in the ideal form. +Set MC_PITCHRATE_K=1 to implement a PID in the parallel form. 0.01 5.0 4 @@ -17849,7 +10165,13 @@ Roll rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_ROLLRATE_K * (MC_ROLLRATE_P * error + MC_ROLLRATE_I * error_integral + MC_ROLLRATE_D * error_derivative) Set MC_ROLLRATE_P=1 to implement a PID in the ideal form. Set MC_ROLLRATE_K=1 to implement a PID in the parallel form. + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_ROLLRATE_K * (MC_ROLLRATE_P * error ++ MC_ROLLRATE_I * error_integral ++ MC_ROLLRATE_D * error_derivative) +Set MC_ROLLRATE_P=1 to implement a PID in the ideal form. +Set MC_ROLLRATE_K=1 to implement a PID in the parallel form. 0.01 5.0 4 @@ -17874,27 +10196,32 @@ Yaw rate D gain Yaw rate differential gain. Small values help reduce fast oscillations. If value is too big oscillations will appear again. 0.0 - 2 - 0.01 + 4 + 0.0005 Yaw rate feedforward Improves tracking performance. 0.0 4 - 0.01 Yaw rate I gain Yaw rate integral gain. Can be set to compensate static thrust difference or gravity center offset. 0.0 - 2 + 3 0.01 Yaw rate controller gain - Global gain of the controller. This gain scales the P, I and D terms of the controller: output = MC_YAWRATE_K * (MC_YAWRATE_P * error + MC_YAWRATE_I * error_integral + MC_YAWRATE_D * error_derivative) Set MC_YAWRATE_P=1 to implement a PID in the ideal form. Set MC_YAWRATE_K=1 to implement a PID in the parallel form. - 0.0 + Global gain of the controller. +This gain scales the P, I and D terms of the controller: +output = MC_YAWRATE_K * (MC_YAWRATE_P * error ++ MC_YAWRATE_I * error_integral ++ MC_YAWRATE_D * error_derivative) +Set MC_YAWRATE_P=1 to implement a PID in the ideal form. +Set MC_YAWRATE_K=1 to implement a PID in the parallel form. + 0.01 5.0 4 0.0005 @@ -17904,9 +10231,18 @@ Yaw rate proportional gain, i.e. control output for angular speed error 1 rad/s. 0.0 0.6 - 2 + 3 0.01 + + Low pass filter cutoff frequency for yaw torque setpoint + Reduces vibrations by lowering high frequency torque caused by rotor acceleration. +0 disables the filter + 0 + 10 + Hz + 3 + Yaw rate integrator limit Yaw rate integrator limit. Can be set to increase the amount of integrator available to counteract disturbances or reduced to improve settling time after large yaw moment trim changes. @@ -17916,38 +10252,11 @@ - - Serial Configuration for MSP OSD - Configure on which serial port to run MSP OSD. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Enable/Disable the ATXXX OSD Chip - Configure the ATXXXX OSD Chip (mounted on the OmnibusF4SD board) and select the transmission standard. - true - - Disabled - NTSC - PAL - - OSD Crosshairs Height - Controls the vertical position of the crosshair display. Resolution is limited by OSD to 15 discrete values. Negative values will display the crosshairs below the horizon + Controls the vertical position of the crosshair display. +Resolution is limited by OSD to 15 discrete values. Negative +values will display the crosshairs below the horizon -8 8 @@ -17961,9 +10270,16 @@ OSD Warning Level Minimum security of log level to display on the OSD. + + OSD RC Stick commands + Forward RC stick input to VTX when disarmed + 0 + 1 + OSD Scroll Rate (ms) - Scroll rate in milliseconds for OSD messages longer than available character width. This is lower-bounded by the nominal loop rate of this module. + Scroll rate in milliseconds for OSD messages longer than available character width. +This is lower-bounded by the nominal loop rate of this module. 100 1000 @@ -17999,13 +10315,13 @@ - - S.BUS out - Set to 1 to enable S.BUS version 1 output instead of RSSI. - Thrust to motor control signal model parameter - Parameter used to model the nonlinear relationship between motor control signal (e.g. PWM) and static thrust. The model is: rel_thrust = factor * rel_signal^2 + (1-factor) * rel_signal, where rel_thrust is the normalized thrust between 0 and 1, and rel_signal is the relative motor control signal between 0 and 1. + Parameter used to model the nonlinear relationship between +motor control signal (e.g. PWM) and static thrust. +The model is: rel_thrust = factor * rel_signal^2 + (1-factor) * rel_signal, +where rel_thrust is the normalized thrust between 0 and 1, and +rel_signal is the relative motor control signal between 0 and 1. 0.0 1.0 1 @@ -18013,13 +10329,12 @@ - - Enable Gripper actuation in Payload Deliverer - True - - + Timeout for successful gripper actuation acknowledgement - Maximum time Gripper will wait while the successful griper actuation isn't recognised. If the gripper has no feedback sensor, it will simply wait for this time before considering gripper actuation successful and publish a 'VehicleCommandAck' signaling successful gripper action + Maximum time Gripper will wait while the successful griper actuation isn't recognised. +If the gripper has no feedback sensor, it will simply wait for +this time before considering gripper actuation successful and publish a +'VehicleCommandAck' signaling successful gripper action 0 s @@ -18087,7 +10402,7 @@ - + Tuning parameter for the pure pursuit controller Lower value -> More aggressive controller (beware overshoot/oscillations) 0.1 @@ -18095,7 +10410,7 @@ 2 0.01 - + Maximum lookahead distance for the pure pursuit controller 0.1 100 @@ -18103,7 +10418,7 @@ 2 0.01 - + Minimum lookahead distance for the pure pursuit controller 0.1 100 @@ -18112,42 +10427,7 @@ 0.01 - - - Crossfire RC telemetry enable - Crossfire telemetry enable - - - Ghost RC telemetry enable - Ghost telemetry enable - - - - - RC input protocol - Select your RC input protocol or auto to scan. - -1 - 7 - - Auto - None - PPM - SBUS - DSM - ST24 - SUMD - CRSF - GHST - - - - - RC channel 10 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 10 maximum Maximum value for this channel. @@ -18179,12 +10459,6 @@ 2200.0 us - - RC channel 11 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 11 maximum Maximum value for this channel. @@ -18216,12 +10490,6 @@ 2200.0 us - - RC channel 12 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 12 maximum Maximum value for this channel. @@ -18253,12 +10521,6 @@ 2200.0 us - - RC channel 13 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 13 maximum Maximum value for this channel. @@ -18290,12 +10552,6 @@ 2200.0 us - - RC channel 14 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 14 maximum Maximum value for this channel. @@ -18327,12 +10583,6 @@ 2200.0 us - - RC channel 15 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 15 maximum Maximum value for this channel. @@ -18364,12 +10614,6 @@ 2200.0 us - - RC channel 16 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 16 maximum Maximum value for this channel. @@ -18401,12 +10645,6 @@ 2200.0 us - - RC channel 17 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 17 maximum Maximum value for this channel. @@ -18438,12 +10676,6 @@ 2200.0 us - - RC channel 18 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 18 maximum Maximum value for this channel. @@ -18475,13 +10707,6 @@ 2200.0 us - - RC channel 1 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 1 maximum Maximum value for RC channel 1 @@ -18513,13 +10738,6 @@ 2200.0 us - - RC channel 2 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 2 maximum Maximum value for this channel. @@ -18551,13 +10769,6 @@ 2200.0 us - - RC channel 3 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 3 maximum Maximum value for this channel. @@ -18589,13 +10800,6 @@ 2200.0 us - - RC channel 4 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - us - RC channel 4 maximum Maximum value for this channel. @@ -18627,12 +10831,6 @@ 2200.0 us - - RC channel 5 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 5 maximum Maximum value for this channel. @@ -18664,12 +10862,6 @@ 2200.0 us - - RC channel 6 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 6 maximum Maximum value for this channel. @@ -18701,12 +10893,6 @@ 2200.0 us - - RC channel 7 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 7 maximum Maximum value for this channel. @@ -18738,12 +10924,6 @@ 2200.0 us - - RC channel 8 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 8 maximum Maximum value for this channel. @@ -18775,12 +10955,6 @@ 2200.0 us - - RC channel 9 dead zone - The +- range of this value around the trim value will be considered as zero. - 0.0 - 100.0 - RC channel 9 maximum Maximum value for this channel. @@ -18814,20 +10988,25 @@ RC channel count - This parameter is used by Ground Station software to save the number of channels which were used during RC calibration. It is only meant for ground station use. + This parameter is used by Ground Station software to save the number +of channels which were used during RC calibration. It is only meant +for ground station use. 0 18 Failsafe channel PWM threshold - Use RC_MAP_FAILSAFE to specify which channel is used to indicate RC loss via this threshold. By default this is the throttle channel. Set to a PWM value slightly above the PWM value for the channel (e.g. throttle) in a failsafe event, but below the minimum PWM value for the channel during normal operation. Note: The default value of 0 disables the feature (it is below the expected range). + Use RC_MAP_FAILSAFE to specify which channel is used to indicate RC loss via this threshold. +By default this is the throttle channel. +Set to a PWM value slightly above the PWM value for the channel (e.g. throttle) in a failsafe event, +but below the minimum PWM value for the channel during normal operation. +Note: The default value of 0 disables the feature (it is below the expected range). 0 2200 us AUX1 Passthrough RC channel - Default function: Camera pitch 0 18 @@ -18854,7 +11033,6 @@ AUX2 Passthrough RC channel - Default function: Camera roll 0 18 @@ -18881,7 +11059,6 @@ AUX3 Passthrough RC channel - Default function: Camera azimuth / yaw 0 18 @@ -19012,7 +11189,11 @@ Failsafe channel mapping - Configures which RC channel is used by the receiver to indicate the signal was lost (on receivers that use output a fixed signal value to report lost signal). If set to 0, the channel mapped to throttle is used. Use RC_FAILS_THR to set the threshold indicating lost signal. By default it's below the expected range and hence disabled. + Configures which RC channel is used by the receiver to indicate the signal was lost +(on receivers that use output a fixed signal value to report lost signal). +If set to 0, the channel mapped to throttle is used. +Use RC_FAILS_THR to set the threshold indicating lost signal. By default it's below +the expected range and hence disabled. 0 18 @@ -19039,7 +11220,8 @@ PARAM1 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 1st parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 1st parameter channel. +Set to 0 to deactivate * 0 18 @@ -19066,7 +11248,8 @@ PARAM2 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 2nd parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 2nd parameter channel. +Set to 0 to deactivate * 0 18 @@ -19093,7 +11276,8 @@ PARAM3 tuning channel - Can be used for parameter tuning with the RC. This one is further referenced as the 3th parameter channel. Set to 0 to deactivate * + Can be used for parameter tuning with the RC. This one is further referenced as the 3th parameter channel. +Set to 0 to deactivate * 0 18 @@ -19120,7 +11304,9 @@ Pitch control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading pitch inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading pitch inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19147,7 +11333,9 @@ Roll control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading roll inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading roll inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19174,7 +11362,9 @@ Throttle control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading throttle inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading throttle inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19201,7 +11391,9 @@ Yaw control channel mapping - The channel index (starting from 1 for channel 1) indicates which channel should be used for reading yaw inputs from. A value of zero indicates the switch is not assigned. + The channel index (starting from 1 for channel 1) indicates +which channel should be used for reading yaw inputs from. +A value of zero indicates the switch is not assigned. 0 18 @@ -19228,7 +11420,9 @@ PWM input channel that provides RSSI - 0: do not read RSSI from input channel 1-18: read RSSI from specified input channel Specify the range for RSSI input with RC_RSSI_PWM_MIN and RC_RSSI_PWM_MAX parameters. + 0: do not read RSSI from input channel +1-18: read RSSI from specified input channel +Specify the range for RSSI input with RC_RSSI_PWM_MIN and RC_RSSI_PWM_MAX parameters. 0 18 @@ -19267,7 +11461,8 @@ Pitch trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19275,7 +11470,8 @@ Roll trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19283,7 +11479,8 @@ Yaw trim - The trim value is the actuator control value the system needs for straight and level flight. + The trim value is the actuator control value the system needs +for straight and level flight. -0.5 0.5 2 @@ -19293,37 +11490,68 @@ Threshold for the arm switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting main motor engage - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the landing gear switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the kill switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting loiter mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Arm switch channel - Use it to arm/disarm via switch instead of default throttle stick. If this is assigned, arming and disarming via stick is disabled. + Use it to arm/disarm via switch instead of default throttle stick. If this is +assigned, arming and disarming via stick is disabled. 0 18 @@ -19376,7 +11604,8 @@ Single channel flight mode selection - If this parameter is non-zero, flight modes are only selected by this channel and are assigned to six slots. + If this parameter is non-zero, flight modes are only selected +by this channel and are assigned to six slots. 0 18 @@ -19403,7 +11632,11 @@ Button flight mode selection - This bitmask allows to specify multiple channels for changing flight modes using momentary buttons. Each channel is assigned to a mode slot ((lowest channel = slot 1). The resulting modes for each slot X is defined by the COM_FLTMODEX parameters. The functionality can be used only if RC_MAP_FLTMODE is disabled. The maximum number of available slots and hence bits set in the mask is 6. + This bitmask allows to specify multiple channels for changing flight modes using +momentary buttons. Each channel is assigned to a mode slot ((lowest channel = slot 1). +The resulting modes for each slot X is defined by the COM_FLTMODEX parameters. +The functionality can be used only if RC_MAP_FLTMODE is disabled. +The maximum number of available slots and hence bits set in the mask is 6. 0 258048 @@ -19455,6 +11688,9 @@ Emergency Kill switch channel + This channel immediately sets all outputs to their disarmed values, parachutes are NOT deployed. +Unlike termination this can be undone. Quickly flipping the switch back restores control. +System auto-disarms after COM_KILL_DISARM seconds, preflight checks and re-arming are then required. 0 18 @@ -19507,7 +11743,10 @@ Mode switch channel mapping (deprecated) - This is the main flight mode selector. The channel index (starting from 1 for channel 1) indicates which channel should be used for deciding about the main mode. A value of zero indicates the switch is not assigned. + This is the main flight mode selector. +The channel index (starting from 1 for channel 1) indicates +which channel should be used for deciding about the main mode. +A value of zero indicates the switch is not assigned. 0 18 @@ -19610,6 +11849,36 @@ Channel 18 + + Termination switch channel + This channel triggers irreversible flight termination: +All outputs are disabled and set to their failsafe values (disarmed by default) +and MAVLink parachutes are triggered. +Unlike a kill switch, this cannot be undone until system reboot. Use with caution. + 0 + 18 + + Unassigned + Channel 1 + Channel 2 + Channel 3 + Channel 4 + Channel 5 + Channel 6 + Channel 7 + Channel 8 + Channel 9 + Channel 10 + Channel 11 + Channel 12 + Channel 13 + Channel 14 + Channel 15 + Channel 16 + Channel 17 + Channel 18 + + VTOL transition switch channel mapping 0 @@ -19638,33 +11907,70 @@ Threshold for selecting offboard mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th + -1 + 1 + 2 + + + Threshold for mid position of payload power switch + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 - Threshold for selecting payload power switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + Threshold for on position of payload power switch + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for selecting return to launch mode - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Threshold for the VTOL transition switch - 0-1 indicate where in the full channel range the threshold sits 0 : min 1 : max sign indicates polarity of comparison positive : true when channel>th negative : true when channel<th + 0-1 indicate where in the full channel range the threshold sits +0 : min +1 : max +sign indicates polarity of comparison +positive : true when channel>th +negative : true when channel<th -1 1 + 2 Half-angle of the return mode altitude cone - Defines the half-angle of a cone centered around the destination position that affects the altitude at which the vehicle returns. + Defines the half-angle of a cone centered around the destination position that +affects the altitude at which the vehicle returns. 0 90 deg @@ -19679,7 +11985,9 @@ Return mode loiter altitude - Descend to this altitude (above destination position) after return, and wait for time defined in RTL_LAND_DELAY. Land (i.e. slowly descend) from this altitude if autolanding allowed. VTOLs do transition to hover in this altitdue above the landing point. + Descend to this altitude (above destination position) after return, and wait for time defined in RTL_LAND_DELAY. +Land (i.e. slowly descend) from this altitude if autolanding allowed. +VTOLs do transition to hover in this altitdue above the landing point. 0 m 1 @@ -19687,7 +11995,8 @@ Return mode delay - Delay before landing (after initial descent) in Return mode. If set to -1 the system will not land but loiter at RTL_DESCEND_ALT. + Delay before landing (after initial descent) in Return mode. +If set to -1 the system will not land but loiter at RTL_DESCEND_ALT. -1 s 1 @@ -19703,7 +12012,8 @@ Horizontal radius from return point within which special rules for return mode apply - The return altitude will be calculated based on RTL_CONE_ANG parameter. The yaw setpoint will switch to the one defined by corresponding waypoint. + The return altitude will be calculated based on RTL_CONE_ANG parameter. +The yaw setpoint will switch to the one defined by corresponding waypoint. 0.5 m 1 @@ -19711,7 +12021,8 @@ RTL precision land mode - Use precision landing when doing an RTL landing phase. + Use precision landing when doing an RTL landing phase. +This setting does not apply for RTL destinations planned as part of a mission. No precision landing Opportunistic precision landing @@ -19719,316 +12030,119 @@ - Return mode return altitude - Default minimum altitude above destination (e.g. home, safe point, landing pattern) for return flight. The vehicle will climb to this altitude when Return mode is enganged, unless it currently is flying higher already. This is affected by RTL_MIN_DIST and RTL_CONE_ANG. - 0 - m - 1 - 0.5 - - - Return type - Return mode destination and flight path (home location, rally point, mission landing pattern, reverse mission) - - Return to closest safe point (home or rally point) via direct path. - Return to closest safe point other than home (mission landing pattern or rally point), via direct path. If no mission landing or rally points are defined return home via direct path. Always chose closest safe landing point if vehicle is a VTOL in hover mode. - Return to a planned mission landing, if available, using the mission path, else return to home via the reverse mission path. Do not consider rally points. - Return via direct path to closest destination: home, start of mission landing pattern or safe point. If the destination is a mission landing pattern, follow the pattern to land. - - - - - - RTL force approach landing - Only consider RTL point, if it has an approach defined. - - - RTL time estimate safety margin factor - Safety factor that is used to scale the actual RTL time estimate. Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN - 1.0 - 2.0 - 1 - 0.1 - - - RTL time estimate safety margin offset - Margin that is added to the time estimate, after it has already been scaled Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN - 0 - 3600 - s - 1 - 1 - - - - - Serial Configuration for Roboclaw Driver - Configure on which serial port to run Roboclaw Driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - - - Address of the ESC on the bus - The ESC has to be configured to have an address from 0x80 to 0x87. This parameter needs to match the configured value. - 128 - 135 - - 0x80 - 0x81 - 0x82 - 0x83 - 0x84 - 0x85 - 0x86 - 0x87 - - - - Number of encoder counts for one wheel revolution - The default value of 1200 corresponds to the default configuration of the Aion R1 rover. - 1 - - - - - Tuning parameter for corner cutting - The geometric ideal acceptance radius is multiplied by this factor to account for kinematic and dynamic effects. Higher value -> The rover starts to cut the corner earlier. - 1 - 100 - 2 - 0.01 - - - Maximum acceptance radius for the waypoints - The controller scales the acceptance radius based on the angle between the previous, current and next waypoint. Higher value -> smoother trajectory at the cost of how close the rover gets to the waypoint (Set to -1 to disable corner cutting). - -1 - 100 - m - 2 - 0.01 - - - Integral gain for lateral acceleration controller - 0 - 100 - 3 - 0.001 - - - Proportional gain for lateral acceleration controller - 0 - 100 - 2 - 0.01 - - - Maximum acceleration for the rover - This is used for the acceleration slew rate. - -1 - 100 - m/s^2 - 2 - 0.01 - - - Maximum deceleration for the rover - This is used for the deceleration slew rate, the feed-forward term for the speed controller during missions and the corner slow down effect. Note: For the corner slow down effect RA_MAX_JERK also has to be set. - -1 - 100 - m/s^2 - 2 - 0.01 - - - Maximum jerk - Limit for forwards acc/deceleration change. This is used for the corner slow down effect. Note: RA_MAX_DECEL also has to be set for this to be enabled. - -1 - 100 - m/s^3 - 2 - 0.01 - - - Maximum allowed lateral acceleration - This parameter is used to cap the lateral acceleration and map controller inputs to desired lateral acceleration in Acro, Stabilized and Position mode. - 0.01 - 1000 - m/s^2 - 2 - 0.01 - - - Maximum allowed speed - This is the maximum allowed speed setpoint when driving in a mode that uses closed loop speed control. - -1 - 100 - m/s - 2 - 0.01 - - - Maximum steering angle - The maximum angle that the rover can steer - 0.1 - 1.5708 - rad - 2 - 0.01 - - - Maximum steering rate for the rover - -1 - 1000 - deg/s - 2 - 0.01 - - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. - -1 - 100 - m/s - 2 - 0.01 - - - Integral gain for ground speed controller - 0 - 100 - 3 - 0.001 - - - Proportional gain for ground speed controller - 0 - 100 - 2 - 0.01 - - - Wheel base - Distance from the front to the rear axle - 0.001 - 100 + Return mode return altitude + Default minimum altitude above destination (e.g. home, safe point, landing pattern) for return flight. +The vehicle will climb to this altitude when Return mode is enganged, unless it currently is flying higher already. +This is affected by RTL_MIN_DIST and RTL_CONE_ANG. + 0 m - 3 - 0.001 + 1 + 0.5 + + + Return type + Return mode destination and flight path (home location, rally point, mission landing pattern, reverse mission) + + Return to closest safe point (home or rally point) via direct path. + Return to closest safe point other than home (mission landing pattern or rally point), via direct path. If no mission landing or rally points are defined return home via direct path. Always choose closest safe landing point if vehicle is a VTOL in hover mode. + Return to a planned mission landing, if available, using the mission path, else return to home via the reverse mission path. Do not consider rally points. + Return via direct path to closest destination: home, start of mission landing pattern or safe point. If the destination is a mission landing pattern, follow the pattern to land. + Return to the planned mission landing, or to home via the reverse mission path, whichever is closer by counting waypoints. Do not consider rally points. + Return directly to safe landing point (do not consider mission landing and Home) + - - - Maximum acceleration - Maximum acceleration is used to limit the acceleration of the rover - 0 - 100 - m/s^2 - 2 - 0.01 + + + RTL force approach landing + Only consider RTL point, if it has an approach defined. - - Maximum deceleration - Maximum decelaration is used to limit the deceleration of the rover. Set to -1 to disable, causing the rover to decelerate as fast as possible. Caution: This disables the slow down effect in auto modes. - -1 - 100 - m/s^2 - 2 - 0.01 + + RTL time estimate safety margin factor + Safety factor that is used to scale the actual RTL time estimate. +Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN + 1.0 + 2.0 + 1 + 0.1 - - Maximum jerk - Limit for forwards acc/deceleration change. + + RTL time estimate safety margin offset + Margin that is added to the time estimate, after it has already been scaled +Time with margin = RTL_TIME_FACTOR * time + RTL_TIME_MARGIN 0 - 100 - m/s^3 - 2 - 0.01 + 3600 + s + 1 + 1 - - Maximum speed setpoint - This parameter is used to cap desired forward speed and map controller inputs to desired speeds in Position mode. - 0 + + + + Tuning parameter for corner cutting + The geometric ideal acceptance radius is multiplied by this factor +to account for kinematic and dynamic effects. +Higher value -> The rover starts to cut the corner earlier. + 1 100 - m/s 2 0.01 - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. - 0 + + Maximum acceptance radius for the waypoints + The controller scales the acceptance radius based on the angle between +the previous, current and next waypoint. +Higher value -> smoother trajectory at the cost of how close the rover gets +to the waypoint (Set to -1 to disable corner cutting). + -1 100 - m/s + m 2 0.01 - - Yaw rate turning left/right wheels at max speed in opposite directions - This parameter is used to calculate the feedforward term of the closed loop yaw rate control. The controller first calculates the required speed difference between the left and right motor to achieve the desired yaw rate. This desired speed difference is then linearly mapped to normalized motor commands. A good starting point is twice the speed the rover drives at maximum throttle (RD_MAX_THR_SPD)). Increase this parameter if the rover turns faster than the setpoint, and decrease if the rover turns slower. + + Maximum steering angle 0 - 100 - m/s + 1.5708 + rad 2 0.01 - - Maximum allowed yaw acceleration for the rover - This parameter is used to cap desired yaw acceleration. This is used to adjust incoming yaw rate setpoints to a feasible yaw rate setpoint based on the physical limitation on how fast the yaw rate can change. This leads to a smooth setpoint trajectory for the closed loop yaw rate controller to track. Set to -1 to disable. + + Steering rate limit + Set to -1 to disable. -1 1000 - deg/s^2 - 2 - 0.01 - - - Maximum allowed yaw rate for the rover - This parameter is used to cap desired yaw rates and map controller inputs to desired yaw rates in Acro,Stabilized and Position mode. - 0.01 - 1000 deg/s 2 0.01 - - Tuning parameter for the speed reduction during waypoint transition - The waypoint transition speed is calculated as: Transition_speed = Maximum_speed * (1 - normalized_transition_angle * RM_MISS_VEL_GAIN) The normalized transition angle is the angle between the line segment from prev-curr WP and curr-next WP interpolated from [0, 180] -> [0, 1]. Higher value -> More speed reduction during waypoint transitions. - 0.05 - 100 - 2 - 0.01 - - - Integral gain for closed loop forward speed controller + + Wheel base + Distance from the front to the rear axle. 0 100 + m 3 - 0.01 + 0.001 - - Proportional gain for closed loop forward speed controller + + + + Proportional gain for closed loop yaw controller 0 100 3 0.01 + + Yaw error threshhold to switch from driving to spot turning - This threshold is used for the state machine to switch from driving to turning based on the error between the desired and actual yaw. It is also used as the threshold whether the rover should come to a smooth stop at the next waypoint. This slow down effect is active if the angle between the line segments from prevWP-currWP and currWP-nextWP is smaller then 180 - RD_TRANS_DRV_TRN. + This threshold is used for the state machine to switch from driving to turning based on the +error between the desired and actual yaw. It is also used as the threshold whether the rover should come +to a smooth stop at the next waypoint. This slow down effect is active if the angle between the +line segments from prevWP-currWP and currWP-nextWP is smaller then 180 - RD_TRANS_DRV_TRN. 0.001 3.14159 rad @@ -20037,367 +12151,264 @@ Yaw error threshhold to switch from spot turning to driving + This threshold is used for the state machine to switch from turning to driving based on the +error between the desired and actual yaw. 0.001 3.14159 rad 3 0.01 - + Wheel track - Distance from the center of the right wheel to the center of the left wheel - 0.001 + Distance from the center of the right wheel to the center of the left wheel. + 0 100 m 3 0.001 - - Integral gain for closed loop yaw controller - 0 - 100 + + Yaw stick gain for Manual mode + Assign value <1.0 to decrease stick response for yaw control. + 0.1 + 1 3 0.01 - - Proportional gain for closed loop yaw controller - 0 - 100 - 3 + + + + Threshold to update course control in manual position mode + Threshold for the angle between the active cruise direction and the cruise direction given +by the stick inputs. +This can be understood as a deadzone for the combined stick inputs for forward/backwards +and lateral speed which defines a course direction. + 0 + 3.14 + rad + 2 0.01 - - Integral gain for closed loop yaw rate controller + + Wheel track + Distance from the center of the right wheel to the center of the left wheel. 0 100 + m 3 - 0.01 + 0.001 - - Proportional gain for closed loop yaw rate controller - 0 - 100 + + Yaw stick gain for Manual mode + Assign value <1.0 to decrease stick response for yaw control. + 0.1 + 1 3 0.01 - - - Maximum acceleration - Maximum acceleration is used to limit the acceleration of the rover - 0 - 100 - m/s^2 + + + Yaw acceleration limit + Used to cap how quickly the magnitude of yaw rate setpoints can increase. +Set to -1 to disable. + -1 + 10000 + deg/s^2 2 0.01 - - Maximum deceleration - Maximum decelaration is used to limit the deceleration of the rover. Set to -1 to disable, causing the rover to decelerate as fast as possible. Caution: Disabling the deceleration limit also disables the slow down effect in auto modes. + + Yaw deceleration limit + Used to cap how quickly the magnitude of yaw rate setpoints can decrease. +Set to -1 to disable. -1 - 100 - m/s^2 + 10000 + deg/s^2 2 0.01 - - Maximum jerk - Limit for forwards acc/deceleration change. + + Yaw rate expo factor + Exponential factor for tuning the input curve shape. +0 Purely linear input curve +1 Purely cubic input curve 0 - 100 - m/s^3 + 1 2 - 0.01 - - Maximum speed the rover is allowed to drive - This parameter is used cap the maximum speed the rover is allowed to drive and to map stick inputs to desired speeds in position mode. - 0 - 100 - m/s + + Yaw rate correction factor + Multiplicative correction factor for the feedforward mapping of the yaw rate controller. +Increase this value (x > 1) if the measured yaw rate is lower than the setpoint, decrease (0 < x < 1) otherwise. +Note: Tuning this is particularly useful for skid-steered rovers, or rovers with misaligned wheels/steering axes +that cause a lot of friction when turning. + 0.01 + 10000 2 0.01 - - Speed the rover drives at maximum throttle - This parameter is used to calculate the feedforward term of the closed loop speed control which linearly maps desired speeds to normalized motor commands [-1. 1]. A good starting point is the observed ground speed when the rover drives at maximum throttle in manual mode. Increase this parameter if the rover is faster than the setpoint, and decrease if the rover is slower. + + Integral gain for closed loop yaw rate controller 0 100 - m/s - 2 + 3 0.01 - - Yaw rate turning left/right wheels at max speed in opposite directions - This parameter is used to calculate the feedforward term of the closed loop yaw rate control. The controller first calculates the required speed difference between the left and right motor to achieve the desired yaw rate. This desired speed difference is then linearly mapped to normalized motor commands. A good starting point is twice the speed the rover drives at maximum throttle (RM_MAX_THRTL_SPD)). Increase this parameter if the rover turns faster than the setpoint, and decrease if the rover turns slower. + + Yaw rate limit + Used to cap yaw rate setpoints and map controller inputs to yaw rate setpoints +in Acro, Stabilized and Position mode. 0 - 100 - m/s - 2 - 0.01 - - - Maximum allowed yaw acceleration for the rover - This parameter is used to cap desired yaw acceleration. This is used to adjust incoming yaw rate setpoints to a feasible yaw rate setpoint based on the physical limitation on how fast the yaw rate can change. This leads to a smooth setpoint trajectory for the closed loop yaw rate controller to track. Set to -1 to disable. - -1 - 1000 - deg/s^2 - 2 - 0.01 - - - Maximum allowed yaw rate for the rover - This parameter is used to cap desired yaw rates and map controller inputs to desired yaw rates in acro mode. - 0.01 - 1000 + 10000 deg/s 2 0.01 - - Tuning parameter for the velocity reduction during waypoint transition - The waypoint transition speed is calculated as: Transition_speed = Maximum_speed * (1 - normalized_transition_angle * RM_MISS_VEL_GAIN) The normalized transition angle is the angle between the line segment from prev-curr WP and curr-next WP interpolated from [0, 180] -> [0, 1]. Higher value -> More velocity reduction during cornering. - 0.05 - 100 - 2 - 0.01 - - - Integral gain for ground speed controller + + Proportional gain for closed loop yaw rate controller 0 100 3 0.01 - - Proportional gain for speed controller + + Yaw rate measurement threshold + The minimum threshold for the yaw rate measurement not to be interpreted as zero. 0 100 - 3 + deg/s + 2 0.01 - - Wheel track - Distance from the center of the right wheels to the center of the left wheels. - 0.001 - 100 - m - 3 - 0.001 - - - Integral gain for closed loop yaw controller + + Yaw stick deadzone + Percentage of stick input range that will be interpreted as zero around the stick centered value. 0 - 100 + 1 2 0.01 - - Proportional gain for closed loop yaw controller + + Yaw rate super expo factor + "Superexponential" factor for refining the input curve shape tuned using RO_YAW_EXPO. +0 Pure Expo function +0.7 reasonable shape enhancement for intuitive stick feel +0.95 very strong bent input curve only near maxima have effect 0 - 100 + 0.95 2 - 0.01 - - Integral gain for the closed-loop yaw rate controller - 0 + + + + Acceleration limit + Set to -1 to disable. +For mecanum rovers this limit is used for longitudinal and lateral acceleration. + -1 100 + m/s^2 2 0.01 - - Proportional gain for the closed-loop yaw rate controller - 0 + + Deceleration limit + Set to -1 to disable. +Note that if it is disabled the rover will not slow down when approaching waypoints in auto modes. +For mecanum rovers this limit is used for longitudinal and lateral deceleration. + -1 100 + m/s^2 2 0.01 - - - - L1 damping - Damping factor for L1 control. - 0.6 - 0.9 + + Jerk limit + Set to -1 to disable. +Note that if it is disabled the rover will not slow down when approaching waypoints in auto modes. +For mecanum rovers this limit is used for longitudinal and lateral jerk. + -1 + 100 + m/s^3 2 - 0.05 - - - L1 distance - This is the distance at which the next waypoint is activated. This should be set to about 2-4x of GND_WHEEL_BASE and not smaller than one meter (due to GPS accuracy). - 1.0 - 50.0 - m - 1 - 0.1 - - - L1 period - This is the L1 distance and defines the tracking point ahead of the rover it's following. Use values around 2-5m for a 0.3m wheel base. Tuning instructions: Shorten slowly during tuning until response is sharp without oscillation. - 0.5 - 50.0 - m - 1 - 0.5 - - - Max manual yaw rate - 0.0 - 400 - deg/s - 1 - - - Maximum turn angle for Ackerman steering - At a control output of 0, the steering wheels are at 0 radians. At a control output of 1, the steering wheels are at GND_MAX_ANG radians. - 0.0 - 3.14159 - rad - 3 0.01 - - Speed proportional gain - This is the derivative gain for the speed closed loop controller - 0.00 - 50.0 - %m/s - 3 - 0.005 - - - Speed Integral gain - This is the integral gain for the speed closed loop controller - 0.00 - 50.0 - %m/s - 3 - 0.005 - - - Speed integral maximum value - This is the maxim value the integral can reach to prevent wind-up. - 0.005 - 50.0 - %m/s - 3 - 0.005 - - - Maximum ground speed - 0.0 - 40 + + Speed the rover drives at maximum throttle + Used to linearly map speeds [m/s] to throttle values [-1. 1]. + 0 + 100 m/s - 1 - 0.5 - - - Speed proportional gain - This is the proportional gain for the speed closed loop controller - 0.005 - 50.0 - %m/s - 3 - 0.005 + 2 + 0.01 - - Speed to throttle scaler - This is a gain to map the speed control output to the throttle linearly. - 0.005 - 50.0 - %m/s + + Integral gain for ground speed controller + 0 + 100 3 - 0.005 + 0.001 - - Trim ground speed - 0.0 - 40 + + Speed limit + Used to cap speed setpoints and map controller inputs to speed setpoints in Position mode. + -1 + 100 m/s - 1 - 0.5 - - - Control mode for speed - This allows the user to choose between closed loop gps speed or open loop cruise throttle speed - 0 - 1 - - open loop control - close the loop with gps speed - - - - Cruise throttle - This is the throttle setting required to achieve the desired cruise speed. 10% is ok for a traxxas stampede vxl with ESC set to training mode - 0.0 - 1.0 - norm 2 0.01 - - Throttle limit max - This is the maximum throttle % that can be used by the controller. For a Traxxas stampede vxl with the ESC set to training, 30 % is enough - 0.0 - 1.0 - norm + + Proportional gain for ground speed controller + 0 + 100 2 0.01 - - Throttle limit min - This is the minimum throttle % that can be used by the controller. Set to 0 for rover - 0.0 - 1.0 - norm + + Tuning parameter for the speed reduction based on the course error + Reduced_speed = RO_MAX_THR_SPEED * (1 - normalized_course_error * RO_SPEED_RED) +The normalized course error is the angle between the current course and the bearing setpoint +interpolated from [0, 180] -> [0, 1]. +Higher value -> More speed reduction. +Note: This is also used to calculate the speed at which the vehicle arrives at a waypoint in auto modes. +Set to -1 to disable bearing error based speed reduction. + -1 + 100 2 0.01 - - Distance from front axle to rear axle - A value of 0.31 is typical for 1/10 RC cars. - 0.0 - m - 3 + + Speed measurement threshold + Set to -1 to disable. +The minimum threshold for the speed measurement not to be interpreted as zero. + 0 + 100 + m/s + 2 0.01 - - Specifies which heading should be held during the runway takeoff ground roll - 0: airframe heading when takeoff is initiated 1: position control along runway direction (bearing defined from vehicle position on takeoff initiation to MAV_CMD_TAKEOFF position defined by operator) - 0 - 1 - - Airframe - Runway - - - Max throttle during runway takeoff + Throttle during runway takeoff 0.0 1.0 norm 2 0.01 - - NPFG period while steering on runway - 1.0 - 100.0 - s - 1 - 0.1 - Enable use of yaw stick for nudging the wheel during runway ground roll - This is useful when map, GNSS, or yaw errors on ground are misaligned with what the operator intends for takeoff course. Particularly useful for skinny runways or if the wheel servo is a bit off trim. + This is useful when map, GNSS, or yaw errors on ground are misaligned with what the operator intends for takeoff course. +Particularly useful for skinny runways or if the wheel servo is a bit off trim. Pitch setpoint during taxi / before takeoff rotation airspeed is reached - A taildragger with steerable wheel might need to pitch up a little to keep its wheel on the ground before airspeed to takeoff is reached. + A taildragger with steerable wheel might need to pitch up +a little to keep its wheel on the ground before airspeed +to takeoff is reached. -10.0 20.0 deg @@ -20414,7 +12425,9 @@ Takeoff rotation airspeed - The calibrated airspeed threshold during the takeoff ground roll when the plane should start rotating (pitching up). Must be less than the takeoff airspeed, will otherwise be capped at the takeoff airpeed (see FW_TKO_AIRSPD). If set <= 0.0, defaults to 0.9 * takeoff airspeed (see FW_TKO_AIRSPD) + The calibrated airspeed threshold during the takeoff ground roll when the plane should start rotating (pitching up). +Must be less than the takeoff airspeed, will otherwise be capped at the takeoff airpeed (see FW_TKO_AIRSPD). +If set <= 0.0, defaults to 0.9 * takeoff airspeed (see FW_TKO_AIRSPD) -1.0 m/s 1 @@ -20433,13 +12446,16 @@ - - Logfile Encryption algorithm - Selects the algorithm used for logfile encryption - - Disabled - XChaCha20 - + + Logging Backend (integer bitmask) + If no logging is set the logger will not be started. Set bits true to enable: 0: SD card logging 1: Mavlink logging + 0 + 3 + True + + SD card logging + Mavlink logging + Battery-only Logging @@ -20450,24 +12466,12 @@ If there are more log directories than this value, the system will delete the oldest directories during startup. In addition, the system will delete old logs if there is not enough free space left. The minimum amount is 300 MB. If this is set to 0, old directories will only be removed if the free space falls below the minimum. Note: this does not apply to mission log files. 0 1000 - true - - - Logfile Encryption key exchange key - If the logfile is encrypted using a symmetric key algorithm, the used encryption key is generated at logging start and stored on the sdcard RSA2048 encrypted using this key. - 0 - 255 - - - Logfile Encryption key index - Selects the key in keystore, used for encrypting the log. When using a symmetric encryption algorithm, the key is generated at logging start and kept stored in this index. For symmetric algorithms, the key is volatile and valid only for the duration of logging. The key is stored in encrypted format on the sdcard alongside the logfile, using an RSA2048 key defined by the SDLOG_EXCHANGE_KEY - 0 - 255 + True Mission Log If enabled, a small additional "mission" log file will be written to the SD card. The log contains just those messages that are useful for tasks like generating flight statistics and geotagging. The different modes can be used to further reduce the logged data (and thus the log file size). For example, choose geotagging mode to only log data required for geotagging. Note that the normal/full log is still created, and contains all the data in the mission log (and more). - true + True Disabled All mission messages @@ -20476,10 +12480,9 @@ Logging Mode - Determines when to start and stop logging. By default, logging is started when arming the system, and stopped when disarming. - true + Determines when to start and stop logging. By default, logging is started when arming the system, and stopped when disarming. Note: The logging start/end points that can be configured here only apply to SD logging. The mavlink backend is started/stopped independently of these points. + True - disabled when armed until disarm (default) from boot until disarm from boot until shutdown @@ -20489,10 +12492,10 @@ Logging topic profile (integer bitmask) - This integer bitmask controls the set and rates of logged topics. The default allows for general log analysis while keeping the log file size reasonably small. Enabling multiple sets leads to higher bandwidth requirements and larger log files. Set bits true to enable: 0 : Default set (used for general log analysis) 1 : Full rate estimator (EKF2) replay topics 2 : Topics for thermal calibration (high rate raw IMU and Baro sensor data) 3 : Topics for system identification (high rate actuator control and IMU data) 4 : Full rates for analysis of fast maneuvers (RC, attitude, rates and actuators) 5 : Debugging topics (debug_*.msg topics, for custom code) 6 : Topics for sensor comparison (low rate raw IMU, Baro and magnetometer data) 7 : Topics for computer vision and collision avoidance 8 : Raw FIFO high-rate IMU (Gyro) 9 : Raw FIFO high-rate IMU (Accel) 10: Logging of mavlink tunnel message (useful for payload communication debugging) + This integer bitmask controls the set and rates of logged topics. The default allows for general log analysis while keeping the log file size reasonably small. Enabling multiple sets leads to higher bandwidth requirements and larger log files. Set bits true to enable: 0 : Default set (used for general log analysis) 1 : Full rate estimator (EKF2) replay topics 2 : Topics for thermal calibration (high rate raw IMU and Baro sensor data) 3 : Topics for system identification (high rate actuator control and IMU data) 4 : Full rates for analysis of fast maneuvers (RC, attitude, rates and actuators) 5 : Debugging topics (debug_*.msg topics, for custom code) 6 : Topics for sensor comparison (low rate raw IMU, Baro and magnetometer data) 7 : Topics for computer vision and collision prevention 8 : Raw FIFO high-rate IMU (Gyro) 9 : Raw FIFO high-rate IMU (Accel) 10: Logging of mavlink tunnel message (useful for payload communication debugging) 0 - 2047 - true + 4095 + True Default set (general log analysis) Estimator replay (EKF2) @@ -20505,6 +12508,7 @@ Raw FIFO high-rate IMU (Gyro) Raw FIFO high-rate IMU (Accel) Mavlink tunnel message logging + High rate sensors @@ -20518,238 +12522,39 @@ Log UUID If set to 1, add an ID to the log, which uniquely identifies the vehicle - - - - Simulator Battery drain interval - 1 - 86400 - s - 1 - - - Simulator Battery enabled - Enable or disable the internal battery simulation. This is useful when the battery is simulated externally and interfaced with PX4 through MAVLink for example. - - - Simulator Battery minimal percentage - Can be used to alter the battery level during SITL- or HITL-simulation on the fly. Particularly useful for testing different low-battery behaviour. - 0 - 100 - % - 0.1 - - - - - Accelerometer 0 calibration device ID - Device ID of the accelerometer this calibration applies to. - 3 - - - Accelerometer 0 priority - 3 - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Accelerometer 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - - - - Accelerometer 0 X-axis offset - m/s^2 - 3 - - - Accelerometer 0 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 0 Y-axis offset - m/s^2 - 3 - - - Accelerometer 0 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 0 Z-axis offset - m/s^2 - 3 - - - Accelerometer 0 Z-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 1 calibration device ID - Device ID of the accelerometer this calibration applies to. - 3 - - - Accelerometer 1 priority - 3 - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Accelerometer 1 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - - - - Accelerometer 1 X-axis offset - m/s^2 - 3 - - - Accelerometer 1 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 1 Y-axis offset - m/s^2 - 3 - - - Accelerometer 1 Y-axis scaling factor - 0.1 - 3.0 - 3 + + + + Simulator Battery drain interval + 1 + 86400 + s + 1 - - Accelerometer 1 Z-axis offset - m/s^2 - 3 + + Simulator Battery enabled + Enable or disable the internal battery simulation. This is useful +when the battery is simulated externally and interfaced with PX4 +through MAVLink for example. - - Accelerometer 1 Z-axis scaling factor - 0.1 - 3.0 - 3 + + Simulator Battery minimal percentage + Can be used to alter the battery level during SITL- or HITL-simulation on the fly. +Particularly useful for testing different low-battery behaviour. + 0 + 100 + % + 0.1 - - Accelerometer 2 calibration device ID + + + + Accelerometer 0 calibration device ID Device ID of the accelerometer this calibration applies to. 3 - - Accelerometer 2 priority + + Accelerometer 0 priority 3 Uninitialized @@ -20761,8 +12566,8 @@ Max - - Accelerometer 2 rotation relative to airframe + + Accelerometer 0 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -20811,46 +12616,46 @@ Roll 90°, Pitch 315° - - Accelerometer 2 X-axis offset + + Accelerometer 0 X-axis offset m/s^2 3 - - Accelerometer 2 X-axis scaling factor + + Accelerometer 0 X-axis scaling factor 0.1 3.0 3 - - Accelerometer 2 Y-axis offset + + Accelerometer 0 Y-axis offset m/s^2 3 - - Accelerometer 2 Y-axis scaling factor + + Accelerometer 0 Y-axis scaling factor 0.1 3.0 3 - - Accelerometer 2 Z-axis offset + + Accelerometer 0 Z-axis offset m/s^2 3 - - Accelerometer 2 Z-axis scaling factor + + Accelerometer 0 Z-axis scaling factor 0.1 3.0 3 - - Accelerometer 3 calibration device ID + + Accelerometer 1 calibration device ID Device ID of the accelerometer this calibration applies to. 3 - - Accelerometer 3 priority + + Accelerometer 1 priority 3 Uninitialized @@ -20862,8 +12667,8 @@ Max - - Accelerometer 3 rotation relative to airframe + + Accelerometer 1 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -20912,206 +12717,47 @@ Roll 90°, Pitch 315° - - Accelerometer 3 X-axis offset - m/s^2 - 3 - - - Accelerometer 3 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Accelerometer 3 Y-axis offset + + Accelerometer 1 X-axis offset m/s^2 3 - - Accelerometer 3 Y-axis scaling factor + + Accelerometer 1 X-axis scaling factor 0.1 3.0 3 - - Accelerometer 3 Z-axis offset + + Accelerometer 1 Y-axis offset m/s^2 - 3 - - - Accelerometer 3 Z-axis scaling factor - 0.1 - 3.0 - 3 - - - Barometer 0 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 0 offset - 3 - - - Barometer 0 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 1 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 1 offset - 3 - - - Barometer 1 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 2 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 2 offset - 3 - - - Barometer 2 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Barometer 3 calibration device ID - Device ID of the barometer this calibration applies to. - - - Barometer 3 offset - 3 - - - Barometer 3 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Gyroscope 0 calibration device ID - Device ID of the gyroscope this calibration applies to. - - - Gyroscope 0 priority - - Uninitialized - Disabled - Min - Low - Medium (Default) - High - Max - - - - Gyroscope 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. - -1 - 40 - - Internal - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - Roll 180° - Roll 180°, Yaw 45° - Roll 180°, Yaw 90° - Roll 180°, Yaw 135° - Pitch 180° - Roll 180°, Yaw 225° - Roll 180°, Yaw 270° - Roll 180°, Yaw 315° - Roll 90° - Roll 90°, Yaw 45° - Roll 90°, Yaw 90° - Roll 90°, Yaw 135° - Roll 270° - Roll 270°, Yaw 45° - Roll 270°, Yaw 90° - Roll 270°, Yaw 135° - Pitch 90° - Pitch 270° - Pitch 180°, Yaw 90° - Pitch 180°, Yaw 270° - Roll 90°, Pitch 90° - Roll 180°, Pitch 90° - Roll 270°, Pitch 90° - Roll 90°, Pitch 180° - Roll 270°, Pitch 180° - Roll 90°, Pitch 270° - Roll 180°, Pitch 270° - Roll 270°, Pitch 270° - Roll 90°, Pitch 180°, Yaw 90° - Roll 90°, Yaw 270° - Roll 90°, Pitch 68°, Yaw 293° - Pitch 315° - Roll 90°, Pitch 315° - + 3 - - Gyroscope 0 X-axis offset - rad/s + + Accelerometer 1 Y-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 0 Y-axis offset - rad/s + + Accelerometer 1 Z-axis offset + m/s^2 3 - - Gyroscope 0 Z-axis offset - rad/s + + Accelerometer 1 Z-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 1 calibration device ID - Device ID of the gyroscope this calibration applies to. + + Accelerometer 2 calibration device ID + Device ID of the accelerometer this calibration applies to. + 3 - - Gyroscope 1 priority + + Accelerometer 2 priority + 3 Uninitialized Disabled @@ -21122,8 +12768,8 @@ Max - - Gyroscope 1 rotation relative to airframe + + Accelerometer 2 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21172,27 +12818,47 @@ Roll 90°, Pitch 315° - - Gyroscope 1 X-axis offset - rad/s + + Accelerometer 2 X-axis offset + m/s^2 3 - - Gyroscope 1 Y-axis offset - rad/s + + Accelerometer 2 X-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 1 Z-axis offset - rad/s + + Accelerometer 2 Y-axis offset + m/s^2 3 - - Gyroscope 2 calibration device ID - Device ID of the gyroscope this calibration applies to. + + Accelerometer 2 Y-axis scaling factor + 0.1 + 3.0 + 3 - - Gyroscope 2 priority + + Accelerometer 2 Z-axis offset + m/s^2 + 3 + + + Accelerometer 2 Z-axis scaling factor + 0.1 + 3.0 + 3 + + + Accelerometer 3 calibration device ID + Device ID of the accelerometer this calibration applies to. + 3 + + + Accelerometer 3 priority + 3 Uninitialized Disabled @@ -21203,8 +12869,8 @@ Max - - Gyroscope 2 rotation relative to airframe + + Accelerometer 3 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21253,27 +12919,125 @@ Roll 90°, Pitch 315° - - Gyroscope 2 X-axis offset - rad/s + + Accelerometer 3 X-axis offset + m/s^2 3 - - Gyroscope 2 Y-axis offset - rad/s + + Accelerometer 3 X-axis scaling factor + 0.1 + 3.0 3 - - Gyroscope 2 Z-axis offset - rad/s + + Accelerometer 3 Y-axis offset + m/s^2 3 - - Gyroscope 3 calibration device ID + + Accelerometer 3 Y-axis scaling factor + 0.1 + 3.0 + 3 + + + Accelerometer 3 Z-axis offset + m/s^2 + 3 + + + Accelerometer 3 Z-axis scaling factor + 0.1 + 3.0 + 3 + + + Barometer 0 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 0 offset + 3 + + + Barometer 0 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 1 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 1 offset + 3 + + + Barometer 1 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 2 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 2 offset + 3 + + + Barometer 2 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Barometer 3 calibration device ID + Device ID of the barometer this calibration applies to. + + + Barometer 3 offset + 3 + + + Barometer 3 priority + + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max + + + + Gyroscope 0 calibration device ID Device ID of the gyroscope this calibration applies to. - - Gyroscope 3 priority + + Gyroscope 0 priority Uninitialized Disabled @@ -21284,8 +13048,8 @@ Max - - Gyroscope 3 rotation relative to airframe + + Gyroscope 0 rotation relative to airframe An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 40 @@ -21334,34 +13098,27 @@ Roll 90°, Pitch 315° - - Gyroscope 3 X-axis offset + + Gyroscope 0 X-axis offset rad/s 3 - - Gyroscope 3 Y-axis offset + + Gyroscope 0 Y-axis offset rad/s 3 - - Gyroscope 3 Z-axis offset + + Gyroscope 0 Z-axis offset rad/s 3 - - Magnetometer 0 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 0 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 1 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 0 priority + + Gyroscope 1 priority Uninitialized Disabled @@ -21371,19 +13128,12 @@ High Max - - - Magnetometer 0 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 0 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG0_ROLL, CAL_MAG0_PITCH and CAL_MAG0_YAW. + + + Gyroscope 1 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21427,88 +13177,29 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 0 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 0 X-axis off diagonal scale factor - 3 - - - Magnetometer 0 X-axis offset - gauss - 3 - - - Magnetometer 0 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 0 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 0 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 0 Y-axis off diagonal scale factor - 3 - - - Magnetometer 0 Y-axis offset - gauss - 3 - - - Magnetometer 0 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 0 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Gyroscope 1 X-axis offset + rad/s 3 - - Magnetometer 0 Z-axis off diagonal scale factor - - - Magnetometer 0 Z-axis offset - gauss + + Gyroscope 1 Y-axis offset + rad/s 3 - - Magnetometer 0 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 1 Z-axis offset + rad/s 3 - - Magnetometer 1 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 1 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 2 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 1 priority + + Gyroscope 2 priority Uninitialized Disabled @@ -21519,18 +13210,11 @@ Max - - Magnetometer 1 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 1 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG1_ROLL, CAL_MAG1_PITCH and CAL_MAG1_YAW. + + Gyroscope 2 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21574,88 +13258,29 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 1 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 1 X-axis off diagonal scale factor - 3 - - - Magnetometer 1 X-axis offset - gauss - 3 - - - Magnetometer 1 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 1 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 1 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 1 Y-axis off diagonal scale factor - 3 - - - Magnetometer 1 Y-axis offset - gauss - 3 - - - Magnetometer 1 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 1 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Gyroscope 2 X-axis offset + rad/s 3 - - Magnetometer 1 Z-axis off diagonal scale factor - - - Magnetometer 1 Z-axis offset - gauss + + Gyroscope 2 Y-axis offset + rad/s 3 - - Magnetometer 1 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 2 Z-axis offset + rad/s 3 - - Magnetometer 2 calibration device ID - Device ID of the magnetometer this calibration applies to. - - - Magnetometer 2 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg + + Gyroscope 3 calibration device ID + Device ID of the gyroscope this calibration applies to. - - Magnetometer 2 priority + + Gyroscope 3 priority Uninitialized Disabled @@ -21666,18 +13291,11 @@ Max - - Magnetometer 2 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 2 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG2_ROLL, CAL_MAG2_PITCH and CAL_MAG2_YAW. + + Gyroscope 3 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. -1 - 100 + 40 Internal No rotation @@ -21721,88 +13339,36 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° - Custom Euler Angle - - Magnetometer 2 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 X-axis off diagonal scale factor - 3 - - - Magnetometer 2 X-axis offset - gauss - 3 - - - Magnetometer 2 X-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 2 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" - -180 - 180 - deg - - - Magnetometer 2 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 Y-axis off diagonal scale factor - 3 - - - Magnetometer 2 Y-axis offset - gauss - 3 - - - Magnetometer 2 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 2 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 2 Z-axis off diagonal scale factor + + Gyroscope 3 X-axis offset + rad/s + 3 - - Magnetometer 2 Z-axis offset - gauss + + Gyroscope 3 Y-axis offset + rad/s 3 - - Magnetometer 2 Z-axis scaling factor - 0.1 - 3.0 + + Gyroscope 3 Z-axis offset + rad/s 3 - - Magnetometer 3 calibration device ID + + Magnetometer 0 calibration device ID Device ID of the magnetometer this calibration applies to. - - Magnetometer 3 Custom Euler Pitch Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 priority + + Magnetometer 0 priority Uninitialized Disabled @@ -21813,16 +13379,17 @@ Max - - Magnetometer 3 Custom Euler Roll Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 rotation relative to airframe - An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. Set to "Custom Euler Angle" to define the rotation using CAL_MAG3_ROLL, CAL_MAG3_PITCH and CAL_MAG3_YAW. + + Magnetometer 0 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG0_ROLL, CAL_MAG0_PITCH and CAL_MAG0_YAW. -1 100 @@ -21871,498 +13438,282 @@ Custom Euler Angle - - Magnetometer 3 X Axis throttle compensation - Coefficient describing linear relationship between X component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] + + Magnetometer 0 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - Magnetometer 3 X-axis off diagonal scale factor + + Magnetometer 0 X-axis off diagonal scale factor 3 - - Magnetometer 3 X-axis offset + + Magnetometer 0 X-axis offset gauss 3 - - Magnetometer 3 X-axis scaling factor + + Magnetometer 0 X-axis scaling factor 0.1 3.0 3 - - Magnetometer 3 Custom Euler Yaw Angle - Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + + Magnetometer 0 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG0_ROT to "Custom Euler Angle" -180 180 deg - - Magnetometer 3 Y Axis throttle compensation - Coefficient describing linear relationship between Y component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 3 Y-axis off diagonal scale factor - 3 - - - Magnetometer 3 Y-axis offset - gauss - 3 - - - Magnetometer 3 Y-axis scaling factor - 0.1 - 3.0 - 3 - - - Magnetometer 3 Z Axis throttle compensation - Coefficient describing linear relationship between Z component of magnetometer in body frame axis and either current or throttle depending on value of CAL_MAG_COMP_TYP. Unit for throttle-based compensation is [G] and for current-based compensation [G/kA] - 3 - - - Magnetometer 3 Z-axis off diagonal scale factor - - - Magnetometer 3 Z-axis offset - gauss + + Magnetometer 0 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - Magnetometer 3 Z-axis scaling factor - 0.1 - 3.0 + + Magnetometer 0 Y-axis off diagonal scale factor 3 - - Type of magnetometer compensation - - Disabled - Throttle-based compensation - Current-based compensation (battery_status instance 0) - Current-based compensation (battery_status instance 1) - - - - Differential pressure sensor analog scaling - Pick the appropriate scaling from the datasheet. this number defines the (linear) conversion from voltage to Pascal (pa). For the MPXV7002DP this is 1000. NOTE: If the sensor always registers zero, try switching the static and dynamic tubes. - - - Differential pressure sensor offset - The offset (zero-reading) in Pascal - - - Maximum height above ground when reliant on optical flow - This parameter defines the maximum distance from ground at which the optical flow sensor operates reliably. The height setpoint will be limited to be no greater than this value when the navigation system is completely reliant on optical flow data and the height above ground estimate is valid. The sensor may be usable above this height, but accuracy will progressively degrade. - 1.0 - 100.0 - m - 2 - 0.1 - - - Magnitude of maximum angular flow rate reliably measurable by the optical flow sensor - Optical flow data will not fused by the estimators if the magnitude of the flow rate exceeds this value and control loops will be instructed to limit ground speed such that the flow rate produced by movement over ground is less than 50% of this value. - 1.0 - rad/s - 2 - - - Minimum height above ground when reliant on optical flow - This parameter defines the minimum distance from ground at which the optical flow sensor operates reliably. The sensor may be usable below this height, but accuracy will progressively reduce to loss of focus. - 0.0 - 1.0 - m - 2 - 0.1 - - - - - Enable external ADS1115 ADC - If enabled, the internal ADC is not used. - true - - - Capacity/current multiplier for high-current capable SMBUS battery - 1 - true - - - Battery device model - 0 - 2 - true - - AutoDetect - BQ40Z50 based - BQ40Z80 based - - - - I2C address for BatMon battery 1 - 1 - true - - - Parameter to enable BatMon module - 0 - 2 - true - - Disabled - Start on default I2C addr(BATMON_ADDR_DFLT) - Autodetect I2C address (TODO) - - - - Airspeed sensor compensation model for the SDP3x - Model with Pitot CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. Model without Pitot (1.5 mm tubes) CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. Tube Pressure Drop CAL_AIR_TUBED_MM: Diameter in mm of the pitot and tubes, must have the same diameter. CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor and the static + dynamic port length of the pitot. - - Model with Pitot - Model without Pitot (1.5 mm tubes) - Tube Pressure Drop - - - - Airspeed sensor tube diameter. Only used for the Tube Pressure Drop Compensation - 1.5 - 100 - mm - - - Airspeed sensor tube length - See the CAL_AIR_CMODEL explanation on how this parameter should be set. - 0.01 - 2.00 - m - - - For legacy QGC support only - Use SENS_MAG_SIDES instead - - - Low pass filter cutoff frequency for accel - The cutoff frequency for the 2nd order butterworth filter on the primary accelerometer. This only affects the signal sent to the controllers, not the estimators. 0 disables the filter. - 0 - 1000 - Hz - true - - - Cutoff frequency for angular acceleration (D-Term filter) - The cutoff frequency for the 2nd order butterworth filter used on the time derivative of the measured angular velocity, also known as the D-term filter in the rate controller. The D-term uses the derivative of the rate and thus is the most susceptible to noise. Therefore, using a D-term filter allows to increase IMU_GYRO_CUTOFF, which leads to reduced control latency and permits to increase the P gains. A value of 0 disables the filter. - 0 - 1000 - Hz - true - - - IMU gyro auto calibration enable - true - - - Low pass filter cutoff frequency for gyro - The cutoff frequency for the 2nd order butterworth filter on the primary gyro. This only affects the angular velocity sent to the controllers, not the estimators. It applies also to the angular acceleration (D-Term filter), see IMU_DGYRO_CUTOFF. A value of 0 disables the filter. - 0 - 1000 - Hz - true - - - IMU gyro ESC notch filter bandwidth - Bandwidth per notch filter when using dynamic notch filtering with ESC RPM. - 5 - 30 - Hz - - - IMU gyro dynamic notch filtering - Enable bank of dynamically updating notch filters. Requires ESC RPM feedback or onboard FFT (IMU_GYRO_FFT_EN). - 0 - 3 - - ESC RPM - FFT - - - - IMU gyro dynamic notch filter harmonics - ESC RPM number of harmonics (multiples of RPM) for ESC RPM dynamic notch filtering. - 1 - 7 - - - IMU gyro dynamic notch filter minimum frequency - Minimum notch filter frequency in Hz. - Hz - - - IMU gyro FFT enable - true - - - IMU gyro FFT length - Hz - true - - 256 - 512 - 1024 - 4096 - - - - IMU gyro FFT maximum frequency - 1 - 1000 - Hz - true + + Magnetometer 0 Y-axis offset + gauss + 3 - - IMU gyro FFT minimum frequency - 1 - 1000 - Hz - true + + Magnetometer 0 Y-axis scaling factor + 0.1 + 3.0 + 3 - - IMU gyro FFT SNR - 1 - 30 + + Magnetometer 0 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Notch filter bandwidth for gyro - The frequency width of the stop band for the 2nd order notch filter on the primary gyro. See "IMU_GYRO_NF0_FRQ" to activate the filter and to set the notch frequency. Applies to both angular velocity and angular acceleration sent to the controllers. - 0 - 100 - Hz - true + + Magnetometer 0 Z-axis off diagonal scale factor - - Notch filter frequency for gyro - The center frequency for the 2nd order notch filter on the primary gyro. This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. This only affects the signal sent to the controllers, not the estimators. Applies to both angular velocity and angular acceleration sent to the controllers. See "IMU_GYRO_NF0_BW" to set the bandwidth of the filter. A value of 0 disables the filter. - 0 - 1000 - Hz - true + + Magnetometer 0 Z-axis offset + gauss + 3 - - Notch filter 1 bandwidth for gyro - The frequency width of the stop band for the 2nd order notch filter on the primary gyro. See "IMU_GYRO_NF1_FRQ" to activate the filter and to set the notch frequency. Applies to both angular velocity and angular acceleration sent to the controllers. - 0 - 100 - Hz - true + + Magnetometer 0 Z-axis scaling factor + 0.1 + 3.0 + 3 - - Notch filter 2 frequency for gyro - The center frequency for the 2nd order notch filter on the primary gyro. This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. This only affects the signal sent to the controllers, not the estimators. Applies to both angular velocity and angular acceleration sent to the controllers. See "IMU_GYRO_NF1_BW" to set the bandwidth of the filter. A value of 0 disables the filter. - 0 - 1000 - Hz - true + + Magnetometer 1 calibration device ID + Device ID of the magnetometer this calibration applies to. - - Gyro control data maximum publication rate (inner loop rate) - The maximum rate the gyro control data (vehicle_angular_velocity) will be allowed to publish at. This is the loop rate for the rate controller and outputs. Note: sensor data is always read and filtered at the full raw rate (eg commonly 8 kHz) regardless of this setting. - 100 - 2000 - Hz - true - - 100 Hz - 250 Hz - 400 Hz - 800 Hz - 1000 Hz - 2000 Hz - + + Magnetometer 1 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - IMU integration rate - The rate at which raw IMU data is integrated to produce delta angles and delta velocities. Recommended to set this to a multiple of the estimator update period (currently 10 ms for ekf2). - 100 - 1000 - Hz - true + + Magnetometer 1 priority - 100 Hz - 200 Hz - 250 Hz - 400 Hz + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max - - INA220 Power Monitor Config - 0 - 65535 - 1 - 1 - - - INA220 Power Monitor Battery Max Current - 0.1 - 500.0 - 2 - 0.1 + + Magnetometer 1 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - INA220 Power Monitor Regulator Max Current - 0.1 - 500.0 - 2 - 0.1 + + Magnetometer 1 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG1_ROLL, CAL_MAG1_PITCH and CAL_MAG1_YAW. + -1 + 100 + + Internal + No rotation + Yaw 45° + Yaw 90° + Yaw 135° + Yaw 180° + Yaw 225° + Yaw 270° + Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° + Custom Euler Angle + - - INA220 Power Monitor Battery Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - INA220 Power Monitor Regulator Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 X-axis off diagonal scale factor + 3 - - INA226 Power Monitor Config - 0 - 65535 - 1 - 1 + + Magnetometer 1 X-axis offset + gauss + 3 - - INA226 Power Monitor Max Current + + Magnetometer 1 X-axis scaling factor 0.1 - 200.0 - 2 - 0.1 + 3.0 + 3 - - INA226 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG1_ROT to "Custom Euler Angle" + -180 + 180 + deg - - INA228 Power Monitor Config - 0 - 65535 - 1 - 1 + + Magnetometer 1 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - INA228 Power Monitor Max Current - 0.1 - 327.68 - 2 - 0.1 + + Magnetometer 1 Y-axis off diagonal scale factor + 3 - - INA228 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + + Magnetometer 1 Y-axis offset + gauss + 3 - - INA238 Power Monitor Max Current + + Magnetometer 1 Y-axis scaling factor 0.1 - 327.68 - 2 - 0.1 - - - INA238 Power Monitor Shunt - 0.000000001 - 0.1 - 10 - .000000001 + 3.0 + 3 - - PCF8583 rotorfreq (i2c) pulse count - Nmumber of signals per rotation of actuator - 1 - true + + Magnetometer 1 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - PCF8583 rotorfreq (i2c) pool interval - Determines how often the sensor is read out. - us - true + + Magnetometer 1 Z-axis off diagonal scale factor - - PCF8583 rotorfreq (i2c) pulse reset value - Internal device counter is reset to 0 when overrun this value, counter is able to store up to 6 digits reset of counter takes some time - measurement with reset has worse accuracy. 0 means reset counter after every measurement. - true + + Magnetometer 1 Z-axis offset + gauss + 3 - - AFBR Rangefinder Short/Long Range Threshold Hysteresis - This parameter defines the hysteresis for switching between short and long range mode. - 1 - 10 - m + + Magnetometer 1 Z-axis scaling factor + 0.1 + 3.0 + 3 - - AFBR Rangefinder Long Range Rate - This parameter defines measurement rate of the AFBR Rangefinder in long range mode. - 1 - 100 + + Magnetometer 2 calibration device ID + Device ID of the magnetometer this calibration applies to. - - AFBR Rangefinder Mode - This parameter defines the mode of the AFBR Rangefinder. - 0 - 3 - true + + Magnetometer 2 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg + + + Magnetometer 2 priority - Short Range Mode - Long Range Mode - High Speed Short Range Mode - High Speed Long Range Mode + Uninitialized + Disabled + Min + Low + Medium (Default) + High + Max - - AFBR Rangefinder Short Range Rate - This parameter defines measurement rate of the AFBR Rangefinder in short range mode. - 1 - 100 - - - AFBR Rangefinder Short/Long Range Threshold - This parameter defines the threshold for switching between short and long range mode. The mode will switch from short to long range when the distance is greater than the threshold plus the hysteresis. The mode will switch from long to short range when the distance is less than the threshold minus the hysteresis. - 1 - 50 - m - - - QNH for barometer - 500 - 1500 - hPa - - - Baro max rate - Barometric air data maximum publication rate. This is an upper bound, actual barometric data rate is still dependent on the sensor. - 1 - 200 - Hz + + Magnetometer 2 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Board rotation - This parameter defines the rotation of the FMU board relative to the platform. + + Magnetometer 2 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG2_ROLL, CAL_MAG2_PITCH and CAL_MAG2_YAW. -1 - 40 - true + 100 + Internal No rotation Yaw 45° Yaw 90° @@ -22404,362 +13755,125 @@ Roll 90°, Pitch 68°, Yaw 293° Pitch 315° Roll 90°, Pitch 315° + Custom Euler Angle - - Board rotation X (Roll) offset - This parameter defines a rotational offset in degrees around the X (Roll) axis It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Board rotation Y (Pitch) offset - This parameter defines a rotational offset in degrees around the Y (Pitch) axis. It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Board rotation Z (YAW) offset - This parameter defines a rotational offset in degrees around the Z (Yaw) axis. It allows the user to fine tune the board offset in the event of misalignment. - deg - - - Serial Configuration for Lanbao PSK-CM8JL65-CC5 - Configure on which serial port to run Lanbao PSK-CM8JL65-CC5. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Distance Sensor Rotation - Distance Sensor Rotation as MAV_SENSOR_ORIENTATION enum - True - - ROTATION_FORWARD_FACING - ROTATION_RIGHT_FACING - ROTATION_BACKWARD_FACING - ROTATION_LEFT_FACING - ROTATION_UPWARD_FACING - ROTATION_DOWNWARD_FACING - - - - Analog Devices ADIS16448 IMU (external SPI) - 0 - 1 - true - - Disabled - Enabled - - - - Analog Devices ADIS16507 IMU (external SPI) - true - - - Simulate Aux Global Position (AGP) - 0 - 1 - true - - Disabled - Enabled - - - - Enable simulated airspeed sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - ASP5033 differential pressure sensor (external I2C) - true - - - Amphenol AUAV differential / absolute pressure sensor (external I2C) - true - - Sensor disabled, when explicitly started treated as AUAV L05D - AUAV L05D - AUAV L10D - AUAV L30D - - - - Enable simulated barometer sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - SMBUS Smart battery driver BQ40Z50 and BQ40Z80 - true - - - Eagle Tree airspeed sensor (external I2C) - true - - - Enable simulated GPS sinstance - 0 - 1 - true - - Disabled - Enabled - - - - Enable INA220 Power Monitor - For systems a INA220 Power Monitor, this should be set to true - true - - - Enable INA226 Power Monitor - For systems a INA226 Power Monitor, this should be set to true - true - - - Enable INA228 Power Monitor - For systems a INA228 Power Monitor, this should be set to true - true - - - Enable INA238 Power Monitor - For systems a INA238 Power Monitor, this should be set to true - true - - - IR-LOCK Sensor (external I2C) - true - - - Lidar-Lite (LL40LS) - 0 - 2 - true - - Disabled - PWM - I2C - - - - Enable simulated magnetometer sensor instance - 0 - 1 - true - - Disabled - Enabled - - - - Maxbotix Sonar (mb12xx) - true - - - Enable Mappydot rangefinder (i2c) - 0 - 1 - true - - Disabled - Autodetect - - - - TE MS4515 differential pressure sensor (external I2C) - true - - - TE MS4525DO differential pressure sensor (external I2C) - true - - - TE MS5525DSO differential pressure sensor (external I2C) - true - - - PAA3905 Optical Flow - true - - - PAW3902/PAW3903 Optical Flow - true - - - PCF8583 eneable driver - Run PCF8583 driver automatically - 0 - 1 - true - - Disabled - Eneabled - + + Magnetometer 2 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - PGA460 Ultrasonic driver (PGA460) - true + + Magnetometer 2 X-axis off diagonal scale factor + 3 - - PMW3901 Optical Flow - true + + Magnetometer 2 X-axis offset + gauss + 3 - - PX4 Flow Optical Flow - true + + Magnetometer 2 X-axis scaling factor + 0.1 + 3.0 + 3 - - Murata SCH16T IMU (external SPI) - 0 - 1 - true - - Disabled - Enabled - + + Magnetometer 2 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG2_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Sensirion SDP3X differential pressure sensor (external I2C) - true + + Magnetometer 2 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Lightware Laser Rangefinder hardware model (serial) - true - - SF02 - SF10/a - SF10/b - SF10/c - SF11/c - SF30/b - SF30/c - LW20/c - + + Magnetometer 2 Y-axis off diagonal scale factor + 3 - - Lightware SF1xx/SF20/LW20 laser rangefinder (i2c) - 0 - 6 - true - - Disabled - SF10/a - SF10/b - SF10/c - SF11/c - SF/LW20/b - SF/LW20/c - SF/LW30/d - - - - Serial Configuration for Lightware SF45 Rangefinder (serial) - Configure on which serial port to run Lightware SF45 Rangefinder (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - SHT3x temperature and hygrometer - true + + Magnetometer 2 Y-axis offset + gauss + 3 - - Goertek SPA06 Barometer (external I2C) - true + + Magnetometer 2 Y-axis scaling factor + 0.1 + 3.0 + 3 - - Goertek SPL06 Barometer (external I2C) - true + + Magnetometer 2 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - HY-SRF05 / HC-SR05 - true + + Magnetometer 2 Z-axis off diagonal scale factor - - TF02 Pro Distance Sensor (i2c) - true + + Magnetometer 2 Z-axis offset + gauss + 3 - - Thermal control of sensor temperature - - Thermal control unavailable - Thermal control off - Thermal control enabled - + + Magnetometer 2 Z-axis scaling factor + 0.1 + 3.0 + 3 - - TeraRanger Rangefinder (i2c) - 0 - 3 - true + + Magnetometer 3 calibration device ID + Device ID of the magnetometer this calibration applies to. + + + Magnetometer 3 Custom Euler Pitch Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg + + + Magnetometer 3 priority + Uninitialized Disabled - Autodetect - TROne - TREvo60m - TREvo600Hz - TREvo3m + Min + Low + Medium (Default) + High + Max - - VL53L0X Distance Sensor - true - - - VL53L1X Distance Sensor - true - - - External I2C probe - Probe for optional external I2C devices. - - - Optical flow max rate - Optical flow data maximum publication rate. This is an upper bound, actual optical flow data rate is still dependent on the sensor. - 1 - 200 - Hz - true + + Magnetometer 3 Custom Euler Roll Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg - - Optical flow rotation - This parameter defines the yaw rotation of the optical flow relative to the vehicle body frame. Zero rotation is defined as X on flow board pointing towards front of vehicle. + + Magnetometer 3 rotation relative to airframe + An internal sensor will force a value of -1, so a GCS should only attempt to configure the rotation if the value is greater than or equal to zero. +Set to "Custom Euler Angle" to define the rotation using CAL_MAG3_ROLL, CAL_MAG3_PITCH and CAL_MAG3_YAW. + -1 + 100 + Internal No rotation Yaw 45° Yaw 90° @@ -22768,421 +13882,432 @@ Yaw 225° Yaw 270° Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° + Custom Euler Angle - - Optical flow scale factor - 0.5 - 1.5 - 2 - - - Serial Configuration for FT Technologies Digital Wind Sensor (serial) - Configure on which serial port to run FT Technologies Digital Wind Sensor (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - + + Magnetometer 3 X Axis throttle compensation + Coefficient describing linear relationship between +X component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Multi GPS Blending Control Mask - Set bits in the following positions to set which GPS accuracy metrics will be used to calculate the blending weight. Set to zero to disable and always used first GPS instance. 0 : Set to true to use speed accuracy 1 : Set to true to use horizontal position accuracy 2 : Set to true to use vertical position accuracy - 0 - 7 - - use speed accuracy - use hpos accuracy - use vpos accuracy - + + Magnetometer 3 X-axis off diagonal scale factor + 3 - - Multi GPS primary instance - When no blending is active, this defines the preferred GPS receiver instance. The GPS selection logic waits until the primary receiver is available to send data to the EKF even if a secondary instance is already available. The secondary instance is then only used if the primary one times out. To have an equal priority of all the instances, set this parameter to -1 and the best receiver will be used. This parameter has no effect if blending is active. - -1 - 1 + + Magnetometer 3 X-axis offset + gauss + 3 - - Multi GPS Blending Time Constant - Sets the longest time constant that will be applied to the calculation of GPS position and height offsets used to correct data from multiple GPS data for steady state position differences. - 1.0 - 100.0 - s - 1 + + Magnetometer 3 X-axis scaling factor + 0.1 + 3.0 + 3 - - IMU auto calibration - Automatically initialize IMU (accel/gyro) calibration from bias estimates if available. + + Magnetometer 3 Custom Euler Yaw Angle + Setting this parameter changes CAL_MAG3_ROT to "Custom Euler Angle" + -180 + 180 + deg - - IMU notify clipping - Notify the user if the IMU is clipping + + Magnetometer 3 Y Axis throttle compensation + Coefficient describing linear relationship between +Y component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] + 3 - - Sensors hub IMU mode - true - - Disabled - Publish primary IMU selection - + + Magnetometer 3 Y-axis off diagonal scale factor + 3 - - Target IMU temperature - 0 - 85.0 - celcius + + Magnetometer 3 Y-axis offset + gauss 3 - - IMU heater controller feedforward value - 0 - 1.0 - % + + Magnetometer 3 Y-axis scaling factor + 0.1 + 3.0 3 - - IMU heater controller integrator gain value - 0 - 1.0 - us/C + + Magnetometer 3 Z Axis throttle compensation + Coefficient describing linear relationship between +Z component of magnetometer in body frame axis +and either current or throttle depending on value of CAL_MAG_COMP_TYP. +Unit for throttle-based compensation is [G] and +for current-based compensation [G/kA] 3 - - IMU heater controller proportional gain value - 0 - 2.0 - us/C + + Magnetometer 3 Z-axis off diagonal scale factor + + + Magnetometer 3 Z-axis offset + gauss 3 - - Enable internal barometers - For systems with an external barometer, this should be set to false to make sure that the external is used. - true + + Magnetometer 3 Z-axis scaling factor + 0.1 + 3.0 + 3 - - Serial Configuration for LeddarOne Rangefinder - Configure on which serial port to run LeddarOne Rangefinder. - true + + Type of magnetometer compensation Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 + Throttle-based compensation + Current-based compensation (battery_status instance 0) + Current-based compensation (battery_status instance 1) - - Magnetometer auto calibration - Automatically initialize magnetometer calibration from bias estimate if available. + + Differential pressure sensor analog scaling + Pick the appropriate scaling from the datasheet. +this number defines the (linear) conversion from voltage +to Pascal (pa). For the MPXV7002DP this is 1000. +NOTE: If the sensor always registers zero, try switching +the static and dynamic tubes. - - Automatically set external rotations - During calibration attempt to automatically determine the rotation of external magnetometers. + + Differential pressure sensor offset + The offset (zero-reading) in Pascal - - Sensors hub mag mode - true - - Publish all magnetometers - Publish primary magnetometer - + + Reverse differential pressure sensor readings + Reverse the raw measurements of all differential pressure sensors. +This can be enabled if the sensors have static and dynamic ports swapped. - - Magnetometer max rate - Magnetometer data maximum publication rate. This is an upper bound, actual magnetometer data rate is still dependent on the sensor. - 1 - 200 - Hz - true + + Maximum height above ground when reliant on optical flow + This parameter defines the maximum distance from ground at which the optical flow sensor operates reliably. +The height setpoint will be limited to be no greater than this value when the navigation system +is completely reliant on optical flow data and the height above ground estimate is valid. +The sensor may be usable above this height, but accuracy will progressively degrade. + 1.0 + 100.0 + m + 2 + 0.1 - - Bitfield selecting mag sides for calibration - If set to two side calibration, only the offsets are estimated, the scale calibration is left unchanged. Thus an initial six side calibration is recommended. Bits: ORIENTATION_TAIL_DOWN = 1 ORIENTATION_NOSE_DOWN = 2 ORIENTATION_LEFT = 4 ORIENTATION_RIGHT = 8 ORIENTATION_UPSIDE_DOWN = 16 ORIENTATION_RIGHTSIDE_UP = 32 - 34 - 63 - - Two side calibration - Three side calibration - Six side calibration - + + Magnitude of maximum angular flow rate reliably measurable by the optical flow sensor + Optical flow data will not fused by the estimators if the magnitude of the flow rate exceeds this value and +control loops will be instructed to limit ground speed such that the flow rate produced by movement over ground +is less than 50% of this value. + 1.0 + rad/s + 2 - - MaxBotix MB12XX Sensor 0 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + Minimum height above ground when reliant on optical flow + This parameter defines the minimum distance from ground at which the optical flow sensor operates reliably. +The sensor may be usable below this height, but accuracy will progressively reduce to loss of focus. + 0.0 + 1.0 + m + 2 + 0.1 - - MaxBotix MB12XX Sensor 10 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true + + + + Airspeed sensor compensation model for the SDP3x + Model with Pitot +CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. +Model without Pitot (1.5 mm tubes) +CAL_AIR_TUBED_MM: Not used, 1.5 mm tubes assumed. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor. +Tube Pressure Drop +CAL_AIR_TUBED_MM: Diameter in mm of the pitot and tubes, must have the same diameter. +CAL_AIR_TUBELEN: Length of the tubes connecting the pitot to the sensor and the static + dynamic port length of the pitot. - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Model with Pitot + Model without Pitot (1.5 mm tubes) + Tube Pressure Drop - - MaxBotix MB12XX Sensor 12 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + Airspeed sensor tube diameter. Only used for the Tube Pressure Drop Compensation + 1.5 + 100 + mm + + + Airspeed sensor tube length + See the CAL_AIR_CMODEL explanation on how this parameter should be set. + 0.01 + 2.00 + m + + + For legacy QGC support only + Use SENS_MAG_SIDES instead - - MaxBotix MB12XX Sensor 1 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Low pass filter cutoff frequency for accel + The cutoff frequency for the 2nd order butterworth filter on the primary accelerometer. +This only affects the signal sent to the controllers, not the estimators. 0 disables the filter. 0 - 7 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 2 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Cutoff frequency for angular acceleration (D-Term filter) + The cutoff frequency for the 2nd order butterworth filter used on +the time derivative of the measured angular velocity, also known as +the D-term filter in the rate controller. The D-term uses the derivative of +the rate and thus is the most susceptible to noise. Therefore, using +a D-term filter allows to increase IMU_GYRO_CUTOFF, which +leads to reduced control latency and permits to increase the P gains. +A value of 0 disables the filter. 0 - 7 + 1000 + Hz + 1 + 0.1 + false + + + IMU gyro auto calibration enable true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 3 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Low pass filter cutoff frequency for gyro + The cutoff frequency for the 2nd order butterworth filter on the primary gyro. +This only affects the angular velocity sent to the controllers, not the estimators. +It applies also to the angular acceleration (D-Term filter), see IMU_DGYRO_CUTOFF. +A value of 0 disables the filter. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 1000 + Hz + 1 + 0.1 + false + + + IMU gyro ESC notch filter bandwidth + Bandwidth per notch filter when using dynamic notch filtering with ESC RPM. + 5 + 30 + Hz + 1 + 0.1 - - MaxBotix MB12XX Sensor 4 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + IMU gyro dynamic notch filtering + Enable bank of dynamically updating notch filters. +Requires ESC RPM feedback or onboard FFT (IMU_GYRO_FFT_EN). 0 + 3 + + ESC RPM + FFT + + + + IMU gyro dynamic notch filter harmonics + ESC RPM number of harmonics (multiples of RPM) for ESC RPM dynamic notch filtering. + 1 7 + + + IMU gyro dynamic notch filter minimum frequency + Minimum notch filter frequency in Hz. + Hz + 1 + 0.1 + + + IMU gyro FFT enable true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 5 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT length + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 256 + 512 + 1024 + 4096 - - MaxBotix MB12XX Sensor 6 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT maximum frequency + 1 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 7 Rotation - This parameter defines the rotation of the sensor relative to the platform. - 0 - 7 + + IMU gyro FFT minimum frequency + 1 + 1000 + Hz true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - - - MaxBotix MB12XX Sensor 8 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + IMU gyro FFT SNR + 1 + 30 + + + Notch filter bandwidth for gyro + The frequency width of the stop band for the 2nd order notch filter on the primary gyro. +See "IMU_GYRO_NF0_FRQ" to activate the filter and to set the notch frequency. +Applies to both angular velocity and angular acceleration sent to the controllers. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 100 + Hz + 1 + 0.1 + false - - MaxBotix MB12XX Sensor 9 Rotation - This parameter defines the rotation of the sensor relative to the platform. + + Notch filter frequency for gyro + The center frequency for the 2nd order notch filter on the primary gyro. +This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. +This only affects the signal sent to the controllers, not the estimators. +Applies to both angular velocity and angular acceleration sent to the controllers. +See "IMU_GYRO_NF0_BW" to set the bandwidth of the filter. +A value of 0 disables the filter. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 1000 + Hz + 1 + 0.1 + false - - MappyDot Sensor 0 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Notch filter 1 bandwidth for gyro + The frequency width of the stop band for the 2nd order notch filter on the primary gyro. +See "IMU_GYRO_NF1_FRQ" to activate the filter and to set the notch frequency. +Applies to both angular velocity and angular acceleration sent to the controllers. 0 - 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + 100 + Hz + 1 + 0.1 + false - - MappyDot Sensor 10 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Notch filter 2 frequency for gyro + The center frequency for the 2nd order notch filter on the primary gyro. +This filter can be enabled to avoid feedback amplification of structural resonances at a specific frequency. +This only affects the signal sent to the controllers, not the estimators. +Applies to both angular velocity and angular acceleration sent to the controllers. +See "IMU_GYRO_NF1_BW" to set the bandwidth of the filter. +A value of 0 disables the filter. 0 - 7 + 1000 + Hz + 1 + 0.1 + false + + + Gyro control data maximum publication rate (inner loop rate) + The maximum rate the gyro control data (vehicle_angular_velocity) will be +allowed to publish at. This is the loop rate for the rate controller and outputs. +Note: sensor data is always read and filtered at the full raw rate (eg commonly 8 kHz) regardless of this setting. + 100 + 2000 + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 100 Hz + 250 Hz + 400 Hz + 800 Hz + 1000 Hz + 2000 Hz - - MappyDot Sensor 12 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + IMU integration rate + The rate at which raw IMU data is integrated to produce delta angles and delta velocities. +Recommended to set this to a multiple of the estimator update period (currently 10 ms for ekf2). + 100 + 1000 + Hz true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + 100 Hz + 200 Hz + 250 Hz + 400 Hz - - MappyDot Sensor 1 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + QNH for barometer + 500 + 1500 + hPa + + + Baro max rate + Barometric air data maximum publication rate. This is an upper bound, +actual barometric data rate is still dependent on the sensor. + 1 + 200 + Hz + + + Barometer auto calibration + Automatically calibrate barometer based on the GNSS height + + + Board rotation + This parameter defines the rotation of the FMU board relative to the platform. + -1 + 40 true No rotation @@ -23193,116 +14318,145 @@ Yaw 225° Yaw 270° Yaw 315° + Roll 180° + Roll 180°, Yaw 45° + Roll 180°, Yaw 90° + Roll 180°, Yaw 135° + Pitch 180° + Roll 180°, Yaw 225° + Roll 180°, Yaw 270° + Roll 180°, Yaw 315° + Roll 90° + Roll 90°, Yaw 45° + Roll 90°, Yaw 90° + Roll 90°, Yaw 135° + Roll 270° + Roll 270°, Yaw 45° + Roll 270°, Yaw 90° + Roll 270°, Yaw 135° + Pitch 90° + Pitch 270° + Pitch 180°, Yaw 90° + Pitch 180°, Yaw 270° + Roll 90°, Pitch 90° + Roll 180°, Pitch 90° + Roll 270°, Pitch 90° + Roll 90°, Pitch 180° + Roll 270°, Pitch 180° + Roll 90°, Pitch 270° + Roll 180°, Pitch 270° + Roll 270°, Pitch 270° + Roll 90°, Pitch 180°, Yaw 90° + Roll 90°, Yaw 270° + Roll 90°, Pitch 68°, Yaw 293° + Pitch 315° + Roll 90°, Pitch 315° - - MappyDot Sensor 2 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Board rotation X (roll) offset + Rotation from flight controller board to vehicle body frame. +This parameter gets set during the "level horizon" calibration or can be +set manually. + -45.0 + 45.0 + deg + 1 + + + Board rotation Y (pitch) offset + Rotation from flight controller board to vehicle body frame. +This parameter gets set during the "level horizon" calibration or can be +set manually. + -45.0 + 45.0 + deg + 1 + + + Board rotation Z (yaw) offset + Rotation from flight controller board to vehicle body frame. +Has to be set manually (not set by any calibration). + -45.0 + 45.0 + deg + 1 + + + Simulate Aux Global Position (AGP) 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 3 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated airspeed sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 4 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated barometer sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 5 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated GPS sinstance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 6 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Enable simulated magnetometer sensor instance 0 - 7 + 1 true - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Disabled + Enabled - - MappyDot Sensor 7 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 - true + + Thermal control of sensor temperature - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° + Thermal control unavailable + Thermal control off + Thermal control enabled - - MappyDot Sensor 8 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. - 0 - 7 + + External I2C probe + Probe for optional external I2C devices. + + + Optical flow max rate + Optical flow data maximum publication rate. This is an upper bound, +actual optical flow data rate is still dependent on the sensor. + 1 + 200 + Hz true + + + Optical flow rotation + This parameter defines the yaw rotation of the optical flow relative to the vehicle body frame. +Zero rotation is defined as X on flow board pointing towards front of vehicle. No rotation Yaw 45° @@ -23314,168 +14468,113 @@ Yaw 315° - - MappyDot Sensor 9 Rotation - This parameter defines the rotation of the Mappydot sensor relative to the platform. + + Optical flow scale factor + 0.5 + 1.5 + 2 + + + Multi GPS Blending Control Mask + Set bits in the following positions to set which GPS accuracy metrics will be used to calculate the blending weight. Set to zero to disable and always used first GPS instance. +0 : Set to true to use speed accuracy +1 : Set to true to use horizontal position accuracy +2 : Set to true to use vertical position accuracy 0 7 - true - - No rotation - Yaw 45° - Yaw 90° - Yaw 135° - Yaw 180° - Yaw 225° - Yaw 270° - Yaw 315° - + + use speed accuracy + use hpos accuracy + use vpos accuracy + + + + Multi GPS primary instance + When no blending is active, this defines the preferred GPS receiver instance. +The GPS selection logic waits until the primary receiver is available to +send data to the EKF even if a secondary instance is already available. +The secondary instance is then only used if the primary one times out. +Accepted values: +-1 : Auto (equal priority for all instances) +0 : Main serial GPS instance +1 : Secondary serial GPS instance +2-127 : UAVCAN module node ID +This parameter has no effect if blending is active. + -1 + 127 + + + Multi GPS Blending Time Constant + Sets the longest time constant that will be applied to the calculation of GPS position and height offsets used to correct data from multiple GPS data for steady state position differences. + 1.0 + 100.0 + s + 1 + + + IMU auto calibration + Automatically initialize IMU (accel/gyro) calibration from bias estimates if available. - - Analog Devices ADIS16448 IMU Orientation(external SPI) - 0 - 101 - true - - ROTATION_NONE - ROTATION_YAW_180 - + + IMU notify clipping + Notify the user if the IMU is clipping - - Serial Configuration for Lightware Laser Rangefinder (serial) - Configure on which serial port to run Lightware Laser Rangefinder (serial). - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Target IMU device ID to regulate temperature - - - Serial Configuration for ThoneFlow-3901U optical flow sensor - Configure on which serial port to run ThoneFlow-3901U optical flow sensor. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for Benewake TFmini Rangefinder - Configure on which serial port to run Benewake TFmini Rangefinder. + + Sensors hub IMU mode true Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for uLanding Radar - Configure on which serial port to run uLanding Radar. + Publish primary IMU selection + + + + Enable internal barometers + For systems with an external barometer, this should be set to false to make sure that the external is used. true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for VectorNav (VN-100, VN-200, VN-300) - Configure on which serial port to run VectorNav (VN-100, VN-200, VN-300). + + + Magnetometer auto calibration + Automatically initialize magnetometer calibration from bias estimate if available. + + + Automatically set external rotations + During calibration attempt to automatically determine the rotation of external magnetometers. + + + Sensors hub mag mode true - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Orientation upright or facing downward - The SF45 mounted facing upward or downward on the frame - True - - Rotation upward - Rotation downward + Publish all magnetometers + Publish primary magnetometer - - Update rate in Hz - The SF45 sets the update rate in Hz to allow greater resolution - True - - 50hz - 100hz - 200hz - 400hz - 500hz - 625hz - 1000hz - 1250hz - 1538hz - 2000hz - 2500hz - 5000hz - - - - Sensor facing forward or backward - The usb port on the sensor indicates 180deg, opposite usb is forward facing - True + + Magnetometer max rate + Magnetometer data maximum publication rate. This is an upper bound, +actual magnetometer data rate is still dependent on the sensor. + 1 + 200 + Hz + true + + + Bitfield selecting mag sides for calibration + If set to two side calibration, only the offsets are estimated, the scale +calibration is left unchanged. Thus an initial six side calibration is +recommended. +Bits: +ORIENTATION_TAIL_DOWN = 1 +ORIENTATION_NOSE_DOWN = 2 +ORIENTATION_LEFT = 4 +ORIENTATION_RIGHT = 8 +ORIENTATION_UPSIDE_DOWN = 16 +ORIENTATION_RIGHTSIDE_UP = 32 + 34 + 63 - Rotation forward - Rotation right - Rotation backward - Rotation left + Two side calibration + Three side calibration + Six side calibration @@ -23488,40 +14587,19 @@ Enabled - - VectorNav driver mode - INS or sensors - - Sensors Only (default) - INS - - - - VOXL Power Monitor Shunt, Battery - 0.000000001 - 0.1 - 10 - .000000001 - true - - - VOXL Power Monitor Shunt, Regulator - 0.000000001 - 0.1 - 10 - .000000001 - true - Toggle automatic receiver configuration - By default, the receiver is automatically configured. Sometimes it may be used for multiple purposes. If the offered parameters aren't sufficient, this parameter can be disabled to have full control of the receiver configuration. A good way to use this is to enable automatic configuration, let the receiver be configured, and then disable it to make manual adjustments. + By default, the receiver is automatically configured. Sometimes it may be used for multiple purposes. +If the offered parameters aren't sufficient, this parameter can be disabled to have full control of the receiver configuration. +A good way to use this is to enable automatic configuration, let the receiver be configured, and then disable it to make manual adjustments. True Usage of different constellations - Choice of which constellations the receiver should use for PVT computation. When this is 0, the constellation usage isn't changed. + Choice of which constellations the receiver should use for PVT computation. +When this is 0, the constellation usage isn't changed. 0 63 True @@ -23535,7 +14613,8 @@ Log GPS communication data - Log raw communication between the driver and connected receivers. For example, "To receiver" will log all commands and corrections sent by the driver to the receiver. + Log raw communication between the driver and connected receivers. +For example, "To receiver" will log all commands and corrections sent by the driver to the receiver. 0 3 @@ -23547,7 +14626,9 @@ Setup and expected use of the hardware - Setup and expected use of the hardware. - Default: Use two receivers as completely separate instances. - Moving base: Use two receivers in a rover & moving base setup for heading. + Setup and expected use of the hardware. +- Default: Use two receivers as completely separate instances. +- Moving base: Use two receivers in a rover & moving base setup for heading. 0 1 True @@ -23609,51 +14690,17 @@ Pitch offset for dual antenna GPS - Vertical offsets can be compensated for by adjusting the Pitch offset. Note that this can be interpreted as the "roll" angle in case the antennas are aligned along the perpendicular axis. This occurs in situations where the two antenna ARPs may not be exactly at the same height in the vehicle reference frame. Since pitch is defined as the right-handed rotation about the vehicle Y axis, a situation where the main antenna is mounted lower than the aux antenna (assuming the default antenna setup) will result in a positive pitch. + Vertical offsets can be compensated for by adjusting the Pitch offset. +Note that this can be interpreted as the "roll" angle in case the antennas are aligned along the perpendicular axis. +This occurs in situations where the two antenna ARPs may not be exactly at the same height in the vehicle reference frame. +Since pitch is defined as the right-handed rotation about the vehicle Y axis, +a situation where the main antenna is mounted lower than the aux antenna (assuming the default antenna setup) will result in a positive pitch. -90 90 deg 3 True - - Serial Configuration for GPS Port - Configure on which serial port to run GPS Port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for Secondary GPS port - Configure on which serial port to run Secondary GPS port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - Enable sat info Enable publication of satellite info (ORB_ID(satellite_info)) if possible. @@ -23661,21 +14708,28 @@ Logging stream used during automatic configuration - The stream the autopilot sets up on the receiver to output the logging data. Set this to another value if the default stream is already used for another purpose. + The stream the autopilot sets up on the receiver to output the logging data. +Set this to another value if the default stream is already used for another purpose. 1 10 True Main stream used during automatic configuration - The stream the autopilot sets up on the receiver to output the main data. Set this to another value if the default stream is already used for another purpose. + The stream the autopilot sets up on the receiver to output the main data. +Set this to another value if the default stream is already used for another purpose. 1 10 True Heading/Yaw offset for dual antenna GPS - Heading offset angle for dual antenna GPS setups that support heading estimation. Set this to 0 if the antennas are parallel to the forward-facing direction of the vehicle and the rover antenna is in front. The offset angle increases clockwise. Set this to 90 if the rover antenna is placed on the right side of the vehicle and the moving base antenna is on the left side. + Heading offset angle for dual antenna GPS setups that support heading estimation. +Set this to 0 if the antennas are parallel to the forward-facing direction +of the vehicle and the rover antenna is in front. +The offset angle increases clockwise. +Set this to 90 if the rover antenna is placed on the +right side of the vehicle and the moving base antenna is on the left side. -360 360 deg @@ -23683,486 +14737,6 @@ True - - - Serial Configuration for CRSF RC Input Driver - Configure on which serial port to run CRSF RC Input Driver. Crossfire RC (CRSF) driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for DSM RC Input Driver - Configure on which serial port to run DSM RC Input Driver. DSM RC (Spektrum) driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for GHST RC Input Driver - Configure on which serial port to run GHST RC Input Driver. Ghost (GHST) RC driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for RC Input Driver - Configure on which serial port to run RC Input Driver. Setting this to 'Disabled' will use a board-specific default port for RC input. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for SBUS RC Input Driver - Configure on which serial port to run SBUS RC Input Driver. SBUS RC driver. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Baudrate for the EXT2 Serial Port - Configure the Baudrate for the EXT2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 1 Serial Port - Configure the Baudrate for the GPS 1 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 2 Serial Port - Configure the Baudrate for the GPS 2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the GPS 3 Serial Port - Configure the Baudrate for the GPS 3 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - MXS Serial Communication Baud rate - Baudrate for the Serial Port connected to the MXS Transponder - 0 - 10 - true - - 38400 - 600 - 4800 - 9600 - RESERVED - 57600 - 115200 - 230400 - 19200 - 460800 - 921600 - - - - Baudrate for the Radio Controller Serial Port - Configure the Baudrate for the Radio Controller Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 1 Serial Port - Configure the Baudrate for the TELEM 1 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 2 Serial Port - Configure the Baudrate for the TELEM 2 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM 3 Serial Port - Configure the Baudrate for the TELEM 3 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the TELEM/SERIAL 4 Serial Port - Configure the Baudrate for the TELEM/SERIAL 4 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the UART 6 Serial Port - Configure the Baudrate for the UART 6 Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - - Baudrate for the Wifi Port Serial Port - Configure the Baudrate for the Wifi Port Serial Port. Note: certain drivers such as the GPS can determine the Baudrate automatically. - true - - Auto - 50 8N1 - 75 8N1 - 110 8N1 - 134 8N1 - 150 8N1 - 200 8N1 - 300 8N1 - 600 8N1 - 1200 8N1 - 1800 8N1 - 2400 8N1 - 4800 8N1 - 9600 8N1 - 19200 8N1 - 38400 8N1 - 57600 8N1 - 115200 8N1 - 230400 8N1 - 460800 8N1 - 500000 8N1 - 921600 8N1 - 1000000 8N1 - 1500000 8N1 - 2000000 8N1 - 3000000 8N1 - - - distance sensor maximum range @@ -24187,7 +14761,8 @@ Vehicle inertia about X axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24195,21 +14770,24 @@ Vehicle cross term inertia xy - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle cross term inertia xz - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle inertia about Y axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24217,14 +14795,16 @@ Vehicle cross term inertia yz - The inertia is a 3 by 3 symmetric matrix. This value can be set to 0 for a quad symmetric about its center of mass. + The inertia is a 3 by 3 symmetric matrix. +This value can be set to 0 for a quad symmetric about its center of mass. kg m^2 3 0.005 Vehicle inertia about Z axis - The inertia is a 3 by 3 symmetric matrix. It represents the difficulty of the vehicle to modify its angular rate. + The inertia is a 3 by 3 symmetric matrix. +It represents the difficulty of the vehicle to modify its angular rate. 0.0 kg m^2 3 @@ -24232,7 +14812,10 @@ First order drag coefficient - Physical coefficient representing the friction with air particules. The greater this value, the slower the quad will move. Drag force function of velocity: D=-KDV*V. The maximum freefall velocity can be computed as V=10*MASS/KDV [m/s] + Physical coefficient representing the friction with air particules. +The greater this value, the slower the quad will move. +Drag force function of velocity: D=-KDV*V. +The maximum freefall velocity can be computed as V=10*MASS/KDV [m/s] 0.0 N/(m/s) 2 @@ -24240,7 +14823,10 @@ First order angular damper coefficient - Physical coefficient representing the friction with air particules during rotations. The greater this value, the slower the quad will rotate. Aerodynamic moment function of body rate: Ma=-KDW*W_B. This value can be set to 0 if unknown. + Physical coefficient representing the friction with air particules during rotations. +The greater this value, the slower the quad will rotate. +Aerodynamic moment function of body rate: Ma=-KDW*W_B. +This value can be set to 0 if unknown. 0.0 Nm/(rad/s) 3 @@ -24248,7 +14834,11 @@ Initial AMSL ground altitude - This value represents the Above Mean Sea Level (AMSL) altitude where the simulation begins. If using FlightGear as a visual animation, this value can be tweaked such that the vehicle lies on the ground at takeoff. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the Above Mean Sea Level (AMSL) altitude where the simulation begins. +If using FlightGear as a visual animation, +this value can be tweaked such that the vehicle lies on the ground at takeoff. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -420.0 8848.0 m @@ -24257,21 +14847,27 @@ Initial geodetic latitude - This value represents the North-South location on Earth where the simulation begins. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the North-South location on Earth where the simulation begins. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -90 90 deg Initial geodetic longitude - This value represents the East-West location on Earth where the simulation begins. LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others to represent a physical ground location on Earth. + This value represents the East-West location on Earth where the simulation begins. +LAT0, LON0, H0, MU_X, MU_Y, and MU_Z should ideally be consistent among each others +to represent a physical ground location on Earth. -180 180 deg Pitch arm length - This is the arm length generating the pitching moment This value can be measured with a ruler. This corresponds to half the distance between the front and rear motors. + This is the arm length generating the pitching moment +This value can be measured with a ruler. +This corresponds to half the distance between the front and rear motors. 0.0 m 2 @@ -24279,7 +14875,9 @@ Roll arm length - This is the arm length generating the rolling moment This value can be measured with a ruler. This corresponds to half the distance between the left and right motors. + This is the arm length generating the rolling moment +This value can be measured with a ruler. +This corresponds to half the distance between the left and right motors. 0.0 m 2 @@ -24295,7 +14893,9 @@ Max propeller torque - This is the maximum torque delivered by one propeller when the motor is running at full speed. This value is usually about few percent of the maximum thrust force. + This is the maximum torque delivered by one propeller +when the motor is running at full speed. +This value is usually about few percent of the maximum thrust force. 0.0 Nm 3 @@ -24303,7 +14903,9 @@ Max propeller thrust force - This is the maximum force delivered by one propeller when the motor is running at full speed. This value is usually about 5 times the mass of the quadrotor. + This is the maximum force delivered by one propeller +when the motor is running at full speed. +This value is usually about 5 times the mass of the quadrotor. 0.0 N 2 @@ -24318,16 +14920,28 @@ Vehicle type true - Multicopter + Quadcopter Fixed-Wing Tailsitter + Standard VTOL + Hexacopter + Rover Ackermann + + Wind velocity from east direction + m/s + + + Wind velocity from north direction + m/s + AGP failure mode - Stuck: freeze the measurement to the current location Drift: add a linearly growing bias to the sensor data + Stuck: freeze the measurement to the current location +Drift: add a linearly growing bias to the sensor data 0 3 @@ -24363,7 +14977,9 @@ Automatically configure default values - Set to 1 to reset parameters on next system startup (setting defaults). Platform-specific values are used if available. RC* parameters are preserved. + Set to 1 to reset parameters on next system startup (setting defaults). +Platform-specific values are used if available. +RC* parameters are preserved. Keep parameters Reset parameters to airframe defaults @@ -24378,30 +14994,49 @@ Bootloader update - If enabled, update the bootloader on the next boot. WARNING: do not cut the power during an update process, otherwise you will have to recover using some alternative method (e.g. JTAG). Instructions: - Insert an SD card - Enable this parameter - Reboot the board (plug the power or send a reboot command) - Wait until the board comes back up (or at least 2 minutes) - If it does not come back, check the file bootlog.txt on the SD card + If enabled, update the bootloader on the next boot. +WARNING: do not cut the power during an update process, otherwise you will +have to recover using some alternative method (e.g. JTAG). +Instructions: +- Insert an SD card +- Enable this parameter +- Reboot the board (plug the power or send a reboot command) +- Wait until the board comes back up (or at least 2 minutes) +- If it does not come back, check the file bootlog.txt on the SD card true Enable auto start of accelerometer thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Enable auto start of barometer thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Enable auto start of rate gyro thermal calibration at the next power up - 0 : Set to 0 to do nothing 1 : Set to 1 to start a calibration at next boot This parameter is reset to zero when the temperature calibration starts. default (0, no calibration) + 0 : Set to 0 to do nothing +1 : Set to 1 to start a calibration at next boot +This parameter is reset to zero when the temperature calibration starts. +default (0, no calibration) 0 1 Required temperature rise during thermal calibration - A temperature increase greater than this value is required during calibration. Calibration will complete for each sensor when the temperature increase above the starting temperature exceeds the value set by SYS_CAL_TDEL. If the temperature rise is insufficient, the calibration will continue indefinitely and the board will need to be repowered to exit. + A temperature increase greater than this value is required during calibration. +Calibration will complete for each sensor when the temperature increase above the starting temperature exceeds the value set by SYS_CAL_TDEL. +If the temperature rise is insufficient, the calibration will continue indefinitely and the board will need to be repowered to exit. 10 celcius @@ -24417,16 +15052,21 @@ Dataman storage backend + If the board supports persistent storage (i.e., the KConfig variable DATAMAN_PERSISTENT_STORAGE is set), +the 'Default storage' backend uses a file on persistent storage. If not supported, this backend uses +non-persistent storage in RAM. true - Disabled - default (SD card) - RAM (not persistent) + Dataman disabled + Default storage + RAM storage Enable factory calibration mode - If enabled, future sensor calibrations will be stored to /fs/mtd_caldata. Note: this is only supported on boards with a separate calibration storage /fs/mtd_caldata. + If enabled, future sensor calibrations will be stored to /fs/mtd_caldata. +Note: this is only supported on boards with a separate calibration storage +/fs/mtd_caldata. Disabled All sensors @@ -24435,38 +15075,59 @@ Enable failure injection - If enabled allows MAVLink INJECT_FAILURE commands. WARNING: the failures can easily cause crashes and are to be used with caution! + If enabled allows MAVLink INJECT_FAILURE commands. +WARNING: the failures can easily cause crashes and are to be used with caution! Control if the vehicle has a barometer - Disable this if the board has no barometer, such as some of the Omnibus F4 SD variants. If disabled, the preflight checks will not check for the presence of a barometer. + Disable this if the board has no barometer, such as some of the Omnibus +F4 SD variants. +If disabled, the preflight checks will not check for the presence of a +barometer. true Control if the vehicle has a GPS - Disable this if the system has no GPS. If disabled, the sensors hub will not process sensor_gps, and GPS will not be available for the rest of the system. + Disable this if the system has no GPS. +If disabled, the sensors hub will not process sensor_gps, +and GPS will not be available for the rest of the system. true - Control if the vehicle has a magnetometer - Set this to 0 if the board has no magnetometer. If set to 0, the preflight checks will not check for the presence of a magnetometer, otherwise N sensors are required. + Control if and how many magnetometers are expected + 0: System has no magnetometer, preflight checks should pass without one. +1-N: Require the presence of N magnetometer sensors for check to pass. true Control if the vehicle has an airspeed sensor - Set this to 0 if the board has no airspeed sensor. If set to 0, the preflight checks will not check for the presence of an airspeed sensor. + Set this to 0 if the board has no airspeed sensor. +If set to 0, the preflight checks will not check for the presence of an +airspeed sensor. 0 1 Number of distance sensors to check being available - The preflight check will fail if fewer than this number of distance sensors with valid data is present. Disable the check with 0. + The preflight check will fail if fewer than this number of distance sensors with valid data is present. +Disable the check with 0. 0 4 + + Number of optical flow sensors required to be available + The preflight check will fail if fewer than this number of optical flow sensors with valid data are present. + 0 + 1 + Enable HITL/SIH mode on next boot - While enabled the system will boot in Hardware-In-The-Loop (HITL) or Simulation-In-Hardware (SIH) mode and not enable all sensors and checks. When disabled the same vehicle can be flown normally. Set to 'external HITL', if the system should perform as if it were a real vehicle (the only difference to a real system is then only the parameter value, which can be used for log analysis). + While enabled the system will boot in Hardware-In-The-Loop (HITL) +or Simulation-In-Hardware (SIH) mode and not enable all sensors and checks. +When disabled the same vehicle can be flown normally. +Set to 'external HITL', if the system should perform as if it were a real +vehicle (the only difference to a real system is then only the parameter +value, which can be used for log analysis). true external HITL @@ -24477,7 +15138,10 @@ Parameter version - This is used internally only: an airframe configuration might set an expected parameter version value via PARAM_DEFAULTS_VER. This is checked on bootup against SYS_PARAM_VER, and if they do not match, parameters are reset and reloaded from the airframe configuration. + This is used internally only: an airframe configuration might set an expected +parameter version value via PARAM_DEFAULTS_VER. This is checked on bootup +against SYS_PARAM_VER, and if they do not match, parameters are reset and +reloaded from the airframe configuration. 0 @@ -24489,51 +15153,6 @@ Enable stack checking - - - Blacksheep telemetry Enable - If true, the FMU will try to connect to Blacksheep telemetry on start up - true - - - Serial Configuration for FrSky Telemetry - Configure on which serial port to run FrSky Telemetry. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Serial Configuration for HoTT Telemetry - Configure on which serial port to run HoTT Telemetry. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - TEST_1 @@ -25304,307 +15923,60 @@ true - - - Sagetech External Configuration Mode - Disables auto-configuration mode enabling MXS config through external software. - true - - - Sagetech MXS mode configuration - This parameter defines the operating mode of the MXS - 0 - 3 - false - - Off - On - Standby - Alt - - - - Serial Configuration for Sagetech MXS Serial Port - Configure on which serial port to run Sagetech MXS Serial Port. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - Sagetech MXS Participant Configuration - The MXS communication port to receive Target data from - 0 - 2 - false - - Auto - COM0 - COM1 - - - - - - UAVCAN CAN bus bitrate - 20000 - 1000000 - - - Enable MovingBaselineData publication - true - - - Enable MovingBaselineData subscription - 1 - true - - - Enable RTCM subscription - true - - - CAN built-in bus termination - 1 - - - Simulator Gazebo bridge enable - true - - - UAVCAN CAN bus bitrate - 20000 - 1000000 - bit/s - true - - - UAVCAN fuel tank fuel type - This parameter defines the type of fuel used in the vehicle's fuel tank. 0: Unknown 1: Liquid (e.g., gasoline, diesel) 2: Gas (e.g., hydrogen, methane, propane) - 0 - 2 - true - - Unknown - Liquid - Gas - - - - UAVCAN fuel tank maximum capacity - This parameter defines the maximum fuel capacity of the vehicle's fuel tank. - 0.0 - 100000.0 - liters - 1 - 0.1 - true - - - UAVCAN mode - 0 - UAVCAN disabled. 1 - Enables support for UAVCAN sensors without dynamic node ID allocation and firmware update. 2 - Enables support for UAVCAN sensors with dynamic node ID allocation and firmware update. 3 - Enables support for UAVCAN sensors and actuators with dynamic node ID allocation and firmware update. Also sets the motor control outputs to UAVCAN. - 0 - 3 - true - - Disabled - Sensors Manual Config - Sensors Automatic Config - Sensors and Actuators (ESCs) Automatic Config - - - - UAVCAN ANTI_COLLISION light operating mode - This parameter defines the minimum condition under which the system will command the ANTI_COLLISION lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN LIGHT_ID_LANDING light operating mode - This parameter defines the minimum condition under which the system will command the LIGHT_ID_LANDING lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN RIGHT_OF_WAY light operating mode - This parameter defines the minimum condition under which the system will command the RIGHT_OF_WAY lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on - 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN STROBE light operating mode - This parameter defines the minimum condition under which the system will command the STROBE lights on 0 - Always off 1 - When autopilot is armed 2 - When autopilot is prearmed 3 - Always on + + + Height rc-button down 0 - 3 - true - - Always off - When autopilot is armed - When autopilot is prearmed - Always on - - - - UAVCAN Node ID - Read the specs at http://uavcan.org to learn more about Node ID. - 1 - 125 - true - - - publish Arming Status stream - Enable UAVCAN Arming Status stream publication uavcan::equipment::safety::ArmingStatus - true - - - publish moving baseline data RTCM stream - Enable UAVCAN RTCM stream publication ardupilot::gnss::MovingBaselineData - true - - - publish RTCM stream - Enable UAVCAN RTCM stream publication uavcan::equipment::gnss::RTCMStream - true - - - UAVCAN rangefinder maximum range - This parameter defines the maximum valid range for a rangefinder connected via UAVCAN. - m - - - UAVCAN rangefinder minimum range - This parameter defines the minimum valid range for a rangefinder connected via UAVCAN. - m - - - subscription airspeed - Enable UAVCAN airspeed subscriptions. uavcan::equipment::air_data::IndicatedAirspeed uavcan::equipment::air_data::TrueAirspeed uavcan::equipment::air_data::StaticTemperature - true - - - subscription barometer - Enable UAVCAN barometer subscription. uavcan::equipment::air_data::StaticPressure uavcan::equipment::air_data::StaticTemperature - true + 16 - - subscription battery - Enable UAVCAN battery subscription. uavcan::equipment::power::BatteryInfo ardupilot::equipment::power::BatteryInfoAux 0 - Disable 1 - Use raw data. Recommended for Smart battery 2 - Filter the data with internal battery library + + Height rc-button up 0 - 2 - true - - Disable - Raw data - Filter data - - - - subscription button - Enable UAVCAN button subscription. ardupilot::indication::Button - true - - - subscription differential pressure - Enable UAVCAN differential pressure subscription. uavcan::equipment::air_data::RawAirData - true - - - subscription flow - Enable UAVCAN optical flow subscription. - true - - - subscription fuel tank - Enable UAVCAN fuel tank status subscription. - true - - - subscription GPS - Enable UAVCAN GPS subscriptions. uavcan::equipment::gnss::Fix uavcan::equipment::gnss::Fix2 uavcan::equipment::gnss::Auxiliary - true - - - subscription GPS Relative - Enable UAVCAN GPS Relative subscription. ardupilot::gnss::RelPosHeading - true + 16 - - subscription hygrometer - Enable UAVCAN hygrometer subscriptions. dronecan::sensors::hygrometer::Hygrometer - true + + Height differential gain + 2 - - subscription ICE - Enable UAVCAN internal combustion engine (ICE) subscription. uavcan::equipment::ice::reciprocating::Status - true + + Height integrational gain + 2 - - subscription IMU - Enable UAVCAN IMU subscription. uavcan::equipment::ahrs::RawIMU - true + + sum speed of error for integrational gain + 2 - - subscription magnetometer - Enable UAVCAN mag subscription. uavcan::equipment::ahrs::MagneticFieldStrength uavcan::equipment::ahrs::MagneticFieldStrength2 - true + + maximum Height distance controlled by manual input. Diff between actual and desired Height cant be higher than that + 2 - - subscription range finder - Enable UAVCAN range finder subscription. uavcan::equipment::range_sensor::Measurement - true + + Height proportional gain + 2 - - - - Direct pitch input + + Height change strength from manual input + 2 - - Direct roll input + + Pitch gain for manual inputs in manual control mode + 0.0 + 2 - - Direct thrust input + + Roll gain for manual inputs in manual control mode + 0.0 + 2 - - Direct yaw input + + Throttle gain for manual inputs in manual control mode + 0.0 + 2 - - Select Input Mode - - use Attitude Setpoints - Direct Feedthrough - + + Yaw gain for manual inputs in manual control mode + 0.0 + 2 Pitch differential gain @@ -25614,6 +15986,26 @@ Pitch proportional gain 2 + + Pitch gain for manual inputs in rate control mode + 0.0 + 2 + + + Roll gain for manual inputs in rate control mode + 0.0 + 2 + + + Throttle gain for manual inputs in rate control mode + 0.0 + 2 + + + Yaw gain for manual inputs in rate control mode + 0.0 + 2 + Roll differential gain 2 @@ -25622,6 +16014,46 @@ Roll proportional gain 2 + + Pitch gain for manual inputs in attitude control mode + 0.0 + 2 + + + Roll gain for manual inputs in attitude control mode + 0.0 + 2 + + + Throttle gain for manual inputs in attitude control mode + 0.0 + 2 + + + Yaw gain for manual inputs in attitude control mode + 0.0 + 2 + + + Maximum time (in seconds) before resetting setpoint + + + Stick mode selector (0=Heave/sway control, roll/pitch leveled; 1=Pitch/roll control) + 0 + 1 + + + UUV Thrust setpoint Saturation + 0.0 + 1.0 + 2 + + + UUV Torque setpoint Saturation + 0.0 + 1.0 + 2 + Yaw differential gain 2 @@ -25650,149 +16082,78 @@ Gain of P controller Z - + + Gain for position control velocity setpoint update + + Stabilization mode(1) or Position Control(0) - Position Control - Stabilization Mode + Moves position setpoint in world frame + Moves position setpoint in body frame - - - - UWB sensor X offset in body frame - UWB sensor positioning in relation to Drone in NED. X offset. A Positive offset results in a Position o - m - 2 - 0.01 - - - UWB sensor Y offset in body frame - UWB sensor positioning in relation to Drone in NED. Y offset. - m - 2 - 0.01 - - - UWB sensor Z offset in body frame - UWB sensor positioning in relation to Drone in NED. Z offset. - m - 2 - 0.01 + + Deadband for changing position setpoint - - Serial Configuration for Ultrawideband position sensor driver - Configure on which serial port to run Ultrawideband position sensor driver. - true + + Stabilization mode(1) or Position Control(0) - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - UWB sensor orientation - The orientation of the sensor relative to the forward direction of the body frame. Look up table in src/lib/conversion/rotation.h Default position is the antannaes downward facing, UWB board parallel with body frame. - - ROTATION_NONE - ROTATION_YAW_45 - ROTATION_YAW_90 - ROTATION_YAW_135 - ROTATION_YAW_180 - ROTATION_YAW_225 - ROTATION_YAW_270 - ROTATION_YAW_315 - ROTATION_ROLL_180 - ROTATION_ROLL_180_YAW_45 - ROTATION_ROLL_180_YAW_90 - ROTATION_ROLL_180_YAW_135 - ROTATION_PITCH_180 - ROTATION_ROLL_180_YAW_225 - ROTATION_ROLL_180_YAW_270 - ROTATION_ROLL_180_YAW_315 - ROTATION_ROLL_90 - ROTATION_ROLL_90_YAW_45 - ROTATION_ROLL_90_YAW_90 - ROTATION_ROLL_90_YAW_135 - ROTATION_ROLL_270 - ROTATION_ROLL_270_YAW_45 - ROTATION_ROLL_270_YAW_90 - ROTATION_ROLL_270_YAW_135 - ROTATION_PITCH_90 - ROTATION_PITCH_270 - ROTATION_PITCH_180_YAW_90 - ROTATION_PITCH_180_YAW_270 - ROTATION_ROLL_90_PITCH_90 - ROTATION_ROLL_180_PITCH_90 - ROTATION_ROLL_270_PITCH_90 - ROTATION_ROLL_90_PITCH_180 - ROTATION_ROLL_270_PITCH_180 - ROTATION_ROLL_90_PITCH_270 - ROTATION_ROLL_180_PITCH_270 - ROTATION_ROLL_270_PITCH_270 - ROTATION_ROLL_90_PITCH_180_YAW_90 - ROTATION_ROLL_90_YAW_270 - ROTATION_ROLL_90_PITCH_68_YAW_293 - ROTATION_PITCH_315 - ROTATION_ROLL_90_PITCH_315 + Tracks previous attitude setpoint + Tracks horizontal attitude (allows yaw change) uXRCE-DDS Agent IP address - If ethernet is enabled and is the selected configuration for uXRCE-DDS, the selected Agent IP address will be set and used. Decimal dot notation is not supported. IP address must be provided in int32 format. For example, 192.168.1.2 is mapped to -1062731518; 127.0.0.1 is mapped to 2130706433. + If ethernet is enabled and is the selected configuration for uXRCE-DDS, +the selected Agent IP address will be set and used. +Decimal dot notation is not supported. IP address must be provided +in int32 format. For example, 192.168.1.2 is mapped to -1062731518; +127.0.0.1 is mapped to 2130706433. True - - Serial Configuration for UXRCE-DDS Client - Configure on which serial port to run UXRCE-DDS Client. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - Ethernet - - uXRCE-DDS domain ID uXRCE-DDS domain ID True + + Enable serial flow control for UXRCE interface + This is used to enable flow control for the serial uXRCE instance. +Used for reliable high bandwidth communication. + True + uXRCE-DDS session key - uXRCE-DDS key, must be different from zero. In a single agent - multi client configuration, each client must have a unique session key. + uXRCE-DDS key, must be different from zero. +In a single agent - multi client configuration, each client +must have a unique session key. + True + + + Define an index-based message namespace + Defines an index-based namespace for DDS messages, e.g, uav_0, uav_1, up to uav_9999 +A value less than zero leaves the namespace empty + -1 + 9999 True uXRCE-DDS UDP port - If ethernet is enabled and is the selected configuration for uXRCE-DDS, the selected UDP port will be set and used. + If ethernet is enabled and is the selected configuration for uXRCE-DDS, +the selected UDP port will be set and used. 0 65535 True uXRCE-DDS participant configuration - Set the participant configuration on the Agent's system. 0: Use the default configuration. 1: Restrict messages to localhost (use in combination with ROS_LOCALHOST_ONLY=1). 2: Use a custom participant with the profile name "px4_participant". + Set the participant configuration on the Agent's system. +0: Use the default configuration. +1: Restrict messages to localhost +(use in combination with ROS_LOCALHOST_ONLY=1). +2: Use a custom participant with the profile name "px4_participant". 0 2 True @@ -25802,6 +16163,13 @@ Custom participant + + RX rate timeout configuration + Specifies after how many seconds without receiving data the DDS connection is reestablished. +A value less than one disables the RX rate timeout. + s + True + Enable uXRCE-DDS system clock synchronization When enabled along with UXRCE_DDS_SYNCT, uxrce_dds_client will set the system clock using the agents UTC timestamp. @@ -25812,171 +16180,12 @@ When enabled, uxrce_dds_client will synchronize the timestamps of the incoming and outgoing messages measuring the offset between the Agent OS time and the PX4 time. True - - - - UART ESC baud rate - Default rate is 250Kbps, which is used in off-the-shelf MoadalAI ESC products. - bit/s - - - UART ESC configuration - Selects what type of UART ESC, if any, is being used. - 0 - 1 - true - - - Disabled - - VOXL ESC - - - - UART ESC Mode - Selects what type of mode is enabled, if any - 0 - 2 - true - - - None - - Turtle Mode enabled via AUX1 - - Turtle Mode enabled via AUX2 - - UART Passthrough Mode - - - - UART ESC Enable publishing of battery status - Only applicable to ESCs that report total battery voltage and current - 0 - 1 - true - - - Disabled - - Enabled - - - - UART ESC RPM Max - Maximum RPM for ESC - rpm - - - UART ESC RPM Min - Minimum RPM for ESC - rpm - - - UART ESC ID 1 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 2 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 3 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC ID 4 Spin Direction Flag - - - Default - - Reverse - - - - UART ESC Turtle Mode Cosphi - 0.000 - 1.000 - 10 - 0.001 - - - UART ESC Turtle Mode Crash Flip Motor Deadband - 0 - 100 - 10 - 1 - - - UART ESC Turtle Mode Crash Flip Motor expo - 0 - 100 - 10 - 1 - - - UART ESC Turtle Mode Crash Flip Motor STICK_MINF - 0.0 - 100.0 - 10 - 1.0 - - - UART ESC Over-Temperature Threshold (Degrees C) - Only applicable to ESCs that report temperature - 0 - 200 - true - - - Disabled - - - - UART ESC Turtle Mode Crash Flip Motor Percent - 1 - 100 - 10 - 1 - - - UART ESC Temperature Warning Threshold (Degrees C) - Only applicable to ESCs that report temperature - 0 - 200 - true - - - Disabled - - - - UART ESC verbose logging - 0 - 1 - true - - - Disabled - - Enabled - - - - - - UART M0065 baud rate - Default rate is 921600, which is used for communicating with M0065. - bit/s - - - M0065 PWM Max - Maximum duration (microseconds) for M0065 PWM - 0 - 2000 - us - - - M0065 PWM Min - Minimum duration (microseconds) for M0065 PWM - 0 - 2000 - us + + TX rate timeout configuration + Specifies after how many seconds without sending data the DDS connection is reestablished. +A value less than one disables the TX rate timeout. + s + True @@ -26008,7 +16217,7 @@ 0.1 - Backtransition deceleration setpoint to pitch I gain + Backtransition deceleration setpoint to tilt I gain 0 0.3 rad s/m @@ -26017,7 +16226,8 @@ Approximate deceleration during back transition - Used to calculate back transition distance in an auto mode. For standard vtol and tiltrotors a controller is used to track this value during the transition. + Used to calculate back transition distance in an auto mode. +For standard vtol and tiltrotors a controller is used to track this value during the transition. 0.5 10 m/s^2 @@ -26048,28 +16258,36 @@ Use fixed-wing actuation in hover to accelerate forward - This feature can be used to avoid the plane having to pitch nose down in order to move forward. Prevents large, negative lift from pitching nose down into wind. Fixed-wing forward actuators refers to puller/pusher (standard VTOL), or forward-tilt (tiltrotor VTOL). Only active if demanded down pitch is below VT_PITCH_MIN. Use VT_FWD_THRUST_SC to tune it. Descend mode is treated as Landing too. Only active (if enabled) in Altitude, Position and Auto modes, not in Stabilized. + Prevents downforce from pitching the body down when facing wind. +Uses puller/pusher (standard VTOL), or forward-tilt (tiltrotor VTOL) to accelerate forward instead. +Only active if demanded pitch is below VT_PITCH_MIN. +Use VT_FWD_THRUST_SC to tune it. +Descend mode is treated as Landing too. +Only active (if enabled) in height-rate controlled modes. Disabled Enabled (except LANDING) - Enabled if distance to ground above MPC_LAND_ALT1 - Enabled if distance to ground above MPC_LAND_ALT2 + Enabled if above MPC_LAND_ALT1 + Enabled if above MPC_LAND_ALT2 Enabled constantly - Enabled if distance to ground above MPC_LAND_ALT1 (except LANDING) - Enabled if distance to ground above MPC_LAND_ALT2 (except LANDING) + Enabled if above MPC_LAND_ALT1 (except LANDING) + Enabled if above MPC_LAND_ALT2 (except LANDING) - Fixed-wing actuation thrust scale for hover forward flight - Scale applied to the demanded down-pitch to get the fixed-wing forward actuation in hover mode. Enabled via VT_FWD_THRUST_EN. + Fixed-wing actuation thrust scale in hover + Scale applied to the demanded pitch (below VT_PITCH_MIN) to get the fixed-wing forward actuation in hover mode. +Enabled via VT_FWD_THRUST_EN. 0.0 - 2.0 + 5.0 2 0.01 Differential thrust in forwards flight - Enable differential thrust seperately for roll, pitch, yaw in forward (fixed-wing) mode. The effectiveness of differential thrust around the corresponding axis can be tuned by setting VT_FW_DIFTHR_S_R / VT_FW_DIFTHR_S_P / VT_FW_DIFTHR_S_Y. + Enable differential thrust seperately for roll, pitch, yaw in forward (fixed-wing) mode. +The effectiveness of differential thrust around the corresponding axis can be +tuned by setting VT_FW_DIFTHR_S_R / VT_FW_DIFTHR_S_P / VT_FW_DIFTHR_S_Y. 0 7 @@ -26104,7 +16322,9 @@ Quad-chute altitude - Minimum altitude for fixed-wing flight. When the vehicle is in fixed-wing mode and the altitude drops below this altitude (relative altitude above local origin), it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. + Minimum altitude for fixed-wing flight. When the vehicle is in fixed-wing mode +and the altitude drops below this altitude (relative altitude above local origin), +it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. 0.0 200.0 m @@ -26113,21 +16333,27 @@ Quad-chute maximum height - Maximum height above the ground (if available, otherwise above Home if available, otherwise above the local origin) where triggering a quad-chute is possible. At high altitudes there is a big risk to deplete the battery and therefore crash if quad-chuting there. + Maximum height above the ground (if available, otherwise above +Home if available, otherwise above the local origin) where triggering a quad-chute is possible. +At high altitudes there is a big risk to deplete the battery and therefore crash if quad-chuting there. 0 m 1 Quad-chute max pitch threshold - Absolute pitch threshold for quad-chute triggering in FW mode. Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Absolute pitch threshold for quad-chute triggering in FW mode. +Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 180 deg Quad-chute max roll threshold - Absolute roll threshold for quad-chute triggering in FW mode. Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Absolute roll threshold for quad-chute triggering in FW mode. +Above this the vehicle will transition back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 180 deg @@ -26150,25 +16376,29 @@ Airspeed-less front transition time (open loop) - The duration of the front transition when there is no airspeed feedback available. + The duration of the front transition when there is no airspeed feedback available. +When airspeed is used, transition timeout is declared if airspeed does not +reach VT_ARSP_BLEND after this time. 1.0 30.0 s 1 0.5 - + Minimum pitch angle during hover landing - Overrides VT_PITCH_MIN when the vehicle is in LAND mode (hovering). During landing it can be beneficial to reduce the pitch angle to reduce the generated lift in head wind. + Overrides VT_PITCH_MIN when the vehicle is in LAND mode (hovering). +During landing it can be beneficial to reduce the pitch angle to reduce the generated lift in head wind. -10.0 45.0 deg 1 0.1 - + Minimum pitch angle during hover - Any pitch setpoint below this value is translated to a forward force by the fixed-wing forward actuation if VT_FW_TRHUST_EN is set to 1. + Any pitch setpoint below this value is translated to a forward force by the fixed-wing forward actuation if +VT_FWD_TRHUST_EN is set. -10.0 45.0 deg @@ -26177,7 +16407,9 @@ Pusher throttle ramp up slew rate - Defines the slew rate of the puller/pusher throttle during transitions. Zero will deactivate the slew rate limiting and thus produce an instant throttle rise to the transition throttle VT_F_TRANS_THR. + Defines the slew rate of the puller/pusher throttle during transitions. +Zero will deactivate the slew rate limiting and thus produce an instant throttle +rise to the transition throttle VT_F_TRANS_THR. 0 1/s 2 @@ -26185,7 +16417,11 @@ Quad-chute uncommanded descent threshold - Altitude error threshold for quad-chute triggering during fixed-wing flight. The check is only active if altitude is controlled and the vehicle is below the current altitude reference. The altitude error is relative to the highest altitude the vehicle has achieved since it has flown below the current altitude reference. Set to 0 do disable. + Altitude error threshold for quad-chute triggering during fixed-wing flight. +The check is only active if altitude is controlled and the vehicle is below the current altitude reference. +The altitude error is relative to the highest altitude the vehicle has achieved since it has flown below the current +altitude reference. +Set to 0 do disable. 0.0 200.0 m @@ -26194,7 +16430,12 @@ Quad-chute transition altitude loss threshold - Altitude loss threshold for quad-chute triggering during VTOL transition to fixed-wing flight in altitude-controlled flight modes. Active until 5s after completing transition to fixed-wing. If the current altitude is more than this value below the altitude at the beginning of the transition, it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. Set to 0 do disable this threshold. + Altitude loss threshold for quad-chute triggering during VTOL transition to fixed-wing flight +in altitude-controlled flight modes. +Active until 5s after completing transition to fixed-wing. +If the current altitude is more than this value below the altitude at the beginning of the +transition, it will instantly switch back to MC mode and execute behavior defined in COM_QC_ACT. +Set to 0 do disable this threshold. 0 50 m @@ -26250,7 +16491,7 @@ Front transition timeout - Time in seconds after which transition will be cancelled. Disabled if set to 0. + Time in seconds after which transition will be cancelled. 0.1 30.00 s @@ -26289,290 +16530,4 @@ 1 - - - Serial Configuration for Vertiq IO - Configure on which serial port to run Vertiq IO. - true - - Disabled - UART 6 - TELEM 1 - TELEM 2 - TELEM 3 - TELEM/SERIAL 4 - GPS 1 - GPS 2 - GPS 3 - Radio Controller - Wifi Port - EXT2 - - - - The triggered behavior on PX4 arm - The behavior triggered when the flight controller arms. You have the option to use your motors' arming behaviors, or to force all of your motors to arm - - Use Motor Arm Behavior - Send Explicit Arm Command - - - - The IQUART driver's baud rate - The baud rate (in bits per second) used by the serial port connected with IQUART communication - True - - - Module Param - The module's control mechanism - PWM Mode: Commands a fraction of battery voltage. This changes as the battery voltage changes. This is the least safe mode because the upper throttle limit is determined by the battery voltage. Voltage Mode: Commands a voltage. The motor will behave the same way throughout the life of a battery, assuming the commanded voltage is less than the battery voltage. You must set the MAX_VOLTS parameter. Velocity Mode: Closed-loop, commands a velocity. The controller will adjust the applied voltage so that the motor spins at the commanded velocity. This mode has faster reaction times. Only use this if you know the properties of your propeller. You must set the MAX_VELOCITY parameter. - - PWM - Voltage - Velocity - - - - The triggered behavior sent to the motors on PX4 disarm - The behavior triggered when the flight controller disarms. You have the option to trigger your motors' disarm behaviors, set all motors to coast, or set a predefined throttle setpoint - - Send Explicit Disarm - Coast Motors - Set Predefined Velocity Setpoint - - - - Velocity sent when DISARM_TRIGGER is Set Predefined Velocity Setpoint - This is the velocity that will be sent to all motors when PX4 is disarmed and DISARM_TRIGGER is Set Predefined Velocity Setpoint - 0 - 100 - - - Module Param - If the flight controller uses 2D or 3D communication - The FC and the ESC must agree upon the meaning of the signal coming out of the ESC. When FCs are in 3D mode they re-map negative signals. This parameter keeps the FC and ESC in agreement. - - 2D - 3D - - - - Module Param - Maximum velocity when CONTROL_MODE is set to Velocity - Only relevant in Velocity Mode. This is the velocity the controller will command at full throttle. - - - Module Param - Maximum voltage when CONTROL_MODE is set to Voltage - Only relevant in Voltage Mode. This is the voltage the controller will command at full throttle. - - - Module Param - The direction that the module should spin - Set the targeted motor's spinning direction (clockwise vs. counter clockwise) and flight mode (2D non-reversible vs. 3D reversible) - - Unconfigured - 3D Counter Clockwise - 3D Clockwise - 2D Counter Clockwise - 2D Clockwise - - - - The number of Vertiq IFCI parameters to use - The total number of IFCI control variables being used across all connected modules - 0 - 16 - True - - - Module Param - Max pulsing voltage limit when in Voltage Limit Mode - This sets the max pulsing voltage limit when in Voltage Limit Mode. - - - Module Param - 0 = Supply Voltage Mode, 1 = Voltage Limit Mode - Supply Voltage Mode means that the maximum voltage applied to pulsing is the supplied voltage. Voltage Limit Mode indicates that PULSE_VOLT_LIM is the maximum allowed voltage to apply towards pulsing. - - Supply Voltage Mode - Voltage Limit Mode - - - - Reinitialize the target module's values into the PX4 parameters - Setting this value to true will reinitialize PX4's IQUART connected parameters to the value stored on the currently targeted motor. This is especially useful if your flight controller powered on before your connected modules - - - Module IDs [0, 31] that you would like to request telemetry from - The module IDs [0, 31] that should be asked for telemetry. The data received from these IDs will be published via the esc_status topic. - 0 - 4294967295 - True - - Module ID 0 - Module ID 1 - Module ID 2 - Module ID 3 - Module ID 4 - Module ID 5 - Module ID 6 - Module ID 7 - Module ID 8 - Module ID 9 - Module ID 10 - Module ID 11 - Module ID 12 - Module ID 13 - Module ID 14 - Module ID 15 - Module ID 16 - Module ID 17 - Module ID 18 - Module ID 19 - Module ID 20 - Module ID 21 - Module ID 22 - Module ID 23 - Module ID 24 - Module ID 25 - Module ID 26 - Module ID 27 - Module ID 28 - Module ID 29 - Module ID 30 - Module ID 31 - - - - Module IDs [32, 62] that you would like to request telemetry from - The module IDs [32, 62] that should be asked for telemetry. The data received from these IDs will be published via the esc_status topic. - 0 - 2147483647 - True - - Module ID 32 - Module ID 33 - Module ID 34 - Module ID 35 - Module ID 36 - Module ID 37 - Module ID 38 - Module ID 39 - Module ID 40 - Module ID 41 - Module ID 42 - Module ID 43 - Module ID 44 - Module ID 45 - Module ID 46 - Module ID 47 - Module ID 48 - Module ID 49 - Module ID 50 - Module ID 51 - Module ID 52 - Module ID 53 - Module ID 54 - Module ID 55 - Module ID 56 - Module ID 57 - Module ID 58 - Module ID 59 - Module ID 60 - Module ID 61 - Module ID 62 - - - - Module Param - The module's Throttle Control Value Index - This represents the Control Value Index where the targeted module will look for throttle commands - 0 - 255 - - - Module Param - Offsets pulse angle to allow for mechanical properties - This offsets where the pulse starts around the motor to allow for propeller mechanical properties. - - - The Module ID of the module you would like to communicate with - This is the value used as the target module ID of all configuration parameters (not operational parameters). The Vertiq module with the module ID matching this value will react to all get and set requests from PX4. Any Vertiq client made with dynamic object IDs should use this value to instantiate itself. - - - Module Param - The minimum velocity required to allow pulsing - This is the velocity at which pulsing is allowed. Any velocity between VELOCITY_CUTOFF and -VELOCITY_CUTOFF will not pulse. - - - Module Param - CVI for the X rectangular coordinate - This represents the Control Value Index where the targeted module will look for the X rectangular coordinate. - 0 - 255 - - - Module Param - CVI for the Y rectangular coordinate - This represents the Control Value Index where the targeted module will look for the Y rectangular coordinate. - 0 - 255 - - - Module Param - The encoder angle at which theta is zero - The encoder angle at which theta is zero. Adjust this number to change the location of 0 phase when pulsing. - - - - - Zenoh Enable - Zenoh - True - - - - - Accel filter settings - true - - 13 Hz - 30 Hz - 68 Hz - 235 Hz - 280 Hz - 370 Hz - No filter - - - - Gyro and Accel decimation settings - true - - None - 5900 Hz - 2950 Hz - 1475 Hz - 738 Hz - - - - Gyro filter settings - true - - 13 Hz - 30 Hz - 68 Hz - 235 Hz - 280 Hz - 370 Hz - No filter - - - - Lightware SF1xx/SF20/LW20 Operation Mode - 0 - 2 - - Disabled - Enabled - Enabled in VTOL MC mode, listen to request from system in FW mode - - - - Skip the controller - - use the module's controller - skip the controller and feedthrough the setpoints - - - diff --git a/src/FlightMap/FlightMap.qml b/src/FlightMap/FlightMap.qml index 0786c2e174e0..58c9ed2a792b 100644 --- a/src/FlightMap/FlightMap.qml +++ b/src/FlightMap/FlightMap.qml @@ -229,7 +229,7 @@ Map { MapQuickItem { anchorPoint.x: sourceItem.width / 2 anchorPoint.y: sourceItem.height / 2 - visible: gcsPosition.isValid + visible: gcsPosition.isValid && !planView coordinate: gcsPosition sourceItem: Image { diff --git a/src/FlightMap/MapItems/MissionItemIndicator.qml b/src/FlightMap/MapItems/MissionItemIndicator.qml index 8f1e3efddd08..76e53fa1220d 100644 --- a/src/FlightMap/MapItems/MissionItemIndicator.qml +++ b/src/FlightMap/MapItems/MissionItemIndicator.qml @@ -3,6 +3,7 @@ import QtLocation import QGroundControl import QGroundControl.Controls +import QGroundControl.PlanView /// Marker for displaying a mission item on the map MapQuickItem { diff --git a/src/FlightMap/MapItems/PlanMapItems.qml b/src/FlightMap/MapItems/PlanMapItems.qml index 329312cd9aad..8a37f7030ca3 100644 --- a/src/FlightMap/MapItems/PlanMapItems.qml +++ b/src/FlightMap/MapItems/PlanMapItems.qml @@ -5,6 +5,7 @@ import QtPositioning import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView // Adds visual items associated with the Flight Plan to the map. // Currently only used by Fly View even though it's called PlanMapItems! diff --git a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml index c54b9c540831..55c93e00a855 100644 --- a/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolygonVisuals.qml @@ -8,6 +8,7 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView /// QGCMapPolygon map visuals Item { @@ -26,7 +27,6 @@ Item { property real _circleRadius property bool _circleRadiusDrag: false property var _circleRadiusDragCoord: QtPositioning.coordinate() - property bool _editCircleRadius: false property string _instructionText: _polygonToolsText property var _savedVertices: [ ] property bool _savedCircleMode @@ -251,7 +251,7 @@ Item { QGCMenuItem { text: qsTr("Set radius..." ) visible: _circleMode - onTriggered: _editCircleRadius = true + onTriggered: editCircleRadiusDialogFactory.open() } QGCMenuItem { @@ -273,9 +273,10 @@ Item { MapPolygon { color: mapPolygon.showAltColor ? altColor : interiorColor opacity: interiorOpacity - border.color: borderColor - border.width: borderWidth - path: mapPolygon.path + visible: _root.visible + border.color: mapPolygon.vertexDrag ? "orange" : borderColor + border.width: mapPolygon.vertexDrag ? 3 : borderWidth + path: mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path } } @@ -309,19 +310,24 @@ Item { delegate: Item { property var _edgeLengthHandle - property var _vertices: mapPolygon.path function _setHandlePosition() { + var vertices = mapPolygon.vertexDrag ? mapPolygon.dragPath : mapPolygon.path var nextIndex = index + 1 - if (nextIndex > _vertices.length - 1) { + if (nextIndex > vertices.length - 1) { nextIndex = 0 } - var distance = _vertices[index].distanceTo(_vertices[nextIndex]) - var azimuth = _vertices[index].azimuthTo(_vertices[nextIndex]) - _edgeLengthHandle.coordinate =_vertices[index].atDistanceAndAzimuth(distance / 3, azimuth) + var distance = vertices[index].distanceTo(vertices[nextIndex]) + var azimuth = vertices[index].azimuthTo(vertices[nextIndex]) + _edgeLengthHandle.coordinate = vertices[index].atDistanceAndAzimuth(distance / 2, azimuth) _edgeLengthHandle.distance = distance } + Connections { + target: mapPolygon + function onDragPathChanged() { _setHandlePosition() } + } + Component.onCompleted: { _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl) _edgeLengthHandle.vertexIndex = index @@ -345,7 +351,7 @@ Item { id: mapQuickItem anchorPoint.x: sourceItem.width / 2 anchorPoint.y: sourceItem.height / 2 - visible: !_circleMode + visible: !_circleMode && !_isVertexBeingDragged && !mapPolygon.centerDrag property int vertexIndex @@ -401,8 +407,8 @@ Item { mapControl: _root.mapControl z: _zorderDragHandle visible: !_circleMode - onDragStart: _isVertexBeingDragged = true - onDragStop: { _isVertexBeingDragged = false; mapPolygon.verifyClockwiseWinding() } + onDragStart: { _isVertexBeingDragged = true; mapPolygon.vertexDrag = true } + onDragStop: { _isVertexBeingDragged = false; mapPolygon.vertexDrag = false; mapPolygon.verifyClockwiseWinding() } property int polygonVertex @@ -428,6 +434,7 @@ Item { anchorPoint.x: dragHandle.width * 0.5 anchorPoint.y: dragHandle.height * 0.5 z: _zorderDragHandle + visible: !_isVertexBeingDragged sourceItem: Rectangle { id: dragHandle width: ScreenTools.defaultFontPixelHeight * 1.5 @@ -458,7 +465,7 @@ Item { anchorPoint.x: dragHandle.width / 2 anchorPoint.y: dragHandle.height / 2 z: _zorderDragHandle - visible: !_circleMode + visible: !_circleMode && !mapPolygon.centerDrag property int polygonVertex @@ -505,6 +512,56 @@ Item { } } + QGCPopupDialogFactory { + id: editCircleRadiusDialogFactory + + dialogComponent: editCircleRadiusDialog + } + + Component { + id: editCircleRadiusDialog + + QGCPopupDialog { + id: popupDialog + title: qsTr("Set Radius") + buttons: Dialog.Save | Dialog.Cancel + + onAccepted: { + const appRadius = Number(radiusField.text) + if (!isNaN(appRadius) && appRadius > 0) { + const radiusMeters = QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsToMeters(appRadius) + _createCircularPolygon(mapPolygon.center, radiusMeters) + } else { + preventClose = true + } + } + + ColumnLayout { + width: ScreenTools.defaultFontPixelWidth * 30 + spacing: ScreenTools.defaultFontPixelHeight + + QGCLabel { + Layout.fillWidth: true + text: qsTr("Enter circle radius.") + wrapMode: Text.WordWrap + } + + QGCTextField { + id: radiusField + Layout.fillWidth: true + text: QGroundControl.unitsConversion.metersToAppSettingsHorizontalDistanceUnits(_circleRadius).toFixed(1) + validator: DoubleValidator { bottom: 0.1; notation: DoubleValidator.StandardNotation } + inputMethodHints: Qt.ImhFormattedNumbersOnly + } + + QGCLabel { + Layout.fillWidth: true + text: QGroundControl.unitsConversion.appSettingsHorizontalDistanceUnitsString + } + } + } + } + QGCPopupDialogFactory { id: editCenterPositionDialogFactory @@ -553,8 +610,9 @@ Item { mapControl: _root.mapControl z: _zorderCenterHandle onItemCoordinateChanged: mapPolygon.center = itemCoordinate - onDragStart: mapPolygon.centerDrag = true - onDragStop: mapPolygon.centerDrag = false + onDragStart: { mapPolygon.centerDrag = true; mapPolygon.vertexDrag = true } + onDragStop: { mapPolygon.centerDrag = false; mapPolygon.vertexDrag = false } + onClicked: if(_root.interactive) menu.popupCenter() } } @@ -567,7 +625,7 @@ Item { Component.onCompleted: { dragHandle = centerDragHandle.createObject(mapControl) - dragHandle.coordinate = Qt.binding(function() { return mapPolygon.center }) + dragHandle.coordinate = Qt.binding(function() { return mapPolygon.centerDrag ? mapPolygon.dragCenter : mapPolygon.center }) mapControl.addMapItem(dragHandle) dragArea = centerDragAreaComponent.createObject(mapControl, { "itemIndicator": dragHandle, "itemCoordinate": mapPolygon.center }) } @@ -654,6 +712,7 @@ Item { anchorPoint.x: dragHandle.width / 2 anchorPoint.y: dragHandle.height / 2 z: QGroundControl.zOrderMapItems + 2 + visible: !mapPolygon.centerDrag sourceItem: Rectangle { id: dragHandle @@ -689,8 +748,19 @@ Item { MissionItemIndicatorDrag { mapControl: _root.mapControl + onDragStart: { + _circleRadiusDrag = true + mapPolygon.vertexDrag = true + } + onDragStop: { + _circleRadiusDrag = false + mapPolygon.vertexDrag = false + } onItemCoordinateChanged: { + // Keep the handle visually attached to the cursor while radius updates are de-bounced. + _circleRadiusDragCoord = itemCoordinate + var radius = mapPolygon.center.distanceTo(itemCoordinate) if (Math.abs(radius - _circleRadius) > 0.1) { diff --git a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml index bd11283becdc..7238024172cd 100644 --- a/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml +++ b/src/FlightMap/MapItems/QGCMapPolylineVisuals.qml @@ -7,6 +7,7 @@ import QtQuick.Dialogs import QGroundControl import QGroundControl.Controls import QGroundControl.FlightMap +import QGroundControl.PlanView /// QGCMapPolyline map visuals Item { @@ -24,6 +25,8 @@ Item { property real _zorderDragHandle: QGroundControl.zOrderMapItems + 3 // Highest to prevent splitting when items overlap property real _zorderSplitHandle: QGroundControl.zOrderMapItems + 2 property var _savedVertices: [ ] + property bool _isVertexBeingDragged: false + property bool dragging: _isVertexBeingDragged readonly property string _corridorToolsText: qsTr("Polyline Tools") readonly property string _traceText: qsTr("Click in the map to add vertices. Click 'Done Tracing' when finished.") @@ -36,7 +39,7 @@ Item { function _addInteractiveVisuals() { if (_objMgrInteractiveVisuals.empty) { - _objMgrInteractiveVisuals.createObjects([ dragHandlesComponent, splitHandlesComponent, toolbarComponent ], mapControl) + _objMgrInteractiveVisuals.createObjects([ dragHandlesComponent, splitHandlesComponent, edgeLengthHandlesComponent, toolbarComponent ], mapControl) } } @@ -161,9 +164,9 @@ Item { id: polylineComponent MapPolyline { - line.width: lineWidth - line.color: lineColor - path: mapPolyline.path + line.width: mapPolyline.vertexDrag ? 3 : lineWidth + line.color: mapPolyline.vertexDrag ? "orange" : lineColor + path: mapPolyline.vertexDrag ? mapPolyline.dragPath : mapPolyline.path visible: _root.visible opacity: _root.opacity } @@ -178,6 +181,7 @@ Item { anchorPoint.y: sourceItem.height / 2 z: _zorderSplitHandle opacity: _root.opacity + visible: !mapPolyline.vertexDrag property int vertexIndex @@ -224,6 +228,71 @@ Item { } } + Component { + id: edgeLengthHandleComponent + + MapQuickItem { + id: edgeLengthMapItem + anchorPoint.x: sourceItem.width / 2 + anchorPoint.y: sourceItem.height / 2 + + property int vertexIndex + property real distance + + property var _unitsConversion: QGroundControl.unitsConversion + + sourceItem: Text { + text: _unitsConversion.metersToAppSettingsHorizontalDistanceUnits(distance).toFixed(1) + " " + + _unitsConversion.appSettingsHorizontalDistanceUnitsString + color: "white" + } + } + } + + Component { + id: edgeLengthHandlesComponent + + Repeater { + model: _isVertexBeingDragged ? mapPolyline.path : undefined + + delegate: Item { + property var _edgeLengthHandle + + function _setHandlePosition() { + var vertices = mapPolyline.vertexDrag ? mapPolyline.dragPath : mapPolyline.path + var nextIndex = index + 1 + if (nextIndex > vertices.length - 1) { + return + } + var distance = vertices[index].distanceTo(vertices[nextIndex]) + var azimuth = vertices[index].azimuthTo(vertices[nextIndex]) + _edgeLengthHandle.coordinate = vertices[index].atDistanceAndAzimuth(distance / 2, azimuth) + _edgeLengthHandle.distance = distance + } + + Connections { + target: mapPolyline + function onDragPathChanged() { _setHandlePosition() } + } + + Component.onCompleted: { + if (index + 1 <= mapPolyline.path.length - 1) { + _edgeLengthHandle = edgeLengthHandleComponent.createObject(mapControl) + _edgeLengthHandle.vertexIndex = index + _setHandlePosition() + mapControl.addMapItem(_edgeLengthHandle) + } + } + + Component.onDestruction: { + if (_edgeLengthHandle) { + _edgeLengthHandle.destroy() + } + } + } + } + } + // Control which is used to drag polygon vertices Component { id: dragAreaComponent @@ -233,6 +302,8 @@ Item { id: dragArea z: _zorderDragHandle opacity: _root.opacity + onDragStart: { _isVertexBeingDragged = true; mapPolyline.vertexDrag = true } + onDragStop: { _isVertexBeingDragged = false; mapPolyline.vertexDrag = false } property int polylineVertex diff --git a/src/FlightMap/Widgets/CenterMapDropButton.qml b/src/FlightMap/Widgets/CenterMapDropButton.qml index d8fd38f8f8f2..eb3ad652a0c0 100644 --- a/src/FlightMap/Widgets/CenterMapDropButton.qml +++ b/src/FlightMap/Widgets/CenterMapDropButton.qml @@ -175,7 +175,7 @@ DropButton { } QGCButton { - text: qsTr("Launch") + text: qsTr("Home") Layout.fillWidth: true enabled: !followVehicleCheckBox.checked diff --git a/src/FlightMap/Widgets/CenterMapDropPanel.qml b/src/FlightMap/Widgets/CenterMapDropPanel.qml index fe5528f3975f..372ce85c4ee2 100644 --- a/src/FlightMap/Widgets/CenterMapDropPanel.qml +++ b/src/FlightMap/Widgets/CenterMapDropPanel.qml @@ -40,7 +40,7 @@ ColumnLayout { } QGCButton { - text: qsTr("Launch") + text: qsTr("Home") Layout.fillWidth: true onClicked: { diff --git a/src/FlightMap/Widgets/CompassDial.qml b/src/FlightMap/Widgets/CompassDial.qml index 253b316eca3f..0e9d23f3f4f6 100644 --- a/src/FlightMap/Widgets/CompassDial.qml +++ b/src/FlightMap/Widgets/CompassDial.qml @@ -21,6 +21,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "N" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 0) @@ -31,6 +32,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "E" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 90) @@ -41,6 +43,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "S" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 180) @@ -51,6 +54,7 @@ Item { QGCLabel { anchors.centerIn: parent text: "W" + rotation: _lockNoseUpCompass ? _heading : 0 transform: Translate { x: translateCenterToAngleX(control.offsetRadius, 270) diff --git a/src/FlightMap/Widgets/PhotoVideoControl.qml b/src/FlightMap/Widgets/PhotoVideoControl.qml index bedac2ec0874..5e9f23339155 100644 --- a/src/FlightMap/Widgets/PhotoVideoControl.qml +++ b/src/FlightMap/Widgets/PhotoVideoControl.qml @@ -21,10 +21,10 @@ Rectangle { property var _activeVehicle: globals.activeVehicle property var _cameraManager: _activeVehicle.cameraManager property var _camera: _cameraManager.currentCameraInstance - property bool _cameraInPhotoMode: _camera.cameraMode === MavlinkCameraControl.CAM_MODE_PHOTO || _camera.cameraMode === MavlinkCameraControl.CAM_MODE_SURVEY + property bool _cameraInPhotoMode: _camera.cameraMode === MavlinkCameraControlInterface.CAM_MODE_PHOTO || _camera.cameraMode === MavlinkCameraControlInterface.CAM_MODE_SURVEY property bool _cameraInVideoMode: !_cameraInPhotoMode - property bool _videoCaptureIdle: _camera.captureVideoState === MavlinkCameraControl.CaptureVideoStateIdle - property bool _photoCaptureIdle: _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateIdle + property bool _videoCaptureIdle: _camera.captureVideoState === MavlinkCameraControlInterface.CaptureVideoStateIdle + property bool _photoCaptureIdle: _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateIdle QGCPalette { id: qgcPal; colorGroupEnabled: enabled } @@ -151,7 +151,7 @@ Rectangle { border.width: 1 border.color: videoCaptureButtonPalette.buttonBorder visible: (_camera.hasModes && _cameraInVideoMode) || (!_camera.hasModes && _camera.capturesVideo) - enabled: _camera.captureVideoState !== MavlinkCameraControl.CaptureVideoStateDisabled + enabled: _camera.captureVideoState !== MavlinkCameraControlInterface.CaptureVideoStateDisabled QGCPalette { id: videoCaptureButtonPalette; colorGroupEnabled: videoCaptureButton.enabled } @@ -174,7 +174,7 @@ Rectangle { border.width: 1 border.color: videoCaptureButtonPalette.buttonBorder - property bool _isCapturing: _camera.captureVideoState === MavlinkCameraControl.CaptureVideoStateCapturing + property bool _isCapturing: _camera.captureVideoState === MavlinkCameraControlInterface.CaptureVideoStateCapturing } MouseArea { @@ -227,7 +227,7 @@ Rectangle { border.width: 1 border.color: photoCaptureButtonPalette.buttonBorder visible: (_camera.hasModes && _cameraInPhotoMode) || (!_camera.hasModes && (_camera.hasVideoStream || _camera.capturesPhotos)) - enabled: _camera.capturePhotosState !== MavlinkCameraControl.CapturePhotosStateDisabled + enabled: _camera.capturePhotosState !== MavlinkCameraControlInterface.CapturePhotosStateDisabled QGCPalette { id: photoCaptureButtonPalette; colorGroupEnabled: photoCaptureButton.enabled } @@ -250,16 +250,16 @@ Rectangle { border.width: 1 border.color: photoCaptureButtonPalette.buttonBorder - property bool _isCapturing: _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingSinglePhoto || - _camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingMultiplePhotos + property bool _isCapturing: _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingSinglePhoto || + _camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingMultiplePhotos } MouseArea { anchors.fill: parent onClicked: { - if (_camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateCapturingMultiplePhotos) { + if (_camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateCapturingMultiplePhotos) { _camera.stopTakePhoto() - } else if (_camera.capturePhotosState === MavlinkCameraControl.CapturePhotosStateIdle) { + } else if (_camera.capturePhotosState === MavlinkCameraControlInterface.CapturePhotosStateIdle) { _camera.takePhoto() } } @@ -304,7 +304,7 @@ Rectangle { Layout.alignment: Qt.AlignHCenter text: qsTr("Free: ") + _camera.storageFreeStr font.pointSize: ScreenTools.defaultFontPointSize - visible: _camera.storageStatus === MavlinkCameraControl.STORAGE_READY + visible: _camera.storageStatus === MavlinkCameraControlInterface.STORAGE_READY } QGCLabel { @@ -390,7 +390,7 @@ Rectangle { property bool _multipleMavlinkCameras: _cameraManager.cameras.count > 1 property bool _multipleMavlinkCameraStreams: _camera.streamLabels.length > 1 - property bool _cameraStorageSupported: _camera.storageStatus !== MavlinkCameraControl.STORAGE_NOT_SUPPORTED + property bool _cameraStorageSupported: _camera.storageStatus !== MavlinkCameraControlInterface.STORAGE_NOT_SUPPORTED property var _videoSettings: QGroundControl.settingsManager.videoSettings ColumnLayout { @@ -424,7 +424,7 @@ Rectangle { QGCLabel { text: qsTr("Blend Opacity") - visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND + visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND onVisibleChanged: gridLayout.dynamicRows += visible ? 1 : -1 } @@ -445,7 +445,7 @@ Rectangle { QGCLabel { text: qsTr("Photo Interval (seconds)") - visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControl.PHOTO_CAPTURE_TIMELAPSE + visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControlInterface.PHOTO_CAPTURE_TIMELAPSE onVisibleChanged: gridLayout.dynamicRows += visible ? 1 : -1 } @@ -506,7 +506,7 @@ Rectangle { from: 0 value: _camera.thermalOpacity live: true - visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND + visible: _camera.thermalStreamInstance && _camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND onValueChanged: _camera.thermalOpacity = value } @@ -582,7 +582,7 @@ Rectangle { value: _camera.photoLapse displayValue: true live: true - visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControl.PHOTO_CAPTURE_TIMELAPSE + visible: _camera.capturesPhotos && _camera.photoCaptureMode === MavlinkCameraControlInterface.PHOTO_CAPTURE_TIMELAPSE onValueChanged: _camera.photoLapse = value } diff --git a/src/FlightMap/Widgets/QGCCompassWidget.qml b/src/FlightMap/Widgets/QGCCompassWidget.qml index 4f7616ce00c8..cd8890b71cc9 100644 --- a/src/FlightMap/Widgets/QGCCompassWidget.qml +++ b/src/FlightMap/Widgets/QGCCompassWidget.qml @@ -58,12 +58,7 @@ Rectangle { Item { id: rotationParent anchors.fill: parent - - transform: Rotation { - origin.x: rotationParent.width / 2 - origin.y: rotationParent.height / 2 - angle: _lockNoseUpCompass ? -_heading : 0 - } + rotation: _lockNoseUpCompass ? -_heading : 0 CompassDial { anchors.fill: parent @@ -84,12 +79,7 @@ Rectangle { anchors.fill: parent sourceSize.height: parent.height visible: showCOG() - - transform: Rotation { - origin.x: cogPointer.width / 2 - origin.y: cogPointer.height / 2 - angle: _courseOverGround - } + rotation: _courseOverGround } Image { @@ -100,12 +90,7 @@ Rectangle { anchors.fill: parent sourceSize.height: parent.height visible: showHeadingToNextWP() - - transform: Rotation { - origin.x: nextWPPointer.width / 2 - origin.y: nextWPPointer.height / 2 - angle: _headingToNextWP - } + rotation: _headingToNextWP } // Launch location indicator @@ -123,13 +108,15 @@ Rectangle { font.bold: true color: qgcPal.text anchors.centerIn: parent + rotation: _lockNoseUpCompass ? _heading : 0 } transform: Translate { - property double _angle: _headingToHome + property double _angle: _headingToHome + property real _labelOffset: root.width / 2 + ScreenTools.defaultFontPixelHeight / 2 - x: translateCenterToAngleX(parent.width / 2, _angle) - y: translateCenterToAngleY(parent.height / 2, _angle) + x: translateCenterToAngleX(_labelOffset, _angle) + y: translateCenterToAngleY(_labelOffset, _angle) } } } diff --git a/src/FlyView/CMakeLists.txt b/src/FlyView/CMakeLists.txt index 2f77718c32fe..752b54e294ac 100644 --- a/src/FlyView/CMakeLists.txt +++ b/src/FlyView/CMakeLists.txt @@ -52,6 +52,7 @@ qt_add_qml_module(FlyViewModule ObstacleDistanceOverlay.qml ObstacleDistanceOverlayMap.qml ObstacleDistanceOverlayVideo.qml + OnScreenCameraTrackingController.qml OnScreenGimbalController.qml PreFlightBatteryCheck.qml PreFlightCheckList.qml diff --git a/src/FlyView/FlightDisplayViewUVC.qml b/src/FlyView/FlightDisplayViewUVC.qml index dbb2812a5a4b..3b78b99445de 100644 --- a/src/FlyView/FlightDisplayViewUVC.qml +++ b/src/FlyView/FlightDisplayViewUVC.qml @@ -7,6 +7,8 @@ Rectangle { id: _root width: parent.width height: parent.height + implicitWidth: videoOutput.implicitWidth + implicitHeight: videoOutput.implicitHeight color: Qt.rgba(0,0,0,0.75) clip: true anchors.centerIn: parent diff --git a/src/FlyView/FlightDisplayViewVideo.qml b/src/FlyView/FlightDisplayViewVideo.qml index f9efb19c385f..5c3b5e8c935f 100644 --- a/src/FlyView/FlightDisplayViewVideo.qml +++ b/src/FlyView/FlightDisplayViewVideo.qml @@ -12,9 +12,11 @@ Item { property bool useSmallFont: true - property double _ar: QGroundControl.videoManager.gstreamerEnabled - ? QGroundControl.videoManager.videoSize.width / QGroundControl.videoManager.videoSize.height - : QGroundControl.videoManager.aspectRatio + property double _ar: (cameraLoader.visible && cameraLoader.status === Loader.Ready) + ? cameraLoader.item.implicitWidth / cameraLoader.item.implicitHeight + : QGroundControl.videoManager.gstreamerEnabled + ? QGroundControl.videoManager.videoSize.width / QGroundControl.videoManager.videoSize.height + : QGroundControl.videoManager.aspectRatio property bool _showGrid: QGroundControl.settingsManager.videoSettings.gridLines.rawValue property var _dynamicCameras: globals.activeVehicle ? globals.activeVehicle.cameraManager : null property bool _connected: globals.activeVehicle ? !globals.activeVehicle.communicationLost : false @@ -23,6 +25,8 @@ Item { property var _camera: _isCamera ? _dynamicCameras.cameras.get(_curCameraIndex) : null property bool _hasZoom: _camera && _camera.hasZoom property int _fitMode: QGroundControl.settingsManager.videoSettings.videoFit.rawValue + property bool _showStreamLoader: QGroundControl.videoManager.decoding + property bool _showUvcLoader: QGroundControl.videoManager.isUvc property bool _isMode_FIT_WIDTH: _fitMode === 0 property bool _isMode_FIT_HEIGHT: _fitMode === 1 @@ -43,7 +47,7 @@ Item { anchors.fill: parent source: "/res/NoVideoBackground.jpg" fillMode: Image.PreserveAspectCrop - visible: !(QGroundControl.videoManager.decoding) + visible: !_showStreamLoader && !_showUvcLoader Rectangle { anchors.centerIn: parent @@ -68,7 +72,7 @@ Item { id: videoBackground anchors.fill: parent color: "black" - visible: QGroundControl.videoManager.decoding + visible: _showStreamLoader || _showUvcLoader function getWidth() { if(_ar != 0.0){ if(_isMode_FIT_HEIGHT @@ -116,59 +120,76 @@ Item { } } + } + } + Loader { + // GStreamer is causing crashes on Lenovo laptop OpenGL Intel drivers. In order to workaround this + // we don't load a QGCVideoBackground object when video is disabled. This prevents any video rendering + // code from running. Hence the Loader to completely remove it. + id: videoStreamLoader + anchors.fill: videoContentArea + visible: _showStreamLoader + sourceComponent: videoBackgroundComponent + + property bool videoDisabled: QGroundControl.settingsManager.videoSettings.videoSource.rawValue === QGroundControl.settingsManager.videoSettings.disabledVideoSource + } + //-- UVC Video (USB Camera or Video Device) + Loader { + id: cameraLoader + anchors.fill: videoContentArea + visible: _showUvcLoader + source: QGroundControl.videoManager.uvcEnabled ? "qrc:/qml/QGroundControl/FlyView/FlightDisplayViewUVC.qml" : "qrc:/qml/QGroundControl/FlyView//FlightDisplayViewDummy.qml" + } + + Item { + id: videoContentArea + height: parent.getHeight() + width: parent.getWidth() + anchors.centerIn: parent + visible: _showStreamLoader || _showUvcLoader + + // grid lines + Item { + anchors.fill: parent + visible: _showGrid && !QGroundControl.videoManager.fullScreen + Rectangle { color: Qt.rgba(1,1,1,0.5) height: parent.height width: 1 x: parent.width * 0.33 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) height: parent.height width: 1 x: parent.width * 0.66 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) width: parent.width height: 1 y: parent.height * 0.33 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } Rectangle { color: Qt.rgba(1,1,1,0.5) width: parent.width height: 1 y: parent.height * 0.66 - visible: _showGrid && !QGroundControl.videoManager.fullScreen } } } - Loader { - // GStreamer is causing crashes on Lenovo laptop OpenGL Intel drivers. In order to workaround this - // we don't load a QGCVideoBackground object when video is disabled. This prevents any video rendering - // code from running. Hence the Loader to completely remove it. - height: parent.getHeight() - width: parent.getWidth() - anchors.centerIn: parent - visible: QGroundControl.videoManager.decoding - sourceComponent: videoBackgroundComponent - - property bool videoDisabled: QGroundControl.settingsManager.videoSettings.videoSource.rawValue === QGroundControl.settingsManager.videoSettings.disabledVideoSource - } //-- Thermal Image Item { id: thermalItem width: height * QGroundControl.videoManager.thermalAspectRatio - height: _camera ? (_camera.thermalMode === MavlinkCameraControl.THERMAL_FULL ? parent.height : (_camera.thermalMode === MavlinkCameraControl.THERMAL_PIP ? ScreenTools.defaultFontPixelHeight * 12 : parent.height * _thermalHeightFactor)) : 0 + height: _camera ? (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_FULL ? parent.height : (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_PIP ? ScreenTools.defaultFontPixelHeight * 12 : parent.height * _thermalHeightFactor)) : 0 anchors.centerIn: parent - visible: QGroundControl.videoManager.hasThermal && _camera.thermalMode !== MavlinkCameraControl.THERMAL_OFF + visible: QGroundControl.videoManager.hasThermal && _camera.thermalMode !== MavlinkCameraControlInterface.THERMAL_OFF function pipOrNot() { if(_camera) { - if(_camera.thermalMode === MavlinkCameraControl.THERMAL_PIP) { + if(_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_PIP) { anchors.centerIn = undefined anchors.top = parent.top anchors.topMargin = mainWindow.header.height + (ScreenTools.defaultFontPixelHeight * 0.5) @@ -194,7 +215,7 @@ Item { id: thermalVideo objectName: "thermalVideo" anchors.fill: parent - opacity: _camera ? (_camera.thermalMode === MavlinkCameraControl.THERMAL_BLEND ? _camera.thermalOpacity / 100 : 1.0) : 0 + opacity: _camera ? (_camera.thermalMode === MavlinkCameraControlInterface.THERMAL_BLEND ? _camera.thermalOpacity / 100 : 1.0) : 0 } } //-- Zoom diff --git a/src/FlyView/FlyView.qml b/src/FlyView/FlyView.qml index a58896c9d590..b617922cfd90 100644 --- a/src/FlyView/FlyView.qml +++ b/src/FlyView/FlyView.qml @@ -12,6 +12,7 @@ import QGroundControl import QGroundControl.Controls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.Toolbar import QGroundControl.Viewer3D Item { diff --git a/src/FlyView/FlyViewMap.qml b/src/FlyView/FlyViewMap.qml index 5b9dbf47b613..d6eacc939377 100644 --- a/src/FlyView/FlyViewMap.qml +++ b/src/FlyView/FlyViewMap.qml @@ -9,6 +9,7 @@ import QGroundControl import QGroundControl.Controls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.PlanView FlightMap { id: _root @@ -650,7 +651,7 @@ FlightMap { Layout.fillWidth: true text: qsTr("Edit Position") onClicked: { - roiEditPositionDialogFactory.open({ showSetPositionFromVehicle: false }) + roiEditPositionDialogFactory.open() roiEditDropPanel.close() } } @@ -680,8 +681,11 @@ FlightMap { gotoLocationItem.show(mapClickCoord) if ((_activeVehicle.flightMode == _activeVehicle.gotoFlightMode) && !_flyViewSettings.goToLocationRequiresConfirmInGuided.value) { - globals.guidedControllerFlyView.executeAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord, gotoLocationItem) - gotoLocationItem.actionConfirmed() // Still need to call this to commit the new coordinate and radius + if (globals.guidedControllerFlyView.executeAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord)) { + gotoLocationItem.actionConfirmed() // Still need to call this to commit the new coordinate and radius + } else { + gotoLocationItem.actionCancelled() + } } else { globals.guidedControllerFlyView.confirmAction(globals.guidedControllerFlyView.actionGoto, mapClickCoord, gotoLocationItem) } @@ -771,8 +775,5 @@ FlightMap { anchors.top: parent.top mapControl: _root visible: !ScreenTools.isTinyScreen && QGroundControl.corePlugin.options.flyView.showMapScale && mapControl.pipState.state === mapControl.pipState.windowState - - property real centerInset: visible ? parent.height - y : 0 } - } diff --git a/src/FlyView/FlyViewVideo.qml b/src/FlyView/FlyViewVideo.qml index 78d9fcdb7499..0b5e95a465a1 100644 --- a/src/FlyView/FlyViewVideo.qml +++ b/src/FlyView/FlyViewVideo.qml @@ -9,9 +9,6 @@ Item { property Item pipView property Item pipState: videoPipState - property int _track_rec_x: 0 - property int _track_rec_y: 0 - PipState { id: videoPipState pipView: _root.pipView @@ -47,20 +44,13 @@ Item { id: videoStreaming anchors.fill: parent useSmallFont: _root.pipState.state !== _root.pipState.fullState - visible: QGroundControl.videoManager.isStreamSource - } - //-- UVC Video (USB Camera or Video Device) - Loader { - id: cameraLoader - anchors.fill: parent - visible: QGroundControl.videoManager.isUvc - source: QGroundControl.videoManager.uvcEnabled ? "qrc:/qml/QGroundControl/FlyView/FlightDisplayViewUVC.qml" : "qrc:/qml/QGroundControl/FlyView//FlightDisplayViewDummy.qml" + visible: QGroundControl.videoManager.isStreamSource || QGroundControl.videoManager.isUvc } QGCLabel { text: qsTr("Double-click to exit full screen") font.pointSize: ScreenTools.largeFontPointSize - visible: QGroundControl.videoManager.fullScreen && flyViewVideoMouseArea.containsMouse + visible: QGroundControl.videoManager.fullScreen anchors.centerIn: parent onVisibleChanged: { @@ -81,159 +71,56 @@ Item { OnScreenGimbalController { id: onScreenGimbalController anchors.fill: parent - screenX: flyViewVideoMouseArea.mouseX - screenY: flyViewVideoMouseArea.mouseY - cameraTrackingEnabled: videoStreaming._camera && videoStreaming._camera.trackingEnabled + cameraTrackingEnabled: !!(videoStreaming._camera && videoStreaming._camera.trackingEnabled) + } + + OnScreenCameraTrackingController { + id: cameraTrackingController + anchors.fill: parent + camera: videoStreaming._camera + videoWidth: videoStreaming.getWidth() + videoHeight: videoStreaming.getHeight() } MouseArea { id: flyViewVideoMouseArea anchors.fill: parent enabled: pipState.state === pipState.fullState - hoverEnabled: true - - property double x0: 0 - property double x1: 0 - property double y0: 0 - property double y1: 0 - property double offset_x: 0 - property double offset_y: 0 - property double radius: 20 - property var trackingROI: null - property var trackingStatus: trackingStatusComponent.createObject(flyViewVideoMouseArea, {}) - - onClicked: onScreenGimbalController.clickControl() - onDoubleClicked: QGroundControl.videoManager.fullScreen = !QGroundControl.videoManager.fullScreen - - onPressed:(mouse) => { - onScreenGimbalController.pressControl() - - _track_rec_x = mouse.x - _track_rec_y = mouse.y - //create a new rectangle at the wanted position - if(videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled) { - trackingROI = trackingROIComponent.createObject(flyViewVideoMouseArea, { - "x": mouse.x, - "y": mouse.y - }); - } - } - } - onPositionChanged: (mouse) => { - //on move, update the width of rectangle - if (trackingROI !== null) { - if (mouse.x < trackingROI.x) { - trackingROI.x = mouse.x - trackingROI.width = Math.abs(mouse.x - _track_rec_x) - } else { - trackingROI.width = Math.abs(mouse.x - trackingROI.x) - } - if (mouse.y < trackingROI.y) { - trackingROI.y = mouse.y - trackingROI.height = Math.abs(mouse.y - _track_rec_y) - } else { - trackingROI.height = Math.abs(mouse.y - trackingROI.y) - } - } - } - onReleased: (mouse) => { - onScreenGimbalController.releaseControl() + property real _pressX: 0 + property real _pressY: 0 + property bool _dragging: false + readonly property real _dragThreshold: 10 - //if there is already a selection, delete it - if (trackingROI !== null) { - trackingROI.destroy(); - } + onDoubleClicked: QGroundControl.videoManager.fullScreen = !QGroundControl.videoManager.fullScreen - if(videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled) { - // order coordinates --> top/left and bottom/right - x0 = Math.min(_track_rec_x, mouse.x) - x1 = Math.max(_track_rec_x, mouse.x) - y0 = Math.min(_track_rec_y, mouse.y) - y1 = Math.max(_track_rec_y, mouse.y) - - //calculate offset between video stream rect and background (black stripes) - offset_x = (parent.width - videoStreaming.getWidth()) / 2 - offset_y = (parent.height - videoStreaming.getHeight()) / 2 - - //convert absolute coords in background to absolute video stream coords - x0 = x0 - offset_x - x1 = x1 - offset_x - y0 = y0 - offset_y - y1 = y1 - offset_y - - //convert absolute to relative coordinates and limit range to 0...1 - x0 = Math.max(Math.min(x0 / videoStreaming.getWidth(), 1.0), 0.0) - x1 = Math.max(Math.min(x1 / videoStreaming.getWidth(), 1.0), 0.0) - y0 = Math.max(Math.min(y0 / videoStreaming.getHeight(), 1.0), 0.0) - y1 = Math.max(Math.min(y1 / videoStreaming.getHeight(), 1.0), 0.0) - - //use point message if rectangle is very small - if (Math.abs(_track_rec_x - mouse.x) < 10 && Math.abs(_track_rec_y - mouse.y) < 10) { - var pt = Qt.point(x0, y0) - videoStreaming._camera.startTracking(pt, radius / videoStreaming.getWidth()) - } else { - var rec = Qt.rect(x0, y0, x1 - x0, y1 - y0) - videoStreaming._camera.startTracking(rec) - } - _track_rec_x = 0 - _track_rec_y = 0 - } - } + onPressed: (mouse) => { + _pressX = mouse.x + _pressY = mouse.y + _dragging = false } - Component { - id: trackingROIComponent - - Rectangle { - color: Qt.rgba(0.1,0.85,0.1,0.25) - border.color: "green" - border.width: 1 + onPositionChanged: (mouse) => { + if (!_dragging && (Math.abs(mouse.x - _pressX) >= _dragThreshold || Math.abs(mouse.y - _pressY) >= _dragThreshold)) { + _dragging = true + onScreenGimbalController.mouseDragStart(_pressX, _pressY) + cameraTrackingController.mouseDragStart(_pressX, _pressY) } - } - - Component { - id: trackingStatusComponent - - Rectangle { - color: "transparent" - border.color: "red" - border.width: 5 - radius: 5 + if (_dragging) { + onScreenGimbalController.mouseDragPositionChanged(mouse.x, mouse.y) + cameraTrackingController.mouseDragPositionChanged(mouse.x, mouse.y) } } - Timer { - id: trackingStatusTimer - interval: 50 - repeat: true - running: true - onTriggered: { - if (videoStreaming._camera) { - if (videoStreaming._camera.trackingEnabled && videoStreaming._camera.trackingImageStatus) { - var margin_hor = (parent.parent.width - videoStreaming.getWidth()) / 2 - var margin_ver = (parent.parent.height - videoStreaming.getHeight()) / 2 - var left = margin_hor + videoStreaming.getWidth() * videoStreaming._camera.trackingImageRect.left - var top = margin_ver + videoStreaming.getHeight() * videoStreaming._camera.trackingImageRect.top - var right = margin_hor + videoStreaming.getWidth() * videoStreaming._camera.trackingImageRect.right - var bottom = margin_ver + !isNaN(videoStreaming._camera.trackingImageRect.bottom) ? videoStreaming.getHeight() * videoStreaming._camera.trackingImageRect.bottom : top + (right - left) - var width = right - left - var height = bottom - top - - flyViewVideoMouseArea.trackingStatus.x = left - flyViewVideoMouseArea.trackingStatus.y = top - flyViewVideoMouseArea.trackingStatus.width = width - flyViewVideoMouseArea.trackingStatus.height = height - } else { - flyViewVideoMouseArea.trackingStatus.x = 0 - flyViewVideoMouseArea.trackingStatus.y = 0 - flyViewVideoMouseArea.trackingStatus.width = 0 - flyViewVideoMouseArea.trackingStatus.height = 0 - } - } + onReleased: (mouse) => { + if (_dragging) { + onScreenGimbalController.mouseDragEnd() + cameraTrackingController.mouseDragEnd(mouse.x, mouse.y) + } else { + onScreenGimbalController.mouseClicked(mouse.x, mouse.y) + cameraTrackingController.mouseClicked(mouse.x, mouse.y) } + _dragging = false } } diff --git a/src/FlyView/FlyViewWidgetLayer.qml b/src/FlyView/FlyViewWidgetLayer.qml index b18a2f8bd85c..f79499a123d7 100644 --- a/src/FlyView/FlyViewWidgetLayer.qml +++ b/src/FlyView/FlyViewWidgetLayer.qml @@ -166,6 +166,7 @@ Item { MapScale { id: mapScale anchors.left: toolStrip.right + anchors.leftMargin: _toolsMargin anchors.top: parent.top mapControl: _mapControl autoHide: true diff --git a/src/FlyView/GuidedActionConfirm.qml b/src/FlyView/GuidedActionConfirm.qml index b08ceb43af42..0f58cb85249e 100644 --- a/src/FlyView/GuidedActionConfirm.qml +++ b/src/FlyView/GuidedActionConfirm.qml @@ -90,9 +90,13 @@ Item { guidedValueSlider.visible = false } hideTrigger = false - guidedController.executeAction(control.action, control.actionData, sliderOutputValue, control.optionChecked) + let success = guidedController.executeAction(control.action, control.actionData, sliderOutputValue, control.optionChecked) if (mapIndicator) { - mapIndicator.actionConfirmed() + if (success) { + mapIndicator.actionConfirmed() + } else { + mapIndicator.actionCancelled() + } mapIndicator = undefined } } diff --git a/src/FlyView/GuidedActionsController.qml b/src/FlyView/GuidedActionsController.qml index 1909f25d01c2..570eab6629e8 100644 --- a/src/FlyView/GuidedActionsController.qml +++ b/src/FlyView/GuidedActionsController.qml @@ -125,15 +125,15 @@ Item { property bool showArm: _guidedActionsEnabled && !_vehicleArmed && _canArm property bool showForceArm: _guidedActionsEnabled && !_vehicleArmed property bool showDisarm: _guidedActionsEnabled && _vehicleArmed && !_vehicleFlying - property bool showRTL: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.guidedModeSupported && _vehicleFlying && !_vehicleInRTLMode - property bool showTakeoff: _guidedActionsEnabled && _activeVehicle.takeoffVehicleSupported && !_vehicleFlying && _canTakeoff - property bool showLand: _guidedActionsEnabled && _activeVehicle.guidedModeSupported && _vehicleArmed && !_activeVehicle.fixedWing && !_vehicleInLandMode + property bool showRTL: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.supports.guidedMode && _vehicleFlying && !_vehicleInRTLMode + property bool showTakeoff: _guidedActionsEnabled && (_activeVehicle.supports.guidedTakeoffWithAltitude || _activeVehicle.supports.guidedTakeoffWithoutAltitude) && !_vehicleFlying && _canTakeoff + property bool showLand: _guidedActionsEnabled && _activeVehicle.supports.guidedMode && _vehicleArmed && !_activeVehicle.fixedWing && !_vehicleInLandMode property bool showStartMission: _guidedActionsEnabled && _missionAvailable && !_missionActive && !_vehicleFlying && _canStartMission property bool showContinueMission: _guidedActionsEnabled && _missionAvailable && !_missionActive && _vehicleArmed && _vehicleFlying && (_currentMissionIndex < _missionItemCount - 1) - property bool showPause: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.pauseVehicleSupported && _vehicleFlying && !_vehiclePaused && !_fixedWingOnApproach - property bool showChangeAlt: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive - property bool showChangeLoiterRadius: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive && _vehicleInFwdFlight && fwdFlightGotoMapCircle.visible - property bool showChangeSpeed: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.guidedModeSupported && _vehicleArmed && !_missionActive && _speedLimitsAvailable + property bool showPause: _guidedActionsEnabled && _vehicleArmed && _activeVehicle.supports.pauseVehicle && _vehicleFlying && !_vehiclePaused && !_fixedWingOnApproach + property bool showChangeAlt: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive + property bool showChangeLoiterRadius: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive && _vehicleInFwdFlight && fwdFlightGotoMapCircle.visible + property bool showChangeSpeed: _guidedActionsEnabled && _vehicleFlying && _activeVehicle.supports.guidedMode && _vehicleArmed && !_missionActive && _speedLimitsAvailable property bool showOrbit: _guidedActionsEnabled && _vehicleFlying && __orbitSupported && !_missionActive && _activeVehicle.homePosition.isValid && !isNaN(_activeVehicle.homePosition.altitude) property bool showROI: _guidedActionsEnabled && _vehicleFlying && __roiSupported property bool showLandAbort: _guidedActionsEnabled && _vehicleFlying && _fixedWingOnApproach @@ -176,10 +176,10 @@ Item { property bool _speedLimitsAvailable: _activeVehicle && ((_vehicleInFwdFlight && _activeVehicle.haveFWSpeedLimits) || (!_vehicleInFwdFlight && _activeVehicle.haveMRSpeedLimits)) // You can turn on log output for GuidedActionsController by turning on GuidedActionsControllerLog category - property bool __guidedModeSupported: _activeVehicle ? _activeVehicle.guidedModeSupported : false - property bool __pauseVehicleSupported: _activeVehicle ? _activeVehicle.pauseVehicleSupported : false - property bool __roiSupported: _activeVehicle ? !_hideROI && _activeVehicle.roiModeSupported : false - property bool __orbitSupported: _activeVehicle ? !_hideOrbit && _activeVehicle.orbitModeSupported : false + property bool __guidedModeSupported: _activeVehicle ? _activeVehicle.supports.guidedMode : false + property bool __pauseVehicleSupported: _activeVehicle ? _activeVehicle.supports.pauseVehicle : false + property bool __roiSupported: _activeVehicle ? !_hideROI && _activeVehicle.supports.roiMode : false + property bool __orbitSupported: _activeVehicle ? !_hideOrbit && _activeVehicle.supports.orbitMode : false property bool __flightMode: _flightMode // Allow custom builds to add custom actions by overriding CustomGuidedActionsController.qml @@ -218,8 +218,8 @@ Item { guidedValueSlider.setupSlider( GuidedValueSlider.SliderType.Speed, _unitsConversion.metersSecondToAppSettingsSpeedUnits(0.1).toFixed(1), - _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotor()).toFixed(1), - _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotor()/2).toFixed(1), + _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotorMetersSecond()).toFixed(1), + _unitsConversion.metersSecondToAppSettingsSpeedUnits(_activeVehicle.maximumHorizontalSpeedMultirotorMetersSecond()/2).toFixed(1), qsTr("Speed")) } else { console.error("setupSlider called for inapproproate change speed action", _vehicleInFwdFlight, _activeVehicle.haveMRSpeedLimits) @@ -426,7 +426,7 @@ Item { confirmDialog.title = takeoffTitle confirmDialog.message = takeoffMessage confirmDialog.hideTrigger = Qt.binding(function() { return !showTakeoff }) - guidedValueSlider.visible = _activeVehicle.guidedTakeoffSupported + guidedValueSlider.visible = _activeVehicle.supports.guidedTakeoffWithAltitude break; case actionStartMission: showImmediate = false @@ -461,7 +461,7 @@ Item { case actionRTL: confirmDialog.title = rtlTitle confirmDialog.message = rtlMessage - if (_activeVehicle.supportsSmartRTL) { + if (_activeVehicle.supports.smartRTL) { confirmDialog.optionText = qsTr("Smart RTL") confirmDialog.optionChecked = false } @@ -549,6 +549,7 @@ Item { } // Executes the specified action + // Returns false if the action failed and any associated map indicator should be restored function executeAction(actionCode, actionData, sliderOutputValue, optionChecked) { var i; var selectedVehicles; @@ -560,7 +561,7 @@ Item { _activeVehicle.guidedModeLand() break case actionTakeoff: - if (_activeVehicle.guidedTakeoffSupported) { + if (_activeVehicle.supports.guidedTakeoffWithAltitude) { var valueInMeters = _unitsConversion.appSettingsVerticalDistanceUnitsToMeters(sliderOutputValue) _activeVehicle.guidedModeTakeoff(valueInMeters) } else { @@ -614,19 +615,23 @@ Item { _activeVehicle.guidedModeChangeAltitude(altitudeChangeInMeters, false /* pauseVehicle */) break case actionChangeLoiterRadius: - _activeVehicle.guidedModeGotoLocation( + if (!_activeVehicle.guidedModeGotoLocation( fwdFlightGotoMapCircle.coordinate, (fwdFlightGotoMapCircle.clockwiseRotation ? 1 : -1) * Math.abs(fwdFlightGotoMapCircle.radius.rawValue) - ) + )) { + return false + } break case actionGoto: - _activeVehicle.guidedModeGotoLocation( + if (!_activeVehicle.guidedModeGotoLocation( actionData, _vehicleInFwdFlight /* forwardFlightLoiterRadius */ ? _flyViewSettings.forwardFlightGoToLocationLoiterRad.value : 0 - ) + )) { + return false + } break case actionSetWaypoint: _activeVehicle.setCurrentMissionSequence(actionData) @@ -678,9 +683,10 @@ Item { default: if (!customController.customExecuteAction(actionCode, actionData, sliderOutputValue, optionChecked)) { console.warn(qsTr("Internal error: unknown actionCode"), actionCode) - return + return false } break } + return true } } diff --git a/src/FlyView/MultiVehicleList.qml b/src/FlyView/MultiVehicleList.qml index 3defc3378eb1..43e25cf07cb6 100644 --- a/src/FlyView/MultiVehicleList.qml +++ b/src/FlyView/MultiVehicleList.qml @@ -51,7 +51,7 @@ Item { function pauseAvailable() { for (var i = 0; i < selectedVehicles.count; i++) { var vehicle = selectedVehicles.get(i) - if (vehicle.armed === true && vehicle.pauseVehicleSupported) { + if (vehicle.armed === true && vehicle.supports.pauseVehicle) { return true } } diff --git a/src/FlyView/OnScreenCameraTrackingController.qml b/src/FlyView/OnScreenCameraTrackingController.qml new file mode 100644 index 000000000000..cb6182358ace --- /dev/null +++ b/src/FlyView/OnScreenCameraTrackingController.qml @@ -0,0 +1,150 @@ +import QtQuick + +Item { + id: rootItem + + required property var camera + required property real videoWidth + required property real videoHeight + + // Drag origin in parent coordinates + property real _dragStartX: 0 + property real _dragStartY: 0 + property bool _dragging: false + property real _dragCurrentX: 0 + property real _dragCurrentY: 0 + + readonly property bool _trackingEnabled: camera && camera.trackingEnabled + readonly property bool _canTrackPoint: _trackingEnabled && camera.supportsTrackingPoint + readonly property bool _canTrackRect: _trackingEnabled && camera.supportsTrackingRect + + function mouseClicked(mouseX, mouseY) { + if (!_canTrackPoint) { + return + } + if (videoWidth <= 0 || videoHeight <= 0) { + return + } + // Click = point tracking + var marginH = (width - videoWidth) / 2 + var marginV = (height - videoHeight) / 2 + var nx = Math.max(Math.min((mouseX - marginH) / videoWidth, 1.0), 0.0) + var ny = Math.max(Math.min((mouseY - marginV) / videoHeight, 1.0), 0.0) + var pointRadius = 20 + camera.startTrackingPoint(Qt.point(nx, ny), Math.min(pointRadius / videoWidth, 1.0)) + } + + function mouseDragStart(mouseX, mouseY) { + if (!_canTrackRect) { + return + } + _dragStartX = mouseX + _dragStartY = mouseY + _dragCurrentX = mouseX + _dragCurrentY = mouseY + _dragging = true + } + + function mouseDragPositionChanged(mouseX, mouseY) { + if (!_dragging) { + return + } + _dragCurrentX = mouseX + _dragCurrentY = mouseY + } + + function mouseDragEnd(mouseX, mouseY) { + _dragging = false + + if (!_canTrackRect) { + return + } + if (videoWidth <= 0 || videoHeight <= 0) { + return + } + + // Order coordinates: top-left and bottom-right + var x0 = Math.min(_dragStartX, mouseX) + var x1 = Math.max(_dragStartX, mouseX) + var y0 = Math.min(_dragStartY, mouseY) + var y1 = Math.max(_dragStartY, mouseY) + + // Letterbox margins (black bars when aspect ratio doesn't match) + var marginH = (width - videoWidth) / 2 + var marginV = (height - videoHeight) / 2 + + // Convert from view coordinates to video-relative coordinates + x0 = x0 - marginH + x1 = x1 - marginH + y0 = y0 - marginV + y1 = y1 - marginV + + // Normalize to 0..1 + x0 = Math.max(Math.min(x0 / videoWidth, 1.0), 0.0) + x1 = Math.max(Math.min(x1 / videoWidth, 1.0), 0.0) + y0 = Math.max(Math.min(y0 / videoHeight, 1.0), 0.0) + y1 = Math.max(Math.min(y1 / videoHeight, 1.0), 0.0) + + var w = x1 - x0 + var h = y1 - y0 + + // Ignore degenerate rectangles (e.g. mostly-horizontal/vertical drags) + if (w < 0.01 || h < 0.01) { + _dragStartX = 0 + _dragStartY = 0 + return + } + + // Drag = rectangle tracking + camera.startTrackingRect(Qt.rect(x0, y0, w, h)) + + _dragStartX = 0 + _dragStartY = 0 + } + + // --- ROI selection overlay (green rectangle while dragging) --- + Rectangle { + visible: _dragging + color: Qt.rgba(0.1, 0.85, 0.1, 0.25) + border.color: "green" + border.width: 1 + x: Math.min(_dragStartX, _dragCurrentX) + y: Math.min(_dragStartY, _dragCurrentY) + width: Math.abs(_dragCurrentX - _dragStartX) + height: Math.abs(_dragCurrentY - _dragStartY) + } + + // --- Tracking status overlay (red rectangle/circle from camera feedback) --- + // Coordinates are normalized to the video frame (0.0–1.0). Convert to screen + // pixels by scaling by the displayed video size and offsetting by letterbox margins. + readonly property bool _trackingActive: _trackingEnabled && camera.trackingImageIsActive + readonly property bool _isPoint: _trackingActive && camera.trackingImageIsPoint + readonly property real _marginH: (rootItem.width - videoWidth) / 2 + readonly property real _marginV: (rootItem.height - videoHeight) / 2 + + Rectangle { + id: trackingStatusOverlay + color: "transparent" + border.color: "red" + border.width: 5 + radius: rootItem._isPoint ? width / 2 : 5 + visible: rootItem._trackingActive + + x: rootItem._trackingActive + ? (rootItem._isPoint ? rootItem._marginH + videoWidth * (camera.trackingImagePoint.x - camera.trackingImageRadius) + : rootItem._marginH + videoWidth * camera.trackingImageRect.x) + : 0 + y: rootItem._trackingActive + ? (rootItem._isPoint ? rootItem._marginV + videoHeight * (camera.trackingImagePoint.y - camera.trackingImageRadius) + : rootItem._marginV + videoHeight * camera.trackingImageRect.y) + : 0 + width: rootItem._trackingActive + ? (rootItem._isPoint ? videoWidth * camera.trackingImageRadius * 2 + : videoWidth * camera.trackingImageRect.width) + : 0 + height: rootItem._trackingActive + ? (rootItem._isPoint ? width + : videoHeight * camera.trackingImageRect.height) + : 0 + } +} diff --git a/src/FlyView/OnScreenGimbalController.qml b/src/FlyView/OnScreenGimbalController.qml index 08d015f30636..dedd1c678296 100644 --- a/src/FlyView/OnScreenGimbalController.qml +++ b/src/FlyView/OnScreenGimbalController.qml @@ -4,86 +4,70 @@ import QGroundControl import QGroundControl.Controls Item { - id: rootItem - anchors.fill: parent + id: rootItem + anchors.fill: parent - property var screenX - property var screenY - property var screenXrateInitCoocked - property var screenYrateInitCoocked + required property bool cameraTrackingEnabled - property var activeVehicle: QGroundControl.multiVehicleManager.activeVehicle - property var gimbalController: activeVehicle ? activeVehicle.gimbalController : undefined - property var activeGimbal: gimbalController ? gimbalController.activeGimbal : undefined - property bool gimbalAvailable: activeGimbal != undefined - property var gimbalControllerSettings: QGroundControl.settingsManager.gimbalControllerSettings - property bool cameraTrackingEnabled: false // Used to ignore clicks when camera tracking operation is active, otherwise it would collide with these gimbal controls - property bool shouldProcessClicks: gimbalControllerSettings.EnableOnScreenControl.value && activeGimbal && !cameraTrackingEnabled ? true : false + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + property var _gimbalController: _activeVehicle ? _activeVehicle.gimbalController : undefined + property var _activeGimbal: _gimbalController ? _gimbalController.activeGimbal : undefined + property bool _gimbalAvailable: _activeGimbal != undefined + property var _gimbalControllerSettings: QGroundControl.settingsManager.gimbalControllerSettings + property bool _shouldProcessClicks: _gimbalControllerSettings.enableOnScreenControl.value && _activeGimbal && !cameraTrackingEnabled ? true : false - function clickControl() { - if (!shouldProcessClicks) { + property real _mouseX: 0 + property real _mouseY: 0 + property real _dragStartNormX: 0 + property real _dragStartNormY: 0 + + function _toNormX(mouseX) { return ((mouseX / width) * 2) - 1 } + function _toNormY(mouseY) { return -((mouseY / height) * 2) + 1 } + + function mouseClicked(mouseX, mouseY) { + if (!_shouldProcessClicks) { return } - // If click and slide control, return, it uses press and release - if (!gimbalControllerSettings.ControlType.rawValue == 0) { + if (_gimbalControllerSettings.clickAndDrag.rawValue) { return } - clickAndPoint(x, y) - } - - // Sends a +-(0-1) xy value to vehicle.gimbalController.gimbalOnScreenControl - function clickAndPoint() { - if (rootItem.gimbalAvailable) { - var xCoocked = ( (screenX / parent.width) * 2) - 1 - var yCoocked = -( (screenY / parent.height) * 2) + 1 - // console.log("X global: " + x + " Y global: " + y) - // console.log("X coocked: " + xCoocked + " Y coocked: " + yCoocked) - gimbalController.gimbalOnScreenControl(xCoocked, yCoocked, true, false, false) - } else { - // We should never be here - console.log("gimbal not available") + if (rootItem._gimbalAvailable) { + _gimbalController.gimbalOnScreenControl(_toNormX(mouseX), _toNormY(mouseY), true, false, false) } } - function pressControl() { - if (!shouldProcessClicks) { + function mouseDragStart(mouseX, mouseY) { + if (!_shouldProcessClicks) { return } - // If click and point control return, that is handled exclusively on clickAndPoint() - if (!gimbalControllerSettings.ControlType.rawValue == 1) { + if (!_gimbalControllerSettings.clickAndDrag.rawValue) { return } + _mouseX = mouseX + _mouseY = mouseY + _dragStartNormX = _toNormX(mouseX) + _dragStartNormY = _toNormY(mouseY) sendRateTimer.start() - screenXrateInitCoocked = ( ( screenX / parent.width) * 2) - 1 - screenYrateInitCoocked = -( ( screenY / parent.height) * 2) + 1 } - function releaseControl() { - if (!shouldProcessClicks) { - return - } - // If click and point control return, that is handled exclusively on clickAndPoint() - if (!gimbalControllerSettings.ControlType.rawValue == 1) { - return - } + function mouseDragPositionChanged(mouseX, mouseY) { + _mouseX = mouseX + _mouseY = mouseY + } + + function mouseDragEnd() { sendRateTimer.stop() - screenXrateInitCoocked = null - screenYrateInitCoocked = null } Timer { - id: sendRateTimer - interval: 100 - repeat: true + id: sendRateTimer + interval: 100 + repeat: true onTriggered: { - if (rootItem.gimbalAvailable) { - var xCoocked = ( ( screenX / parent.width) * 2) - 1 - var yCoocked = -( ( screenY / parent.height) * 2) + 1 - xCoocked -= screenXrateInitCoocked - yCoocked -= screenYrateInitCoocked - gimbalController.gimbalOnScreenControl(xCoocked, yCoocked, false, true, true) - } else { - console.log("gimbal not available") + if (rootItem._gimbalAvailable) { + var dx = rootItem._toNormX(rootItem._mouseX) - rootItem._dragStartNormX + var dy = rootItem._toNormY(rootItem._mouseY) - rootItem._dragStartNormY + _gimbalController.gimbalOnScreenControl(dx, dy, false, true, true) } } } diff --git a/src/GPS/CMakeLists.txt b/src/GPS/CMakeLists.txt index 7a3aba76de22..902a62e6e255 100644 --- a/src/GPS/CMakeLists.txt +++ b/src/GPS/CMakeLists.txt @@ -18,8 +18,6 @@ target_sources(${CMAKE_PROJECT_NAME} GPSRtk.h GPSRTKFactGroup.cc GPSRTKFactGroup.h - NTRIP.cc - NTRIP.h RTCMMavlink.cc RTCMMavlink.h satellite_info.h @@ -31,6 +29,11 @@ target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +# ---------------------------------------------------------------------------- +# NTRIP Subsystem +# ---------------------------------------------------------------------------- +add_subdirectory(NTRIP) + # ============================================================================ # PX4 GPS Drivers Integration # ============================================================================ diff --git a/src/GPS/NTRIP.cc b/src/GPS/NTRIP.cc deleted file mode 100644 index 77b922c1f141..000000000000 --- a/src/GPS/NTRIP.cc +++ /dev/null @@ -1,1303 +0,0 @@ -#include "NTRIP.h" -#include "NTRIPSettings.h" -#include "Fact.h" -#include "FactGroup.h" -#include "QGCApplication.h" -#include "QGCLoggingCategory.h" -#include "RTCMMavlink.h" -#include "SettingsManager.h" -#include -#include - -#include "MultiVehicleManager.h" -#include "Vehicle.h" -#include "PositionManager.h" -#include -#include - -#include -#include -#include - -#include -#include - -QGC_LOGGING_CATEGORY(NTRIPLog, "qgc.ntrip") - -// Register the QML type without constructing the singleton up-front. -// This avoids creating NTRIPManager during a temporary Q(Core)Application -// used for command-line parsing, which could lead to it being destroyed -// early and leaving a dangling static pointer. -static QObject* _ntripManagerQmlProvider(QQmlEngine*, QJSEngine*) -{ - return NTRIPManager::instance(); -} - -static void _ntripManagerRegisterQmlTypes() -{ - qmlRegisterSingletonType("QGroundControl.NTRIP", 1, 0, "NTRIPManager", _ntripManagerQmlProvider); -} -Q_COREAPP_STARTUP_FUNCTION(_ntripManagerRegisterQmlTypes) - -NTRIPManager* NTRIPManager::_instance = nullptr; - -// constructor -NTRIPManager::NTRIPManager(QObject* parent) - : QObject(parent) - , _ntripStatus(tr("Disconnected")) -{ - qCDebug(NTRIPLog) << "NTRIPManager created"; - _startupTimer.start(); - - _rtcmMavlink = qgcApp() ? qgcApp()->findChild() : nullptr; - if (!_rtcmMavlink) { - QObject* parentObj = qgcApp() ? static_cast(qgcApp()) : static_cast(this); - // Ensure an RTCMMavlink helper exists for forwarding RTCM messages - _rtcmMavlink = new RTCMMavlink(parentObj); - _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink")); - qCDebug(NTRIPLog) << "NTRIP Created RTCMMavlink helper"; - } - - // Force NTRIP checkbox OFF during app startup and keep it OFF until first settings tick. - { - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - if (settings && settings->ntripServerConnectEnabled()) { - Fact* fact = settings->ntripServerConnectEnabled(); - - // Force OFF immediately - fact->setRawValue(false); - - // While !_forcedOffOnce, if something tries to flip it ON, flip it back OFF. - _ntripEnableConn = connect(fact, &Fact::rawValueChanged, - this, [this, fact]() { - if (!_forcedOffOnce) { - const bool wantOn = fact->rawValue().toBool(); - if (wantOn) { - qCDebug(NTRIPLog) << "NTRIP Startup: coercing ntripServerConnectEnabled back to false"; - fact->setRawValue(false); - } - } - }); - } - } - - // Check settings periodically - _settingsCheckTimer = new QTimer(this); - connect(_settingsCheckTimer, &QTimer::timeout, this, &NTRIPManager::_checkSettings); - _settingsCheckTimer->start(1000); // Check every second - - _ggaTimer = new QTimer(this); - _ggaTimer->setInterval(5000); - connect(_ggaTimer, &QTimer::timeout, this, &NTRIPManager::_sendGGA); - - connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection); - - _checkSettings(); -} - -// destructor -NTRIPManager::~NTRIPManager() -{ - qCDebug(NTRIPLog) << "NTRIPManager destroyed"; - stopNTRIP(); - // Clear singleton pointer in case we were destroyed (e.g., if created under - // a short-lived Q(Core)Application). - if (_instance == this) { - _instance = nullptr; - } -} - -RTCMParser::RTCMParser() -{ - reset(); -} - -void RTCMParser::reset() -{ - _state = WaitingForPreamble; - _messageLength = 0; - _bytesRead = 0; - _lengthBytesRead = 0; - _crcBytesRead = 0; -} - -bool RTCMParser::addByte(uint8_t byte) -{ - switch (_state) { - case WaitingForPreamble: - if (byte == RTCM3_PREAMBLE) { - _buffer[0] = byte; - _bytesRead = 1; - _state = ReadingLength; - _lengthBytesRead = 0; - } - break; - - case ReadingLength: - _lengthBytes[_lengthBytesRead++] = byte; - _buffer[_bytesRead++] = byte; - if (_lengthBytesRead == 2) { - // Extract 10-bit length from bytes (6 reserved bits + 10 length bits) - _messageLength = ((_lengthBytes[0] & 0x03) << 8) | _lengthBytes[1]; - if (_messageLength > 0 && _messageLength < 1021) { // Valid RTCM3 length - _state = ReadingMessage; - } else { - reset(); // Invalid length, restart - } - } - break; - - case ReadingMessage: - _buffer[_bytesRead++] = byte; - if (_bytesRead >= _messageLength + 3) { // +3 for header - _state = ReadingCRC; - _crcBytesRead = 0; - } - break; - - case ReadingCRC: - _crcBytes[_crcBytesRead++] = byte; - if (_crcBytesRead == 3) { - // Message complete - for simplicity, we'll skip CRC validation - return true; - } - break; - } - return false; -} - -uint16_t RTCMParser::messageId() -{ - if (_messageLength >= 2) { - // Message ID is in bits 14-25 of the message (after the 24-bit header) - return ((_buffer[3] << 4) | (_buffer[4] >> 4)) & 0xFFF; - } - return 0; -} - -NTRIPTCPLink::NTRIPTCPLink(const QString& hostAddress, - int port, - const QString& username, - const QString& password, - const QString& mountpoint, - const QString& whitelist, - bool useSpartn, - bool autoStart, - QObject* parent) - : QObject(parent) - , _hostAddress(hostAddress) - , _port(port) - , _username(username) - , _password(password) - , _mountpoint(mountpoint) - , _useSpartn(useSpartn) -{ - - - if (_useSpartn) { - // SPARTN path does not use RTCM whitelist or parser - if (!whitelist.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP SPARTN enabled; ignoring RTCM whitelist:" << whitelist; - } - // Initialize SPARTN header-strip guard - _spartnBuf.clear(); - _spartnNeedHeaderStrip = true; - - // Helpful hint if user left the default RTCM port - if (_port != 2102) { - qCWarning(NTRIPLog) << "NTRIP SPARTN is enabled but port is" << _port << "(expected 2102 for TLS)"; - } - } else { - // RTCM path: build whitelist and parser as before - for (const auto& msg : whitelist.split(',')) { - int msg_int = msg.toInt(); - if (msg_int) - _whitelist.append(msg_int); - } - qCDebug(NTRIPLog) << "NTRIP whitelist:" << _whitelist; - if (_whitelist.empty()) { - qCDebug(NTRIPLog) << "NTRIP whitelist is empty; all RTCM message IDs will be forwarded."; - } - if (!_rtcmParser) { - _rtcmParser = new RTCMParser(); - } - _rtcmParser->reset(); - } - - _state = NTRIPState::uninitialised; - - // Start only when requested so unit tests can exercise parser behavior without sockets. - if (autoStart) { - start(); - } -} - -NTRIPTCPLink::~NTRIPTCPLink() -{ - _stopping.store(true); - - if (_socket) { - QObject::disconnect(_readyReadConn); - _socket->disconnectFromHost(); - _socket->close(); - delete _socket; - _socket = nullptr; - } - - if (_rtcmParser) { - delete _rtcmParser; - _rtcmParser = nullptr; - } -} - -void NTRIPTCPLink::start() -{ - // This runs in the worker thread after QThread::started is emitted - _hardwareConnect(); -} - -void NTRIPTCPLink::_hardwareConnect() -{ - if (_stopping.load()) { - return; - } - - qCDebug(NTRIPLog) << "NTRIP connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint - << " tls=" << (_useSpartn ? "yes" : "no"); - - // allocate the appropriate socket type - // SPARTN selection (TLS on 2102 or when explicitly enabled) - if (_useSpartn) { - QSslSocket* sslSocket = new QSslSocket(this); - _socket = sslSocket; - } else { - _socket = new QTcpSocket(this); - } - - _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); - _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); - _socket->setReadBufferSize(0); - - QObject::connect(_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) { - // Suppress errors generated during intentional shutdown - if (_stopping.load() || !_socket) { - qCDebug(NTRIPLog) << "NTRIP suppressing socket error during intentional stop:" << int(code); - return; - } - - QString msg = _socket->errorString(); - - // If the peer closes before we receive any HTTP headers, try to give a helpful hint. - if (code == QAbstractSocket::RemoteHostClosedError && _state == NTRIPState::waiting_for_http_response) { - const QByteArray leftover = _socket->readAll(); - if (!leftover.isEmpty()) { - qCWarning(NTRIPLog) << "NTRIP peer closed; trailing bytes before close:" << leftover; - msg += QString(" (peer closed after %1 bytes)").arg(leftover.size()); - } else { - if (_port == 2102) { - msg += " (hint: port 2102 expects HTTPS/TLS; current socket is plain TCP)"; - } else if (!_mountpoint.isEmpty()) { - msg += " (peer closed before HTTP response; check mountpoint and credentials)"; - } - } - } - - qCWarning(NTRIPLog) << "NTRIP socket error code:" << int(code) - << " msg:" << msg - << " state:" << int(_socket->state()) - << " peer:" << _socket->peerAddress().toString() << ":" << _socket->peerPort() - << " local:" << _socket->localAddress().toString() << ":" << _socket->localPort(); - emit error(msg); - }, Qt::DirectConnection); - - // If the server cleanly disconnects, capture reason and emit error so NTRIPManager updates status - QObject::connect(_socket, &QTcpSocket::disconnected, this, [this]() { - // Suppress disconnect noise during intentional shutdown - if (_stopping.load() || !_socket) { - qCDebug(NTRIPLog) << "NTRIP suppressing disconnect signal during intentional stop"; - return; - } - - const QByteArray trailing = _socket->readAll(); - QString reason; - if (!trailing.isEmpty()) { - reason = QString::fromUtf8(trailing).trimmed(); - qCWarning(NTRIPLog) << "NTRIP disconnected; trailing bytes:" << trailing; - } else { - reason = "Server disconnected"; - qCWarning(NTRIPLog) << "NTRIP disconnected cleanly by server"; - } - // Emit richer context on disconnect - const qint64 t0 = this->property("ntrip_postok_t0_ms").toLongLong(); - const bool saw = this->property("ntrip_saw_rtcm").toBool(); - const qint64 dt = (t0 > 0) ? (QDateTime::currentMSecsSinceEpoch() - t0) : -1; - qCWarning(NTRIPLog) << "NTRIP disconnect context:" - << "rtcm_received_first_byte=" << (saw ? "yes" : "no") - << "ms_since_200=" << dt - << "peer=" << _socket->peerAddress().toString() << ":" << _socket->peerPort() - << "local=" << _socket->localAddress().toString() << ":" << _socket->localPort(); - emit error(reason); // Will call NTRIPManager::_tcpError() - }, Qt::DirectConnection); - - _readyReadConn = QObject::connect(_socket, &QTcpSocket::readyRead,this, &NTRIPTCPLink::_readBytes,Qt::DirectConnection); - - auto sendHttpRequest = [this]() { - if (!_mountpoint.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP Sending HTTP request"; - const QByteArray authB64 = QString(_username + ":" + _password).toUtf8().toBase64(); - QString query = - "GET /%1 HTTP/1.1\r\n" - "Host: %2\r\n" - "Ntrip-Version: Ntrip/2.0\r\n" - "User-Agent: QGC-NTRIP\r\n" - "Connection: keep-alive\r\n" - "Accept: */*\r\n" - "Authorization: Basic %3\r\n" - "\r\n"; - const QByteArray req = query.arg(_mountpoint).arg(_hostAddress).arg(QString::fromUtf8(authB64)).toUtf8(); - const qint64 written = _socket->write(req); - _socket->flush(); - - // Extra diagnostics: request line and auth presence (length only) - const QString reqStr = QString::fromUtf8(req); - const int cr = reqStr.indexOf("\r"); - const QString firstLine = (cr > 0) ? reqStr.left(cr).trimmed() : QString(); - const int authIdx = reqStr.indexOf("Authorization: Basic "); - int b64Len = -1; - if (authIdx >= 0) { - const int eol = reqStr.indexOf("\r\n", authIdx); - if (eol > authIdx) b64Len = eol - (authIdx + 21); - } - qCDebug(NTRIPLog) << "NTRIP HTTP request first line:" << firstLine; - qCDebug(NTRIPLog) << "NTRIP HTTP auth present:" << (authIdx >= 0) << "auth_b64_len:" << b64Len; - - qCDebug(NTRIPLog) << "NTRIP HTTP request bytes written:" << written; - _state = NTRIPState::waiting_for_http_response; - } - else { - _state = NTRIPState::waiting_for_rtcm_header; - emit connected(); - } - qCDebug(NTRIPLog) << "NTRIP Socket connected" - << "local" << _socket->localAddress().toString() << ":" << _socket->localPort() - << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort(); - }; - - if (_useSpartn) { - QSslSocket* sslSocket = qobject_cast(_socket); - Q_ASSERT(sslSocket); - - QObject::connect(sslSocket, &QSslSocket::encrypted, this, [sendHttpRequest]() { - qCDebug(NTRIPLog) << "SPARTN TLS connection established"; - sendHttpRequest(); - }, Qt::DirectConnection); - - QObject::connect(sslSocket, QOverload&>::of(&QSslSocket::sslErrors), - this, [](const QList &errors) { - for (const QSslError &e : errors) { - qCWarning(NTRIPLog) << "TLS Error:" << e.errorString(); - } - }, Qt::DirectConnection); - - sslSocket->connectToHostEncrypted(_hostAddress, static_cast(_port)); - - if (!sslSocket->waitForEncrypted(10000)) { - qCDebug(NTRIPLog) << "NTRIP TLS socket failed to establish encryption"; - emit error(_socket->errorString()); - delete _socket; - _socket = nullptr; - return; - } - - if (sslSocket->isEncrypted()) { - sendHttpRequest(); - } - } else { - QTcpSocket* tcpSocket = qobject_cast(_socket); - Q_ASSERT(tcpSocket); - - tcpSocket->connectToHost(_hostAddress, static_cast(_port)); - - if (!tcpSocket->waitForConnected(10000)) { - qCDebug(NTRIPLog) << "NTRIP Socket failed to connect"; - emit error(_socket->errorString()); - delete _socket; - _socket = nullptr; - return; - } - - sendHttpRequest(); - } -} - -void NTRIPTCPLink::_parse(const QByteArray& buffer) -{ - if (_stopping.load()) { - return; - } - - static bool logged_empty_once = false; - if (!logged_empty_once && _whitelist.empty()) { - qCDebug(NTRIPLog) << "NTRIP whitelist is empty at runtime; forwarding all RTCM messages."; - logged_empty_once = true; - } - - // RTCM v3 transport sizes - constexpr int kRtcmHeaderSize = 3; // preamble (1) + length (2) - - for (char ch : buffer) { - const uint8_t byte = static_cast(static_cast(ch)); - - if (_state == NTRIPState::waiting_for_rtcm_header) { - if (byte != RTCM3_PREAMBLE) { - continue; - } - _state = NTRIPState::accumulating_rtcm_packet; - } - - if (_rtcmParser->addByte(byte)) { - _state = NTRIPState::waiting_for_rtcm_header; - - // Build exact on-wire frame: [D3 | len(2) | payload | CRC(3)] - const int payload_len = static_cast(_rtcmParser->messageLength()); - QByteArray message(reinterpret_cast(_rtcmParser->message()), - kRtcmHeaderSize + payload_len); - - const uint8_t* crc_ptr = _rtcmParser->crcBytes(); - const int crc_len = _rtcmParser->crcSize(); - message.append(reinterpret_cast(crc_ptr), crc_len); - - const uint16_t id = _rtcmParser->messageId(); - - if (_whitelist.empty() || _whitelist.contains(id)) { - qCDebug(NTRIPLog) << "NTRIP RTCM packet id" << id << "len" << message.length(); - emit RTCMDataUpdate(message); - qCDebug(NTRIPLog) << "NTRIP Sending" << id << "of size" << message.length(); - } else { - qCDebug(NTRIPLog) << "NTRIP Ignoring" << id; - } - - _rtcmParser->reset(); - } - } -} - -void NTRIPTCPLink::_handleSpartnData(const QByteArray& dataIn) -{ - if (dataIn.isEmpty()) { - return; - } - - // Accumulate so we can remove a response header exactly once if it ever appears here. - _spartnBuf.append(dataIn); - - if (_spartnNeedHeaderStrip) { - const bool looksHttp = _spartnBuf.startsWith("HTTP/") || _spartnBuf.startsWith("ICY "); - if (looksHttp) { - const int headerEnd = _spartnBuf.indexOf("\r\n\r\n"); - if (headerEnd < 0) { - // Header incomplete; cap buffer to avoid growth on a bad stream. - if (_spartnBuf.size() > 32768) { - _spartnBuf = _spartnBuf.right(32768); - } - return; - } - // Drop the header and keep the payload. - _spartnBuf.remove(0, headerEnd + 4); - } - _spartnNeedHeaderStrip = false; - } - - if (_spartnBuf.isEmpty()) { - return; - } - - // Deliver raw SPARTN bytes to whoever is consuming them (e.g., a GNSS forwarder). - emit SPARTNDataUpdate(_spartnBuf); - - // Clear after handing off. - _spartnBuf.clear(); -} - -void NTRIPTCPLink::_readBytes() -{ - if (_stopping.load()) { - return; - } - - // SPARTN path: after HTTP 200 OK, feed raw bytes through the SPARTN handler - if (_socket && _state == NTRIPState::waiting_for_spartn_data) { - const QByteArray bytes = _socket->readAll(); - if (!bytes.isEmpty()) { - _handleSpartnData(bytes); - } - return; - } - - if (!_socket) { - return; - } - - // Static counters and flags scoped to this function (no header changes needed) - static quint64 s_totalRtcm = 0; - - if (_state == NTRIPState::waiting_for_http_response) { - QByteArray responseData = _socket->readAll(); - if (responseData.isEmpty()) { - return; - } - - QString response = QString::fromUtf8(responseData); - qCDebug(NTRIPLog) << "NTRIP HTTP response received:" << response.left(200); - - // Dump full headers once for diagnostics - const int hdrEnd = response.indexOf("\r\n\r\n"); - const QString headers = (hdrEnd >= 0) ? response.left(hdrEnd) : response; - qCDebug(NTRIPLog) << "NTRIP HTTP response headers BEGIN >>>"; - for (const QString& line : headers.split("\r\n")) { - if (!line.isEmpty()) qCDebug(NTRIPLog) << line; - } - qCDebug(NTRIPLog) << "NTRIP HTTP response headers <<< END"; - - // Parse status line - QStringList lines = response.split('\n'); - bool foundOkResponse = false; - for (const QString& line : lines) { - const QString trimmed = line.trimmed(); - if ((trimmed.startsWith("HTTP/") || trimmed.startsWith("ICY ")) && - (trimmed.contains(" 200 ") || trimmed.contains(" 201 "))) { - foundOkResponse = true; - qCDebug(NTRIPLog) << "NTRIP: Found OK response:" << trimmed; - break; - } else if ((trimmed.startsWith("HTTP/") || trimmed.startsWith("ICY ")) && - (trimmed.contains(" 4") || trimmed.contains(" 5"))) { - qCWarning(NTRIPLog) << "NTRIP: Server error response:" << trimmed; - emit error(QString("NTRIP HTTP error: %1").arg(trimmed)); - - // Null out _socket before disconnectFromHost() so the - // synchronous 'disconnected' handler bails via !_socket guard. - QTcpSocket* sock = _socket; - _socket = nullptr; - _state = NTRIPState::uninitialised; - - QObject::disconnect(_readyReadConn); - sock->disconnectFromHost(); - sock->close(); - sock->deleteLater(); - - if (!_stopping.load()) { - QTimer::singleShot(3000, this, [this]() { - if (!_stopping.load()) { - _hardwareConnect(); - } - }); - } - return; - } - } - - if (foundOkResponse) { - // Mark the 200 OK time and reset first-RTCM flag using QObject properties. - const qint64 nowMs = QDateTime::currentMSecsSinceEpoch(); - this->setProperty("ntrip_postok_t0_ms", nowMs); - this->setProperty("ntrip_saw_rtcm", false); - this->setProperty("ntrip_watchdog_fired", false); - - // Fire a one-shot watchdog that only logs if no RTCM arrives by ~28s. - QTimer::singleShot(28000, this, [this]() { - if (this->property("ntrip_watchdog_fired").toBool()) return; - const bool saw = this->property("ntrip_saw_rtcm").toBool(); - if (!saw) { - this->setProperty("ntrip_watchdog_fired", true); - qCWarning(NTRIPLog) - << "NTRIP no RTCM received 28s after 200 OK. Likely caster timeout." - << "Check: duplicate login, entitlement/region, GGA validity."; - } - }); - - _state = _useSpartn ? NTRIPState::waiting_for_spartn_data - : NTRIPState::waiting_for_rtcm_header; - - qCDebug(NTRIPLog) << "NTRIP: HTTP handshake complete, transitioning to data state"; - emit connected(); - - // Process any remaining data after the headers - if (hdrEnd >= 0) { - QByteArray remainingData = responseData.mid(hdrEnd + 4); - if (!remainingData.isEmpty()) { - qCDebug(NTRIPLog) << "NTRIP: Processing data after HTTP headers:" << remainingData.size() << "bytes"; - if (_useSpartn) { - _handleSpartnData(remainingData); - } else { - _parse(remainingData); - } - } - } - } else { - qCDebug(NTRIPLog) << "NTRIP: Waiting for complete HTTP response..."; - } - - return; // done with HTTP response handling - } - - // Data after handshake (RTCM path) - QByteArray bytes = _socket->readAll(); - if (!bytes.isEmpty()) { - // Mark first RTCM arrival and log timing/sample - if (!this->property("ntrip_saw_rtcm").toBool()) { - this->setProperty("ntrip_saw_rtcm", true); - const qint64 t0 = this->property("ntrip_postok_t0_ms").toLongLong(); - const qint64 dt = (t0 > 0) ? (QDateTime::currentMSecsSinceEpoch() - t0) : -1; - const QByteArray sample = bytes.left(48); - const unsigned char b0 = static_cast(sample.isEmpty() ? 0 : sample[0]); - qCDebug(NTRIPLog) << "NTRIP first RTCM bytes at" << dt << "ms after 200 OK"; - qCDebug(NTRIPLog) << "NTRIP rx sample (hex, first 48B):" << sample.toHex(' '); - qCDebug(NTRIPLog) << "NTRIP preamble_check first_byte=0x" << QByteArray::number(b0, 16); - } - - s_totalRtcm += static_cast(bytes.size()); - qCDebug(NTRIPLog) << "NTRIP rx bytes:" << bytes.size() - << "total:" << s_totalRtcm - << "bytesAvailable:" << _socket->bytesAvailable(); - - _parse(bytes); - } -} - -void NTRIPTCPLink::debugFetchSourceTable() -{ - // Build request (include Authorization so the caster can tailor the table to your creds) - const QByteArray authB64 = QString(_username + ":" + _password).toUtf8().toBase64(); - const QByteArray req = - QByteArray("GET / HTTP/1.1\r\n") - + "Host: " + _hostAddress.toUtf8() + "\r\n" - + "User-Agent: QGC-NTRIP\r\n" - + "Ntrip-Version: Ntrip/2.0\r\n" - + "Accept: */*\r\n" - + "Authorization: Basic " + authB64 + "\r\n" - + "Connection: close\r\n\r\n"; - - QByteArray all; - - if (_useSpartn || _port == 2102) { - // TLS (e.g., SPARTN/2102) - QSslSocket sock; - sock.connectToHostEncrypted(_hostAddress, static_cast(_port)); - if (!sock.waitForEncrypted(7000)) { - qCWarning(NTRIPLog) << "SOURCETABLE TLS connect failed:" << sock.errorString(); - return; - } - if (sock.write(req) <= 0 || !sock.waitForBytesWritten(3000)) { - qCWarning(NTRIPLog) << "SOURCETABLE TLS write failed:" << sock.errorString(); - return; - } - while (sock.waitForReadyRead(3000)) { - all += sock.readAll(); - if (sock.bytesAvailable() == 0 && sock.state() != QAbstractSocket::ConnectedState) break; - } - sock.close(); - } else { - // Plain TCP (e.g., RTCM/2101) - QTcpSocket sock; - sock.connectToHost(_hostAddress, static_cast(_port)); - if (!sock.waitForConnected(5000)) { - qCWarning(NTRIPLog) << "SOURCETABLE connect failed:" << sock.errorString(); - return; - } - if (sock.write(req) <= 0 || !sock.waitForBytesWritten(3000)) { - qCWarning(NTRIPLog) << "SOURCETABLE write failed:" << sock.errorString(); - return; - } - while (sock.waitForReadyRead(3000)) { - all += sock.readAll(); - if (sock.bytesAvailable() == 0 && sock.state() != QAbstractSocket::ConnectedState) break; - } - sock.close(); - } - - // Split headers/body and dump the body (the sourcetable) - const int hdrEnd = all.indexOf("\r\n\r\n"); - const QByteArray body = (hdrEnd >= 0) ? all.mid(hdrEnd + 4) : all; - - qCDebug(NTRIPLog) << "----- NTRIP SOURCETABLE BEGIN -----"; - for (const QByteArray& line : body.split('\n')) { - const QByteArray l = line.trimmed(); - if (!l.isEmpty()) qCDebug(NTRIPLog) << l; - } - qCDebug(NTRIPLog) << "------ NTRIP SOURCETABLE END ------"; -} - -void NTRIPTCPLink::sendNMEA(const QByteArray& sentence) -{ - if (_stopping.load()) { - return; - } - if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) { - return; - } - - QByteArray line = sentence; - - // Validate or repair checksum in the form $CORE*XX - if (line.size() >= 5 && line.at(0) == '$') { - int star = line.lastIndexOf('*'); - if (star > 1) { - // Calculate checksum over bytes between '$' and '*' - quint8 calc = 0; - for (int i = 1; i < star; ++i) { - calc ^= static_cast(line.at(i)); - } - - // Format calculated checksum as two uppercase hex digits - QByteArray calcCks = QByteArray::number(calc, 16) - .rightJustified(2, '0') - .toUpper(); - - bool needsRepair = false; - if (star + 3 > line.size()) { - // Not enough chars after '*', definitely needs repair - needsRepair = true; - } else { - QByteArray txCks = line.mid(star + 1, 2).toUpper(); - if (txCks != calcCks) { - needsRepair = true; - } - } - - if (needsRepair) { - // Remove any existing checksum and replace with correct one - line = line.left(star + 1) + calcCks; - } - } else { - // No '*' found, append one and correct checksum - quint8 calc = 0; - for (int i = 1; i < line.size(); ++i) { - calc ^= static_cast(line.at(i)); - } - QByteArray calcCks = QByteArray::number(calc, 16) - .rightJustified(2, '0') - .toUpper(); - line.append('*').append(calcCks); - } - } - - // Ensure CRLF termination - if (!line.endsWith("\r\n")) { - line.append("\r\n"); - } - - qCDebug(NTRIPLog) << "NTRIP Sent NMEA:" << QString::fromUtf8(line.trimmed()); - - const qint64 written = _socket->write(line); - _socket->flush(); - - qCDebug(NTRIPLog) << "NTRIP Socket state:" << (_socket ? _socket->state() : -1); - qCDebug(NTRIPLog) << "NTRIP Bytes written:" << written; -} - - -void NTRIPTCPLink::requestStop() -{ - _stopping.store(true); - - if (_socket) { - QObject::disconnect(_readyReadConn); - _socket->disconnectFromHost(); - _socket->close(); - delete _socket; - _socket = nullptr; - } - - emit finished(); // let NTRIPManager's QThread quit in response -} - -NTRIPManager* NTRIPManager::instance() -{ - if (!_instance) { - // Prefer QGCApplication as parent if available, otherwise fall back to qApp - QObject* parent = qgcApp() ? static_cast(qgcApp()) : static_cast(qApp); - _instance = new NTRIPManager(parent); - } - return _instance; -} - -void NTRIPManager::startNTRIP() -{ - if (_startStopBusy) { - return; - } - _startStopBusy = true; // guard begin - - qCDebug(NTRIPLog) << "startNTRIP: begin"; - - if (_tcpLink) { - _startStopBusy = false; // already started; release guard - return; - } - - _ntripStatus = tr("Connecting..."); - emit ntripStatusChanged(); - - - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - QString host; - int port = 2101; - QString user; - QString pass; - QString mount; - QString whitelist; - - if (settings) { - if (settings->ntripServerHostAddress()) host = settings->ntripServerHostAddress()->rawValue().toString(); - if (settings->ntripServerPort()) port = settings->ntripServerPort()->rawValue().toInt(); - if (settings->ntripUsername()) user = settings->ntripUsername()->rawValue().toString(); - if (settings->ntripPassword()) pass = settings->ntripPassword()->rawValue().toString(); - if (settings->ntripMountpoint()) mount = settings->ntripMountpoint()->rawValue().toString(); - if (settings->ntripWhitelist()) whitelist = settings->ntripWhitelist()->rawValue().toString(); - if (settings->ntripUseSpartn()) _useSpartn = settings->ntripUseSpartn()->rawValue().toBool(); - } else { - qCWarning(NTRIPLog) << "NTRIP settings group is null; starting with defaults"; - } - - qCDebug(NTRIPLog) << "startNTRIP: host=" << host - << " port=" << port - << " mount=" << mount - << " userIsEmpty=" << user.isEmpty(); - - _tcpThread = new QThread(this); - NTRIPTCPLink* link = new NTRIPTCPLink(host, port, user, pass, mount, whitelist, _useSpartn); - _tcpLink = link; - - link->moveToThread(_tcpThread); - - connect(_tcpThread, &QThread::started, link, &NTRIPTCPLink::start); - connect(link, &NTRIPTCPLink::finished, _tcpThread, &QThread::quit); - connect(_tcpThread, &QThread::finished, _tcpThread, &QObject::deleteLater); - connect(_tcpThread, &QThread::finished, link, &QObject::deleteLater); - - // Surface errors (including server disconnect reasons) to status/UI - connect(_tcpLink, &NTRIPTCPLink::error, - this, &NTRIPManager::_tcpError, - Qt::QueuedConnection); - - connect(_tcpLink, &NTRIPTCPLink::connected, this, [this]() { - _casterStatus = CasterStatus::Connected; - emit casterStatusChanged(_casterStatus); - - const QString want = _useSpartn ? tr("Connected (SPARTN)") : tr("Connected"); - if (_ntripStatus != want) { - _ntripStatus = want; - emit ntripStatusChanged(); - } - - // >>> ONE-SHOT SOURCETABLE DUMP (enable with env var QGC_NTRIP_DUMP_TABLE=1) - if (qEnvironmentVariableIsSet("QGC_NTRIP_DUMP_TABLE")) { - static bool dumped = false; - if (!dumped && _tcpLink) { - dumped = true; - QMetaObject::invokeMethod(_tcpLink, "debugFetchSourceTable", Qt::QueuedConnection); - } - } - // <<< - - // Start rapid 1 Hz GGA until we get the first valid coord - if (_ggaTimer && !_ggaTimer->isActive()) { - _ggaTimer->setInterval(1000); // 1 Hz while waiting for fix - _sendGGA(); // try immediately - _ggaTimer->start(); - } - - }, Qt::QueuedConnection); - - if (_useSpartn) { - connect(_tcpLink, &NTRIPTCPLink::SPARTNDataUpdate, this, [this](const QByteArray& data){ - static uint32_t spartn_count = 0; - if ((spartn_count++ % 50) == 0) { - qCDebug(NTRIPLog) << "SPARTN bytes flowing:" << data.size(); - } - if (_ntripStatus != tr("Connected (SPARTN)")) { - _ntripStatus = tr("Connected (SPARTN)"); - emit ntripStatusChanged(); - } - _casterStatus = CasterStatus::Connected; - emit casterStatusChanged(_casterStatus); - - _rtcmDataReceived(data); - }, Qt::QueuedConnection); - } else { - connect(_tcpLink, &NTRIPTCPLink::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived, Qt::QueuedConnection); - } - - _tcpThread->start(); - qCDebug(NTRIPLog) << "NTRIP started"; - - _startStopBusy = false; // guard end -} - -void NTRIPManager::stopNTRIP() -{ - if (_startStopBusy) { - return; - } - _startStopBusy = true; // guard begin - - if (_tcpLink) { - // Prevent spurious "NTRIP error: ..." toasts during user-initiated shutdown - disconnect(_tcpLink, &NTRIPTCPLink::error, this, &NTRIPManager::_tcpError); - - // Ask the worker to stop in its own thread - QMetaObject::invokeMethod(_tcpLink, "requestStop", Qt::QueuedConnection); - - // If we still own the thread, stop and wait for it - if (_tcpThread) { - _tcpThread->quit(); // or just rely on 'finished' from requestStop() - _tcpThread->wait(); - _tcpThread = nullptr; - } - - // _tcpLink will be deleted via deleteLater() when the thread finishes - _tcpLink = nullptr; - - _ntripStatus = tr("Disconnected"); - emit ntripStatusChanged(); - qCDebug(NTRIPLog) << "NTRIP stopped"; - } - - if (_ggaTimer && _ggaTimer->isActive()) { - _ggaTimer->stop(); - } - - _startStopBusy = false; // guard end -} - -void NTRIPManager::_tcpError(const QString& errorMsg) -{ - qCWarning(NTRIPLog) << "NTRIP Server Error:" << errorMsg; - _ntripStatus = errorMsg; - emit ntripStatusChanged(); - _startStopBusy = false; // make sure guard clears on error - - // Check for specific "no location provided" disconnect reason - if (errorMsg.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { - _onCasterDisconnected(errorMsg); - } else { - _casterStatus = CasterStatus::OtherError; - emit casterStatusChanged(_casterStatus); - } - - // Show error to user via QGC notification (toast) - if (qgcApp()) { - qgcApp()->showAppMessage(tr("NTRIP error: %1").arg(errorMsg)); - } -} - -void NTRIPManager::_rtcmDataReceived(const QByteArray& data) -{ - - qCDebug(NTRIPLog) << "NTRIP Forwarding RTCM to vehicle:" << data.size() << "bytes"; - - // Lazily resolve the tool if we don't have it yet - if (!_rtcmMavlink && qgcApp()) { - _rtcmMavlink = qgcApp()->findChild(); - } - - if (_rtcmMavlink) { - // Forward raw RTCM bytes to the autopilot via MAVLink GPS_RTCM_DATA - _rtcmMavlink->RTCMDataUpdate(data); - - if (_ntripStatus != tr("Connected")) { - _ntripStatus = tr("Connected"); - emit ntripStatusChanged(); - } - } else { - // Tool not constructed yet — avoid crash, keep status, and don't spam - static uint32_t drop_warn_counter = 0; - if ((drop_warn_counter++ % 50) == 0) { - qCWarning(NTRIPLog) << "RTCMMavlink tool not ready; dropping RTCM bytes:" << data.size(); - } - } - - _startStopBusy = false; // guard clears once bytes flow (or we tried) -} - -static QByteArray _makeGGA(const QGeoCoordinate& coord, double altitude_msl) -{ - // UTC time hhmmss format - const QTime utc = QDateTime::currentDateTimeUtc().time(); - const QString hhmmss = QString("%1%2%3") - .arg(utc.hour(), 2, 10, QChar('0')) - .arg(utc.minute(), 2, 10, QChar('0')) - .arg(utc.second(), 2, 10, QChar('0')); - - // Convert decimal degrees to NMEA DMM with zero-padded minutes: - // lat: ddmm.mmmm, lon: dddmm.mmmm - auto dmm = [](double deg, bool lat) -> QString { - double a = qFabs(deg); - int d = int(a); - double m = (a - d) * 60.0; - - // Round to 4 decimals and handle 59.99995 -> 60.0000 carry into degrees - int m10000 = int(m * 10000.0 + 0.5); - double m_rounded = m10000 / 10000.0; - if (m_rounded >= 60.0) { - m_rounded -= 60.0; - d += 1; - } - - // Ensure minutes have two digits before the decimal (e.g. 08.1234) - QString mm = QString::number(m_rounded, 'f', 4); - if (m_rounded < 10.0) { - mm.prepend("0"); - } - - if (lat) { - return QString("%1%2").arg(d, 2, 10, QChar('0')).arg(mm); - } else { - return QString("%1%2").arg(d, 3, 10, QChar('0')).arg(mm); - } - }; - - const bool latNorth = coord.latitude() >= 0.0; - const bool lonEast = coord.longitude() >= 0.0; - - const QString latField = dmm(coord.latitude(), true); - const QString lonField = dmm(coord.longitude(), false); - - // IMPORTANT: include geoid separation as 0.0 and unit 'M' - // Fields: time,lat,N,lon,E,fix,nsat,hdop,alt,M,geoid,M,age,station - const QString core = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,") - .arg(hhmmss) - .arg(latField) - .arg(latNorth ? "N" : "S") - .arg(lonField) - .arg(lonEast ? "E" : "W") - .arg(QString::number(altitude_msl, 'f', 1)); - - // NMEA checksum over bytes between '$' and '*' - quint8 cksum = 0; - const QByteArray coreBytes = core.toUtf8(); - for (char ch : coreBytes) { - cksum ^= static_cast(ch); - } - - const QString cks = QString("%1").arg(cksum, 2, 16, QChar('0')).toUpper(); - const QByteArray sentence = QByteArray("$") + coreBytes + QByteArray("*") + cks.toUtf8(); - return sentence; -} - -void NTRIPManager::_sendGGA() -{ - if (!_tcpLink) { - return; - } - - QGeoCoordinate coord; - double alt_msl = 0.0; - bool validCoord = false; - QString srcUsed; - - if (qgcApp()) { - // PRIORITY 1: Raw GPS data from vehicle GPS facts (most important for RTK) - MultiVehicleManager* mvm = MultiVehicleManager::instance(); - if (mvm) { - if (Vehicle* veh = mvm->activeVehicle()) { - FactGroup* gps = veh->gpsFactGroup(); - if (gps) { - // Try common GPS fact names - Fact* latF = gps->getFact(QStringLiteral("lat")); - Fact* lonF = gps->getFact(QStringLiteral("lon")); - - // If "lat"/"lon" don't work, try alternative names - if (!latF) latF = gps->getFact(QStringLiteral("latitude")); - if (!lonF) lonF = gps->getFact(QStringLiteral("longitude")); - if (!latF) latF = gps->getFact(QStringLiteral("lat_deg")); - if (!lonF) lonF = gps->getFact(QStringLiteral("lon_deg")); - if (!latF) latF = gps->getFact(QStringLiteral("latitude_deg")); - if (!lonF) lonF = gps->getFact(QStringLiteral("longitude_deg")); - - if (latF && lonF) { - const double glat = latF->rawValue().toDouble(); - const double glon = lonF->rawValue().toDouble(); - - // GPS coordinates might be in 1e-7 degrees format (int32 scaled) - double lat_deg = glat; - double lon_deg = glon; - - // Check if values look like scaled integers (> 1000 suggests 1e-7 format) - if (qAbs(glat) > 1000.0 || qAbs(glon) > 1000.0) { - lat_deg = glat * 1e-7; - lon_deg = glon * 1e-7; - } - - if (qIsFinite(lat_deg) && qIsFinite(lon_deg) && - !(lat_deg == 0.0 && lon_deg == 0.0) && - qAbs(lat_deg) <= 90.0 && qAbs(lon_deg) <= 180.0) { - - coord = QGeoCoordinate(lat_deg, lon_deg); - validCoord = true; - - // Get altitude from GPS if available - Fact* altF = gps->getFact(QStringLiteral("alt")); - if (!altF) altF = gps->getFact(QStringLiteral("altitude")); - if (altF) { - double raw_alt = altF->rawValue().toDouble(); - // Altitude might be in mm or cm, convert to meters - if (qAbs(raw_alt) > 10000.0) { // > 10km suggests mm format - alt_msl = raw_alt * 1e-3; // mm to meters - } else if (qAbs(raw_alt) > 1000.0) { // > 1km suggests cm format - alt_msl = raw_alt * 1e-2; // cm to meters - } else { - alt_msl = raw_alt; // already in meters - } - if (!qIsFinite(alt_msl)) alt_msl = 0.0; - } - - srcUsed = QStringLiteral("GPS Raw"); - qCDebug(NTRIPLog) << "NTRIP: Using raw GPS data for GGA" - << "lat:" << lat_deg << "lon:" << lon_deg << "alt:" << alt_msl; - } - } - } else { - // Vehicle exists but no GPS FactGroup yet - qCDebug(NTRIPLog) << "NTRIP: Vehicle found but no GPS FactGroup available yet"; - } - } else { - // MultiVehicleManager exists but no active vehicle yet - static int no_vehicle_count = 0; - if ((no_vehicle_count++ % 10) == 0) { // Log every 10th time to avoid spam - qCDebug(NTRIPLog) << "NTRIP: MultiVehicleManager found but no active vehicle yet"; - } - } - } else { - // MultiVehicleManager not found yet - this is normal during early startup - static int no_mvm_count = 0; - if ((no_mvm_count++ % 10) == 0) { // Log every 10th time to avoid spam - qCDebug(NTRIPLog) << "NTRIP: MultiVehicleManager not available yet (startup)"; - } - } - - // PRIORITY 2: Vehicle global position estimate (EKF output) - if (!validCoord) { - MultiVehicleManager* mvm2 = MultiVehicleManager::instance(); - if (mvm2) { - if (Vehicle* veh2 = mvm2->activeVehicle()) { - coord = veh2->coordinate(); - if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { - validCoord = true; - double a = coord.altitude(); - alt_msl = qIsFinite(a) ? a : 0.0; - srcUsed = QStringLiteral("Vehicle EKF"); - qCDebug(NTRIPLog) << "NTRIP: Using vehicle EKF position for GGA" << coord; - } - } - } - } - - } - - if (!validCoord) { - qCDebug(NTRIPLog) << "NTRIP: No valid position available, skipping GGA."; - return; - } - - // Once we have a real valid coord, slow down GGA to 5 seconds - if (_ggaTimer && _ggaTimer->interval() != 5000) { - _ggaTimer->setInterval(5000); - qCDebug(NTRIPLog) << "NTRIP: Real position acquired, reducing GGA frequency to 5s"; - } - - const QByteArray gga = _makeGGA(coord, alt_msl); - - // Debug: Log the GGA sentence being sent (but reduce spam) - static int gga_send_count = 0; - if ((gga_send_count++ % 5) == 0) { // Log every 5th GGA to reduce spam - qCDebug(NTRIPLog) << "NTRIP: Sending GGA:" << gga; - qCDebug(NTRIPLog) << "NTRIP: GGA coord:" << coord << "alt:" << alt_msl << "source:" << srcUsed; - } - - QMetaObject::invokeMethod(_tcpLink, "sendNMEA", - Qt::QueuedConnection, - Q_ARG(QByteArray, gga)); - - // Update GGA source for UI display - if (!srcUsed.isEmpty() && srcUsed != _ggaSource) { - _ggaSource = srcUsed; - emit ggaSourceChanged(); - } -} - -void NTRIPManager::_checkSettings() -{ - if (_startStopBusy) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: busy, skipping"; - return; - } - - NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); - Fact* enableFact = nullptr; - if (settings && settings->ntripServerConnectEnabled()) { - enableFact = settings->ntripServerConnectEnabled(); - } - - // During startup, keep forcing OFF until we observe the Fact stable at false - // for 2 consecutive ticks. This defeats late restores from QML/settings. - if (_startupSuppress) { - if (enableFact) { - if (enableFact->rawValue().toBool()) { - qCDebug(NTRIPLog) << "NTRIP Startup: coercing OFF (late restore detected)"; - enableFact->setRawValue(false); - _startupStableTicks = 0; // reset stability counter on any true - } else { - _startupStableTicks++; - } - } else { - _startupStableTicks = 0; // no fact yet, not stable - } - - // Release once we have seen false for 2 ticks - if (_startupStableTicks >= 2) { - _startupSuppress = false; - if (!_forcedOffOnce) { - _forcedOffOnce = true; - if (_ntripEnableConn) { - disconnect(_ntripEnableConn); - } - } - qCDebug(NTRIPLog) << "NTRIP Startup: suppression released; honoring user setting from now on"; - } else { - return; // keep suppressing this tick - } - } - - bool shouldBeRunning = false; - if (enableFact) { - shouldBeRunning = enableFact->rawValue().toBool(); - } else { - qCWarning(NTRIPLog) << "NTRIP settings missing; defaulting to disabled"; - } - - bool isRunning = (_tcpLink != nullptr); - - qCDebug(NTRIPLog) << "NTRIP _checkSettings:" - << " isRunning=" << isRunning - << " shouldBeRunning=" << shouldBeRunning; - - if (shouldBeRunning && !isRunning) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: calling startNTRIP()"; - startNTRIP(); - } else if (!shouldBeRunning && isRunning) { - qCDebug(NTRIPLog) << "NTRIP _checkSettings: calling stopNTRIP()"; - stopNTRIP(); - } -} - -void NTRIPManager::_onCasterDisconnected(const QString& reason) -{ - qWarning() << "NTRIP: Disconnected by server:" << reason; - if (reason.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { - _casterStatus = CasterStatus::NoLocationError; - } else { - _casterStatus = CasterStatus::OtherError; - } - emit casterStatusChanged(_casterStatus); -} diff --git a/src/GPS/NTRIP.h b/src/GPS/NTRIP.h deleted file mode 100644 index 5c580af0b3d8..000000000000 --- a/src/GPS/NTRIP.h +++ /dev/null @@ -1,187 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -Q_DECLARE_LOGGING_CATEGORY(NTRIPLog) - -class RTCMMavlink; -class NTRIPSettings; -class Vehicle; -class MultiVehicleManager; -class GpsTest; - -#define RTCM3_PREAMBLE 0xD3 - -class RTCMParser -{ -public: - RTCMParser(); - void reset(); - bool addByte(uint8_t byte); - uint8_t* message() { return _buffer; } - uint16_t messageLength() { return _messageLength; } - uint16_t messageId(); - const uint8_t* crcBytes() const { return _crcBytes; } - int crcSize() const { return 3; } - -private: - enum State { - WaitingForPreamble, - ReadingLength, - ReadingMessage, - ReadingCRC - }; - - State _state; - uint8_t _buffer[1024]; - uint16_t _messageLength; - uint16_t _bytesRead; - uint16_t _lengthBytesRead; - uint8_t _lengthBytes[2]; - uint16_t _crcBytesRead; - uint8_t _crcBytes[3]; -}; - -class NTRIPTCPLink : public QObject -{ - Q_OBJECT - -public: -#ifdef QGC_UNITTEST_BUILD - friend class GpsTest; -#endif - - NTRIPTCPLink(const QString& hostAddress, - int port, - const QString& username, - const QString& password, - const QString& mountpoint, - const QString& whitelist, - bool useSpartn, - bool autoStart = true, - QObject* parent = nullptr); - - Q_INVOKABLE void debugFetchSourceTable(); - - ~NTRIPTCPLink(); - -public slots: - void start(); - void requestStop(); - void sendNMEA(const QByteArray& sentence); - -signals: - void finished(); - void error(const QString& errorMsg); - void RTCMDataUpdate(const QByteArray& message); - // Called when SPARTN corrections are received from the NTRIPTCPLink - // These corrections are forwarded to the connected vehicle (PX4/ArduPilot) - // using the same path as RTCM corrections via _rtcmDataReceived(). - void SPARTNDataUpdate(const QByteArray& message); - void connected(); - - -private slots: - void _readBytes(); - -private: - enum class NTRIPState { - uninitialised, - waiting_for_http_response, - waiting_for_spartn_data, - waiting_for_rtcm_header, - accumulating_rtcm_packet, - }; - - void _hardwareConnect(); - void _parse(const QByteArray& buffer); - void _handleSpartnData(const QByteArray& data); - - QTcpSocket* _socket = nullptr; - - QString _hostAddress; - int _port; - QString _username; - QString _password; - QString _mountpoint; - bool _useSpartn = false; - QVector _whitelist; - - RTCMParser* _rtcmParser = nullptr; - NTRIPState _state; - - std::atomic _stopping{false}; - QMetaObject::Connection _readyReadConn; - - // Small buffer to strip a response header only once if needed - QByteArray _spartnBuf; - bool _spartnNeedHeaderStrip = true; - -}; - -class NTRIPManager : public QObject -{ - Q_OBJECT - Q_PROPERTY(QString ntripStatus READ ntripStatus NOTIFY ntripStatusChanged) - Q_PROPERTY(CasterStatus casterStatus READ casterStatus NOTIFY casterStatusChanged) - Q_PROPERTY(QString ggaSource READ ggaSource NOTIFY ggaSourceChanged) - -public: - enum class CasterStatus { Connected, NoLocationError, OtherError }; - - NTRIPManager(QObject* parent = nullptr); - ~NTRIPManager(); - - static NTRIPManager* instance(); - - QString ntripStatus() const { return _ntripStatus; } - CasterStatus casterStatus() const { return _casterStatus; } - QString ggaSource() const { return _ggaSource; } - - void startNTRIP(); - void stopNTRIP(); - -signals: - void ntripStatusChanged(); - void casterStatusChanged(CasterStatus status); - void ggaSourceChanged(); - -public slots: - void _tcpError(const QString& errorMsg); - void _rtcmDataReceived(const QByteArray& data); - -private: - void _checkSettings(); - void _sendGGA(); - void _onCasterDisconnected(const QString& reason); - - QTimer* _ggaTimer = nullptr; - - QString _ntripStatus; - QString _ggaSource; - QMetaObject::Connection _ntripEnableConn; - - NTRIPTCPLink* _tcpLink = nullptr; - QThread* _tcpThread = nullptr; - RTCMMavlink* _rtcmMavlink = nullptr; - QTimer* _settingsCheckTimer = nullptr; - bool _startStopBusy = false; - bool _forcedOffOnce = false; - bool _useSpartn = false; - - QElapsedTimer _startupTimer; - bool _startupSuppress = true; - int _startupStableTicks = 0; - - CasterStatus _casterStatus = CasterStatus::OtherError; - - static NTRIPManager* _instance; -}; diff --git a/src/GPS/NTRIP/CMakeLists.txt b/src/GPS/NTRIP/CMakeLists.txt new file mode 100644 index 000000000000..e8a891d846f5 --- /dev/null +++ b/src/GPS/NTRIP/CMakeLists.txt @@ -0,0 +1,18 @@ +# ============================================================================ +# NTRIP Module +# NTRIP client, RTCM parsing, and correction streaming +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + NTRIPHttpTransport.cc + NTRIPHttpTransport.h + NTRIPManager.cc + NTRIPManager.h + NTRIPSourceTable.cc + NTRIPSourceTable.h + RTCMParser.cc + RTCMParser.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.cc b/src/GPS/NTRIP/NTRIPHttpTransport.cc new file mode 100644 index 000000000000..5a202dba1542 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPHttpTransport.cc @@ -0,0 +1,416 @@ +#include "NTRIPHttpTransport.h" +#include "QGCLoggingCategory.h" + +#include +#include +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPHttpTransportLog, "GPS.NTRIPHttpTransport") + +NTRIPHttpTransport::NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent) + : QObject(parent) + , _hostAddress(config.host) + , _port(config.port) + , _username(config.username) + , _password(config.password) + , _mountpoint(config.mountpoint) + , _useTls(config.useTls) +{ + for (const auto& msg : config.whitelist.split(',')) { + int msg_int = msg.toInt(); + if (msg_int) + _whitelist.append(msg_int); + } + qCDebug(NTRIPHttpTransportLog) << "RTCM message filter:" << _whitelist; + if (_whitelist.empty()) { + qCDebug(NTRIPHttpTransportLog) << "Message filter empty; all RTCM message IDs will be forwarded."; + } + + _connectTimeoutTimer = new QTimer(this); + _connectTimeoutTimer->setSingleShot(true); + connect(_connectTimeoutTimer, &QTimer::timeout, this, [this]() { + qCWarning(NTRIPHttpTransportLog) << "Connection timeout"; + emit error(QStringLiteral("Connection timeout")); + }); + + _dataWatchdogTimer = new QTimer(this); + _dataWatchdogTimer->setSingleShot(true); + _dataWatchdogTimer->setInterval(kDataWatchdogMs); + connect(_dataWatchdogTimer, &QTimer::timeout, this, [this]() { + qCWarning(NTRIPHttpTransportLog) << "No data received for" << kDataWatchdogMs / 1000 << "seconds"; + emit error(tr("No data received for %1 seconds").arg(kDataWatchdogMs / 1000)); + }); +} + +NTRIPHttpTransport::~NTRIPHttpTransport() +{ + stop(); +} + +void NTRIPHttpTransport::start() +{ + _stopped = false; + _connect(); +} + +void NTRIPHttpTransport::stop() +{ + _stopped = true; + _connectTimeoutTimer->stop(); + _dataWatchdogTimer->stop(); + + if (_socket) { + _socket->disconnect(this); + _socket->disconnectFromHost(); + _socket->close(); + _socket->deleteLater(); + _socket = nullptr; + } + + emit finished(); +} + +void NTRIPHttpTransport::_sendHttpRequest() +{ + if (!_socket || _stopped) { + return; + } + + if (!_mountpoint.isEmpty()) { + static const QRegularExpression controlChars(QStringLiteral("[\\r\\n\\x00-\\x1f]")); + if (_mountpoint.contains(controlChars)) { + qCWarning(NTRIPHttpTransportLog) << "Mountpoint contains control characters, rejecting"; + emit error(tr("Invalid mountpoint name (contains control characters)")); + return; + } + + qCDebug(NTRIPHttpTransportLog) << "Sending HTTP request"; + QByteArray req; + req += "GET /" + _mountpoint.toUtf8() + " HTTP/1.0\r\n"; + req += "Host: " + _hostAddress.toUtf8() + "\r\n"; + req += "Ntrip-Version: Ntrip/2.0\r\n"; + req += "User-Agent: NTRIP QGroundControl/1.0\r\n"; + + if (!_username.isEmpty() || !_password.isEmpty()) { + const QByteArray authB64 = (_username + ":" + _password).toUtf8().toBase64(); + req += "Authorization: Basic " + authB64 + "\r\n"; + } + + req += "\r\n"; + _socket->write(req); + _socket->flush(); + + qCDebug(NTRIPHttpTransportLog) << "HTTP request sent for mount:" << _mountpoint; + } else { + _httpHandshakeDone = true; + emit connected(); + _dataWatchdogTimer->start(); + } + + qCDebug(NTRIPHttpTransportLog) << "Socket connected" + << "local" << _socket->localAddress().toString() << ":" << _socket->localPort() + << "-> peer" << _socket->peerAddress().toString() << ":" << _socket->peerPort(); +} + +void NTRIPHttpTransport::_connect() +{ + if (_stopped) { + return; + } + + if (_socket) { + qCWarning(NTRIPHttpTransportLog) << "Socket already exists, aborting connect"; + return; + } + + qCDebug(NTRIPHttpTransportLog) << "connectToHost" << _hostAddress << ":" << _port << " mount=" << _mountpoint; + + _httpHandshakeDone = false; + _httpResponseBuf.clear(); + _rtcmParser.reset(); + + if (_useTls) { + QSslSocket* sslSocket = new QSslSocket(this); + _socket = sslSocket; + connect(sslSocket, QOverload&>::of(&QSslSocket::sslErrors), + this, [](const QList& errors) { + for (const QSslError& e : errors) { + qCWarning(NTRIPHttpTransportLog) << "TLS error:" << e.errorString(); + } + }); + } else { + _socket = new QTcpSocket(this); + } + _socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); + _socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); + _socket->setReadBufferSize(0); + + connect(_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError code) { + if (_stopped || !_socket) { + return; + } + _connectTimeoutTimer->stop(); + + QString msg = _socket->errorString(); + if (code == QAbstractSocket::RemoteHostClosedError && !_httpHandshakeDone) { + if (!_mountpoint.isEmpty()) { + msg += " (peer closed before HTTP response; check mountpoint and credentials)"; + } + } + + qCWarning(NTRIPHttpTransportLog) << "Socket error code:" << int(code) << " msg:" << msg; + emit error(msg); + }); + + connect(_socket, &QTcpSocket::disconnected, this, [this]() { + if (_stopped || !_socket) { + return; + } + _connectTimeoutTimer->stop(); + + const QByteArray trailing = _socket->readAll(); + QString reason; + if (!trailing.isEmpty()) { + reason = QString::fromUtf8(trailing).trimmed(); + } else { + reason = QStringLiteral("Server disconnected"); + } + + qCWarning(NTRIPHttpTransportLog) << "Disconnected:" + << "reason=" << reason + << "ms_since_200=" << (_postOkTimestampMs > 0 ? QDateTime::currentMSecsSinceEpoch() - _postOkTimestampMs : -1); + emit error(reason); + }); + + connect(_socket, &QTcpSocket::readyRead, this, &NTRIPHttpTransport::_readBytes); + + if (_useTls) { + QSslSocket* sslSocket = qobject_cast(_socket); + connect(sslSocket, &QSslSocket::encrypted, this, [this]() { + _connectTimeoutTimer->stop(); + _sendHttpRequest(); + }); + sslSocket->connectToHostEncrypted(_hostAddress, static_cast(_port)); + } else { + connect(_socket, &QTcpSocket::connected, this, [this]() { + _connectTimeoutTimer->stop(); + _sendHttpRequest(); + }); + _socket->connectToHost(_hostAddress, static_cast(_port)); + } + _connectTimeoutTimer->start(kConnectTimeoutMs); +} + +void NTRIPHttpTransport::_parseRtcm(const QByteArray& buffer) +{ + if (_stopped) { + return; + } + + for (char ch : buffer) { + const uint8_t byte = static_cast(static_cast(ch)); + + if (!_rtcmParser.addByte(byte)) { + continue; + } + + if (!_rtcmParser.validateCrc()) { + qCWarning(NTRIPHttpTransportLog) << "RTCM CRC mismatch, dropping message id" << _rtcmParser.messageId(); + _rtcmParser.reset(); + continue; + } + + constexpr int kRtcmHeaderSize = 3; + const int payload_len = static_cast(_rtcmParser.messageLength()); + QByteArray message(reinterpret_cast(_rtcmParser.message()), + kRtcmHeaderSize + payload_len); + + const uint8_t* crc_ptr = _rtcmParser.crcBytes(); + message.append(reinterpret_cast(crc_ptr), RTCMParser::kCrcSize); + + const uint16_t id = _rtcmParser.messageId(); + + if (_whitelist.empty() || _whitelist.contains(id)) { + qCDebug(NTRIPHttpTransportLog) << "RTCM packet id" << id << "len" << message.length(); + emit RTCMDataUpdate(message); + } else { + qCDebug(NTRIPHttpTransportLog) << "Ignoring RTCM" << id; + } + + _rtcmParser.reset(); + } +} + +void NTRIPHttpTransport::_readBytes() +{ + if (_stopped || !_socket) { + return; + } + + if (!_httpHandshakeDone) { + _httpResponseBuf.append(_socket->readAll()); + if (_httpResponseBuf.isEmpty()) { + return; + } + + const int hdrEnd = _httpResponseBuf.indexOf("\r\n\r\n"); + if (hdrEnd < 0) { + if (_httpResponseBuf.size() > kMaxHttpHeaderSize) { + qCWarning(NTRIPHttpTransportLog) << "HTTP response header too large, dropping"; + _httpResponseBuf.clear(); + emit error(tr("HTTP response header too large")); + } + return; + } + + const QString header = QString::fromUtf8(_httpResponseBuf.left(hdrEnd)); + qCDebug(NTRIPHttpTransportLog) << "HTTP response received:" << header.left(200); + + const QStringList lines = header.split('\n'); + for (const QString& line : lines) { + const HttpStatus status = parseHttpStatusLine(line); + if (!status.valid) { + continue; + } + + if (isHttpSuccess(status.code)) { + qCDebug(NTRIPHttpTransportLog) << "HTTP" << status.code << status.reason; + _postOkTimestampMs = QDateTime::currentMSecsSinceEpoch(); + _httpHandshakeDone = true; + + qCDebug(NTRIPHttpTransportLog) << "HTTP handshake complete"; + emit connected(); + + _dataWatchdogTimer->start(); + + const QByteArray remainingData = _httpResponseBuf.mid(hdrEnd + 4); + _httpResponseBuf.clear(); + + if (!remainingData.isEmpty()) { + qCDebug(NTRIPHttpTransportLog) << "Processing trailing data:" << remainingData.size() << "bytes"; + _parseRtcm(remainingData); + } + return; + } + + const QString body = QString::fromUtf8(_httpResponseBuf.mid(hdrEnd + 4)).trimmed(); + _httpResponseBuf.clear(); + + if (status.code == 401) { + qCWarning(NTRIPHttpTransportLog) << "Authentication failed:" << status.reason; + emit error(tr("Authentication failed (401): check username and password")); + return; + } + + qCWarning(NTRIPHttpTransportLog) << "HTTP error" << status.code << status.reason + << "body:" << body.left(200); + QString msg = status.reason.isEmpty() + ? tr("HTTP %1").arg(status.code) + : tr("HTTP %1: %2").arg(status.code).arg(status.reason); + if (!body.isEmpty()) { + QString cleanBody = body.left(500); + static const QRegularExpression htmlTags(QStringLiteral("<[^>]*>")); + cleanBody.remove(htmlTags); + cleanBody = cleanBody.simplified().left(200); + if (!cleanBody.isEmpty()) { + msg += QStringLiteral(" — ") + cleanBody; + } + } + emit error(msg); + return; + } + + qCWarning(NTRIPHttpTransportLog) << "No HTTP status line found in response. First line:" + << (lines.isEmpty() ? QStringLiteral("(empty)") : lines.first().left(120)); + _httpResponseBuf.clear(); + emit error(tr("Invalid HTTP response from caster")); + return; + } + + QByteArray bytes = _socket->readAll(); + if (!bytes.isEmpty()) { + _dataWatchdogTimer->start(); + qCDebug(NTRIPHttpTransportLog) << "rx bytes:" << bytes.size(); + _parseRtcm(bytes); + } +} + +void NTRIPHttpTransport::sendNMEA(const QByteArray& nmea) +{ + if (_stopped) { + return; + } + if (!_socket || _socket->state() != QAbstractSocket::ConnectedState) { + return; + } + + const QByteArray line = repairNmeaChecksum(nmea); + qCDebug(NTRIPHttpTransportLog) << "Sent NMEA:" << QString::fromUtf8(line.trimmed()); + _socket->write(line); + _socket->flush(); +} + +NTRIPHttpTransport::HttpStatus NTRIPHttpTransport::parseHttpStatusLine(const QString& line) +{ + static const QRegularExpression re(QStringLiteral("^\\S+\\s+(\\d{3})(?:\\s+(.*))?$")); + const QRegularExpressionMatch match = re.match(line.trimmed()); + + if (!match.hasMatch()) { + return HttpStatus{0, {}, false}; + } + + return HttpStatus{ + match.captured(1).toInt(), + match.captured(2).trimmed(), + true + }; +} + +QByteArray NTRIPHttpTransport::repairNmeaChecksum(const QByteArray& sentence) +{ + QByteArray line = sentence; + + if (line.size() >= 5 && line.at(0) == '$') { + int star = line.lastIndexOf('*'); + if (star > 1) { + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(line.at(i)); + } + + QByteArray calcCks = QByteArray::number(calc, 16) + .rightJustified(2, '0') + .toUpper(); + + bool needsRepair = false; + if (star + 3 > line.size()) { + needsRepair = true; + } else { + QByteArray txCks = line.mid(star + 1, 2).toUpper(); + if (txCks != calcCks) { + needsRepair = true; + } + } + + if (needsRepair) { + line = line.left(star + 1) + calcCks; + } + } else { + quint8 calc = 0; + for (int i = 1; i < line.size(); ++i) { + calc ^= static_cast(line.at(i)); + } + QByteArray calcCks = QByteArray::number(calc, 16) + .rightJustified(2, '0') + .toUpper(); + line.append('*').append(calcCks); + } + } + + if (!line.endsWith("\r\n")) { + line.append("\r\n"); + } + + return line; +} diff --git a/src/GPS/NTRIP/NTRIPHttpTransport.h b/src/GPS/NTRIP/NTRIPHttpTransport.h new file mode 100644 index 000000000000..522d387c9467 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPHttpTransport.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "RTCMParser.h" + +Q_DECLARE_LOGGING_CATEGORY(NTRIPHttpTransportLog) + +struct NTRIPTransportConfig { + QString host; + int port = 2101; + QString username; + QString password; + QString mountpoint; + QString whitelist; + bool useTls = false; + + bool operator==(const NTRIPTransportConfig& other) const { + return host == other.host && port == other.port && + username == other.username && password == other.password && + mountpoint == other.mountpoint && whitelist == other.whitelist && + useTls == other.useTls; + } + bool operator!=(const NTRIPTransportConfig& other) const { return !(*this == other); } +}; + +class NTRIPHttpTransport : public QObject +{ + Q_OBJECT + friend class NTRIPHttpTransportTest; + +public: + static constexpr int kConnectTimeoutMs = 10000; + static constexpr int kDataWatchdogMs = 30000; + static constexpr int kMaxHttpHeaderSize = 32768; + + explicit NTRIPHttpTransport(const NTRIPTransportConfig& config, QObject* parent = nullptr); + ~NTRIPHttpTransport() override; + + void start(); + void stop(); + void sendNMEA(const QByteArray& nmea); + +signals: + void connected(); + void error(const QString& errorMsg); + void RTCMDataUpdate(const QByteArray& message); + void finished(); + +protected: + struct HttpStatus { + int code = 0; + QString reason; + bool valid = false; + }; + + static HttpStatus parseHttpStatusLine(const QString& line); + static QByteArray repairNmeaChecksum(const QByteArray& sentence); + static bool isHttpSuccess(int code) { return code >= 200 && code < 300; } + +private: + void _connect(); + void _sendHttpRequest(); + void _readBytes(); + void _parseRtcm(const QByteArray& buffer); + + QTcpSocket* _socket = nullptr; + QTimer* _connectTimeoutTimer = nullptr; + QTimer* _dataWatchdogTimer = nullptr; + + QString _hostAddress; + int _port; + QString _username; + QString _password; + QString _mountpoint; + QVector _whitelist; + + RTCMParser _rtcmParser; + bool _httpHandshakeDone = false; + bool _useTls = false; + bool _stopped = false; + + qint64 _postOkTimestampMs = 0; + + QByteArray _httpResponseBuf; +}; diff --git a/src/GPS/NTRIP/NTRIPManager.cc b/src/GPS/NTRIP/NTRIPManager.cc new file mode 100644 index 000000000000..081d22cb1236 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPManager.cc @@ -0,0 +1,630 @@ +#include "NTRIPManager.h" +#include "NTRIPHttpTransport.h" +#include "NTRIPSourceTable.h" +#include "NTRIPSettings.h" +#include "QmlObjectListModel.h" +#include "Fact.h" +#include "FactGroup.h" +#include "QGCApplication.h" +#include "QGCLoggingCategory.h" +#include "RTCMMavlink.h" +#include "SettingsManager.h" + +#include "MultiVehicleManager.h" +#include "PositionManager.h" +#include "Vehicle.h" +#include +#include + +#include +#include +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPManagerLog, "GPS.NTRIPManager") + +Q_APPLICATION_STATIC(NTRIPManager, _ntripManagerInstance); + +NTRIPManager* NTRIPManager::instance() +{ + return _ntripManagerInstance(); +} + +NTRIPManager::NTRIPManager(QObject* parent) + : QObject(parent) +{ + qCDebug(NTRIPManagerLog) << "NTRIPManager created"; + + _rtcmMavlink = qgcApp() ? qgcApp()->findChild() : nullptr; + if (!_rtcmMavlink) { + QObject* parentObj = qgcApp() ? static_cast(qgcApp()) : static_cast(this); + _rtcmMavlink = new RTCMMavlink(parentObj); + _rtcmMavlink->setObjectName(QStringLiteral("RTCMMavlink")); + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings) { + auto connectSetting = [this](Fact* fact) { + if (fact) { + connect(fact, &Fact::rawValueChanged, this, &NTRIPManager::_onSettingChanged); + } + }; + connectSetting(settings->ntripServerConnectEnabled()); + connectSetting(settings->ntripServerHostAddress()); + connectSetting(settings->ntripServerPort()); + connectSetting(settings->ntripUsername()); + connectSetting(settings->ntripPassword()); + connectSetting(settings->ntripMountpoint()); + connectSetting(settings->ntripWhitelist()); + connectSetting(settings->ntripUseTls()); + connectSetting(settings->ntripUdpForwardEnabled()); + connectSetting(settings->ntripUdpTargetAddress()); + connectSetting(settings->ntripUdpTargetPort()); + } + + // Check initial state (handles connect-on-start) + QTimer::singleShot(0, this, &NTRIPManager::_onSettingChanged); + + _ggaTimer = new QTimer(this); + _ggaTimer->setInterval(5000); + connect(_ggaTimer, &QTimer::timeout, this, &NTRIPManager::_sendGGA); + + _dataRateTimer = new QTimer(this); + _dataRateTimer->setInterval(1000); + connect(_dataRateTimer, &QTimer::timeout, this, [this]() { + const double rate = static_cast(_bytesReceived - _dataRatePrevBytes); + _dataRatePrevBytes = _bytesReceived; + if (!qFuzzyCompare(_dataRateBytesPerSec + 1.0, rate + 1.0)) { + _dataRateBytesPerSec = rate; + emit dataRateChanged(); + } + emit bytesReceivedChanged(); + emit messagesReceivedChanged(); + }); + + connect(qApp, &QCoreApplication::aboutToQuit, this, &NTRIPManager::stopNTRIP, Qt::QueuedConnection); +} + +NTRIPManager::~NTRIPManager() +{ + qCDebug(NTRIPManagerLog) << "NTRIPManager destroyed"; + stopNTRIP(); +} + +void NTRIPManager::_setStatus(ConnectionStatus status, const QString& msg) +{ + bool changed = false; + + if (_connectionStatus != status) { + _connectionStatus = status; + changed = true; + emit connectionStatusChanged(); + } + + if (_statusMessage != msg) { + _statusMessage = msg; + changed = true; + emit statusMessageChanged(); + } + + if (changed) { + qCDebug(NTRIPManagerLog) << "NTRIP status:" << static_cast(status) << msg; + } +} + +NTRIPTransportConfig NTRIPManager::_configFromSettings() const +{ + NTRIPTransportConfig config; + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (!settings) { + return config; + } + + if (settings->ntripServerHostAddress()) config.host = settings->ntripServerHostAddress()->rawValue().toString(); + if (settings->ntripServerPort()) config.port = settings->ntripServerPort()->rawValue().toInt(); + if (settings->ntripUsername()) config.username = settings->ntripUsername()->rawValue().toString(); + if (settings->ntripPassword()) config.password = settings->ntripPassword()->rawValue().toString(); + if (settings->ntripMountpoint()) config.mountpoint = settings->ntripMountpoint()->rawValue().toString(); + if (settings->ntripWhitelist()) config.whitelist = settings->ntripWhitelist()->rawValue().toString(); + if (settings->ntripUseTls()) config.useTls = settings->ntripUseTls()->rawValue().toBool(); + + return config; +} + +void NTRIPManager::startNTRIP() +{ + if (_startStopBusy || _transport) { + if (_startStopBusy) { + qCWarning(NTRIPManagerLog) << "startNTRIP called while start/stop in progress"; + } + return; + } + _startStopBusy = true; + + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + + NTRIPTransportConfig config = _configFromSettings(); + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings && settings->ntripUdpForwardEnabled()) { + _udpForwardEnabled = settings->ntripUdpForwardEnabled()->rawValue().toBool(); + } + + if (_udpForwardEnabled && settings) { + const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString(); + const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt()); + const QHostAddress parsedAddr(udpAddr); + if (!udpAddr.isEmpty() && !parsedAddr.isNull() && udpPort > 0) { + _udpTargetAddress = parsedAddr; + _udpTargetPort = udpPort; + if (!_udpSocket) { + _udpSocket = new QUdpSocket(this); + } + qCDebug(NTRIPManagerLog) << "NTRIP UDP forwarding enabled:" << udpAddr << ":" << udpPort; + } else { + qCWarning(NTRIPManagerLog) << "NTRIP UDP forwarding enabled but address/port invalid"; + _udpForwardEnabled = false; + } + } + + if (config.host.isEmpty()) { + qCWarning(NTRIPManagerLog) << "NTRIP host address is empty"; + _setStatus(ConnectionStatus::Error, tr("No host address")); + _startStopBusy = false; + return; + } + + if (config.port <= 0 || config.port > 65535) { + qCWarning(NTRIPManagerLog) << "NTRIP port is invalid:" << config.port; + _setStatus(ConnectionStatus::Error, tr("Invalid port")); + _startStopBusy = false; + return; + } + + qCDebug(NTRIPManagerLog) << "startNTRIP: host=" << config.host << " port=" << config.port + << " mount=" << config.mountpoint; + + _setStatus(ConnectionStatus::Connecting, tr("Connecting to %1:%2...").arg(config.host).arg(config.port)); + + _bytesReceived = 0; + _messagesReceived = 0; + _dataRateBytesPerSec = 0.0; + _dataRatePrevBytes = 0; + emit bytesReceivedChanged(); + emit messagesReceivedChanged(); + emit dataRateChanged(); + + _runningConfig = config; + _runningUdpForward = _udpForwardEnabled; + _runningUdpAddr = _udpForwardEnabled ? _udpTargetAddress.toString() : QString(); + _runningUdpPort = _udpForwardEnabled ? _udpTargetPort : 0; + + _transport = new NTRIPHttpTransport(config, this); + + connect(_transport, &NTRIPHttpTransport::error, + this, &NTRIPManager::_tcpError); + + connect(_transport, &NTRIPHttpTransport::connected, this, [this]() { + _reconnectAttempts = 0; + _casterStatus = CasterStatus::CasterConnected; + emit casterStatusChanged(_casterStatus); + + _setStatus(ConnectionStatus::Connected, tr("Connected")); + + if (_ggaTimer && !_ggaTimer->isActive()) { + _ggaTimer->setInterval(1000); + _sendGGA(); + _ggaTimer->start(); + } + _dataRateTimer->start(); + }); + + connect(_transport, &NTRIPHttpTransport::RTCMDataUpdate, this, &NTRIPManager::_rtcmDataReceived); + + _transport->start(); + qCDebug(NTRIPManagerLog) << "NTRIP started"; + + _startStopBusy = false; +} + +void NTRIPManager::stopNTRIP() +{ + if (_startStopBusy) { + qCWarning(NTRIPManagerLog) << "stopNTRIP called while start/stop in progress"; + return; + } + _startStopBusy = true; + + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + + if (_transport) { + disconnect(_transport, &NTRIPHttpTransport::error, this, &NTRIPManager::_tcpError); + _transport->stop(); + _transport->deleteLater(); + _transport = nullptr; + + _runningConfig = {}; + _runningUdpForward = false; + _runningUdpAddr.clear(); + _runningUdpPort = 0; + + _setStatus(ConnectionStatus::Disconnected, tr("Disconnected")); + qCDebug(NTRIPManagerLog) << "NTRIP stopped"; + } + + if (_ggaTimer && _ggaTimer->isActive()) { + _ggaTimer->stop(); + } + _dataRateTimer->stop(); + if (_dataRateBytesPerSec != 0.0) { + _dataRateBytesPerSec = 0.0; + _dataRatePrevBytes = 0; + emit dataRateChanged(); + } + + if (_udpSocket) { + _udpSocket->close(); + delete _udpSocket; + _udpSocket = nullptr; + } + _udpForwardEnabled = false; + + _startStopBusy = false; +} + +void NTRIPManager::_tcpError(const QString& errorMsg) +{ + qCWarning(NTRIPManagerLog) << "NTRIP error:" << errorMsg; + _setStatus(ConnectionStatus::Error, errorMsg); + + if (errorMsg.contains("NO_LOCATION_PROVIDED", Qt::CaseInsensitive)) { + _casterStatus = CasterStatus::CasterNoLocation; + } else { + _casterStatus = CasterStatus::CasterError; + } + emit casterStatusChanged(_casterStatus); + + _scheduleReconnect(); +} + +void NTRIPManager::_scheduleReconnect() +{ + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + bool shouldRun = settings && settings->ntripServerConnectEnabled() && + settings->ntripServerConnectEnabled()->rawValue().toBool(); + if (!shouldRun) { + return; + } + + stopNTRIP(); + + int backoffMs = qMin(kMinReconnectMs * (1 << qMin(_reconnectAttempts, 4)), kMaxReconnectMs); + _reconnectAttempts = qMin(_reconnectAttempts + 1, kMaxReconnectAttempts); + + qCDebug(NTRIPManagerLog) << "NTRIP reconnecting in" << backoffMs << "ms (attempt" << _reconnectAttempts << ")"; + + _setStatus(ConnectionStatus::Reconnecting, tr("Reconnecting in %1s...").arg(backoffMs / 1000)); + + if (!_reconnectTimer) { + _reconnectTimer = new QTimer(this); + _reconnectTimer->setSingleShot(true); + connect(_reconnectTimer, &QTimer::timeout, this, [this]() { + if (!_transport) { + startNTRIP(); + } + }); + } + + _reconnectTimer->start(backoffMs); +} + +void NTRIPManager::_rtcmDataReceived(const QByteArray& data) +{ + _bytesReceived += static_cast(data.size()); + _messagesReceived++; + + qCDebug(NTRIPManagerLog) << "NTRIP Forwarding RTCM to vehicle:" << data.size() << "bytes"; + + if (!_rtcmMavlink && qgcApp()) { + _rtcmMavlink = qgcApp()->findChild(); + } + + if (_rtcmMavlink) { + _rtcmMavlink->RTCMDataUpdate(data); + + if (_connectionStatus != ConnectionStatus::Connected) { + _setStatus(ConnectionStatus::Connected, tr("Connected")); + } + } else { + qCWarning(NTRIPManagerLog) << "RTCMMavlink not ready; dropping" << data.size() << "bytes"; + } + + if (_udpForwardEnabled && _udpSocket && _udpTargetPort > 0) { + const qint64 sent = _udpSocket->writeDatagram(data, _udpTargetAddress, _udpTargetPort); + if (sent < 0) { + qCWarning(NTRIPManagerLog) << "NTRIP UDP forward failed:" << _udpSocket->errorString(); + } + } +} + +QByteArray NTRIPManager::makeGGA(const QGeoCoordinate& coord, double altitude_msl) +{ + const QTime utc = QDateTime::currentDateTimeUtc().time(); + const QString hhmmss = QString("%1%2%3") + .arg(utc.hour(), 2, 10, QChar('0')) + .arg(utc.minute(), 2, 10, QChar('0')) + .arg(utc.second(), 2, 10, QChar('0')); + + auto dmm = [](double deg, bool lat) -> QString { + double a = qFabs(deg); + int d = int(a); + double m = (a - d) * 60.0; + + int m10000 = int(m * 10000.0 + 0.5); + double m_rounded = m10000 / 10000.0; + if (m_rounded >= 60.0) { + m_rounded -= 60.0; + d += 1; + } + + QString mm = QString::number(m_rounded, 'f', 4); + if (m_rounded < 10.0) { + mm.prepend("0"); + } + + if (lat) { + return QString("%1%2").arg(d, 2, 10, QChar('0')).arg(mm); + } else { + return QString("%1%2").arg(d, 3, 10, QChar('0')).arg(mm); + } + }; + + const bool latNorth = coord.latitude() >= 0.0; + const bool lonEast = coord.longitude() >= 0.0; + + const QString latField = dmm(coord.latitude(), true); + const QString lonField = dmm(coord.longitude(), false); + + const QString core = QString("GPGGA,%1,%2,%3,%4,%5,1,12,1.0,%6,M,0.0,M,,") + .arg(hhmmss) + .arg(latField) + .arg(latNorth ? "N" : "S") + .arg(lonField) + .arg(lonEast ? "E" : "W") + .arg(QString::number(altitude_msl, 'f', 1)); + + quint8 cksum = 0; + const QByteArray coreBytes = core.toUtf8(); + for (char ch : coreBytes) { + cksum ^= static_cast(ch); + } + + const QString cks = QString("%1").arg(cksum, 2, 16, QChar('0')).toUpper(); + const QByteArray sentence = QByteArray("$") + coreBytes + QByteArray("*") + cks.toUtf8(); + return sentence; +} + +QPair NTRIPManager::_getBestPosition() const +{ + MultiVehicleManager* mvm = MultiVehicleManager::instance(); + if (mvm) { + if (Vehicle* veh = mvm->activeVehicle()) { + FactGroup* gps = veh->gpsFactGroup(); + if (gps) { + Fact* latF = gps->getFact(QStringLiteral("lat")); + Fact* lonF = gps->getFact(QStringLiteral("lon")); + + if (latF && lonF) { + const double lat = latF->rawValue().toDouble(); + const double lon = lonF->rawValue().toDouble(); + + if (qIsFinite(lat) && qIsFinite(lon) && + !(lat == 0.0 && lon == 0.0) && + qAbs(lat) <= 90.0 && qAbs(lon) <= 180.0) { + + return {QGeoCoordinate(lat, lon, veh->coordinate().altitude()), + QStringLiteral("GPS Raw")}; + } + } + } + + QGeoCoordinate coord = veh->coordinate(); + if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { + return {coord, QStringLiteral("Vehicle EKF")}; + } + } + } + + QGCPositionManager* posMgr = QGCPositionManager::instance(); + if (posMgr) { + QGeoCoordinate coord = posMgr->gcsPosition(); + if (coord.isValid() && !(coord.latitude() == 0.0 && coord.longitude() == 0.0)) { + return {coord, QStringLiteral("GCS Position")}; + } + } + + return {QGeoCoordinate(), QString()}; +} + +void NTRIPManager::_sendGGA() +{ + if (!_transport) { + return; + } + + const auto [coord, srcUsed] = _getBestPosition(); + + if (!coord.isValid()) { + qCDebug(NTRIPManagerLog) << "NTRIP: No valid position, skipping GGA"; + return; + } + + if (_ggaTimer && _ggaTimer->interval() != 5000) { + _ggaTimer->setInterval(5000); + qCDebug(NTRIPManagerLog) << "NTRIP: Position acquired, reducing GGA to 5s"; + } + + double alt_msl = coord.altitude(); + if (!qIsFinite(alt_msl)) { + alt_msl = 0.0; + } + + const QByteArray gga = makeGGA(coord, alt_msl); + _transport->sendNMEA(gga); + + if (!srcUsed.isEmpty() && srcUsed != _ggaSource) { + _ggaSource = srcUsed; + emit ggaSourceChanged(); + } +} + +void NTRIPManager::_onSettingChanged() +{ + if (_startStopBusy) { + return; + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + bool shouldRun = settings && settings->ntripServerConnectEnabled() && + settings->ntripServerConnectEnabled()->rawValue().toBool(); + bool isRunning = (_transport != nullptr); + + if (shouldRun && isRunning && settings) { + NTRIPTransportConfig config = _configFromSettings(); + const bool udpFwd = settings->ntripUdpForwardEnabled()->rawValue().toBool(); + const QString udpAddr = settings->ntripUdpTargetAddress()->rawValue().toString(); + const quint16 udpPort = static_cast(settings->ntripUdpTargetPort()->rawValue().toUInt()); + + if (config != _runningConfig || + udpFwd != _runningUdpForward || udpAddr != _runningUdpAddr || udpPort != _runningUdpPort) { + qCDebug(NTRIPManagerLog) << "NTRIP settings changed while running, restarting"; + stopNTRIP(); + startNTRIP(); + return; + } + } + + const bool reconnectPending = _reconnectTimer && _reconnectTimer->isActive(); + + if (shouldRun && !isRunning && !reconnectPending) { + startNTRIP(); + } else if (!shouldRun) { + if (_reconnectTimer) { + _reconnectTimer->stop(); + } + if (isRunning) { + stopNTRIP(); + } else if (reconnectPending) { + _reconnectAttempts = 0; + _setStatus(ConnectionStatus::Disconnected, tr("Disconnected")); + } + } +} + +QmlObjectListModel* NTRIPManager::mountpointModel() const +{ + if (_sourceTableModel) { + return _sourceTableModel->mountpoints(); + } + return nullptr; +} + +void NTRIPManager::fetchMountpoints() +{ + if (_mountpointFetchStatus == MountpointFetchStatus::FetchInProgress) { + return; + } + + if (_sourceTableModel && _sourceTableModel->count() > 0 && _sourceTableFetchedAtMs > 0) { + const qint64 age = QDateTime::currentMSecsSinceEpoch() - _sourceTableFetchedAtMs; + if (age < kSourceTableCacheTtlMs) { + qCDebug(NTRIPManagerLog) << "Source table cache hit, age:" << age << "ms"; + _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess; + emit mountpointFetchStatusChanged(); + return; + } + } + + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (!settings) { + return; + } + + const QString host = settings->ntripServerHostAddress()->rawValue().toString(); + const int port = settings->ntripServerPort()->rawValue().toInt(); + const QString user = settings->ntripUsername()->rawValue().toString(); + const QString pass = settings->ntripPassword()->rawValue().toString(); + const bool useTls = settings->ntripUseTls() ? settings->ntripUseTls()->rawValue().toBool() : false; + + if (host.isEmpty()) { + _mountpointFetchError = tr("Host address is empty"); + _mountpointFetchStatus = MountpointFetchStatus::FetchError; + emit mountpointFetchErrorChanged(); + emit mountpointFetchStatusChanged(); + return; + } + + if (!_sourceTableModel) { + _sourceTableModel = new NTRIPSourceTableModel(this); + emit mountpointModelChanged(); + } + + if (_sourceTableFetcher) { + _sourceTableFetcher->deleteLater(); + _sourceTableFetcher = nullptr; + } + + _mountpointFetchStatus = MountpointFetchStatus::FetchInProgress; + _mountpointFetchError.clear(); + emit mountpointFetchStatusChanged(); + emit mountpointFetchErrorChanged(); + + _sourceTableFetcher = new NTRIPSourceTableFetcher(host, port, user, pass, useTls, this); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::sourceTableReceived, this, [this](const QString& table) { + _sourceTableModel->parseSourceTable(table); + _sourceTableFetchedAtMs = QDateTime::currentMSecsSinceEpoch(); + + MultiVehicleManager* mvm = MultiVehicleManager::instance(); + if (mvm) { + if (Vehicle* veh = mvm->activeVehicle()) { + QGeoCoordinate coord = veh->coordinate(); + if (coord.isValid()) { + _sourceTableModel->updateDistances(coord); + } + } + } + + _mountpointFetchStatus = MountpointFetchStatus::FetchSuccess; + emit mountpointFetchStatusChanged(); + qCDebug(NTRIPManagerLog) << "NTRIP source table fetched:" << _sourceTableModel->count() << "mountpoints"; + }); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::error, this, [this](const QString& errorMsg) { + _mountpointFetchError = errorMsg; + _mountpointFetchStatus = MountpointFetchStatus::FetchError; + emit mountpointFetchErrorChanged(); + emit mountpointFetchStatusChanged(); + qCWarning(NTRIPManagerLog) << "NTRIP source table fetch error:" << errorMsg; + }); + + connect(_sourceTableFetcher, &NTRIPSourceTableFetcher::finished, this, [this]() { + _sourceTableFetcher->deleteLater(); + _sourceTableFetcher = nullptr; + }); + + _sourceTableFetcher->fetch(); +} + +void NTRIPManager::selectMountpoint(const QString& mountpoint) +{ + NTRIPSettings* settings = SettingsManager::instance()->ntripSettings(); + if (settings && settings->ntripMountpoint()) { + settings->ntripMountpoint()->setRawValue(mountpoint); + qCDebug(NTRIPManagerLog) << "NTRIP mountpoint selected:" << mountpoint; + } +} diff --git a/src/GPS/NTRIP/NTRIPManager.h b/src/GPS/NTRIP/NTRIPManager.h new file mode 100644 index 000000000000..0ed8bc2b5ffc --- /dev/null +++ b/src/GPS/NTRIP/NTRIPManager.h @@ -0,0 +1,149 @@ +#pragma once + +#include "NTRIPHttpTransport.h" + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(NTRIPManagerLog) + +class RTCMMavlink; +class NTRIPSettings; +class Vehicle; +class MultiVehicleManager; +class NTRIPSourceTableModel; +class NTRIPSourceTableFetcher; +class QmlObjectListModel; + +class NTRIPManager : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + Q_MOC_INCLUDE("QmlObjectListModel.h") + Q_PROPERTY(ConnectionStatus connectionStatus READ connectionStatus NOTIFY connectionStatusChanged) + Q_PROPERTY(QString statusMessage READ statusMessage NOTIFY statusMessageChanged) + Q_PROPERTY(CasterStatus casterStatus READ casterStatus NOTIFY casterStatusChanged) + Q_PROPERTY(QString ggaSource READ ggaSource NOTIFY ggaSourceChanged) + Q_PROPERTY(QmlObjectListModel* mountpointModel READ mountpointModel NOTIFY mountpointModelChanged) + Q_PROPERTY(MountpointFetchStatus mountpointFetchStatus READ mountpointFetchStatus NOTIFY mountpointFetchStatusChanged) + Q_PROPERTY(QString mountpointFetchError READ mountpointFetchError NOTIFY mountpointFetchErrorChanged) + Q_PROPERTY(quint64 bytesReceived READ bytesReceived NOTIFY bytesReceivedChanged) + Q_PROPERTY(quint32 messagesReceived READ messagesReceived NOTIFY messagesReceivedChanged) + Q_PROPERTY(double dataRateBytesPerSec READ dataRateBytesPerSec NOTIFY dataRateChanged) + +public: + enum class ConnectionStatus { + Disconnected, + Connecting, + Connected, + Reconnecting, + Error + }; + Q_ENUM(ConnectionStatus) + + enum class CasterStatus { CasterConnected, CasterNoLocation, CasterError }; + Q_ENUM(CasterStatus) + + enum class MountpointFetchStatus { + FetchIdle, + FetchInProgress, + FetchSuccess, + FetchError + }; + Q_ENUM(MountpointFetchStatus) + + static constexpr int kMinReconnectMs = 1000; + static constexpr int kMaxReconnectMs = 30000; + static constexpr int kMaxReconnectAttempts = 100; + static constexpr int kSourceTableCacheTtlMs = 60000; + + explicit NTRIPManager(QObject* parent = nullptr); + ~NTRIPManager() override; + + static NTRIPManager* instance(); + + ConnectionStatus connectionStatus() const { return _connectionStatus; } + QString statusMessage() const { return _statusMessage; } + CasterStatus casterStatus() const { return _casterStatus; } + QString ggaSource() const { return _ggaSource; } + QmlObjectListModel* mountpointModel() const; + MountpointFetchStatus mountpointFetchStatus() const { return _mountpointFetchStatus; } + QString mountpointFetchError() const { return _mountpointFetchError; } + quint64 bytesReceived() const { return _bytesReceived; } + quint32 messagesReceived() const { return _messagesReceived; } + double dataRateBytesPerSec() const { return _dataRateBytesPerSec; } + + Q_INVOKABLE void fetchMountpoints(); + Q_INVOKABLE void selectMountpoint(const QString& mountpoint); + + void startNTRIP(); + void stopNTRIP(); + + static QByteArray makeGGA(const QGeoCoordinate& coord, double altitude_msl); + +signals: + void connectionStatusChanged(); + void statusMessageChanged(); + void casterStatusChanged(CasterStatus status); + void ggaSourceChanged(); + void mountpointFetchStatusChanged(); + void mountpointModelChanged(); + void mountpointFetchErrorChanged(); + void bytesReceivedChanged(); + void messagesReceivedChanged(); + void dataRateChanged(); + +private: + void _tcpError(const QString& errorMsg); + void _rtcmDataReceived(const QByteArray& data); + void _onSettingChanged(); + void _sendGGA(); + void _scheduleReconnect(); + void _setStatus(ConnectionStatus status, const QString& msg = {}); + + NTRIPTransportConfig _configFromSettings() const; + QPair _getBestPosition() const; + + QTimer* _ggaTimer = nullptr; + + ConnectionStatus _connectionStatus = ConnectionStatus::Disconnected; + QString _statusMessage; + QString _ggaSource; + + NTRIPHttpTransport* _transport = nullptr; + RTCMMavlink* _rtcmMavlink = nullptr; + QTimer* _reconnectTimer = nullptr; + int _reconnectAttempts = 0; + bool _startStopBusy = false; + + CasterStatus _casterStatus = CasterStatus::CasterError; + + QUdpSocket* _udpSocket = nullptr; + QHostAddress _udpTargetAddress; + quint16 _udpTargetPort = 0; + bool _udpForwardEnabled = false; + + NTRIPTransportConfig _runningConfig; + bool _runningUdpForward = false; + QString _runningUdpAddr; + quint16 _runningUdpPort = 0; + + NTRIPSourceTableModel* _sourceTableModel = nullptr; + NTRIPSourceTableFetcher* _sourceTableFetcher = nullptr; + MountpointFetchStatus _mountpointFetchStatus = MountpointFetchStatus::FetchIdle; + QString _mountpointFetchError; + + quint64 _bytesReceived = 0; + quint32 _messagesReceived = 0; + double _dataRateBytesPerSec = 0.0; + quint64 _dataRatePrevBytes = 0; + QTimer* _dataRateTimer = nullptr; + + qint64 _sourceTableFetchedAtMs = 0; +}; diff --git a/src/GPS/NTRIP/NTRIPSourceTable.cc b/src/GPS/NTRIP/NTRIPSourceTable.cc new file mode 100644 index 000000000000..9439e50e455b --- /dev/null +++ b/src/GPS/NTRIP/NTRIPSourceTable.cc @@ -0,0 +1,202 @@ +#include "NTRIPSourceTable.h" +#include "QGCLoggingCategory.h" +#include "QGCNetworkHelper.h" +#include "QmlObjectListModel.h" + +#include +#include + +QGC_LOGGING_CATEGORY(NTRIPSourceTableLog, "GPS.NTRIPSourceTable") + +NTRIPMountpointModel* NTRIPMountpointModel::fromSourceTableLine(const QString& line, QObject* parent) +{ + const QStringList fields = line.split(';'); + if (fields.size() < 18 || fields.at(0).trimmed().toUpper() != QStringLiteral("STR")) { + return nullptr; + } + + auto* model = new NTRIPMountpointModel(parent); + model->_mountpoint = fields.at(1).trimmed(); + model->_identifier = fields.at(2).trimmed(); + model->_format = fields.at(3).trimmed(); + model->_formatDetails = fields.at(4).trimmed(); + model->_carrier = fields.at(5).trimmed().toInt(); + model->_navSystem = fields.at(6).trimmed(); + model->_network = fields.at(7).trimmed(); + model->_country = fields.at(8).trimmed(); + model->_latitude = fields.at(9).trimmed().toDouble(); + model->_longitude = fields.at(10).trimmed().toDouble(); + model->_nmea = fields.at(11).trimmed() == QStringLiteral("1"); + model->_solution = fields.at(12).trimmed() == QStringLiteral("1"); + model->_generator = fields.at(13).trimmed(); + model->_compression = fields.at(14).trimmed(); + model->_authentication = fields.at(15).trimmed(); + model->_fee = fields.at(16).trimmed() == QStringLiteral("Y"); + model->_bitrate = fields.at(17).trimmed().toInt(); + + return model; +} + +void NTRIPMountpointModel::updateDistance(const QGeoCoordinate& from) +{ + if (!from.isValid() || (_latitude == 0.0 && _longitude == 0.0)) { + return; + } + + const QGeoCoordinate mountCoord(_latitude, _longitude); + const double dist = from.distanceTo(mountCoord) / 1000.0; + if (qFuzzyCompare(_distanceKm, dist)) { + return; + } + _distanceKm = dist; + emit distanceKmChanged(); +} + +// --------------------------------------------------------------------------- +// NTRIPSourceTableModel +// --------------------------------------------------------------------------- + +NTRIPSourceTableModel::NTRIPSourceTableModel(QObject* parent) + : QObject(parent) + , _mountpoints(new QmlObjectListModel(this)) +{ +} + +int NTRIPSourceTableModel::count() const +{ + return _mountpoints ? _mountpoints->count() : 0; +} + +void NTRIPSourceTableModel::parseSourceTable(const QString& raw) +{ + clear(); + + const QStringList lines = raw.split('\n'); + for (const QString& line : lines) { + const QString trimmed = line.trimmed(); + if (trimmed.isEmpty() || trimmed.startsWith(QStringLiteral("ENDSOURCETABLE"))) { + continue; + } + NTRIPMountpointModel* mp = NTRIPMountpointModel::fromSourceTableLine(trimmed, _mountpoints); + if (mp) { + _mountpoints->append(mp); + } + } + + emit countChanged(); +} + +void NTRIPSourceTableModel::updateDistances(const QGeoCoordinate& from) +{ + if (!_mountpoints) { + return; + } + for (int i = 0; i < _mountpoints->count(); ++i) { + auto* mp = qobject_cast(_mountpoints->get(i)); + if (mp) { + mp->updateDistance(from); + } + } + sortByDistance(); +} + +void NTRIPSourceTableModel::sortByDistance() +{ + if (!_mountpoints || _mountpoints->count() < 2) { + return; + } + + QObjectList sorted = *_mountpoints->objectList(); + std::sort(sorted.begin(), sorted.end(), [](QObject* a, QObject* b) { + auto* ma = qobject_cast(a); + auto* mb = qobject_cast(b); + if (!ma || !mb) return false; + const double da = ma->distanceKm(); + const double db = mb->distanceKm(); + if (da < 0 && db < 0) return false; + if (da < 0) return false; + if (db < 0) return true; + return da < db; + }); + _mountpoints->swapObjectList(sorted); +} + +void NTRIPSourceTableModel::clear() +{ + if (_mountpoints) { + _mountpoints->clearAndDeleteContents(); + emit countChanged(); + } +} + +// --------------------------------------------------------------------------- +// NTRIPSourceTableFetcher +// --------------------------------------------------------------------------- + +NTRIPSourceTableFetcher::NTRIPSourceTableFetcher(const QString& host, int port, + const QString& username, const QString& password, + bool useTls, + QObject* parent) + : QObject(parent) + , _host(host) + , _port(port) + , _username(username) + , _password(password) + , _useTls(useTls) +{ + _networkManager = QGCNetworkHelper::createNetworkManager(this); +} + +void NTRIPSourceTableFetcher::fetch() +{ + QUrl url; + url.setScheme(_useTls ? QStringLiteral("https") : QStringLiteral("http")); + url.setHost(_host); + url.setPort(_port); + url.setPath(QStringLiteral("/")); + + QGCNetworkHelper::RequestConfig config; + config.timeoutMs = kFetchTimeoutMs; + config.userAgent = QStringLiteral("QGC-NTRIP"); + config.http2Allowed = false; + config.cacheEnabled = false; + + QNetworkRequest request = QGCNetworkHelper::createRequest(url, config); + request.setRawHeader("Ntrip-Version", "Ntrip/2.0"); + if (!_username.isEmpty() || !_password.isEmpty()) { + QGCNetworkHelper::setBasicAuth(request, _username, _password); + } + + _reply = _networkManager->get(request); + connect(_reply, &QNetworkReply::finished, this, &NTRIPSourceTableFetcher::_onReplyFinished); +} + +void NTRIPSourceTableFetcher::_onReplyFinished() +{ + if (!_reply) { + emit error(tr("No reply received")); + emit finished(); + return; + } + + const QString body = QString::fromUtf8(_reply->readAll()); + + if (_reply->error() != QNetworkReply::NoError && !body.contains(QStringLiteral("ENDSOURCETABLE"))) { + emit error(QGCNetworkHelper::errorMessage(_reply)); + _reply->deleteLater(); + _reply = nullptr; + emit finished(); + return; + } + _reply->deleteLater(); + _reply = nullptr; + + if (!body.contains(QStringLiteral("ENDSOURCETABLE"))) { + emit error(tr("Response does not contain a valid source table")); + emit finished(); + return; + } + + emit sourceTableReceived(body); + emit finished(); +} diff --git a/src/GPS/NTRIP/NTRIPSourceTable.h b/src/GPS/NTRIP/NTRIPSourceTable.h new file mode 100644 index 000000000000..1804a1dd5849 --- /dev/null +++ b/src/GPS/NTRIP/NTRIPSourceTable.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(NTRIPSourceTableLog) + +class QmlObjectListModel; +class QNetworkAccessManager; + +class NTRIPMountpointModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString mountpoint READ mountpoint CONSTANT) + Q_PROPERTY(QString identifier READ identifier CONSTANT) + Q_PROPERTY(QString format READ format CONSTANT) + Q_PROPERTY(QString formatDetails READ formatDetails CONSTANT) + Q_PROPERTY(int carrier READ carrier CONSTANT) + Q_PROPERTY(QString navSystem READ navSystem CONSTANT) + Q_PROPERTY(QString network READ network CONSTANT) + Q_PROPERTY(QString country READ country CONSTANT) + Q_PROPERTY(double latitude READ latitude CONSTANT) + Q_PROPERTY(double longitude READ longitude CONSTANT) + Q_PROPERTY(bool nmea READ nmea CONSTANT) + Q_PROPERTY(bool solution READ solution CONSTANT) + Q_PROPERTY(QString generator READ generator CONSTANT) + Q_PROPERTY(QString compression READ compression CONSTANT) + Q_PROPERTY(QString authentication READ authentication CONSTANT) + Q_PROPERTY(bool fee READ fee CONSTANT) + Q_PROPERTY(int bitrate READ bitrate CONSTANT) + Q_PROPERTY(double distanceKm READ distanceKm NOTIFY distanceKmChanged) + +public: + explicit NTRIPMountpointModel(QObject* parent = nullptr) : QObject(parent) {} + + QString mountpoint() const { return _mountpoint; } + QString identifier() const { return _identifier; } + QString format() const { return _format; } + QString formatDetails() const { return _formatDetails; } + int carrier() const { return _carrier; } + QString navSystem() const { return _navSystem; } + QString network() const { return _network; } + QString country() const { return _country; } + double latitude() const { return _latitude; } + double longitude() const { return _longitude; } + bool nmea() const { return _nmea; } + bool solution() const { return _solution; } + QString generator() const { return _generator; } + QString compression() const { return _compression; } + QString authentication() const { return _authentication; } + bool fee() const { return _fee; } + int bitrate() const { return _bitrate; } + double distanceKm() const { return _distanceKm; } + + static NTRIPMountpointModel* fromSourceTableLine(const QString& line, QObject* parent = nullptr); + void updateDistance(const QGeoCoordinate& from); + +signals: + void distanceKmChanged(); + +private: + QString _mountpoint; + QString _identifier; + QString _format; + QString _formatDetails; + int _carrier = 0; + QString _navSystem; + QString _network; + QString _country; + double _latitude = 0.0; + double _longitude = 0.0; + bool _nmea = false; + bool _solution = false; + QString _generator; + QString _compression; + QString _authentication; + bool _fee = false; + int _bitrate = 0; + double _distanceKm = -1.0; +}; + +// --------------------------------------------------------------------------- +// NTRIPSourceTableModel +// --------------------------------------------------------------------------- + +class NTRIPSourceTableModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QmlObjectListModel* mountpoints READ mountpoints CONSTANT) + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit NTRIPSourceTableModel(QObject* parent = nullptr); + + QmlObjectListModel* mountpoints() const { return _mountpoints; } + int count() const; + + void parseSourceTable(const QString& raw); + void updateDistances(const QGeoCoordinate& from); + void sortByDistance(); + void clear(); + +signals: + void countChanged(); + +private: + QmlObjectListModel* _mountpoints = nullptr; +}; + +// --------------------------------------------------------------------------- +// NTRIPSourceTableFetcher +// --------------------------------------------------------------------------- + +class NTRIPSourceTableFetcher : public QObject +{ + Q_OBJECT + +public: + static constexpr int kFetchTimeoutMs = 10000; + + NTRIPSourceTableFetcher(const QString& host, int port, + const QString& username, const QString& password, + bool useTls = false, + QObject* parent = nullptr); + ~NTRIPSourceTableFetcher() override = default; + + void fetch(); + +signals: + void sourceTableReceived(const QString& table); + void error(const QString& errorMsg); + void finished(); + +private: + void _onReplyFinished(); + + QNetworkAccessManager* _networkManager = nullptr; + QNetworkReply* _reply = nullptr; + QString _host; + int _port; + QString _username; + QString _password; + bool _useTls = false; +}; diff --git a/src/GPS/NTRIP/RTCMParser.cc b/src/GPS/NTRIP/RTCMParser.cc new file mode 100644 index 000000000000..b0eaef98b1e8 --- /dev/null +++ b/src/GPS/NTRIP/RTCMParser.cc @@ -0,0 +1,97 @@ +#include "RTCMParser.h" + +RTCMParser::RTCMParser() +{ + reset(); +} + +void RTCMParser::reset() +{ + _state = WaitingForPreamble; + _messageLength = 0; + _bytesRead = 0; + _lengthBytesRead = 0; + _crcBytesRead = 0; +} + +bool RTCMParser::addByte(uint8_t byte) +{ + switch (_state) { + case WaitingForPreamble: + if (byte == RTCM3_PREAMBLE) { + _buffer[0] = byte; + _bytesRead = 1; + _state = ReadingLength; + _lengthBytesRead = 0; + } + break; + + case ReadingLength: + _lengthBytes[_lengthBytesRead++] = byte; + _buffer[_bytesRead++] = byte; + if (_lengthBytesRead == 2) { + _messageLength = ((_lengthBytes[0] & 0x03) << 8) | _lengthBytes[1]; + if (_messageLength > 0 && _messageLength <= kMaxPayloadLength) { + _state = ReadingMessage; + } else { + reset(); + } + } + break; + + case ReadingMessage: + if (_bytesRead < kHeaderSize + kMaxPayloadLength) { + _buffer[_bytesRead++] = byte; + } + if (_bytesRead >= _messageLength + kHeaderSize) { + _state = ReadingCRC; + _crcBytesRead = 0; + } + break; + + case ReadingCRC: + _crcBytes[_crcBytesRead++] = byte; + if (_crcBytesRead == kCrcSize) { + return true; + } + break; + } + return false; +} + +uint16_t RTCMParser::messageId() const +{ + if (_messageLength >= 2) { + return ((_buffer[3] << 4) | (_buffer[4] >> 4)) & 0xFFF; + } + return 0; +} + +uint32_t RTCMParser::crc24q(const uint8_t* data, size_t len) +{ + static constexpr uint32_t kPoly = 0x1864CFB; + uint32_t crc = 0; + for (size_t i = 0; i < len; i++) { + crc ^= static_cast(data[i]) << 16; + for (int j = 0; j < 8; j++) { + crc <<= 1; + if (crc & 0x1000000) { + crc ^= kPoly; + } + } + } + return crc & 0xFFFFFF; +} + +bool RTCMParser::validateCrc() const +{ + if (_messageLength == 0 || _bytesRead < kHeaderSize + _messageLength) { + return false; + } + + const uint32_t computed = crc24q(_buffer, kHeaderSize + _messageLength); + const uint32_t received = (static_cast(_crcBytes[0]) << 16) | + (static_cast(_crcBytes[1]) << 8) | + static_cast(_crcBytes[2]); + return computed == received; +} diff --git a/src/GPS/NTRIP/RTCMParser.h b/src/GPS/NTRIP/RTCMParser.h new file mode 100644 index 000000000000..23dd6df05f1e --- /dev/null +++ b/src/GPS/NTRIP/RTCMParser.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +static constexpr uint8_t RTCM3_PREAMBLE = 0xD3; + +class RTCMParser +{ +public: + RTCMParser(); + void reset(); + bool addByte(uint8_t byte); + uint8_t* message() { return _buffer; } + uint16_t messageLength() const { return _messageLength; } + uint16_t messageId() const; + const uint8_t* crcBytes() const { return _crcBytes; } + static constexpr int kCrcSize = 3; + + bool validateCrc() const; + static uint32_t crc24q(const uint8_t* data, size_t len); + +private: + enum State { + WaitingForPreamble, + ReadingLength, + ReadingMessage, + ReadingCRC + }; + + static constexpr uint16_t kMaxPayloadLength = 1023; + static constexpr int kHeaderSize = 3; + + State _state; + uint8_t _buffer[kHeaderSize + kMaxPayloadLength]; + uint16_t _messageLength; + uint16_t _bytesRead; + uint16_t _lengthBytesRead; + uint8_t _lengthBytes[2]; + uint16_t _crcBytesRead; + uint8_t _crcBytes[3]; +}; diff --git a/src/GPS/RTCMMavlink.cc b/src/GPS/RTCMMavlink.cc index 1853608080ca..9ec7c75bdf3e 100644 --- a/src/GPS/RTCMMavlink.cc +++ b/src/GPS/RTCMMavlink.cc @@ -21,9 +21,7 @@ RTCMMavlink::~RTCMMavlink() void RTCMMavlink::RTCMDataUpdate(QByteArrayView data) { -#ifdef QT_DEBUG _calculateBandwith(data.size()); -#endif mavlink_gps_rtcm_data_t gpsRtcmData{}; diff --git a/src/Gimbal/GimbalController.cc b/src/Gimbal/GimbalController.cc index 2122369b41e6..8826cf9baf7b 100644 --- a/src/Gimbal/GimbalController.cc +++ b/src/Gimbal/GimbalController.cc @@ -39,7 +39,7 @@ void GimbalController::_initialConnectCompleted() void GimbalController::setActiveGimbal(Gimbal *gimbal) { if (!gimbal) { - qCDebug(GimbalControllerLog) << "Set active gimbal: attempted to set a nullptr, returning"; + qCCritical(GimbalControllerLog) << "Set active gimbal: attempted to set a nullptr, returning"; return; } @@ -344,7 +344,7 @@ void GimbalController::_checkComplete(Gimbal &gimbal, GimbalPairId pairId) bool GimbalController::_tryGetGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "_tryGetGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "_tryGetGimbalControl: active gimbal is nullptr, returning"; return false; } @@ -375,7 +375,7 @@ bool GimbalController::_yawInVehicleFrame(uint32_t flags) void GimbalController::gimbalPitchStart(int direction) { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalPitchStart: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalPitchStart: active gimbal is nullptr, returning"; return; } @@ -388,7 +388,7 @@ void GimbalController::gimbalPitchStart(int direction) void GimbalController::gimbalYawStart(int direction) { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStart: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStart: active gimbal is nullptr, returning"; return; } @@ -400,7 +400,7 @@ void GimbalController::gimbalYawStart(int direction) void GimbalController::gimbalPitchStop() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalPitchStop: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalPitchStop: active gimbal is nullptr, returning"; return; } @@ -411,7 +411,7 @@ void GimbalController::gimbalPitchStop() void GimbalController::gimbalYawStop() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStop: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStop: active gimbal is nullptr, returning"; return; } @@ -422,7 +422,7 @@ void GimbalController::gimbalYawStop() void GimbalController::centerGimbal() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalYawStep: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalYawStep: active gimbal is nullptr, returning"; return; } sendPitchBodyYaw(0.0, 0.0, true); @@ -433,13 +433,13 @@ void GimbalController::gimbalOnScreenControl(float panPct, float tiltPct, bool c // Pan and tilt comes as +-(0-1) if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "gimbalOnScreenControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "gimbalOnScreenControl: active gimbal is nullptr, returning"; return; } if (clickAndPoint) { // based on FOV - const float hFov = SettingsManager::instance()->gimbalControllerSettings()->CameraHFov()->rawValue().toFloat(); - const float vFov = SettingsManager::instance()->gimbalControllerSettings()->CameraVFov()->rawValue().toFloat(); + const float hFov = SettingsManager::instance()->gimbalControllerSettings()->cameraHFov()->rawValue().toFloat(); + const float vFov = SettingsManager::instance()->gimbalControllerSettings()->cameraVFov()->rawValue().toFloat(); const float panIncDesired = panPct * hFov * 0.5f; const float tiltIncDesired = tiltPct * vFov * 0.5f; @@ -456,7 +456,7 @@ void GimbalController::gimbalOnScreenControl(float panPct, float tiltPct, bool c // Should send rate commands, but it seems for some reason it is not working on AP side. // Pitch works ok but yaw doesn't stop, it keeps like inertia, like if it was buffering the messages. // So we do a workaround with angle targets - const float maxSpeed = SettingsManager::instance()->gimbalControllerSettings()->CameraSlideSpeed()->rawValue().toFloat(); + const float maxSpeed = SettingsManager::instance()->gimbalControllerSettings()->cameraSlideSpeed()->rawValue().toFloat(); const float panIncDesired = panPct * maxSpeed * 0.1f; const float tiltIncDesired = tiltPct * maxSpeed * 0.1f; @@ -686,7 +686,7 @@ void GimbalController::sendPitchYawFlags(uint32_t flags) void GimbalController::acquireGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "acquireGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "acquireGimbalControl: active gimbal is nullptr, returning"; return; } @@ -706,7 +706,7 @@ void GimbalController::acquireGimbalControl() void GimbalController::releaseGimbalControl() { if (!_activeGimbal) { - qCDebug(GimbalControllerLog) << "releaseGimbalControl: active gimbal is nullptr, returning"; + qCCritical(GimbalControllerLog) << "releaseGimbalControl: active gimbal is nullptr, returning"; return; } diff --git a/src/Joystick/Joystick.cc b/src/Joystick/Joystick.cc index b7e996aa7769..83f6409e7ba4 100644 --- a/src/Joystick/Joystick.cc +++ b/src/Joystick/Joystick.cc @@ -9,6 +9,7 @@ #include "QmlObjectListModel.h" #include "SettingsManager.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "JoystickManager.h" #include "MultiVehicleManager.h" @@ -1001,8 +1002,8 @@ void Joystick::_handleAxis() } // Adjust throttle to 0:1 range - if (throttleModeCenterZero && vehicle->supportsThrottleModeCenterZero()) { - if (!vehicle->supportsNegativeThrust() || !negativeThrust) { + if (throttleModeCenterZero && vehicle->supports()->throttleModeCenterZero()) { + if (!vehicle->supports()->negativeThrust() || !negativeThrust) { throttle = std::max(0.0f, throttle); } } else { diff --git a/src/MAVLink/CMakeLists.txt b/src/MAVLink/CMakeLists.txt index c31b7b55196d..5bf39bd94f07 100644 --- a/src/MAVLink/CMakeLists.txt +++ b/src/MAVLink/CMakeLists.txt @@ -12,6 +12,8 @@ target_sources(${CMAKE_PROJECT_NAME} MAVLinkLib.h MAVLinkSigning.cc MAVLinkSigning.h + MAVLinkSigningKeys.cc + MAVLinkSigningKeys.h MAVLinkStreamConfig.cc MAVLinkStreamConfig.h QGCMAVLink.cc diff --git a/src/MAVLink/ImageProtocolManager.cc b/src/MAVLink/ImageProtocolManager.cc index fb60b80bb674..593e2e84ff72 100644 --- a/src/MAVLink/ImageProtocolManager.cc +++ b/src/MAVLink/ImageProtocolManager.cc @@ -1,6 +1,8 @@ #include "ImageProtocolManager.h" #include "QGCLoggingCategory.h" +#include + QGC_LOGGING_CATEGORY(ImageProtocolManagerLog, "MAVLink.ImageProtocolManager") ImageProtocolManager::ImageProtocolManager(QObject *parent) @@ -49,6 +51,27 @@ void ImageProtocolManager::mavlinkMessageReceived(const mavlink_message_t &messa _imageBytes.clear(); mavlink_msg_data_transmission_handshake_decode(&message, &_imageHandshake); qCDebug(ImageProtocolManagerLog) << QStringLiteral("DATA_TRANSMISSION_HANDSHAKE: type(%1) width(%2) height (%3)").arg(_imageHandshake.type).arg(_imageHandshake.width).arg(_imageHandshake.height); + + // Validate handshake fields to prevent out-of-bounds writes on subsequent ENCAPSULATED_DATA + static constexpr uint32_t kMaxImageSize = 1u * 1024u * 1024u; // 1 MB upper bound (optical flow images are typically small grayscale frames) + if (_imageHandshake.size == 0 || _imageHandshake.payload == 0 || _imageHandshake.packets == 0) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: Invalid field(s) - size:" << _imageHandshake.size + << "payload:" << _imageHandshake.payload << "packets:" << _imageHandshake.packets; + _imageHandshake = {}; + break; + } + if (_imageHandshake.size > kMaxImageSize) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: Image size exceeds limit. size:" << _imageHandshake.size; + _imageHandshake = {}; + break; + } + if (_imageHandshake.payload > sizeof(mavlink_encapsulated_data_t::data)) { + qCWarning(ImageProtocolManagerLog) << "DATA_TRANSMISSION_HANDSHAKE: payload exceeds ENCAPSULATED_DATA data field size. payload:" << _imageHandshake.payload; + _imageHandshake = {}; + break; + } + + _imageBytes.resize(_imageHandshake.size, '\0'); break; } case MAVLINK_MSG_ID_ENCAPSULATED_DATA: @@ -61,16 +84,17 @@ void ImageProtocolManager::mavlinkMessageReceived(const mavlink_message_t &messa mavlink_encapsulated_data_t encapsulatedData; mavlink_msg_encapsulated_data_decode(&message, &encapsulatedData); - uint32_t bytePosition = encapsulatedData.seqnr * _imageHandshake.payload; + const uint32_t bytePosition = static_cast(encapsulatedData.seqnr) * _imageHandshake.payload; if (bytePosition >= _imageHandshake.size) { qCWarning(ImageProtocolManagerLog) << "ENCAPSULATED_DATA: seqnr is past end of image size. seqnr:" << encapsulatedData.seqnr << "_imageHandshake.size:" << _imageHandshake.size; break; } - for (uint8_t i = 0; i < _imageHandshake.payload; i++) { - _imageBytes[bytePosition] = encapsulatedData.data[i]; - bytePosition++; - } + // Clamp the number of bytes to copy so we never write past the declared image size + const uint32_t bytesRemaining = _imageHandshake.size - bytePosition; + const uint32_t bytesToCopy = qMin(static_cast(_imageHandshake.payload), bytesRemaining); + + (void) memcpy(_imageBytes.data() + bytePosition, encapsulatedData.data, bytesToCopy); // We use the packets field to track completion _imageHandshake.packets--; diff --git a/src/MAVLink/MAVLinkSigning.cc b/src/MAVLink/MAVLinkSigning.cc index d9cd8794f8aa..b420ab8b2d9c 100644 --- a/src/MAVLink/MAVLinkSigning.cc +++ b/src/MAVLink/MAVLinkSigning.cc @@ -1,12 +1,18 @@ #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" #include "QGCMAVLink.h" -#include "DeviceInfo.h" +#include "QmlObjectListModel.h" +#include #include namespace { +// Per-channel cache of the last key name that successfully matched. +// Empty means no cached hint. Avoids O(n) SHA-256 on every incoming signed packet. +static QString s_channelKeyHint[MAVLINK_COMM_NUM_BUFFERS]; + mavlink_signing_t* _getChannelSigning(uint8_t channel) { mavlink_status_t* const status = mavlink_get_channel_status(channel); @@ -22,16 +28,10 @@ mavlink_channel_t _getMessageChannel(const mavlink_message_t &message) return static_cast(message.signature[0]); } -void _setSigningKey(mavlink_signing_t *signing, QByteArrayView key, bool randomize = false) +void _setSigningKey(mavlink_signing_t *signing, QByteArrayView key) { - if (randomize) { - const size_t key_size = sizeof(signing->secret_key) / 4; - uint32_t secret_key[key_size]; - QRandomGenerator::global()->fillRange(secret_key, key_size); - (void) memcpy(signing->secret_key, secret_key, sizeof(signing->secret_key)); - } else if (!key.isEmpty()) { - const QByteArray hash = QCryptographicHash::hash(key, QCryptographicHash::Sha256); - (void) memcpy(signing->secret_key, hash.constData(), sizeof(signing->secret_key)); + if (!key.isEmpty() && key.size() >= static_cast(sizeof(signing->secret_key))) { + (void) memcpy(signing->secret_key, key.constData(), sizeof(signing->secret_key)); } else { (void) memset(signing->secret_key, 0, sizeof(signing->secret_key)); } @@ -50,7 +50,7 @@ void _setSigningTimestamp(mavlink_signing_t *signing) namespace MAVLinkSigning { -bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) +bool secureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) { Q_UNUSED(status); Q_UNUSED(message_id); @@ -58,7 +58,7 @@ bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uin return true; } -bool insecureConnectionAccceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) +bool insecureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id) { Q_UNUSED(status); @@ -85,6 +85,7 @@ bool initSigning(mavlink_channel_t channel, QByteArrayView key, mavlink_accept_u if (key.isEmpty()) { status->signing = nullptr; status->signing_streams = nullptr; + s_channelKeyHint[channel].clear(); } else { static mavlink_signing_t s_signing[MAVLINK_COMM_NUM_BUFFERS]; static mavlink_signing_streams_t s_signing_streams; @@ -115,19 +116,112 @@ bool checkSigningLinkId(mavlink_channel_t channel, const mavlink_message_t &mess return (signing->link_id == _getMessageChannel(message)); } -/// Create a setup signing message for a target system. -/// Assumes that signing has already been initialized for the channel. -void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing) +void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, QByteArrayView keyBytes, mavlink_setup_signing_t &setup_signing) { (void) memset(&setup_signing, 0, sizeof(setup_signing)); setup_signing.target_system = target_system.sysid; setup_signing.target_component = target_system.compid; + if (!keyBytes.isEmpty() && keyBytes.size() >= static_cast(sizeof(setup_signing.secret_key))) { + const mavlink_signing_t* const signing = _getChannelSigning(channel); + if (signing) { + setup_signing.initial_timestamp = signing->timestamp; + } else { + static const QDateTime offset_time = QDateTime(QDate(2015, 1, 1).startOfDay()); + setup_signing.initial_timestamp = static_cast(offset_time.msecsTo(QDateTime::currentDateTimeUtc())) * 100; + } + (void) memcpy(setup_signing.secret_key, keyBytes.constData(), sizeof(setup_signing.secret_key)); + } +} + +bool isSigningEnabled(mavlink_channel_t channel) +{ const mavlink_signing_t* const signing = _getChannelSigning(channel); - if (signing) { - setup_signing.initial_timestamp = signing->timestamp; - (void) memcpy(setup_signing.secret_key, signing->secret_key, sizeof(setup_signing.secret_key)); + return (signing != nullptr); +} + +void createDisableSigning(mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing) +{ + (void) memset(&setup_signing, 0, sizeof(setup_signing)); + setup_signing.target_system = target_system.sysid; + setup_signing.target_component = target_system.compid; +} + +bool isMessageSigned(const mavlink_message_t &message) +{ + return (message.incompat_flags & MAVLINK_IFLAG_SIGNED) != 0; +} + +bool verifySignature(QByteArrayView key, const mavlink_message_t &message) +{ + if (key.size() < 32) { + return false; + } + + // Replicate the C library's signature computation: + // SHA-256(secret_key + header_bytes + payload + CRC + link_id + timestamp) + // Then compare first 6 bytes with message.signature[7..12] + + const uint8_t* header = reinterpret_cast(&message.magic); + const char* payload = reinterpret_cast(&message.payload64[0]); + const uint8_t* crc = reinterpret_cast(payload + message.len); + const uint8_t* sig = message.signature; // [link_id(1), timestamp(6), hash(6)] + + QCryptographicHash sha256(QCryptographicHash::Sha256); + sha256.addData(key.first(32)); + sha256.addData(QByteArrayView(reinterpret_cast(header), MAVLINK_NUM_HEADER_BYTES)); + sha256.addData(QByteArrayView(payload, message.len)); + sha256.addData(QByteArrayView(reinterpret_cast(crc), 2)); + sha256.addData(QByteArrayView(reinterpret_cast(sig), 7)); // link_id + timestamp + + const QByteArray hash = sha256.result(); + return (memcmp(hash.constData(), sig + 7, 6) == 0); +} + +QString tryDetectKey(mavlink_channel_t channel, const mavlink_message_t &message) +{ + if (!isMessageSigned(message)) { + return QString(); + } + + // If signing is already configured on this channel, the C library already verified it + if (isSigningEnabled(channel)) { + return QString(); + } + + auto* signingKeys = MAVLinkSigningKeys::instance(); + const auto* keys = signingKeys->keys(); + const int keyCount = keys->count(); + + // Try the cached hint for this channel first + const QString& hintName = s_channelKeyHint[channel]; + if (!hintName.isEmpty()) { + const QByteArray keyBytes = signingKeys->keyBytesByName(hintName); + if (!keyBytes.isEmpty() && verifySignature(keyBytes, message)) { + if (initSigning(channel, keyBytes, insecureConnectionAcceptUnsignedCallback)) { + qCInfo(QGCMAVLinkLog) << "Auto-detected signing key" << hintName << "on channel" << channel << "(cached hint)"; + return hintName; + } + } + } + + // Fall back to trying all keys + for (int i = 0; i < keyCount; ++i) { + const QString name = signingKeys->keyNameAt(i); + if (name == hintName) { + continue; // already tried above + } + const QByteArray keyBytes = signingKeys->keyBytesAt(i); + if (verifySignature(keyBytes, message)) { + if (initSigning(channel, keyBytes, insecureConnectionAcceptUnsignedCallback)) { + s_channelKeyHint[channel] = name; + qCInfo(QGCMAVLinkLog) << "Auto-detected signing key" << name << "on channel" << channel; + return name; + } + } } + + return QString(); } } // namespace MAVLinkSigning diff --git a/src/MAVLink/MAVLinkSigning.h b/src/MAVLink/MAVLinkSigning.h index 4f5947138d35..75b161c5e076 100644 --- a/src/MAVLink/MAVLinkSigning.h +++ b/src/MAVLink/MAVLinkSigning.h @@ -1,15 +1,31 @@ #pragma once #include +#include #include #include "MAVLinkLib.h" namespace MAVLinkSigning { - bool secureConnectionAccceptUnsignedCallback(const mavlink_status_t *s0tatus, uint32_t message_id); - bool insecureConnectionAccceptUnsignedCallback(const mavlink_status_t *s0tatus, uint32_t message_id); + bool secureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id); + bool insecureConnectionAcceptUnsignedCallback(const mavlink_status_t *status, uint32_t message_id); bool initSigning(mavlink_channel_t channel, QByteArrayView key, mavlink_accept_unsigned_t callback); bool checkSigningLinkId(mavlink_channel_t channel, const mavlink_message_t &message); - void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing); + bool isSigningEnabled(mavlink_channel_t channel); + + /// Create a SETUP_SIGNING payload using the given key bytes directly. + void createSetupSigning(mavlink_channel_t channel, mavlink_system_t target_system, QByteArrayView keyBytes, mavlink_setup_signing_t &setup_signing); + void createDisableSigning(mavlink_system_t target_system, mavlink_setup_signing_t &setup_signing); + + /// Returns true if the message has a MAVLink2 signature. + bool isMessageSigned(const mavlink_message_t &message); + + /// Verify a key against a signed message's signature. + bool verifySignature(QByteArrayView key, const mavlink_message_t &message); + + /// Try all stored signing keys against a signed message. If a match is found, + /// configure signing on the channel with that key. + /// Returns the matching key name, or empty string if no match. + QString tryDetectKey(mavlink_channel_t channel, const mavlink_message_t &message); }; // namespace MAVLinkSigning diff --git a/src/MAVLink/MAVLinkSigningKeys.cc b/src/MAVLink/MAVLinkSigningKeys.cc new file mode 100644 index 000000000000..a33a20f2946c --- /dev/null +++ b/src/MAVLink/MAVLinkSigningKeys.cc @@ -0,0 +1,170 @@ +#include "MAVLinkSigningKeys.h" +#include "QGCLoggingCategory.h" +#include "QmlObjectListModel.h" +#include "MultiVehicleManager.h" +#include "Vehicle.h" + +#include +#include + +QGC_LOGGING_CATEGORY(MAVLinkSigningKeysLog, "MAVLink.SigningKeys") + +Q_APPLICATION_STATIC(MAVLinkSigningKeys, _mavlinkSigningKeysInstance); + +// ── MAVLinkSigningKey ────────────────────────────────────────────────────── + +MAVLinkSigningKey::MAVLinkSigningKey(const QString& name, const QByteArray& keyBytes, QObject* parent) + : QObject(parent) + , _name(name) + , _keyBytes(keyBytes) +{ +} + +// ── MAVLinkSigningKeys ───────────────────────────────────────────────────── + +MAVLinkSigningKeys* MAVLinkSigningKeys::instance() +{ + return _mavlinkSigningKeysInstance(); +} + +MAVLinkSigningKeys::MAVLinkSigningKeys(QObject* parent) + : QObject(parent) + , _keys(new QmlObjectListModel(this)) +{ + _load(); + + auto* mvm = MultiVehicleManager::instance(); + connect(mvm, &MultiVehicleManager::vehicleAdded, this, &MAVLinkSigningKeys::_connectVehicle); + connect(mvm, &MultiVehicleManager::vehicleRemoved, this, &MAVLinkSigningKeys::_disconnectVehicle); + + // Wire any vehicles that already exist (in case singleton was created late) + for (int i = 0; i < mvm->vehicles()->count(); ++i) { + _connectVehicle(mvm->vehicles()->value(i)); + } +} + +MAVLinkSigningKeys::~MAVLinkSigningKeys() +{ +} + +bool MAVLinkSigningKeys::isKeyInUse(const QString& name) const +{ + auto* mvm = MultiVehicleManager::instance(); + for (int i = 0; i < mvm->vehicles()->count(); ++i) { + const auto* vehicle = mvm->vehicles()->value(i); + if (vehicle && vehicle->mavlinkSigningKeyName() == name) { + return true; + } + } + return false; +} + +void MAVLinkSigningKeys::_connectVehicle(Vehicle* vehicle) +{ + connect(vehicle, &Vehicle::mavlinkSigningChanged, this, &MAVLinkSigningKeys::keyUsageChanged); + emit keyUsageChanged(); +} + +void MAVLinkSigningKeys::_disconnectVehicle(Vehicle* vehicle) +{ + disconnect(vehicle, &Vehicle::mavlinkSigningChanged, this, &MAVLinkSigningKeys::keyUsageChanged); + emit keyUsageChanged(); +} + +QByteArray MAVLinkSigningKeys::keyBytesAt(int index) const +{ + if (index >= 0 && index < _keys->count()) { + return _keys->value(index)->keyBytes(); + } + return QByteArray(); +} + +QString MAVLinkSigningKeys::keyNameAt(int index) const +{ + if (index >= 0 && index < _keys->count()) { + return _keys->value(index)->name(); + } + return QString(); +} + +QByteArray MAVLinkSigningKeys::keyBytesByName(const QString& name) const +{ + for (int i = 0; i < _keys->count(); ++i) { + const auto* key = _keys->value(i); + if (key->name() == name) { + return key->keyBytes(); + } + } + return QByteArray(); +} + +void MAVLinkSigningKeys::addKey(const QString& name, const QString& passphrase) +{ + if (name.isEmpty() || passphrase.isEmpty()) { + qCWarning(MAVLinkSigningKeysLog) << "Name and passphrase must not be empty"; + return; + } + + // Check for duplicate name + for (int i = 0; i < _keys->count(); ++i) { + if (_keys->value(i)->name() == name) { + qCWarning(MAVLinkSigningKeysLog) << "Key with name already exists:" << name; + return; + } + } + + const QByteArray hash = QCryptographicHash::hash(passphrase.toUtf8(), QCryptographicHash::Sha256); + + auto* key = new MAVLinkSigningKey(name, hash, _keys); + _keys->append(key); + + _save(); + emit keysChanged(); +} + +void MAVLinkSigningKeys::removeKey(int index) +{ + if (index < 0 || index >= _keys->count()) { + return; + } + + _keys->removeAt(index)->deleteLater(); + _save(); + emit keysChanged(); +} + +void MAVLinkSigningKeys::_save() +{ + QSettings settings; + settings.beginGroup(kSettingsGroup); + + settings.beginWriteArray(kKeysArrayKey); + for (int i = 0; i < _keys->count(); ++i) { + settings.setArrayIndex(i); + const auto* key = _keys->value(i); + settings.setValue(kNameKey, key->name()); + settings.setValue(kKeyBytesKey, key->keyBytes().toHex()); + } + settings.endArray(); + + settings.endGroup(); +} + +void MAVLinkSigningKeys::_load() +{ + QSettings settings; + settings.beginGroup(kSettingsGroup); + + const int count = settings.beginReadArray(kKeysArrayKey); + for (int i = 0; i < count; ++i) { + settings.setArrayIndex(i); + const QString name = settings.value(kNameKey).toString(); + const QByteArray keyBytes = QByteArray::fromHex(settings.value(kKeyBytesKey).toByteArray()); + if (!name.isEmpty() && keyBytes.size() == 32) { + _keys->append(new MAVLinkSigningKey(name, keyBytes, _keys)); + } + } + settings.endArray(); + + settings.endGroup(); +} diff --git a/src/MAVLink/MAVLinkSigningKeys.h b/src/MAVLink/MAVLinkSigningKeys.h new file mode 100644 index 000000000000..d76ed11bb658 --- /dev/null +++ b/src/MAVLink/MAVLinkSigningKeys.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +class QmlObjectListModel; +class Vehicle; + +/// A single named signing key entry +class MAVLinkSigningKey : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + Q_PROPERTY(QString name READ name CONSTANT) + +public: + explicit MAVLinkSigningKey(const QString& name, const QByteArray& keyBytes, QObject* parent = nullptr); + + QString name() const { return _name; } + QByteArray keyBytes() const { return _keyBytes; } + +private: + QString _name; + QByteArray _keyBytes; // 32-byte SHA-256 hash +}; + +/// Manages the collection of named MAVLink signing keys. +/// There is no "active key" concept. Keys are stored as a bag. The correct key +/// for each vehicle is auto-detected from incoming signed packets. +class MAVLinkSigningKeys : public QObject +{ + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(QmlObjectListModel* keys READ keys CONSTANT) + +public: + static MAVLinkSigningKeys* instance(); + + explicit MAVLinkSigningKeys(QObject* parent = nullptr); + ~MAVLinkSigningKeys() override; + + QmlObjectListModel* keys() const { return _keys; } + + /// Add a new named key. The passphrase is SHA-256 hashed; only the hash is stored. + Q_INVOKABLE void addKey(const QString& name, const QString& passphrase); + + /// Remove a key by index + Q_INVOKABLE void removeKey(int index); + + /// Returns true if any connected vehicle is using the key with the given name + Q_INVOKABLE bool isKeyInUse(const QString& name) const; + + /// Returns the key bytes for a key at the given index, or empty if invalid + QByteArray keyBytesAt(int index) const; + + /// Returns the key bytes for the key with the given name, or empty if not found + QByteArray keyBytesByName(const QString& name) const; + + /// Returns the key name at the given index, or empty if invalid + QString keyNameAt(int index) const; + +signals: + void keysChanged(); + void keyUsageChanged(); + +private: + void _save(); + void _load(); + void _connectVehicle(Vehicle* vehicle); + void _disconnectVehicle(Vehicle* vehicle); + + QmlObjectListModel* _keys = nullptr; + + static constexpr const char* kSettingsGroup = "MAVLinkSigningKeys"; + static constexpr const char* kKeysArrayKey = "keys"; + static constexpr const char* kNameKey = "name"; + static constexpr const char* kKeyBytesKey = "keyBytes"; +}; + diff --git a/src/MissionManager/CMakeLists.txt b/src/MissionManager/CMakeLists.txt index 3a86a95c481b..5c2d25c40b94 100644 --- a/src/MissionManager/CMakeLists.txt +++ b/src/MissionManager/CMakeLists.txt @@ -37,6 +37,9 @@ target_sources(${CMAKE_PROJECT_NAME} MissionCommandUIInfo.h MissionController.cc MissionController.h + MissionFlightStatus.h + MissionFlightStatusCalculator.cc + MissionFlightStatusCalculator.h MissionItem.cc MissionItem.h MissionManager.cc diff --git a/src/MissionManager/CameraCalc.cc b/src/MissionManager/CameraCalc.cc index 17fe3c72bf57..78522a0b238f 100644 --- a/src/MissionManager/CameraCalc.cc +++ b/src/MissionManager/CameraCalc.cc @@ -8,7 +8,7 @@ CameraCalc::CameraCalc(PlanMasterController* masterController, const QString& settingsGroup, QObject* parent) : CameraSpec (settingsGroup, parent) - , _distanceMode (masterController->missionController()->globalAltitudeModeDefault()) + , _distanceMode (masterController->missionController()->globalAltitudeFrameDefault()) , _knownCameraList (masterController->controllerVehicle()->staticCameraList()) , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CameraCalc.FactMetaData.json"), this)) , _cameraNameFact (settingsGroup, _metaDataMap[cameraNameName]) @@ -110,9 +110,9 @@ void CameraCalc::_cameraNameChanged(void) } _recalcTriggerDistance(); - if (!isManualCamera() && distanceMode() == QGroundControlQmlGlobal::AltitudeModeAbsolute) { + if (!isManualCamera() && distanceMode() == QGroundControlQmlGlobal::AltitudeFrameAbsolute) { // Manual grids support absolute alts whereas nothing else does. Make sure we are not left in absolute - setDistanceMode(QGroundControlQmlGlobal::AltitudeModeRelative); + setDistanceMode(QGroundControlQmlGlobal::AltitudeFrameRelative); } } @@ -203,11 +203,11 @@ bool CameraCalc::load(const QJsonObject& originalJson, bool deprecatedFollowTerr if (version == 1) { // Version 1->2 differences: // - _jsonDistanceToSurfaceRelativeKeyDeprecated changed to distanceMode - // - deprecatedFollowTerrain value was loaded from upper level callers and represents AltitudeModeCalcAboveTerrain. AtitudeModeTerrainFrame was not supported yet. + // - deprecatedFollowTerrain value was loaded from upper level callers and represents AltitudeFrameCalcAboveTerrain. AltitudeFrameTerrain was not supported yet. if (deprecatedFollowTerrain) { - json[distanceModeName] = QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain; + json[distanceModeName] = QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain; } else { - json[distanceModeName] = json[_jsonDistanceToSurfaceRelativeKeyDeprecated].toBool() ? QGroundControlQmlGlobal::AltitudeModeRelative : QGroundControlQmlGlobal::AltitudeModeAbsolute; + json[distanceModeName] = json[_jsonDistanceToSurfaceRelativeKeyDeprecated].toBool() ? QGroundControlQmlGlobal::AltitudeFrameRelative : QGroundControlQmlGlobal::AltitudeFrameAbsolute; } json.remove(_jsonDistanceToSurfaceRelativeKeyDeprecated); version = 2; @@ -235,7 +235,7 @@ bool CameraCalc::load(const QJsonObject& originalJson, bool deprecatedFollowTerr QString canonicalCameraName = _validCanonicalCameraName(json[cameraNameName].toString()); _cameraNameFact.setRawValue(canonicalCameraName); - setDistanceMode(static_cast(json[distanceModeName].toInt())); + setDistanceMode(static_cast(json[distanceModeName].toInt())); _adjustedFootprintSideFact.setRawValue (json[adjustedFootprintSideName].toDouble()); _adjustedFootprintFrontalFact.setRawValue (json[adjustedFootprintFrontalName].toDouble()); @@ -293,10 +293,10 @@ QString CameraCalc::xlatManualCameraName(void) return tr("Manual (no camera specs)"); } -void CameraCalc::setDistanceMode(QGroundControlQmlGlobal::AltMode altMode) +void CameraCalc::setDistanceMode(QGroundControlQmlGlobal::AltitudeFrame altFrame) { - if (altMode != _distanceMode) { - _distanceMode = altMode; + if (altFrame != _distanceMode) { + _distanceMode = altFrame; emit distanceModeChanged(_distanceMode); } } diff --git a/src/MissionManager/CameraCalc.h b/src/MissionManager/CameraCalc.h index 2308e3177f13..054297ceef4c 100644 --- a/src/MissionManager/CameraCalc.h +++ b/src/MissionManager/CameraCalc.h @@ -38,7 +38,7 @@ class CameraCalc : public CameraSpec // grid altitude mode - distanceMode // trigger distance - adjustedFootprintFrontal // transect spacing - adjustedFootprintSide - Q_PROPERTY(QGroundControlQmlGlobal::AltMode distanceMode READ distanceMode WRITE setDistanceMode NOTIFY distanceModeChanged) + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame distanceMode READ distanceMode WRITE setDistanceMode NOTIFY distanceModeChanged) // The following values are calculated from the camera properties Q_PROPERTY(double imageFootprintSide READ imageFootprintSide NOTIFY imageFootprintSideChanged) ///< Size of image size side in meters @@ -69,9 +69,9 @@ class CameraCalc : public CameraSpec bool isCustomCamera (void) const { return _cameraNameFact.rawValue().toString() == canonicalCustomCameraName(); } double imageFootprintSide (void) const { return _imageFootprintSide; } double imageFootprintFrontal (void) const { return _imageFootprintFrontal; } - QGroundControlQmlGlobal::AltMode distanceMode(void) const { return _distanceMode; } + QGroundControlQmlGlobal::AltitudeFrame distanceMode(void) const { return _distanceMode; } - void setDistanceMode (QGroundControlQmlGlobal::AltMode altMode); + void setDistanceMode (QGroundControlQmlGlobal::AltitudeFrame altFrame); void setCameraBrand (const QString& cameraBrand); void setCameraModel (const QString& cameraModel); @@ -93,7 +93,7 @@ class CameraCalc : public CameraSpec signals: void imageFootprintSideChanged (double imageFootprintSide); void imageFootprintFrontalChanged (double imageFootprintFrontal); - void distanceModeChanged (int altMode); + void distanceModeChanged (int altFrame); void isManualCameraChanged (void); void isCustomCameraChanged (void); void cameraBrandChanged (void); @@ -116,7 +116,7 @@ private slots: QString _cameraModel; QStringList _cameraBrandList; QStringList _cameraModelList; - QGroundControlQmlGlobal::AltMode _distanceMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _distanceMode = QGroundControlQmlGlobal::AltitudeFrameRelative; double _imageFootprintSide = 0; double _imageFootprintFrontal = 0; QVariantList _knownCameraList; diff --git a/src/MissionManager/CorridorScanComplexItem.cc b/src/MissionManager/CorridorScanComplexItem.cc index 8a64cb7e5555..91c748cb09d5 100644 --- a/src/MissionManager/CorridorScanComplexItem.cc +++ b/src/MissionManager/CorridorScanComplexItem.cc @@ -18,7 +18,7 @@ CorridorScanComplexItem::CorridorScanComplexItem(PlanMasterController* masterCon , _metaDataMap (FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/CorridorScan.SettingsGroup.json"), this)) , _corridorWidthFact (settingsGroup, _metaDataMap[corridorWidthName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/CorridorScanEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/CorridorScanEditor.qml"; // We override the altitude to the mission default if (_cameraCalc.isManualCamera() || !_cameraCalc.valueSetIsDistance()->rawValue().toBool()) { @@ -154,6 +154,25 @@ bool CorridorScanComplexItem::specifiesCoordinate(void) const return _corridorPolyline.count() > 1; } +void CorridorScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + if (!coordinate.isValid() || !_entryCoordinate.isValid() || _corridorPolyline.count() < 2) { + return; + } + + const double distanceMeters = _entryCoordinate.distanceTo(coordinate); + const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate); + const QList vertices = _corridorPolyline.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _corridorPolyline.setPath(translatedVertices); +} + int CorridorScanComplexItem::_calcTransectCount(void) const { double fullWidth = _corridorWidthFact.rawValue().toDouble(); diff --git a/src/MissionManager/CorridorScanComplexItem.h b/src/MissionManager/CorridorScanComplexItem.h index 4030db4480fa..c061070ad56f 100644 --- a/src/MissionManager/CorridorScanComplexItem.h +++ b/src/MissionManager/CorridorScanComplexItem.h @@ -52,6 +52,7 @@ class CorridorScanComplexItem : public TransectStyleComplexItem QString commandDescription (void) const final { return tr("Corridor Scan"); } QString commandName (void) const final { return tr("Corridor Scan"); } QString abbreviation (void) const final { return tr("C"); } + void setCoordinate (const QGeoCoordinate& coordinate) final; ReadyForSaveState readyForSaveState (void) const final; double additionalTimeDelay (void) const final { return 0; } diff --git a/src/MissionManager/FixedWingLandingComplexItem.cc b/src/MissionManager/FixedWingLandingComplexItem.cc index f65cf7ecb7c9..58c2c325463a 100644 --- a/src/MissionManager/FixedWingLandingComplexItem.cc +++ b/src/MissionManager/FixedWingLandingComplexItem.cc @@ -29,7 +29,7 @@ FixedWingLandingComplexItem::FixedWingLandingComplexItem(PlanMasterController* m , _stopTakingVideoFact (settingsGroup, _metaDataMap[stopTakingVideoName]) , _valueSetIsDistanceFact (settingsGroup, _metaDataMap[valueSetIsDistanceName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/FWLandingPatternEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/FWLandingPatternEditor.qml"; _isIncomplete = false; _init(); diff --git a/src/MissionManager/GeoFenceController.cc b/src/MissionManager/GeoFenceController.cc index 799aedbdb82b..8e60144206a8 100644 --- a/src/MissionManager/GeoFenceController.cc +++ b/src/MissionManager/GeoFenceController.cc @@ -215,9 +215,9 @@ void GeoFenceController::removeAll(void) void GeoFenceController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::removeAllFromVehicle called while syncInProgress"; } else { _geoFenceManager->removeAll(); } @@ -226,9 +226,9 @@ void GeoFenceController::removeAllFromVehicle(void) void GeoFenceController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _geoFenceManager->loadFromVehicle(); @@ -238,9 +238,9 @@ void GeoFenceController::loadFromVehicle(void) void GeoFenceController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while syncInProgress"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle called while syncInProgress"; } else { qCDebug(GeoFenceControllerLog) << "GeoFenceController::sendToVehicle"; _geoFenceManager->sendToVehicle(_breachReturnPoint, _polygons, _circles); @@ -357,7 +357,7 @@ bool GeoFenceController::showPlanFromManagerVehicle(void) { qCDebug(GeoFenceControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(GeoFenceControllerLog) << "GeoFenceController::showPlanFromManagerVehicle called while offline"; + qCCritical(GeoFenceControllerLog) << "GeoFenceController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { _itemsRequested = true; diff --git a/src/MissionManager/LandingComplexItem.cc b/src/MissionManager/LandingComplexItem.cc index a84ba3b85fb8..98aad80b41a9 100644 --- a/src/MissionManager/LandingComplexItem.cc +++ b/src/MissionManager/LandingComplexItem.cc @@ -41,6 +41,12 @@ void LandingComplexItem::_init(void) connect(useLoiterToAlt(), &Fact::rawValueChanged, this, &LandingComplexItem::_recalcFromApproachModeChange); + connect(this, &LandingComplexItem::finalApproachCoordinateChanged,this, &LandingComplexItem::entryCoordinateChanged); + connect(this, &LandingComplexItem::landingCoordinateChanged, this, &LandingComplexItem::exitCoordinateChanged); + + // The main coordinate is aliased to the exit (landing point) because that's the core of this complex item and the master for all transformations + connect(this, &LandingComplexItem::exitCoordinateChanged, this, &LandingComplexItem::coordinateChanged); + connect(this, &LandingComplexItem::finalApproachCoordinateChanged,this, &LandingComplexItem::_recalcFromCoordinateChange); connect(this, &LandingComplexItem::landingCoordinateChanged, this, &LandingComplexItem::_recalcFromCoordinateChange); @@ -109,11 +115,9 @@ void LandingComplexItem::setLandingCoordinate(const QGeoCoordinate& coordinate) if (coordinate != _landingCoordinate) { _landingCoordinate = coordinate; if (_landingCoordSet) { - emit exitCoordinateChanged(coordinate); emit landingCoordinateChanged(coordinate); } else { _ignoreRecalcSignals = true; - emit exitCoordinateChanged(coordinate); emit landingCoordinateChanged(coordinate); _ignoreRecalcSignals = false; _landingCoordSet = true; @@ -127,7 +131,6 @@ void LandingComplexItem::setFinalApproachCoordinate(const QGeoCoordinate& coordi { if (coordinate != _finalApproachCoordinate) { _finalApproachCoordinate = coordinate; - emit coordinateChanged(coordinate); emit finalApproachCoordinateChanged(coordinate); } } @@ -176,7 +179,6 @@ void LandingComplexItem::_recalcFromHeadingAndDistanceChange(void) _ignoreRecalcSignals = true; emit slopeStartCoordinateChanged(_slopeStartCoordinate); emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _calcGlideSlope(); _ignoreRecalcSignals = false; } @@ -219,7 +221,6 @@ void LandingComplexItem::_recalcFromRadiusChange(void) _ignoreRecalcSignals = true; emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _ignoreRecalcSignals = false; } } @@ -252,7 +253,6 @@ void LandingComplexItem::_recalcFromApproachModeChange(void) _ignoreRecalcSignals = true; emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); _calcGlideSlope(); _ignoreRecalcSignals = false; } @@ -656,6 +656,11 @@ void LandingComplexItem::setSequenceNumber(int sequenceNumber) } } +double LandingComplexItem::editableAlt() const +{ + return finalApproachAltitude()->rawValue().toDouble(); +} + double LandingComplexItem::amslEntryAlt(void) const { return finalApproachAltitude()->rawValue().toDouble() + (_altitudesAreRelative ? _missionController->plannedHomePosition().altitude() : 0); @@ -676,7 +681,6 @@ void LandingComplexItem::_updateFinalApproachCoodinateAltitudeFromFact(void) { _finalApproachCoordinate.setAltitude(finalApproachAltitude()->rawValue().toDouble()); emit finalApproachCoordinateChanged(_finalApproachCoordinate); - emit coordinateChanged(_finalApproachCoordinate); } void LandingComplexItem::_updateLandingCoodinateAltitudeFromFact(void) @@ -822,7 +826,9 @@ bool LandingComplexItem::_load(const QJsonObject& complexObject, int sequenceNum _ignoreRecalcSignals = false; _recalcFromCoordinateChange(); - emit coordinateChanged(this->coordinate()); // This will kick off terrain query + // These will kick off terrain query + emit finalApproachCoordinateChanged(_finalApproachCoordinate); + emit landingCoordinateChanged(_landingCoordinate); return true; } diff --git a/src/MissionManager/LandingComplexItem.h b/src/MissionManager/LandingComplexItem.h index edebb2fad4dd..602e7748fc56 100644 --- a/src/MissionManager/LandingComplexItem.h +++ b/src/MissionManager/LandingComplexItem.h @@ -89,7 +89,8 @@ class LandingComplexItem : public ComplexMissionItem QString commandDescription (void) const final { return "Landing Pattern"; } QString commandName (void) const final { return "Landing Pattern"; } QString abbreviation (void) const final { return "L"; } - QGeoCoordinate coordinate (void) const final { return _finalApproachCoordinate; } + QGeoCoordinate coordinate (void) const final { return exitCoordinate(); } + QGeoCoordinate entryCoordinate (void) const final { return _finalApproachCoordinate; } QGeoCoordinate exitCoordinate (void) const final { return _landingCoordinate; } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } @@ -103,6 +104,7 @@ class LandingComplexItem : public ComplexMissionItem void setDirty (bool dirty) final; void setCoordinate (const QGeoCoordinate& coordinate) final; void setSequenceNumber (int sequenceNumber) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final; double minAMSLAltitude (void) const final { return amslExitAlt(); } diff --git a/src/MissionManager/MissionController.cc b/src/MissionManager/MissionController.cc index 9af6fcffff83..73675d9f1baa 100644 --- a/src/MissionManager/MissionController.cc +++ b/src/MissionManager/MissionController.cc @@ -1,5 +1,6 @@ #include "MissionController.h" #include "Vehicle.h" +#include "VehicleSupports.h" #include "MissionManager.h" #include "FlightPathSegment.h" #include "FirmwarePlugin.h" @@ -26,6 +27,7 @@ #include #include +#include #define UPDATE_TIMEOUT 5000 ///< How often we check for bounding box changes @@ -66,35 +68,8 @@ MissionController::~MissionController() void MissionController::_resetMissionFlightStatus(void) { - _missionFlightStatus.totalDistance = 0.0; - _missionFlightStatus.plannedDistance = 0.0; - _missionFlightStatus.maxTelemetryDistance = 0.0; - _missionFlightStatus.totalTime = 0.0; - _missionFlightStatus.hoverTime = 0.0; - _missionFlightStatus.cruiseTime = 0.0; - _missionFlightStatus.hoverDistance = 0.0; - _missionFlightStatus.cruiseDistance = 0.0; - _missionFlightStatus.cruiseSpeed = _controllerVehicle->defaultCruiseSpeed(); - _missionFlightStatus.hoverSpeed = _controllerVehicle->defaultHoverSpeed(); - _missionFlightStatus.vehicleSpeed = _controllerVehicle->multiRotor() || _managerVehicle->vtol() ? _missionFlightStatus.hoverSpeed : _missionFlightStatus.cruiseSpeed; - _missionFlightStatus.vehicleYaw = qQNaN(); - _missionFlightStatus.gimbalYaw = qQNaN(); - _missionFlightStatus.gimbalPitch = qQNaN(); - _missionFlightStatus.mAhBattery = 0; - _missionFlightStatus.hoverAmps = 0; - _missionFlightStatus.cruiseAmps = 0; - _missionFlightStatus.ampMinutesAvailable = 0; - _missionFlightStatus.hoverAmpsTotal = 0; - _missionFlightStatus.cruiseAmpsTotal = 0; - _missionFlightStatus.batteryChangePoint = -1; - _missionFlightStatus.batteriesRequired = -1; - _missionFlightStatus.vtolMode = _missionContainsVTOLTakeoff ? QGCMAVLink::VehicleClassMultiRotor : QGCMAVLink::VehicleClassFixedWing; - - _controllerVehicle->firmwarePlugin()->batteryConsumptionData(_controllerVehicle, _missionFlightStatus.mAhBattery, _missionFlightStatus.hoverAmps, _missionFlightStatus.cruiseAmps); - if (_missionFlightStatus.mAhBattery != 0) { - double batteryPercentRemainingAnnounce = SettingsManager::instance()->appSettings()->batteryPercentRemainingAnnounce()->rawValue().toDouble(); - _missionFlightStatus.ampMinutesAvailable = static_cast(_missionFlightStatus.mAhBattery) / 1000.0 * 60.0 * ((100.0 - batteryPercentRemainingAnnounce) / 100.0); - } + _flightStatusCalc.reset(_controllerVehicle, _managerVehicle, _missionContainsVTOLTakeoff); + _missionFlightStatus = _flightStatusCalc.status(); emit missionPlannedDistanceChanged(_missionFlightStatus.plannedDistance); emit missionTimeChanged(); @@ -105,7 +80,6 @@ void MissionController::_resetMissionFlightStatus(void) emit missionMaxTelemetryChanged(_missionFlightStatus.maxTelemetryDistance); emit batteryChangePointChanged(_missionFlightStatus.batteryChangePoint); emit batteriesRequiredChanged(_missionFlightStatus.batteriesRequired); - } void MissionController::start(bool flyView) @@ -123,6 +97,10 @@ void MissionController::_init(void) { // We start with an empty mission _addMissionSettings(_visualItems); + + // Set up the tree model structure once (groups + static children) + _setupTreeModel(); + _initAllVisualItems(); } @@ -143,29 +121,17 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque // - A load from vehicle was manually requested // - The initial automatic load from a vehicle completed and the current editor is empty - _deinitAllVisualItems(); - _visualItems->clearAndDeleteContents(); - _visualItems->deleteLater(); - _visualItems = nullptr; - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; - _updateContainsItems(); // This will clear containsItems which will be set again below. This will re-pop Start Mission confirmation. + _setupNewVisualItems(); - QmlObjectListModel* newControllerMissionItems = new QmlObjectListModel(this); const QList& newMissionItems = _missionManager->missionItems(); qCDebug(MissionControllerLog) << "loading from vehicle: count"<< newMissionItems.count(); - _missionItemCount = newMissionItems.count(); - emit missionItemCountChanged(_missionItemCount); - - MissionSettingsItem* settingsItem = _addMissionSettings(newControllerMissionItems); - int i=0; if (_controllerVehicle->firmwarePlugin()->sendHomePositionToVehicle() && newMissionItems.count() != 0) { // First item is fake home position MissionItem* fakeHomeItem = newMissionItems[0]; if (fakeHomeItem->coordinate().latitude() != 0 || fakeHomeItem->coordinate().longitude() != 0) { - settingsItem->setInitialHomePosition(fakeHomeItem->coordinate()); + _settingsItem->setCoordinate(fakeHomeItem->coordinate()); } i = 1; } @@ -177,25 +143,24 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque SimpleMissionItem* simpleItem = new SimpleMissionItem(_masterController, _flyView, *missionItem); if (TakeoffMissionItem::isTakeoffCommand(static_cast(simpleItem->command()))) { // This needs to be a TakeoffMissionItem - _takeoffMissionItem = new TakeoffMissionItem(*missionItem, _masterController, _flyView, settingsItem, false /* forLoad */); + _takeoffMissionItem = new TakeoffMissionItem(*missionItem, _masterController, _flyView, _settingsItem, false /* forLoad */); _takeoffMissionItem->setWizardMode(false); simpleItem->deleteLater(); simpleItem = _takeoffMissionItem; } - newControllerMissionItems->append(simpleItem); + _visualItems->append(simpleItem); } - _visualItems = newControllerMissionItems; - _settingsItem = settingsItem; - - // We set Altitude mode to mixed, otherwise if we need a non relative altitude frame we won't be able to change it - setGlobalAltitudeMode(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeModeMixed : QGroundControlQmlGlobal::AltitudeModeRelative); + // We set Altitude frame to mixed, otherwise if we need a non relative altitude frame we won't be able to change it + setGlobalAltitudeFrame(weHaveItemsFromVehicle ? QGroundControlQmlGlobal::AltitudeFrameMixed : QGroundControlQmlGlobal::AltitudeFrameRelative); MissionController::_scanForAdditionalSettings(_visualItems, _masterController); _initAllVisualItems(); - _updateContainsItems(); + emit newItemsFromVehicle(); + + emit containsItemsChanged(); } _itemsRequested = false; } @@ -203,9 +168,9 @@ void MissionController::_newMissionItemsAvailableFromVehicle(bool removeAllReque void MissionController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _managerVehicle->missionManager()->loadFromVehicle(); @@ -215,9 +180,9 @@ void MissionController::loadFromVehicle(void) void MissionController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::sendToVehicle called while syncInProgress"; } else { qCDebug(MissionControllerLog) << "MissionControllerLog::sendToVehicle"; if (_visualItems->count() == 1) { @@ -309,13 +274,13 @@ VisualMissionItem* MissionController::_insertSimpleMissionItemWorker(QGeoCoordin if (newItem->specifiesAltitude()) { if (!MissionCommandTree::instance()->isLandCommand(command)) { double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { newItem->altitude()->setRawValue(prevAltitude); - if (globalAltitudeMode() == QGroundControlQmlGlobal::AltitudeModeMixed) { - // We are in mixed altitude modes, so copy from previous. Otherwise alt mode will be set from global setting. - newItem->setAltitudeMode(static_cast(prevAltMode)); + if (globalAltitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameMixed) { + // We are in mixed altitude frames, so copy from previous. Otherwise altitude frame will be set from global setting. + newItem->setAltitudeFrame(static_cast(prevAltFrame)); } } } @@ -353,11 +318,11 @@ VisualMissionItem* MissionController::insertTakeoffItem(QGeoCoordinate /*coordin if (_takeoffMissionItem->specifiesAltitude()) { double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { _takeoffMissionItem->altitude()->setRawValue(prevAltitude); - _takeoffMissionItem->setAltitudeMode(static_cast(prevAltMode)); + _takeoffMissionItem->setAltitudeFrame(static_cast(prevAltFrame)); } } if (visualItemIndex == -1) { @@ -406,7 +371,6 @@ VisualMissionItem* MissionController::insertROIMissionItem(QGeoCoordinate coordi simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ; simpleItem->missionItem().setParam1(MAV_ROI_LOCATION); } - _recalcROISpecialVisuals(); return simpleItem; } @@ -418,7 +382,6 @@ VisualMissionItem* MissionController::insertCancelROIMissionItem(int visualItemI simpleItem->setCommand(MAV_CMD_DO_SET_ROI) ; simpleItem->missionItem().setParam1(MAV_ROI_NONE); } - _recalcROISpecialVisuals(); return simpleItem; } @@ -431,11 +394,11 @@ VisualMissionItem* MissionController::insertComplexMissionItem(QString itemName, newItem->setCoordinate(mapCenterCoordinate); double prevAltitude; - QGroundControlQmlGlobal::AltMode prevAltMode; - if (globalAltitudeMode() == QGroundControlQmlGlobal::AltitudeModeMixed) { - // We are in mixed altitude modes, so copy from previous. Otherwise alt mode will be set from global setting in constructor. - if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltMode)) { - qobject_cast(newItem)->cameraCalc()->setDistanceMode(prevAltMode); + QGroundControlQmlGlobal::AltitudeFrame prevAltFrame; + if (globalAltitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameMixed) { + // We are in mixed altitude frames, so copy from previous. Otherwise alt mode will be set from global setting in constructor. + if (_findPreviousAltitude(visualItemIndex, &prevAltitude, &prevAltFrame)) { + qobject_cast(newItem)->cameraCalc()->setDistanceMode(prevAltFrame); } } } else if (itemName == FixedWingLandingComplexItem::name) { @@ -533,6 +496,11 @@ void MissionController::_insertComplexMissionItemWorker(const QGeoCoordinate& ma _firstItemAdded(); } +int MissionController::visualItemIndexForObject(QObject* object) const +{ + return _visualItems ? _visualItems->indexOf(object) : -1; +} + void MissionController::removeVisualItem(int viIndex) { if (viIndex <= 0 || viIndex >= _visualItems->count()) { @@ -595,123 +563,44 @@ void MissionController::removeVisualItem(int viIndex) } } -void MissionController::removeAll(void) +void MissionController::_setupNewVisualItems(QmlObjectListModel* newItems) { - if (_visualItems) { - _deinitAllVisualItems(); - _visualItems->clearAndDeleteContents(); - _visualItems->deleteLater(); - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; - _visualItems = new QmlObjectListModel(this); - _addMissionSettings(_visualItems); - _initAllVisualItems(); - setDirty(true); - _resetMissionFlightStatus(); - _allItemsRemoved(); - } -} + QmlObjectListModel* oldItems = _visualItems; -bool MissionController::_loadJsonMissionFileV1(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) -{ - // Validate root object keys - QList rootKeyInfoList = { - { _jsonPlannedHomePositionKey, QJsonValue::Object, true }, - { _jsonItemsKey, QJsonValue::Array, true }, - { _jsonMavAutopilotKey, QJsonValue::Double, false }, - { _jsonComplexItemsKey, QJsonValue::Array, true }, - }; - if (!JsonHelper::validateKeys(json, rootKeyInfoList, errorString)) { - return false; - } - - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); - - // Read complex items - QList surveyItems; - QJsonArray complexArray(json[_jsonComplexItemsKey].toArray()); - qCDebug(MissionControllerLog) << "Json load: complex item count" << complexArray.count(); - for (int i=0; iload(itemObject, itemObject["id"].toInt(), errorString)) { - surveyItems.append(item); - } else { - return false; - } + // Destroy old items after a delay — TreeView delegates are torn down + // asynchronously during a polish cycle and may still hold bindings. + QTimer::singleShot(1000, oldItems, [oldItems] { + oldItems->clearAndDeleteContents(); + oldItems->deleteLater(); + }); } - // Read simple items, interspersing complex items into the full list + _settingsItem = nullptr; + _takeoffMissionItem = nullptr; - int nextSimpleItemIndex= 0; - int nextComplexItemIndex= 0; - int nextSequenceNumber = 1; // Start with 1 since home is in 0 - QJsonArray itemArray(json[_jsonItemsKey].toArray()); - - MissionSettingsItem* settingsItem = _addMissionSettings(visualItems); - if (json.contains(_jsonPlannedHomePositionKey)) { - SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); - if (item->load(json[_jsonPlannedHomePositionKey].toObject(), 0, errorString)) { - settingsItem->setInitialHomePositionFromUser(item->coordinate()); - item->deleteLater(); + if (newItems) { + _visualItems = newItems; + if (_visualItems->count() == 0) { + _addMissionSettings(_visualItems); } else { - return false; + _settingsItem = _visualItems->value(0); } + } else { + _visualItems = new QmlObjectListModel(this); + _addMissionSettings(_visualItems); } +} - qCDebug(MissionControllerLog) << "Json load: simple item loop start simpleItemCount:ComplexItemCount" << itemArray.count() << surveyItems.count(); - do { - qCDebug(MissionControllerLog) << "Json load: simple item loop nextSimpleItemIndex:nextComplexItemIndex:nextSequenceNumber" << nextSimpleItemIndex << nextComplexItemIndex << nextSequenceNumber; - - // If there is a complex item that should be next in sequence add it in - if (nextComplexItemIndex < surveyItems.count()) { - SurveyComplexItem* complexItem = surveyItems[nextComplexItemIndex]; - - if (complexItem->sequenceNumber() == nextSequenceNumber) { - qCDebug(MissionControllerLog) << "Json load: injecting complex item expectedSequence:actualSequence:" << nextSequenceNumber << complexItem->sequenceNumber(); - visualItems->append(complexItem); - nextSequenceNumber = complexItem->lastSequenceNumber() + 1; - nextComplexItemIndex++; - continue; - } - } - - // Add the next available simple item - if (nextSimpleItemIndex < itemArray.count()) { - const QJsonValue& itemValue = itemArray[nextSimpleItemIndex++]; - - if (!itemValue.isObject()) { - errorString = QStringLiteral("Mission item is not an object"); - return false; - } - - const QJsonObject itemObject = itemValue.toObject(); - SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); - if (item->load(itemObject, itemObject["id"].toInt(), errorString)) { - if (TakeoffMissionItem::isTakeoffCommand(item->mavCommand())) { - // This needs to be a TakeoffMissionItem - TakeoffMissionItem* takeoffItem = new TakeoffMissionItem(_masterController, _flyView, settingsItem, true /* forLoad */); - takeoffItem->load(itemObject, itemObject["id"].toInt(), errorString); - item->deleteLater(); - item = takeoffItem; - } - qCDebug(MissionControllerLog) << "Json load: adding simple item expectedSequence:actualSequence" << nextSequenceNumber << item->sequenceNumber(); - nextSequenceNumber = item->lastSequenceNumber() + 1; - visualItems->append(item); - } else { - return false; - } - } - } while (nextSimpleItemIndex < itemArray.count() || nextComplexItemIndex < surveyItems.count()); - - return true; +void MissionController::removeAll(void) +{ + _setupNewVisualItems(); + _initAllVisualItems(); + setDirty(true); + _resetMissionFlightStatus(); + _allItemsRemoved(); } bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) @@ -730,7 +619,7 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec return false; } - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); + setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); qCDebug(MissionControllerLog) << "MissionController::_loadJsonMissionFileV2 itemCount:" << json[_jsonItemsKey].toArray().count(); @@ -763,7 +652,7 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec appSettings->offlineEditingHoverSpeed()->setRawValue(json[_jsonHoverSpeedKey].toDouble()); } if (json.contains(_jsonGlobalPlanAltitudeModeKey)) { - setGlobalAltitudeMode(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value()); + setGlobalAltitudeFrame(json[_jsonGlobalPlanAltitudeModeKey].toVariant().value()); } QGeoCoordinate homeCoordinate; @@ -905,19 +794,6 @@ bool MissionController::_loadJsonMissionFileV2(const QJsonObject& json, QmlObjec return true; } -bool MissionController::_loadItemsFromJson(const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString) -{ - int fileVersion; - JsonHelper::validateExternalQGCJsonFile(json, - _jsonFileTypeValue, // expected file type - 2, // minimum supported version - 2, // maximum supported version - fileVersion, - errorString); - - return _loadJsonMissionFileV2(json, visualItems, errorString); -} - bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString) { bool firstItem = true; @@ -946,7 +822,7 @@ bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListM SimpleMissionItem* item = new SimpleMissionItem(_masterController, _flyView, true /* forLoad */); if (item->load(stream)) { if (firstItem && plannedHomePositionInFile) { - settingsItem->setInitialHomePositionFromUser(item->coordinate()); + settingsItem->setCoordinate(item->coordinate()); } else { if (TakeoffMissionItem::isTakeoffCommand(static_cast(item->command()))) { // This needs to be a TakeoffMissionItem @@ -982,20 +858,7 @@ bool MissionController::_loadTextMissionFile(QTextStream& stream, QmlObjectListM void MissionController::_initLoadedVisualItems(QmlObjectListModel* loadedVisualItems) { - if (_visualItems) { - _deinitAllVisualItems(); - _visualItems->deleteLater(); - } - _settingsItem = nullptr; - _takeoffMissionItem = nullptr; - - _visualItems = loadedVisualItems; - - if (_visualItems->count() == 0) { - _addMissionSettings(_visualItems); - } else { - _settingsItem = _visualItems->value(0); - } + _setupNewVisualItems(loadedVisualItems); MissionController::_scanForAdditionalSettings(_visualItems, _masterController); @@ -1030,7 +893,7 @@ bool MissionController::loadTextFile(QFile& file, QString& errorString) QByteArray bytes = file.readAll(); QTextStream stream(bytes); - setGlobalAltitudeMode(QGroundControlQmlGlobal::AltitudeModeMixed); + setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameMixed); QmlObjectListModel* loadedVisualItems = new QmlObjectListModel(this); if (!_loadTextMissionFile(stream, loadedVisualItems, errorStr)) { @@ -1073,7 +936,7 @@ void MissionController::save(QJsonObject& json) json[_jsonVehicleTypeKey] = _controllerVehicle->vehicleType(); json[_jsonCruiseSpeedKey] = _controllerVehicle->defaultCruiseSpeed(); json[_jsonHoverSpeedKey] = _controllerVehicle->defaultHoverSpeed(); - json[_jsonGlobalPlanAltitudeModeKey] = _globalAltMode; + json[_jsonGlobalPlanAltitudeModeKey] = _globalAltFrame; // Save the visual items @@ -1102,29 +965,6 @@ void MissionController::save(QJsonObject& json) json[_jsonItemsKey] = rgJsonMissionItems; } -void MissionController::_calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference) -{ - QGeoCoordinate currentCoord = currentItem->coordinate(); - QGeoCoordinate prevCoord = prevItem->exitCoordinate(); - - // Convert to fixed altitudes - - *altDifference = currentItem->amslEntryAlt() - prevItem->amslExitAlt(); - *distance = prevCoord.distanceTo(currentCoord); - *azimuth = prevCoord.azimuthTo(currentCoord); -} - -double MissionController::_calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem) -{ - QGeoCoordinate currentCoord = currentItem->coordinate(); - QGeoCoordinate homeCoord = homeItem->exitCoordinate(); - bool distanceOk = false; - - distanceOk = true; - - return distanceOk ? homeCoord.distanceTo(currentCoord) : 0.0; -} - FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemPair& pair, bool mavlinkTerrainFrame) { // The takeoff goes straight up from ground to alt and then over to specified position at same alt. Which means @@ -1132,7 +972,7 @@ FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemP bool takeoffStraightUp = pair.second->isTakeoffItem() && !_controllerVehicle->fixedWing(); QGeoCoordinate coord1 = pair.first->exitCoordinate(); - QGeoCoordinate coord2 = pair.second->coordinate(); + QGeoCoordinate coord2 = pair.second->entryCoordinate(); double coord2AMSLAlt = pair.second->amslEntryAlt(); double coord1AMSLAlt = takeoffStraightUp ? coord2AMSLAlt : pair.first->amslExitAlt(); @@ -1150,11 +990,12 @@ FlightPathSegment* MissionController::_createFlightPathSegmentWorker(VisualItemP } else { connect(pair.first, &VisualMissionItem::amslExitAltChanged, segment, &FlightPathSegment::setCoord1AMSLAlt); } - connect(pair.first, &VisualMissionItem::exitCoordinateChanged, segment, &FlightPathSegment::setCoordinate1); - connect(pair.second, &VisualMissionItem::coordinateChanged, segment, &FlightPathSegment::setCoordinate2); - connect(pair.second, &VisualMissionItem::amslEntryAltChanged, segment, &FlightPathSegment::setCoord2AMSLAlt); + connect(pair.first, &VisualMissionItem::exitCoordinateChanged, segment, &FlightPathSegment::setCoordinate1); + connect(pair.second, &VisualMissionItem::entryCoordinateChanged, segment, &FlightPathSegment::setCoordinate2); + connect(pair.second, &VisualMissionItem::amslEntryAltChanged, segment, &FlightPathSegment::setCoord2AMSLAlt); - connect(pair.second, &VisualMissionItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); + connect(pair.second, &VisualMissionItem::entryCoordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); + connect(pair.second, &VisualMissionItem::exitCoordinateChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); connect(segment, &FlightPathSegment::totalDistanceChanged, this, &MissionController::recalcTerrainProfile, Qt::QueuedConnection); connect(segment, &FlightPathSegment::coord1AMSLAltChanged, this, &MissionController::_recalcMissionFlightStatusSignal, Qt::QueuedConnection); @@ -1182,39 +1023,6 @@ FlightPathSegment* MissionController::_addFlightPathSegment(FlightPathSegmentHas return segment; } -void MissionController::_recalcROISpecialVisuals(void) -{ - return; - VisualMissionItem* lastCoordinateItem = qobject_cast(_visualItems->get(0)); - bool roiActive = false; - - for (int i=1; i<_visualItems->count(); i++) { - VisualMissionItem* visualItem = qobject_cast(_visualItems->get(i)); - SimpleMissionItem* simpleItem = qobject_cast(visualItem); - VisualItemPair viPair; - - if (simpleItem) { - if (roiActive) { - if (_isROICancelItem(simpleItem)) { - roiActive = false; - } - } else { - if (_isROIBeginItem(simpleItem)) { - roiActive = true; - } - } - } - - if (visualItem->specifiesCoordinate() && !visualItem->isStandaloneCoordinate()) { - viPair = VisualItemPair(lastCoordinateItem, visualItem); - if (_flightPathSegmentHashTable.contains(viPair)) { - _flightPathSegmentHashTable[viPair]->setSpecialVisual(roiActive); - } - lastCoordinateItem = visualItem; - } - } -} - void MissionController::_recalcFlightPathSegments(void) { VisualItemPair lastSegmentVisualItemPair; @@ -1376,9 +1184,9 @@ void MissionController::_recalcFlightPathSegments(void) // Create a new segment. Since this is the fly view there is no need to wire change signals or worry about correct SegmentType coordVector = new FlightPathSegment( FlightPathSegment::SegmentTypeGeneric, - lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->coordinate() : lastSegmentVisualItemPair.first->exitCoordinate(), + lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->entryCoordinate() : lastSegmentVisualItemPair.first->exitCoordinate(), lastSegmentVisualItemPair.first->isSimpleItem() ? lastSegmentVisualItemPair.first->amslEntryAlt() : lastSegmentVisualItemPair.first->amslExitAlt(), - lastSegmentVisualItemPair.second->coordinate(), + lastSegmentVisualItemPair.second->entryCoordinate(), lastSegmentVisualItemPair.second->amslEntryAlt(), !_flyView /* queryTerrainData */, this); @@ -1402,317 +1210,18 @@ void MissionController::_recalcFlightPathSegments(void) } } -void MissionController::_updateBatteryInfo(int waypointIndex) -{ - if (_missionFlightStatus.mAhBattery != 0) { - _missionFlightStatus.hoverAmpsTotal = (_missionFlightStatus.hoverTime / 60.0) * _missionFlightStatus.hoverAmps; - _missionFlightStatus.cruiseAmpsTotal = (_missionFlightStatus.cruiseTime / 60.0) * _missionFlightStatus.cruiseAmps; - _missionFlightStatus.batteriesRequired = ceil((_missionFlightStatus.hoverAmpsTotal + _missionFlightStatus.cruiseAmpsTotal) / _missionFlightStatus.ampMinutesAvailable); - // FIXME: Battery change point code pretty much doesn't work. The reason is that is treats complex items as a black box. It needs to be able to look - // inside complex items in order to determine a swap point that is interior to a complex item. Current the swap point display in PlanToolbar is - // disabled to do this problem. - if (waypointIndex != -1 && _missionFlightStatus.batteriesRequired == 2 && _missionFlightStatus.batteryChangePoint == -1) { - _missionFlightStatus.batteryChangePoint = waypointIndex - 1; - } - } -} - -void MissionController::_addHoverTime(double hoverTime, double hoverDistance, int waypointIndex) -{ - _missionFlightStatus.totalTime += hoverTime; - _missionFlightStatus.hoverTime += hoverTime; - _missionFlightStatus.hoverDistance += hoverDistance; - _missionFlightStatus.plannedDistance += hoverDistance; - _updateBatteryInfo(waypointIndex); -} - -void MissionController::_addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex) -{ - _missionFlightStatus.totalTime += cruiseTime; - _missionFlightStatus.cruiseTime += cruiseTime; - _missionFlightStatus.cruiseDistance += cruiseDistance; - _missionFlightStatus.plannedDistance += cruiseDistance; - _updateBatteryInfo(waypointIndex); -} - -/// Adds the specified time to the appropriate hover or cruise time values. -/// @param vtolInHover true: vtol is currrent in hover mode -/// @param hoverTime Amount of time tp add to hover -/// @param cruiseTime Amount of time to add to cruise -/// @param extraTime Amount of additional time to add to hover/cruise -/// @param seqNum Sequence number of waypoint for these values, -1 for no waypoint associated -void MissionController::_addTimeDistance(bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum) -{ - if (_controllerVehicle->vtol()) { - if (vtolInHover) { - _addHoverTime(hoverTime, distance, seqNum); - _addHoverTime(extraTime, 0, -1); - } else { - _addCruiseTime(cruiseTime, distance, seqNum); - _addCruiseTime(extraTime, 0, -1); - } - } else { - if (_controllerVehicle->multiRotor()) { - _addHoverTime(hoverTime, distance, seqNum); - _addHoverTime(extraTime, 0, -1); - } else { - _addCruiseTime(cruiseTime, distance, seqNum); - _addCruiseTime(extraTime, 0, -1); - } - } -} - void MissionController::_recalcMissionFlightStatus() { if (!_visualItems->count()) { return; } - bool firstCoordinateItem = true; - VisualMissionItem* lastFlyThroughVI = qobject_cast(_visualItems->get(0)); - - bool homePositionValid = _settingsItem->coordinate().isValid(); - qCDebug(MissionControllerLog) << "_recalcMissionFlightStatus"; - // If home position is valid we can calculate distances between all waypoints. - // If home position is not valid we can only calculate distances between waypoints which are - // both relative altitude. - - // No values for first item - lastFlyThroughVI->setAltDifference(0); - lastFlyThroughVI->setAzimuth(0); - lastFlyThroughVI->setDistance(0); - lastFlyThroughVI->setDistanceFromStart(0); - - _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); - - _resetMissionFlightStatus(); - - bool linkStartToHome = false; - bool foundRTL = false; - bool pastLandCommand = false; - double totalHorizontalDistance = 0; - - for (int i=0; i<_visualItems->count(); i++) { - VisualMissionItem* item = qobject_cast(_visualItems->get(i)); - SimpleMissionItem* simpleItem = qobject_cast(item); - ComplexMissionItem* complexItem = qobject_cast(item); - - if (simpleItem && simpleItem->mavCommand() == MAV_CMD_NAV_RETURN_TO_LAUNCH) { - foundRTL = true; - } - - // Assume the worst - item->setAzimuth(0); - item->setDistance(0); - item->setDistanceFromStart(0); - - // Gimbal states reflect the state AFTER executing the item - - // ROI commands cancel out previous gimbal yaw/pitch - if (simpleItem) { - switch (simpleItem->command()) { - case MAV_CMD_NAV_ROI: - case MAV_CMD_DO_SET_ROI_LOCATION: - case MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET: - case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW: - _missionFlightStatus.gimbalYaw = qQNaN(); - _missionFlightStatus.gimbalPitch = qQNaN(); - break; - default: - break; - } - } - - // Look for specific gimbal changes - double gimbalYaw = item->specifiedGimbalYaw(); - if (!qIsNaN(gimbalYaw) || _planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { - _missionFlightStatus.gimbalYaw = gimbalYaw; - } - double gimbalPitch = item->specifiedGimbalPitch(); - if (!qIsNaN(gimbalPitch) || _planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { - _missionFlightStatus.gimbalPitch = gimbalPitch; - } - - // We don't need to do any more processing if: - // Mission Settings Item - // We are after an RTL command - if (i != 0 && !foundRTL) { - // We must set the mission flight status prior to querying for any values from the item. This is because things like - // current speed, gimbal, vtol state impact the values. - item->setMissionFlightStatus(_missionFlightStatus); - - // Link back to home if first item is takeoff and we have home position - if (firstCoordinateItem && simpleItem && (simpleItem->mavCommand() == MAV_CMD_NAV_TAKEOFF || simpleItem->mavCommand() == MAV_CMD_NAV_VTOL_TAKEOFF)) { - if (homePositionValid) { - linkStartToHome = true; - if (_controllerVehicle->multiRotor() || _controllerVehicle->vtol()) { - // We have to special case takeoff, assuming vehicle takes off straight up to specified altitude - double azimuth, distance, altDifference; - _calcPrevWaypointValues(_settingsItem, simpleItem, &azimuth, &distance, &altDifference); - double takeoffTime = qAbs(altDifference) / _appSettings->offlineEditingAscentSpeed()->rawValue().toDouble(); - _addHoverTime(takeoffTime, 0, -1); - } - } - } - - if (!pastLandCommand) - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, 0, 0, item->additionalTimeDelay(), 0, -1); - - if (item->specifiesCoordinate()) { - - // Keep track of the min/max AMSL altitude for entire mission so we can calculate altitude percentages in terrain status display - if (simpleItem) { - double amslAltitude = item->amslEntryAlt(); - _minAMSLAltitude = std::fmin(_minAMSLAltitude, amslAltitude); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, amslAltitude); - } else { - // Complex item - double complexMinAMSLAltitude = complexItem->minAMSLAltitude(); - double complexMaxAMSLAltitude = complexItem->maxAMSLAltitude(); - _minAMSLAltitude = std::fmin(_minAMSLAltitude, complexMinAMSLAltitude); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, complexMaxAMSLAltitude); - } - - if (!item->isStandaloneCoordinate()) { - firstCoordinateItem = false; - - // Update vehicle yaw assuming direction to next waypoint and/or mission item change - if (simpleItem) { - double newVehicleYaw = simpleItem->specifiedVehicleYaw(); - if (qIsNaN(newVehicleYaw)) { - // No specific vehicle yaw set. Current vehicle yaw is determined from flight path segment direction. - if (simpleItem != lastFlyThroughVI) { - _missionFlightStatus.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->coordinate()); - } - } else { - _missionFlightStatus.vehicleYaw = newVehicleYaw; - } - simpleItem->setMissionVehicleYaw(_missionFlightStatus.vehicleYaw); - } - - if (lastFlyThroughVI != _settingsItem || linkStartToHome) { - // This is a subsequent waypoint or we are forcing the first waypoint back to home - double azimuth, distance, altDifference; - - _calcPrevWaypointValues(item, lastFlyThroughVI, &azimuth, &distance, &altDifference); - - // If the last waypoint was a land command, there's a discontinuity at this point - if (!lastFlyThroughVI->isLandCommand()) { - totalHorizontalDistance += distance; - item->setDistance(distance); - - if (!pastLandCommand) { - // Calculate time/distance - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); - } - } - - item->setAltDifference(altDifference); - item->setAzimuth(azimuth); - item->setDistanceFromStart(totalHorizontalDistance); - - _missionFlightStatus.maxTelemetryDistance = qMax(_missionFlightStatus.maxTelemetryDistance, _calcDistanceToHome(item, _settingsItem)); - } - - if (complexItem) { - // Add in distance/time inside complex items as well - double distance = complexItem->complexDistance(); - _missionFlightStatus.maxTelemetryDistance = qMax(_missionFlightStatus.maxTelemetryDistance, complexItem->greatestDistanceTo(complexItem->exitCoordinate())); - - if (!pastLandCommand) { - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); - } - - totalHorizontalDistance += distance; - } - - - lastFlyThroughVI = item; - } - } - } - - // Speed, VTOL states changes are processed last since they take affect on the next item - - double newSpeed = item->specifiedFlightSpeed(); - if (!qIsNaN(newSpeed)) { - if (_controllerVehicle->multiRotor()) { - _missionFlightStatus.hoverSpeed = newSpeed; - } else if (_controllerVehicle->vtol()) { - if (_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor) { - _missionFlightStatus.hoverSpeed = newSpeed; - } else { - _missionFlightStatus.cruiseSpeed = newSpeed; - } - } else { - _missionFlightStatus.cruiseSpeed = newSpeed; - } - _missionFlightStatus.vehicleSpeed = newSpeed; - } - - // Update VTOL state - if (simpleItem && _controllerVehicle->vtol()) { - switch (simpleItem->command()) { - case MAV_CMD_NAV_TAKEOFF: // This will do a fixed wing style takeoff - case MAV_CMD_NAV_VTOL_TAKEOFF: // Vehicle goes straight up and then transitions to FW - case MAV_CMD_NAV_LAND: - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassFixedWing; - break; - case MAV_CMD_NAV_VTOL_LAND: - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassMultiRotor; - break; - case MAV_CMD_DO_VTOL_TRANSITION: - { - int transitionState = simpleItem->missionItem().param1(); - if (transitionState == MAV_VTOL_STATE_MC) { - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassMultiRotor; - } else if (transitionState == MAV_VTOL_STATE_FW) { - _missionFlightStatus.vtolMode = QGCMAVLink::VehicleClassFixedWing; - } - } - break; - default: - break; - } - } - - if (item->isLandCommand()) { - pastLandCommand = true; - } - } - lastFlyThroughVI->setMissionVehicleYaw(_missionFlightStatus.vehicleYaw); - - // Add the information for the final segment back to home - if (foundRTL && lastFlyThroughVI != _settingsItem && homePositionValid) { - double azimuth, distance, altDifference; - _calcPrevWaypointValues(lastFlyThroughVI, _settingsItem, &azimuth, &distance, &altDifference); - - if (!pastLandCommand) { - // Calculate time/distance - double hoverTime = distance / _missionFlightStatus.hoverSpeed; - double cruiseTime = distance / _missionFlightStatus.cruiseSpeed; - double landTime = qAbs(altDifference) / _appSettings->offlineEditingDescentSpeed()->rawValue().toDouble(); - _addTimeDistance(_missionFlightStatus.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, landTime, distance, -1); - } - } - - _missionFlightStatus.totalDistance = totalHorizontalDistance; - - if (_missionFlightStatus.mAhBattery != 0 && _missionFlightStatus.batteryChangePoint == -1) { - _missionFlightStatus.batteryChangePoint = 0; - } - - if (linkStartToHome) { - // Home position is taken into account for min/max values - _minAMSLAltitude = std::fmin(_minAMSLAltitude, _settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); - _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, _settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); - } + _flightStatusCalc.recalc(_visualItems, _settingsItem, _controllerVehicle, _managerVehicle, _appSettings, _planViewSettings, _missionContainsVTOLTakeoff); + _missionFlightStatus = _flightStatusCalc.status(); + _minAMSLAltitude = _flightStatusCalc.minAMSLAltitude(); + _maxAMSLAltitude = _flightStatusCalc.maxAMSLAltitude(); emit missionMaxTelemetryChanged (_missionFlightStatus.maxTelemetryDistance); emit missionTotalDistanceChanged (_missionFlightStatus.totalDistance); @@ -1727,31 +1236,6 @@ void MissionController::_recalcMissionFlightStatus() emit minAMSLAltitudeChanged (_minAMSLAltitude); emit maxAMSLAltitudeChanged (_maxAMSLAltitude); - // Walk the list again calculating altitude percentages - double altRange = _maxAMSLAltitude - _minAMSLAltitude; - for (int i=0; i<_visualItems->count(); i++) { - VisualMissionItem* item = qobject_cast(_visualItems->get(i)); - - if (item->specifiesCoordinate()) { - double amslAlt = item->amslEntryAlt(); - if (altRange == 0.0) { - item->setAltPercent(0.0); - item->setTerrainPercent(qQNaN()); - item->setTerrainCollision(false); - } else { - item->setAltPercent((amslAlt - _minAMSLAltitude) / altRange); - double terrainAltitude = item->terrainAltitude(); - if (qIsNaN(terrainAltitude)) { - item->setTerrainPercent(qQNaN()); - item->setTerrainCollision(false); - } else { - item->setTerrainPercent((terrainAltitude - _minAMSLAltitude) / altRange); - item->setTerrainCollision(amslAlt < terrainAltitude); - } - } - } - } - _updateTimer.start(UPDATE_TIMEOUT); emit recalcTerrainProfile(); @@ -1804,6 +1288,175 @@ void MissionController::_recalcChildItems(void) } } +void MissionController::_setupTreeModel(void) +{ + _visualItemsTree.beginResetModel(); + + _visualItemsTree.clear(); + + // ── Plan File group ── + _planFileGroupNode.setObjectName(tr("Plan Info")); + _visualItemsTree.appendItem(&_planFileGroupNode, QModelIndex(), QStringLiteral("planFileGroup")); + + _planFileInfoMarker.setObjectName(QStringLiteral("planFileInfo")); + _visualItemsTree.appendItem(&_planFileInfoMarker, + _visualItemsTree.indexForObject(&_planFileGroupNode), QStringLiteral("planFileInfo")); + + // ── Defaults group ── + _defaultsGroupNode.setObjectName(tr("Defaults")); + _visualItemsTree.appendItem(&_defaultsGroupNode, QModelIndex(), QStringLiteral("defaultsGroup")); + + _defaultsInfoMarker.setObjectName(QStringLiteral("defaultsInfo")); + _visualItemsTree.appendItem(&_defaultsInfoMarker, + _visualItemsTree.indexForObject(&_defaultsGroupNode), QStringLiteral("defaultsInfo")); + + // ── Mission Items group ── + _missionItemsGroupNode.setObjectName(tr("Mission Items")); + _visualItemsTree.appendItem(&_missionItemsGroupNode, QModelIndex(), QStringLiteral("missionGroup")); + + // ── GeoFence group ── + _fenceGroupNode.setObjectName(tr("GeoFence")); + _visualItemsTree.appendItem(&_fenceGroupNode, QModelIndex(), QStringLiteral("fenceGroup")); + + // Single marker child — delegate loads the full GeoFenceEditor + _fenceEditorMarker.setObjectName(QStringLiteral("fenceEditor")); + _visualItemsTree.appendItem(&_fenceEditorMarker, + _visualItemsTree.indexForObject(&_fenceGroupNode), QStringLiteral("fenceEditor")); + + // ── Rally Points group ── + _rallyGroupNode.setObjectName(tr("Rally Points")); + _visualItemsTree.appendItem(&_rallyGroupNode, QModelIndex(), QStringLiteral("rallyGroup")); + + // Marker child for the rally header / instructions + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, + _visualItemsTree.indexForObject(&_rallyGroupNode), QStringLiteral("rallyHeader")); + + // ── Transform group ── + _transformGroupNode.setObjectName(tr("Transform")); + _visualItemsTree.appendItem(&_transformGroupNode, QModelIndex(), QStringLiteral("transformGroup")); + + // Single marker child — delegate loads the inline TransformEditor + _transformEditorMarker.setObjectName(QStringLiteral("transformEditor")); + _visualItemsTree.appendItem(&_transformEditorMarker, + _visualItemsTree.indexForObject(&_transformGroupNode), QStringLiteral("transformEditor")); + + _visualItemsTree.endResetModel(); + + // Capture persistent indexes after the reset so they aren't invalidated + _planFileGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_planFileGroupNode)); + _defaultsGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_defaultsGroupNode)); + _missionGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_missionItemsGroupNode)); + _fenceGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_fenceGroupNode)); + _rallyGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_rallyGroupNode)); + _transformGroupIndex = QPersistentModelIndex(_visualItemsTree.indexForObject(&_transformGroupNode)); +} + +//----------------------------------------------------------------------------- +// Incremental tree model sync — Mission Items +//----------------------------------------------------------------------------- + +void MissionController::_syncTreeMissionItemsInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + for (int i = first; i <= last; i++) { + auto* item = _visualItems->value(i); + if (item) { + _visualItemsTree.insertItem(i, item, _missionGroupIndex, QStringLiteral("missionItem")); + } + } +} + +void MissionController::_syncTreeMissionItemsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + // Remove in reverse order so indexes stay valid + for (int i = last; i >= first; i--) { + _visualItemsTree.removeAt(_missionGroupIndex, i); + } +} + +void MissionController::_syncTreeMissionItemsReset(void) +{ + // removeChildren + appendItem each fire their own row-level signals scoped + // to _missionGroupIndex, so persistent indexes for other groups survive. + _visualItemsTree.removeChildren(_missionGroupIndex); + + if (_visualItems) { + for (int i = 0; i < _visualItems->count(); i++) { + auto* item = _visualItems->value(i); + if (item) { + _visualItemsTree.appendItem(item, _missionGroupIndex, QStringLiteral("missionItem")); + } + } + } +} + +//----------------------------------------------------------------------------- +// Incremental tree model sync — Rally Points +//----------------------------------------------------------------------------- + +void MissionController::_syncTreeRallyPointsInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + auto* rallyController = _masterController->rallyPointController(); + if (!rallyController) return; + + // If this is the first rally point, remove the header marker + if (first == 0 && _visualItemsTree.rowCount(_rallyGroupIndex) == 1) { + const QModelIndex child = _visualItemsTree.index(0, 0, _rallyGroupIndex); + if (_visualItemsTree.data(child, QmlObjectTreeModel::NodeTypeRole).toString() == QStringLiteral("rallyHeader")) { + _visualItemsTree.removeItem(child); + } + } + + auto* pts = rallyController->points(); + for (int i = first; i <= last; i++) { + _visualItemsTree.insertItem(i, (*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); + } +} + +void MissionController::_syncTreeRallyPointsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + for (int i = last; i >= first; i--) { + _visualItemsTree.removeAt(_rallyGroupIndex, i); + } +} + +void MissionController::_syncTreeRallyPointsRemoved(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent); + Q_UNUSED(first); + Q_UNUSED(last); + + // Re-add the header marker once the source model has finished removing rows + auto* rallyController = _masterController->rallyPointController(); + if (rallyController && rallyController->points()->count() == 0) { + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); + } +} + +void MissionController::_syncTreeRallyPointsReset(void) +{ + // removeChildren + appendItem each fire their own row-level signals scoped + // to _rallyGroupIndex, so persistent indexes for other groups survive. + _visualItemsTree.removeChildren(_rallyGroupIndex); + + // Repopulate — either rally items or the header marker + auto* rallyController = _masterController->rallyPointController(); + if (rallyController && rallyController->points()->count() > 0) { + auto* pts = rallyController->points(); + for (int i = 0; i < pts->count(); i++) { + _visualItemsTree.appendItem((*pts)[i], _rallyGroupIndex, QStringLiteral("rallyItem")); + } + } else { + _rallyHeaderMarker.setObjectName(QStringLiteral("rallyHeader")); + _visualItemsTree.appendItem(&_rallyHeaderMarker, _rallyGroupIndex, QStringLiteral("rallyHeader")); + } +} + void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate) { bool foundFirstCoordinate = false; @@ -1817,9 +1470,9 @@ void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoo for (int i=1; i<_visualItems->count(); i++) { VisualMissionItem* item = _visualItems->value(i); - if (item->specifiesCoordinate() && item->coordinate().isValid()) { + if (item->specifiesCoordinate() && item->entryCoordinate().isValid()) { foundFirstCoordinate = true; - firstCoordinate = item->coordinate(); + firstCoordinate = item->entryCoordinate(); break; } } @@ -1832,7 +1485,7 @@ void MissionController::_setPlannedHomePositionFromFirstCoordinate(const QGeoCoo if (firstCoordinate.isValid()) { QGeoCoordinate plannedHomeCoord = firstCoordinate.atDistanceAndAzimuth(30, 0); plannedHomeCoord.setAltitude(0); - _settingsItem->setInitialHomePositionFromUser(plannedHomeCoord); + _settingsItem->setCoordinate(plannedHomeCoord); } } @@ -1882,11 +1535,34 @@ void MissionController::_initAllVisualItems(void) _recalcAll(); connect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); - connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::containsItemsChanged); + connect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::missionItemCountChanged); + + // Connect for incremental tree model sync + connect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); + connect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved); + connect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset); + + // Populate tree's mission group from current _visualItems + _syncTreeMissionItemsReset(); + + // Connect rally controller for incremental tree model sync + auto* rallyController = _masterController->rallyPointController(); + if (rallyController) { + auto* pts = rallyController->points(); + connect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted); + connect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved); + connect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved); + connect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset); + + // Populate any existing rally points + _syncTreeRallyPointsReset(); + } emit visualItemsChanged(); emit containsItemsChanged(); emit plannedHomePositionChanged(plannedHomePosition()); + emit missionItemCountChanged(); if (!_flyView) { setCurrentPlanViewSeqNum(0, true); @@ -1897,15 +1573,31 @@ void MissionController::_initAllVisualItems(void) void MissionController::_deinitAllVisualItems(void) { - disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcAll); + disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::_recalcMissionFlightStatus); disconnect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &MissionController::plannedHomePositionChanged); for (int i=0; i<_visualItems->count(); i++) { _deinitVisualItem(qobject_cast(_visualItems->get(i))); } - disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::dirtyChanged); - disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::_updateContainsItems); + disconnect(_visualItems, &QmlObjectListModel::dirtyChanged, this, &MissionController::_visualItemsDirtyChanged); + disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::containsItemsChanged); + disconnect(_visualItems, &QmlObjectListModel::countChanged, this, &MissionController::missionItemCountChanged); + + // Disconnect incremental tree model sync + disconnect(_visualItems, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeMissionItemsInserted); + disconnect(_visualItems, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeMissionItemsAboutToBeRemoved); + disconnect(_visualItems, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeMissionItemsReset); + + // Disconnect rally controller tree model sync + auto* rallyController = _masterController->rallyPointController(); + if (rallyController) { + auto* pts = rallyController->points(); + disconnect(pts, &QAbstractItemModel::rowsInserted, this, &MissionController::_syncTreeRallyPointsInserted); + disconnect(pts, &QAbstractItemModel::rowsAboutToBeRemoved, this, &MissionController::_syncTreeRallyPointsAboutToBeRemoved); + disconnect(pts, &QAbstractItemModel::rowsRemoved, this, &MissionController::_syncTreeRallyPointsRemoved); + disconnect(pts, &QAbstractItemModel::modelReset, this, &MissionController::_syncTreeRallyPointsReset); + } } void MissionController::_initVisualItem(VisualMissionItem* visualItem) @@ -1946,8 +1638,10 @@ void MissionController::_initVisualItem(VisualMissionItem* visualItem) void MissionController::_deinitVisualItem(VisualMissionItem* visualItem) { - // Disconnect all signals - disconnect(visualItem, nullptr, nullptr, nullptr); + // Disconnect only signals connected to this MissionController, not all receivers. + // A full wildcard disconnect(obj, 0, 0, 0) tears out internal destroyed-signal + // connections that Qt (and the tree/list models) rely on for cleanup. + disconnect(visualItem, nullptr, this, nullptr); } void MissionController::_itemCommandChanged(void) @@ -1994,11 +1688,11 @@ void MissionController::_inProgressChanged(bool inProgress) emit syncInProgressChanged(inProgress); } -bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltMode* prevAltitudeMode) +bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltitudeMode) { bool found = false; double foundAltitude = 0; - QGroundControlQmlGlobal::AltMode foundAltMode = QGroundControlQmlGlobal::AltitudeModeNone; + QGroundControlQmlGlobal::AltitudeFrame foundAltFrame = QGroundControlQmlGlobal::AltitudeFrameNone; if (newIndex > _visualItems->count()) { return false; @@ -2013,7 +1707,7 @@ bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude SimpleMissionItem* simpleItem = qobject_cast(visualItem); if (simpleItem->specifiesAltitude()) { foundAltitude = simpleItem->altitude()->rawValue().toDouble(); - foundAltMode = simpleItem->altitudeMode(); + foundAltFrame = simpleItem->altitudeFrame(); found = true; break; } @@ -2023,7 +1717,7 @@ bool MissionController::_findPreviousAltitude(int newIndex, double* prevAltitude if (found) { *prevAltitude = foundAltitude; - *prevAltitudeMode = foundAltMode; + *prevAltitudeMode = foundAltFrame; } return found; @@ -2056,43 +1750,6 @@ MissionSettingsItem* MissionController::_addMissionSettings(QmlObjectListModel* return settingsItem; } -void MissionController::_centerHomePositionOnMissionItems(QmlObjectListModel *visualItems) -{ - qCDebug(MissionControllerLog) << "_centerHomePositionOnMissionItems"; - - if (visualItems->count() > 1) { - double north = 0.0; - double south = 0.0; - double east = 0.0; - double west = 0.0; - bool firstCoordSet = false; - - for (int i=1; icount(); i++) { - VisualMissionItem* item = qobject_cast(visualItems->get(i)); - if (item->specifiesCoordinate()) { - if (firstCoordSet) { - double lat = _normalizeLat(item->coordinate().latitude()); - double lon = _normalizeLon(item->coordinate().longitude()); - north = fmax(north, lat); - south = fmin(south, lat); - east = fmax(east, lon); - west = fmin(west, lon); - } else { - firstCoordSet = true; - north = _normalizeLat(item->coordinate().latitude()); - south = north; - east = _normalizeLon(item->coordinate().longitude()); - west = east; - } - } - } - - if (firstCoordSet) { - _settingsItem->setInitialHomePositionFromUser(QGeoCoordinate((south + ((north - south) / 2)) - 90.0, (west + ((east - west) / 2)) - 180.0, 0.0)); - } - } -} - int MissionController::resumeMissionIndex(void) const { @@ -2190,11 +1847,6 @@ void MissionController::_scanForAdditionalSettings(QmlObjectListModel* visualIte } } -void MissionController::_updateContainsItems(void) -{ - emit containsItemsChanged(); -} - bool MissionController::containsItems(void) const { return _visualItems ? _visualItems->count() > 1 : false; @@ -2203,9 +1855,9 @@ bool MissionController::containsItems(void) const void MissionController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress"; + qCCritical(MissionControllerLog) << "MissionControllerLog::removeAllFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _missionManager->removeAll(); @@ -2272,7 +1924,7 @@ bool MissionController::showPlanFromManagerVehicle (void) { qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline"; + qCCritical(MissionControllerLog) << "MissionController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { if (!_managerVehicle->initialPlanRequestComplete()) { @@ -2284,7 +1936,7 @@ bool MissionController::showPlanFromManagerVehicle (void) qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: syncInProgress wait for signal"; return true; } else { - // Fake a _newMissionItemsAvailable with the current items + // Sync has already completed, fake a _newMissionItemsAvailable with the current items qCDebug(MissionControllerLog) << "showPlanFromManagerVehicle: sync complete simulate signal"; _itemsRequested = true; _newMissionItemsAvailableFromVehicle(false /* removeAllRequested */); @@ -2346,7 +1998,7 @@ void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force) _previousCoordinate = QGeoCoordinate(); bool noItemsAddedYet = _visualItems->count() == 1; - if (_masterController->controllerVehicle()->takeoffVehicleSupported() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) { + if (_masterController->controllerVehicle()->supports()->takeoffMissionCommand() && !_planViewSettings->takeoffItemNotRequired()->rawValue().toBool() && noItemsAddedYet) { _onlyInsertTakeoffValid = true; } @@ -2490,20 +2142,162 @@ void MissionController::setCurrentPlanViewSeqNum(int sequenceNumber, bool force) _isInsertLandValid = _isInsertLandValid && !_onlyInsertTakeoffValid; _flyThroughCommandsAllowed = _flyThroughCommandsAllowed && !_onlyInsertTakeoffValid; - emit currentPlanViewSeqNumChanged(); - emit currentPlanViewVIIndexChanged(); - emit currentPlanViewItemChanged(); + // These 10 properties are all recomputed together above, so a single signal is sufficient. + // QML property bindings list planViewStateChanged as their NOTIFY signal which means + // one emit re-evaluates all dependent bindings in one pass instead of 10 separate updates. + // splitSegmentChanged is kept separate because PlanView.qml has an explicit onSplitSegmentChanged handler. + emit planViewStateChanged(); emit splitSegmentChanged(); - emit onlyInsertTakeoffValidChanged(); - emit isInsertTakeoffValidChanged(); - emit isInsertLandValidChanged(); - emit isROIActiveChanged(); - emit isROIBeginCurrentItemChanged(); - emit flyThroughCommandsAllowedChanged(); - emit previousCoordinateChanged(); } } +void MissionController::repositionMission(const QGeoCoordinate& newHome, + bool repositionTakeoffItems, + bool repositionLandingItems) +{ + if (!newHome.isValid()) { + qCWarning(MissionControllerLog) << "Cannot reposition mission to an invalid coordinate"; + return; + } + + if (!_settingsItem || !_settingsItem->coordinate().isValid()) { + qCWarning(MissionControllerLog) << "Cannot reposition mission while home is invalid"; + return; + } + + const QGeoCoordinate oldHome = _settingsItem->coordinate(); + + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!repositionTakeoffItems && item->isTakeoffItem()) || + (!repositionLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + const double distanceMeters = oldHome.distanceTo(oldCoord); + const double azimuthDegrees = oldHome.azimuthTo(oldCoord); + + QGeoCoordinate newCoord = newHome.atDistanceAndAzimuth(distanceMeters, azimuthDegrees); + item->setCoordinate(newCoord); + } + + setDirty(true); +} + +void MissionController::offsetMission(double eastMeters, + double northMeters, + double upMeters, + bool offsetTakeoffItems, + bool offsetLandingItems) +{ + if (!qFuzzyIsNull(eastMeters) || !qFuzzyIsNull(northMeters)) { + const double distanceMeters = qSqrt(eastMeters * eastMeters + northMeters * northMeters); + double azimuthDegrees = 0.0; + if (!qFuzzyIsNull(distanceMeters)) { + const double azimuthRadians = qAtan2(eastMeters, northMeters); + azimuthDegrees = qRadiansToDegrees(azimuthRadians); + } + + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!offsetTakeoffItems && item->isTakeoffItem()) || + (!offsetLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + item->setCoordinate(oldCoord.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + setDirty(true); + } + + if (!qFuzzyIsNull(upMeters)) { + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || item == _settingsItem) { + continue; + } + + if ((!offsetTakeoffItems && item->isTakeoffItem()) || + (!offsetLandingItems && item->isLandCommand())) { + continue; + } + + if (!item->specifiesCoordinate() && !item->specifiesAltitudeOnly()) { + continue; + } + + if (!qIsNaN(item->editableAlt())) { + item->applyNewAltitude(item->editableAlt() + upMeters); + } + } + + setDirty(true); + } +} + +void MissionController::rotateMission(double degreesCW, + bool rotateTakeoffItems, + bool rotateLandingItems) +{ + if (qFuzzyIsNull(degreesCW)) { + return; + } + + if (!_settingsItem || !_settingsItem->coordinate().isValid()) { + qCWarning(MissionControllerLog) << "Cannot rotate mission while home is invalid"; + return; + } + + const QGeoCoordinate home = _settingsItem->coordinate(); + + for (int i = 0; i < _visualItems->count(); ++i) { + auto* item = qobject_cast(_visualItems->get(i)); + if (!item || !item->specifiesCoordinate() || item->isStandaloneCoordinate()) { + continue; + } + + if ((!rotateTakeoffItems && item->isTakeoffItem()) || + (!rotateLandingItems && item->isLandCommand())) { + continue; + } + + const QGeoCoordinate oldCoord = item->coordinate(); + if (!oldCoord.isValid()) { + continue; + } + + const double distanceMeters = home.distanceTo(oldCoord); + if (qFuzzyIsNull(distanceMeters)) { + continue; + } + const double azimuthDegrees = home.azimuthTo(oldCoord); + + QGeoCoordinate newCoord = home.atDistanceAndAzimuth(distanceMeters, azimuthDegrees + degreesCW); + item->setCoordinate(newCoord); + } + + setDirty(true); +} + void MissionController::_updateTimeout() { QGeoCoordinate firstCoordinate; @@ -2555,8 +2349,8 @@ void MissionController::_updateTimeout() if(pComplexItem) { QGCGeoBoundingCube bc = pComplexItem->boundingCube(); if(bc.isValid()) { - if(!firstCoordinate.isValid() && pComplexItem->coordinate().isValid()) { - firstCoordinate = pComplexItem->coordinate(); + if(!firstCoordinate.isValid() && pComplexItem->entryCoordinate().isValid()) { + firstCoordinate = pComplexItem->entryCoordinate(); } north = fmax(north, bc.pointNW.latitude() + 90.0); south = fmin(south, bc.pointSE.latitude() + 90.0); @@ -2662,24 +2456,24 @@ MissionController::SendToVehiclePreCheckState MissionController::sendToVehiclePr return SendToVehiclePreCheckStateOk; } -QGroundControlQmlGlobal::AltMode MissionController::globalAltitudeMode(void) +QGroundControlQmlGlobal::AltitudeFrame MissionController::globalAltitudeFrame(void) { - return _globalAltMode; + return _globalAltFrame; } -QGroundControlQmlGlobal::AltMode MissionController::globalAltitudeModeDefault(void) +QGroundControlQmlGlobal::AltitudeFrame MissionController::globalAltitudeFrameDefault(void) { - if (_globalAltMode == QGroundControlQmlGlobal::AltitudeModeMixed) { - return QGroundControlQmlGlobal::AltitudeModeRelative; + if (_globalAltFrame == QGroundControlQmlGlobal::AltitudeFrameMixed) { + return QGroundControlQmlGlobal::AltitudeFrameRelative; } else { - return _globalAltMode; + return _globalAltFrame; } } -void MissionController::setGlobalAltitudeMode(QGroundControlQmlGlobal::AltMode altMode) +void MissionController::setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altFrame) { - if (_globalAltMode != altMode) { - _globalAltMode = altMode; - emit globalAltitudeModeChanged(); + if (_globalAltFrame != altFrame) { + _globalAltFrame = altFrame; + emit globalAltitudeFrameChanged(); } } diff --git a/src/MissionManager/MissionController.h b/src/MissionManager/MissionController.h index 8a45f38dbcf9..abb8130c3731 100644 --- a/src/MissionManager/MissionController.h +++ b/src/MissionManager/MissionController.h @@ -3,13 +3,18 @@ #include #include #include +#include +#include #include #include "PlanElementController.h" #include "QmlObjectListModel.h" +#include "QmlObjectTreeModel.h" #include "QGCGeoBoundingCube.h" #include "QGroundControlQmlGlobal.h" #include "QGCMAVLink.h" +#include "MissionFlightStatus.h" +#include "MissionFlightStatusCalculator.h" Q_DECLARE_LOGGING_CATEGORY(MissionControllerLog) @@ -43,47 +48,30 @@ class MissionController : public PlanElementController MissionController(PlanMasterController* masterController, QObject* parent = nullptr); ~MissionController(); - typedef struct { - double maxTelemetryDistance; - double totalDistance; - double plannedDistance; - double totalTime; - double hoverDistance; - double hoverTime; - double cruiseDistance; - double cruiseTime; - int mAhBattery; ///< 0 for not available - double hoverAmps; ///< Amp consumption during hover - double cruiseAmps; ///< Amp consumption during cruise - double ampMinutesAvailable; ///< Amp minutes available from single battery - double hoverAmpsTotal; ///< Total hover amps used - double cruiseAmpsTotal; ///< Total cruise amps used - int batteryChangePoint; ///< -1 for not supported, 0 for not needed - int batteriesRequired; ///< -1 for not supported - double vehicleYaw; - double gimbalYaw; ///< NaN signals yaw was never changed - double gimbalPitch; ///< NaN signals pitch was never changed - // The following values are the state prior to executing this item - QGCMAVLink::VehicleClass_t vtolMode; ///< Either VehicleClassFixedWing, VehicleClassMultiRotor, VehicleClassGeneric (mode unknown) - double cruiseSpeed; - double hoverSpeed; - double vehicleSpeed; ///< Either cruise or hover speed based on vehicle type and vtol state - } MissionFlightStatus_t; + // Legacy alias kept for source compatibility with external code + using MissionFlightStatus_t = ::MissionFlightStatus_t; Q_PROPERTY(QmlObjectListModel* visualItems READ visualItems NOTIFY visualItemsChanged) + Q_PROPERTY(QmlObjectTreeModel* visualItemsTree READ visualItemsTree CONSTANT) ///< Tree-structured view of visualItems for TreeView + Q_PROPERTY(QPersistentModelIndex planFileGroupIndex READ planFileGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex defaultsGroupIndex READ defaultsGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex missionGroupIndex READ missionGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex fenceGroupIndex READ fenceGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex rallyGroupIndex READ rallyGroupIndex CONSTANT) + Q_PROPERTY(QPersistentModelIndex transformGroupIndex READ transformGroupIndex CONSTANT) Q_PROPERTY(QmlObjectListModel* simpleFlightPathSegments READ simpleFlightPathSegments CONSTANT) ///< Used by Plan view only for interactive editing Q_PROPERTY(QmlObjectListModel* directionArrows READ directionArrows CONSTANT) Q_PROPERTY(QStringList complexMissionItemNames READ complexMissionItemNames NOTIFY complexMissionItemNamesChanged) Q_PROPERTY(QGeoCoordinate plannedHomePosition READ plannedHomePosition NOTIFY plannedHomePositionChanged) ///< Includes AMSL altitude - Q_PROPERTY(QGeoCoordinate previousCoordinate MEMBER _previousCoordinate NOTIFY previousCoordinateChanged) + Q_PROPERTY(QGeoCoordinate previousCoordinate MEMBER _previousCoordinate NOTIFY planViewStateChanged) Q_PROPERTY(FlightPathSegment* splitSegment MEMBER _splitSegment NOTIFY splitSegmentChanged) ///< Segment which show show + split ui element Q_PROPERTY(double progressPct READ progressPct NOTIFY progressPctChanged) Q_PROPERTY(int missionItemCount READ missionItemCount NOTIFY missionItemCountChanged) ///< True mission item command count (only valid in Fly View) Q_PROPERTY(int currentMissionIndex READ currentMissionIndex NOTIFY currentMissionIndexChanged) Q_PROPERTY(int resumeMissionIndex READ resumeMissionIndex NOTIFY resumeMissionIndexChanged) ///< Returns the item index two which a mission should be resumed. -1 indicates resume mission not available. - Q_PROPERTY(int currentPlanViewSeqNum READ currentPlanViewSeqNum NOTIFY currentPlanViewSeqNumChanged) - Q_PROPERTY(int currentPlanViewVIIndex READ currentPlanViewVIIndex NOTIFY currentPlanViewVIIndexChanged) - Q_PROPERTY(VisualMissionItem* currentPlanViewItem READ currentPlanViewItem NOTIFY currentPlanViewItemChanged) + Q_PROPERTY(int currentPlanViewSeqNum READ currentPlanViewSeqNum NOTIFY planViewStateChanged) + Q_PROPERTY(int currentPlanViewVIIndex READ currentPlanViewVIIndex NOTIFY planViewStateChanged) + Q_PROPERTY(VisualMissionItem* currentPlanViewItem READ currentPlanViewItem NOTIFY planViewStateChanged) Q_PROPERTY(TakeoffMissionItem* takeoffMissionItem READ takeoffMissionItem NOTIFY takeoffMissionItemChanged) Q_PROPERTY(double missionTotalDistance READ missionTotalDistance NOTIFY missionTotalDistanceChanged) Q_PROPERTY(double missionPlannedDistance READ missionPlannedDistance NOTIFY missionPlannedDistanceChanged) @@ -99,22 +87,25 @@ class MissionController : public PlanElementController Q_PROPERTY(QString surveyComplexItemName READ surveyComplexItemName CONSTANT) Q_PROPERTY(QString corridorScanComplexItemName READ corridorScanComplexItemName CONSTANT) Q_PROPERTY(QString structureScanComplexItemName READ structureScanComplexItemName CONSTANT) - Q_PROPERTY(bool onlyInsertTakeoffValid MEMBER _onlyInsertTakeoffValid NOTIFY onlyInsertTakeoffValidChanged) - Q_PROPERTY(bool isInsertTakeoffValid MEMBER _isInsertTakeoffValid NOTIFY isInsertTakeoffValidChanged) - Q_PROPERTY(bool isInsertLandValid MEMBER _isInsertLandValid NOTIFY isInsertLandValidChanged) + Q_PROPERTY(bool onlyInsertTakeoffValid MEMBER _onlyInsertTakeoffValid NOTIFY planViewStateChanged) + Q_PROPERTY(bool isInsertTakeoffValid MEMBER _isInsertTakeoffValid NOTIFY planViewStateChanged) + Q_PROPERTY(bool isInsertLandValid MEMBER _isInsertLandValid NOTIFY planViewStateChanged) Q_PROPERTY(bool hasLandItem MEMBER _hasLandItem NOTIFY hasLandItemChanged) Q_PROPERTY(bool multipleLandPatternsAllowed READ multipleLandPatternsAllowed NOTIFY multipleLandPatternsAllowedChanged) - Q_PROPERTY(bool isROIActive MEMBER _isROIActive NOTIFY isROIActiveChanged) - Q_PROPERTY(bool isROIBeginCurrentItem MEMBER _isROIBeginCurrentItem NOTIFY isROIBeginCurrentItemChanged) - Q_PROPERTY(bool flyThroughCommandsAllowed MEMBER _flyThroughCommandsAllowed NOTIFY flyThroughCommandsAllowedChanged) + Q_PROPERTY(bool isROIActive MEMBER _isROIActive NOTIFY planViewStateChanged) + Q_PROPERTY(bool isROIBeginCurrentItem MEMBER _isROIBeginCurrentItem NOTIFY planViewStateChanged) + Q_PROPERTY(bool flyThroughCommandsAllowed MEMBER _flyThroughCommandsAllowed NOTIFY planViewStateChanged) Q_PROPERTY(double minAMSLAltitude MEMBER _minAMSLAltitude NOTIFY minAMSLAltitudeChanged) ///< Minimum altitude associated with this mission. Used to calculate percentages for terrain status. Q_PROPERTY(double maxAMSLAltitude MEMBER _maxAMSLAltitude NOTIFY maxAMSLAltitudeChanged) ///< Maximum altitude associated with this mission. Used to calculate percentages for terrain status. - Q_PROPERTY(QGroundControlQmlGlobal::AltMode globalAltitudeMode READ globalAltitudeMode WRITE setGlobalAltitudeMode NOTIFY globalAltitudeModeChanged) - Q_PROPERTY(QGroundControlQmlGlobal::AltMode globalAltitudeModeDefault READ globalAltitudeModeDefault NOTIFY globalAltitudeModeChanged) ///< Default to use for newly created items + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrame READ globalAltitudeFrame WRITE setGlobalAltitudeFrame NOTIFY globalAltitudeFrameChanged) + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrameDefault READ globalAltitudeFrameDefault NOTIFY globalAltitudeFrameChanged) ///< Default to use for newly created items Q_INVOKABLE void removeVisualItem(int viIndex); + /// Returns the visual item index for the given VisualMissionItem object, or -1 if not found + Q_INVOKABLE int visualItemIndexForObject(QObject* object) const; + /// Add a new simple mission item to the list /// @param coordinate: Coordinate for item /// @param visualItemIndex: index to insert at, -1 for end of list @@ -176,6 +167,47 @@ class MissionController : public PlanElementController /// @param force - true: reset internals even if specified item is already selected Q_INVOKABLE void setCurrentPlanViewSeqNum(int sequenceNumber, bool force); + /// Repositions all mission items which specify a coordinate around a new + /// home coordinate. Requires a valid planned home position; otherwise the + /// mission is not modified and a warning is logged. + /// @param newHome New coordinate for the home item + /// @param repositionTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be repositioned + /// @param repositionLandingItems If true, items identified as landing items + /// (isLandCommand) will be repositioned + Q_INVOKABLE void repositionMission(const QGeoCoordinate& newHome, + bool repositionTakeoffItems = true, + bool repositionLandingItems = true); + + /// Offsets all mission items which specify a coordinate by the specified + /// ENU amounts in meters. Home altitude remains unchanged. + /// @param eastMeters Distance to offset items to the east, in meters + /// @param northMeters Distance to offset items to the north, in meters + /// @param upMeters Distance to offset items upwards, in meters + /// @param offsetTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be offset + /// @param offsetLandingItems If true, items identified as landing items + /// (isLandCommand) will be offset + Q_INVOKABLE void offsetMission(double eastMeters, + double northMeters, + double upMeters = 0.0, + bool offsetTakeoffItems = false, + bool offsetLandingItems = false); + + /// Rotates all mission items which specify a coordinate around the up axis + /// of the home position. Complex items are rotated by moving their + /// reference coordinate: their geometry and orientation are not modified. + /// Requires a valid planned home position; otherwise the mission is not + /// modified and a warning is logged. + /// @param degreesCW Angle to rotate items by, in degrees clockwise + /// @param rotateTakeoffItems If true, items identified as takeoff items + /// (isTakeoffItem) will be rotated + /// @param rotateLandingItems If true, items identified as landing items + /// (isLandCommand) will be rotated + Q_INVOKABLE void rotateMission(double degreesCW, + bool rotateTakeoffItems = false, + bool rotateLandingItems = false); + enum SendToVehiclePreCheckState { SendToVehiclePreCheckStateOk, // Ok to send plan to vehicle SendToVehiclePreCheckStateNoActiveVehicle, // There is no active vehicle @@ -220,6 +252,13 @@ class MissionController : public PlanElementController // Property accessors QmlObjectListModel* visualItems (void) { return _visualItems; } + QmlObjectTreeModel* visualItemsTree (void) { return &_visualItemsTree; } + QPersistentModelIndex planFileGroupIndex (void) const { return _planFileGroupIndex; } + QPersistentModelIndex defaultsGroupIndex (void) const { return _defaultsGroupIndex; } + QPersistentModelIndex missionGroupIndex (void) const { return _missionGroupIndex; } + QPersistentModelIndex fenceGroupIndex (void) const { return _fenceGroupIndex; } + QPersistentModelIndex rallyGroupIndex (void) const { return _rallyGroupIndex; } + QPersistentModelIndex transformGroupIndex (void) const { return _transformGroupIndex; } QmlObjectListModel* simpleFlightPathSegments (void) { return &_simpleFlightPathSegments; } QmlObjectListModel* directionArrows (void) { return &_directionArrows; } QStringList complexMissionItemNames (void) const; @@ -235,7 +274,7 @@ class MissionController : public PlanElementController double minAMSLAltitude (void) const { return _minAMSLAltitude; } double maxAMSLAltitude (void) const { return _maxAMSLAltitude; } - int missionItemCount (void) const { return _missionItemCount; } + int missionItemCount (void) const { return _visualItems ? _visualItems->count() : 0; } int currentMissionIndex (void) const; int resumeMissionIndex (void) const; int currentPlanViewSeqNum (void) const { return _currentPlanViewSeqNum; } @@ -256,9 +295,18 @@ class MissionController : public PlanElementController bool isFirstLandingComplexItem (const LandingComplexItem* item) const; bool isEmpty (void) const; - QGroundControlQmlGlobal::AltMode globalAltitudeMode(void); - QGroundControlQmlGlobal::AltMode globalAltitudeModeDefault(void); - void setGlobalAltitudeMode(QGroundControlQmlGlobal::AltMode altMode); + QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrame(void); + QGroundControlQmlGlobal::AltitudeFrame globalAltitudeFrameDefault(void); + void setGlobalAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altFrame); + + // Top-level group row indices in _visualItemsTree (must match _setupTreeModel order) + static constexpr int kPlanFileGroupRow = 0; + static constexpr int kDefaultsGroupRow = 1; + static constexpr int kMissionGroupRow = 2; + static constexpr int kFenceGroupRow = 3; + static constexpr int kRallyGroupRow = 4; + static constexpr int kTransformGroupRow = 5; + static constexpr int kGroupCount = 6; signals: void visualItemsChanged (void); @@ -281,27 +329,18 @@ class MissionController : public PlanElementController void plannedHomePositionChanged (QGeoCoordinate plannedHomePosition); void progressPctChanged (double progressPct); void currentMissionIndexChanged (int currentMissionIndex); - void currentPlanViewSeqNumChanged (void); - void currentPlanViewVIIndexChanged (void); - void currentPlanViewItemChanged (void); + void planViewStateChanged (void); ///< All plan-view properties are recomputed together in setCurrentPlanViewSeqNum, so one signal covers them all void takeoffMissionItemChanged (void); void missionBoundingCubeChanged (void); - void missionItemCountChanged (int missionItemCount); - void onlyInsertTakeoffValidChanged (void); - void isInsertTakeoffValidChanged (void); - void isInsertLandValidChanged (void); + void missionItemCountChanged (void); void hasLandItemChanged (void); void multipleLandPatternsAllowedChanged (void); - void isROIActiveChanged (void); - void isROIBeginCurrentItemChanged (void); - void flyThroughCommandsAllowedChanged (void); - void previousCoordinateChanged (void); void minAMSLAltitudeChanged (double minAMSLAltitude); void maxAMSLAltitudeChanged (double maxAMSLAltitude); void recalcTerrainProfile (void); void _recalcMissionFlightStatusSignal (void); void _recalcFlightPathSegmentsSignal (void); - void globalAltitudeModeChanged (void); + void globalAltitudeFrameChanged (void); private slots: void _newMissionItemsAvailableFromVehicle (bool removeAllRequested); @@ -310,7 +349,6 @@ private slots: void _currentMissionIndexChanged (int sequenceNumber); void _recalcFlightPathSegments (void); void _recalcMissionFlightStatus (void); - void _updateContainsItems (void); void _progressPctChanged (double progressPct); void _visualItemsDirtyChanged (bool dirty); void _managerSendComplete (bool error); @@ -320,37 +358,37 @@ private slots: void _recalcAll (void); void _managerVehicleChanged (Vehicle* managerVehicle); void _forceRecalcOfAllowedBits (void); - + // Incremental tree model sync slots + void _syncTreeMissionItemsInserted (const QModelIndex& parent, int first, int last); + void _syncTreeMissionItemsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeMissionItemsReset (void); + void _syncTreeRallyPointsInserted (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsAboutToBeRemoved (const QModelIndex& parent, int first, int last); + void _syncTreeRallyPointsRemoved (const QModelIndex& parent, int first, int last); + + void _syncTreeRallyPointsReset (void); private: void _init (void); + void _setupTreeModel (void); void _recalcSequence (void); void _recalcChildItems (void); void _recalcAllWithCoordinate (const QGeoCoordinate& coordinate); - void _recalcROISpecialVisuals (void); + void _setupNewVisualItems (QmlObjectListModel* newItems = nullptr); void _initAllVisualItems (void); void _deinitAllVisualItems (void); void _initVisualItem (VisualMissionItem* item); void _deinitVisualItem (VisualMissionItem* item); void _setupActiveVehicle (Vehicle* activeVehicle, bool forceLoadFromVehicle); - void _calcPrevWaypointValues (VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference); - bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltMode* prevAltMode); + bool _findPreviousAltitude (int newIndex, double* prevAltitude, QGroundControlQmlGlobal::AltitudeFrame* prevAltFrame); MissionSettingsItem* _addMissionSettings (QmlObjectListModel* visualItems); - void _centerHomePositionOnMissionItems (QmlObjectListModel* visualItems); - bool _loadJsonMissionFile (const QByteArray& bytes, QmlObjectListModel* visualItems, QString& errorString); - bool _loadJsonMissionFileV1 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); bool _loadJsonMissionFileV2 (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); bool _loadTextMissionFile (QTextStream& stream, QmlObjectListModel* visualItems, QString& errorString); int _nextSequenceNumber (void); void _scanForAdditionalSettings (QmlObjectListModel* visualItems, PlanMasterController* masterController); void _setPlannedHomePositionFromFirstCoordinate(const QGeoCoordinate& clickCoordinate); void _resetMissionFlightStatus (void); - void _addHoverTime (double hoverTime, double hoverDistance, int waypointIndex); - void _addCruiseTime (double cruiseTime, double cruiseDistance, int wayPointIndex); - void _updateBatteryInfo (int waypointIndex); - bool _loadItemsFromJson (const QJsonObject& json, QmlObjectListModel* visualItems, QString& errorString); void _initLoadedVisualItems (QmlObjectListModel* loadedVisualItems); FlightPathSegment* _addFlightPathSegment (FlightPathSegmentHashTable& prevItemPairHashTable, VisualItemPair& pair, bool mavlinkTerrainFrame); - void _addTimeDistance (bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum); VisualMissionItem* _insertSimpleMissionItemWorker (QGeoCoordinate coordinate, MAV_CMD command, int visualItemIndex, bool makeCurrentItem); void _insertComplexMissionItemWorker (const QGeoCoordinate& mapCenterCoordinate, ComplexMissionItem* complexItem, int visualItemIndex, bool makeCurrentItem); bool _isROIBeginItem (SimpleMissionItem* simpleItem); @@ -359,7 +397,6 @@ private slots: void _allItemsRemoved (void); void _firstItemAdded (void); - static double _calcDistanceToHome (VisualMissionItem* currentItem, VisualMissionItem* homeItem); static double _normalizeLat (double lat); static double _normalizeLon (double lon); static bool _convertToMissionItems (QmlObjectListModel* visualMissionItems, QList& rgMissionItems, QObject* missionItemParent); @@ -368,8 +405,25 @@ private slots: Vehicle* _controllerVehicle = nullptr; Vehicle* _managerVehicle = nullptr; MissionManager* _missionManager = nullptr; - int _missionItemCount = 0; QmlObjectListModel* _visualItems = nullptr; + QPersistentModelIndex _planFileGroupIndex; ///< Persistent index for "Plan File" group in tree + QPersistentModelIndex _defaultsGroupIndex; ///< Persistent index for "Defaults" group in tree + QPersistentModelIndex _missionGroupIndex; ///< Persistent index for "Mission Items" group in tree + QPersistentModelIndex _fenceGroupIndex; ///< Persistent index for "GeoFence" group in tree + QPersistentModelIndex _rallyGroupIndex; ///< Persistent index for "Rally Points" group in tree + QPersistentModelIndex _transformGroupIndex; ///< Persistent index for "Transform" group in tree + QObject _planFileGroupNode; ///< Group node for "Plan File" in tree view + QObject _planFileInfoMarker; ///< Marker child for plan file info delegate + QObject _defaultsGroupNode; ///< Group node for "Defaults" in tree view + QObject _defaultsInfoMarker; ///< Marker child for defaults editor delegate + QObject _missionItemsGroupNode; ///< Group node for "Mission Items" in tree view + QObject _fenceGroupNode; ///< Group node for "GeoFence" in tree view + QObject _rallyGroupNode; ///< Group node for "Rally Points" in tree view + QObject _transformGroupNode; ///< Group node for "Transform" in tree view + QObject _fenceEditorMarker; ///< Marker child for GeoFenceEditor delegate + QObject _rallyHeaderMarker; ///< Marker child for RallyPointEditorHeader delegate + QObject _transformEditorMarker; ///< Marker child for TransformEditor delegate + QmlObjectTreeModel _visualItemsTree; // Must be declared after group nodes so it's destroyed first MissionSettingsItem* _settingsItem = nullptr; PlanViewSettings* _planViewSettings = nullptr; QmlObjectListModel _simpleFlightPathSegments; @@ -378,6 +432,7 @@ private slots: bool _firstItemsFromVehicle = false; bool _itemsRequested = false; bool _inRecalcSequence = false; + MissionFlightStatusCalculator _flightStatusCalc; MissionFlightStatus_t _missionFlightStatus; AppSettings* _appSettings = nullptr; double _progressPct = 0; @@ -402,10 +457,9 @@ private slots: double _maxAMSLAltitude = 0; bool _missionContainsVTOLTakeoff = false; - QGroundControlQmlGlobal::AltMode _globalAltMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _globalAltFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; static constexpr const char* _settingsGroup = "MissionController"; - static constexpr const char* _jsonFileTypeValue = "Mission"; static constexpr const char* _jsonItemsKey = "items"; static constexpr const char* _jsonPlannedHomePositionKey = "plannedHomePosition"; static constexpr const char* _jsonFirmwareTypeKey = "firmwareType"; @@ -415,9 +469,5 @@ private slots: static constexpr const char* _jsonParamsKey = "params"; static constexpr const char* _jsonGlobalPlanAltitudeModeKey = "globalPlanAltitudeMode"; - // Deprecated V1 format keys - static constexpr const char* _jsonComplexItemsKey = "complexItems"; - static constexpr const char* _jsonMavAutopilotKey = "MAV_AUTOPILOT"; - static constexpr int _missionFileVersion = 2; }; diff --git a/src/MissionManager/MissionFlightStatus.h b/src/MissionManager/MissionFlightStatus.h new file mode 100644 index 000000000000..09df427bbf2f --- /dev/null +++ b/src/MissionManager/MissionFlightStatus.h @@ -0,0 +1,30 @@ +#pragma once + +#include "QGCMAVLink.h" + +struct MissionFlightStatus_t { + double maxTelemetryDistance; + double totalDistance; + double plannedDistance; + double totalTime; + double hoverDistance; + double hoverTime; + double cruiseDistance; + double cruiseTime; + int mAhBattery; ///< 0 for not available + double hoverAmps; ///< Amp consumption during hover + double cruiseAmps; ///< Amp consumption during cruise + double ampMinutesAvailable; ///< Amp minutes available from single battery + double hoverAmpsTotal; ///< Total hover amps used + double cruiseAmpsTotal; ///< Total cruise amps used + int batteryChangePoint; ///< -1 for not supported, 0 for not needed + int batteriesRequired; ///< -1 for not supported + double vehicleYaw; + double gimbalYaw; ///< NaN signals yaw was never changed + double gimbalPitch; ///< NaN signals pitch was never changed + // The following values are the state prior to executing this item + QGCMAVLink::VehicleClass_t vtolMode; ///< Either VehicleClassFixedWing, VehicleClassMultiRotor, VehicleClassGeneric (mode unknown) + double cruiseSpeed; + double hoverSpeed; + double vehicleSpeed; ///< Either cruise or hover speed based on vehicle type and vtol state +}; diff --git a/src/MissionManager/MissionFlightStatusCalculator.cc b/src/MissionManager/MissionFlightStatusCalculator.cc new file mode 100644 index 000000000000..99ebedafff4c --- /dev/null +++ b/src/MissionManager/MissionFlightStatusCalculator.cc @@ -0,0 +1,393 @@ +#include "MissionFlightStatusCalculator.h" +#include "Vehicle.h" +#include "FirmwarePlugin.h" +#include "QmlObjectListModel.h" +#include "VisualMissionItem.h" +#include "SimpleMissionItem.h" +#include "ComplexMissionItem.h" +#include "MissionSettingsItem.h" +#include "AppSettings.h" +#include "PlanViewSettings.h" +#include "SettingsManager.h" + +#include + +void MissionFlightStatusCalculator::reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff) +{ + _status.totalDistance = 0.0; + _status.plannedDistance = 0.0; + _status.maxTelemetryDistance = 0.0; + _status.totalTime = 0.0; + _status.hoverTime = 0.0; + _status.cruiseTime = 0.0; + _status.hoverDistance = 0.0; + _status.cruiseDistance = 0.0; + _status.cruiseSpeed = controllerVehicle->defaultCruiseSpeed(); + _status.hoverSpeed = controllerVehicle->defaultHoverSpeed(); + _status.vehicleSpeed = controllerVehicle->multiRotor() || managerVehicle->vtol() ? _status.hoverSpeed : _status.cruiseSpeed; + _status.vehicleYaw = qQNaN(); + _status.gimbalYaw = qQNaN(); + _status.gimbalPitch = qQNaN(); + _status.mAhBattery = 0; + _status.hoverAmps = 0; + _status.cruiseAmps = 0; + _status.ampMinutesAvailable = 0; + _status.hoverAmpsTotal = 0; + _status.cruiseAmpsTotal = 0; + _status.batteryChangePoint = -1; + _status.batteriesRequired = -1; + _status.vtolMode = missionContainsVTOLTakeoff ? QGCMAVLink::VehicleClassMultiRotor : QGCMAVLink::VehicleClassFixedWing; + + controllerVehicle->firmwarePlugin()->batteryConsumptionData(controllerVehicle, _status.mAhBattery, _status.hoverAmps, _status.cruiseAmps); + if (_status.mAhBattery != 0) { + double batteryPercentRemainingAnnounce = SettingsManager::instance()->appSettings()->batteryPercentRemainingAnnounce()->rawValue().toDouble(); + _status.ampMinutesAvailable = static_cast(_status.mAhBattery) / 1000.0 * 60.0 * ((100.0 - batteryPercentRemainingAnnounce) / 100.0); + } +} + +void MissionFlightStatusCalculator::recalc(QmlObjectListModel* visualItems, + MissionSettingsItem* settingsItem, + Vehicle* controllerVehicle, + Vehicle* managerVehicle, + AppSettings* appSettings, + PlanViewSettings* planViewSettings, + bool missionContainsVTOLTakeoff) +{ + bool firstCoordinateItem = true; + VisualMissionItem* lastFlyThroughVI = qobject_cast(visualItems->get(0)); + + bool homePositionValid = settingsItem->coordinate().isValid(); + + // If home position is valid we can calculate distances between all waypoints. + // If home position is not valid we can only calculate distances between waypoints which are + // both relative altitude. + + // No values for first item + lastFlyThroughVI->setAltDifference(0); + lastFlyThroughVI->setAzimuth(0); + lastFlyThroughVI->setDistance(0); + lastFlyThroughVI->setDistanceFromStart(0); + + _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); + + reset(controllerVehicle, managerVehicle, missionContainsVTOLTakeoff); + + bool linkStartToHome = false; + bool foundRTL = false; + bool pastLandCommand = false; + double totalHorizontalDistance = 0; + + for (int i=0; icount(); i++) { + VisualMissionItem* item = qobject_cast(visualItems->get(i)); + SimpleMissionItem* simpleItem = qobject_cast(item); + ComplexMissionItem* complexItem = qobject_cast(item); + + if (simpleItem && simpleItem->mavCommand() == MAV_CMD_NAV_RETURN_TO_LAUNCH) { + foundRTL = true; + } + + // Assume the worst + item->setAzimuth(0); + item->setDistance(0); + item->setDistanceFromStart(0); + + // Gimbal states reflect the state AFTER executing the item + + // ROI commands cancel out previous gimbal yaw/pitch + if (simpleItem) { + switch (simpleItem->command()) { + case MAV_CMD_NAV_ROI: + case MAV_CMD_DO_SET_ROI_LOCATION: + case MAV_CMD_DO_SET_ROI_WPNEXT_OFFSET: + case MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW: + _status.gimbalYaw = qQNaN(); + _status.gimbalPitch = qQNaN(); + break; + default: + break; + } + } + + // Look for specific gimbal changes + double gimbalYaw = item->specifiedGimbalYaw(); + if (!qIsNaN(gimbalYaw) || planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { + _status.gimbalYaw = gimbalYaw; + } + double gimbalPitch = item->specifiedGimbalPitch(); + if (!qIsNaN(gimbalPitch) || planViewSettings->showGimbalOnlyWhenSet()->rawValue().toBool()) { + _status.gimbalPitch = gimbalPitch; + } + + // We don't need to do any more processing if: + // Mission Settings Item + // We are after an RTL command + if (i != 0 && !foundRTL) { + // We must set the mission flight status prior to querying for any values from the item. This is because things like + // current speed, gimbal, vtol state impact the values. + item->setMissionFlightStatus(_status); + + // Link back to home if first item is takeoff and we have home position + if (firstCoordinateItem && simpleItem && (simpleItem->mavCommand() == MAV_CMD_NAV_TAKEOFF || simpleItem->mavCommand() == MAV_CMD_NAV_VTOL_TAKEOFF)) { + if (homePositionValid) { + linkStartToHome = true; + if (controllerVehicle->multiRotor() || controllerVehicle->vtol()) { + // We have to special case takeoff, assuming vehicle takes off straight up to specified altitude + double azimuth, distance, altDifference; + calcPrevWaypointValues(settingsItem, simpleItem, &azimuth, &distance, &altDifference); + double takeoffTime = qAbs(altDifference) / appSettings->offlineEditingAscentSpeed()->rawValue().toDouble(); + _addHoverTime(takeoffTime, 0, -1); + } + } + } + + if (!pastLandCommand) + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, 0, 0, item->additionalTimeDelay(), 0, -1); + + if (item->specifiesCoordinate()) { + + // Keep track of the min/max AMSL altitude for entire mission so we can calculate altitude percentages in terrain status display + if (simpleItem) { + double amslAltitude = item->amslEntryAlt(); + _minAMSLAltitude = std::fmin(_minAMSLAltitude, amslAltitude); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, amslAltitude); + } else { + // Complex item + double complexMinAMSLAltitude = complexItem->minAMSLAltitude(); + double complexMaxAMSLAltitude = complexItem->maxAMSLAltitude(); + _minAMSLAltitude = std::fmin(_minAMSLAltitude, complexMinAMSLAltitude); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, complexMaxAMSLAltitude); + } + + if (!item->isStandaloneCoordinate()) { + firstCoordinateItem = false; + + // Update vehicle yaw assuming direction to next waypoint and/or mission item change + if (simpleItem) { + double newVehicleYaw = simpleItem->specifiedVehicleYaw(); + if (qIsNaN(newVehicleYaw)) { + // No specific vehicle yaw set. Current vehicle yaw is determined from flight path segment direction. + if (simpleItem != lastFlyThroughVI) { + _status.vehicleYaw = lastFlyThroughVI->exitCoordinate().azimuthTo(simpleItem->entryCoordinate()); + } + } else { + _status.vehicleYaw = newVehicleYaw; + } + simpleItem->setMissionVehicleYaw(_status.vehicleYaw); + } + + if (lastFlyThroughVI != settingsItem || linkStartToHome) { + // This is a subsequent waypoint or we are forcing the first waypoint back to home + double azimuth, distance, altDifference; + + calcPrevWaypointValues(item, lastFlyThroughVI, &azimuth, &distance, &altDifference); + + // If the last waypoint was a land command, there's a discontinuity at this point + if (!lastFlyThroughVI->isLandCommand()) { + totalHorizontalDistance += distance; + item->setDistance(distance); + + if (!pastLandCommand) { + // Calculate time/distance + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); + } + } + + item->setAltDifference(altDifference); + item->setAzimuth(azimuth); + item->setDistanceFromStart(totalHorizontalDistance); + + _status.maxTelemetryDistance = qMax(_status.maxTelemetryDistance, calcDistanceToHome(item, settingsItem)); + } + + if (complexItem) { + // Add in distance/time inside complex items as well + double distance = complexItem->complexDistance(); + _status.maxTelemetryDistance = qMax(_status.maxTelemetryDistance, complexItem->greatestDistanceTo(complexItem->exitCoordinate())); + + if (!pastLandCommand) { + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, 0, distance, item->sequenceNumber()); + } + + totalHorizontalDistance += distance; + } + + + lastFlyThroughVI = item; + } + } + } + + // Speed, VTOL states changes are processed last since they take affect on the next item + + double newSpeed = item->specifiedFlightSpeed(); + if (!qIsNaN(newSpeed)) { + if (controllerVehicle->multiRotor()) { + _status.hoverSpeed = newSpeed; + } else if (controllerVehicle->vtol()) { + if (_status.vtolMode == QGCMAVLink::VehicleClassMultiRotor) { + _status.hoverSpeed = newSpeed; + } else { + _status.cruiseSpeed = newSpeed; + } + } else { + _status.cruiseSpeed = newSpeed; + } + _status.vehicleSpeed = newSpeed; + } + + // Update VTOL state + if (simpleItem && controllerVehicle->vtol()) { + switch (simpleItem->command()) { + case MAV_CMD_NAV_TAKEOFF: // This will do a fixed wing style takeoff + case MAV_CMD_NAV_VTOL_TAKEOFF: // Vehicle goes straight up and then transitions to FW + case MAV_CMD_NAV_LAND: + _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; + break; + case MAV_CMD_NAV_VTOL_LAND: + _status.vtolMode = QGCMAVLink::VehicleClassMultiRotor; + break; + case MAV_CMD_DO_VTOL_TRANSITION: + { + int transitionState = simpleItem->missionItem().param1(); + if (transitionState == MAV_VTOL_STATE_MC) { + _status.vtolMode = QGCMAVLink::VehicleClassMultiRotor; + } else if (transitionState == MAV_VTOL_STATE_FW) { + _status.vtolMode = QGCMAVLink::VehicleClassFixedWing; + } + } + break; + default: + break; + } + } + + if (item->isLandCommand()) { + pastLandCommand = true; + } + } + lastFlyThroughVI->setMissionVehicleYaw(_status.vehicleYaw); + + // Add the information for the final segment back to home + if (foundRTL && lastFlyThroughVI != settingsItem && homePositionValid) { + double azimuth, distance, altDifference; + calcPrevWaypointValues(lastFlyThroughVI, settingsItem, &azimuth, &distance, &altDifference); + + if (!pastLandCommand) { + // Calculate time/distance + double hoverTime = distance / _status.hoverSpeed; + double cruiseTime = distance / _status.cruiseSpeed; + double landTime = qAbs(altDifference) / appSettings->offlineEditingDescentSpeed()->rawValue().toDouble(); + _addTimeDistance(controllerVehicle, _status.vtolMode == QGCMAVLink::VehicleClassMultiRotor, hoverTime, cruiseTime, landTime, distance, -1); + } + } + + _status.totalDistance = totalHorizontalDistance; + + if (_status.mAhBattery != 0 && _status.batteryChangePoint == -1) { + _status.batteryChangePoint = 0; + } + + if (linkStartToHome) { + // Home position is taken into account for min/max values + _minAMSLAltitude = std::fmin(_minAMSLAltitude, settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); + _maxAMSLAltitude = std::fmax(_maxAMSLAltitude, settingsItem->plannedHomePositionAltitude()->rawValue().toDouble()); + } + + // Walk the list calculating altitude percentages + double altRange = _maxAMSLAltitude - _minAMSLAltitude; + for (int i=0; icount(); i++) { + VisualMissionItem* item = qobject_cast(visualItems->get(i)); + + if (item->specifiesCoordinate()) { + double amslAlt = item->amslEntryAlt(); + if (altRange == 0.0) { + item->setAltPercent(0.0); + item->setTerrainPercent(qQNaN()); + item->setTerrainCollision(false); + } else { + item->setAltPercent((amslAlt - _minAMSLAltitude) / altRange); + double terrainAltitude = item->terrainAltitude(); + if (qIsNaN(terrainAltitude)) { + item->setTerrainPercent(qQNaN()); + item->setTerrainCollision(false); + } else { + item->setTerrainPercent((terrainAltitude - _minAMSLAltitude) / altRange); + item->setTerrainCollision(amslAlt < terrainAltitude); + } + } + } + } +} + +void MissionFlightStatusCalculator::calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, double* azimuth, double* distance, double* altDifference) +{ + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); + QGeoCoordinate prevCoord = prevItem->exitCoordinate(); + + *altDifference = currentItem->amslEntryAlt() - prevItem->amslExitAlt(); + *distance = prevCoord.distanceTo(currentCoord); + *azimuth = prevCoord.azimuthTo(currentCoord); +} + +double MissionFlightStatusCalculator::calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem) +{ + QGeoCoordinate currentCoord = currentItem->entryCoordinate(); + QGeoCoordinate homeCoord = homeItem->exitCoordinate(); + + return homeCoord.distanceTo(currentCoord); +} + +void MissionFlightStatusCalculator::_updateBatteryInfo(int waypointIndex) +{ + if (_status.mAhBattery != 0) { + _status.hoverAmpsTotal = (_status.hoverTime / 60.0) * _status.hoverAmps; + _status.cruiseAmpsTotal = (_status.cruiseTime / 60.0) * _status.cruiseAmps; + _status.batteriesRequired = ceil((_status.hoverAmpsTotal + _status.cruiseAmpsTotal) / _status.ampMinutesAvailable); + if (waypointIndex != -1 && _status.batteriesRequired == 2 && _status.batteryChangePoint == -1) { + _status.batteryChangePoint = waypointIndex - 1; + } + } +} + +void MissionFlightStatusCalculator::_addHoverTime(double hoverTime, double hoverDistance, int waypointIndex) +{ + _status.totalTime += hoverTime; + _status.hoverTime += hoverTime; + _status.hoverDistance += hoverDistance; + _status.plannedDistance += hoverDistance; + _updateBatteryInfo(waypointIndex); +} + +void MissionFlightStatusCalculator::_addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex) +{ + _status.totalTime += cruiseTime; + _status.cruiseTime += cruiseTime; + _status.cruiseDistance += cruiseDistance; + _status.plannedDistance += cruiseDistance; + _updateBatteryInfo(waypointIndex); +} + +void MissionFlightStatusCalculator::_addTimeDistance(Vehicle* controllerVehicle, bool vtolInHover, double hoverTime, double cruiseTime, double extraTime, double distance, int seqNum) +{ + if (controllerVehicle->vtol()) { + if (vtolInHover) { + _addHoverTime(hoverTime, distance, seqNum); + _addHoverTime(extraTime, 0, -1); + } else { + _addCruiseTime(cruiseTime, distance, seqNum); + _addCruiseTime(extraTime, 0, -1); + } + } else { + if (controllerVehicle->multiRotor()) { + _addHoverTime(hoverTime, distance, seqNum); + _addHoverTime(extraTime, 0, -1); + } else { + _addCruiseTime(cruiseTime, distance, seqNum); + _addCruiseTime(extraTime, 0, -1); + } + } +} diff --git a/src/MissionManager/MissionFlightStatusCalculator.h b/src/MissionManager/MissionFlightStatusCalculator.h new file mode 100644 index 000000000000..603c19790d3c --- /dev/null +++ b/src/MissionManager/MissionFlightStatusCalculator.h @@ -0,0 +1,52 @@ +#pragma once + +#include "MissionFlightStatus.h" + +class AppSettings; +class ComplexMissionItem; +class MissionSettingsItem; +class PlanViewSettings; +class QmlObjectListModel; +class SimpleMissionItem; +class Vehicle; +class VisualMissionItem; + +/// Computes mission flight status (distances, times, battery, altitude range) +/// from a list of visual mission items and vehicle properties. +/// Extracted from MissionController to reduce its complexity. +class MissionFlightStatusCalculator +{ +public: + /// Resets the flight status fields to defaults based on vehicle properties. + void reset(Vehicle* controllerVehicle, Vehicle* managerVehicle, bool missionContainsVTOLTakeoff); + + /// Runs the full recalculation over all visual items, updating per-item + /// display properties and computing aggregate flight statistics. + void recalc(QmlObjectListModel* visualItems, + MissionSettingsItem* settingsItem, + Vehicle* controllerVehicle, + Vehicle* managerVehicle, + AppSettings* appSettings, + PlanViewSettings* planViewSettings, + bool missionContainsVTOLTakeoff); + + const MissionFlightStatus_t& status() const { return _status; } + double minAMSLAltitude() const { return _minAMSLAltitude; } + double maxAMSLAltitude() const { return _maxAMSLAltitude; } + + static void calcPrevWaypointValues(VisualMissionItem* currentItem, VisualMissionItem* prevItem, + double* azimuth, double* distance, double* altDifference); + static double calcDistanceToHome(VisualMissionItem* currentItem, VisualMissionItem* homeItem); + +private: + void _updateBatteryInfo(int waypointIndex); + void _addHoverTime(double hoverTime, double hoverDistance, int waypointIndex); + void _addCruiseTime(double cruiseTime, double cruiseDistance, int waypointIndex); + void _addTimeDistance(Vehicle* controllerVehicle, bool vtolInHover, + double hoverTime, double cruiseTime, double extraTime, + double distance, int seqNum); + + MissionFlightStatus_t _status {}; + double _minAMSLAltitude = 0; + double _maxAMSLAltitude = 0; +}; diff --git a/src/MissionManager/MissionSettingsItem.cc b/src/MissionManager/MissionSettingsItem.cc index ec1f92728593..02ba471867db 100644 --- a/src/MissionManager/MissionSettingsItem.cc +++ b/src/MissionManager/MissionSettingsItem.cc @@ -19,7 +19,7 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, , _speedSection (masterController) { _isIncomplete = false; - _editorQml = "qrc:/qml/QGroundControl/Controls/MissionSettingsEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/MissionSettingsEditor.qml"; if (_metaDataMap.isEmpty()) { _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/MissionSettings.FactMetaData.json"), nullptr /* metaDataParent */); @@ -41,6 +41,8 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, connect(&_cameraSection, &CameraSection::specifiedGimbalYawChanged, this, &MissionSettingsItem::specifiedGimbalYawChanged); connect(&_cameraSection, &CameraSection::specifiedGimbalPitchChanged, this, &MissionSettingsItem::specifiedGimbalPitchChanged); connect(&_speedSection, &SpeedSection::specifiedFlightSpeedChanged, this, &MissionSettingsItem::specifiedFlightSpeedChanged); + connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::entryCoordinateChanged); + connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::exitCoordinateChanged); connect(this, &MissionSettingsItem::coordinateChanged, this, &MissionSettingsItem::_amslEntryAltChanged); connect(this, &MissionSettingsItem::amslEntryAltChanged, this, &MissionSettingsItem::amslExitAltChanged); connect(this, &MissionSettingsItem::amslEntryAltChanged, this, &MissionSettingsItem::minAMSLAltitudeChanged); @@ -48,8 +50,8 @@ MissionSettingsItem::MissionSettingsItem(PlanMasterController* masterController, connect(&_plannedHomePositionAltitudeFact, &Fact::rawValueChanged, this, &MissionSettingsItem::_updateAltitudeInCoordinate); - connect(_managerVehicle, &Vehicle::homePositionChanged, this, &MissionSettingsItem::_updateHomePosition); - _updateHomePosition(_managerVehicle->homePosition()); + connect(_managerVehicle, &Vehicle::homePositionChanged, this, &MissionSettingsItem::_updateFlyViewHomePosition); + _updateFlyViewHomePosition(_managerVehicle->homePosition()); } int MissionSettingsItem::lastSequenceNumber(void) const @@ -171,53 +173,11 @@ void MissionSettingsItem::_setDirty(void) setDirty(true); } -void MissionSettingsItem::_setCoordinateWorker(const QGeoCoordinate& coordinate) +void MissionSettingsItem::setCoordinate(const QGeoCoordinate& coordinate) { if (_plannedHomePositionCoordinate != coordinate) { _plannedHomePositionCoordinate = coordinate; emit coordinateChanged(coordinate); - emit exitCoordinateChanged(coordinate); - if (_plannedHomePositionFromVehicle) { - _plannedHomePositionAltitudeFact.setRawValue(coordinate.altitude()); - } - } -} - -void MissionSettingsItem::setHomePositionFromVehicle(Vehicle* vehicle) -{ - // If the user hasn't moved the planned home position manually we use the value from the vehicle - if (!_plannedHomePositionMovedByUser) { - QGeoCoordinate coordinate = vehicle->homePosition(); - // ArduPilot tends to send crap home positions at initial vehicle boot, discard them - if (coordinate.isValid() && (coordinate.latitude() != 0 || coordinate.longitude() != 0)) { - _plannedHomePositionFromVehicle = true; - _setCoordinateWorker(coordinate); - } - } -} - -void MissionSettingsItem::setInitialHomePosition(const QGeoCoordinate& coordinate) -{ - _plannedHomePositionMovedByUser = false; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); -} - -void MissionSettingsItem::setInitialHomePositionFromUser(const QGeoCoordinate& coordinate) -{ - _plannedHomePositionMovedByUser = true; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); -} - - -void MissionSettingsItem::setCoordinate(const QGeoCoordinate& coordinate) -{ - if (coordinate != this->coordinate()) { - // The user is moving the planned home position manually. Stop tracking vehicle home position. - _plannedHomePositionMovedByUser = true; - _plannedHomePositionFromVehicle = false; - _setCoordinateWorker(coordinate); } } @@ -252,7 +212,6 @@ void MissionSettingsItem::_updateAltitudeInCoordinate(QVariant value) qCDebug(MissionSettingsItemLog) << "MissionSettingsItem::_updateAltitudeInCoordinate" << newAltitude; _plannedHomePositionCoordinate.setAltitude(newAltitude); emit coordinateChanged(_plannedHomePositionCoordinate); - emit exitCoordinateChanged(_plannedHomePositionCoordinate); } } @@ -267,7 +226,7 @@ double MissionSettingsItem::specifiedFlightSpeed(void) void MissionSettingsItem::_setHomeAltFromTerrain(double terrainAltitude) { - if (!_plannedHomePositionFromVehicle && !qIsNaN(terrainAltitude)) { + if (!qIsNaN(terrainAltitude)) { qCDebug(MissionSettingsItemLog) << "MissionSettingsItem::_setHomeAltFromTerrain" << terrainAltitude; _plannedHomePositionAltitudeFact.setRawValue(terrainAltitude); } @@ -275,10 +234,10 @@ void MissionSettingsItem::_setHomeAltFromTerrain(double terrainAltitude) QString MissionSettingsItem::abbreviation(void) const { - return _flyView ? tr("L") : tr("Launch"); + return _flyView ? tr("H") : tr("Home"); } -void MissionSettingsItem::_updateHomePosition(const QGeoCoordinate& homePosition) +void MissionSettingsItem::_updateFlyViewHomePosition(const QGeoCoordinate& homePosition) { if (_flyView) { setCoordinate(homePosition); diff --git a/src/MissionManager/MissionSettingsItem.h b/src/MissionManager/MissionSettingsItem.h index d03adbca1205..7b03566e6e23 100644 --- a/src/MissionManager/MissionSettingsItem.h +++ b/src/MissionManager/MissionSettingsItem.h @@ -38,15 +38,6 @@ class MissionSettingsItem : public ComplexMissionItem /// @return true: Mission end action was added bool addMissionEndAction(QList& items, int seqNum, QObject* missionItemParent); - /// Called to update home position coordinate when it comes from a connected vehicle - void setHomePositionFromVehicle(Vehicle* vehicle); - - // Called to set the initial home position. Vehicle can still update home position after this. - void setInitialHomePosition(const QGeoCoordinate& coordinate); - - // Called to set the initial home position specified by user. Vehicle will no longer affect home position. - void setInitialHomePositionFromUser(const QGeoCoordinate& coordinate); - // Overrides from ComplexMissionItem QString patternName (void) const final { return QString(); } double complexDistance (void) const final; @@ -63,11 +54,12 @@ class MissionSettingsItem : public ComplexMissionItem bool isStandaloneCoordinate (void) const final { return false; } bool specifiesCoordinate (void) const final; bool specifiesAltitudeOnly (void) const final { return false; } - QString commandDescription (void) const final { return tr("Mission Settings"); } - QString commandName (void) const final { return tr("Mission Settings"); } + QString commandDescription (void) const final { return tr("Initial Camera Settings"); } + QString commandName (void) const final { return tr("Initial Camera Settings"); } QString abbreviation (void) const final; QGeoCoordinate coordinate (void) const final { return _plannedHomePositionCoordinate; } // Includes altitude - QGeoCoordinate exitCoordinate (void) const final { return _plannedHomePositionCoordinate; } + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } + QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedGimbalYaw (void) final; double specifiedGimbalPitch (void) final; @@ -80,6 +72,7 @@ class MissionSettingsItem : public ComplexMissionItem void setCoordinate (const QGeoCoordinate& coordinate) final; // Should only be called if the end user is moving void setSequenceNumber (int sequenceNumber) final; void save (QJsonArray& missionItems) final; + double editableAlt (void) const final { return _plannedHomePositionAltitudeFact.rawValue().toDouble(); } double amslEntryAlt (void) const final { return _plannedHomePositionCoordinate.altitude(); } double amslExitAlt (void) const final { return amslEntryAlt(); } double minAMSLAltitude (void) const final { return amslEntryAlt(); } @@ -94,16 +87,13 @@ private slots: void _sectionDirtyChanged (bool dirty); void _updateAltitudeInCoordinate (QVariant value); void _setHomeAltFromTerrain (double terrainAltitude); - void _setCoordinateWorker (const QGeoCoordinate& coordinate); - void _updateHomePosition (const QGeoCoordinate& homePosition); + void _updateFlyViewHomePosition (const QGeoCoordinate& homePosition); private: Vehicle* _managerVehicle = nullptr; QGeoCoordinate _plannedHomePositionCoordinate; // Does not include altitude Fact _plannedHomePositionAltitudeFact; int _sequenceNumber = 0; - bool _plannedHomePositionFromVehicle = false; - bool _plannedHomePositionMovedByUser = false; CameraSection _cameraSection; SpeedSection _speedSection; diff --git a/src/MissionManager/PlanMasterController.cc b/src/MissionManager/PlanMasterController.cc index cfdbe94eb6d9..db4a8fbb2008 100644 --- a/src/MissionManager/PlanMasterController.cc +++ b/src/MissionManager/PlanMasterController.cc @@ -22,8 +22,9 @@ #include #include -#include #include +#include +#include QGC_LOGGING_CATEGORY(PlanMasterControllerLog, "PlanManager.PlanMasterController") @@ -55,7 +56,6 @@ PlanMasterController::PlanMasterController(MAV_AUTOPILOT firmwareType, MAV_TYPE void PlanMasterController::_commonInit(void) { - _previousOverallDirty = dirty(); connect(&_missionController, &MissionController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); connect(&_geoFenceController, &GeoFenceController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); connect(&_rallyPointController, &RallyPointController::dirtyChanged, this, &PlanMasterController::_updateOverallDirty); @@ -157,8 +157,11 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) } else { // We are in the Plan view. if (containsItems()) { + // We have a plan which is from a different vehicle than the new active vehicle. By definition this plan requires and upload. + _setDirtyForUpload(true); + // The plan view has a stale plan in it - if (dirty()) { + if (dirtyForSave()) { // Plan is dirty, the user must decide what to do in all cases qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - Previous dirty plan exists, no new active vehicle, sending promptForPlanUsageOnVehicleChange signal"; emit promptForPlanUsageOnVehicleChange(); @@ -176,6 +179,7 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) } } else { // There is no previous Plan in the view + _setDirtyStates(false, false); if (newOffline) { // Nothing special to do in this case qCDebug(PlanMasterControllerLog) << "_activeVehicleChanged: Plan View - No previous plan, no longer connected to vehicle, nothing to do"; @@ -190,7 +194,8 @@ void PlanMasterController::_activeVehicleChanged(Vehicle* activeVehicle) // Vehicle changed so we need to signal everything emit containsItemsChanged(); emit syncInProgressChanged(); - emit dirtyChanged(dirty()); + emit dirtyForSaveChanged(dirtyForSave()); + emit dirtyForUploadChanged(dirtyForUpload()); _updatePlanCreatorsList(); } @@ -209,16 +214,15 @@ void PlanMasterController::loadFromVehicle(void) } if (offline()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while offline"; } else if (_flyView) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called from Fly view"; } else if (syncInProgress()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle called while syncInProgress"; } else { _loadGeoFence = true; qCDebug(PlanMasterControllerLog) << "PlanMasterController::loadFromVehicle calling _missionController.loadFromVehicle"; _missionController.loadFromVehicle(); - setDirty(false); } } @@ -236,7 +240,6 @@ void PlanMasterController::_loadMissionComplete(void) _geoFenceController.removeAll(); _loadGeoFenceComplete(); } - setDirty(false); } } @@ -252,13 +255,13 @@ void PlanMasterController::_loadGeoFenceComplete(void) _rallyPointController.removeAll(); _loadRallyPointsComplete(); } - setDirty(false); } } void PlanMasterController::_loadRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::_loadRallyPointsComplete"; + _setDirtyStates(containsItems() /* dirtyForSave */, false /* dirtyForUpload */); } void PlanMasterController::_sendMissionComplete(void) @@ -273,7 +276,6 @@ void PlanMasterController::_sendMissionComplete(void) qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle GeoFence not supported skipping"; _sendGeoFenceComplete(); } - setDirty(false); } } @@ -294,6 +296,7 @@ void PlanMasterController::_sendGeoFenceComplete(void) void PlanMasterController::_sendRallyPointsComplete(void) { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle Rally Point send complete"; + _setDirtyForUpload(false); if (_deleteWhenSendCompleted) { this->deleteLater(); } @@ -313,14 +316,13 @@ void PlanMasterController::sendToVehicle(void) } if (offline()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle called while syncInProgress"; } else { qCDebug(PlanMasterControllerLog) << "PlanMasterController::sendToVehicle start mission sendToVehicle"; _sendGeoFence = true; _missionController.sendToVehicle(); - setDirty(false); } } @@ -389,15 +391,44 @@ void PlanMasterController::loadFromFile(const QString& filename) } } - if(success){ + if (success){ + const bool oldRenamed = planFileRenamed(); _currentPlanFile = QString::asprintf("%s/%s.%s", fileInfo.path().toLocal8Bit().data(), fileInfo.completeBaseName().toLocal8Bit().data(), AppSettings::planFileExtension); + const bool currentNameChanged = (_currentPlanFileName != fileInfo.completeBaseName()); + const bool originalNameChanged = (_originalPlanFileName != fileInfo.completeBaseName()); + _currentPlanFileName = fileInfo.completeBaseName(); + _originalPlanFileName = _currentPlanFileName; + _setDirtyStates(false /* dirtyForSave */, true /* dirtyForUpload */); + emit currentPlanFileChanged(); + if (currentNameChanged) { + emit currentPlanFileNameChanged(); + } + if (originalNameChanged) { + emit originalPlanFileNameChanged(); + } + if (oldRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } } else { + const bool hadFile = !_currentPlanFile.isEmpty(); + const bool hadCurrentName = !_currentPlanFileName.isEmpty(); + const bool hadOriginalName = !_originalPlanFileName.isEmpty(); + const bool wasRenamed = planFileRenamed(); _currentPlanFile.clear(); - } - emit currentPlanFileChanged(); - - if (!offline()) { - setDirty(true); + _currentPlanFileName.clear(); + _originalPlanFileName.clear(); + if (hadFile) { + emit currentPlanFileChanged(); + } + if (hadCurrentName) { + emit currentPlanFileNameChanged(); + } + if (hadOriginalName) { + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } } } @@ -423,18 +454,21 @@ QJsonDocument PlanMasterController::saveToJson() return QJsonDocument(planJson); } -void +bool PlanMasterController::saveToCurrent() { - if(!_currentPlanFile.isEmpty()) { - saveToFile(_currentPlanFile); + if (!_currentPlanFile.isEmpty()) { + const bool saveSuccess = saveToFile(_currentPlanFile); + return saveSuccess; } + + return false; } -void PlanMasterController::saveToFile(const QString& filename) +bool PlanMasterController::saveToFile(const QString& filename) { if (filename.isEmpty()) { - return; + return false; } QString planFilename = filename; @@ -446,21 +480,35 @@ void PlanMasterController::saveToFile(const QString& filename) if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString())); - _currentPlanFile.clear(); - emit currentPlanFileChanged(); + return false; } else { - QJsonDocument saveDoc = saveToJson(); - file.write(saveDoc.toJson()); + const QByteArray saveBytes = saveToJson().toJson(); + const qint64 bytesWritten = file.write(saveBytes); + if (bytesWritten != saveBytes.size()) { + qgcApp()->showAppMessage(tr("Plan save error %1 : %2").arg(filename).arg(file.errorString())); + return false; + } if(_currentPlanFile != planFilename) { _currentPlanFile = planFilename; emit currentPlanFileChanged(); } + const bool wasRenamed = planFileRenamed(); + const QString savedBaseName = QFileInfo(planFilename).completeBaseName(); + if (_currentPlanFileName != savedBaseName) { + _currentPlanFileName = savedBaseName; + emit currentPlanFileNameChanged(); + } + if (_originalPlanFileName != savedBaseName) { + _originalPlanFileName = savedBaseName; + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } + _setDirtyForSave(false); } - // Only clear dirty bit if we are offline - if (offline()) { - setDirty(false); - } + return true; } void PlanMasterController::saveToKml(const QString& filename) @@ -489,15 +537,18 @@ void PlanMasterController::saveToKml(const QString& filename) void PlanMasterController::removeAll(void) { + _suppressOverallDirtyUpdate = true; _missionController.removeAll(); _geoFenceController.removeAll(); _rallyPointController.removeAll(); + _missionController.setDirty(false); + _geoFenceController.setDirty(false); + _rallyPointController.setDirty(false); + _suppressOverallDirtyUpdate = false; + + _setDirtyStates(false, false); if (_offline) { - _missionController.setDirty(false); - _geoFenceController.setDirty(false); - _rallyPointController.setDirty(false); - _currentPlanFile.clear(); - emit currentPlanFileChanged(); + _clearFileNames(); } setManualCreation(false); } @@ -512,9 +563,10 @@ void PlanMasterController::removeAllFromVehicle(void) if (_rallyPointController.supported()) { _rallyPointController.removeAllFromVehicle(); } - setDirty(false); + _setDirtyForUpload(false); + _clearFileNames(); } else { - qWarning() << "PlanMasterController::removeAllFromVehicle called while offline"; + qCCritical(PlanMasterControllerLog) << "PlanMasterController::removeAllFromVehicle called while offline"; } setManualCreation(false); } @@ -524,21 +576,81 @@ bool PlanMasterController::containsItems(void) const return _missionController.containsItems() || _geoFenceController.containsItems() || _rallyPointController.containsItems(); } -bool PlanMasterController::dirty(void) const +QString PlanMasterController::fileExtension(void) const { - return _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty(); + return AppSettings::planFileExtension; } -void PlanMasterController::setDirty(bool dirty) +void PlanMasterController::setCurrentPlanFileName(const QString& name) { - _missionController.setDirty(dirty); - _geoFenceController.setDirty(dirty); - _rallyPointController.setDirty(dirty); + // Normalize to a base name: trim whitespace, strip known extension, remove illegal characters + QString sanitized = name.trimmed(); + const QString ext = QStringLiteral(".") + fileExtension(); + if (sanitized.endsWith(ext, Qt::CaseInsensitive)) { + sanitized.chop(ext.length()); + sanitized = sanitized.trimmed(); + } + sanitized.remove(QRegularExpression(QStringLiteral("[/\\\\:*?\"<>|]"))); + if (_currentPlanFileName != sanitized) { + const bool wasRenamed = planFileRenamed(); + _currentPlanFileName = sanitized; + emit currentPlanFileNameChanged(); + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } + } } -QString PlanMasterController::fileExtension(void) const +bool PlanMasterController::saveWithCurrentName() { - return AppSettings::planFileExtension; + if (_currentPlanFileName.isEmpty()) { + return false; + } + return saveToFile(_resolvedPlanFilePath()); +} + +bool PlanMasterController::planFileRenamed() const +{ + return !_originalPlanFileName.isEmpty() && _currentPlanFileName != _originalPlanFileName; +} + +bool PlanMasterController::resolvedPlanFileExists() const +{ + if (_currentPlanFileName.isEmpty()) { + return false; + } + return QFile::exists(_resolvedPlanFilePath()); +} + +QString PlanMasterController::_resolvedPlanFilePath() const +{ + const QString dir = _currentPlanFile.isEmpty() + ? SettingsManager::instance()->appSettings()->missionSavePath() + : QFileInfo(_currentPlanFile).path(); + return QStringLiteral("%1/%2.%3").arg(dir, _currentPlanFileName, fileExtension()); +} + +void PlanMasterController::_clearFileNames() +{ + const bool hadFile = !_currentPlanFile.isEmpty(); + const bool hadCurrentName = !_currentPlanFileName.isEmpty(); + const bool hadOriginalName = !_originalPlanFileName.isEmpty(); + const bool wasRenamed = planFileRenamed(); + _currentPlanFile.clear(); + _currentPlanFileName.clear(); + _originalPlanFileName.clear(); + if (hadFile) { + emit currentPlanFileChanged(); + } + if (hadCurrentName) { + emit currentPlanFileNameChanged(); + } + if (hadOriginalName) { + emit originalPlanFileNameChanged(); + } + if (wasRenamed != planFileRenamed()) { + emit planFileRenamedChanged(); + } } QString PlanMasterController::kmlFileExtension(void) const @@ -575,9 +687,9 @@ void PlanMasterController::sendPlanToVehicle(Vehicle* vehicle, const QString& fi void PlanMasterController::_showPlanFromManagerVehicle(void) { - if (!_managerVehicle->initialPlanRequestComplete() && !syncInProgress()) { - // Something went wrong with initial load. All controllers are idle, so just force it off - _managerVehicle->forceInitialPlanRequestComplete(); + if (!_managerVehicle->initialPlanRequestComplete()) { + // We need to wait until initial load is complete before we show anything. + return; } // The crazy if structure is to handle the load propagating by itself through the system @@ -586,6 +698,12 @@ void PlanMasterController::_showPlanFromManagerVehicle(void) _rallyPointController.showPlanFromManagerVehicle(); } } + + // Showing the vehicle plan should leave both dirty states clean. + _missionController.setDirty(false); + _geoFenceController.setDirty(false); + _rallyPointController.setDirty(false); + _setDirtyStates(false, false); } bool PlanMasterController::syncInProgress(void) const @@ -604,9 +722,49 @@ bool PlanMasterController::isEmpty(void) const void PlanMasterController::_updateOverallDirty(void) { - if(_previousOverallDirty != dirty()){ - _previousOverallDirty = dirty(); - emit dirtyChanged(_previousOverallDirty); + if (syncInProgress() || _suppressOverallDirtyUpdate) { + return; + } + + const bool saveDirty = _missionController.dirty() || _geoFenceController.dirty() || _rallyPointController.dirty(); + if (saveDirty) { + _setDirtyForSave(true); + } +} + +void PlanMasterController::_setDirtyForSave(bool dirtyForSave) +{ + if (_dirtyForSave != dirtyForSave) { + _dirtyForSave = dirtyForSave; + emit dirtyForSaveChanged(_dirtyForSave); + + if (_dirtyForSave) { + _setDirtyForUpload(true); + } + } +} + +void PlanMasterController::_setDirtyForUpload(bool dirtyForUpload) +{ + if (_dirtyForUpload != dirtyForUpload) { + _dirtyForUpload = dirtyForUpload; + emit dirtyForUploadChanged(_dirtyForUpload); + } +} + +void PlanMasterController::_setDirtyStates(bool dirtyForSave, bool dirtyForUpload) +{ + const bool saveChanged = (_dirtyForSave != dirtyForSave); + const bool uploadChanged = (_dirtyForUpload != dirtyForUpload); + + _dirtyForSave = dirtyForSave; + _dirtyForUpload = dirtyForUpload; + + if (saveChanged) { + emit dirtyForSaveChanged(_dirtyForSave); + } + if (uploadChanged) { + emit dirtyForUploadChanged(_dirtyForUpload); } } diff --git a/src/MissionManager/PlanMasterController.h b/src/MissionManager/PlanMasterController.h index 7f2c08dcb1a9..9cbe6cb86226 100644 --- a/src/MissionManager/PlanMasterController.h +++ b/src/MissionManager/PlanMasterController.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -14,6 +15,9 @@ class QGCCompressionJob; class QmlObjectListModel; class MultiVehicleManager; class Vehicle; +#ifdef QGC_UNITTEST_BUILD +class PlanMasterControllerTest; +#endif /// Master controller for mission, fence, rally class PlanMasterController : public QObject @@ -23,6 +27,10 @@ class PlanMasterController : public QObject Q_MOC_INCLUDE("QmlObjectListModel.h") Q_MOC_INCLUDE("Vehicle.h") +#ifdef QGC_UNITTEST_BUILD + friend class PlanMasterControllerTest; +#endif + public: PlanMasterController(QObject* parent = nullptr); #ifdef QT_DEBUG @@ -41,10 +49,14 @@ class PlanMasterController : public QObject Q_PROPERTY(bool offline READ offline NOTIFY offlineChanged) ///< true: controller is not connected to an active vehicle Q_PROPERTY(bool containsItems READ containsItems NOTIFY containsItemsChanged) ///< true: Elemement is non-empty Q_PROPERTY(bool syncInProgress READ syncInProgress NOTIFY syncInProgressChanged) ///< true: Information is currently being saved/sent, false: no active save/send in progress - Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) ///< true: Unsaved/sent changes are present, false: no changes since last save/send + Q_PROPERTY(bool dirtyForSave READ dirtyForSave NOTIFY dirtyForSaveChanged) ///< true: Unsaved changes to disk are present, false: no changes since last save to disk + Q_PROPERTY(bool dirtyForUpload READ dirtyForUpload NOTIFY dirtyForUploadChanged) ///< true: Unsent changes are present, false: no changes since last upload/download sync Q_PROPERTY(QString fileExtension READ fileExtension CONSTANT) ///< File extension for missions Q_PROPERTY(QString kmlFileExtension READ kmlFileExtension CONSTANT) - Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) + Q_PROPERTY(QString currentPlanFile READ currentPlanFile NOTIFY currentPlanFileChanged) ///< Fully qualified path, empty if not yet saved + Q_PROPERTY(QString currentPlanFileName READ currentPlanFileName WRITE setCurrentPlanFileName NOTIFY currentPlanFileNameChanged) ///< Editable base name, no path or extension + Q_PROPERTY(QString originalPlanFileName READ originalPlanFileName NOTIFY originalPlanFileNameChanged) ///< On-disk name when last loaded/saved + Q_PROPERTY(bool planFileRenamed READ planFileRenamed NOTIFY planFileRenamedChanged) ///< true if currentPlanFileName differs from originalPlanFileName Q_PROPERTY(QStringList loadNameFilters READ loadNameFilters CONSTANT) ///< File filter list loading plan files Q_PROPERTY(QStringList saveNameFilters READ saveNameFilters CONSTANT) ///< File filter list saving plan files Q_PROPERTY(QmlObjectListModel* planCreators MEMBER _planCreators NOTIFY planCreatorsChanged) @@ -79,9 +91,12 @@ class PlanMasterController : public QObject /// @param archivePath Path to the archive file Q_INVOKABLE void loadFromArchive(const QString& archivePath); - Q_INVOKABLE void saveToCurrent(); - Q_INVOKABLE void saveToFile(const QString& filename); + Q_INVOKABLE bool saveToCurrent(); + Q_INVOKABLE bool saveToFile(const QString& filename); Q_INVOKABLE void saveToKml(const QString& filename); + + Q_INVOKABLE bool saveWithCurrentName(); ///< Save using the (possibly renamed) currentPlanFileName + Q_INVOKABLE bool resolvedPlanFileExists() const; ///< true if a file at the renamed path already exists on disk Q_INVOKABLE void removeAll(void); ///< Removes all from controller only, synce required to remove from vehicle Q_INVOKABLE void removeAllFromVehicle(void); ///< Removes all from vehicle and controller @@ -92,11 +107,15 @@ class PlanMasterController : public QObject bool offline (void) const { return _offline; } bool containsItems (void) const; bool syncInProgress (void) const; - bool dirty (void) const; - void setDirty (bool dirty); + bool dirtyForSave (void) const { return _dirtyForSave; } + bool dirtyForUpload (void) const { return _dirtyForUpload; } QString fileExtension (void) const; QString kmlFileExtension(void) const; - QString currentPlanFile (void) const { return _currentPlanFile; } + QString currentPlanFile (void) const { return _currentPlanFile; } + QString currentPlanFileName (void) const { return _currentPlanFileName; } + void setCurrentPlanFileName(const QString& name); + QString originalPlanFileName(void) const { return _originalPlanFileName; } + bool planFileRenamed(void) const; QStringList loadNameFilters (void) const; QStringList saveNameFilters (void) const; bool isEmpty (void) const; @@ -119,9 +138,13 @@ class PlanMasterController : public QObject signals: void containsItemsChanged (); void syncInProgressChanged (void); - void dirtyChanged (bool dirty); + void dirtyForSaveChanged (bool dirtyForSave); + void dirtyForUploadChanged (bool dirtyForUpload); void offlineChanged (bool offlineEditing); void currentPlanFileChanged (void); + void currentPlanFileNameChanged (void); + void originalPlanFileNameChanged (void); + void planFileRenamedChanged (void); void planCreatorsChanged (QmlObjectListModel* planCreators); void managerVehicleChanged (Vehicle* managerVehicle); void promptForPlanUsageOnVehicleChange (void); @@ -142,6 +165,17 @@ private slots: private: void _commonInit (void); void _showPlanFromManagerVehicle(void); + void _setDirtyForSave(bool dirtyForSave); + void _setDirtyForUpload(bool dirtyForUpload); + void _setDirtyStates(bool dirtyForSave, bool dirtyForUpload); + QString _resolvedPlanFilePath() const; + void _clearFileNames(); + +#ifdef QGC_UNITTEST_BUILD + // Used by unit tests to set dirty flags for initial state + void _setDirtyForSaveUnitTest(bool dirtyForSave) { _dirtyForSave = dirtyForSave; } + void _setDirtyForUploadUnitTest(bool dirtyForUpload) { _dirtyForUpload = dirtyForUpload; } +#endif MultiVehicleManager* _multiVehicleMgr = nullptr; Vehicle* _controllerVehicle = nullptr; ///< Offline controller vehicle @@ -156,8 +190,15 @@ private slots: bool _sendGeoFence = false; bool _sendRallyPoints = false; QString _currentPlanFile; + // NOTE: _currentPlanFileName and _originalPlanFileName must be kept in sync + // with _currentPlanFile across all code paths that modify any of them: + // loadFromFile, saveToFile, removeAll, removeAllFromVehicle, _clearFileNames. + QString _currentPlanFileName; + QString _originalPlanFileName; bool _deleteWhenSendCompleted = false; - bool _previousOverallDirty = false; + bool _dirtyForSave = false; + bool _dirtyForUpload = false; + bool _suppressOverallDirtyUpdate = false; QmlObjectListModel* _planCreators = nullptr; bool _manualCreation = false; QGCCompressionJob* _extractionJob = nullptr; diff --git a/src/MissionManager/RallyPoint.FactMetaData.json b/src/MissionManager/RallyPoint.FactMetaData.json index 789eaf061978..8bb1a7c81d2b 100644 --- a/src/MissionManager/RallyPoint.FactMetaData.json +++ b/src/MissionManager/RallyPoint.FactMetaData.json @@ -5,23 +5,23 @@ [ { "name": "Latitude", - "shortDesc": "Latitude of rally point position", + "shortDesc": "Latitude", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 6 }, { "name": "Longitude", - "shortDesc": "Longitude of rally point position", + "shortDesc": "Longitude", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 6 }, { "name": "RelativeAltitude", - "shortDesc": "Altitude of rally point position (home relative)", + "shortDesc": "Altitude (rel)", "type": "double", "decimalPlaces": 2, "units": "m", - "default": 0.0 + "default": 0.0 } ] } diff --git a/src/MissionManager/RallyPoint.cc b/src/MissionManager/RallyPoint.cc index 1205313244c2..4ba58b3635aa 100644 --- a/src/MissionManager/RallyPoint.cc +++ b/src/MissionManager/RallyPoint.cc @@ -48,16 +48,16 @@ void RallyPoint::_factSetup(void) { _cacheFactMetadata(); - _longitudeFact.setMetaData(_metaDataMap[_longitudeFactName]); _latitudeFact.setMetaData(_metaDataMap[_latitudeFactName]); + _longitudeFact.setMetaData(_metaDataMap[_longitudeFactName]); _altitudeFact.setMetaData(_metaDataMap[_altitudeFactName]); - _textFieldFacts.append(QVariant::fromValue(&_longitudeFact)); _textFieldFacts.append(QVariant::fromValue(&_latitudeFact)); + _textFieldFacts.append(QVariant::fromValue(&_longitudeFact)); _textFieldFacts.append(QVariant::fromValue(&_altitudeFact)); - connect(&_longitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); connect(&_latitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); + connect(&_longitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); connect(&_altitudeFact, &Fact::valueChanged, this, &RallyPoint::_sendCoordinateChanged); } diff --git a/src/MissionManager/RallyPointController.cc b/src/MissionManager/RallyPointController.cc index c37b031660c7..46c46745b066 100644 --- a/src/MissionManager/RallyPointController.cc +++ b/src/MissionManager/RallyPointController.cc @@ -133,9 +133,9 @@ void RallyPointController::removeAll(void) void RallyPointController::removeAllFromVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::removeAllFromVehicle called while syncInProgress"; } else { _rallyPointManager->removeAll(); } @@ -144,9 +144,9 @@ void RallyPointController::removeAllFromVehicle(void) void RallyPointController::loadFromVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::loadFromVehicle called while syncInProgress"; } else { _itemsRequested = true; _rallyPointManager->loadFromVehicle(); @@ -156,9 +156,9 @@ void RallyPointController::loadFromVehicle(void) void RallyPointController::sendToVehicle(void) { if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while offline"; } else if (syncInProgress()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while syncInProgress"; + qCCritical(RallyPointControllerLog) << "RallyPointController::sendToVehicle called while syncInProgress"; } else { qCDebug(RallyPointControllerLog) << "RallyPointController::sendToVehicle"; setDirty(false); @@ -290,7 +290,7 @@ bool RallyPointController::showPlanFromManagerVehicle (void) { qCDebug(RallyPointControllerLog) << "showPlanFromManagerVehicle _flyView" << _flyView; if (_masterController->offline()) { - qCWarning(RallyPointControllerLog) << "RallyPointController::showPlanFromManagerVehicle called while offline"; + qCCritical(RallyPointControllerLog) << "RallyPointController::showPlanFromManagerVehicle called while offline"; return true; // stops further propagation of showPlanFromManagerVehicle due to error } else { if (!_managerVehicle->initialPlanRequestComplete()) { diff --git a/src/MissionManager/SimpleMissionItem.cc b/src/MissionManager/SimpleMissionItem.cc index 4b50e43891eb..5014d8d8fe59 100644 --- a/src/MissionManager/SimpleMissionItem.cc +++ b/src/MissionManager/SimpleMissionItem.cc @@ -36,7 +36,7 @@ SimpleMissionItem::SimpleMissionItem(PlanMasterController* masterController, boo , _param6MetaData (FactMetaData::valueTypeDouble) , _param7MetaData (FactMetaData::valueTypeDouble) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); _setupMetaData(); @@ -64,23 +64,23 @@ SimpleMissionItem::SimpleMissionItem(PlanMasterController* masterController, boo , _param6MetaData (FactMetaData::valueTypeDouble) , _param7MetaData (FactMetaData::valueTypeDouble) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); - struct MavFrame2AltMode_s { + struct MavFrame2AltFrame_s { MAV_FRAME mavFrame; - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; }; - const struct MavFrame2AltMode_s rgMavFrame2AltMode[] = { - { MAV_FRAME_GLOBAL_TERRAIN_ALT, QGroundControlQmlGlobal::AltitudeModeTerrainFrame }, - { MAV_FRAME_GLOBAL, QGroundControlQmlGlobal::AltitudeModeAbsolute }, - { MAV_FRAME_GLOBAL_RELATIVE_ALT, QGroundControlQmlGlobal::AltitudeModeRelative }, + const struct MavFrame2AltFrame_s rgMavFrame2AltFrame[] = { + { MAV_FRAME_GLOBAL_TERRAIN_ALT, QGroundControlQmlGlobal::AltitudeFrameTerrain }, + { MAV_FRAME_GLOBAL, QGroundControlQmlGlobal::AltitudeFrameAbsolute }, + { MAV_FRAME_GLOBAL_RELATIVE_ALT, QGroundControlQmlGlobal::AltitudeFrameRelative }, }; - _altitudeMode = QGroundControlQmlGlobal::AltitudeModeRelative; - for (size_t i=0; iappSettings()->defaultMissionItemAltitude()->rawValue().toDouble(); _altitudeFact.setRawValue(defaultAlt); _missionItem._param7Fact.setRawValue(defaultAlt); - // Note that setAltitudeMode will also set MAV_FRAME correctly through signalling + // Note that setAltitudeFrame will also set MAV_FRAME correctly through signalling // Takeoff items always use relative alt since that is the highest quality data to base altitude from - setAltitudeMode(isTakeoffItem() ? QGroundControlQmlGlobal::AltitudeModeRelative : _missionController->globalAltitudeModeDefault()); + setAltitudeFrame(isTakeoffItem() ? QGroundControlQmlGlobal::AltitudeFrameRelative : _missionController->globalAltitudeFrameDefault()); } else { _altitudeFact.setRawValue(0); _missionItem._param7Fact.setRawValue(0); @@ -1057,11 +1058,11 @@ void SimpleMissionItem::setMissionFlightStatus(MissionController::MissionFlightS } } -void SimpleMissionItem::setAltitudeMode(QGroundControlQmlGlobal::AltMode altitudeMode) +void SimpleMissionItem::setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame) { - if (altitudeMode != _altitudeMode) { - _altitudeMode = altitudeMode; - emit altitudeModeChanged(); + if (altitudeFrame != _altitudeFrame) { + _altitudeFrame = altitudeFrame; + emit altitudeFrameChanged(); } } @@ -1104,25 +1105,30 @@ QGeoCoordinate SimpleMissionItem::coordinate(void) const } } +double SimpleMissionItem::editableAlt() const +{ + return _missionItem.param7(); +} + double SimpleMissionItem::amslEntryAlt(void) const { - switch (_altitudeMode) { - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + switch (_altitudeFrame) { + case QGroundControlQmlGlobal::AltitudeFrameTerrain: return _missionItem.param7() + _terrainAltitude; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: return _missionItem.param7(); - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: return _missionItem.param7() + _masterController->missionController()->plannedHomePosition().altitude(); - case QGroundControlQmlGlobal::AltitudeModeNone: - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:AltitudeModeNone"; + case QGroundControlQmlGlobal::AltitudeFrameNone: + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:AltitudeFrameNone"; return qQNaN(); - case QGroundControlQmlGlobal::AltitudeModeMixed: - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:AltitudeModeMixed"; + case QGroundControlQmlGlobal::AltitudeFrameMixed: + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:AltitudeFrameMixed"; return qQNaN(); } - qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeMode:" << _altitudeMode; + qWarning() << "Internal Error SimpleMissionItem::amslEntryAlt: Invalid altitudeFrame:" << _altitudeFrame; return qQNaN(); } diff --git a/src/MissionManager/SimpleMissionItem.h b/src/MissionManager/SimpleMissionItem.h index 6a071d9db836..3e275d622b14 100644 --- a/src/MissionManager/SimpleMissionItem.h +++ b/src/MissionManager/SimpleMissionItem.h @@ -24,9 +24,9 @@ class SimpleMissionItem : public VisualMissionItem Q_PROPERTY(bool friendlyEditAllowed READ friendlyEditAllowed NOTIFY friendlyEditAllowedChanged) Q_PROPERTY(bool rawEdit READ rawEdit WRITE setRawEdit NOTIFY rawEditChanged) ///< true: raw item editing with all params Q_PROPERTY(bool specifiesAltitude READ specifiesAltitude NOTIFY commandChanged) - Q_PROPERTY(Fact* altitude READ altitude CONSTANT) ///< Altitude as specified by altitudeMode. Not necessarily true mission item altitude - Q_PROPERTY(QGroundControlQmlGlobal::AltMode altitudeMode READ altitudeMode WRITE setAltitudeMode NOTIFY altitudeModeChanged) - Q_PROPERTY(Fact* amslAltAboveTerrain READ amslAltAboveTerrain CONSTANT) ///< Actual AMSL altitude for item if altitudeMode == AltitudeAboveTerrain + Q_PROPERTY(Fact* altitude READ altitude CONSTANT) ///< Altitude as specified by altitudeFrame. Not necessarily true mission item altitude + Q_PROPERTY(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame READ altitudeFrame WRITE setAltitudeFrame NOTIFY altitudeFrameChanged) + Q_PROPERTY(Fact* amslAltAboveTerrain READ amslAltAboveTerrain CONSTANT) ///< Actual AMSL altitude for item if altitudeFrame is AltitudeFrameCalcAboveTerrain or AltitudeFrameTerrain Q_PROPERTY(int command READ command WRITE setCommand NOTIFY commandChanged) Q_PROPERTY(bool isLoiterItem READ isLoiterItem NOTIFY isLoiterItemChanged) Q_PROPERTY(bool showLoiterRadius READ showLoiterRadius NOTIFY showLoiterRadiusChanged) @@ -64,7 +64,7 @@ class SimpleMissionItem : public VisualMissionItem bool friendlyEditAllowed (void) const; bool rawEdit (void) const; bool specifiesAltitude (void) const; - QGroundControlQmlGlobal::AltMode altitudeMode(void) const { return _altitudeMode; } + QGroundControlQmlGlobal::AltitudeFrame altitudeFrame(void) const { return _altitudeFrame; } Fact* altitude (void) { return &_altitudeFact; } Fact* amslAltAboveTerrain (void) { return &_amslAltAboveTerrainFact; } bool isLoiterItem (void) const; @@ -82,7 +82,7 @@ class SimpleMissionItem : public VisualMissionItem QmlObjectListModel* comboboxFactsAdvanced (void) { return &_comboboxFactsAdvanced; } void setRawEdit(bool rawEdit); - void setAltitudeMode(QGroundControlQmlGlobal::AltMode altitudeMode); + void setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrame altitudeFrame); void setCommandByIndex(int index); @@ -111,7 +111,9 @@ class SimpleMissionItem : public VisualMissionItem QString commandName (void) const final; QString abbreviation (void) const final; QGeoCoordinate coordinate (void) const final; + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final { return amslEntryAlt(); } int sequenceNumber (void) const final { return _missionItem.sequenceNumber(); } @@ -140,7 +142,7 @@ class SimpleMissionItem : public VisualMissionItem void rawEditChanged (bool rawEdit); void cameraSectionChanged (QObject* cameraSection); void speedSectionChanged (QObject* cameraSection); - void altitudeModeChanged (void); + void altitudeFrameChanged (void); void isLoiterItemChanged (void); void showLoiterRadiusChanged (void); void loiterRadiusChanged (double loiterRadius); @@ -152,7 +154,7 @@ private slots: void _sendCoordinateChanged (void); void _sendFriendlyEditAllowedChanged (void); void _altitudeChanged (void); - void _altitudeModeChanged (void); + void _altitudeFrameChanged (void); void _terrainAltChanged (void); void _updateLastSequenceNumber (void); void _rebuildFacts (void); @@ -182,7 +184,7 @@ private slots: Fact _supportedCommandFact; - QGroundControlQmlGlobal::AltMode _altitudeMode = QGroundControlQmlGlobal::AltitudeModeRelative; + QGroundControlQmlGlobal::AltitudeFrame _altitudeFrame = QGroundControlQmlGlobal::AltitudeFrameRelative; Fact _altitudeFact; Fact _amslAltAboveTerrainFact; diff --git a/src/MissionManager/StructureScanComplexItem.cc b/src/MissionManager/StructureScanComplexItem.cc index 6fc2ba29467f..25d547219b53 100644 --- a/src/MissionManager/StructureScanComplexItem.cc +++ b/src/MissionManager/StructureScanComplexItem.cc @@ -31,7 +31,7 @@ StructureScanComplexItem::StructureScanComplexItem(PlanMasterController* masterC , _startFromTopFact (settingsGroup, _metaDataMap[startFromTopName]) , _entranceAltFact (settingsGroup, _metaDataMap[_entranceAltName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/StructureScanEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/StructureScanEditor.qml"; _entranceAltFact.setRawValue(SettingsManager::instance()->appSettings()->defaultMissionItemAltitude()->rawValue()); @@ -74,6 +74,9 @@ StructureScanComplexItem::StructureScanComplexItem(PlanMasterController* masterC connect(this, &StructureScanComplexItem::wizardModeChanged, this, &StructureScanComplexItem::readyForSaveStateChanged); + connect(this, &StructureScanComplexItem::coordinateChanged, this, &StructureScanComplexItem::entryCoordinateChanged); + connect(this, &StructureScanComplexItem::coordinateChanged, this, &StructureScanComplexItem::exitCoordinateChanged); + connect(&_entranceAltFact, &Fact::valueChanged, this, &StructureScanComplexItem::_amslEntryAltChanged); connect(this, &StructureScanComplexItem::amslEntryAltChanged, this, &StructureScanComplexItem::amslExitAltChanged); @@ -270,7 +273,6 @@ void StructureScanComplexItem::_flightPathChanged(void) QGeoCoordinate(south - 90.0, east - 180.0, top))); emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); emit greatestDistanceToChanged(); if (_isIncomplete) { @@ -466,6 +468,26 @@ void StructureScanComplexItem::applyNewAltitude(double newAltitude) _entranceAltFact.setRawValue(newAltitude); } +void StructureScanComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + const QGeoCoordinate oldCoordinate = this->coordinate(); + if (!oldCoordinate.isValid() || !coordinate.isValid() || _structurePolygon.count() < 3) { + return; + } + + const double distanceMeters = oldCoordinate.distanceTo(coordinate); + const double azimuthDegrees = oldCoordinate.azimuthTo(coordinate); + const QList vertices = _structurePolygon.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _structurePolygon.setPath(translatedVertices); +} + void StructureScanComplexItem::_polygonDirtyChanged(bool dirty) { if (dirty) { @@ -491,7 +513,6 @@ QGeoCoordinate StructureScanComplexItem::coordinate(void) const void StructureScanComplexItem::_updateCoordinateAltitudes(void) { emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::rotateEntryPoint(void) @@ -501,7 +522,6 @@ void StructureScanComplexItem::rotateEntryPoint(void) _entryVertex = 0; } emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::_rebuildFlightPolygon(void) @@ -521,7 +541,6 @@ void StructureScanComplexItem::_rebuildFlightPolygon(void) } emit coordinateChanged(coordinate()); - emit exitCoordinateChanged(exitCoordinate()); } void StructureScanComplexItem::_recalcCameraShots(void) @@ -640,6 +659,11 @@ void StructureScanComplexItem::_updateWizardMode(void) } } +double StructureScanComplexItem::editableAlt() const +{ + return _entranceAltFact.rawValue().toDouble(); +} + double StructureScanComplexItem::amslEntryAlt(void) const { return _entranceAltFact.rawValue().toDouble() + _missionController->plannedHomePosition().altitude(); diff --git a/src/MissionManager/StructureScanComplexItem.h b/src/MissionManager/StructureScanComplexItem.h index e7b4d6d85987..79c322279136 100644 --- a/src/MissionManager/StructureScanComplexItem.h +++ b/src/MissionManager/StructureScanComplexItem.h @@ -70,6 +70,7 @@ class StructureScanComplexItem : public ComplexMissionItem QString commandName (void) const final { return tr("Structure Scan"); } QString abbreviation (void) const final { return "S"; } QGeoCoordinate coordinate (void) const final; + QGeoCoordinate entryCoordinate (void) const final { return coordinate(); } QGeoCoordinate exitCoordinate (void) const final { return coordinate(); } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } @@ -82,9 +83,10 @@ class StructureScanComplexItem : public ComplexMissionItem ReadyForSaveState readyForSaveState (void) const final; bool exitCoordinateSameAsEntry (void) const final { return true; } void setDirty (bool dirty) final; - void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } + void setCoordinate (const QGeoCoordinate& coordinate) final; void setSequenceNumber (int sequenceNumber) final; void save (QJsonArray& missionItems) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final { return amslEntryAlt(); }; double minAMSLAltitude (void) const final; diff --git a/src/MissionManager/SurveyComplexItem.cc b/src/MissionManager/SurveyComplexItem.cc index 095035616ee7..2ccd192ed7fb 100644 --- a/src/MissionManager/SurveyComplexItem.cc +++ b/src/MissionManager/SurveyComplexItem.cc @@ -26,7 +26,7 @@ SurveyComplexItem::SurveyComplexItem(PlanMasterController* masterController, boo , _splitConcavePolygonsFact (settingsGroup, _metaDataMap[splitConcavePolygonsName]) , _entryPoint (EntryLocationTopLeft) { - _editorQml = "qrc:/qml/QGroundControl/Controls/SurveyItemEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/SurveyItemEditor.qml"; if (_controllerVehicle && !(_controllerVehicle->fixedWing() || _controllerVehicle->vtol())) { // Only fixed wing flight paths support alternate transects @@ -239,7 +239,7 @@ bool SurveyComplexItem::_loadV3(const QJsonObject& complexObject, int sequenceNu _cameraTriggerInTurnAroundFact.setRawValue (complexObject[_jsonV3CameraTriggerInTurnaroundKey].toBool(true)); _cameraCalc.valueSetIsDistance()->setRawValue (complexObject[_jsonV3FixedValueIsAltitudeKey].toBool(true)); - _cameraCalc.setDistanceMode(complexObject[_jsonV3GridAltitudeRelativeKey].toBool(true) ? QGroundControlQmlGlobal::AltitudeModeRelative : QGroundControlQmlGlobal::AltitudeModeAbsolute); + _cameraCalc.setDistanceMode(complexObject[_jsonV3GridAltitudeRelativeKey].toBool(true) ? QGroundControlQmlGlobal::AltitudeFrameRelative : QGroundControlQmlGlobal::AltitudeFrameAbsolute); bool manualGrid = complexObject[_jsonV3ManualGridKey].toBool(true); diff --git a/src/MissionManager/TakeoffMissionItem.cc b/src/MissionManager/TakeoffMissionItem.cc index f2e9d2dd1635..1536a6fa6b52 100644 --- a/src/MissionManager/TakeoffMissionItem.cc +++ b/src/MissionManager/TakeoffMissionItem.cc @@ -37,7 +37,7 @@ TakeoffMissionItem::~TakeoffMissionItem() void TakeoffMissionItem::_init(bool forLoad) { - _editorQml = QStringLiteral("qrc:/qml/QGroundControl/Controls/SimpleItemEditor.qml"); + _editorQml = QStringLiteral("qrc:/qml/QGroundControl/PlanView/SimpleItemEditor.qml"); connect(_settingsItem, &MissionSettingsItem::coordinateChanged, this, &TakeoffMissionItem::launchCoordinateChanged); @@ -167,7 +167,7 @@ void TakeoffMissionItem::_setLaunchCoordinate(const QGeoCoordinate& launchCoordi if (_controllerVehicle->fixedWing()) { double altitude = this->altitude()->rawValue().toDouble(); - if (altitudeMode() == QGroundControlQmlGlobal::AltitudeModeRelative) { + if (altitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameRelative) { // Offset for fixed wing climb out of 30 degrees to specified altitude if (altitude != 0.0) { distance = altitude / tan(qDegreesToRadians(30.0)); diff --git a/src/MissionManager/TransectStyleComplexItem.cc b/src/MissionManager/TransectStyleComplexItem.cc index 88a8754fca30..3cf5efec3c15 100644 --- a/src/MissionManager/TransectStyleComplexItem.cc +++ b/src/MissionManager/TransectStyleComplexItem.cc @@ -87,6 +87,9 @@ TransectStyleComplexItem::TransectStyleComplexItem(PlanMasterController* masterC connect(&_hoverAndCaptureFact, &Fact::rawValueChanged, this, &TransectStyleComplexItem::_handleHoverAndCaptureEnabled); + // The main coordinate is aliased to the entry + connect(this, &TransectStyleComplexItem::entryCoordinateChanged, this, &TransectStyleComplexItem::coordinateChanged); + connect(this, &TransectStyleComplexItem::visualTransectPointsChanged, this, &TransectStyleComplexItem::complexDistanceChanged); connect(this, &TransectStyleComplexItem::visualTransectPointsChanged, this, &TransectStyleComplexItem::greatestDistanceToChanged); connect(this, &TransectStyleComplexItem::wizardModeChanged, this, &TransectStyleComplexItem::readyForSaveStateChanged); @@ -117,6 +120,25 @@ void TransectStyleComplexItem::setDirty(bool dirty) } } +void TransectStyleComplexItem::setCoordinate(const QGeoCoordinate& coordinate) +{ + if (!coordinate.isValid() || !_entryCoordinate.isValid() || _surveyAreaPolygon.count() < 3) { + return; + } + + const double distanceMeters = _entryCoordinate.distanceTo(coordinate); + const double azimuthDegrees = _entryCoordinate.azimuthTo(coordinate); + const QList vertices = _surveyAreaPolygon.coordinateList(); + + QList translatedVertices; + translatedVertices.reserve(vertices.count()); + for (const QGeoCoordinate& vertex: vertices) { + translatedVertices.append(vertex.atDistanceAndAzimuth(distanceMeters, azimuthDegrees)); + } + + _surveyAreaPolygon.setPath(translatedVertices); +} + void TransectStyleComplexItem::_save(QJsonObject& complexObject) { QJsonObject innerObject; @@ -128,7 +150,7 @@ void TransectStyleComplexItem::_save(QJsonObject& complexObject) innerObject[refly90DegreesName] = _refly90DegreesFact.rawValue().toBool(); innerObject[_jsonCameraShotsKey] = _cameraShots; - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { innerObject[terrainAdjustToleranceName] = _terrainAdjustToleranceFact.rawValue().toDouble(); innerObject[terrainAdjustMaxClimbRateName] = _terrainAdjustMaxClimbRateFact.rawValue().toDouble(); innerObject[terrainAdjustMaxDescentRateName] = _terrainAdjustMaxDescentRateFact.rawValue().toDouble(); @@ -227,7 +249,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP if (!JsonHelper::loadGeoCoordinateArray(innerObject[_jsonVisualTransectPointsKey], false /* altitudeRequired */, _visualTransectPoints, errorString)) { return false; } - _coordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); + _entryCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); _exitCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.last().value() : QGeoCoordinate(); _isIncomplete = false; @@ -262,7 +284,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP _cameraShots = innerObject[_jsonCameraShotsKey].toInt(); } - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { QList followTerrainKeyInfoList = { { terrainAdjustToleranceName, QJsonValue::Double, true }, { terrainAdjustMaxClimbRateName, QJsonValue::Double, true }, @@ -295,7 +317,7 @@ bool TransectStyleComplexItem::_load(const QJsonObject& complexObject, bool forP } if (!forPresets) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeTerrainFrame) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameTerrain) { // Terrain frame requires terrain data in order to know AMSL coordinate heights for each mission item _queryMissionItemCoordHeights(); } else { @@ -349,7 +371,7 @@ void TransectStyleComplexItem::_setIfDirty(bool dirty) void TransectStyleComplexItem::_updateCoordinateAltitudes(void) { - emit coordinateChanged(coordinate()); + emit entryCoordinateChanged(entryCoordinate()); emit exitCoordinateChanged(exitCoordinate()); } @@ -388,18 +410,18 @@ void TransectStyleComplexItem::_rebuildTransects(void) _minAMSLAltitude = _maxAMSLAltitude = qQNaN(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _rebuildTransects - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: // Terrain height not needed to calculate path, as TerrainFrame specifies a fixed altitude over terrain, doesn't need to know the actual terrain height // so vehicle is responsible for having or not this altitude calculation, so we can build the flight path right away. _buildFlightPathCoordInfoFromTransects(); break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: // Query the terrain data. Once available flight path will be calculated, as on this mode QGC actually calculates the individual altitude for each waypoint // having into account terrain data. _queryTransectsPathHeightInfo(); @@ -434,10 +456,9 @@ void TransectStyleComplexItem::_rebuildTransects(void) QGeoCoordinate(south - 90.0, east - 180.0, top))); emit visualTransectPointsChanged(); - _coordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); + _entryCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.first().value() : QGeoCoordinate(); _exitCoordinate = _visualTransectPoints.count() ? _visualTransectPoints.last().value() : QGeoCoordinate(); - emit coordinateChanged(_coordinate); - emit exitCoordinateChanged(_exitCoordinate); + _updateCoordinateAltitudes(); if (_isIncomplete) { _isIncomplete = false; @@ -478,12 +499,12 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) _flightPathSegments.clearAndDeleteContents(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _updateFlightPathSegmentsDontCallDirectly - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: { // Since we aren't following terrain all the transects are at the same height. We can use _visualTransectPoints to build the // flight path segments. @@ -498,9 +519,9 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) } } break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain && _loadedMissionItems.count()) { + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain && _loadedMissionItems.count()) { // Build segments from loaded mission item data QGeoCoordinate prevCoord = QGeoCoordinate(); double prevAlt = 0; @@ -519,7 +540,7 @@ void TransectStyleComplexItem::_updateFlightPathSegmentsDontCallDirectly(void) // - Working from loaded mission items which have had terrain heights queried for // In both cases _rgFlightPathCoordInfo will be set up for use if (_rgFlightPathCoordInfo.count()) { - FlightPathSegment::SegmentType segmentType = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? FlightPathSegment::SegmentTypeGeneric : FlightPathSegment::SegmentTypeTerrainFrame; + FlightPathSegment::SegmentType segmentType = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? FlightPathSegment::SegmentTypeGeneric : FlightPathSegment::SegmentTypeTerrainFrame; for (int i=0; i<_rgFlightPathCoordInfo.count() - 1; i++) { const QGeoCoordinate& fromCoord = _rgFlightPathCoordInfo[i].coord; const QGeoCoordinate& toCoord = _rgFlightPathCoordInfo[i+1].coord; @@ -646,7 +667,7 @@ void TransectStyleComplexItem::_missionItemCoordTerrainData(bool success, QList< TransectStyleComplexItem::ReadyForSaveState TransectStyleComplexItem::readyForSaveState(void) const { bool terrainReady = false; - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { if (_loadedMissionItems.count()) { // We have loaded mission items. Everything is ready to go. terrainReady = true; @@ -667,15 +688,15 @@ TransectStyleComplexItem::ReadyForSaveState TransectStyleComplexItem::readyForSa void TransectStyleComplexItem::_adjustForAvailableTerrainData(void) { switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _adjustForAvailableTerrainData - invalid _cameraCalc.distanceMode()" << _cameraCalc.distanceMode(); return; - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: // No additional work needed return; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: _buildFlightPathCoordInfoFromPathHeightInfoForCalcAboveTerrain(); _adjustForMaxRates(); _adjustForTolerance(); @@ -687,7 +708,7 @@ void TransectStyleComplexItem::_adjustForAvailableTerrainData(void) } emit lastSequenceNumberChanged(lastSequenceNumber()); break; - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { _buildFlightPathCoordInfoFromMissionItems(); } else { @@ -1084,7 +1105,7 @@ int TransectStyleComplexItem::lastSequenceNumber(void) const void TransectStyleComplexItem::_distanceModeChanged(int distanceMode) { - if (static_cast(distanceMode) == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (static_cast(distanceMode) == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { _refly90DegreesFact.setRawValue(false); _hoverAndCaptureFact.setRawValue(false); } @@ -1110,7 +1131,7 @@ void TransectStyleComplexItem::appendMissionItems(QList& items, QO void TransectStyleComplexItem::_appendWaypoint(QList& items, QObject* missionItemParent, int& seqNum, MAV_FRAME mavFrame, float holdTime, const QGeoCoordinate& coordinate) { - double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); + double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); MissionItem* item = new MissionItem(seqNum++, MAV_CMD_NAV_WAYPOINT, @@ -1146,7 +1167,7 @@ void TransectStyleComplexItem::_appendSinglePhotoCapture(QList& it void TransectStyleComplexItem::_appendConditionGate(QList& items, QObject* missionItemParent, int& seqNum, MAV_FRAME mavFrame, const QGeoCoordinate& coordinate) { - double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); + double altitude = _cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain ? coordinate.altitude() : _cameraCalc.distanceToSurface()->rawValue().toDouble(); MissionItem* item = new MissionItem(seqNum++, MAV_CMD_CONDITION_GATE, @@ -1211,18 +1232,18 @@ void TransectStyleComplexItem::_buildAndAppendMissionItems(QList& qCDebug(TransectStyleComplexItemLog) << "_buildAndAppendMissionItems"; switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: mavFrame = MAV_FRAME_GLOBAL_RELATIVE_ALT; break; - case QGroundControlQmlGlobal::AltitudeModeAbsolute: - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: mavFrame = MAV_FRAME_GLOBAL; break; - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: mavFrame = MAV_FRAME_GLOBAL_TERRAIN_ALT; break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: _buildAndAppendMissionItems incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); mavFrame = MAV_FRAME_GLOBAL_RELATIVE_ALT; break; @@ -1320,31 +1341,36 @@ void TransectStyleComplexItem::_recalcComplexDistance(void) emit complexDistanceChanged(); } +double TransectStyleComplexItem::editableAlt() const +{ + return _cameraCalc.distanceToSurface()->rawValue().toDouble(); +} + double TransectStyleComplexItem::amslEntryAlt(void) const { double alt = qQNaN(); double distanceToSurface = _cameraCalc.distanceToSurface()->rawValue().toDouble(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: + case QGroundControlQmlGlobal::AltitudeFrameRelative: alt = distanceToSurface + _missionController->plannedHomePosition().altitude(); break; - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: alt = distanceToSurface; break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { // The first item might not be a waypoint we have to find it. for (int i=0; i<_loadedMissionItems.count(); i++) { MissionItem* item = _loadedMissionItems[i]; const MissionCommandUIInfo* uiInfo = MissionCommandTree::instance()->getUIInfo(_controllerVehicle, QGCMAVLink::VehicleClassGeneric, item->command()); if (uiInfo && uiInfo->specifiesCoordinate() && !uiInfo->isStandaloneCoordinate()) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { - // AltitudeModeCalcAboveTerrain has AMSL alt in param 7 + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { + // AltitudeFrameCalcAboveTerrain has AMSL alt in param 7 alt = item->param7(); } else { - // AltitudeModeTerrainFrame has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. + // AltitudeFrameTerrain has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. if (_rgPathHeightInfo.count()) { alt = item->param7() + _rgPathHeightInfo.first().heights.first(); } @@ -1358,8 +1384,8 @@ double TransectStyleComplexItem::amslEntryAlt(void) const } } break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: amslEntryAlt incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); break; } @@ -1372,23 +1398,23 @@ double TransectStyleComplexItem::amslExitAlt(void) const double alt = qQNaN(); switch (_cameraCalc.distanceMode()) { - case QGroundControlQmlGlobal::AltitudeModeRelative: - case QGroundControlQmlGlobal::AltitudeModeAbsolute: + case QGroundControlQmlGlobal::AltitudeFrameRelative: + case QGroundControlQmlGlobal::AltitudeFrameAbsolute: alt = amslEntryAlt(); break; - case QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain: - case QGroundControlQmlGlobal::AltitudeModeTerrainFrame: + case QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain: + case QGroundControlQmlGlobal::AltitudeFrameTerrain: if (_loadedMissionItems.count()) { // The last item might not be a waypoint we have to find it. for (int i=_loadedMissionItems.count()-1; i>0; i--) { MissionItem* item = _loadedMissionItems[i]; const MissionCommandUIInfo* uiInfo = MissionCommandTree::instance()->getUIInfo(_controllerVehicle, QGCMAVLink::VehicleClassGeneric, item->command()); if (uiInfo && uiInfo->specifiesCoordinate() && !uiInfo->isStandaloneCoordinate()) { - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { - // AltitudeModeCalcAboveTerrain has AMSL alt in param 7 + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { + // AltitudeFrameCalcAboveTerrain has AMSL alt in param 7 alt = item->param7(); } else { - // AltitudeModeTerrainFrame has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. + // AltitudeFrameTerrain has terrain frame relative alt in param 7. So we need terrain heights to calc AMSL. if (_rgPathHeightInfo.count()) { alt = item->param7() + _rgPathHeightInfo.last().heights.last(); } @@ -1402,8 +1428,8 @@ double TransectStyleComplexItem::amslExitAlt(void) const } } break; - case QGroundControlQmlGlobal::AltitudeModeMixed: - case QGroundControlQmlGlobal::AltitudeModeNone: + case QGroundControlQmlGlobal::AltitudeFrameMixed: + case QGroundControlQmlGlobal::AltitudeFrameNone: qCWarning(TransectStyleComplexItemLog) << "Internal Error: amslExitAlt incorrect _cameraCalc.distanceMode" << _cameraCalc.distanceMode(); break; } @@ -1421,10 +1447,10 @@ double TransectStyleComplexItem::minAMSLAltitude(void) const { // FIXME: What about terrain frame - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { return _minAMSLAltitude; } else { - return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative ? _missionController->plannedHomePosition().altitude() : 0); + return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative ? _missionController->plannedHomePosition().altitude() : 0); } } @@ -1432,9 +1458,9 @@ double TransectStyleComplexItem::maxAMSLAltitude(void) const { // FIXME: What about terrain frame - if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain) { + if (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain) { return _maxAMSLAltitude; } else { - return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative ? _missionController->plannedHomePosition().altitude() : 0); + return _cameraCalc.distanceToSurface()->rawValue().toDouble() + (_cameraCalc.distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative ? _missionController->plannedHomePosition().altitude() : 0); } } diff --git a/src/MissionManager/TransectStyleComplexItem.h b/src/MissionManager/TransectStyleComplexItem.h index edc1c70e76ec..b147f1e634e8 100644 --- a/src/MissionManager/TransectStyleComplexItem.h +++ b/src/MissionManager/TransectStyleComplexItem.h @@ -81,7 +81,8 @@ class TransectStyleComplexItem : public ComplexMissionItem bool isSimpleItem (void) const final { return false; } bool isStandaloneCoordinate (void) const final { return false; } bool specifiesAltitudeOnly (void) const final { return false; } - QGeoCoordinate coordinate (void) const final { return _coordinate; } + QGeoCoordinate coordinate (void) const final { return entryCoordinate(); } + QGeoCoordinate entryCoordinate (void) const final { return _entryCoordinate; } QGeoCoordinate exitCoordinate (void) const final { return _exitCoordinate; } int sequenceNumber (void) const final { return _sequenceNumber; } double specifiedFlightSpeed (void) final { return std::numeric_limits::quiet_NaN(); } @@ -94,8 +95,9 @@ class TransectStyleComplexItem : public ComplexMissionItem QString abbreviation (void) const override { return tr("T"); } bool exitCoordinateSameAsEntry (void) const final { return false; } void setDirty (bool dirty) final; - void setCoordinate (const QGeoCoordinate& coordinate) final { Q_UNUSED(coordinate); } + void setCoordinate (const QGeoCoordinate& coordinate) override; void setSequenceNumber (int sequenceNumber) final; + double editableAlt (void) const final; double amslEntryAlt (void) const final; double amslExitAlt (void) const final; double minAMSLAltitude (void) const final; @@ -146,7 +148,7 @@ protected slots: void _recalcComplexDistance (void); int _sequenceNumber = 0; - QGeoCoordinate _coordinate; + QGeoCoordinate _entryCoordinate; QGeoCoordinate _exitCoordinate; QGCMapPolygon _surveyAreaPolygon; diff --git a/src/MissionManager/VTOLLandingComplexItem.cc b/src/MissionManager/VTOLLandingComplexItem.cc index b99b3e5b0c1c..b6fdd084e825 100644 --- a/src/MissionManager/VTOLLandingComplexItem.cc +++ b/src/MissionManager/VTOLLandingComplexItem.cc @@ -30,7 +30,7 @@ VTOLLandingComplexItem::VTOLLandingComplexItem(PlanMasterController* masterContr , _stopTakingPhotosFact (settingsGroup, _metaDataMap[stopTakingPhotosName]) , _stopTakingVideoFact (settingsGroup, _metaDataMap[stopTakingVideoName]) { - _editorQml = "qrc:/qml/QGroundControl/Controls/VTOLLandingPatternEditor.qml"; + _editorQml = "qrc:/qml/QGroundControl/PlanView/VTOLLandingPatternEditor.qml"; _isIncomplete = false; _init(); diff --git a/src/MissionManager/VisualMissionItem.cc b/src/MissionManager/VisualMissionItem.cc index 44ce7166e130..7037321356ea 100644 --- a/src/MissionManager/VisualMissionItem.cc +++ b/src/MissionManager/VisualMissionItem.cc @@ -165,6 +165,11 @@ void VisualMissionItem::_updateTerrainAltitude(void) _terrainAltitude = qQNaN(); emit terrainAltitudeChanged(qQNaN()); + if (_terrainQueryFailed) { + _terrainQueryFailed = false; + emit terrainQueryFailedChanged(_terrainQueryFailed); + } + if (!_flyView && specifiesCoordinate() && coordinate().isValid()) { // We use a timer so that any additional requests before the timer fires result in only a single request _updateTerrainTimer.start(); @@ -194,6 +199,11 @@ void VisualMissionItem::_terrainDataReceived(bool success, QList heights _terrainAltitude = success ? heights[0] : qQNaN(); emit terrainAltitudeChanged(_terrainAltitude); _currentTerrainAtCoordinateQuery = nullptr; + + if (_terrainQueryFailed != !success) { + _terrainQueryFailed = !success; + emit terrainQueryFailedChanged(_terrainQueryFailed); + } } void VisualMissionItem::_setBoundingCube(QGCGeoBoundingCube bc) diff --git a/src/MissionManager/VisualMissionItem.h b/src/MissionManager/VisualMissionItem.h index 44d3b9727e27..b057e9f1530c 100644 --- a/src/MissionManager/VisualMissionItem.h +++ b/src/MissionManager/VisualMissionItem.h @@ -42,9 +42,11 @@ class VisualMissionItem : public QObject Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslEntryAlt READ amslEntryAlt NOTIFY amslEntryAltChanged) Q_PROPERTY(double terrainAltitude READ terrainAltitude NOTIFY terrainAltitudeChanged) ///< The altitude of terrain at the coordinate position, NaN if not known + Q_PROPERTY(bool terrainQueryFailed READ terrainQueryFailed NOTIFY terrainQueryFailedChanged) ///< true: Last terrain query failed + Q_PROPERTY(QGeoCoordinate entryCoordinate READ entryCoordinate NOTIFY entryCoordinateChanged) ///< Does not include altitude Q_PROPERTY(QGeoCoordinate exitCoordinate READ exitCoordinate NOTIFY exitCoordinateChanged) ///< Does not include altitude Q_PROPERTY(double amslExitAlt READ amslExitAlt NOTIFY amslExitAltChanged) - Q_PROPERTY(bool exitCoordinateSameAsEntry READ exitCoordinateSameAsEntry NOTIFY exitCoordinateSameAsEntryChanged) ///< true: exitCoordinate and coordinate are the same value + Q_PROPERTY(bool exitCoordinateSameAsEntry READ exitCoordinateSameAsEntry NOTIFY exitCoordinateSameAsEntryChanged) ///< true: entryCoordinate and exitCoordinate are the same value Q_PROPERTY(QString commandDescription READ commandDescription NOTIFY commandDescriptionChanged) Q_PROPERTY(QString commandName READ commandName NOTIFY commandNameChanged) Q_PROPERTY(QString abbreviation READ abbreviation NOTIFY abbreviationChanged) @@ -100,6 +102,7 @@ class VisualMissionItem : public QObject bool isCurrentItem (void) const { return _isCurrentItem; } bool hasCurrentChildItem (void) const { return _hasCurrentChildItem; } double terrainAltitude (void) const { return _terrainAltitude; } + bool terrainQueryFailed (void) const { return _terrainQueryFailed; } bool flyView (void) const { return _flyView; } bool wizardMode (void) const { return _wizardMode; } VisualMissionItem* parentItem(void) { return _parentItem; } @@ -140,7 +143,9 @@ class VisualMissionItem : public QObject virtual QString commandName (void) const = 0; virtual QString abbreviation (void) const = 0; virtual QGeoCoordinate coordinate (void) const = 0; + virtual QGeoCoordinate entryCoordinate (void) const = 0; virtual QGeoCoordinate exitCoordinate (void) const = 0; + virtual double editableAlt (void) const = 0; virtual double amslEntryAlt (void) const = 0; virtual double amslExitAlt (void) const = 0; virtual int sequenceNumber (void) const = 0; @@ -202,6 +207,7 @@ class VisualMissionItem : public QObject void commandNameChanged (void); void abbreviationChanged (void); void coordinateChanged (const QGeoCoordinate& coordinate); + void entryCoordinateChanged (const QGeoCoordinate& entryCoordinate); void exitCoordinateChanged (const QGeoCoordinate& exitCoordinate); void dirtyChanged (bool dirty); void distanceChanged (double distance); @@ -223,6 +229,7 @@ class VisualMissionItem : public QObject void missionGimbalYawChanged (double missionGimbalYaw); void missionVehicleYawChanged (double missionVehicleYaw); void terrainAltitudeChanged (double terrainAltitude); + void terrainQueryFailedChanged (bool terrainQueryFailed); void additionalTimeDelayChanged (void); void boundingCubeChanged (void); void readyForSaveStateChanged (void); @@ -247,6 +254,7 @@ protected slots: bool _homePositionSpecialCase = false; ///< true: This item is being used as a ui home position indicator bool _wizardMode = false; ///< true: Item editor is showing wizard completion panel double _terrainAltitude = qQNaN(); ///< Altitude of terrain at coordinate position, NaN for not known + bool _terrainQueryFailed = false; ///< true: Last terrain query failed double _altDifference = 0; ///< Difference in altitude from previous waypoint double _altPercent = 0; ///< Percent of total altitude change in mission double _terrainPercent = qQNaN(); ///< Percent of terrain altitude for coordinate diff --git a/src/PlanView/CMakeLists.txt b/src/PlanView/CMakeLists.txt new file mode 100644 index 000000000000..ae9ecfa6fa17 --- /dev/null +++ b/src/PlanView/CMakeLists.txt @@ -0,0 +1,55 @@ +# ============================================================================ +# Plan View Module +# Mission planning view, editors, and map visuals +# ============================================================================ + +qt_add_library(PlanViewModule STATIC) + +qt_add_qml_module(PlanViewModule + URI QGroundControl.PlanView + VERSION 1.0 + RESOURCE_PREFIX /qml + QML_FILES + CameraCalcCamera.qml + CameraCalcGrid.qml + CameraSection.qml + CorridorScanEditor.qml + CorridorScanMapVisual.qml + FWLandingPatternEditor.qml + FWLandingPatternMapVisual.qml + GeoFenceEditor.qml + GeoFenceMapVisuals.qml + MissionDefaultsEditor.qml + MissionItemEditor.qml + MissionItemIndexLabel.qml + MissionItemMapVisual.qml + MissionItemStatus.qml + MissionSettingsEditor.qml + MissionStats.qml + PlanEditToolbar.qml + PlanInfoEditor.qml + PlanToolBarIndicators.qml + PlanTreeView.qml + PlanView.qml + PlanViewRightPanel.qml + RallyPointEditorHeader.qml + RallyPointItemEditor.qml + RallyPointMapVisuals.qml + SimpleItemEditor.qml + SimpleItemMapVisual.qml + StructureScanEditor.qml + StructureScanMapVisual.qml + SurveyItemEditor.qml + SurveyMapVisual.qml + TakeoffItemMapVisual.qml + TerrainStatus.qml + TransectStyleComplexItemEditor.qml + TransectStyleComplexItemStats.qml + TransectStyleComplexItemTabBar.qml + TransectStyleComplexItemTerrainFollow.qml + TransectStyleMapVisuals.qml + TransformEditor.qml + VTOLLandingPatternEditor.qml + VTOLLandingPatternMapVisual.qml + NO_PLUGIN +) diff --git a/src/QmlControls/CameraCalcCamera.qml b/src/PlanView/CameraCalcCamera.qml similarity index 100% rename from src/QmlControls/CameraCalcCamera.qml rename to src/PlanView/CameraCalcCamera.qml diff --git a/src/QmlControls/CameraCalcGrid.qml b/src/PlanView/CameraCalcGrid.qml similarity index 97% rename from src/QmlControls/CameraCalcGrid.qml rename to src/PlanView/CameraCalcGrid.qml index 50c0c6fe5fe6..7e081d517c2c 100644 --- a/src/QmlControls/CameraCalcGrid.qml +++ b/src/PlanView/CameraCalcGrid.qml @@ -87,7 +87,7 @@ Column { AltitudeFactTextField { fact: cameraCalc.distanceToSurface - altitudeMode: cameraCalc.distanceMode + altitudeFrame: cameraCalc.distanceMode enabled: fixedDistanceRadio.checked Layout.fillWidth: true } @@ -120,7 +120,7 @@ Column { QGCLabel { text: distanceToSurfaceLabel } AltitudeFactTextField { fact: cameraCalc.distanceToSurface - altitudeMode: cameraCalc.distanceMode + altitudeFrame: cameraCalc.distanceMode Layout.fillWidth: true } diff --git a/src/QmlControls/CameraSection.qml b/src/PlanView/CameraSection.qml similarity index 98% rename from src/QmlControls/CameraSection.qml rename to src/PlanView/CameraSection.qml index 34b7dee95dd1..f970221d2865 100644 --- a/src/QmlControls/CameraSection.qml +++ b/src/PlanView/CameraSection.qml @@ -8,6 +8,8 @@ import QGroundControl.FactControls // Camera section for mission item editors Column { + required property var missionItem + property alias buttonGroup: cameraSectionHeader.buttonGroup property alias showSpacer: cameraSectionHeader.showSpacer property alias checked: cameraSectionHeader.checked diff --git a/src/QmlControls/CorridorScanEditor.qml b/src/PlanView/CorridorScanEditor.qml similarity index 89% rename from src/QmlControls/CorridorScanEditor.qml rename to src/PlanView/CorridorScanEditor.qml index c0c9e6fe1651..8d7a1d2e7f9a 100644 --- a/src/QmlControls/CorridorScanEditor.qml +++ b/src/PlanView/CorridorScanEditor.qml @@ -15,10 +15,6 @@ TransectStyleComplexItemEditor { transectValuesComponent: _transectValuesComponent presetsTransectValuesComponent: _transectValuesComponent - // The following properties must be available up the hierarchy chain - // property real availableWidth ///< Width for control - // property var missionItem ///< Mission Item for editor - property real _margin: ScreenTools.defaultFontPixelWidth / 2 property var _missionItem: missionItem diff --git a/src/QmlControls/CorridorScanMapVisual.qml b/src/PlanView/CorridorScanMapVisual.qml similarity index 92% rename from src/QmlControls/CorridorScanMapVisual.qml rename to src/PlanView/CorridorScanMapVisual.qml index ce5a82c8351c..0c32d1d15717 100644 --- a/src/QmlControls/CorridorScanMapVisual.qml +++ b/src/PlanView/CorridorScanMapVisual.qml @@ -10,6 +10,7 @@ import QGroundControl.FlightMap /// Corridor Scan Complex Mission Item visuals TransectStyleMapVisuals { polygonInteractive: false + hideMapPolygon: mapPolylineVisuals.dragging property bool _currentItem: object.isCurrentItem diff --git a/src/QmlControls/FWLandingPatternEditor.qml b/src/PlanView/FWLandingPatternEditor.qml similarity index 95% rename from src/QmlControls/FWLandingPatternEditor.qml rename to src/PlanView/FWLandingPatternEditor.qml index 15974fb44b0f..e4420b4c332e 100644 --- a/src/QmlControls/FWLandingPatternEditor.qml +++ b/src/PlanView/FWLandingPatternEditor.qml @@ -15,18 +15,17 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth - property var _masterControler: masterController + property var _masterControler: missionItem.masterController property var _missionController: _masterControler.missionController property var _missionVehicle: _masterControler.controllerVehicle property real _margin: ScreenTools.defaultFontPixelWidth / 2 property real _spacer: ScreenTools.defaultFontPixelWidth / 2 property string _setToVehicleHeadingStr: qsTr("Set to vehicle heading") property string _setToVehicleLocationStr: qsTr("Set to vehicle location") - property int _altitudeMode: missionItem.altitudesAreRelative ? QGroundControl.AltitudeModeRelative : QGroundControl.AltitudeModeAbsolute + property int _altitudeFrame: missionItem.altitudesAreRelative ? QGroundControl.AltitudeFrameRelative : QGroundControl.AltitudeFrameAbsolute Column { @@ -67,7 +66,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.finalApproachAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } FactCheckBox { @@ -141,7 +140,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.landingAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCRadioButton { diff --git a/src/QmlControls/FWLandingPatternMapVisual.qml b/src/PlanView/FWLandingPatternMapVisual.qml similarity index 100% rename from src/QmlControls/FWLandingPatternMapVisual.qml rename to src/PlanView/FWLandingPatternMapVisual.qml diff --git a/src/QmlControls/GeoFenceEditor.qml b/src/PlanView/GeoFenceEditor.qml similarity index 100% rename from src/QmlControls/GeoFenceEditor.qml rename to src/PlanView/GeoFenceEditor.qml diff --git a/src/QmlControls/GeoFenceMapVisuals.qml b/src/PlanView/GeoFenceMapVisuals.qml similarity index 100% rename from src/QmlControls/GeoFenceMapVisuals.qml rename to src/PlanView/GeoFenceMapVisuals.qml diff --git a/src/PlanView/MissionDefaultsEditor.qml b/src/PlanView/MissionDefaultsEditor.qml new file mode 100644 index 000000000000..e8dd6d2a2556 --- /dev/null +++ b/src/PlanView/MissionDefaultsEditor.qml @@ -0,0 +1,153 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls +import QGroundControl.PlanView + +Rectangle { + id: _root + + required property var missionController + required property var planMasterController + + property var _controllerVehicle: planMasterController.controllerVehicle + property var _visualItems: missionController.visualItems + property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true + property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null + property bool _showCruiseSpeed: _controllerVehicle ? !_controllerVehicle.multiRotor : false + property bool _showHoverSpeed: _controllerVehicle ? (_controllerVehicle.multiRotor || _controllerVehicle.vtol) : false + property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 + + width: parent ? parent.width : 0 + height: mainColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + QGCPalette { id: qgcPal; colorGroupEnabled: _root.enabled } + + Connections { + target: _root._controllerVehicle + function onFirmwareTypeChanged() { + if (!_root._controllerVehicle.supports.terrainFrame + && _root.missionController.globalAltitudeFrame === QGroundControl.AltitudeFrameTerrain) { + _root.missionController.globalAltitudeFrame = QGroundControl.AltitudeFrameCalcAboveTerrain + } + } + } + + Component { id: altFrameDialogComponent; AltFrameDialog { } } + + QGCPopupDialogFactory { + id: altFrameDialogFactory + dialogComponent: altFrameDialogComponent + } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + LabelledButton { + Layout.fillWidth: true + label: qsTr("Alt Frame") + buttonText: QGroundControl.altitudeFrameExtraUnits(_root.missionController.globalAltitudeFrame) + + onClicked: { + let removeModes = [] + let updateFunction = function(altFrame) { _root.missionController.globalAltitudeFrame = altFrame } + if (!_root._controllerVehicle.supports.terrainFrame) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) + } + if (!_root._noMissionItemsAdded) { + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameRelative) { + removeModes.push(QGroundControl.AltitudeFrameRelative) + } + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameAbsolute) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) + } + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameCalcAboveTerrain) { + removeModes.push(QGroundControl.AltitudeFrameCalcAboveTerrain) + } + if (_root.missionController.globalAltitudeFrame !== QGroundControl.AltitudeFrameTerrain) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) + } + } + altFrameDialogFactory.open({ currentAltFrame: _root.missionController.globalAltitudeFrame, rgRemoveModes: removeModes, updateAltFrameFn: updateFunction }) + } + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Waypoints Altitude") + fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude + } + + FactTextFieldSlider { + Layout.fillWidth: true + label: qsTr("Flight Speed") + fact: _root._settingsItem ? _root._settingsItem.speedSection.flightSpeed : null + showEnableCheckbox: true + enableCheckBoxChecked: _root._settingsItem ? _root._settingsItem.speedSection.specifyFlightSpeed : false + visible: _root._settingsItem ? _root._settingsItem.speedSection.available : false + + onEnableCheckboxClicked: { + if (_root._settingsItem) { + _root._settingsItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked + } + } + } + + // ── Vehicle Speeds ── + SectionHeader { + id: vehicleSpeedsSectionHeader + Layout.fillWidth: true + text: qsTr("Vehicle Speeds") + visible: _root._showCruiseSpeed || _root._showHoverSpeed + checked: false + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: vehicleSpeedsSectionHeader.visible && vehicleSpeedsSectionHeader.checked + + QGCLabel { + Layout.columnSpan: 2 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") + } + + QGCLabel { + text: qsTr("Cruise speed") + visible: _root._showCruiseSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed + visible: _root._showCruiseSpeed + Layout.preferredWidth: _root._fieldWidth + } + + QGCLabel { + text: qsTr("Hover speed") + visible: _root._showHoverSpeed + Layout.fillWidth: true + } + FactTextField { + fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed + visible: _root._showHoverSpeed + Layout.preferredWidth: _root._fieldWidth + } + } + } +} diff --git a/src/QmlControls/MissionItemEditor.qml b/src/PlanView/MissionItemEditor.qml similarity index 85% rename from src/QmlControls/MissionItemEditor.qml rename to src/PlanView/MissionItemEditor.qml index 26ca9d0aea3a..1b9540a64fa0 100644 --- a/src/QmlControls/MissionItemEditor.qml +++ b/src/PlanView/MissionItemEditor.qml @@ -10,6 +10,13 @@ import QGroundControl.FactControls /// Mission item edit control Rectangle { + required property var missionItem ///< MissionItem associated with this editor + required property var map ///< Map control + + signal clicked + signal remove + signal selectNextNotReadyItem + id: _root height: _currentItem ? (editorLoader.y + editorLoader.height + _innerMargin) : (topRowLayout.y + topRowLayout.height + _margin) color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade @@ -18,20 +25,11 @@ Rectangle { border.width: _readyForSave ? 0 : 2 border.color: qgcPal.warningText - property var map ///< Map control - property var masterController - property var missionItem ///< MissionItem associated with this editor - property bool readOnly ///< true: read only view, false: full editing view - - signal clicked - signal remove - signal selectNextNotReadyItem - - property var _masterController: masterController + property var _masterController: missionItem.masterController property var _missionController: _masterController.missionController property bool _currentItem: missionItem.isCurrentItem property color _outerTextColor: _currentItem ? qgcPal.buttonHighlightText : qgcPal.text - property bool _noMissionItemsAdded: ListView.view.model.count === 1 + property bool _noMissionItemsAdded: _missionController.visualItems ? _missionController.visualItems.count <= 1 : true property real _sectionSpacer: ScreenTools.defaultFontPixelWidth / 2 // spacing between section headings property bool _singleComplexItem: _missionController.complexMissionItemNames.length === 1 property bool _readyForSave: missionItem.readyForSaveState === VisualMissionItem.ReadyForSave @@ -44,6 +42,23 @@ Rectangle { readonly property real _trashSize: commandPicker.height * 0.75 readonly property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + // setSource() injects missionItem before internal bindings activate + function _loadEditor() { + if (missionItem.isCurrentItem) { + editorLoader.setSource(missionItem.editorQml, { + missionItem: _root.missionItem, + availableWidth: _root.width - (editorLoader.anchors.margins * 2) + }) + } else { + editorLoader.setSource("") + } + } + + Connections { + target: missionItem + function onIsCurrentItemChanged() { _root._loadEditor() } + } + QGCPalette { id: qgcPal colorGroupEnabled: enabled @@ -74,8 +89,14 @@ Rectangle { id: editPositionDialog EditPositionDialog { - coordinate: missionItem.isSurveyItem ? missionItem.centerCoordinate : missionItem.coordinate - onCoordinateChanged: missionItem.isSurveyItem ? missionItem.centerCoordinate = coordinate : missionItem.coordinate = coordinate + property bool _editCenterCoordinate: false + + onCoordinateChanged: { + if (_editCenterCoordinate) + missionItem.centerCoordinate = coordinate + else + missionItem.coordinate = coordinate + } } } @@ -168,7 +189,7 @@ Rectangle { id: commandDialog MissionCommandDialog { - vehicle: masterController.controllerVehicle + vehicle: _masterController.controllerVehicle missionItem: _root.missionItem map: _root.map // FIXME: Disabling fly through commands doesn't work since you may need to change from an RTL to something else @@ -194,6 +215,7 @@ Rectangle { DropPanel { id: hamburgerMenuDropPanel + onClosed: destroy() sourceComponent: Component { ColumnLayout { @@ -227,7 +249,13 @@ Rectangle { text: qsTr("Edit position...") enabled: missionItem.specifiesCoordinate onClicked: { - editPositionDialogFactory.open() + const editCenterCoordinate = missionItem.isSurveyItem + editPositionDialogFactory.open({ + _editCenterCoordinate: editCenterCoordinate, + coordinate: editCenterCoordinate ? missionItem.centerCoordinate : missionItem.coordinate, + altitudeFact: !editCenterCoordinate && missionItem.specifiesAltitude ? missionItem.altitude : null, + altitudeFrame: !editCenterCoordinate && missionItem.specifiesAltitude ? missionItem.altitudeFrame : QGroundControl.AltitudeFrameNone, + }) hamburgerMenuDropPanel.close() } } @@ -271,7 +299,6 @@ Rectangle { } } - QGCColoredImage { id: hamburger anchors.margins: _margin @@ -291,6 +318,7 @@ Rectangle { position = Qt.point(position.x, position.y) // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow position = mapToItem(globals.parent, position) + var dropPanel = hamburgerMenuDropPanelComponent.createObject(mainWindow, { clickRect: Qt.rect(position.x, position.y, 0, 0) }) dropPanel.open() } @@ -320,11 +348,8 @@ Rectangle { anchors.margins: _innerMargin anchors.left: parent.left anchors.top: topRowLayout.bottom - source: _currentItem ? missionItem.editorQml : "" asynchronous: true - property var masterController: _masterController - property real availableWidth: _root.width - (anchors.margins * 2) ///< How wide the editor should be - property var editorRoot: _root + Component.onCompleted: _root._loadEditor() } } diff --git a/src/QmlControls/MissionItemIndexLabel.qml b/src/PlanView/MissionItemIndexLabel.qml similarity index 100% rename from src/QmlControls/MissionItemIndexLabel.qml rename to src/PlanView/MissionItemIndexLabel.qml diff --git a/src/QmlControls/MissionItemMapVisual.qml b/src/PlanView/MissionItemMapVisual.qml similarity index 100% rename from src/QmlControls/MissionItemMapVisual.qml rename to src/PlanView/MissionItemMapVisual.qml diff --git a/src/QmlControls/MissionItemStatus.qml b/src/PlanView/MissionItemStatus.qml similarity index 100% rename from src/QmlControls/MissionItemStatus.qml rename to src/PlanView/MissionItemStatus.qml diff --git a/src/PlanView/MissionSettingsEditor.qml b/src/PlanView/MissionSettingsEditor.qml new file mode 100644 index 000000000000..9e17406a38b1 --- /dev/null +++ b/src/PlanView/MissionSettingsEditor.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +// Editor for Initial Camera Settings (mission item 0) +Rectangle { + id: root + width: availableWidth + height: valuesColumn.height + (_margin * 2) + color: qgcPal.windowShadeDark + radius: ScreenTools.defaultBorderRadius + + required property var missionItem + required property real availableWidth + + property var _masterController: missionItem.masterController + property var _controllerVehicle: _masterController.controllerVehicle + property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + property bool _showCameraSection: _waypointsOnlyMode || QGroundControl.corePlugin.showAdvancedUI + property bool _simpleMissionStart: QGroundControl.corePlugin.options.showSimpleMissionStart + + readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 + + QGCPalette { id: qgcPal } + + ColumnLayout { + id: valuesColumn + anchors.margins: _margin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: ScreenTools.defaultFontPixelHeight / 2 + + Column { + Layout.fillWidth: true + spacing: _margin + visible: !_simpleMissionStart + + CameraSection { + id: cameraSection + anchors.left: parent.left + anchors.right: parent.right + missionItem: root.missionItem + visible: _showCameraSection + showSectionHeader: false + + Component.onCompleted: checked = !_waypointsOnlyMode && missionItem.cameraSection.settingsSpecified + } + + QGCLabel { + anchors.left: parent.left + anchors.right: parent.right + text: qsTr("Above camera commands will take affect immediately upon mission start.") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + font.pointSize: ScreenTools.smallFontPointSize + visible: _showCameraSection && cameraSection.checked + } + } + } +} diff --git a/src/QmlControls/MissionStats.qml b/src/PlanView/MissionStats.qml similarity index 100% rename from src/QmlControls/MissionStats.qml rename to src/PlanView/MissionStats.qml diff --git a/src/QmlControls/PlanEditToolbar.qml b/src/PlanView/PlanEditToolbar.qml similarity index 100% rename from src/QmlControls/PlanEditToolbar.qml rename to src/PlanView/PlanEditToolbar.qml diff --git a/src/PlanView/PlanInfoEditor.qml b/src/PlanView/PlanInfoEditor.qml new file mode 100644 index 000000000000..10eb1bde1666 --- /dev/null +++ b/src/PlanView/PlanInfoEditor.qml @@ -0,0 +1,164 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +Rectangle { + id: _root + + required property var planMasterController + required property var missionController + required property var editorMap + + property var _controllerVehicle: planMasterController.controllerVehicle + property var _visualItems: missionController.visualItems + property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true + property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null + property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport + property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport + property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle + property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly + property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 + + width: parent ? parent.width : 0 + height: mainColumn.height + ScreenTools.defaultFontPixelHeight + color: qgcPal.windowShadeDark + + QGCPalette { id: qgcPal; colorGroupEnabled: _root.enabled } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QGCLabel { + text: qsTr("Plan File") + } + + QGCTextField { + id: planNameField + placeholderText: qsTr("Untitled") + Layout.fillWidth: true + + Component.onCompleted: text = _root.planMasterController.currentPlanFileName + + Connections { + target: _root.planMasterController + function onCurrentPlanFileNameChanged() { + if (!planNameField.activeFocus) { + planNameField.text = _root.planMasterController.currentPlanFileName + } + } + } + + onEditingFinished: _root.planMasterController.currentPlanFileName = text + } + } + + // ── Vehicle Info ── + SectionHeader { + id: vehicleInfoSectionHeader + Layout.fillWidth: true + text: qsTr("Vehicle Info") + visible: !_root._waypointsOnlyMode + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + rowSpacing: columnSpacing + columns: 2 + visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked + + QGCLabel { + text: qsTr("Firmware") + Layout.fillWidth: true + visible: _root._multipleFirmware + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass + indexModel: false + Layout.preferredWidth: _root._fieldWidth + visible: _root._multipleFirmware && _root._allowFWVehicleTypeSelection + } + QGCLabel { + text: _root._controllerVehicle ? _root._controllerVehicle.firmwareTypeString : "" + visible: _root._multipleFirmware && !_root._allowFWVehicleTypeSelection + } + + QGCLabel { + text: qsTr("Vehicle") + Layout.fillWidth: true + visible: _root._multipleVehicleTypes + } + FactComboBox { + fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass + indexModel: false + Layout.preferredWidth: _root._fieldWidth + visible: _root._multipleVehicleTypes && _root._allowFWVehicleTypeSelection + } + QGCLabel { + text: _root._controllerVehicle ? _root._controllerVehicle.vehicleTypeString : "" + visible: _root._multipleVehicleTypes && !_root._allowFWVehicleTypeSelection + } + } + + // ── Expected Home Position ── + SectionHeader { + id: plannedHomePositionSection + Layout.fillWidth: true + text: qsTr("Expected Home Position") + } + + GridLayout { + Layout.fillWidth: true + columnSpacing: ScreenTools.defaultFontPixelWidth + columns: 2 + visible: plannedHomePositionSection.checked + + QGCLabel { + text: qsTr("Altitude (AMSL)") + } + FactTextField { + fact: _root._settingsItem ? _root._settingsItem.plannedHomePositionAltitude : null + Layout.fillWidth: true + visible: _root._settingsItem && _root._settingsItem.terrainQueryFailed + } + QGCLabel { + text: _root._settingsItem ? _root._settingsItem.plannedHomePositionAltitude.valueString + " " + _root._settingsItem.plannedHomePositionAltitude.units : "" + Layout.fillWidth: true + visible: !_root._settingsItem || !_root._settingsItem.terrainQueryFailed + } + } + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Actual position/alt set by vehicle at flight time.") + horizontalAlignment: Text.AlignHCenter + visible: plannedHomePositionSection.checked + } + + QGCButton { + text: qsTr("Move To Map Center") + Layout.alignment: Qt.AlignHCenter + visible: plannedHomePositionSection.checked + onClicked: { + if (_root._settingsItem) { + _root._settingsItem.coordinate = _root.editorMap.center + } + } + } + } +} diff --git a/src/QmlControls/PlanToolBarIndicators.qml b/src/PlanView/PlanToolBarIndicators.qml similarity index 77% rename from src/QmlControls/PlanToolBarIndicators.qml rename to src/PlanView/PlanToolBarIndicators.qml index 8002ac204aff..f4955a0eceab 100644 --- a/src/QmlControls/PlanToolBarIndicators.qml +++ b/src/PlanView/PlanToolBarIndicators.qml @@ -10,6 +10,7 @@ import QGroundControl.FactControls // Toolbar for Plan View RowLayout { required property var planMasterController + property bool showRallyPointsHelp: false id: root spacing: ScreenTools.defaultFontPixelWidth @@ -19,7 +20,8 @@ RowLayout { property var _geoFenceController: _planMasterController.geoFenceController property var _rallyPointController: _planMasterController.rallyPointController property bool _controllerOffline: _planMasterController.offline - property var _controllerDirty: _planMasterController.dirty + property var _saveDirty: _planMasterController.dirtyForSave + property var _uploadDirty: _planMasterController.dirtyForUpload property var _syncInProgress: _planMasterController.syncInProgress property var _visualItems: _missionController.visualItems property bool _hasPlanItems: _planMasterController.containsItems @@ -31,9 +33,9 @@ RowLayout { } function _downloadClicked() { - if (_planMasterController.dirty) { + if (_saveDirty) { QGroundControl.showMessageDialog(root, qsTr("Download"), - qsTr("You have unsaved/unsent changes. Downloading from the Vehicle will lose these changes. Are you sure?"), + qsTr("You have unsaved changes. Downloading from the Vehicle will lose these changes. Are you sure?"), Dialog.Yes | Dialog.Cancel, function() { _planMasterController.loadFromVehicle() }) } else { @@ -42,7 +44,7 @@ RowLayout { } function _openButtonClicked() { - if (_planMasterController.dirty) { + if (_saveDirty || _uploadDirty) { QGroundControl.showMessageDialog(root, qsTr("Open Plan"), qsTr("You have unsaved/unsent changes. Loading a new Plan will lose these changes. Are you sure?"), Dialog.Yes | Dialog.Cancel, @@ -53,13 +55,28 @@ RowLayout { } function _saveButtonClicked() { - if(_planMasterController.currentPlanFile !== "") { - _planMasterController.saveToCurrent() - QGroundControl.showMessageDialog(root, qsTr("Save"), - qsTr("Plan saved to `%1`").arg(_planMasterController.currentPlanFile), - Dialog.Ok) + if (_planMasterController.currentPlanFileName === "") { + if (_planMasterController.currentPlanFile === "") { + // No file and no name typed — open the file dialog + _planMasterController.saveToSelectedFile() + } else { + // Have a file but name was cleared — save to the existing file + _planMasterController.saveToCurrent() + } + return + } + + if (_planMasterController.currentPlanFile === "" || _planMasterController.planFileRenamed) { + // First save with a typed name, or name was changed since last save + let fullName = _planMasterController.currentPlanFileName + "." + _planMasterController.fileExtension + let msg = _planMasterController.resolvedPlanFileExists() + ? qsTr("'%1' already exists. Overwrite?").arg(fullName) + : qsTr("Save as '%1'?").arg(fullName) + QGroundControl.showMessageDialog(root, qsTr("Save"), msg, + Dialog.Yes | Dialog.No, + function() { _planMasterController.saveWithCurrentName() }) } else { - _planMasterController.saveToSelectedFile() + _planMasterController.saveToCurrent() } } @@ -104,10 +121,10 @@ RowLayout { } QGCButton { - text: _planMasterController.currentPlanFile === "" ? qsTr("Save As") : qsTr("Save") + text: qsTr("Save") iconSource: "/res/SaveToDisk.svg" enabled: !_syncInProgress && _hasPlanItems - primary: _controllerDirty + primary: _saveDirty onClicked: _saveButtonClicked() } @@ -117,7 +134,7 @@ RowLayout { iconSource: "/res/UploadToVehicle.svg" enabled: !_syncInProgress && _hasPlanItems visible: !_syncInProgress - primary: _controllerDirty + primary: _uploadDirty onClicked: _uploadClicked() } @@ -140,29 +157,10 @@ RowLayout { } } - ColumnLayout { + QGCLabel { + text: qsTr("Click in map to add rally points") + visible: root.showRallyPointsHelp Layout.alignment: Qt.AlignVCenter - spacing: 0 - - QGCLabel { - text: _leftClickText() - font.pointSize: ScreenTools.smallFontPointSize - visible: _editingLayer === _layerMission || _editingLayer === _layerRally - - function _leftClickText() { - if (_editingLayer === _layerMission) { - return qsTr("- Click on the map to add Waypoint") - } else { - return qsTr("- Click on the map to add Rally Point") - } - } - } - - QGCLabel { - text: qsTr("- %1 to add ROI %2").arg(ScreenTools.isMobile ? qsTr("Press and hold") : qsTr("Right click")).arg(_missionController.isROIActive ? qsTr("or Cancel ROI") : "") - font.pointSize: ScreenTools.smallFontPointSize - visible: _editingLayer === _layerMission && _planMasterController.controllerVehicle.roiModeSupported - } } Component { @@ -189,7 +187,7 @@ RowLayout { QGCButton { Layout.fillWidth: true text: qsTr("Download") - enabled: !_syncInProgress + enabled: !_syncInProgress && !_controllerOffline visible: !_syncInProgress onClicked: { diff --git a/src/PlanView/PlanTreeView.qml b/src/PlanView/PlanTreeView.qml new file mode 100644 index 000000000000..b1d61c38d56d --- /dev/null +++ b/src/PlanView/PlanTreeView.qml @@ -0,0 +1,295 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls +import QGroundControl.PlanView + +/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points +/// as collapsible sections using a real TreeView with type-discriminating delegates. +TreeView { + id: root + model: _missionController.visualItemsTree + clip: true + boundsBehavior: Flickable.StopAtBounds + reuseItems: false + pointerNavigationEnabled: false + selectionBehavior: TableView.SelectionDisabled + rowSpacing: 2 + + required property var editorMap + required property var planMasterController + + signal editingLayerChangeRequested(int layer) + + readonly property int _layerMission: 1 + readonly property int _layerFence: 2 + readonly property int _layerRally: 3 + + property var _missionController: planMasterController.missionController + property var _geoFenceController: planMasterController.geoFenceController + property var _rallyPointController: planMasterController.rallyPointController + + // Helper: convert a persistent model index to the current visual row + function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) } + + // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView) + property color indicatorColor: qgcPal.text + + QGCPalette { id: qgcPal; colorGroupEnabled: enabled } + + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal } + QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical } + + Connections { + target: root._missionController + function onVisualItemsChanged() { + // Mission group always expanded after rebuild (clear / load) + root.collapseRecursively() + root.expand(_rowFor(_missionController.missionGroupIndex)) + root.editingLayerChangeRequested(root._layerMission) + } + } + + // Public API: select a layer and expand its group. Called by the layer tool buttons. + function selectLayer(nodeType) { + let targetRow = -1 + switch (nodeType) { + case "missionGroup": + targetRow = _rowFor(_missionController.missionGroupIndex) + editingLayerChangeRequested(_layerMission) + break + case "fenceGroup": + targetRow = _rowFor(_missionController.fenceGroupIndex) + editingLayerChangeRequested(_layerFence) + break + case "rallyGroup": + targetRow = _rowFor(_missionController.rallyGroupIndex) + editingLayerChangeRequested(_layerRally) + break + } + + if (targetRow >= 0) { + if (!root.isExpanded(targetRow)) + root.expand(targetRow) + root.forceLayout() + root.positionViewAtRow(targetRow, TableView.AlignTop) + } + } + + // Toggle expand/collapse for a group header. Does not affect the editing layer. + function _toggleGroup(row) { + if (root.isExpanded(row)) + root.collapse(row) + else + root.expand(row) + root.forceLayout() + } + + // Subtitle text shown on group headers, varies by node type + function _groupSubtitle(nodeType) { + switch (nodeType) { + case "planFileGroup": return planMasterController.currentPlanFileName === "" ? qsTr("") : planMasterController.currentPlanFileName + case "missionGroup": return _missionController.visualItems ? (_missionController.visualItems.count - 1) + qsTr(" items") : "" + case "rallyGroup": return _rallyPointController.points ? _rallyPointController.points.count + qsTr(" points") : "" + default: return "" + } + } + + // Coalesces multiple delegate height changes into a single forceLayout() call + Timer { + id: layoutTimer + interval: 0 + running: false + repeat: false + onTriggered: root.forceLayout() + } + + delegate: Item { + id: delegateRoot + implicitWidth: root.width + implicitHeight: (loader.item ? loader.item.height : 1) + (separatorLine.visible ? separatorLine.height + root.rowSpacing : 0) + width: root.width + height: implicitHeight + + required property TreeView treeView + required property bool isTreeNode + required property bool expanded + required property bool hasChildren + required property int depth + required property int row + required property var model + + readonly property var nodeObject: model.object + readonly property string nodeType: model.nodeType + readonly property bool separator: model.separator ?? false + + onImplicitHeightChanged: layoutTimer.restart() + + readonly property string _qrcBase: "qrc:/qml/QGroundControl/PlanView/" + + // We use setSource() instead of sourceComponent so that required properties + // (e.g. missionItem) are injected before internal bindings activate, + // preventing "Cannot read property of null" warnings. + Loader { + id: loader + width: parent.width + + Component.onCompleted: { + switch (delegateRoot.nodeType) { + case "planFileGroup": + case "defaultsGroup": + case "missionGroup": + case "fenceGroup": + case "rallyGroup": + case "transformGroup": + sourceComponent = groupHeaderComponent + break + case "planFileInfo": + setSource(delegateRoot._qrcBase + "PlanInfoEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + planMasterController: root.planMasterController, + missionController: root._missionController, + editorMap: root.editorMap + }) + break + case "defaultsInfo": + setSource(delegateRoot._qrcBase + "MissionDefaultsEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + missionController: root._missionController, + planMasterController: root.planMasterController + }) + break + case "missionItem": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "MissionItemEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + map: root.editorMap, + missionItem: delegateRoot.nodeObject + }) + } + break + case "fenceEditor": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "GeoFenceEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + myGeoFenceController: root._geoFenceController, + flightMap: root.editorMap + }) + } + break + case "rallyHeader": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "RallyPointEditorHeader.qml", { + width: Qt.binding(() => delegateRoot.width), + controller: root._rallyPointController + }) + } + break + case "rallyItem": + if (delegateRoot.nodeObject) { + setSource(delegateRoot._qrcBase + "RallyPointItemEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + rallyPoint: delegateRoot.nodeObject, + controller: root._rallyPointController + }) + } + break + case "transformEditor": + setSource(delegateRoot._qrcBase + "TransformEditor.qml", { + width: Qt.binding(() => delegateRoot.width), + missionController: root._missionController + }) + break + } + } + + onLoaded: { + if (delegateRoot.nodeType === "missionItem" && item) { + item.clicked.connect(function() { + root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false) + }) + item.remove.connect(function() { + var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject) + if (viIndex > 0) { + root._missionController.removeVisualItem(viIndex) + } + }) + item.selectNextNotReadyItem.connect(function() { + for (var i = 0; i < root._missionController.visualItems.count; i++) { + var vmi = root._missionController.visualItems.get(i) + if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { + root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) + break + } + } + }) + } + } + } + + Rectangle { + id: separatorLine + anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5 + anchors.topMargin: root.rowSpacing + anchors.top: loader.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: qgcPal.groupBorder + visible: delegateRoot.separator + } + + // ── Group header (Mission Items / GeoFence / Rally Points) ── + Component { + id: groupHeaderComponent + + Rectangle { + width: delegateRoot.width + height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth + color: qgcPal.windowShade + + RowLayout { + id: groupHeaderRow + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5 + + QGCColoredImage { + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.75 + Layout.preferredHeight: Layout.preferredWidth + source: "/InstrumentValueIcons/cheveron-right.svg" + color: qgcPal.text + rotation: delegateRoot.expanded ? 90 : 0 + } + + QGCLabel { + Layout.alignment: Qt.AlignBaseline + text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : "" + font.bold: true + } + + QGCLabel { + Layout.alignment: Qt.AlignBaseline + Layout.fillWidth: true + text: root._groupSubtitle(delegateRoot.nodeType) + elide: Text.ElideRight + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.colorGrey + } + } + + MouseArea { + anchors.fill: parent + onClicked: root._toggleGroup(delegateRoot.row) + } + } + } + + } +} diff --git a/src/QmlControls/PlanView.qml b/src/PlanView/PlanView.qml similarity index 77% rename from src/QmlControls/PlanView.qml rename to src/PlanView/PlanView.qml index 2234a19b830b..cd48235c1f65 100644 --- a/src/QmlControls/PlanView.qml +++ b/src/PlanView/PlanView.qml @@ -11,6 +11,7 @@ import QGroundControl.FlightMap import QGroundControl.Controls import QGroundControl.FactControls import QGroundControl.FlyView +import QGroundControl.Toolbar Item { id: _root @@ -18,30 +19,27 @@ Item { readonly property int _decimalPlaces: 8 readonly property real _margin: ScreenTools.defaultFontPixelHeight * 0.5 readonly property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 - readonly property real _radius: ScreenTools.defaultFontPixelWidth * 0.5 readonly property real _rightPanelWidth: Math.min(width / 3, ScreenTools.defaultFontPixelWidth * 30) - readonly property var _defaultVehicleCoordinate: QtPositioning.coordinate(37.803784, -122.462276) - readonly property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly property var _planMasterController: planMasterController property var _missionController: _planMasterController.missionController property var _geoFenceController: _planMasterController.geoFenceController property var _rallyPointController: _planMasterController.rallyPointController property var _visualItems: _missionController.visualItems - property bool _lightWidgetBorders: editorMap.isSatelliteMap property bool _singleComplexItem: _missionController.complexMissionItemNames.length === 1 property int _editingLayer: _layerMission - property int _toolStripBottom: toolStrip.height + toolStrip.y property var _appSettings: QGroundControl.settingsManager.appSettings property var _planViewSettings: QGroundControl.settingsManager.planViewSettings property bool _promptForPlanUsageShowing: false + property bool _addROIOnClick: false + property bool _addWaypointOnClick: false + property bool _homeTrackingMapCenter: true + property bool _updatingHomeFromMapCenter: false readonly property int _layerMission: 1 readonly property int _layerFence: 2 readonly property int _layerRally: 3 - readonly property string _armedVehicleUploadPrompt: qsTr("Vehicle is currently armed. Do you want to upload the mission to the vehicle?") - onVisibleChanged: { if(visible) { editorMap.zoomLevel = QGroundControl.flightMapZoom @@ -57,11 +55,6 @@ Item { return coordinate } - property bool _firstMissionLoadComplete: false - property bool _firstFenceLoadComplete: false - property bool _firstRallyLoadComplete: false - property bool _firstLoadComplete: false - MapFitFunctions { id: mapFitFunctions // The name for this id cannot be changed without breaking references outside of this code. Beware! map: editorMap @@ -169,6 +162,31 @@ Item { } } + // Stop tracking map center when the home position is changed externally (e.g. drag, file load) + Connections { + target: _visualItems.count > 0 ? _visualItems.get(0) : null + function onCoordinateChanged() { + if (!_updatingHomeFromMapCenter && !_planMasterController.containsItems) { + _homeTrackingMapCenter = false + } + } + } + + // Resume tracking when the plan becomes empty again + Connections { + target: _planMasterController + function onContainsItemsChanged() { + if (!_planMasterController.containsItems) { + _homeTrackingMapCenter = true + if (_visualItems.count > 0) { + _updatingHomeFromMapCenter = true + _visualItems.get(0).coordinate = editorMap.center + _updatingHomeFromMapCenter = false + } + } + } + } + function insertSimpleItemAfterCurrent(coordinate) { var nextIndex = _missionController.currentPlanViewVIIndex + 1 _missionController.insertSimpleMissionItem(coordinate, nextIndex, true /* makeCurrentItem */) @@ -207,11 +225,13 @@ Item { onAcceptedForSave: (file) => { if (planFiles) { - _planMasterController.saveToFile(file) + if (_planMasterController.saveToFile(file)) { + close() + } } else { _planMasterController.saveToKml(file) + close() } - close() } onAcceptedForLoad: (file) => { @@ -225,6 +245,7 @@ Item { PlanViewToolBar { id: planToolBar planMasterController: _planMasterController + showRallyPointsHelp: _editingLayer === _layerRally } Item { @@ -255,18 +276,26 @@ Item { // Initial map position duplicates Fly view position Component.onCompleted: editorMap.center = QGroundControl.flightMapPosition - QGCMapPalette { id: mapPal; lightColors: editorMap.isSatelliteMap } - onZoomLevelChanged: { QGroundControl.flightMapZoom = editorMap.zoomLevel } onCenterChanged: { QGroundControl.flightMapPosition = editorMap.center + if (_homeTrackingMapCenter && !_planMasterController.containsItems && _visualItems.count > 0) { + _updatingHomeFromMapCenter = true + _visualItems.get(0).coordinate = editorMap.center + _updatingHomeFromMapCenter = false + } } onMapClicked: (mouse) => { // Take focus to close any previous editing editorMap.focus = true + + // Collapse layer switcher on any map click + layerSwitcher.expanded = false + collapseTimer.stop() + if (!mainWindow.allowViewSwitch()) { return } @@ -277,7 +306,20 @@ Item { switch (_editingLayer) { case _layerMission: - insertSimpleItemAfterCurrent(coordinate) + if (_addROIOnClick) { + _addROIOnClick = false + if (_missionController.isROIActive) { + var pos = Qt.point(mouse.x, mouse.y) + // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow + pos = editorMap.mapToItem(globals.parent, pos) + var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(pos.x, pos.y, 0, 0) }) + dropPanel.open() + } else { + insertROIAfterCurrent(coordinate) + } + } else if (_addWaypointOnClick) { + insertSimpleItemAfterCurrent(coordinate) + } break case _layerRally: if (_rallyPointController.supported) { @@ -287,35 +329,6 @@ Item { } } - function _mapRightClicked(position) { - // Take focus to close any previous editing - editorMap.focus = true - if (!mainWindow.allowViewSwitch()) { - return - } - var coordinate = editorMap.toCoordinate(position, false /* clipToViewPort */) - coordinate.latitude = coordinate.latitude.toFixed(_decimalPlaces) - coordinate.longitude = coordinate.longitude.toFixed(_decimalPlaces) - coordinate.altitude = coordinate.altitude.toFixed(_decimalPlaces) - - if (_editingLayer === _layerMission) { - if (!planMasterController.controllerVehicle.roiModeSupported) { - return - } - if (_missionController.isROIActive) { - // For some strange reason using mainWindow in mapToItem doesn't work, so we use globals.parent instead which also gets us mainWindow - position = editorMap.mapToItem(globals.parent, position) - var dropPanel = insertOrCancelROIDropPanelComponent.createObject(mainWindow, { mapClickCoord: coordinate, clickRect: Qt.rect(position.x, position.y, 0, 0) }) - dropPanel.open() - } else { - insertROIAfterCurrent(coordinate) - } - } - } - - onMapRightClicked: (position) => _mapRightClicked(position) - onMapPressAndHold: (position) => _mapRightClicked(position) - // Add the mission item visuals to the map Repeater { model: _missionController.visualItems @@ -425,17 +438,20 @@ Item { maxHeight: parent.height - toolStrip.y visible: _editingLayer == _layerMission - readonly property int fileButtonIndex: 0 - readonly property int takeoffButtonIndex: 1 - readonly property int waypointButtonIndex: 2 - readonly property int roiButtonIndex: 3 - readonly property int patternButtonIndex: 4 - readonly property int landButtonIndex: 5 - readonly property int centerButtonIndex: 6 - - property bool _isRallyLayer: _editingLayer == _layerRally property bool _isMissionLayer: _editingLayer == _layerMission + Binding { + target: waypointButton + property: "checked" + value: _addWaypointOnClick + } + + Binding { + target: roiButton + property: "checked" + value: _addROIOnClick + } + ToolStripActionList { id: toolStripActionList model: [ @@ -460,6 +476,22 @@ Item { } } }, + ToolStripAction { + id: waypointButton + text: qsTr("Waypoint") + iconSource: "/res/waypoint.svg" + visible: toolStrip._isMissionLayer + checkable: true + onTriggered: { _addWaypointOnClick = !_addWaypointOnClick; if (_addWaypointOnClick) _addROIOnClick = false } + }, + ToolStripAction { + id: roiButton + text: qsTr("ROI") + iconSource: "/qmlimages/roi.svg" + visible: toolStrip._isMissionLayer && _planMasterController.controllerVehicle.supports.roiMode + checkable: true + onTriggered: { _addROIOnClick = !_addROIOnClick; if (_addROIOnClick) _addWaypointOnClick = false } + }, ToolStripAction { text: _planMasterController.controllerVehicle.multiRotor ? qsTr("Return") @@ -501,6 +533,109 @@ Item { width: _rightPanelWidth planMasterController: _planMasterController editorMap: editorMap + onEditingLayerChangeRequested: (layer) => _editingLayer = layer + } + + // Layer switching icons — only active icon visible; click to expand choices leftward + Item { + id: layerSwitcher + anchors.right: rightPanel.left + anchors.rightMargin: _toolsMargin + anchors.top: parent.top + anchors.topMargin: _toolsMargin + width: layerRow.width + height: _layerButtonSize + z: QGroundControl.zOrderWidgets + + property bool expanded: false + property real _layerButtonSize: ScreenTools.defaultFontPixelHeight * 2.0 + property real _spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + readonly property var _layers: [ + { layer: _layerMission, icon: "/res/waypoint.svg", nodeType: "missionGroup" }, + { layer: _layerFence, icon: "/res/GeoFence.svg", nodeType: "fenceGroup" }, + { layer: _layerRally, icon: "/res/RallyPoint.svg", nodeType: "rallyGroup" } + ] + + Timer { + id: collapseTimer + interval: 5000 + onTriggered: layerSwitcher.expanded = false + } + + function toggle() { + expanded = !expanded + if (expanded) { + collapseTimer.restart() + } else { + collapseTimer.stop() + } + } + + function choose(nodeType) { + expanded = false + collapseTimer.stop() + rightPanel.selectLayer(nodeType) + } + + // Row laid out right-to-left: active icon on the right, choices expand left + Row { + id: layerRow + anchors.right: parent.right + spacing: layerSwitcher._spacing + layoutDirection: Qt.RightToLeft + + // Active layer button (always visible) + Rectangle { + width: layerSwitcher._layerButtonSize + height: width + radius: ScreenTools.defaultBorderRadius + color: QGroundControl.globalPalette.buttonHighlight + + QGCColoredImage { + anchors.centerIn: parent + width: parent.width * 0.6 + height: width + source: layerSwitcher._layers.find(l => l.layer === _editingLayer)?.icon ?? "/res/waypoint.svg" + color: QGroundControl.globalPalette.buttonHighlightText + } + + QGCMouseArea { + anchors.fill: parent + onClicked: layerSwitcher.toggle() + } + } + + // Choice buttons (only layers that are NOT the current one) + Repeater { + model: layerSwitcher._layers.filter(l => l.layer !== _editingLayer) + + Rectangle { + required property var modelData + width: layerSwitcher._layerButtonSize + height: width + radius: ScreenTools.defaultBorderRadius + color: QGroundControl.globalPalette.button + visible: opacity > 0 + opacity: layerSwitcher.expanded ? 1 : 0 + + Behavior on opacity { NumberAnimation { duration: 150 } } + + QGCColoredImage { + anchors.centerIn: parent + width: parent.width * 0.6 + height: width + source: modelData.icon + color: QGroundControl.globalPalette.buttonText + } + + QGCMouseArea { + anchors.fill: parent + onClicked: layerSwitcher.choose(modelData.nodeType) + } + } + } + } } RowLayout { @@ -675,7 +810,7 @@ Item { QGCButton { Layout.fillWidth: true - text: _planMasterController.dirty ? + text: (_planMasterController.dirtyForSave) ? (_planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Discard Unsaved Changes") : qsTr("Discard Unsaved Changes, Load New Plan From Vehicle")) : qsTr("Load New Plan From Vehicle") onClicked: { @@ -690,9 +825,6 @@ Item { text: _planMasterController.managerVehicle.isOfflineEditingVehicle ? qsTr("Keep Current Plan") : qsTr("Keep Current Plan, Don't Update From Vehicle") onClicked: { - if (!_planMasterController.managerVehicle.isOfflineEditingVehicle) { - _planMasterController.dirty = true - } _promptForPlanUsageShowing = false close() } @@ -706,6 +838,7 @@ Item { DropPanel { id: insertOrCancelROIDropPanel + onClosed: destroy() property var mapClickCoord diff --git a/src/PlanView/PlanViewRightPanel.qml b/src/PlanView/PlanViewRightPanel.qml new file mode 100644 index 000000000000..cd4ce1affe70 --- /dev/null +++ b/src/PlanView/PlanViewRightPanel.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +Item { + required property var editorMap + required property var planMasterController + + signal editingLayerChangeRequested(int layer) + + id: root + + property var _missionController: planMasterController.missionController + property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 + + function selectNextNotReady() { + for (var i = 0; i < _missionController.visualItems.count; i++) { + var vmi = _missionController.visualItems.get(i) + if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { + _missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) + break + } + } + } + + QGCPalette { id: qgcPal } + + Rectangle { + id: rightPanelBackground + anchors.fill: parent + color: qgcPal.window + opacity: 0.85 + } + + + // Open/Close panel + Item { + id: panelOpenCloseButton + anchors.right: parent.left + anchors.verticalCenter: parent.verticalCenter + width: toggleButtonRect.width - toggleButtonRect.radius + height: toggleButtonRect.height + clip: true + + property bool _expanded: root.anchors.right == root.parent.right + + Rectangle { + id: toggleButtonRect + width: ScreenTools.defaultFontPixelWidth * 2.25 + height: width * 3 + radius: ScreenTools.defaultBorderRadius + color: rightPanelBackground.color + opacity: rightPanelBackground.opacity + + QGCLabel { + id: toggleButtonLabel + anchors.centerIn: parent + text: panelOpenCloseButton._expanded ? ">" : "<" + color: qgcPal.buttonText + } + + } + + QGCMouseArea { + anchors.fill: parent + + onClicked: { + if (panelOpenCloseButton._expanded) { + // Close panel + root.anchors.right = undefined + root.anchors.left = root.parent.right + } else { + // Open panel + root.anchors.left = undefined + root.anchors.right = root.parent.right + } + } + } + } + + //------------------------------------------------------- + // Right Panel Controls + Item { + anchors.fill: rightPanelBackground + + DeadMouseArea { + anchors.fill: parent + } + + PlanTreeView { + id: planTreeView + anchors.fill: parent + editorMap: root.editorMap + planMasterController: root.planMasterController + onEditingLayerChangeRequested: (layer) => root.editingLayerChangeRequested(layer) + } + } + + function selectLayer(nodeType) { + // Ensure panel is open + if (!panelOpenCloseButton._expanded) { + root.anchors.left = undefined + root.anchors.right = root.parent.right + } + planTreeView.selectLayer(nodeType) + } +} diff --git a/src/QmlControls/RallyPointEditorHeader.qml b/src/PlanView/RallyPointEditorHeader.qml similarity index 93% rename from src/QmlControls/RallyPointEditorHeader.qml rename to src/PlanView/RallyPointEditorHeader.qml index 11172512a113..70fb4c8374c7 100644 --- a/src/QmlControls/RallyPointEditorHeader.qml +++ b/src/PlanView/RallyPointEditorHeader.qml @@ -41,7 +41,7 @@ Rectangle { anchors.right: parent.right wrapMode: Text.WordWrap font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("Rally Points provide alternate landing points when performing a Return to Launch (RTL).\n\nClick on the map to add Rally Points.") + text: qsTr("Rally Points provide alternate landing points when performing a Return to Launch (RTL).") } } } diff --git a/src/PlanView/RallyPointItemEditor.qml b/src/PlanView/RallyPointItemEditor.qml new file mode 100644 index 000000000000..89c2886f0dda --- /dev/null +++ b/src/PlanView/RallyPointItemEditor.qml @@ -0,0 +1,100 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +Rectangle { + id: root + height: _currentItem ? valuesRect.y + valuesRect.height + _innerMargin : titleLayout.y + titleLayout.height + _margin + color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade + radius: _radius + + property var rallyPoint ///< RallyPoint object associated with editor + property var controller ///< RallyPointController + + property bool _currentItem: rallyPoint ? rallyPoint === controller.currentRallyPoint : false + property color _outerTextColor: qgcPal.text + + readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 + readonly property real _innerMargin: 2 + readonly property real _radius: ScreenTools.defaultFontPixelWidth / 2 + readonly property real _titleHeight: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth + + QGCPalette { id: qgcPal; colorGroupEnabled: true } + + RowLayout { + id: titleLayout + anchors.margins: _margin + anchors.left: parent.left + anchors.rightMargin: _margin * 2 + anchors.right: parent.right + height: _titleHeight + spacing: ScreenTools.defaultFontPixelWidth + + QGCLabel { + text: qsTr("Rally Point") + color: _outerTextColor + } + + QGCColoredImage { + id: deleteButton + Layout.alignment: Qt.AlignRight + Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.5 + Layout.preferredHeight: Layout.preferredWidth + source: "/res/XDelete.svg" + color: _outerTextColor + } + } + + QGCMouseArea { + id: selectMouseArea + anchors.top: titleLayout.top + anchors.bottomMargin: -_margin + anchors.bottom: titleLayout.bottom + anchors.left: titleLayout.left + anchors.right: titleLayout.right + onClicked: controller.currentRallyPoint = rallyPoint + } + + QGCMouseArea { + anchors.top: selectMouseArea.top + anchors.bottom: selectMouseArea.bottom + anchors.right: selectMouseArea.right + width: height + onClicked: controller.removePoint(rallyPoint) + } + + Rectangle { + id: valuesRect + anchors.margins: _innerMargin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: titleLayout.bottom + height: valuesLayout.height + (_margin * 2) + color: qgcPal.windowShadeDark + visible: _currentItem + radius: _radius + + ColumnLayout { + id: valuesLayout + anchors.margins: _margin + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: _margin + + Repeater { + model: rallyPoint ? rallyPoint.textFieldFacts : 0 + + LabelledFactTextField { + Layout.fillWidth: true + label: modelData.shortDescription + fact: modelData + } + } + } + } +} diff --git a/src/QmlControls/RallyPointMapVisuals.qml b/src/PlanView/RallyPointMapVisuals.qml similarity index 100% rename from src/QmlControls/RallyPointMapVisuals.qml rename to src/PlanView/RallyPointMapVisuals.qml diff --git a/src/QmlControls/SimpleItemEditor.qml b/src/PlanView/SimpleItemEditor.qml similarity index 91% rename from src/QmlControls/SimpleItemEditor.qml rename to src/PlanView/SimpleItemEditor.qml index a6c2160d6051..dc476dc4aa5a 100644 --- a/src/QmlControls/SimpleItemEditor.qml +++ b/src/PlanView/SimpleItemEditor.qml @@ -8,18 +8,22 @@ import QGroundControl.FactControls // Editor for Simple mission items Rectangle { + required property var missionItem + required property real availableWidth + id: root width: availableWidth height: editorColumn.height + (_margin * 2) color: qgcPal.windowShadeDark radius: _radius + property bool _specifiesAltitude: missionItem.specifiesAltitude property real _margin: ScreenTools.defaultFontPixelHeight / 2 property real _altRectMargin: ScreenTools.defaultFontPixelWidth / 2 property var _controllerVehicle: missionItem.masterController.controllerVehicle - property int _globalAltMode: missionItem.masterController.missionController.globalAltitudeMode - property bool _globalAltModeIsMixed: _globalAltMode == QGroundControl.AltitudeModeMixed + property int _globalAltFrame: missionItem.masterController.missionController.globalAltitudeFrame + property bool _globalAltFrameIsMixed: _globalAltFrame == QGroundControl.AltitudeFrameMixed property real _radius: ScreenTools.defaultFontPixelWidth / 2 property real _fieldSpacing: ScreenTools.defaultFontPixelHeight / 2 @@ -144,17 +148,17 @@ Rectangle { RowLayout { Layout.fillWidth: true - visible: _globalAltModeIsMixed + visible: _globalAltFrameIsMixed QGCLabel { Layout.fillWidth: true - text: qsTr("Altitude Mode") + text: qsTr("Alt Frame") } - AltModeCombo { - altitudeMode: missionItem.altitudeMode + AltFrameCombo { + altitudeFrame: missionItem.altitudeFrame vehicle: _controllerVehicle - onAltitudeModeChanged: missionItem.altitudeMode = altitudeMode + onAltitudeFrameChanged: missionItem.altitudeFrame = altitudeFrame } } @@ -165,18 +169,14 @@ Rectangle { fact: missionItem.altitude function _extraLabelText() { - if (!_globalAltModeIsMixed && missionItem.altitudeMode !== QGroundControl.AltitudeModeRelative) { - return qsTr(" (%1)").arg(QGroundControl.altitudeModeShortDescription(missionItem.altitudeMode)) - } else { - return "" - } + return qsTr(" (%1)").arg(QGroundControl.altitudeFrameExtraUnits(missionItem.altitudeFrame)) } } QGCLabel { font.pointSize: ScreenTools.smallFontPointSize text: qsTr("Actual AMSL alt sent: %1 %2").arg(missionItem.amslAltAboveTerrain.valueString).arg(missionItem.amslAltAboveTerrain.units) - visible: missionItem.altitudeMode === QGroundControl.AltitudeModeCalcAboveTerrain + visible: missionItem.altitudeFrame === QGroundControl.AltitudeFrameCalcAboveTerrain } } @@ -247,6 +247,7 @@ Rectangle { CameraSection { Layout.fillWidth: true showSectionHeader: false + missionItem: root.missionItem visible: tabBar.currentIndex === 1 Component.onCompleted: checked = missionItem.cameraSection.settingsSpecified diff --git a/src/QmlControls/SimpleItemMapVisual.qml b/src/PlanView/SimpleItemMapVisual.qml similarity index 100% rename from src/QmlControls/SimpleItemMapVisual.qml rename to src/PlanView/SimpleItemMapVisual.qml diff --git a/src/QmlControls/StructureScanEditor.qml b/src/PlanView/StructureScanEditor.qml similarity index 96% rename from src/QmlControls/StructureScanEditor.qml rename to src/PlanView/StructureScanEditor.qml index 94f624954800..56c090f7cddc 100644 --- a/src/QmlControls/StructureScanEditor.qml +++ b/src/PlanView/StructureScanEditor.qml @@ -16,9 +16,8 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth property real _margin: ScreenTools.defaultFontPixelWidth / 2 property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 10.5 @@ -140,14 +139,14 @@ Rectangle { QGCLabel { text: qsTr("Scan Bottom Alt") } AltitudeFactTextField { fact: missionItem.scanBottomAlt - altitudeMode: QGroundControl.AltitudeModeRelative + altitudeFrame: QGroundControl.AltitudeFrameRelative Layout.fillWidth: true } QGCLabel { text: qsTr("Entrance/Exit Alt") } AltitudeFactTextField { fact: missionItem.entranceAlt - altitudeMode: QGroundControl.AltitudeModeRelative + altitudeFrame: QGroundControl.AltitudeFrameRelative Layout.fillWidth: true } diff --git a/src/QmlControls/StructureScanMapVisual.qml b/src/PlanView/StructureScanMapVisual.qml similarity index 100% rename from src/QmlControls/StructureScanMapVisual.qml rename to src/PlanView/StructureScanMapVisual.qml diff --git a/src/QmlControls/SurveyItemEditor.qml b/src/PlanView/SurveyItemEditor.qml similarity index 91% rename from src/QmlControls/SurveyItemEditor.qml rename to src/PlanView/SurveyItemEditor.qml index c99df42a8346..3bc82a05b3ad 100644 --- a/src/QmlControls/SurveyItemEditor.qml +++ b/src/PlanView/SurveyItemEditor.qml @@ -15,10 +15,6 @@ TransectStyleComplexItemEditor { transectValuesComponent: _transectValuesComponent presetsTransectValuesComponent: _transectValuesComponent - // The following properties must be available up the hierarchy chain - // property real availableWidth ///< Width for control - // property var missionItem ///< Mission Item for editor - property real _margin: ScreenTools.defaultFontPixelWidth / 2 property var _missionItem: missionItem @@ -70,13 +66,13 @@ TransectStyleComplexItemEditor { { text: qsTr("Hover and capture image"), fact: missionItem.hoverAndCapture, - enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeRelative || missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeAbsolute, + enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameRelative || missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameAbsolute, visible: missionItem.hoverAndCaptureAllowed }, { text: qsTr("Refly at 90 deg offset"), fact: missionItem.refly90Degrees, - enabled: missionItem.cameraCalc.distanceMode !== QGroundControl.AltitudeModeCalcAboveTerrain, + enabled: missionItem.cameraCalc.distanceMode !== QGroundControl.AltitudeFrameCalcAboveTerrain, visible: true }, { diff --git a/src/QmlControls/SurveyMapVisual.qml b/src/PlanView/SurveyMapVisual.qml similarity index 100% rename from src/QmlControls/SurveyMapVisual.qml rename to src/PlanView/SurveyMapVisual.qml diff --git a/src/QmlControls/TakeoffItemMapVisual.qml b/src/PlanView/TakeoffItemMapVisual.qml similarity index 99% rename from src/QmlControls/TakeoffItemMapVisual.qml rename to src/PlanView/TakeoffItemMapVisual.qml index f175d3121004..20985a3758a7 100644 --- a/src/QmlControls/TakeoffItemMapVisual.qml +++ b/src/PlanView/TakeoffItemMapVisual.qml @@ -118,7 +118,7 @@ Item { sourceItem: MissionItemIndexLabel { checked: _missionItem.isCurrentItem - label: qsTr("Launch") + label: qsTr("Home") highlightSelected: true onClicked: _root.clicked(_missionItem.sequenceNumber) visible: _root.interactive diff --git a/src/QmlControls/TerrainStatus.qml b/src/PlanView/TerrainStatus.qml similarity index 100% rename from src/QmlControls/TerrainStatus.qml rename to src/PlanView/TerrainStatus.qml diff --git a/src/QmlControls/TransectStyleComplexItemEditor.qml b/src/PlanView/TransectStyleComplexItemEditor.qml similarity index 99% rename from src/QmlControls/TransectStyleComplexItemEditor.qml rename to src/PlanView/TransectStyleComplexItemEditor.qml index 68b6e2a953ef..a9d478d16984 100644 --- a/src/QmlControls/TransectStyleComplexItemEditor.qml +++ b/src/PlanView/TransectStyleComplexItemEditor.qml @@ -15,6 +15,9 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius + required property var missionItem + required property real availableWidth + property bool transectAreaDefinitionComplete: true property string transectAreaDefinitionHelp: _internalError property string transectValuesHeaderName: _internalError diff --git a/src/QmlControls/TransectStyleComplexItemStats.qml b/src/PlanView/TransectStyleComplexItemStats.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemStats.qml rename to src/PlanView/TransectStyleComplexItemStats.qml diff --git a/src/QmlControls/TransectStyleComplexItemTabBar.qml b/src/PlanView/TransectStyleComplexItemTabBar.qml similarity index 100% rename from src/QmlControls/TransectStyleComplexItemTabBar.qml rename to src/PlanView/TransectStyleComplexItemTabBar.qml diff --git a/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml b/src/PlanView/TransectStyleComplexItemTerrainFollow.qml similarity index 66% rename from src/QmlControls/TransectStyleComplexItemTerrainFollow.qml rename to src/PlanView/TransectStyleComplexItemTerrainFollow.qml index 66c034f82a5a..1d289091628e 100644 --- a/src/QmlControls/TransectStyleComplexItemTerrainFollow.qml +++ b/src/PlanView/TransectStyleComplexItemTerrainFollow.qml @@ -18,29 +18,29 @@ ColumnLayout { onClicked: { var removeModes = [] - var updateFunction = function(altMode){ missionItem.cameraCalc.distanceMode = altMode } - removeModes.push(QGroundControl.AltitudeModeMixed) - if (!missionItem.masterController.controllerVehicle.supportsTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) + var updateFunction = function(altFrame){ missionItem.cameraCalc.distanceMode = altFrame } + removeModes.push(QGroundControl.AltitudeFrameMixed) + if (!missionItem.masterController.controllerVehicle.supports.terrainFrame) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) } - if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude || !_missionItem.cameraCalc.isManualCamera) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) + if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude || !missionItem.cameraCalc.isManualCamera) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) } - altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) + altFrameDialogFactory.open({ currentAltFrame: missionItem.cameraCalc.distanceMode, rgRemoveModes: removeModes, updateAltFrameFn: updateFunction }) } QGCPopupDialogFactory { - id: altModeDialogFactory + id: altFrameDialogFactory - dialogComponent: altModeDialogComponent + dialogComponent: altFrameDialogComponent } - Component { id: altModeDialogComponent; AltModeDialog { } } + Component { id: altFrameDialogComponent; AltFrameDialog { } } RowLayout { spacing: ScreenTools.defaultFontPixelWidth / 2 - QGCLabel { text: QGroundControl.altitudeModeShortDescription(missionItem.cameraCalc.distanceMode) } + QGCLabel { text: QGroundControl.altitudeFrameShortDescription(missionItem.cameraCalc.distanceMode) } QGCColoredImage { height: ScreenTools.defaultFontPixelHeight / 2 width: height @@ -55,7 +55,7 @@ ColumnLayout { columnSpacing: _margin rowSpacing: _margin columns: 2 - enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeModeCalcAboveTerrain + enabled: missionItem.cameraCalc.distanceMode === QGroundControl.AltitudeFrameCalcAboveTerrain QGCLabel { text: qsTr("Tolerance") } FactTextField { diff --git a/src/QmlControls/TransectStyleMapVisuals.qml b/src/PlanView/TransectStyleMapVisuals.qml similarity index 89% rename from src/QmlControls/TransectStyleMapVisuals.qml rename to src/PlanView/TransectStyleMapVisuals.qml index b987ce079ef3..ce6ddf17da54 100644 --- a/src/QmlControls/TransectStyleMapVisuals.qml +++ b/src/PlanView/TransectStyleMapVisuals.qml @@ -15,10 +15,12 @@ Item { property bool polygonInteractive: true property bool interactive: true property var vehicle + property bool hideMapPolygon: false property var _missionItem: object property var _mapPolygon: object.surveyAreaPolygon property bool _currentItem: object.isCurrentItem + property bool _vertexDrag: _mapPolygon.vertexDrag || hideMapPolygon property var _transectPoints: _missionItem.visualTransectPoints property int _transectCount: _transectPoints.length / (_hasTurnaround ? 4 : 2) property bool _hasTurnaround: _missionItem.turnAroundDistance.rawValue !== 0 @@ -67,6 +69,7 @@ Item { interiorColor: QGroundControl.globalPalette.surveyPolygonInterior altColor: QGroundControl.globalPalette.surveyPolygonTerrainCollision interiorOpacity: 0.5 * _root.opacity + visible: !hideMapPolygon } // Full set of transects lines. Shown when item is selected. @@ -77,7 +80,7 @@ Item { line.color: "white" line.width: 2 path: _transectPoints - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -90,7 +93,7 @@ Item { line.color: "white" line.width: 2 path: _showPartialEntryExit ? [ _transectPoints[0], _transectPoints[1] ] : [] - visible: _showPartialEntryExit + visible: _showPartialEntryExit && !_vertexDrag opacity: _root.opacity } } @@ -101,7 +104,7 @@ Item { line.color: "white" line.width: 2 path: _showPartialEntryExit ? [ _transectPoints[_lastPointIndex - 1], _transectPoints[_lastPointIndex] ] : [] - visible: _showPartialEntryExit + visible: _showPartialEntryExit && !_vertexDrag opacity: _root.opacity } } @@ -115,7 +118,7 @@ Item { anchorPoint.y: sourceItem.anchorPointY z: QGroundControl.zOrderMapItems coordinate: _missionItem.coordinate - visible: _missionItem.exitCoordinate.isValid + visible: _missionItem.exitCoordinate.isValid && !_vertexDrag opacity: _root.opacity sourceItem: MissionItemIndexLabel { @@ -133,7 +136,7 @@ Item { fromCoord: _transectPoints[_firstTrueTransectIndex] toCoord: _transectPoints[_firstTrueTransectIndex + 1] arrowPosition: 1 - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -145,7 +148,7 @@ Item { fromCoord: _transectPoints[nextTrueTransectIndex] toCoord: _transectPoints[nextTrueTransectIndex + 1] arrowPosition: 1 - visible: _currentItem && _transectCount > 3 + visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity property int nextTrueTransectIndex: _firstTrueTransectIndex + (_hasTurnaround ? 4 : 2) @@ -159,7 +162,7 @@ Item { fromCoord: _transectPoints[_lastTrueTransectIndex - 1] toCoord: _transectPoints[_lastTrueTransectIndex] arrowPosition: 3 - visible: _currentItem + visible: _currentItem && !_vertexDrag opacity: _root.opacity } } @@ -171,7 +174,7 @@ Item { fromCoord: _transectPoints[prevTrueTransectIndex - 1] toCoord: _transectPoints[prevTrueTransectIndex] arrowPosition: 13 - visible: _currentItem && _transectCount > 3 + visible: _currentItem && _transectCount > 3 && !_vertexDrag opacity: _root.opacity property int prevTrueTransectIndex: _lastTrueTransectIndex - (_hasTurnaround ? 4 : 2) @@ -187,7 +190,7 @@ Item { anchorPoint.y: sourceItem.anchorPointY z: QGroundControl.zOrderMapItems coordinate: _missionItem.exitCoordinate - visible: _missionItem.exitCoordinate.isValid + visible: _missionItem.exitCoordinate.isValid && !_vertexDrag opacity: _root.opacity sourceItem: MissionItemIndexLabel { diff --git a/src/PlanView/TransformEditor.qml b/src/PlanView/TransformEditor.qml new file mode 100644 index 000000000000..f990ec517a3e --- /dev/null +++ b/src/PlanView/TransformEditor.qml @@ -0,0 +1,336 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls +import QGroundControl.FactControls + +Rectangle { + id: _root + + required property var missionController + + width: parent ? parent.width : 0 + height: mainColumn.height + (_margins * 2) + color: QGroundControl.globalPalette.windowShadeDark + + property real _margins: ScreenTools.defaultFontPixelWidth / 2 + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 20 + property real _labelWidth: ScreenTools.defaultFontPixelWidth * 14 + property bool _hasHome: missionController ? missionController.plannedHomePosition.isValid : false + + TransformPositionController { + id: positionController + Component.onCompleted: { + if (_hasHome) { + coordinate = _root.missionController.plannedHomePosition + initValues() + } + } + } + + Connections { + target: _root.missionController + function onPlannedHomePositionChanged() { + positionController.coordinate = _root.missionController.plannedHomePosition + positionController.initValues() + } + } + + ColumnLayout { + id: mainColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: _margins + spacing: ScreenTools.defaultFontPixelHeight * 0.5 + + // ── Offset Mission ── + SectionHeader { + id: offsetSection + Layout.fillWidth: true + text: qsTr("Offset Mission") + checked: false + } + + ColumnLayout { + Layout.fillWidth: true + spacing: _margins + visible: offsetSection.checked + + LabelledFactTextField { + id: eastField + label: qsTr("East") + fact: positionController.offsetEast + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + LabelledFactTextField { + id: northField + label: qsTr("North") + fact: positionController.offsetNorth + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + LabelledFactTextField { + id: upField + label: qsTr("Up") + fact: positionController.offsetUp + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + QGCCheckBox { + id: offsetTakeoffCheck + text: qsTr("Also move takeoff items") + } + + QGCCheckBox { + id: offsetLandingCheck + text: qsTr("Also move landing items") + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Note: Home altitude is not modified.") + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Apply Offset") + enabled: !eastField.textField.validationError + && !northField.textField.validationError + && !upField.textField.validationError + + onClicked: { + _root.missionController.offsetMission( + positionController.offsetEast.rawValue, + positionController.offsetNorth.rawValue, + positionController.offsetUp.rawValue, + offsetTakeoffCheck.checked, + offsetLandingCheck.checked + ) + } + } + } + + // ── Reposition Mission ── + SectionHeader { + id: repositionSection + Layout.fillWidth: true + text: qsTr("Reposition Mission") + checked: false + } + + ColumnLayout { + id: repositionContent + Layout.fillWidth: true + spacing: _margins + visible: repositionSection.checked + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Home position must be set to reposition the mission.") + visible: !_hasHome + } + + property bool _showGeographic: coordinateSystemCombo.currentIndex === 0 + property bool _showUTM: coordinateSystemCombo.currentIndex === 1 + property bool _showMGRS: coordinateSystemCombo.currentIndex === 2 + property bool _showVehicle: coordinateSystemCombo.currentIndex === 3 + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + QGCLabel { + text: qsTr("Coordinate System") + } + + QGCComboBox { + id: coordinateSystemCombo + Layout.fillWidth: true + model: globals.activeVehicle + ? [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference"), qsTr("Vehicle Position") ] + : [ qsTr("Geographic"), qsTr("Universal Transverse Mercator"), qsTr("Military Grid Reference") ] + } + } + + LabelledFactTextField { + id: latitudeField + label: qsTr("Latitude") + fact: positionController.latitude + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showGeographic + } + + LabelledFactTextField { + id: longitudeField + label: qsTr("Longitude") + fact: positionController.longitude + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showGeographic + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !latitudeField.textField.validationError && !longitudeField.textField.validationError + visible: repositionContent._showGeographic + onClicked: { + positionController.setFromGeo() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + LabelledFactTextField { + id: zoneField + label: qsTr("Zone") + fact: positionController.zone + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactComboBox { + label: qsTr("Hemisphere") + fact: positionController.hemisphere + indexModel: false + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactTextField { + id: eastingField + label: qsTr("Easting") + fact: positionController.easting + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + LabelledFactTextField { + id: northingField + label: qsTr("Northing") + fact: positionController.northing + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showUTM + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !zoneField.textField.validationError && !eastingField.textField.validationError && !northingField.textField.validationError + visible: repositionContent._showUTM + onClicked: { + positionController.setFromUTM() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + LabelledFactTextField { + id: mgrsField + label: qsTr("MGRS") + fact: positionController.mgrs + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + visible: repositionContent._showMGRS + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Position") + enabled: _hasHome && !mgrsField.textField.validationError + visible: repositionContent._showMGRS + onClicked: { + positionController.setFromMGRS() + _root.missionController.repositionMission(positionController.coordinate) + } + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Move to Vehicle Position") + enabled: _hasHome + visible: repositionContent._showVehicle + onClicked: { + positionController.setFromVehicle() + _root.missionController.repositionMission(positionController.coordinate) + } + } + } + + // ── Rotate Mission ── + SectionHeader { + id: rotateSection + Layout.fillWidth: true + text: qsTr("Rotate Mission") + checked: false + } + + ColumnLayout { + Layout.fillWidth: true + spacing: _margins + visible: rotateSection.checked + + QGCLabel { + Layout.fillWidth: true + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Home position must be set to rotate the mission.") + visible: !_hasHome + } + + LabelledFactTextField { + id: degreesCWField + label: qsTr("Clockwise") + fact: positionController.rotateDegreesCW + textFieldPreferredWidth: _textFieldWidth + Layout.fillWidth: true + } + + QGCCheckBox { + id: rotateTakeoffCheck + text: qsTr("Also move takeoff items") + } + + QGCCheckBox { + id: rotateLandingCheck + text: qsTr("Also move landing items") + } + + QGCLabel { + Layout.fillWidth: true + Layout.maximumWidth: _labelWidth + _textFieldWidth + wrapMode: Text.WordWrap + font.pointSize: ScreenTools.smallFontPointSize + text: qsTr("Note: Complex items are rotated by moving their reference coordinate: their geometry and orientation are not changed.") + } + + QGCButton { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Apply Rotation") + enabled: _hasHome && !degreesCWField.textField.validationError + + onClicked: { + _root.missionController.rotateMission( + positionController.rotateDegreesCW.rawValue, + rotateTakeoffCheck.checked, + rotateLandingCheck.checked + ) + } + } + } + } +} diff --git a/src/QmlControls/VTOLLandingPatternEditor.qml b/src/PlanView/VTOLLandingPatternEditor.qml similarity index 95% rename from src/QmlControls/VTOLLandingPatternEditor.qml rename to src/PlanView/VTOLLandingPatternEditor.qml index 4a6402df208f..77cc58a678e6 100644 --- a/src/QmlControls/VTOLLandingPatternEditor.qml +++ b/src/PlanView/VTOLLandingPatternEditor.qml @@ -15,18 +15,17 @@ Rectangle { color: qgcPal.windowShadeDark radius: _radius - // The following properties must be available up the hierarchy chain - //property real availableWidth ///< Width for control - //property var missionItem ///< Mission Item for editor + required property var missionItem + required property real availableWidth - property var _masterControler: masterController + property var _masterControler: missionItem.masterController property var _missionController: _masterControler.missionController property var _missionVehicle: _masterControler.controllerVehicle property real _margin: ScreenTools.defaultFontPixelWidth / 2 property real _spacer: ScreenTools.defaultFontPixelWidth / 2 property string _setToVehicleHeadingStr: qsTr("Set to vehicle heading") property string _setToVehicleLocationStr: qsTr("Set to vehicle location") - property int _altitudeMode: missionItem.altitudesAreRelative ? QGroundControl.AltitudeModeRelative : QGroundControl.AltitudeModeAbsolute + property int _altitudeFrame: missionItem.altitudesAreRelative ? QGroundControl.AltitudeFrameRelative : QGroundControl.AltitudeFrameAbsolute Column { @@ -67,7 +66,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.finalApproachAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCLabel { @@ -129,7 +128,7 @@ Rectangle { AltitudeFactTextField { Layout.fillWidth: true fact: missionItem.landingAltitude - altitudeMode: _altitudeMode + altitudeFrame: _altitudeFrame } QGCLabel { text: qsTr("Landing Dist") } diff --git a/src/QmlControls/VTOLLandingPatternMapVisual.qml b/src/PlanView/VTOLLandingPatternMapVisual.qml similarity index 100% rename from src/QmlControls/VTOLLandingPatternMapVisual.qml rename to src/PlanView/VTOLLandingPatternMapVisual.qml diff --git a/src/PositionManager/PositionManager.cpp b/src/PositionManager/PositionManager.cpp index e4da61421da0..1e4670419f52 100644 --- a/src/PositionManager/PositionManager.cpp +++ b/src/PositionManager/PositionManager.cpp @@ -7,8 +7,6 @@ #include #include -#include -#include #include QGC_LOGGING_CATEGORY(QGCPositionManagerLog, "PositionManager.QGCPositionManager") @@ -106,6 +104,7 @@ void QGCPositionManager::setNmeaSourceDevice(QIODevice *device) void QGCPositionManager::_positionUpdated(const QGeoPositionInfo &update) { _geoPositionInfo = update; + _gcsPositioningError = QGeoPositionInfoSource::NoError; QGeoCoordinate newGCSPosition(_gcsPosition); @@ -143,6 +142,12 @@ void QGCPositionManager::_positionUpdated(const QGeoPositionInfo &update) emit positionInfoUpdated(update); } +void QGCPositionManager::_positionError(QGeoPositionInfoSource::Error gcsPositioningError) +{ + qCWarning(QGCPositionManagerLog) << Q_FUNC_INFO << "Positioning error:" << gcsPositioningError; + _gcsPositioningError = gcsPositioningError; +} + void QGCPositionManager::_setGCSHeading(qreal newGCSHeading) { if (newGCSHeading != _gcsHeading) { @@ -201,10 +206,9 @@ void QGCPositionManager::_setPositionSource(QGCPositionSource source) #if !defined(Q_OS_DARWIN) && !defined(Q_OS_IOS) _currentSource->setUpdateInterval(_updateInterval); #endif + (void) connect(_currentSource, &QGeoPositionInfoSource::positionUpdated, this, &QGCPositionManager::_positionUpdated); - (void) connect(_currentSource, &QGeoPositionInfoSource::errorOccurred, this, [](QGeoPositionInfoSource::Error positioningError) { - qCWarning(QGCPositionManagerLog) << Q_FUNC_INFO << positioningError; - }); + (void) connect(_currentSource, &QGeoPositionInfoSource::errorOccurred, this, &QGCPositionManager::_positionError); // (void) connect(QGCCompass::instance(), &QGCCompass::positionUpdated, this, &QGCPositionManager::_positionUpdated); diff --git a/src/PositionManager/PositionManager.h b/src/PositionManager/PositionManager.h index 516628f66c84..40490ed8ae25 100644 --- a/src/PositionManager/PositionManager.h +++ b/src/PositionManager/PositionManager.h @@ -4,11 +4,11 @@ #include #include #include +#include #include Q_DECLARE_LOGGING_CATEGORY(QGCPositionManagerLog) -class QGeoPositionInfoSource; class QNmeaPositionInfoSource; class QGCCompass; @@ -35,6 +35,8 @@ class QGCPositionManager : public QObject qreal gcsHeading() const { return _gcsHeading; } qreal gcsPositionHorizontalAccuracy() const { return _gcsPositionHorizontalAccuracy; } QGeoPositionInfo geoPositionInfo() const { return _geoPositionInfo; } + QGeoPositionInfoSource::Error gcsPositioningError() const { return _gcsPositioningError; } + int updateInterval() const { return _updateInterval; } void setNmeaSourceDevice(QIODevice *device); @@ -47,6 +49,7 @@ class QGCPositionManager : public QObject private slots: void _positionUpdated(const QGeoPositionInfo &update); + void _positionError(QGeoPositionInfoSource::Error gcsPositioningError); private: enum QGCPositionSource { @@ -68,6 +71,8 @@ private slots: int _updateInterval = 0; QGeoPositionInfo _geoPositionInfo; + QGeoPositionInfoSource::Error _gcsPositioningError = QGeoPositionInfoSource::NoError; + QGeoCoordinate _gcsPosition; qreal _gcsHeading = qQNaN(); qreal _gcsPositionHorizontalAccuracy = std::numeric_limits::infinity(); diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index b2f8da65b9f3..b504457a2abe 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -228,13 +228,13 @@ void QGCApplication::init() if (_simpleBootTest) { // Since GStream builds are so problematic we initialize video during the simple boot test // to make sure it works and verfies plugin availability. - _initVideo(); + _bootTestPassed = _initVideo(); } else if (!_runningUnitTests) { _initForNormalAppBoot(); } } -void QGCApplication::_initVideo() +bool QGCApplication::_initVideo() { #ifdef QGC_GST_STREAMING // Gstreamer video playback requires OpenGL @@ -242,13 +242,16 @@ void QGCApplication::_initVideo() #endif QGCCorePlugin::instance(); // CorePlugin must be initialized before VideoManager for Video Cleanup - VideoManager::instance(); + VideoManager *videoManager = VideoManager::instance(); + videoManager->startGStreamerInit(); + const bool initSucceeded = !_simpleBootTest || videoManager->waitForGStreamerInit(); _videoManagerInitialized = true; + return initSucceeded; } void QGCApplication::_initForNormalAppBoot() { - _initVideo(); // GStreamer must be initialized before QmlEngine + (void) _initVideo(); QQuickStyle::setStyle("Basic"); QGCCorePlugin::instance()->init(); @@ -677,6 +680,38 @@ void QGCApplication::shutdown() QGCCorePlugin::instance()->cleanup(); + if (_runningUnitTests || _simpleBootTest) { + const QSettings settings; + const QString settingsFile = settings.fileName(); + if (QFile::exists(settingsFile)) { + if (QFile::remove(settingsFile)) { + qCDebug(QGCApplicationLog) << "Removed test run settings file:" << settingsFile; + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run settings file:" << settingsFile; + } + } + + // Remove the app-specific settings directory (parent of ParamCache) + QDir settingsAppDir(ParameterManager::parameterCacheDir()); + settingsAppDir.cdUp(); + if (settingsAppDir.exists()) { + if (settingsAppDir.removeRecursively()) { + qCDebug(QGCApplicationLog) << "Removed test run settings directory:" << settingsAppDir.absolutePath(); + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run settings directory:" << settingsAppDir.absolutePath(); + } + } + + QDir appDir(SettingsManager::instance()->appSettings()->savePath()->rawValue().toString()); + if (appDir.exists()) { + if (appDir.removeRecursively()) { + qCDebug(QGCApplicationLog) << "Removed test run app data directory:" << appDir.absolutePath(); + } else { + qCWarning(QGCApplicationLog) << "Failed to remove test run app data directory:" << appDir.absolutePath(); + } + } + } + // This is bad, but currently qobject inheritances are incorrect and cause crashes on exit without delete _qmlAppEngine; } diff --git a/src/QGCApplication.h b/src/QGCApplication.h index 3d80a9fb9a3e..dc5a59973b33 100644 --- a/src/QGCApplication.h +++ b/src/QGCApplication.h @@ -56,6 +56,7 @@ class QGCApplication : public QApplication bool runningUnitTests() const { return _runningUnitTests; } bool simpleBootTest() const { return _simpleBootTest; } + bool bootTestPassed() const { return _bootTestPassed; } /// Returns true if Qt debug output should be logged to a file bool logOutput() const { return _logOutput; } @@ -125,7 +126,7 @@ private slots: private: bool compressEvent(QEvent *event, QObject *receiver, QPostEventList *postedEvents) final; - void _initVideo(); + bool _initVideo(); /// Initialize the application for normal application boot. Or in other words we are not going to run unit tests. void _initForNormalAppBoot(); @@ -156,6 +157,7 @@ private slots: bool _showErrorsInToolbar = false; QElapsedTimer _msecsElapsedTime; bool _videoManagerInitialized = false; + bool _bootTestPassed = true; QList> _delayedAppMessages; diff --git a/src/QmlControls/AltFrameCombo.qml b/src/QmlControls/AltFrameCombo.qml new file mode 100644 index 000000000000..f0e3620c9d9b --- /dev/null +++ b/src/QmlControls/AltFrameCombo.qml @@ -0,0 +1,60 @@ +import QtQuick + +import QGroundControl +import QGroundControl.Controls + +QGCComboBox { + required property int altitudeFrame + required property var vehicle + + textRole: "modeName" + + onActivated: (index) => { + let modeValue = altFrameModel.get(index).modeValue + altitudeFrame = modeValue + } + + ListModel { + id: altFrameModel + } + + Component.onCompleted: { + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameRelative), + modeValue: QGroundControl.AltitudeFrameRelative }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameAbsolute), + modeValue: QGroundControl.AltitudeFrameAbsolute }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameTerrain), + modeValue: QGroundControl.AltitudeFrameTerrain }) + altFrameModel.append({ modeName: QGroundControl.altitudeFrameExtraUnits(QGroundControl.AltitudeFrameCalcAboveTerrain), + modeValue: QGroundControl.AltitudeFrameCalcAboveTerrain }) + + let removeModes = [] + + if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude && altitudeFrame != QGroundControl.AltitudeFrameAbsolute) { + removeModes.push(QGroundControl.AltitudeFrameAbsolute) + } + if (!vehicle.supports.terrainFrame) { + removeModes.push(QGroundControl.AltitudeFrameTerrain) + } + + // Remove modes specified by consumer + for (var i=0; i { - let modeValue = altModeModel.get(index).modeValue - altitudeMode = modeValue - } - - ListModel { - id: altModeModel - - ListElement { - modeName: qsTr("Relative") - modeValue: QGroundControl.AltitudeModeRelative - } - ListElement { - modeName: qsTr("Absolute") - modeValue: QGroundControl.AltitudeModeAbsolute - } - ListElement { - modeName: qsTr("Terrain") - modeValue: QGroundControl.AltitudeModeTerrainFrame - } - ListElement { - modeName: qsTr("TerrainC") - modeValue: QGroundControl.AltitudeModeCalcAboveTerrain - } - } - - Component.onCompleted: { - let removeModes = [] - - if (!QGroundControl.corePlugin.options.showMissionAbsoluteAltitude && altitudeMode != QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) - } - if (!vehicle.supportsTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - - // Remove modes specified by consumer - for (var i=0; i EditPositionDialogController::_metaDataMap; - -EditPositionDialogController::EditPositionDialogController(QObject *parent) - : QObject(parent) - , _latitudeFact(new Fact(0, _latitudeFactName, FactMetaData::valueTypeDouble, this)) - , _longitudeFact(new Fact(0, _longitudeFactName, FactMetaData::valueTypeDouble, this)) - , _zoneFact(new Fact(0, _zoneFactName, FactMetaData::valueTypeUint8, this)) - , _hemisphereFact(new Fact(0, _hemisphereFactName, FactMetaData::valueTypeUint8, this)) - , _eastingFact(new Fact(0, _eastingFactName, FactMetaData::valueTypeDouble, this)) - , _northingFact(new Fact(0, _northingFactName, FactMetaData::valueTypeDouble, this)) - , _mgrsFact(new Fact(0, _mgrsFactName, FactMetaData::valueTypeString, this)) -{ - // qCDebug(EditPositionDialogControllerLog) << Q_FUNC_INFO << this; - - if (_metaDataMap.isEmpty()) { - _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/EditPositionDialog.FactMetaData.json"), nullptr /* QObject parent */); - } - - _latitudeFact->setMetaData(_metaDataMap[_latitudeFactName]); - _longitudeFact->setMetaData(_metaDataMap[_longitudeFactName]); - _zoneFact->setMetaData(_metaDataMap[_zoneFactName]); - _hemisphereFact->setMetaData(_metaDataMap[_hemisphereFactName]); - _eastingFact->setMetaData(_metaDataMap[_eastingFactName]); - _northingFact->setMetaData(_metaDataMap[_northingFactName]); - _mgrsFact->setMetaData(_metaDataMap[_mgrsFactName]); -} - -EditPositionDialogController::~EditPositionDialogController() -{ - // qCDebug(EditPositionDialogControllerLog) << Q_FUNC_INFO << this; -} - -void EditPositionDialogController::setCoordinate(QGeoCoordinate coordinate) -{ - if (coordinate != _coordinate) { - _coordinate = coordinate; - emit coordinateChanged(coordinate); - } -} - -void EditPositionDialogController::initValues() -{ - _latitudeFact->setRawValue(_coordinate.latitude()); - _longitudeFact->setRawValue(_coordinate.longitude()); - - double easting, northing; - const int zone = QGCGeo::convertGeoToUTM(_coordinate, easting, northing); - if ((zone >= 1) && (zone <= 60)) { - _zoneFact->setRawValue(zone); - _hemisphereFact->setRawValue(_coordinate.latitude() < 0); - _eastingFact->setRawValue(easting); - _northingFact->setRawValue(northing); - } - - const QString mgrs = QGCGeo::convertGeoToMGRS(_coordinate); - if (!mgrs.isEmpty()) { - _mgrsFact->setRawValue(mgrs); - } -} - -void EditPositionDialogController::setFromGeo() -{ - _coordinate.setLatitude(_latitudeFact->rawValue().toDouble()); - _coordinate.setLongitude(_longitudeFact->rawValue().toDouble()); - emit coordinateChanged(_coordinate); -} - -void EditPositionDialogController::setFromUTM() -{ - qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1); - if (QGCGeo::convertUTMToGeo(_eastingFact->rawValue().toDouble(), _northingFact->rawValue().toDouble(), _zoneFact->rawValue().toInt(), _hemisphereFact->rawValue().toInt() == 1, _coordinate)) { - qCDebug(EditPositionDialogControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << _coordinate; - emit coordinateChanged(_coordinate); - } else { - initValues(); - } -} - -void EditPositionDialogController::setFromMGRS() -{ - if (QGCGeo::convertMGRSToGeo(_mgrsFact->rawValue().toString(), _coordinate)) { - emit coordinateChanged(_coordinate); - } else { - initValues(); - } -} - -void EditPositionDialogController::setFromVehicle() -{ - setCoordinate(MultiVehicleManager::instance()->activeVehicle()->coordinate()); -} diff --git a/src/QmlControls/EditPositionDialogController.h b/src/QmlControls/EditPositionDialogController.h deleted file mode 100644 index 6d4cabf878b0..000000000000 --- a/src/QmlControls/EditPositionDialogController.h +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "Fact.h" - -Q_DECLARE_LOGGING_CATEGORY(EditPositionDialogControllerLog) - -class EditPositionDialogController : public QObject -{ - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) - Q_PROPERTY(Fact *latitude READ latitude CONSTANT) - Q_PROPERTY(Fact *longitude READ longitude CONSTANT) - Q_PROPERTY(Fact *zone READ zone CONSTANT) - Q_PROPERTY(Fact *hemisphere READ hemisphere CONSTANT) - Q_PROPERTY(Fact *easting READ easting CONSTANT) - Q_PROPERTY(Fact *northing READ northing CONSTANT) - Q_PROPERTY(Fact *mgrs READ mgrs CONSTANT) - -public: - explicit EditPositionDialogController(QObject *parent = nullptr); - ~EditPositionDialogController(); - - Q_INVOKABLE void initValues(); - Q_INVOKABLE void setFromGeo(); - Q_INVOKABLE void setFromUTM(); - Q_INVOKABLE void setFromMGRS(); - Q_INVOKABLE void setFromVehicle(); - - void setCoordinate(QGeoCoordinate coordinate); - QGeoCoordinate coordinate() const { return _coordinate; } - - Fact *latitude() { return _latitudeFact; } - Fact *longitude() { return _longitudeFact; } - Fact *zone() { return _zoneFact; } - Fact *hemisphere() { return _hemisphereFact; } - Fact *easting() { return _eastingFact; } - Fact *northing() { return _northingFact; } - Fact *mgrs() { return _mgrsFact; } - -signals: - void coordinateChanged(QGeoCoordinate coordinate); - -private: - QGeoCoordinate _coordinate; - - Fact *_latitudeFact = nullptr; - Fact *_longitudeFact = nullptr; - Fact *_zoneFact = nullptr; - Fact *_hemisphereFact = nullptr; - Fact *_eastingFact = nullptr; - Fact *_northingFact = nullptr; - Fact *_mgrsFact = nullptr; - - static QMap _metaDataMap; - - static constexpr const char *_latitudeFactName = "Latitude"; - static constexpr const char *_longitudeFactName = "Longitude"; - static constexpr const char *_zoneFactName = "Zone"; - static constexpr const char *_hemisphereFactName = "Hemisphere"; - static constexpr const char *_eastingFactName = "Easting"; - static constexpr const char *_northingFactName = "Northing"; - static constexpr const char *_mgrsFactName = "MGRS"; -}; diff --git a/src/QmlControls/MissionSettingsEditor.qml b/src/QmlControls/MissionSettingsEditor.qml deleted file mode 100644 index adb2ccf93a8e..000000000000 --- a/src/QmlControls/MissionSettingsEditor.qml +++ /dev/null @@ -1,368 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import QGroundControl -import QGroundControl.Controls -import QGroundControl.FactControls - -// Editor for Mission Settings -Rectangle { - id: root - width: availableWidth - height: valuesColumn.height + (_margin * 2) - color: qgcPal.windowShadeDark - radius: ScreenTools.defaultBorderRadius - - property var _masterController: masterController - property var _missionController: _masterController.missionController - property var _controllerVehicle: _masterController.controllerVehicle - property var _visualItems: _missionController.visualItems - property bool _vehicleHasHomePosition: _controllerVehicle.homePosition.isValid - property bool _showCruiseSpeed: !_controllerVehicle.multiRotor - property bool _showHoverSpeed: _controllerVehicle.multiRotor || _controllerVehicle.vtol - property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport - property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport - property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16 - property bool _mobile: ScreenTools.isMobile - property var _savePath: QGroundControl.settingsManager.appSettings.missionSavePath - property var _appSettings: QGroundControl.settingsManager.appSettings - property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly - property bool _showCameraSection: _waypointsOnlyMode || QGroundControl.corePlugin.showAdvancedUI - property bool _simpleMissionStart: QGroundControl.corePlugin.options.showSimpleMissionStart - property bool _showFlightSpeed: !_controllerVehicle.vtol && !_simpleMissionStart && !_controllerVehicle.apmFirmware - property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle - - readonly property string _firmwareLabel: qsTr("Firmware") - readonly property string _vehicleLabel: qsTr("Vehicle") - readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 - - QGCPalette { id: qgcPal } - Component { id: altModeDialogComponent; AltModeDialog { } } - - QGCPopupDialogFactory { - id: altModeDialogFactory - - dialogComponent: altModeDialogComponent - } - - Connections { - target: _controllerVehicle - function onSupportsTerrainFrameChanged() { - if (!_controllerVehicle.supportsTerrainFrame && _missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) { - _missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain - } - } - } - - ColumnLayout { - id: valuesColumn - anchors.margins: _margin - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - spacing: ScreenTools.defaultFontPixelHeight / 2 - - LabelledButton { - Layout.fillWidth: true - label: qsTr("Altitude Mode") - buttonText: QGroundControl.altitudeModeShortDescription(_missionController.globalAltitudeMode) - - onClicked: { - var removeModes = [] - var updateFunction = function(altMode){ _missionController.globalAltitudeMode = altMode } - if (!_controllerVehicle.supportsTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - if (!_noMissionItemsAdded) { - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) { - removeModes.push(QGroundControl.AltitudeModeRelative) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) { - removeModes.push(QGroundControl.AltitudeModeAbsolute) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) { - removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain) - } - if (_missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) { - removeModes.push(QGroundControl.AltitudeModeTerrainFrame) - } - } - altModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction }) - } - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Waypoints Altitude") - fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude - } - - QGCButton { - id: applyDefaultAltitudeButton - Layout.fillWidth: true - text: qsTr("Apply New Altitude") - visible: false - - onClicked: QGroundControl.showMessageDialog(root, qsTr("Apply New Altitude"), - qsTr("You have changed the default altitude for mission items. Would you like to apply that altitude to all the items in the current mission?"), - Dialog.Yes | Dialog.No, - function() { applyDefaultAltitudeButton.visible = false; _missionController.applyDefaultMissionAltitude() }) - - Connections { - target: _appSettings.defaultMissionItemAltitude - function onRawValueChanged() { - if (_visualItems.count > 1) { - applyDefaultAltitudeButton.visible = true - } - } - } - - Connections { - target: _visualItems - function onCountChanged() { - if (_visualItems.count <= 1) { - applyDefaultAltitudeButton.visible = false - } - } - } - } - - FactTextFieldSlider { - Layout.fillWidth: true - label: qsTr("Flight Speed") - fact: missionItem.speedSection.flightSpeed - showEnableCheckbox: true - enableCheckBoxChecked: missionItem.speedSection.specifyFlightSpeed - visible: missionItem.speedSection.available - - onEnableCheckboxClicked: missionItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked - } - - /* - Removed for now. May come back... - SectionHeader { - id: createFromTemplateSection - Layout.fillWidth: true - text: qsTr("Create Plan From Template") - checked: true - visible: !_masterController.containsItems && !_masterController.manualCreation - } - - GridLayout { - Layout.fillWidth: true - columns: 2 - columnSpacing: ScreenTools.defaultFontPixelWidth * 0.75 - rowSpacing: columnSpacing - visible: createFromTemplateSection.visible && createFromTemplateSection.checked - - Repeater { - model: _masterController.planCreators - - Rectangle { - id: planCreatorButton - Layout.fillWidth: true - implicitHeight: planCreatorLayout.implicitHeight - color: planCreatorButtonMouseArea.pressed || planCreatorButtonMouseArea.containsMouse ? qgcPal.buttonHighlight : qgcPal.button - - ColumnLayout { - id: planCreatorLayout - width: parent.width - spacing: 0 - - Image { - id: planCreatorImage - Layout.fillWidth: true - source: object.imageResource - sourceSize.width: width - fillMode: Image.PreserveAspectFit - mipmap: true - } - - QGCLabel { - id: planCreatorNameLabel - Layout.fillWidth: true - Layout.maximumWidth: parent.width - horizontalAlignment: Text.AlignHCenter - text: object.name - color: planCreatorButtonMouseArea.pressed || planCreatorButtonMouseArea.containsMouse ? qgcPal.buttonHighlightText : qgcPal.buttonText - } - } - - QGCMouseArea { - id: planCreatorButtonMouseArea - anchors.fill: parent - hoverEnabled: true - preventStealing: true - onClicked: { - if (object.blankPlan) { - _masterController.manualCreation = true - } else { - object.createPlan(_mapCenter()) - } - } - - function _mapCenter() { - var centerPoint = Qt.point(editorMap.centerViewport.left + (editorMap.centerViewport.width / 2), editorMap.centerViewport.top + (editorMap.centerViewport.height / 2)) - return editorMap.toCoordinate(centerPoint, false) - } - } - } - } - } - */ - - Column { - Layout.fillWidth: true - spacing: _margin - visible: !_simpleMissionStart - - CameraSection { - id: cameraSection - anchors.left: parent.left - anchors.right: parent.right - visible: _showCameraSection - - Component.onCompleted: checked = !_waypointsOnlyMode && missionItem.cameraSection.settingsSpecified - } - - QGCLabel { - anchors.left: parent.left - anchors.right: parent.right - text: qsTr("Above camera commands will take affect immediately upon mission start.") - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - font.pointSize: ScreenTools.smallFontPointSize - visible: _showCameraSection && cameraSection.checked - } - - SectionHeader { - id: vehicleInfoSectionHeader - anchors.left: parent.left - anchors.right: parent.right - text: qsTr("Vehicle Info") - visible: !_waypointsOnlyMode - checked: false - } - - GridLayout { - anchors.left: parent.left - anchors.right: parent.right - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked - - QGCLabel { - text: _firmwareLabel - Layout.fillWidth: true - visible: _multipleFirmware - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass - indexModel: false - Layout.preferredWidth: _fieldWidth - visible: _multipleFirmware && _allowFWVehicleTypeSelection - } - QGCLabel { - text: _controllerVehicle.firmwareTypeString - visible: _multipleFirmware && !_allowFWVehicleTypeSelection - } - - QGCLabel { - text: _vehicleLabel - Layout.fillWidth: true - visible: _multipleVehicleTypes - } - FactComboBox { - fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass - indexModel: false - Layout.preferredWidth: _fieldWidth - visible: _multipleVehicleTypes && _allowFWVehicleTypeSelection - } - QGCLabel { - text: _controllerVehicle.vehicleTypeString - visible: _multipleVehicleTypes && !_allowFWVehicleTypeSelection - } - - QGCLabel { - Layout.columnSpan: 2 - Layout.alignment: Qt.AlignHCenter - Layout.fillWidth: true - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.") - visible: _showCruiseSpeed || _showHoverSpeed - } - - QGCLabel { - text: qsTr("Cruise speed") - visible: _showCruiseSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed - visible: _showCruiseSpeed - Layout.preferredWidth: _fieldWidth - } - - QGCLabel { - text: qsTr("Hover speed") - visible: _showHoverSpeed - Layout.fillWidth: true - } - FactTextField { - fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed - visible: _showHoverSpeed - Layout.preferredWidth: _fieldWidth - } - } // GridLayout - - SectionHeader { - id: plannedHomePositionSection - anchors.left: parent.left - anchors.right: parent.right - text: qsTr("Launch Position") - visible: !_vehicleHasHomePosition - checked: false - } - - Column { - anchors.left: parent.left - anchors.right: parent.right - spacing: _margin - visible: plannedHomePositionSection.checked && !_vehicleHasHomePosition - - GridLayout { - anchors.left: parent.left - anchors.right: parent.right - columnSpacing: ScreenTools.defaultFontPixelWidth - rowSpacing: columnSpacing - columns: 2 - - QGCLabel { - text: qsTr("Altitude") - } - FactTextField { - fact: missionItem.plannedHomePositionAltitude - Layout.fillWidth: true - } - } - - QGCLabel { - width: parent.width - wrapMode: Text.WordWrap - font.pointSize: ScreenTools.smallFontPointSize - text: qsTr("Actual position set by vehicle at flight time.") - horizontalAlignment: Text.AlignHCenter - } - - QGCButton { - text: qsTr("Set To Map Center") - onClicked: missionItem.coordinate = map.center - anchors.horizontalCenter: parent.horizontalCenter - } - } - } // Column - } // Column -} // Rectangle diff --git a/src/QmlControls/ObjectItemModelBase.cc b/src/QmlControls/ObjectItemModelBase.cc new file mode 100644 index 000000000000..ad2a9d47e34d --- /dev/null +++ b/src/QmlControls/ObjectItemModelBase.cc @@ -0,0 +1,74 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#include "ObjectItemModelBase.h" +#include "QGCLoggingCategory.h" + +#include + +QGC_LOGGING_CATEGORY(ObjectItemModelBaseLog, "API.ObjectItemModelBase") + +ObjectItemModelBase::ObjectItemModelBase(QObject* parent) + : QAbstractItemModel(parent) +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); +} + +ObjectItemModelBase::~ObjectItemModelBase() +{ + if (_resetModelNestingCount > 0) { + qCWarning(ObjectItemModelBaseLog) << "Destroyed with unbalanced nesting of begin/endResetModel calls - _resetModelNestingCount:" << _resetModelNestingCount << this; + } +} + +QHash ObjectItemModelBase::roleNames() const +{ + return { + { ObjectRole, "object" }, + { TextRole, "text" }, + }; +} + +void ObjectItemModelBase::_childDirtyChanged(bool dirty) +{ + _dirty |= dirty; + emit dirtyChanged(_dirty); +} + +void ObjectItemModelBase::beginResetModel() +{ + if (_resetModelNestingCount == 0) { + qCDebug(ObjectItemModelBaseLog) << "First beginResetModel - calling QAbstractItemModel::beginResetModel" << this; + QAbstractItemModel::beginResetModel(); + } + _resetModelNestingCount++; + qCDebug(ObjectItemModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; +} + +void ObjectItemModelBase::endResetModel() +{ + if (_resetModelNestingCount == 0) { + qCWarning(ObjectItemModelBaseLog) << "endResetModel called without prior beginResetModel"; + return; + } + _resetModelNestingCount--; + qCDebug(ObjectItemModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; + if (_resetModelNestingCount == 0) { + qCDebug(ObjectItemModelBaseLog) << "Last endResetModel - calling QAbstractItemModel::endResetModel" << this; + QAbstractItemModel::endResetModel(); + emit countChanged(count()); + } +} + +void ObjectItemModelBase::_signalCountChangedIfNotNested() +{ + if (_resetModelNestingCount == 0) { + emit countChanged(count()); + } +} diff --git a/src/QmlControls/ObjectItemModelBase.h b/src/QmlControls/ObjectItemModelBase.h new file mode 100644 index 000000000000..40f5310f0cc8 --- /dev/null +++ b/src/QmlControls/ObjectItemModelBase.h @@ -0,0 +1,60 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#pragma once + +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(ObjectItemModelBaseLog) + +/// Common base for QObject*-based item models (flat lists and trees). +/// Provides: dirty tracking, depth-counted begin/endResetModel, shared role constants, roleNames. +class ObjectItemModelBase : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit ObjectItemModelBase(QObject* parent = nullptr); + ~ObjectItemModelBase() override; + + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + + bool dirty() const { return _dirty; } + + /// Depth-counted beginResetModel — only the outermost call has effect. + void beginResetModel(); + + /// Depth-counted endResetModel — only the outermost call has effect. + void endResetModel(); + + virtual int count() const = 0; + virtual bool isEmpty() const { return (count() == 0); } + virtual void setDirty(bool dirty) = 0; + virtual void clear() = 0; + +signals: + void countChanged(int count); + void dirtyChanged(bool dirty); + +protected slots: + void _childDirtyChanged(bool dirty); + +protected: + QHash roleNames() const override; + void _signalCountChangedIfNotNested(); + + bool _dirty = false; + uint _resetModelNestingCount = 0; + + static constexpr int ObjectRole = Qt::UserRole; + static constexpr int TextRole = Qt::UserRole + 1; +}; diff --git a/src/QmlControls/ObjectListModelBase.cc b/src/QmlControls/ObjectListModelBase.cc index 9e184344fb67..f38f4a0ecdd5 100644 --- a/src/QmlControls/ObjectListModelBase.cc +++ b/src/QmlControls/ObjectListModelBase.cc @@ -10,72 +10,41 @@ #include "ObjectListModelBase.h" #include "QGCLoggingCategory.h" -#include -#include - QGC_LOGGING_CATEGORY(ObjectListModelBaseLog, "API.ObjectListModelBase") ObjectListModelBase::ObjectListModelBase(QObject* parent) - : QAbstractListModel (parent) - , _dirty (false) - , _skipDirtyFirstItem (false) + : ObjectItemModelBase(parent) { - QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); } ObjectListModelBase::~ObjectListModelBase() { - if (_resetModelNestingCount > 0) { - qCWarning(ObjectListModelBaseLog) << "Destroyed with unbalanced nesting of begin/endResetModel calls - _resetModelNestingCount:" << _resetModelNestingCount << this; - } } -QHash ObjectListModelBase::roleNames(void) const -{ - QHash hash; - - hash[ObjectRole] = "object"; - hash[TextRole] = "text"; - - return hash; -} +// Flat-list overrides — same semantics as QAbstractListModel -void ObjectListModelBase::_childDirtyChanged(bool dirty) +QModelIndex ObjectListModelBase::index(int row, int column, const QModelIndex& parent) const { - _dirty |= dirty; - // We want to emit dirtyChanged even if the actual value of _dirty didn't change. It can be a useful - // signal to know when a child has changed dirty state - emit dirtyChanged(_dirty); + if (parent.isValid() || column != 0 || row < 0 || row >= rowCount()) { + return {}; + } + return createIndex(row, 0); } -void ObjectListModelBase::beginResetModel() +QModelIndex ObjectListModelBase::parent(const QModelIndex& child) const { - if (_resetModelNestingCount == 0) { - qCDebug(ObjectListModelBaseLog) << "First call to begiResetModel - calling QAbstractListModel::beginResetModel" << this; - QAbstractListModel::beginResetModel(); - } - _resetModelNestingCount++; - qCDebug(ObjectListModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; + Q_UNUSED(child); + return {}; } -void ObjectListModelBase::endResetModel() +int ObjectListModelBase::columnCount(const QModelIndex& parent) const { - if (_resetModelNestingCount == 0) { - qCWarning(ObjectListModelBaseLog) << "Internal Error: endResetModel called without prior beginResetModel"; - return; - } - _resetModelNestingCount--; - qCDebug(ObjectListModelBaseLog) << "Reset model nesting count" << _resetModelNestingCount << this; - if (_resetModelNestingCount == 0) { - qCDebug(ObjectListModelBaseLog) << "Last call to endResetModel - calling QAbstractListModel::endResetModel" << this; - QAbstractListModel::endResetModel(); - emit countChanged(count()); - } + Q_UNUSED(parent); + return 1; } -void ObjectListModelBase::_signalCountChangedIfNotNested() +bool ObjectListModelBase::hasChildren(const QModelIndex& parent) const { - if (_resetModelNestingCount == 0) { - emit countChanged(count()); - } + // Only the root (invalid parent) has children in a flat list + return !parent.isValid() && rowCount() > 0; } diff --git a/src/QmlControls/ObjectListModelBase.h b/src/QmlControls/ObjectListModelBase.h index 34e4ee9c7a0d..320c0e642e36 100644 --- a/src/QmlControls/ObjectListModelBase.h +++ b/src/QmlControls/ObjectListModelBase.h @@ -10,61 +10,37 @@ #pragma once -#include -#include -#include +#include "ObjectItemModelBase.h" Q_DECLARE_LOGGING_CATEGORY(ObjectListModelBaseLog) -/// Base class for custom object list models: QmlObjectListModel, SparselObjectListModel -class ObjectListModelBase : public QAbstractListModel +/// Base class for flat QObject* list models. Inherits common dirty/reset/role +/// handling from ObjectItemModelBase and adds flat-list index()/parent() overrides. +class ObjectListModelBase : public ObjectItemModelBase { Q_OBJECT public: ObjectListModelBase(QObject* parent = nullptr); - ~ObjectListModelBase(); + ~ObjectListModelBase() override; - Q_PROPERTY(int count READ count NOTIFY countChanged) - - /// Returns true if any of the items in the list are dirty. Requires each object to have - /// a dirty property and dirtyChanged signal. - Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) - - bool dirty() const { return _dirty; } - void beginResetModel(); ///< Supported nesting of calls such that only outermost call has effect - void endResetModel(); ///< Supported nesting of calls such that only outermost call has effect - - virtual int count() const = 0; - virtual bool isEmpty() const { return (count() == 0); } - virtual void setDirty(bool dirty) = 0; - virtual void clear() = 0; virtual void clearAndDeleteContents() = 0; ///< Clears the list and calls deleteLater on each entry virtual QObject* removeOne(const QObject* object) = 0; virtual bool contains(const QObject* object) = 0; -signals: - void countChanged(int count); - void dirtyChanged(bool dirtyChanged); - -protected slots: - void _childDirtyChanged(bool dirty); + // Flat-list overrides of QAbstractItemModel — same behavior as QAbstractListModel + QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex& parent = QModelIndex()) const override; protected: - // Overrides from QAbstractListModel which must be implemented by derived classes - int rowCount(const QModelIndex & parent = QModelIndex()) const override = 0; + // Overrides from QAbstractItemModel which must be implemented by derived classes + int rowCount(const QModelIndex& parent = QModelIndex()) const override = 0; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override = 0; bool insertRows(int position, int rows, const QModelIndex& index = QModelIndex()) override = 0; bool removeRows(int position, int rows, const QModelIndex& index = QModelIndex()) override = 0; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override = 0; - void _signalCountChangedIfNotNested(); - - QHash roleNames(void) const override; - bool _dirty; - bool _skipDirtyFirstItem; - uint _resetModelNestingCount = 0; - - static constexpr int ObjectRole = Qt::UserRole; - static constexpr int TextRole = Qt::UserRole + 1; + bool _skipDirtyFirstItem = false; }; diff --git a/src/QmlControls/PIDTuning.qml b/src/QmlControls/PIDTuning.qml index 3f186ce1427f..bbb4091926bb 100644 --- a/src/QmlControls/PIDTuning.qml +++ b/src/QmlControls/PIDTuning.qml @@ -333,13 +333,16 @@ RowLayout { model: axis Repeater { + id: paramRepeater model: axis[index].params + property int axisIndex: index + SettingsGroupLayout { id: tuningGroup heading: title headingDescription: description - visible: _currentAxis === index + visible: _currentAxis === paramRepeater.axisIndex Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 40 FactSlider { diff --git a/src/QmlControls/PlanViewRightPanel.qml b/src/QmlControls/PlanViewRightPanel.qml deleted file mode 100644 index 0866393a361a..000000000000 --- a/src/QmlControls/PlanViewRightPanel.qml +++ /dev/null @@ -1,210 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import QGroundControl -import QGroundControl.Controls - -Item { - required property var editorMap - required property var planMasterController - - id: root - - // These must match the indices of _editingToolComponents - readonly property int _editingToolMission: 0 - readonly property int _editingToolFence: 1 - readonly property int _editingToolRally: 2 - - property int _editingTool: _editingToolMission - property var _missionController: planMasterController.missionController - property var _geoFenceController: planMasterController.geoFenceController - property var _rallyPointController: planMasterController.rallyPointController - property var _visualItems: _missionController.visualItems - property var _editingToolComponents: [ missionToolComponent, fenceToolComponent, rallyToolComponent ] - property real _toolsMargin: ScreenTools.defaultFontPixelWidth * 0.75 - - function selectNextNotReady() { - var foundCurrent = false - for (var i=0; i<_missionController.visualItems.count; i++) { - var vmi = _missionController.visualItems.get(i) - if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) { - _missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true) - break - } - } - } - - QGCPalette { id: qgcPal } - - Rectangle { - id: rightPanelBackground - anchors.fill: parent - color: qgcPal.window - opacity: 0.85 - } - - - // Open/Close panel - Item { - id: panelOpenCloseButton - anchors.right: parent.left - anchors.verticalCenter: parent.verticalCenter - width: toggleButtonRect.width - toggleButtonRect.radius - height: toggleButtonRect.height - clip: true - - property bool _expanded: root.anchors.right == root.parent.right - - Rectangle { - id: toggleButtonRect - width: ScreenTools.defaultFontPixelWidth * 2.25 - height: width * 3 - radius: ScreenTools.defaultBorderRadius - color: rightPanelBackground.color - opacity: rightPanelBackground.opacity - - QGCLabel { - id: toggleButtonLabel - anchors.centerIn: parent - text: panelOpenCloseButton._expanded ? ">" : "<" - color: qgcPal.buttonText - } - - } - - QGCMouseArea { - anchors.fill: parent - - onClicked: { - if (panelOpenCloseButton._expanded) { - // Close panel - root.anchors.right = undefined - root.anchors.left = root.parent.right - } else { - // Open panel - root.anchors.left = undefined - root.anchors.right = root.parent.right - } - } - } - } - - //------------------------------------------------------- - // Right Panel Controls - Item { - anchors.fill: rightPanelBackground - - DeadMouseArea { - anchors.fill: parent - } - - ColumnLayout { - anchors.fill: parent - spacing: ScreenTools.defaultFontPixelHeight * 0.5 - - QGCTabBar { - Layout.fillWidth: true - - QGCTabButton { - text: qsTr("Mission") - onClicked: { root._editingTool = root._editingToolMission; _editingLayer = _layerMission } - } - - QGCTabButton { - text: qsTr("Fence") - onClicked: { root._editingTool = root._editingToolFence; _editingLayer = _layerFence } - } - - QGCTabButton { - text: qsTr("Rally") - onClicked: { root._editingTool = root._editingToolRally; _editingLayer = _layerRally } - } - } - - Loader { - id: editingToolLoader - Layout.fillWidth: true - Layout.fillHeight: true - sourceComponent: root._editingToolComponents[root._editingTool] - } - } - - Component { - id: missionToolComponent - - ColumnLayout { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - QGCListView { - id: missionItemEditorListView - Layout.fillWidth: true - Layout.fillHeight: true - spacing: ScreenTools.defaultFontPixelHeight / 4 - orientation: ListView.Vertical - model: _missionController.visualItems - cacheBuffer: Math.max(height * 2, 0) - clip: true - currentIndex: _missionController.currentPlanViewSeqNum - highlightMoveDuration: 250 - - delegate: MissionItemEditor { - map: editorMap - masterController: planMasterController - missionItem: object - width: missionItemEditorListView.width - readOnly: false - - onClicked: _missionController.setCurrentPlanViewSeqNum(object.sequenceNumber, false) - - onRemove: { - var removeVIIndex = index - _missionController.removeVisualItem(removeVIIndex) - if (removeVIIndex >= _missionController.visualItems.count) { - removeVIIndex-- - } - } - - onSelectNextNotReadyItem: selectNextNotReady() - } - } - } - } - - Component { - id: fenceToolComponent - - Column { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - GeoFenceEditor { - width: parent.width - myGeoFenceController: root._geoFenceController - flightMap: root.editorMap - } - } - } - - - Component { - id: rallyToolComponent - - Column { - spacing: ScreenTools.defaultFontPixelHeight / 2 - - RallyPointEditorHeader { - width: parent.width - controller: root._rallyPointController - } - - RallyPointItemEditor { - width: parent.width - visible: root._rallyPointController.points.count - rallyPoint: root._rallyPointController.currentRallyPoint - controller: root._rallyPointController - } - } - } - - } -} diff --git a/src/QmlControls/QGCMapPolygon.cc b/src/QmlControls/QGCMapPolygon.cc index 8b0c9b49fda7..f309f62390dc 100644 --- a/src/QmlControls/QGCMapPolygon.cc +++ b/src/QmlControls/QGCMapPolygon.cc @@ -4,12 +4,15 @@ #include "JsonParsing.h" #include "QGCQGeoCoordinate.h" #include "QGCApplication.h" +#include "QGCLoggingCategory.h" #include "ShapeFileHelper.h" #include "KMLDomDocument.h" #include #include +QGC_LOGGING_CATEGORY(QGCMapPolygonLog, "QMLControls.QGCMapPolygon") + QGCMapPolygon::QGCMapPolygon(QObject* parent) : QObject (parent) , _dirty (false) @@ -74,7 +77,11 @@ void QGCMapPolygon::clear(void) while (_polygonPath.count() > 1) { _polygonPath.takeLast(); } - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } // Although this code should remove the polygon from the map it doesn't. There appears // to be a bug in QGCMapPolygon which causes it to not be redrawn if the list is empty. So @@ -94,14 +101,20 @@ void QGCMapPolygon::adjustVertex(int vertexIndex, const QGeoCoordinate coordinat _polygonPath[vertexIndex] = QVariant::fromValue(coordinate); _polygonModel.value(vertexIndex)->setCoordinate(coordinate); if (!_centerDrag) { - // When dragging center we don't signal path changed until all vertices are updated if (!_deferredPathChanged) { - // Only update the path once per event loop, to prevent lag-spikes _deferredPathChanged = true; - QTimer::singleShot(0, this, [this]() { - emit pathChanged(); - _deferredPathChanged = false; - }); + if (_vertexDrag) { + // During vertex drag only emit the lightweight visual signal + QTimer::singleShot(0, this, [this]() { + emit dragPathChanged(); + _deferredPathChanged = false; + }); + } else { + QTimer::singleShot(0, this, [this]() { + emit pathChanged(); + _deferredPathChanged = false; + }); + } } } setDirty(true); @@ -272,7 +285,11 @@ void QGCMapPolygon::appendVertex(const QGeoCoordinate& coordinate) // Only update the path once per event loop, to prevent lag-spikes _deferredPathChanged = true; QTimer::singleShot(0, this, [this]() { - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } _deferredPathChanged = false; }); } @@ -290,7 +307,11 @@ void QGCMapPolygon::appendVertices(const QList& coordinates) _polygonModel.append(objects); endReset(); - emit pathChanged(); + if (_vertexDrag) { + emit dragPathChanged(); + } else { + emit pathChanged(); + } } void QGCMapPolygon::appendVertices(const QVariantList& varCoords) @@ -311,8 +332,8 @@ void QGCMapPolygon::_polygonModelDirtyChanged(bool dirty) void QGCMapPolygon::removeVertex(int vertexIndex) { - if (vertexIndex < 0 && vertexIndex > _polygonPath.length() - 1) { - qWarning() << "Call to removePolygonCoordinate with bad vertexIndex:count" << vertexIndex << _polygonPath.length(); + if (vertexIndex < 0 || vertexIndex >= _polygonPath.length()) { + qCWarning(QGCMapPolygonLog) << "Call to removePolygonCoordinate with bad vertexIndex:count" << vertexIndex << _polygonPath.length(); return; } @@ -344,12 +365,35 @@ void QGCMapPolygon::_updateCenter(void) QGeoCoordinate center; if (_polygonPath.count() > 2) { - QPointF centroid(0, 0); QPolygonF polygonF = _toPolygonF(); - for (int i=0; i= 0 && vertex < _polygonPath.count()) { return _polygonPath[vertex].value(); } else { - qWarning() << "QGCMapPolygon::vertexCoordinate bad vertex requested:count" << vertex << _polygonPath.count(); + qCWarning(QGCMapPolygonLog) << "QGCMapPolygon::vertexCoordinate bad vertex requested:count" << vertex << _polygonPath.count(); return QGeoCoordinate(); } } @@ -487,7 +551,7 @@ void QGCMapPolygon::offset(double distance) auto intersect = rgOffsetEdges[prevIndex].intersects(rgOffsetEdges[i], &newVertex); if (intersect == QLineF::NoIntersection) { // FIXME: Better error handling? - qWarning("Intersection failed"); + qCWarning(QGCMapPolygonLog, "Intersection failed"); return; } QGeoCoordinate coord; @@ -649,10 +713,8 @@ void QGCMapPolygon::selectVertex(int index) if(-1 <= index && index < count()) { _selectedVertexIndex = index; } else { - if (!qgcApp()->runningUnitTests()) { - qWarning() << QString("QGCMapPolygon: Selected vertex index (%1) is out of bounds! " - "Polygon vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); - } + qCWarning(QGCMapPolygonLog) << QString("QGCMapPolygon: Selected vertex index (%1) is out of bounds! " + "Polygon vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); _selectedVertexIndex = -1; // deselect vertex } diff --git a/src/QmlControls/QGCMapPolygon.h b/src/QmlControls/QGCMapPolygon.h index 2037222b4ccf..7a3c58de832f 100644 --- a/src/QmlControls/QGCMapPolygon.h +++ b/src/QmlControls/QGCMapPolygon.h @@ -27,11 +27,14 @@ class QGCMapPolygon : public QObject Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged) + Q_PROPERTY(QVariantList dragPath READ path NOTIFY dragPathChanged) Q_PROPERTY(double area READ area NOTIFY pathChanged) Q_PROPERTY(QmlObjectListModel* pathModel READ qmlPathModel CONSTANT) Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) Q_PROPERTY(QGeoCoordinate center READ center WRITE setCenter NOTIFY centerChanged) + Q_PROPERTY(QGeoCoordinate dragCenter READ center NOTIFY dragCenterChanged) Q_PROPERTY(bool centerDrag READ centerDrag WRITE setCenterDrag NOTIFY centerDragChanged) + Q_PROPERTY(bool vertexDrag READ vertexDrag WRITE setVertexDrag NOTIFY vertexDragChanged) Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) Q_PROPERTY(bool empty READ empty NOTIFY isEmptyChanged) @@ -102,6 +105,7 @@ class QGCMapPolygon : public QObject void setDirty (bool dirty); QGeoCoordinate center (void) const { return _center; } bool centerDrag (void) const { return _centerDrag; } + bool vertexDrag (void) const { return _vertexDrag; } bool interactive (void) const { return _interactive; } bool isValid (void) const { return _polygonModel.count() >= 3; } bool empty (void) const { return _polygonModel.count() == 0; } @@ -117,6 +121,7 @@ class QGCMapPolygon : public QObject void setPath (const QVariantList& path); void setCenter (QGeoCoordinate newCenter); void setCenterDrag (bool centerDrag); + void setVertexDrag (bool vertexDrag); void setInteractive (bool interactive); void setTraceMode (bool traceMode); void setShowAltColor(bool showAltColor); @@ -127,10 +132,13 @@ class QGCMapPolygon : public QObject signals: void countChanged (int count); void pathChanged (void); + void dragPathChanged (void); void dirtyChanged (bool dirty); void cleared (void); void centerChanged (QGeoCoordinate center); + void dragCenterChanged (QGeoCoordinate center); void centerDragChanged (bool centerDrag); + void vertexDragChanged (bool vertexDrag); void interactiveChanged (bool interactive); bool isValidChanged (void); bool isEmptyChanged (void); @@ -154,6 +162,7 @@ private slots: bool _dirty = false; QGeoCoordinate _center; bool _centerDrag = false; + bool _vertexDrag = false; bool _ignoreCenterUpdates = false; bool _interactive = false; bool _traceMode = false; diff --git a/src/QmlControls/QGCMapPolyline.cc b/src/QmlControls/QGCMapPolyline.cc index ea1c632be9e4..1d3ee9b0a352 100644 --- a/src/QmlControls/QGCMapPolyline.cc +++ b/src/QmlControls/QGCMapPolyline.cc @@ -4,12 +4,14 @@ #include "JsonParsing.h" #include "QGCQGeoCoordinate.h" #include "QGCApplication.h" -#include "ShapeFileHelper.h" #include "QGCLoggingCategory.h" +#include "ShapeFileHelper.h" #include #include +QGC_LOGGING_CATEGORY(QGCMapPolylineLog, "QMLControls.QGCMapPolyline") + QGCMapPolyline::QGCMapPolyline(QObject* parent) : QObject (parent) , _dirty (false) @@ -77,10 +79,18 @@ void QGCMapPolyline::adjustVertex(int vertexIndex, const QGeoCoordinate coordina _polylineModel.value(vertexIndex)->setCoordinate(coordinate); if (!_deferredPathChanged) { _deferredPathChanged = true; - QTimer::singleShot(0, this, [this]() { - emit pathChanged(); - _deferredPathChanged = false; - }); + if (_vertexDrag) { + // During vertex drag only emit the lightweight visual signal + QTimer::singleShot(0, this, [this]() { + emit dragPathChanged(); + _deferredPathChanged = false; + }); + } else { + QTimer::singleShot(0, this, [this]() { + emit pathChanged(); + _deferredPathChanged = false; + }); + } } setDirty(true); } @@ -231,7 +241,7 @@ void QGCMapPolyline::appendVertex(const QGeoCoordinate& coordinate) void QGCMapPolyline::removeVertex(int vertexIndex) { if (vertexIndex < 0 || vertexIndex > _polylinePath.length() - 1) { - qWarning() << "Call to removeVertex with bad vertexIndex:count" << vertexIndex << _polylinePath.length(); + qCWarning(QGCMapPolylineLog) << "Call to removeVertex with bad vertexIndex:count" << vertexIndex << _polylinePath.length(); return; } @@ -260,12 +270,24 @@ void QGCMapPolyline::setInteractive(bool interactive) } } +void QGCMapPolyline::setVertexDrag(bool vertexDrag) +{ + if (_vertexDrag != vertexDrag) { + _vertexDrag = vertexDrag; + if (!vertexDrag) { + // Drag ended - signal path changed so downstream can recalculate + emit pathChanged(); + } + emit vertexDragChanged(vertexDrag); + } +} + QGeoCoordinate QGCMapPolyline::vertexCoordinate(int vertex) const { if (vertex >= 0 && vertex < _polylinePath.count()) { return _polylinePath[vertex].value(); } else { - qWarning() << "QGCMapPolyline::vertexCoordinate bad vertex requested"; + qCWarning(QGCMapPolylineLog) << "QGCMapPolyline::vertexCoordinate bad vertex requested"; return QGeoCoordinate(); } } @@ -439,10 +461,8 @@ void QGCMapPolyline::selectVertex(int index) if(-1 <= index && index < count()) { _selectedVertexIndex = index; } else { - if (!qgcApp()->runningUnitTests()) { - qWarning() << QStringLiteral("QGCMapPolyline: Selected vertex index (%1) is out of bounds! " - "Polyline vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); - } + qCWarning(QGCMapPolylineLog) << QStringLiteral("QGCMapPolyline: Selected vertex index (%1) is out of bounds! " + "Polyline vertices indexes range is [%2..%3].").arg(index).arg(0).arg(count()-1); _selectedVertexIndex = -1; // deselect vertex } diff --git a/src/QmlControls/QGCMapPolyline.h b/src/QmlControls/QGCMapPolyline.h index 0068a0f81447..9b1974f89a01 100644 --- a/src/QmlControls/QGCMapPolyline.h +++ b/src/QmlControls/QGCMapPolyline.h @@ -20,9 +20,11 @@ class QGCMapPolyline : public QObject Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged) + Q_PROPERTY(QVariantList dragPath READ path NOTIFY dragPathChanged) Q_PROPERTY(QmlObjectListModel* pathModel READ qmlPathModel CONSTANT) Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) Q_PROPERTY(bool interactive READ interactive WRITE setInteractive NOTIFY interactiveChanged) + Q_PROPERTY(bool vertexDrag READ vertexDrag WRITE setVertexDrag NOTIFY vertexDragChanged) Q_PROPERTY(bool isValid READ isValid NOTIFY isValidChanged) Q_PROPERTY(bool empty READ empty NOTIFY isEmptyChanged) Q_PROPERTY(bool traceMode READ traceMode WRITE setTraceMode NOTIFY traceModeChanged) @@ -80,6 +82,7 @@ class QGCMapPolyline : public QObject bool dirty (void) const { return _dirty; } void setDirty (bool dirty); bool interactive (void) const { return _interactive; } + bool vertexDrag (void) const { return _vertexDrag; } QVariantList path (void) const { return _polylinePath; } bool isValid (void) const { return _polylineModel.count() >= 2; } bool empty (void) const { return _polylineModel.count() == 0; } @@ -92,6 +95,7 @@ class QGCMapPolyline : public QObject void setPath (const QList& path); void setPath (const QVariantList& path); void setInteractive (bool interactive); + void setVertexDrag (bool vertexDrag); void setTraceMode (bool traceMode); void selectVertex (int index); @@ -100,9 +104,11 @@ class QGCMapPolyline : public QObject signals: void countChanged (int count); void pathChanged (void); + void dragPathChanged (void); void dirtyChanged (bool dirty); void cleared (void); void interactiveChanged (bool interactive); + void vertexDragChanged (bool vertexDrag); void isValidChanged (void); void isEmptyChanged (void); void traceModeChanged (bool traceMode); @@ -122,6 +128,7 @@ private slots: bool _deferredPathChanged = false; bool _dirty; bool _interactive; + bool _vertexDrag = false; bool _traceMode = false; int _selectedVertexIndex = -1; }; diff --git a/src/QmlControls/QGCPalette.cc b/src/QmlControls/QGCPalette.cc index af258e4001e3..5b5d339f2910 100644 --- a/src/QmlControls/QGCPalette.cc +++ b/src/QmlControls/QGCPalette.cc @@ -41,7 +41,6 @@ void QGCPalette::_buildMap() DECLARE_QGC_COLOR(windowShade, "#d9d9d9", "#d9d9d9", "#333333", "#333333") DECLARE_QGC_COLOR(windowShadeDark, "#bdbdbd", "#bdbdbd", "#282828", "#282828") DECLARE_QGC_COLOR(text, "#9d9d9d", "#333333", "#707070", "#ffffff") - DECLARE_QGC_COLOR(windowTransparentText,"#9d9d9d", "#000000", "#707070", "#ffffff") DECLARE_QGC_COLOR(warningText, "#cc0808", "#cc0808", "#f85761", "#f85761") DECLARE_QGC_COLOR(button, "#ffffff", "#ffffff", "#707070", "#626270") DECLARE_QGC_COLOR(buttonBorder, "#9d9d9d", "#3A9BDC", "#707070", "#adadb8") diff --git a/src/QmlControls/QGCPalette.h b/src/QmlControls/QGCPalette.h index fdd1dca00ca4..7c41c587b5ab 100644 --- a/src/QmlControls/QGCPalette.h +++ b/src/QmlControls/QGCPalette.h @@ -110,7 +110,6 @@ class QGCPalette : public QObject DEFINE_QGC_COLOR(windowShade, setWindowShade) DEFINE_QGC_COLOR(windowShadeDark, setWindowShadeDark) DEFINE_QGC_COLOR(text, setText) - DEFINE_QGC_COLOR(windowTransparentText, setWindowTransparentText) DEFINE_QGC_COLOR(warningText, setWarningText) DEFINE_QGC_COLOR(button, setButton) DEFINE_QGC_COLOR(buttonBorder, setButtonBorder) diff --git a/src/QmlControls/QGCTabBar.qml b/src/QmlControls/QGCTabBar.qml index 8c52b2ed34c6..e99d47844d8b 100644 --- a/src/QmlControls/QGCTabBar.qml +++ b/src/QmlControls/QGCTabBar.qml @@ -15,43 +15,75 @@ RowLayout { spacing: 0 property bool _preventCurrentIndexBindingLoop: false + property var _buttons: [] + + function _updateButtons() { + let btns = [] + for (var i = 0; i < control.children.length; i++) { + if (control.children[i].hasOwnProperty("checkable")) { + btns.push(control.children[i]) + } + } + _buttons = btns + buttonGroup.buttons = _buttons + _selectCurrentIndexButton() + } function _selectCurrentIndexButton() { - if (control.children.length > 0) { - _preventCurrentIndexBindingLoop = true - if (control.currentIndex == -1) { - // No tab selected, select none - for (var i = 0; i < control.children.length; i++) { - control.children[i].checked = false - } + if (_buttons.length === 0) return + + _preventCurrentIndexBindingLoop = true + + if (control.currentIndex === -1) { + for (var i = 0; i < _buttons.length; i++) { + _buttons[i].checked = false + } + } else { + let index = Math.min(Math.max(control.currentIndex, 0), _buttons.length - 1) + if (_buttons[index].visible) { + _buttons[index].checked = true } else { - let filteredCurrentIndex = Math.min(Math.max(control.currentIndex, 0), control.children.length - 1) - if (control.children[filteredCurrentIndex].visible) { - control.children[filteredCurrentIndex].checked = true - } else { - // We select the first visible tab if the current index is not visible - for (var j = 0; j < control.children.length; j++) { - if (control.children[j].visible) { - control.children[j].checked = true - break - } + // Select the first visible tab if the current index is not visible + index = -1 + for (var j = 0; j < _buttons.length; j++) { + if (_buttons[j].visible) { + _buttons[j].checked = true + index = j + break } } } - _preventCurrentIndexBindingLoop = false + // Sync currentIndex to the actually-selected button so it never remains stale + if (control.currentIndex !== index) { + control.currentIndex = index + } } + + _preventCurrentIndexBindingLoop = false } - Component.onCompleted: _selectCurrentIndexButton() + Component.onCompleted: _updateButtons() + onChildrenChanged: _updateButtons() onCurrentIndexChanged: _selectCurrentIndexButton() onVisibleChildrenChanged: _selectCurrentIndexButton() + onVisibleChanged: { + if (visible) { + // When becoming visible, ensure the current index is valid and a button is selected + currentIndex = 0 + _selectCurrentIndexButton() + } + } + + ButtonGroup { - buttons: control.children + id: buttonGroup onCheckedButtonChanged: { - for (var i = 0; i < control.children.length; i++) { - if (control.children[i] === checkedButton) { + if (control._preventCurrentIndexBindingLoop) return + + for (var i = 0; i < control._buttons.length; i++) { + if (control._buttons[i] === checkedButton) { control.currentIndex = i break } diff --git a/src/QmlControls/QGroundControlQmlGlobal.cc b/src/QmlControls/QGroundControlQmlGlobal.cc index ed2176cd7c76..6ff5e2896e34 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.cc +++ b/src/QmlControls/QGroundControlQmlGlobal.cc @@ -11,6 +11,8 @@ #include "PositionManager.h" #include "QGCMapEngineManager.h" #include "ADSBVehicleManager.h" +#include "NTRIPManager.h" +#include "MAVLinkSigningKeys.h" #include "MissionCommandTree.h" #include "VideoManager.h" #include "MultiVehicleManager.h" @@ -35,8 +37,10 @@ QGroundControlQmlGlobal::QGroundControlQmlGlobal(QObject *parent) : QObject(parent) , _mapEngineManager(QGCMapEngineManager::instance()) , _adsbVehicleManager(ADSBVehicleManager::instance()) + , _ntripManager(NTRIPManager::instance()) , _qgcPositionManager(QGCPositionManager::instance()) , _missionCommandTree(MissionCommandTree::instance()) + , _mavlinkSigningKeys(MAVLinkSigningKeys::instance()) , _videoManager(VideoManager::instance()) , _linkManager(LinkManager::instance()) , _multiVehicleManager(MultiVehicleManager::instance()) @@ -257,44 +261,42 @@ QString QGroundControlQmlGlobal::qgcVersion(void) return versionStr; } -QString QGroundControlQmlGlobal::altitudeModeExtraUnits(AltMode altMode) +QString QGroundControlQmlGlobal::altitudeFrameExtraUnits(AltitudeFrame altFrame) { - switch (altMode) { - case AltitudeModeNone: - return QString(); - case AltitudeModeRelative: - // Showing (Rel) all the time ends up being too noisy - return QString(); - case AltitudeModeAbsolute: - return tr("(AMSL)"); - case AltitudeModeCalcAboveTerrain: - return tr("(TerrC)"); - case AltitudeModeTerrainFrame: - return tr("(Terr)"); - case AltitudeModeMixed: - qWarning() << "Internal Error: QGroundControlQmlGlobal::altitudeModeExtraUnits called with altMode == AltitudeModeMixed"; + switch (altFrame) { + case AltitudeFrameNone: return QString(); + case AltitudeFrameRelative: + return tr("Rel"); + case AltitudeFrameAbsolute: + return tr("AMSL"); + case AltitudeFrameCalcAboveTerrain: + return tr("AGLC"); + case AltitudeFrameTerrain: + return tr("AGL"); + case AltitudeFrameMixed: + return tr("Mixed"); } // Should never get here but makes some compilers happy return QString(); } -QString QGroundControlQmlGlobal::altitudeModeShortDescription(AltMode altMode) +QString QGroundControlQmlGlobal::altitudeFrameShortDescription(AltitudeFrame altFrame) { - switch (altMode) { - case AltitudeModeNone: + switch (altFrame) { + case AltitudeFrameNone: return QString(); - case AltitudeModeRelative: - return tr("Relative"); - case AltitudeModeAbsolute: - return tr("Absolute"); - case AltitudeModeCalcAboveTerrain: - return tr("TerrainC"); - case AltitudeModeTerrainFrame: - return tr("Terrain"); - case AltitudeModeMixed: - return tr("Waypoint"); + case AltitudeFrameRelative: + return tr("Relative (%1)").arg(altitudeFrameExtraUnits(altFrame)); + case AltitudeFrameAbsolute: + return tr("Absolute (%1)").arg(altitudeFrameExtraUnits(altFrame)); + case AltitudeFrameCalcAboveTerrain: + return tr("Above Terrain Calced (%1)").arg(altitudeFrameExtraUnits(altFrame)); + case AltitudeFrameTerrain: + return tr("Above Terrain (%1)").arg(altitudeFrameExtraUnits(altFrame)); + case AltitudeFrameMixed: + return tr("Mixed"); } // Should never get here but makes some compilers happy diff --git a/src/QmlControls/QGroundControlQmlGlobal.h b/src/QmlControls/QGroundControlQmlGlobal.h index 5464cc04467a..efaa0b70904b 100644 --- a/src/QmlControls/QGroundControlQmlGlobal.h +++ b/src/QmlControls/QGroundControlQmlGlobal.h @@ -15,10 +15,12 @@ Q_DECLARE_LOGGING_CATEGORY(GuidedActionsControllerLog) class ADSBVehicleManager; class FactGroup; class LinkManager; +class MAVLinkSigningKeys; class MissionCommandTree; class MultiVehicleManager; class QGCCorePlugin; class QGCMapEngineManager; +class NTRIPManager; class QGCPalette; class QGCPositionManager; class SettingsManager; @@ -26,8 +28,10 @@ class VideoManager; class QmlObjectListModel; Q_MOC_INCLUDE("ADSBVehicleManager.h") +Q_MOC_INCLUDE("NTRIPManager.h") Q_MOC_INCLUDE("FactGroup.h") Q_MOC_INCLUDE("LinkManager.h") +Q_MOC_INCLUDE("MAVLinkSigningKeys.h") Q_MOC_INCLUDE("MissionCommandTree.h") Q_MOC_INCLUDE("MultiVehicleManager.h") Q_MOC_INCLUDE("QGCCorePlugin.h") @@ -49,15 +53,15 @@ class QGroundControlQmlGlobal : public QObject explicit QGroundControlQmlGlobal(QObject *parent = nullptr); ~QGroundControlQmlGlobal(); - enum AltMode { - AltitudeModeMixed, // Used by global altitude mode for mission planning - AltitudeModeRelative, // MAV_FRAME_GLOBAL_RELATIVE_ALT - AltitudeModeAbsolute, // MAV_FRAME_GLOBAL - AltitudeModeCalcAboveTerrain, // Absolute altitude above terrain calculated from terrain data - AltitudeModeTerrainFrame, // MAV_FRAME_GLOBAL_TERRAIN_ALT - AltitudeModeNone, // Being used as distance value unrelated to ground (for example distance to structure) + enum AltitudeFrame { + AltitudeFrameMixed, // Used by global altitude frame for mission planning + AltitudeFrameRelative, // MAV_FRAME_GLOBAL_RELATIVE_ALT + AltitudeFrameAbsolute, // MAV_FRAME_GLOBAL + AltitudeFrameCalcAboveTerrain, // Absolute altitude above terrain calculated from terrain data + AltitudeFrameTerrain, // MAV_FRAME_GLOBAL_TERRAIN_ALT + AltitudeFrameNone, // Being used as distance value unrelated to ground (for example distance to structure) }; - Q_ENUM(AltMode) + Q_ENUM(AltitudeFrame) Q_PROPERTY(QString appName READ appName CONSTANT) Q_PROPERTY(LinkManager* linkManager READ linkManager CONSTANT) @@ -67,8 +71,10 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(VideoManager* videoManager READ videoManager CONSTANT) Q_PROPERTY(SettingsManager* settingsManager READ settingsManager CONSTANT) Q_PROPERTY(ADSBVehicleManager* adsbVehicleManager READ adsbVehicleManager CONSTANT) + Q_PROPERTY(NTRIPManager* ntripManager READ ntripManager CONSTANT) Q_PROPERTY(QGCCorePlugin* corePlugin READ corePlugin CONSTANT) Q_PROPERTY(MissionCommandTree* missionCommandTree READ missionCommandTree CONSTANT) + Q_PROPERTY(MAVLinkSigningKeys* mavlinkSigningKeys READ mavlinkSigningKeys CONSTANT) #ifndef QGC_NO_SERIAL_LINK Q_PROPERTY(FactGroup* gpsRtk READ gpsRtkFactGroup CONSTANT) #endif @@ -97,7 +103,6 @@ class QGroundControlQmlGlobal : public QObject Q_PROPERTY(qreal zOrderTrajectoryLines READ zOrderTrajectoryLines CONSTANT) Q_PROPERTY(qreal zOrderWaypointLines READ zOrderWaypointLines CONSTANT) Q_PROPERTY(bool hasAPMSupport READ hasAPMSupport CONSTANT) - Q_PROPERTY(bool hasMAVLinkInspector READ hasMAVLinkInspector CONSTANT) //------------------------------------------------------------------------- @@ -137,8 +142,8 @@ class QGroundControlQmlGlobal : public QObject Q_INVOKABLE bool linesIntersect(QPointF xLine1, QPointF yLine1, QPointF xLine2, QPointF yLine2); - Q_INVOKABLE QString altitudeModeExtraUnits(AltMode altMode); ///< String shown in the FactTextField.extraUnits ui - Q_INVOKABLE QString altitudeModeShortDescription(AltMode altMode); ///< String shown when a user needs to select an altitude mode + Q_INVOKABLE QString altitudeFrameExtraUnits(AltitudeFrame altFrame); ///< String shown in the FactTextField.extraUnits ui + Q_INVOKABLE QString altitudeFrameShortDescription(AltitudeFrame altFrame); ///< String shown when a user needs to select an altitude frame /// Shows a simple message dialog. The dialog is parented to owner and will automatically close /// if the owner is destroyed, preventing orphaned dialogs that can cause crashes. @@ -166,6 +171,7 @@ class QGroundControlQmlGlobal : public QObject QGCMapEngineManager* mapEngineManager () { return _mapEngineManager; } QGCPositionManager* qgcPositionManger () { return _qgcPositionManager; } MissionCommandTree* missionCommandTree () { return _missionCommandTree; } + MAVLinkSigningKeys* mavlinkSigningKeys () { return _mavlinkSigningKeys; } VideoManager* videoManager () { return _videoManager; } QGCCorePlugin* corePlugin () { return _corePlugin; } SettingsManager* settingsManager () { return _settingsManager; } @@ -173,6 +179,7 @@ class QGroundControlQmlGlobal : public QObject FactGroup* gpsRtkFactGroup () { return _gpsRtkFactGroup; } #endif ADSBVehicleManager* adsbVehicleManager () { return _adsbVehicleManager; } + NTRIPManager* ntripManager () { return _ntripManager; } QmlUnitsConversion* unitsConversion () { return &_unitsConversion; } static QGeoCoordinate flightMapPosition () { return _coord; } static double flightMapZoom () { return _zoom; } @@ -191,12 +198,6 @@ class QGroundControlQmlGlobal : public QObject bool hasAPMSupport () { return true; } #endif -#if defined(QGC_DISABLE_MAVLINK_INSPECTOR) - bool hasMAVLinkInspector () { return false; } -#else - bool hasMAVLinkInspector () { return true; } -#endif - QString elevationProviderName (); QString elevationProviderNotice (); @@ -229,8 +230,10 @@ class QGroundControlQmlGlobal : public QObject private: QGCMapEngineManager* _mapEngineManager = nullptr; ADSBVehicleManager* _adsbVehicleManager = nullptr; + NTRIPManager* _ntripManager = nullptr; QGCPositionManager* _qgcPositionManager = nullptr; MissionCommandTree* _missionCommandTree = nullptr; + MAVLinkSigningKeys* _mavlinkSigningKeys = nullptr; VideoManager* _videoManager = nullptr; LinkManager* _linkManager = nullptr; MultiVehicleManager* _multiVehicleManager = nullptr; @@ -244,7 +247,7 @@ class QGroundControlQmlGlobal : public QObject double _flightMapInitialZoom = 17.0; QmlUnitsConversion _unitsConversion; - QStringList _altitudeModeEnumString; + QStringList _altitudeFrameEnumString; static QGeoCoordinate _coord; static double _zoom; diff --git a/src/QmlControls/QmlObjectListModel.cc b/src/QmlControls/QmlObjectListModel.cc index 7532e2057741..eb88d3ffac12 100644 --- a/src/QmlControls/QmlObjectListModel.cc +++ b/src/QmlControls/QmlObjectListModel.cc @@ -110,8 +110,10 @@ bool QmlObjectListModel::insertRows(int position, int rows, const QModelIndex& p qCWarning(QmlObjectListModelLog) << "Invalid position - position:count" << position << _objectList.count() << this; } - beginInsertRows(QModelIndex(), position, position + rows - 1); - endInsertRows(); + if (_resetModelNestingCount == 0) { + beginInsertRows(QModelIndex(), position, position + rows - 1); + endInsertRows(); + } _signalCountChangedIfNotNested(); @@ -128,11 +130,15 @@ bool QmlObjectListModel::removeRows(int position, int rows, const QModelIndex& p qCWarning(QmlObjectListModelLog) << "Invalid rows - position:rows:count" << position << rows << _objectList.count() << this; } - beginRemoveRows(QModelIndex(), position, position + rows - 1); + if (_resetModelNestingCount == 0) { + beginRemoveRows(QModelIndex(), position, position + rows - 1); + } for (int row=0; row + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#include "QmlObjectTreeModel.h" + +#include +#include + +#include "QGCLoggingCategory.h" + +QGC_LOGGING_CATEGORY(QmlObjectTreeModelLog, "API.QmlObjectTreeModel") + +namespace { + +constexpr const char* kDirtyChangedSignature = "dirtyChanged(bool)"; +constexpr const char* kChildDirtyChangedSlotSignature = "_childDirtyChanged(bool)"; + +QMetaMethod childDirtyChangedSlot() +{ + const QMetaObject* metaObject = &QmlObjectTreeModel::staticMetaObject; + const int slotIndex = metaObject->indexOfSlot(kChildDirtyChangedSlotSignature); + Q_ASSERT_X(slotIndex >= 0, "childDirtyChangedSlot", "slot signature mismatch — update kChildDirtyChangedSlotSignature"); + return (slotIndex >= 0) ? metaObject->method(slotIndex) : QMetaMethod(); +} + +QMetaMethod dirtyChangedSignal(const QObject* object) +{ + if (!object) { + return QMetaMethod(); + } + + const int signalIndex = object->metaObject()->indexOfSignal(kDirtyChangedSignature); + return (signalIndex >= 0) ? object->metaObject()->method(signalIndex) : QMetaMethod(); +} + +} // namespace + +//----------------------------------------------------------------------------- +// TreeNode +//----------------------------------------------------------------------------- + +int QmlObjectTreeModel::TreeNode::row() const +{ + if (parentNode) { + return parentNode->children.indexOf(const_cast(this)); + } + return 0; +} + +//----------------------------------------------------------------------------- +// Construction / Destruction +//----------------------------------------------------------------------------- + +QmlObjectTreeModel::QmlObjectTreeModel(QObject* parent) + : ObjectItemModelBase(parent) +{ +} + +QmlObjectTreeModel::~QmlObjectTreeModel() +{ + // Skip disconnect — objects may already be destroyed during application shutdown. + // Just delete the tree nodes. + _deleteSubtree(&_rootNode, false); +} + +//----------------------------------------------------------------------------- +// QAbstractItemModel overrides +//----------------------------------------------------------------------------- + +QModelIndex QmlObjectTreeModel::index(int row, int column, const QModelIndex& parent) const +{ + if (column != 0) { + return {}; + } + + const TreeNode* parentNode = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + if (!parentNode || row < 0 || row >= parentNode->children.count()) { + return {}; + } + + return createIndex(row, 0, parentNode->children.at(row)); +} + +QModelIndex QmlObjectTreeModel::parent(const QModelIndex& child) const +{ + if (!child.isValid()) { + return {}; + } + + const TreeNode* node = _nodeFromIndex(child); + if (!node || !node->parentNode || node->parentNode == &_rootNode) { + return {}; + } + + return _indexForNode(node->parentNode); +} + +int QmlObjectTreeModel::rowCount(const QModelIndex& parent) const +{ + const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + return node ? node->children.count() : 0; +} + +int QmlObjectTreeModel::columnCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return 1; +} + +bool QmlObjectTreeModel::hasChildren(const QModelIndex& parent) const +{ + const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode; + return node && !node->children.isEmpty(); +} + +QVariant QmlObjectTreeModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const TreeNode* node = _nodeFromIndex(index); + if (!node) { + return {}; + } + + switch (role) { + case ObjectRole: + return node->object ? QVariant::fromValue(node->object) : QVariant{}; + case TextRole: + return node->object ? QVariant::fromValue(node->object->objectName()) : QVariant{}; + case NodeTypeRole: + return QVariant::fromValue(node->nodeType); + case SeparatorRole: { + if (!node->children.isEmpty()) { + return false; + } + const TreeNode* parentNode = node->parentNode; + if (!parentNode) { + return false; + } + return node->row() < parentNode->children.size() - 1; + } + default: + return {}; + } +} + +bool QmlObjectTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid() || role != ObjectRole) { + return false; + } + + TreeNode* node = _nodeFromIndex(index); + if (!node) { + return false; + } + + if (node->object) { + _disconnectDirtyChanged(node->object); + } + + node->object = value.value(); + + if (node->object) { + QQmlEngine::setObjectOwnership(node->object, QQmlEngine::CppOwnership); + _connectDirtyChanged(node->object); + } + + emit dataChanged(index, index); + return true; +} + +bool QmlObjectTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/) +{ + qCWarning(QmlObjectTreeModelLog) << "insertRows() not supported — use insertItem()"; + return false; +} + +bool QmlObjectTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/) +{ + qCWarning(QmlObjectTreeModelLog) << "removeRows() not supported — use removeItem()"; + return false; +} + +QHash QmlObjectTreeModel::roleNames() const +{ + auto roles = ObjectItemModelBase::roleNames(); + roles[NodeTypeRole] = "nodeType"; + roles[SeparatorRole] = "separator"; + return roles; +} + +//----------------------------------------------------------------------------- +// Properties +//----------------------------------------------------------------------------- + +int QmlObjectTreeModel::count() const +{ + return _totalCount; +} + +void QmlObjectTreeModel::setDirty(bool dirty) +{ + if (_dirty != dirty) { + _dirty = dirty; + emit dirtyChanged(_dirty); + } +} + +//----------------------------------------------------------------------------- +// QML-accessible tree operations +//----------------------------------------------------------------------------- + +QObject* QmlObjectTreeModel::getObject(const QModelIndex& index) const +{ + if (!index.isValid()) { + return nullptr; + } + + const TreeNode* node = _nodeFromIndex(index); + return node ? node->object : nullptr; +} + +QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index"; + return {}; + } + + return insertItem(parentNode->children.count(), object, parentIndex); +} + +QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex) +{ + return insertItem(row, object, parentIndex, QString()); +} + +QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid parent index"; + return {}; + } + + if (row < 0 || row > parentNode->children.count()) { + qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid row" << row << "count:" << parentNode->children.count(); + return {}; + } + + auto* node = new TreeNode; + node->object = object; + node->parentNode = parentNode; + node->nodeType = nodeType; + + if (object) { + QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership); + _connectDirtyChanged(object); + } + + if (_resetModelNestingCount > 0) { + // During a batch reset we just accumulate nodes silently + parentNode->children.insert(row, node); + } else { + beginInsertRows(parentIndex, row, row); + parentNode->children.insert(row, node); + endInsertRows(); + _emitSeparatorChanged(parentIndex, row > 0 ? row - 1 : 0); + _signalCountChangedIfNotNested(); + } + + _totalCount++; + setDirty(true); + + return createIndex(row, 0, node); +} + +QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode) { + qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index"; + return {}; + } + return insertItem(parentNode->children.count(), object, parentIndex, nodeType); +} + +QObject* QmlObjectTreeModel::removeItem(const QModelIndex& index) +{ + if (!index.isValid()) { + qCWarning(QmlObjectTreeModelLog) << "removeItem: invalid index"; + return nullptr; + } + + TreeNode* node = _nodeFromIndex(index); + if (!node || !node->parentNode) { + qCWarning(QmlObjectTreeModelLog) << "removeItem: node not found or is the root"; + return nullptr; + } + + QObject* object = node->object; + TreeNode* parentNode = node->parentNode; + const int row = node->row(); + const QModelIndex parentIdx = _indexForNode(parentNode); + + _disconnectSubtree(node); + + // Count the subtree nodes being removed (the node itself + all descendants) + const int removedCount = 1 + _subtreeCount(node); + + if (_resetModelNestingCount == 0) { + beginRemoveRows(parentIdx, row, row); + } + parentNode->children.removeAt(row); + if (_resetModelNestingCount == 0) { + endRemoveRows(); + } + + // Free the subtree's TreeNode objects but NOT the QObjects + _deleteSubtree(node, false); + delete node; + + _totalCount -= removedCount; + if (_resetModelNestingCount == 0 && !parentNode->children.isEmpty()) { + _emitSeparatorChanged(parentIdx, parentNode->children.count() - 1); + } + _signalCountChangedIfNotNested(); + setDirty(true); + + return object; +} + +QModelIndex QmlObjectTreeModel::indexForObject(QObject* object) const +{ + if (!object) { + return {}; + } + + const TreeNode* node = _findNode(&_rootNode, object); + return node ? _indexForNode(node) : QModelIndex(); +} + +int QmlObjectTreeModel::depth(const QModelIndex& index) const +{ + if (!index.isValid()) { + return -1; + } + + int d = 0; + const TreeNode* node = _nodeFromIndex(index); + while (node && node->parentNode && node->parentNode != &_rootNode) { + d++; + node = node->parentNode; + } + return d; +} + +//----------------------------------------------------------------------------- +// C++ convenience API +//----------------------------------------------------------------------------- + +void QmlObjectTreeModel::appendRootItem(QObject* object) +{ + appendItem(object); +} + +void QmlObjectTreeModel::appendChild(const QModelIndex& parentIndex, QObject* object) +{ + appendItem(object, parentIndex); +} + +QObject* QmlObjectTreeModel::removeAt(const QModelIndex& parentIndex, int row) +{ + return removeItem(index(row, 0, parentIndex)); +} + +void QmlObjectTreeModel::removeChildren(const QModelIndex& parentIndex) +{ + TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode; + if (!parentNode || parentNode->children.isEmpty()) { + return; + } + + const int childCount = parentNode->children.count(); + + // Count all nodes being removed (direct children + their subtrees) + int removedCount = childCount; + for (const TreeNode* child : parentNode->children) { + removedCount += _subtreeCount(child); + } + + if (_resetModelNestingCount == 0) { + beginRemoveRows(parentIndex, 0, childCount - 1); + } + + // Disconnect signals but don't free nodes yet — views may still access them + for (TreeNode* child : parentNode->children) { + _disconnectSubtree(child); + } + + // Detach from parent + QList orphans = parentNode->children; + parentNode->children.clear(); + _totalCount -= removedCount; + + if (_resetModelNestingCount == 0) { + endRemoveRows(); + _signalCountChangedIfNotNested(); + } + + // Now safe to free the TreeNode structs + for (TreeNode* child : orphans) { + _deleteSubtree(child, false); + delete child; + } +} + +void QmlObjectTreeModel::clear() +{ + if (_rootNode.children.isEmpty()) { + return; + } + + beginResetModel(); + _disconnectSubtree(&_rootNode); + _deleteSubtree(&_rootNode, false); + _totalCount = 0; + endResetModel(); +} + +void QmlObjectTreeModel::clearAndDeleteContents() +{ + if (_rootNode.children.isEmpty()) { + return; + } + + beginResetModel(); + _disconnectSubtree(&_rootNode); + _deleteSubtree(&_rootNode, true); + _totalCount = 0; + endResetModel(); +} + +bool QmlObjectTreeModel::contains(QObject* object) const +{ + return _findNode(&_rootNode, object) != nullptr; +} + +//----------------------------------------------------------------------------- +// Private helpers +//----------------------------------------------------------------------------- + +QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_nodeFromIndex(const QModelIndex& index) const +{ + if (!index.isValid()) { + return nullptr; + } + return static_cast(index.internalPointer()); +} + +QModelIndex QmlObjectTreeModel::_indexForNode(const TreeNode* node) const +{ + if (!node || node == &_rootNode) { + return {}; + } + return createIndex(node->row(), 0, const_cast(node)); +} + +QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_findNode(const TreeNode* root, const QObject* object) const +{ + for (TreeNode* child : root->children) { + if (child->object == object) { + return child; + } + TreeNode* found = _findNode(child, object); + if (found) { + return found; + } + } + return nullptr; +} + +int QmlObjectTreeModel::_subtreeCount(const TreeNode* node) +{ + int result = node->children.count(); + for (const TreeNode* child : node->children) { + result += _subtreeCount(child); + } + return result; +} + +void QmlObjectTreeModel::_disconnectSubtree(TreeNode* node) +{ + if (node != &_rootNode && node->object) { + _disconnectDirtyChanged(node->object); + } + for (TreeNode* child : node->children) { + _disconnectSubtree(child); + } +} + +void QmlObjectTreeModel::_deleteSubtree(TreeNode* node, bool deleteObjects) +{ + for (TreeNode* child : node->children) { + _deleteSubtree(child, deleteObjects); + if (deleteObjects && child->object) { + child->object->deleteLater(); + } + delete child; + } + node->children.clear(); +} + +void QmlObjectTreeModel::_connectDirtyChanged(QObject* object) +{ + const QMetaMethod signal = dirtyChangedSignal(object); + const QMetaMethod slot = childDirtyChangedSlot(); + if (signal.isValid() && slot.isValid()) { + connect(object, signal, this, slot); + } +} + +void QmlObjectTreeModel::_disconnectDirtyChanged(QObject* object) +{ + const QMetaMethod signal = dirtyChangedSignal(object); + const QMetaMethod slot = childDirtyChangedSlot(); + if (signal.isValid() && slot.isValid()) { + disconnect(object, signal, this, slot); + } +} + +void QmlObjectTreeModel::_emitSeparatorChanged(const QModelIndex& parentIdx, int fromRow) +{ + const TreeNode* parentNode = parentIdx.isValid() ? _nodeFromIndex(parentIdx) : &_rootNode; + if (!parentNode) { + return; + } + const int last = parentNode->children.count() - 1; + if (fromRow > last) { + fromRow = last; + } + if (fromRow < 0) { + return; + } + const QModelIndex first = index(fromRow, 0, parentIdx); + const QModelIndex end = index(last, 0, parentIdx); + emit dataChanged(first, end, {SeparatorRole}); +} diff --git a/src/QmlControls/QmlObjectTreeModel.h b/src/QmlControls/QmlObjectTreeModel.h new file mode 100644 index 000000000000..ed71422bd38e --- /dev/null +++ b/src/QmlControls/QmlObjectTreeModel.h @@ -0,0 +1,122 @@ +/**************************************************************************** + * + * (c) 2009-2024 QGROUNDCONTROL PROJECT + * + * QGroundControl is licensed according to the terms in the file + * COPYING.md in the root of the source code directory. + * + ****************************************************************************/ + +#pragma once + +#include "ObjectItemModelBase.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(QmlObjectTreeModelLog) + +/// A tree model for QObject* items, usable from both C++ and QML. +/// Works like QmlObjectListModel but supports hierarchical parent/child relationships. +/// Compatible with Qt 6 TreeView. +/// +/// Top-level items are children of an invisible root node. The root is represented +/// by an invalid QModelIndex (the default). +class QmlObjectTreeModel : public ObjectItemModelBase +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + explicit QmlObjectTreeModel(QObject* parent = nullptr); + ~QmlObjectTreeModel() override; + + // -- ObjectItemModelBase overrides -- + int count() const override; + void setDirty(bool dirty) override; + void clear() override; + + // -- QAbstractItemModel overrides -- + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& child) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + bool hasChildren(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + bool insertRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; + QHash roleNames() const override; + + // -- QML-accessible tree operations -- + + /// Returns the QObject* stored at @p index, or nullptr if invalid + Q_INVOKABLE QObject* getObject(const QModelIndex& index) const; + + /// Appends @p object as the last child of @p parentIndex (root if invalid). Returns the new item's index. + Q_INVOKABLE QModelIndex appendItem(QObject* object, const QModelIndex& parentIndex = QModelIndex()); + + /// Same as above but also sets a nodeType tag on the created tree node. + Q_INVOKABLE QModelIndex appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType); + + /// Inserts @p object at @p row under @p parentIndex. Returns the new item's index. + Q_INVOKABLE QModelIndex insertItem(int row, QObject* object, const QModelIndex& parentIndex = QModelIndex()); + + /// Same as above but also sets a nodeType tag on the created tree node. + Q_INVOKABLE QModelIndex insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType); + + /// Removes the item (and its entire subtree) at @p index. Returns the removed QObject* (caller takes ownership). + /// Child QObjects are NOT deleted; only the internal tree nodes are freed. + Q_INVOKABLE QObject* removeItem(const QModelIndex& index); + + /// Number of direct children under @p parentIndex + Q_INVOKABLE int childCount(const QModelIndex& parentIndex = QModelIndex()) const { return rowCount(parentIndex); } + + /// Returns the QModelIndex for the child at @p row under @p parentIndex + Q_INVOKABLE QModelIndex childIndex(int row, const QModelIndex& parentIndex = QModelIndex()) const { return index(row, 0, parentIndex); } + + /// Searches the entire tree for @p object and returns its QModelIndex (invalid if not found) + Q_INVOKABLE QModelIndex indexForObject(QObject* object) const; + + /// Convenience wrapper around parent() + Q_INVOKABLE QModelIndex parentIndex(const QModelIndex& index) const { return parent(index); } + + /// Returns the depth of @p index (0 = root-level item, -1 = invalid index) + Q_INVOKABLE int depth(const QModelIndex& index) const; + + // -- C++ convenience API -- + void appendRootItem(QObject* object); + void appendChild(const QModelIndex& parentIndex, QObject* object); + QObject* removeAt(const QModelIndex& parentIndex, int row); + void removeChildren(const QModelIndex& parentIndex); ///< Removes all children of parentIndex without removing the parent itself + void clearAndDeleteContents(); ///< Clears the tree and calls deleteLater on every QObject + bool contains(QObject* object) const; + + static constexpr int NodeTypeRole = Qt::UserRole + 2; + static constexpr int SeparatorRole = Qt::UserRole + 3; + +private: + struct TreeNode { + QObject* object = nullptr; + TreeNode* parentNode = nullptr; + QList children; + QString nodeType; ///< Discriminator tag for delegates (e.g. "missionGroup", "fenceEditor") + + /// Row of this node within its parent's children list + int row() const; + }; + + TreeNode* _nodeFromIndex(const QModelIndex& index) const; + QModelIndex _indexForNode(const TreeNode* node) const; + TreeNode* _findNode(const TreeNode* root, const QObject* object) const; + static int _subtreeCount(const TreeNode* node); + void _disconnectSubtree(TreeNode* node); + void _deleteSubtree(TreeNode* node, bool deleteObjects); + void _connectDirtyChanged(QObject* object); + void _disconnectDirtyChanged(QObject* object); + void _emitSeparatorChanged(const QModelIndex& parentIdx, int fromRow); + + TreeNode _rootNode; ///< Invisible root; top-level items are its children + int _totalCount = 0; ///< Cached total node count (all nodes in tree) +}; diff --git a/src/QmlControls/RallyPointItemEditor.qml b/src/QmlControls/RallyPointItemEditor.qml deleted file mode 100644 index 3486128565b4..000000000000 --- a/src/QmlControls/RallyPointItemEditor.qml +++ /dev/null @@ -1,119 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts - -import QGroundControl -import QGroundControl.Controls -import QGroundControl.FactControls - -Rectangle { - id: root - height: _currentItem ? valuesRect.y + valuesRect.height + (_margin * 2) : titleBar.y - titleBar.height + _margin - color: _currentItem ? qgcPal.buttonHighlight : qgcPal.windowShade - radius: _radius - - signal clicked() - - property var rallyPoint ///< RallyPoint object associated with editor - property var controller ///< RallyPointController - - property bool _currentItem: rallyPoint ? rallyPoint === controller.currentRallyPoint : false - property color _outerTextColor: qgcPal.text // _currentItem ? "black" : qgcPal.text - - readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2 - readonly property real _radius: ScreenTools.defaultFontPixelWidth / 2 - readonly property real _titleHeight: ScreenTools.defaultFontPixelHeight * 2 - - QGCPalette { id: qgcPal; colorGroupEnabled: true } - - Item { - id: titleBar - anchors.margins: _margin - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - height: _titleHeight - - MissionItemIndexLabel { - id: indicator - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - label: "R" - checked: true - } - - QGCLabel { - anchors.leftMargin: _margin - anchors.left: indicator.right - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Rally Point") - color: _outerTextColor - } - - QGCColoredImage { - id: hamburger - anchors.rightMargin: _margin - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: ScreenTools.defaultFontPixelWidth * 2 - height: width - sourceSize.height: height - source: "qrc:/qmlimages/Hamburger.svg" - color: qgcPal.text - - MouseArea { - anchors.fill: parent - onClicked: hamburgerMenu.popup() - - QGCMenu { - id: hamburgerMenu - - QGCMenuItem { - text: qsTr("Delete") - onTriggered: controller.removePoint(rallyPoint) - } - } - } - } - } // Item - titleBar - - Rectangle { - id: valuesRect - anchors.margins: _margin - anchors.left: parent.left - anchors.right: parent.right - anchors.top: titleBar.bottom - height: valuesGrid.height + (_margin * 2) - color: qgcPal.windowShadeDark - visible: _currentItem - radius: _radius - - GridLayout { - id: valuesGrid - anchors.margins: _margin - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - rowSpacing: _margin - columnSpacing: _margin - rows: rallyPoint ? rallyPoint.textFieldFacts.length : 0 - flow: GridLayout.TopToBottom - - Repeater { - model: rallyPoint ? rallyPoint.textFieldFacts : 0 - QGCLabel { - text: modelData.name + ":" - } - } - - Repeater { - model: rallyPoint ? rallyPoint.textFieldFacts : 0 - FactTextField { - Layout.fillWidth: true - showUnits: true - fact: modelData - } - } - } // GridLayout - } // Rectangle -} // Rectangle diff --git a/src/QmlControls/ServoOutputMonitorController.cc b/src/QmlControls/ServoOutputMonitorController.cc new file mode 100644 index 000000000000..530230cc6c42 --- /dev/null +++ b/src/QmlControls/ServoOutputMonitorController.cc @@ -0,0 +1,41 @@ +#include "ServoOutputMonitorController.h" + +#include "QGCLoggingCategory.h" +#include "Vehicle.h" + +QGC_LOGGING_CATEGORY(ServoOutputMonitorControllerLog, "QMLControls.ServoOutputMonitorController") + +ServoOutputMonitorController::ServoOutputMonitorController(QObject *parent) + : FactPanelController(parent) +{ + (void) connect(_vehicle, &Vehicle::servoOutputsChanged, this, &ServoOutputMonitorController::servoValuesChanged); +} + +ServoOutputMonitorController::~ServoOutputMonitorController() +{ +} + +int ServoOutputMonitorController::servoValue(int servoIndex) const +{ + if (servoIndex >= 0 && servoIndex < _servoValues.size()) { + return _servoValues[servoIndex]; + } + + return -1; +} + +void ServoOutputMonitorController::servoValuesChanged(QVector pwmValues) +{ + const int servoCount = pwmValues.size(); + + _servoValues = pwmValues; + + if (_servoCount != servoCount) { + _servoCount = servoCount; + emit servoCountChanged(_servoCount); + } + + for (int servo = 0; servo < servoCount; servo++) { + emit servoValueChanged(servo, pwmValues[servo]); + } +} diff --git a/src/QmlControls/ServoOutputMonitorController.h b/src/QmlControls/ServoOutputMonitorController.h new file mode 100644 index 000000000000..3d051d6905c8 --- /dev/null +++ b/src/QmlControls/ServoOutputMonitorController.h @@ -0,0 +1,35 @@ +#pragma once + +#include "FactPanelController.h" + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(ServoOutputMonitorControllerLog) + +class ServoOutputMonitorController : public FactPanelController +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(int servoCount READ servoCount NOTIFY servoCountChanged) + +public: + explicit ServoOutputMonitorController(QObject *parent = nullptr); + ~ServoOutputMonitorController(); + + int servoCount() const { return _servoCount; } + + /// Servo index is 0..15 (SERVO1..SERVO16) + Q_INVOKABLE int servoValue(int servoIndex) const; + +signals: + void servoCountChanged(int servoCount); + void servoValueChanged(int servo, int pwmValue); + +private slots: + void servoValuesChanged(QVector pwmValues); + +private: + int _servoCount = 0; + QVector _servoValues = QVector(16, -1); +}; diff --git a/src/QmlControls/SigningKeyManager.qml b/src/QmlControls/SigningKeyManager.qml new file mode 100644 index 000000000000..c9d0c0b8b82a --- /dev/null +++ b/src/QmlControls/SigningKeyManager.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +SettingsGroupLayout { + id: _signingKeyManager + + Layout.fillWidth: true + heading: qsTr("MAVLink 2 Signing") + headingDescription: qsTr("Signing keys should only be sent to the vehicle over secure links.") + + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + + // Reactive key-usage tracking (re-evaluated on any vehicle's signing change) + property int _keyUsageRevision: 0 + Connections { + target: QGroundControl.mavlinkSigningKeys + function onKeyUsageChanged() { _keyUsageRevision++ } + } + + QGCPopupDialogFactory { + id: addKeyDialogFactory + dialogComponent: addKeyDialogComponent + } + + Component { + id: addKeyDialogComponent + + QGCPopupDialog { + title: qsTr("Add Signing Key") + buttons: Dialog.Ok | Dialog.Cancel + acceptButtonEnabled: keyNameField.text !== "" && keyPassphraseField.text !== "" + + onAccepted: { + QGroundControl.mavlinkSigningKeys.addKey(keyNameField.text, keyPassphraseField.text) + } + + ColumnLayout { + spacing: ScreenTools.defaultFontPixelHeight / 2 + + QGCLabel { text: qsTr("Key Name") } + QGCTextField { + id: keyNameField + Layout.fillWidth: true + Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 30 + placeholderText: qsTr("Enter a friendly name") + } + + QGCLabel { text: qsTr("Passphrase") } + QGCTextField { + id: keyPassphraseField + Layout.fillWidth: true + Layout.preferredWidth: ScreenTools.defaultFontPixelWidth * 30 + placeholderText: qsTr("Enter passphrase") + echoMode: TextInput.Password + } + } + } + } + + LabelledLabel { + Layout.fillWidth: true + label: qsTr("Signing Status") + labelText: _activeVehicle ? (_activeVehicle.mavlinkSigning ? qsTr("Active") : qsTr("Inactive")) : qsTr("Not Connected") + } + + LabelledLabel { + Layout.fillWidth: true + label: qsTr("Active Key") + labelText: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" ? _activeVehicle.mavlinkSigningKeyName : qsTr("None") + visible: _activeVehicle + } + + Repeater { + model: QGroundControl.mavlinkSigningKeys.keys + + RowLayout { + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelWidth + + property bool _keyIsActive: { void(_keyUsageRevision); return QGroundControl.mavlinkSigningKeys.isKeyInUse(object.name) } + property bool _keyIsActiveVehicle: _activeVehicle && _activeVehicle.mavlinkSigningKeyName === object.name + property bool _anyKeyActive: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" + + QGCLabel { + text: object.name + Layout.fillWidth: true + } + + QGCButton { + text: qsTr("Enable") + visible: !parent._anyKeyActive + enabled: _activeVehicle + onClicked: _activeVehicle.sendSetupSigning(index) + } + + QGCButton { + text: qsTr("Disable") + visible: parent._keyIsActiveVehicle + onClicked: _activeVehicle.sendDisableSigning() + } + + QGCButton { + text: qsTr("Delete") + visible: !parent._keyIsActive + onClicked: QGroundControl.showMessageDialog( + _signingKeyManager, + qsTr("Delete Signing Key"), + qsTr("Are you sure you want to delete '%1'? If a vehicle still has this key configured, you will no longer be able to communicate with it over a signed connection.").arg(object.name), + Dialog.Ok | Dialog.Cancel, + function () { QGroundControl.mavlinkSigningKeys.removeKey(index) }) + } + } + } + + QGCLabel { + text: qsTr("No keys configured") + visible: QGroundControl.mavlinkSigningKeys.keys.count === 0 + } + + QGCButton { + text: qsTr("Add Key") + onClicked: addKeyDialogFactory.open() + } +} diff --git a/src/QmlControls/ToolStripHoverButton.qml b/src/QmlControls/ToolStripHoverButton.qml index b5ddae984414..52c58303b3fc 100644 --- a/src/QmlControls/ToolStripHoverButton.qml +++ b/src/QmlControls/ToolStripHoverButton.qml @@ -27,8 +27,8 @@ Button { property real imageScale: forceImageScale11 && (text == "") ? 0.8 : 0.6 property real contentMargins: innerText.height * 0.1 - property color _currentContentColor: (checked || pressed) ? qgcPal.buttonHighlightText : qgcPal.windowTransparentText - property color _currentContentColorSecondary: (checked || pressed) ? qgcPal.windowTransparentText : qgcPal.buttonHighlight + property color _currentContentColor: (checked || pressed) ? qgcPal.buttonHighlightText : qgcPal.text + property color _currentContentColorSecondary: (checked || pressed) ? qgcPal.text : qgcPal.buttonHighlight signal dropped(int index) diff --git a/src/QmlControls/EditPositionDialog.FactMetaData.json b/src/QmlControls/TransformPositionController.FactMetaData.json similarity index 56% rename from src/QmlControls/EditPositionDialog.FactMetaData.json rename to src/QmlControls/TransformPositionController.FactMetaData.json index afc198d3097f..32645461c90d 100644 --- a/src/QmlControls/EditPositionDialog.FactMetaData.json +++ b/src/QmlControls/TransformPositionController.FactMetaData.json @@ -7,6 +7,7 @@ "name": "Latitude", "shortDesc": "Latitude of item position", "type": "double", + "units": "deg", "min": -90.0, "max": 90.0, "decimalPlaces": 7 @@ -15,6 +16,7 @@ "name": "Longitude", "shortDesc": "Longitude of item position", "type": "double", + "units": "deg", "min": -180.0, "max": 180.0, "decimalPlaces": 7 @@ -23,13 +25,13 @@ "name": "Easting", "shortDesc": "Easting of item position", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 2 }, { "name": "Northing", "shortDesc": "Northing of item position", "type": "double", - "decimalPlaces": 7 + "decimalPlaces": 2 }, { "name": "Zone", @@ -49,6 +51,38 @@ "name": "MGRS", "shortDesc": "MGRS coordinate", "type": "string" +}, +{ + "name": "OffsetEast", + "shortDesc": "East offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "OffsetNorth", + "shortDesc": "North offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "OffsetUp", + "shortDesc": "Up offset", + "type": "double", + "units": "m", + "defaultValue": 0, + "decimalPlaces": 2 +}, +{ + "name": "RotateDegreesCW", + "shortDesc": "Clockwise rotation", + "type": "double", + "units": "deg", + "defaultValue": 0, + "decimalPlaces": 1 } ] } diff --git a/src/QmlControls/TransformPositionController.cc b/src/QmlControls/TransformPositionController.cc new file mode 100644 index 000000000000..991b288aa245 --- /dev/null +++ b/src/QmlControls/TransformPositionController.cc @@ -0,0 +1,133 @@ +#include "TransformPositionController.h" +#include "QGCGeo.h" +#include "MultiVehicleManager.h" +#include "Vehicle.h" +#include "QGCLoggingCategory.h" + +QGC_LOGGING_CATEGORY(TransformPositionControllerLog, "QMLControls.TransformPositionController") + +QMap TransformPositionController::_metaDataMap; + +TransformPositionController::TransformPositionController(QObject *parent) + : QObject(parent) + , _latitudeFact(new Fact(0, _latitudeFactName, FactMetaData::valueTypeDouble, this)) + , _longitudeFact(new Fact(0, _longitudeFactName, FactMetaData::valueTypeDouble, this)) + , _zoneFact(new Fact(0, _zoneFactName, FactMetaData::valueTypeUint8, this)) + , _hemisphereFact(new Fact(0, _hemisphereFactName, FactMetaData::valueTypeUint8, this)) + , _eastingFact(new Fact(0, _eastingFactName, FactMetaData::valueTypeDouble, this)) + , _northingFact(new Fact(0, _northingFactName, FactMetaData::valueTypeDouble, this)) + , _mgrsFact(new Fact(0, _mgrsFactName, FactMetaData::valueTypeString, this)) + , _offsetEastFact(new Fact(0, _offsetEastFactName, FactMetaData::valueTypeDouble, this)) + , _offsetNorthFact(new Fact(0, _offsetNorthFactName, FactMetaData::valueTypeDouble, this)) + , _offsetUpFact(new Fact(0, _offsetUpFactName, FactMetaData::valueTypeDouble, this)) + , _rotateDegreesCWFact(new Fact(0, _rotateDegreesCWFactName, FactMetaData::valueTypeDouble, this)) +{ + // qCDebug(TransformPositionControllerLog) << Q_FUNC_INFO << this; + + if (_metaDataMap.isEmpty()) { + _metaDataMap = FactMetaData::createMapFromJsonFile(QStringLiteral(":/json/TransformPositionController.FactMetaData.json"), nullptr /* QObject parent */); + } + + _latitudeFact->setMetaData(_metaDataMap[_latitudeFactName]); + _longitudeFact->setMetaData(_metaDataMap[_longitudeFactName]); + _zoneFact->setMetaData(_metaDataMap[_zoneFactName]); + _hemisphereFact->setMetaData(_metaDataMap[_hemisphereFactName]); + _eastingFact->setMetaData(_metaDataMap[_eastingFactName]); + _northingFact->setMetaData(_metaDataMap[_northingFactName]); + _mgrsFact->setMetaData(_metaDataMap[_mgrsFactName]); + _offsetEastFact->setMetaData(_metaDataMap[_offsetEastFactName]); + _offsetNorthFact->setMetaData(_metaDataMap[_offsetNorthFactName]); + _offsetUpFact->setMetaData(_metaDataMap[_offsetUpFactName]); + _rotateDegreesCWFact->setMetaData(_metaDataMap[_rotateDegreesCWFactName]); +} + +TransformPositionController::~TransformPositionController() +{ + // qCDebug(TransformPositionControllerLog) << Q_FUNC_INFO << this; +} + +void TransformPositionController::setCoordinate(QGeoCoordinate coordinate) +{ + if (!coordinate.isValid()) { + qCWarning(TransformPositionControllerLog) << "Attempt to set invalid coordinate"; + return; + } + + if (coordinate == _coordinate) { + return; + } + + const bool wasInvalid = !_coordinate.isValid(); + _coordinate = coordinate; + + // Do not emit on the initial invalid->valid transition to prevent + // onCoordinateChanged handlers from firing on initial dialog setup. + if (!wasInvalid) { + emit coordinateChanged(_coordinate); + } +} + +void TransformPositionController::initValues() +{ + if (!_coordinate.isValid()) { + return; + } + + _latitudeFact->setRawValue(_coordinate.latitude()); + _longitudeFact->setRawValue(_coordinate.longitude()); + + double easting, northing; + const int zone = QGCGeo::convertGeoToUTM(_coordinate, easting, northing); + if ((zone >= 1) && (zone <= 60)) { + _zoneFact->setRawValue(zone); + _hemisphereFact->setRawValue(_coordinate.latitude() < 0); + _eastingFact->setRawValue(easting); + _northingFact->setRawValue(northing); + } + + const QString mgrs = QGCGeo::convertGeoToMGRS(_coordinate); + if (!mgrs.isEmpty()) { + _mgrsFact->setRawValue(mgrs); + } +} + +void TransformPositionController::setFromGeo() +{ + QGeoCoordinate newCoordinate = _coordinate; + newCoordinate.setLatitude(_latitudeFact->rawValue().toDouble()); + newCoordinate.setLongitude(_longitudeFact->rawValue().toDouble()); + setCoordinate(newCoordinate); +} + +void TransformPositionController::setFromUTM() +{ + qCDebug(TransformPositionControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1); + QGeoCoordinate newCoordinate; + if (QGCGeo::convertUTMToGeo(_eastingFact->rawValue().toDouble(), _northingFact->rawValue().toDouble(), _zoneFact->rawValue().toInt(), _hemisphereFact->rawValue().toInt() == 1, newCoordinate)) { + qCDebug(TransformPositionControllerLog) << _eastingFact->rawValue().toDouble() << _northingFact->rawValue().toDouble() << _zoneFact->rawValue().toInt() << (_hemisphereFact->rawValue().toInt() == 1) << newCoordinate; + setCoordinate(newCoordinate); + } else { + initValues(); + } +} + +void TransformPositionController::setFromMGRS() +{ + QGeoCoordinate newCoordinate; + if (QGCGeo::convertMGRSToGeo(_mgrsFact->rawValue().toString(), newCoordinate)) { + setCoordinate(newCoordinate); + } else { + initValues(); + } +} + +void TransformPositionController::setFromVehicle() +{ + Vehicle* activeVehicle = MultiVehicleManager::instance()->activeVehicle(); + if (!activeVehicle) { + qCWarning(TransformPositionControllerLog) << "Cannot set coordinate from vehicle: no active vehicle"; + return; + } + + setCoordinate(activeVehicle->coordinate()); +} diff --git a/src/QmlControls/TransformPositionController.h b/src/QmlControls/TransformPositionController.h new file mode 100644 index 000000000000..e2ef4bbaf45c --- /dev/null +++ b/src/QmlControls/TransformPositionController.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "Fact.h" + +Q_DECLARE_LOGGING_CATEGORY(TransformPositionControllerLog) + +class TransformPositionController : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QGeoCoordinate coordinate READ coordinate WRITE setCoordinate NOTIFY coordinateChanged) + Q_PROPERTY(Fact *latitude READ latitude CONSTANT) + Q_PROPERTY(Fact *longitude READ longitude CONSTANT) + Q_PROPERTY(Fact *zone READ zone CONSTANT) + Q_PROPERTY(Fact *hemisphere READ hemisphere CONSTANT) + Q_PROPERTY(Fact *easting READ easting CONSTANT) + Q_PROPERTY(Fact *northing READ northing CONSTANT) + Q_PROPERTY(Fact *mgrs READ mgrs CONSTANT) + Q_PROPERTY(Fact *offsetEast READ offsetEast CONSTANT) + Q_PROPERTY(Fact *offsetNorth READ offsetNorth CONSTANT) + Q_PROPERTY(Fact *offsetUp READ offsetUp CONSTANT) + Q_PROPERTY(Fact *rotateDegreesCW READ rotateDegreesCW CONSTANT) + +public: + explicit TransformPositionController(QObject *parent = nullptr); + ~TransformPositionController(); + + Q_INVOKABLE void initValues(); + Q_INVOKABLE void setFromGeo(); + Q_INVOKABLE void setFromUTM(); + Q_INVOKABLE void setFromMGRS(); + Q_INVOKABLE void setFromVehicle(); + + void setCoordinate(QGeoCoordinate coordinate); + QGeoCoordinate coordinate() const { return _coordinate; } + + Fact *latitude() { return _latitudeFact; } + Fact *longitude() { return _longitudeFact; } + Fact *zone() { return _zoneFact; } + Fact *hemisphere() { return _hemisphereFact; } + Fact *easting() { return _eastingFact; } + Fact *northing() { return _northingFact; } + Fact *mgrs() { return _mgrsFact; } + Fact *offsetEast() { return _offsetEastFact; } + Fact *offsetNorth() { return _offsetNorthFact; } + Fact *offsetUp() { return _offsetUpFact; } + Fact *rotateDegreesCW() { return _rotateDegreesCWFact; } + +signals: + void coordinateChanged(QGeoCoordinate coordinate); + +private: + QGeoCoordinate _coordinate; + + Fact *_latitudeFact = nullptr; + Fact *_longitudeFact = nullptr; + Fact *_zoneFact = nullptr; + Fact *_hemisphereFact = nullptr; + Fact *_eastingFact = nullptr; + Fact *_northingFact = nullptr; + Fact *_mgrsFact = nullptr; + Fact *_offsetEastFact = nullptr; + Fact *_offsetNorthFact = nullptr; + Fact *_offsetUpFact = nullptr; + Fact *_rotateDegreesCWFact = nullptr; + + static QMap _metaDataMap; + + static constexpr const char *_latitudeFactName = "Latitude"; + static constexpr const char *_longitudeFactName = "Longitude"; + static constexpr const char *_zoneFactName = "Zone"; + static constexpr const char *_hemisphereFactName = "Hemisphere"; + static constexpr const char *_eastingFactName = "Easting"; + static constexpr const char *_northingFactName = "Northing"; + static constexpr const char *_mgrsFactName = "MGRS"; + static constexpr const char *_offsetEastFactName = "OffsetEast"; + static constexpr const char *_offsetNorthFactName = "OffsetNorth"; + static constexpr const char *_offsetUpFactName = "OffsetUp"; + static constexpr const char *_rotateDegreesCWFactName = "RotateDegreesCW"; +}; diff --git a/src/QmlControls/VehicleSummaryRow.qml b/src/QmlControls/VehicleSummaryRow.qml index 29c24b6d6c4a..491b9e2c512d 100644 --- a/src/QmlControls/VehicleSummaryRow.qml +++ b/src/QmlControls/VehicleSummaryRow.qml @@ -2,24 +2,28 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QGroundControl +import QGroundControl.Controls + RowLayout { id: root + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelHeight property string labelText: "Label" property string valueText: "value" property string valueColor: "" - width: parent.width - QGCLabel { - id: label - text: root.labelText + id: label + Layout.fillWidth: true + text: root.labelText } + QGCLabel { - text: root.valueText - color: root.valueColor !== "" ? root.valueColor : palette.text - elide: Text.ElideRight - horizontalAlignment: Text.AlignRight - Layout.fillWidth: true + Layout.maximumWidth: ScreenTools.defaultFontPixelWidth * 20 + text: root.valueText + color: root.valueColor !== "" ? root.valueColor : QGroundControl.globalPalette.text + elide: Text.ElideRight } } diff --git a/src/QtLocationPlugin/CMakeLists.txt b/src/QtLocationPlugin/CMakeLists.txt index d403c768d929..83d57ff1d7a8 100644 --- a/src/QtLocationPlugin/CMakeLists.txt +++ b/src/QtLocationPlugin/CMakeLists.txt @@ -92,6 +92,4 @@ target_include_directories(QGCLocation Providers ) -set_source_files_properties(QMLControl/OfflineMapEditor.qml PROPERTIES QT_RESOURCE_ALIAS OfflineMapEditor.qml) - target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE QGCLocation) diff --git a/src/Settings/AppSettings.cc b/src/Settings/AppSettings.cc index c7a7a309dc2b..1c511926d6bb 100644 --- a/src/Settings/AppSettings.cc +++ b/src/Settings/AppSettings.cc @@ -96,7 +96,12 @@ DECLARE_SETTINGGROUP(App, "") #endif savePathFact->setVisible(false); #else - QDir rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + QDir rootDir; + if (qgcApp()->runningUnitTests() || qgcApp()->simpleBootTest()) { + rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + } else { + rootDir = QDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + } savePathFact->setRawValue(rootDir.filePath(appName)); #endif } diff --git a/src/Settings/GimbalController.SettingsGroup.json b/src/Settings/GimbalController.SettingsGroup.json index 343a2550dfa2..84227e875486 100644 --- a/src/Settings/GimbalController.SettingsGroup.json +++ b/src/Settings/GimbalController.SettingsGroup.json @@ -4,35 +4,33 @@ "QGC.MetaData.Facts": [ { - "name": "EnableOnScreenControl", + "name": "enableOnScreenControl", "shortDesc": "Enable on Screen Camera Control", "type": "bool", "default": false }, { - "name": "ControlType", - "shortDesc": "Type of on-screen control", - "type": "uint32", - "enumStrings": "Click to point, Click and drag", - "enumValues": "0, 1", - "default": 0 + "name": "clickAndDrag", + "shortDesc": "Use click and drag control instead of click to point", + "type": "bool", + "default": false }, { - "name": "CameraVFov", + "name": "cameraVFov", "shortDesc": "Vertical camera field of view", "type": "uint32", "default": 80, "units": "deg" }, { - "name": "CameraHFov", + "name": "cameraHFov", "shortDesc": "Horizontal camera field of view", "type": "uint32", "default": 80, "units": "deg" }, { - "name": "CameraSlideSpeed", + "name": "cameraSlideSpeed", "shortDesc": "Maximum gimbal speed on click and drag (deg/sec)", "type": "uint32", "default": 30, diff --git a/src/Settings/GimbalControllerSettings.cc b/src/Settings/GimbalControllerSettings.cc index 4e16810f49f1..864aec5e3ea5 100644 --- a/src/Settings/GimbalControllerSettings.cc +++ b/src/Settings/GimbalControllerSettings.cc @@ -2,13 +2,31 @@ DECLARE_SETTINGGROUP(GimbalController, "GimbalController") { + // Setting names were changed from PascalCase to camelCase + // Migrate old settings to new names + QSettings settings; + settings.beginGroup(_name); + static const QMap renamedKeys = { + { "EnableOnScreenControl", "enableOnScreenControl" }, + { "ControlType", "clickAndDrag" }, + { "CameraVFov", "cameraVFov" }, + { "CameraHFov", "cameraHFov" }, + { "CameraSlideSpeed", "cameraSlideSpeed" }, + }; + for (auto it = renamedKeys.constBegin(); it != renamedKeys.constEnd(); ++it) { + if (settings.contains(it.key())) { + settings.setValue(it.value(), settings.value(it.key())); + settings.remove(it.key()); + } + } + settings.endGroup(); } -DECLARE_SETTINGSFACT(GimbalControllerSettings, EnableOnScreenControl) -DECLARE_SETTINGSFACT(GimbalControllerSettings, ControlType) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraVFov) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraHFov) -DECLARE_SETTINGSFACT(GimbalControllerSettings, CameraSlideSpeed) +DECLARE_SETTINGSFACT(GimbalControllerSettings, enableOnScreenControl) +DECLARE_SETTINGSFACT(GimbalControllerSettings, clickAndDrag) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraVFov) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraHFov) +DECLARE_SETTINGSFACT(GimbalControllerSettings, cameraSlideSpeed) DECLARE_SETTINGSFACT(GimbalControllerSettings, showAzimuthIndicatorOnMap) DECLARE_SETTINGSFACT(GimbalControllerSettings, toolbarIndicatorShowAzimuth) DECLARE_SETTINGSFACT(GimbalControllerSettings, toolbarIndicatorShowAcquireReleaseControl) diff --git a/src/Settings/GimbalControllerSettings.h b/src/Settings/GimbalControllerSettings.h index 27fa38090474..17fa0fe51339 100644 --- a/src/Settings/GimbalControllerSettings.h +++ b/src/Settings/GimbalControllerSettings.h @@ -13,11 +13,11 @@ class GimbalControllerSettings : public SettingsGroup GimbalControllerSettings(QObject* parent = nullptr); DEFINE_SETTING_NAME_GROUP() - DEFINE_SETTINGFACT(EnableOnScreenControl) - DEFINE_SETTINGFACT(ControlType) - DEFINE_SETTINGFACT(CameraVFov) - DEFINE_SETTINGFACT(CameraHFov) - DEFINE_SETTINGFACT(CameraSlideSpeed) + DEFINE_SETTINGFACT(enableOnScreenControl) + DEFINE_SETTINGFACT(clickAndDrag) + DEFINE_SETTINGFACT(cameraVFov) + DEFINE_SETTINGFACT(cameraHFov) + DEFINE_SETTINGFACT(cameraSlideSpeed) DEFINE_SETTINGFACT(showAzimuthIndicatorOnMap) DEFINE_SETTINGFACT(toolbarIndicatorShowAzimuth) DEFINE_SETTINGFACT(toolbarIndicatorShowAcquireReleaseControl) diff --git a/src/Settings/Mavlink.SettingsGroup.json b/src/Settings/Mavlink.SettingsGroup.json index 632336f1aa29..aa206bce4709 100644 --- a/src/Settings/Mavlink.SettingsGroup.json +++ b/src/Settings/Mavlink.SettingsGroup.json @@ -53,12 +53,6 @@ "type": "string", "default": "support.ardupilot.org:xxxx" }, -{ - "name": "mavlink2SigningKey", - "shortDesc": "MAVLink 2.0 signing key", - "type": "string", - "default": "" -}, { "name": "sendGCSHeartbeat", "shortDesc": "Send GCS Heartbeat", diff --git a/src/Settings/MavlinkSettings.cc b/src/Settings/MavlinkSettings.cc index 215755587b76..da378751f284 100644 --- a/src/Settings/MavlinkSettings.cc +++ b/src/Settings/MavlinkSettings.cc @@ -1,5 +1,4 @@ #include "MavlinkSettings.h" -#include "LinkManager.h" DECLARE_SETTINGGROUP(Mavlink, "") { @@ -37,17 +36,3 @@ DECLARE_SETTINGSFACT(MavlinkSettings, forwardMavlinkHostName) DECLARE_SETTINGSFACT(MavlinkSettings, forwardMavlinkAPMSupportHostName) DECLARE_SETTINGSFACT(MavlinkSettings, sendGCSHeartbeat) DECLARE_SETTINGSFACT(MavlinkSettings, gcsMavlinkSystemID) - -DECLARE_SETTINGSFACT_NO_FUNC(MavlinkSettings, mavlink2SigningKey) -{ - if (!_mavlink2SigningKeyFact) { - _mavlink2SigningKeyFact = _createSettingsFact(mavlink2SigningKeyName); - connect(_mavlink2SigningKeyFact, &Fact::rawValueChanged, this, &MavlinkSettings::_mavlink2SigningKeyChanged); - } - return _mavlink2SigningKeyFact; -} - -void MavlinkSettings::_mavlink2SigningKeyChanged(void) -{ - LinkManager::instance()->resetMavlinkSigning(); -} diff --git a/src/Settings/MavlinkSettings.h b/src/Settings/MavlinkSettings.h index 81fe1f70f0bd..bc888d5f3feb 100644 --- a/src/Settings/MavlinkSettings.h +++ b/src/Settings/MavlinkSettings.h @@ -21,13 +21,9 @@ class MavlinkSettings : public SettingsGroup DEFINE_SETTINGFACT(forwardMavlink) DEFINE_SETTINGFACT(forwardMavlinkHostName) DEFINE_SETTINGFACT(forwardMavlinkAPMSupportHostName) - DEFINE_SETTINGFACT(mavlink2SigningKey) DEFINE_SETTINGFACT(sendGCSHeartbeat) DEFINE_SETTINGFACT(gcsMavlinkSystemID) // Although this is a global setting it only affects ArduPilot vehicle since PX4 automatically starts the stream from the vehicle side DEFINE_SETTINGFACT(apmStartMavlinkStreams) - -private slots: - void _mavlink2SigningKeyChanged(); }; diff --git a/src/Settings/NTRIP.SettingsGroup.json b/src/Settings/NTRIP.SettingsGroup.json index 527e19f39bc7..2348a5481c7e 100644 --- a/src/Settings/NTRIP.SettingsGroup.json +++ b/src/Settings/NTRIP.SettingsGroup.json @@ -4,63 +4,76 @@ "QGC.MetaData.Facts": [ { "name": "ntripServerConnectEnabled", - "shortDescription": "Connect to NTRIP server", - "longDescription": "Connect to NTRIP server using specified address/port", + "shortDesc": "Connect to NTRIP server", + "longDesc": "Connect to NTRIP server using specified address/port", "type": "bool", - "defaultValue": false, - "qgcRebootRequired": true + "default": false }, { "name": "ntripServerHostAddress", - "shortDescription": "Host address", + "shortDesc": "Host address", "type": "string", - "defaultValue": "127.0.0.1", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripServerPort", - "shortDescription": "Server port", - "type": "int32", - "defaultValue": 2101, - "qgcRebootRequired": true + "shortDesc": "Server port", + "type": "uint16", + "default": 2101 }, { "name": "ntripUsername", - "shortDescription": "Username", + "shortDesc": "Username", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripPassword", - "shortDescription": "Password", + "shortDesc": "Password", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripMountpoint", - "shortDescription": "Mount Point", - "longDescription": "NTRIP Mount Point. Leave blank for RTCM over TCP", + "shortDesc": "Mount Point", + "longDesc": "NTRIP mount point. Leave blank for RTCM over TCP", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { "name": "ntripWhitelist", - "shortDescription": "RTCM Message Whitelist", - "longDescription": "RTCM Messages to send over mavlink. Leave blank for all messages", + "shortDesc": "RTCM Message Filter", + "longDesc": "Comma-separated RTCM message IDs to forward (e.g. 1005,1077,1087). Leave blank for all messages.", "type": "string", - "defaultValue": "", - "qgcRebootRequired": true + "default": "" }, { - "name": "ntripUseSpartn", - "shortDescription": "Use SPARTN pipeline", - "longDescription": "If enabled, connect with TLS (2102) and treat payload as SPARTN instead of RTCM. Requires SPARTN keys handled by the receiver.", + "name": "ntripUseTls", + "shortDesc": "Use TLS encryption", + "longDesc": "Connect using TLS/SSL encryption (required for some SPARTN casters on port 2102)", "type": "bool", - "defaultValue": false, - "qgcRebootRequired": true + "default": false + }, + { + "name": "ntripUdpForwardEnabled", + "shortDesc": "UDP forward RTCM data", + "longDesc": "Forward received RTCM correction data via UDP to the specified address and port", + "type": "bool", + "default": false + }, + { + "name": "ntripUdpTargetAddress", + "shortDesc": "UDP target address", + "longDesc": "IP address to forward RTCM data to via UDP", + "type": "string", + "default": "" + }, + { + "name": "ntripUdpTargetPort", + "shortDesc": "UDP target port", + "longDesc": "Port to forward RTCM data to via UDP", + "type": "uint16", + "default": 0 } ] } diff --git a/src/Settings/NTRIPSettings.cc b/src/Settings/NTRIPSettings.cc index cb3d9048867b..4ca4617359f4 100644 --- a/src/Settings/NTRIPSettings.cc +++ b/src/Settings/NTRIPSettings.cc @@ -1,62 +1,6 @@ #include "NTRIPSettings.h" -NTRIPSettings::NTRIPSettings(QObject* parent) - : SettingsGroup("NTRIP", "NTRIP", parent) -{ - FactMetaData* metaData = new FactMetaData(FactMetaData::valueTypeBool, this); - metaData->setName("ntripServerConnectEnabled"); - metaData->setShortDescription(tr("Enable NTRIP")); - metaData->setRawDefaultValue(false); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripServerHostAddress"); - metaData->setShortDescription(tr("Host Address")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeUint16, this); - metaData->setName("ntripServerPort"); - metaData->setShortDescription(tr("Port")); - metaData->setRawDefaultValue(2101); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripUsername"); - metaData->setShortDescription(tr("Username")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripPassword"); - metaData->setShortDescription(tr("Password")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripMountpoint"); - metaData->setShortDescription(tr("Mount Point")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeString, this); - metaData->setName("ntripWhitelist"); - metaData->setShortDescription(tr("RTCM Message Whitelist")); - metaData->setRawDefaultValue(""); - _nameToMetaDataMap[metaData->name()] = metaData; - - metaData = new FactMetaData(FactMetaData::valueTypeBool, this); - metaData->setName("ntripUseSpartn"); - metaData->setShortDescription(tr("Use SPARTN pipeline")); - metaData->setRawDefaultValue(false); - _nameToMetaDataMap[metaData->name()] = metaData; - - // Force ntripServerConnectEnabled to false at every startup, ignoring saved settings - // This ensures NTRIP never auto-starts regardless of previous user state - if (ntripServerConnectEnabled()) { - ntripServerConnectEnabled()->setRawValue(false); - } -} +DECLARE_SETTINGGROUP(NTRIP, "NTRIP") {} DECLARE_SETTINGSFACT(NTRIPSettings, ntripServerConnectEnabled) DECLARE_SETTINGSFACT(NTRIPSettings, ntripServerHostAddress) @@ -65,4 +9,7 @@ DECLARE_SETTINGSFACT(NTRIPSettings, ntripUsername) DECLARE_SETTINGSFACT(NTRIPSettings, ntripPassword) DECLARE_SETTINGSFACT(NTRIPSettings, ntripMountpoint) DECLARE_SETTINGSFACT(NTRIPSettings, ntripWhitelist) -DECLARE_SETTINGSFACT(NTRIPSettings, ntripUseSpartn) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUseTls) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpForwardEnabled) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpTargetAddress) +DECLARE_SETTINGSFACT(NTRIPSettings, ntripUdpTargetPort) diff --git a/src/Settings/NTRIPSettings.h b/src/Settings/NTRIPSettings.h index d3619c2039fe..8361ed850a7f 100644 --- a/src/Settings/NTRIPSettings.h +++ b/src/Settings/NTRIPSettings.h @@ -18,5 +18,8 @@ class NTRIPSettings : public SettingsGroup DEFINE_SETTINGFACT(ntripPassword) DEFINE_SETTINGFACT(ntripMountpoint) DEFINE_SETTINGFACT(ntripWhitelist) - DEFINE_SETTINGFACT(ntripUseSpartn) + DEFINE_SETTINGFACT(ntripUseTls) + DEFINE_SETTINGFACT(ntripUdpForwardEnabled) + DEFINE_SETTINGFACT(ntripUdpTargetAddress) + DEFINE_SETTINGFACT(ntripUdpTargetPort) }; diff --git a/src/UI/AppSettings/CMakeLists.txt b/src/UI/AppSettings/CMakeLists.txt index 84eb16fa66a7..1883a2399d81 100644 --- a/src/UI/AppSettings/CMakeLists.txt +++ b/src/UI/AppSettings/CMakeLists.txt @@ -28,5 +28,8 @@ qt_add_qml_module(AppSettingsModule TelemetrySettings.qml UdpSettings.qml VideoSettings.qml + + OfflineMapEditor.qml + OfflineMapInfo.qml NO_PLUGIN ) diff --git a/src/UI/AppSettings/NTRIPSettings.qml b/src/UI/AppSettings/NTRIPSettings.qml index 4062ebbb0e30..dab0e15eea01 100644 --- a/src/UI/AppSettings/NTRIPSettings.qml +++ b/src/UI/AppSettings/NTRIPSettings.qml @@ -4,116 +4,322 @@ import QtQuick.Layouts import QGroundControl import QGroundControl.FactControls import QGroundControl.Controls -import QGroundControl.NTRIP 1.0 SettingsPage { - property var _settingsManager: QGroundControl.settingsManager - property var _ntrip: _settingsManager.ntripSettings - property Fact _enabled: _ntrip.ntripServerConnectEnabled + property var _settingsManager: QGroundControl.settingsManager + property var _ntrip: _settingsManager.ntripSettings + property Fact _enabled: _ntrip.ntripServerConnectEnabled + property var _ntripMgr: QGroundControl.ntripManager + property bool _isConnected: _ntripMgr.connectionStatus === NTRIPManager.Connected + property bool _isConnecting: _ntripMgr.connectionStatus === NTRIPManager.Connecting + property bool _isReconnecting: _ntripMgr.connectionStatus === NTRIPManager.Reconnecting + property bool _isError: _ntripMgr.connectionStatus === NTRIPManager.Error + property bool _isActive: _enabled.rawValue + property bool _hasHost: _ntrip.ntripServerHostAddress.rawValue !== "" + property real _textFieldWidth: ScreenTools.defaultFontPixelWidth * 30 SettingsGroupLayout { Layout.fillWidth: true - heading: qsTr("NTRIP / RTK") + heading: qsTr("Connection") visible: _ntrip.visible - FactCheckBoxSlider { + RowLayout { Layout.fillWidth: true - text: _enabled.shortDescription - fact: _enabled - visible: _enabled.visible - } - } + spacing: ScreenTools.defaultFontPixelWidth - SettingsGroupLayout { - Layout.fillWidth: true - visible: _ntrip.ntripServerHostAddress.visible || _ntrip.ntripServerPort.visible || - _ntrip.ntripUsername.visible || _ntrip.ntripPassword.visible || - _ntrip.ntripMountpoint.visible || _ntrip.ntripWhitelist.visible || - _ntrip.ntripUseSpartn.visible - enabled: _enabled.rawValue + Rectangle { + width: ScreenTools.defaultFontPixelHeight * 0.75 + height: width + radius: width / 2 + color: { + switch (_ntripMgr.connectionStatus) { + case NTRIPManager.Connected: return qgcPal.colorGreen + case NTRIPManager.Connecting: + case NTRIPManager.Reconnecting: return qgcPal.colorOrange + case NTRIPManager.Error: return qgcPal.colorRed + default: return qgcPal.colorGrey + } + } + Layout.alignment: Qt.AlignVCenter + } + + QGCLabel { + Layout.fillWidth: true + text: _ntripMgr.statusMessage || qsTr("Disconnected") + wrapMode: Text.WordWrap + } + + QGCButton { + text: { + if (_isConnecting) return qsTr("Connecting…") + if (_isReconnecting) return qsTr("Reconnecting…") + if (_isConnected) return qsTr("Disconnect") + return qsTr("Connect") + } + enabled: !_isConnecting && (_isActive || _hasHost) + onClicked: _enabled.rawValue = !_enabled.rawValue + } + } - // Status line QGCLabel { Layout.fillWidth: true - Layout.minimumHeight: 30 - visible: true + visible: _isConnected && _ntripMgr.messagesReceived > 0 text: { - try { - return NTRIPManager ? (NTRIPManager.ntripStatus || "Disconnected") : "NTRIP Manager not available" - } catch (e) { - return "Disconnected" - } + var parts = [qsTr("%1 messages").arg(_ntripMgr.messagesReceived), + formatBytes(_ntripMgr.bytesReceived)] + if (_ntripMgr.dataRateBytesPerSec > 0) + parts.push(formatRate(_ntripMgr.dataRateBytesPerSec)) + if (_ntripMgr.ggaSource) + parts.push(qsTr("GGA: %1").arg(_ntripMgr.ggaSource)) + return parts.join(" · ") } - wrapMode: Text.WordWrap - color: { - try { - if (!NTRIPManager) return qgcPal.text - var status = NTRIPManager.ntripStatus || "" - if (status.toLowerCase().includes("connected")) return qgcPal.colorGreen - if (status.toLowerCase().includes("connecting")) return qgcPal.colorOrange - if (status.toLowerCase().includes("error") || status.toLowerCase().includes("failed")) return qgcPal.colorRed - return qgcPal.text - } catch (e) { - return qgcPal.text - } + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.colorGrey + + function formatBytes(bytes) { + if (bytes < 1024) return bytes + " B" + if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB" + return (bytes / 1048576).toFixed(1) + " MB" + } + + function formatRate(bytesPerSec) { + if (bytesPerSec < 1024) return bytesPerSec.toFixed(0) + " B/s" + return (bytesPerSec / 1024).toFixed(1) + " KB/s" } } - LabelledFactTextField { + QGCLabel { Layout.fillWidth: true - label: _ntrip.ntripServerHostAddress.shortDescription + visible: _isError && _ntripMgr.statusMessage !== "" + text: _ntripMgr.statusMessage + wrapMode: Text.WordWrap + color: qgcPal.colorRed + font.pointSize: ScreenTools.smallFontPointSize + } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Server") + visible: _ntrip.ntripServerHostAddress.visible || _ntrip.ntripServerPort.visible || + _ntrip.ntripUsername.visible || _ntrip.ntripPassword.visible + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth fact: _ntrip.ntripServerHostAddress - visible: _ntrip.ntripServerHostAddress.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + visible: fact.visible + enabled: !_isActive } LabelledFactTextField { - Layout.fillWidth: true - label: _ntrip.ntripServerPort.shortDescription + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth fact: _ntrip.ntripServerPort - visible: _ntrip.ntripServerPort.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 20 + visible: fact.visible + enabled: !_isActive } LabelledFactTextField { - Layout.fillWidth: true - label: _ntrip.ntripUsername.shortDescription + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription fact: _ntrip.ntripUsername - visible: _ntrip.ntripUsername.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + visible: fact.visible + enabled: !_isActive } - LabelledFactTextField { + RowLayout { Layout.fillWidth: true - label: _ntrip.ntripPassword.shortDescription - fact: _ntrip.ntripPassword visible: _ntrip.ntripPassword.visible - textField.echoMode: TextInput.Password - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 60 + spacing: ScreenTools.defaultFontPixelWidth * 0.5 + + LabelledFactTextField { + id: passwordField + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripPassword + textField.echoMode: _showPassword ? TextInput.Normal : TextInput.Password + enabled: !_isActive + + property bool _showPassword: false + } + + QGCButton { + text: passwordField._showPassword ? qsTr("Hide") : qsTr("Show") + onClicked: passwordField._showPassword = !passwordField._showPassword + Layout.alignment: Qt.AlignBottom + } } - LabelledFactTextField { + FactCheckBoxSlider { Layout.fillWidth: true - label: _ntrip.ntripMountpoint.shortDescription - fact: _ntrip.ntripMountpoint - visible: _ntrip.ntripMountpoint.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40 + text: fact.shortDescription + fact: _ntrip.ntripUseTls + visible: fact.visible + enabled: !_isActive } + } - LabelledFactTextField { + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Mountpoint") + visible: _ntrip.ntripMountpoint.visible + + RowLayout { + Layout.fillWidth: true + spacing: ScreenTools.defaultFontPixelWidth + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripMountpoint + enabled: !_isActive + } + + QGCButton { + text: qsTr("Browse") + enabled: !_isActive && _hasHost && + _ntripMgr.mountpointFetchStatus !== NTRIPManager.FetchInProgress + onClicked: _ntripMgr.fetchMountpoints() + } + } + + QGCLabel { + Layout.fillWidth: true + visible: _ntripMgr.mountpointFetchStatus === NTRIPManager.FetchInProgress + text: qsTr("Fetching mountpoints…") + color: qgcPal.colorOrange + } + + QGCLabel { Layout.fillWidth: true - label: _ntrip.ntripWhitelist.shortDescription + visible: _ntripMgr.mountpointFetchStatus === NTRIPManager.FetchError + text: _ntripMgr.mountpointFetchError + color: qgcPal.colorRed + wrapMode: Text.WordWrap + } + + QGCListView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(contentHeight, ScreenTools.defaultFontPixelHeight * 20) + visible: _ntripMgr.mountpointModel && _ntripMgr.mountpointModel.count > 0 + model: _ntripMgr.mountpointModel + spacing: ScreenTools.defaultFontPixelHeight * 0.25 + + delegate: Rectangle { + required property int index + required property var object + + width: ListView.view.width + height: mountRow.height + ScreenTools.defaultFontPixelHeight * 0.5 + radius: ScreenTools.defaultFontPixelHeight * 0.25 + color: { + if (object.mountpoint === _ntrip.ntripMountpoint.rawValue) + return qgcPal.buttonHighlight + return index % 2 === 0 ? qgcPal.windowShade : qgcPal.window + } + + RowLayout { + id: mountRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: ScreenTools.defaultFontPixelWidth + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + RowLayout { + spacing: ScreenTools.defaultFontPixelWidth + + QGCLabel { + text: object.mountpoint + font.bold: true + color: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qgcPal.buttonHighlightText : qgcPal.text + } + QGCLabel { + visible: object.mountpoint === _ntrip.ntripMountpoint.rawValue + text: qsTr("(selected)") + font.pointSize: ScreenTools.smallFontPointSize + color: qgcPal.buttonHighlightText + } + } + + QGCLabel { + text: { + var parts = [] + if (object.format) parts.push(object.format) + if (object.navSystem) parts.push(object.navSystem) + if (object.country) parts.push(object.country) + if (object.bitrate > 0) parts.push(object.bitrate + " bps") + if (object.distanceKm >= 0) parts.push(object.distanceKm.toFixed(1) + " km") + return parts.join(" · ") + } + font.pointSize: ScreenTools.smallFontPointSize + color: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qgcPal.buttonHighlightText : qgcPal.colorGrey + } + } + + QGCButton { + text: object.mountpoint === _ntrip.ntripMountpoint.rawValue + ? qsTr("Selected") : qsTr("Select") + enabled: object.mountpoint !== _ntrip.ntripMountpoint.rawValue + onClicked: _ntripMgr.selectMountpoint(object.mountpoint) + } + } + } + } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("Options") + visible: _ntrip.ntripWhitelist.visible + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription fact: _ntrip.ntripWhitelist - visible: _ntrip.ntripWhitelist.visible - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 40 + visible: fact.visible + textField.placeholderText: qsTr("e.g. 1005,1077,1087") } + } + + SettingsGroupLayout { + Layout.fillWidth: true + heading: qsTr("UDP Forwarding") + visible: _ntrip.ntripUdpForwardEnabled.visible FactCheckBoxSlider { Layout.fillWidth: true - text: _ntrip.ntripUseSpartn.shortDescription - fact: _ntrip.ntripUseSpartn - visible: _ntrip.ntripUseSpartn.visible - enabled: false + text: fact.shortDescription + fact: _ntrip.ntripUdpForwardEnabled + visible: fact.visible + } + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripUdpTargetAddress + visible: fact.visible + enabled: _ntrip.ntripUdpForwardEnabled.rawValue + } + + LabelledFactTextField { + Layout.fillWidth: true + textFieldPreferredWidth: _textFieldWidth + label: fact.shortDescription + fact: _ntrip.ntripUdpTargetPort + visible: fact.visible + enabled: _ntrip.ntripUdpForwardEnabled.rawValue } } } diff --git a/src/QmlControls/OfflineMapEditor.qml b/src/UI/AppSettings/OfflineMapEditor.qml similarity index 100% rename from src/QmlControls/OfflineMapEditor.qml rename to src/UI/AppSettings/OfflineMapEditor.qml diff --git a/src/QmlControls/OfflineMapInfo.qml b/src/UI/AppSettings/OfflineMapInfo.qml similarity index 100% rename from src/QmlControls/OfflineMapInfo.qml rename to src/UI/AppSettings/OfflineMapInfo.qml diff --git a/src/UI/AppSettings/TelemetrySettings.qml b/src/UI/AppSettings/TelemetrySettings.qml index d2be9037e1ed..53f7ab728dcf 100644 --- a/src/UI/AppSettings/TelemetrySettings.qml +++ b/src/UI/AppSettings/TelemetrySettings.qml @@ -35,47 +35,7 @@ SettingsPage { } } - SettingsGroupLayout { - id: mavlink2SigningGroup - Layout.fillWidth: true - heading: qsTr("MAVLink 2 Signing") - headingDescription: qsTr("Signing keys should only be sent to the vehicle over secure links.") - visible: _mavlink2SigningKey.visible - - property Fact _mavlink2SigningKey: _mavlinkSettings.mavlink2SigningKey - - Connections { - target: mavlink2SigningGroup._mavlink2SigningKey - function onRawValueChanged(value) { sendToVehiclePrompt.visible = true } - } - - RowLayout { - spacing: ScreenTools.defaultFontPixelWidth - - LabelledFactTextField { - Layout.fillWidth: true - textFieldPreferredWidth: ScreenTools.defaultFontPixelWidth * 32 - label: qsTr("Key") - fact: mavlink2SigningGroup._mavlink2SigningKey - } - - QGCButton { - text: qsTr("Send to Vehicle") - enabled: _activeVehicle - - onClicked: { - sendToVehiclePrompt.visible = false - _activeVehicle.sendSetupSigning() - } - } - } - - QGCLabel { - id: sendToVehiclePrompt - Layout.fillWidth: true - text: qsTr("Signing key has changed. Don't forget to send to Vehicle(s) if needed.") - visible: false - } + SigningKeyManager { } SettingsGroupLayout { diff --git a/src/UI/FirstRunPromptDialogs/CMakeLists.txt b/src/UI/FirstRunPromptDialogs/CMakeLists.txt index f2c89fc493c7..822b4bed3791 100644 --- a/src/UI/FirstRunPromptDialogs/CMakeLists.txt +++ b/src/UI/FirstRunPromptDialogs/CMakeLists.txt @@ -5,6 +5,7 @@ qt_add_qml_module(FirstRunPromptDialogsModule VERSION 1.0 RESOURCE_PREFIX /qml QML_FILES + FirstRunPrompt.qml OfflineVehicleFirstRunPrompt.qml UnitsFirstRunPrompt.qml NO_PLUGIN diff --git a/src/QmlControls/FirstRunPrompt.qml b/src/UI/FirstRunPromptDialogs/FirstRunPrompt.qml similarity index 100% rename from src/QmlControls/FirstRunPrompt.qml rename to src/UI/FirstRunPromptDialogs/FirstRunPrompt.qml diff --git a/src/UI/MainWindow.qml b/src/UI/MainWindow.qml index 8d43c719df16..80e0d28592da 100644 --- a/src/UI/MainWindow.qml +++ b/src/UI/MainWindow.qml @@ -9,6 +9,7 @@ import QGroundControl.Controls import QGroundControl.FactControls import QGroundControl.FlyView import QGroundControl.FlightMap +import QGroundControl.PlanView import QGroundControl.Toolbar /// @brief Native QML top level window @@ -213,9 +214,9 @@ ApplicationWindow { property string closeDialogTitle: qsTr("Close %1").arg(QGroundControl.appName) function checkForUnsavedMission() { - if (planView._planMasterController.dirty) { + if (planView._planMasterController.dirtyForSave || planView._planMasterController.dirtyForUpload) { QGroundControl.showMessageDialog(mainWindow, closeDialogTitle, - qsTr("You have a mission edit in progress which has not been saved/sent. If you close you will lose changes. Are you sure you want to close?"), + qsTr("You have a mission edit in progress which has not been saved/uploaded. If you close you will lose changes. Are you sure you want to close?"), Dialog.Yes | Dialog.No, function() { _closeChecksToSkip |= _skipUnsavedMissionCheckMask; performCloseChecks() }) return false @@ -607,7 +608,7 @@ ApplicationWindow { // to mainWindow. Otherwise if they are rooted to the AnalyzeView itself they will die when the analyze viewSwitch // closes. - function createrWindowedAnalyzePage(title, source) { + function createWindowedAnalyzePage(title, source) { var windowedPage = windowedAnalyzePage.createObject(mainWindow) windowedPage.title = title windowedPage.source = source diff --git a/src/QmlControls/BatteryIndicator.qml b/src/UI/toolbar/BatteryIndicator.qml similarity index 99% rename from src/QmlControls/BatteryIndicator.qml rename to src/UI/toolbar/BatteryIndicator.qml index 5a97d42a9774..62ca564c673e 100644 --- a/src/QmlControls/BatteryIndicator.qml +++ b/src/UI/toolbar/BatteryIndicator.qml @@ -328,7 +328,7 @@ Item { QGCLabel { Layout.alignment: Qt.AlignHCenter verticalAlignment: Text.AlignVCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: getBatteryPercentageText() font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize visible: _showBoth || _showPercentage @@ -337,7 +337,7 @@ Item { QGCLabel { Layout.alignment: Qt.AlignHCenter font.pointSize: _showBoth ? ScreenTools.defaultFontPointSize : ScreenTools.mediumFontPointSize - color: qgcPal.windowTransparentText + color: qgcPal.text text: getBatteryVoltageText() visible: _showBoth || _showVoltage } diff --git a/src/UI/toolbar/CMakeLists.txt b/src/UI/toolbar/CMakeLists.txt index 547d24baecca..72e71133b05d 100644 --- a/src/UI/toolbar/CMakeLists.txt +++ b/src/UI/toolbar/CMakeLists.txt @@ -6,19 +6,34 @@ qt_add_qml_module(ToolbarModule RESOURCE_PREFIX /qml QML_FILES ArmedIndicator.qml + BatteryIndicator.qml EscIndicator.qml EscIndicatorPage.qml + FlightModeIndicator.qml + FlightModeMenuIndicator.qml + FlyViewToolBar.qml + FlyViewToolBarIndicators.qml GCSControlIndicator.qml GimbalIndicator.qml + GPSIndicator.qml + GPSIndicatorPage.qml GPSResilienceIndicator.qml JoystickIndicator.qml + MainStatusIndicator.qml + MainStatusIndicatorOfflinePage.qml ModeIndicator.qml MultiVehicleSelector.qml + ParameterDownloadProgress.qml + PlanViewToolBar.qml RCRSSIIndicator.qml RemoteIDIndicator.qml + RemoteIDIndicatorPage.qml RTKGPSIndicator.qml SelectViewDropdown.qml + SignalStrength.qml + SigningIndicator.qml TelemetryRSSIIndicator.qml VehicleGPSIndicator.qml + VehicleMessageList.qml NO_PLUGIN ) diff --git a/src/UI/toolbar/EscIndicator.qml b/src/UI/toolbar/EscIndicator.qml index 7e18d0e52198..ff980ef5b145 100644 --- a/src/UI/toolbar/EscIndicator.qml +++ b/src/UI/toolbar/EscIndicator.qml @@ -73,7 +73,7 @@ Item { source: "/qmlimages/EscIndicator.svg" fillMode: Image.PreserveAspectFit sourceSize.height: height - color: qgcPal.windowTransparentText + color: qgcPal.text } Column { @@ -83,7 +83,7 @@ Item { QGCLabel { anchors.horizontalCenter: parent.horizontalCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: _onlineMotorCount.toString() font.pointSize: ScreenTools.smallFontPointSize } diff --git a/src/QmlControls/FlightModeIndicator.qml b/src/UI/toolbar/FlightModeIndicator.qml similarity index 98% rename from src/QmlControls/FlightModeIndicator.qml rename to src/UI/toolbar/FlightModeIndicator.qml index b7b0cd93d0c2..7abfb380fb06 100644 --- a/src/QmlControls/FlightModeIndicator.qml +++ b/src/UI/toolbar/FlightModeIndicator.qml @@ -35,14 +35,14 @@ Item { Layout.preferredHeight: ScreenTools.defaultFontPixelHeight fillMode: Image.PreserveAspectFit mipmap: true - color: qgcPal.windowTransparentText + color: qgcPal.text source: "/qmlimages/FlightModesComponentIcon.png" } QGCLabel { id: flightModeLabel text: activeVehicle ? activeVehicle.flightMode : qsTr("N/A", "No data to display") - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: fontPointSize } diff --git a/src/QmlControls/FlightModeMenuIndicator.qml b/src/UI/toolbar/FlightModeMenuIndicator.qml similarity index 100% rename from src/QmlControls/FlightModeMenuIndicator.qml rename to src/UI/toolbar/FlightModeMenuIndicator.qml diff --git a/src/QmlControls/FlyViewToolBar.qml b/src/UI/toolbar/FlyViewToolBar.qml similarity index 100% rename from src/QmlControls/FlyViewToolBar.qml rename to src/UI/toolbar/FlyViewToolBar.qml diff --git a/src/QmlControls/FlyViewToolBarIndicators.qml b/src/UI/toolbar/FlyViewToolBarIndicators.qml similarity index 100% rename from src/QmlControls/FlyViewToolBarIndicators.qml rename to src/UI/toolbar/FlyViewToolBarIndicators.qml diff --git a/src/QmlControls/GPSIndicator.qml b/src/UI/toolbar/GPSIndicator.qml similarity index 90% rename from src/QmlControls/GPSIndicator.qml rename to src/UI/toolbar/GPSIndicator.qml index 6f65c67e0b2a..347f800b196e 100644 --- a/src/QmlControls/GPSIndicator.qml +++ b/src/UI/toolbar/GPSIndicator.qml @@ -32,7 +32,7 @@ Item { id: gpsLabel rotation: 90 text: qsTr("RTK") - color: qgcPal.windowTransparentText + color: qgcPal.text anchors.verticalCenter: parent.verticalCenter visible: _rtkConnected } @@ -46,7 +46,7 @@ Item { fillMode: Image.PreserveAspectFit sourceSize.height: height opacity: (_activeVehicle && _activeVehicle.gps.count.value >= 0) ? 1 : 0.5 - color: qgcPal.windowTransparentText + color: qgcPal.text } } @@ -58,13 +58,13 @@ Item { QGCLabel { anchors.horizontalCenter: hdopValue.horizontalCenter - color: qgcPal.windowTransparentText + color: qgcPal.text text: _activeVehicle ? _activeVehicle.gps.count.valueString : "" } QGCLabel { id: hdopValue - color: qgcPal.windowTransparentText + color: qgcPal.text text: _activeVehicle ? _activeVehicle.gps.hdop.value.toFixed(1) : "" } } diff --git a/src/QmlControls/GPSIndicatorPage.qml b/src/UI/toolbar/GPSIndicatorPage.qml similarity index 100% rename from src/QmlControls/GPSIndicatorPage.qml rename to src/UI/toolbar/GPSIndicatorPage.qml diff --git a/src/UI/toolbar/GimbalIndicator.qml b/src/UI/toolbar/GimbalIndicator.qml index 95763056c564..01bd5fc278ed 100644 --- a/src/UI/toolbar/GimbalIndicator.qml +++ b/src/UI/toolbar/GimbalIndicator.qml @@ -52,7 +52,7 @@ Item { source: "/res/CameraGimbal.png" fillMode: Image.PreserveAspectFit sourceSize.height: height - color: qgcPal.windowTransparentText + color: qgcPal.text } @@ -61,7 +61,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter font.pointSize: ScreenTools.smallFontPointSize text: activeGimbal ? activeGimbal.deviceId.rawValue : "" - color: qgcPal.windowTransparentText + color: qgcPal.text visible: multiGimbalSetup } } @@ -80,7 +80,7 @@ Item { text: activeGimbal && activeGimbal.retracted ? qsTr("Retracted") : (activeGimbal && activeGimbal.yawLock ? qsTr("Yaw locked") : qsTr("Yaw follow")) - color: qgcPal.windowTransparentText + color: qgcPal.text Layout.columnSpan: 2 Layout.alignment: Qt.AlignHCenter } @@ -88,7 +88,7 @@ Item { id: pitchLabel font.pointSize: ScreenTools.smallFontPointSize text: activeGimbal ? qsTr("P: ") + activeGimbal.absolutePitch.valueString : "" - color: qgcPal.windowTransparentText + color: qgcPal.text } QGCLabel { id: panLabel @@ -98,7 +98,7 @@ Item { (qsTr("Az: ") + activeGimbal.absoluteYaw.valueString) : (qsTr("Y: ") + activeGimbal.bodyYaw.valueString)) : "" - color: qgcPal.windowTransparentText + color: qgcPal.text } } } @@ -235,31 +235,32 @@ Item { id: enableOnScreenControlCheckbox Layout.fillWidth: true text: qsTr("Enabled") - fact: _gimbalControllerSettings.EnableOnScreenControl + fact: _gimbalControllerSettings.enableOnScreenControl } - LabelledFactComboBox { - label: qsTr("Control type") - fact: _gimbalControllerSettings.ControlType - visible: enableOnScreenControlCheckbox.checked + FactCheckBoxSlider { + Layout.fillWidth: true + text: qsTr("Click and drag") + fact: _gimbalControllerSettings.clickAndDrag + visible: enableOnScreenControlCheckbox.checked } LabelledFactTextField { label: qsTr("Horizontal FOV") - fact: _gimbalControllerSettings.CameraHFov - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 0 + fact: _gimbalControllerSettings.cameraHFov + visible: enableOnScreenControlCheckbox.checked && !_gimbalControllerSettings.clickAndDrag.rawValue } LabelledFactTextField { label: qsTr("Vertical FOV") - fact: _gimbalControllerSettings.CameraVFov - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 0 + fact: _gimbalControllerSettings.cameraVFov + visible: enableOnScreenControlCheckbox.checked && !_gimbalControllerSettings.clickAndDrag.rawValue } LabelledFactTextField { label: qsTr("Max speed") - fact: _gimbalControllerSettings.CameraSlideSpeed - visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.ControlType.rawValue === 1 + fact: _gimbalControllerSettings.cameraSlideSpeed + visible: enableOnScreenControlCheckbox.checked && _gimbalControllerSettings.clickAndDrag.rawValue } } diff --git a/src/QmlControls/MainStatusIndicator.qml b/src/UI/toolbar/MainStatusIndicator.qml similarity index 99% rename from src/QmlControls/MainStatusIndicator.qml rename to src/UI/toolbar/MainStatusIndicator.qml index 92f57fea9d9a..003d924073ba 100644 --- a/src/QmlControls/MainStatusIndicator.qml +++ b/src/UI/toolbar/MainStatusIndicator.qml @@ -31,7 +31,7 @@ RowLayout { Layout.preferredWidth: contentWidth + (vehicleMessagesIcon.visible ? vehicleMessagesIcon.width + control.spacing : 0) verticalAlignment: Text.AlignVCenter text: mainStatusText() - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: ScreenTools.largeFontPointSize property string _commLostText: qsTr("Comms Lost") @@ -120,7 +120,7 @@ RowLayout { visible: _activeVehicle && _activeVehicle.messageCount > 0 function getIconColor() { - let iconColor = qgcPal.windowTransparentText + let iconColor = qgcPal.text if (_activeVehicle) { if (_activeVehicle.messageTypeWarning) { iconColor = qgcPal.colorOrange @@ -143,7 +143,7 @@ RowLayout { Layout.fillHeight: true verticalAlignment: Text.AlignVCenter text: _vtolInFWDFlight ? qsTr("FW(vtol)") : qsTr("MR(vtol)") - color: qgcPal.windowTransparentText + color: qgcPal.text font.pointSize: _vehicleInAir ? ScreenTools.largeFontPointSize : ScreenTools.defaultFontPointSize visible: _activeVehicle && _activeVehicle.vtol diff --git a/src/QmlControls/MainStatusIndicatorOfflinePage.qml b/src/UI/toolbar/MainStatusIndicatorOfflinePage.qml similarity index 100% rename from src/QmlControls/MainStatusIndicatorOfflinePage.qml rename to src/UI/toolbar/MainStatusIndicatorOfflinePage.qml diff --git a/src/QmlControls/ParameterDownloadProgress.qml b/src/UI/toolbar/ParameterDownloadProgress.qml similarity index 100% rename from src/QmlControls/ParameterDownloadProgress.qml rename to src/UI/toolbar/ParameterDownloadProgress.qml diff --git a/src/QmlControls/PlanViewToolBar.qml b/src/UI/toolbar/PlanViewToolBar.qml similarity index 96% rename from src/QmlControls/PlanViewToolBar.qml rename to src/UI/toolbar/PlanViewToolBar.qml index 60a20f04fc06..62013c92346a 100644 --- a/src/QmlControls/PlanViewToolBar.qml +++ b/src/UI/toolbar/PlanViewToolBar.qml @@ -5,6 +5,7 @@ import QtQuick.Dialogs import QGroundControl import QGroundControl.Controls +import QGroundControl.PlanView Rectangle { id: _root @@ -13,6 +14,7 @@ Rectangle { color: qgcPal.toolbarBackground property var planMasterController + property bool showRallyPointsHelp: false property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle property real _controllerProgressPct: planMasterController.missionController.progressPct @@ -52,6 +54,7 @@ Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom planMasterController: _root.planMasterController + showRallyPointsHelp: _root.showRallyPointsHelp } } diff --git a/src/UI/toolbar/RCRSSIIndicator.qml b/src/UI/toolbar/RCRSSIIndicator.qml index a310fa5a0640..44b037e32fb2 100644 --- a/src/UI/toolbar/RCRSSIIndicator.qml +++ b/src/UI/toolbar/RCRSSIIndicator.qml @@ -12,7 +12,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - property bool showIndicator: _activeVehicle.supportsRadio && _rcRSSIAvailable + property bool showIndicator: _activeVehicle.supports.radio && _rcRSSIAvailable property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle property bool _rcRSSIAvailable: _activeVehicle.rcRSSI > 0 && _activeVehicle.rcRSSI <= 100 diff --git a/src/QmlControls/RemoteIDIndicatorPage.qml b/src/UI/toolbar/RemoteIDIndicatorPage.qml similarity index 100% rename from src/QmlControls/RemoteIDIndicatorPage.qml rename to src/UI/toolbar/RemoteIDIndicatorPage.qml diff --git a/src/QmlControls/SignalStrength.qml b/src/UI/toolbar/SignalStrength.qml similarity index 100% rename from src/QmlControls/SignalStrength.qml rename to src/UI/toolbar/SignalStrength.qml diff --git a/src/UI/toolbar/SigningIndicator.qml b/src/UI/toolbar/SigningIndicator.qml new file mode 100644 index 000000000000..d30b8430edcb --- /dev/null +++ b/src/UI/toolbar/SigningIndicator.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import QGroundControl +import QGroundControl.Controls + +Item { + id: control + width: signingIcon.width * 1.1 + anchors.top: parent.top + anchors.bottom: parent.bottom + + property bool showIndicator: QGroundControl.mavlinkSigningKeys.keys.count > 0 + + property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle + property bool _signed: _activeVehicle ? _activeVehicle.mavlinkSigning : false + + QGCPalette { id: qgcPal } + + QGCColoredImage { + id: signingIcon + anchors.top: parent.top + anchors.bottom: parent.bottom + width: height + sourceSize.height: height + source: _signed ? "/InstrumentValueIcons/lock-closed.svg" : "/InstrumentValueIcons/lock-open.svg" + fillMode: Image.PreserveAspectFit + color: _signed ? qgcPal.colorGreen : qgcPal.text + } + + MouseArea { + anchors.fill: parent + onClicked: mainWindow.showIndicatorDrawer(signingInfoPage, control) + } + + Component { + id: signingInfoPage + + ToolIndicatorPage { + showExpand: true + contentComponent: signingContentComponent + expandedComponent: signingExpandedComponent + } + } + + Component { + id: signingContentComponent + + SettingsGroupLayout { + heading: qsTr("MAVLink Signing") + + LabelledLabel { + label: qsTr("Status:") + labelText: { + if (!_activeVehicle) + return qsTr("No Vehicle") + return _signed ? qsTr("Active") : qsTr("Inactive") + } + } + + LabelledLabel { + label: qsTr("Active Key:") + labelText: _activeVehicle && _activeVehicle.mavlinkSigningKeyName !== "" ? _activeVehicle.mavlinkSigningKeyName : qsTr("None") + visible: _activeVehicle + } + + LabelledLabel { + label: qsTr("Saved Keys:") + labelText: QGroundControl.mavlinkSigningKeys.keys.count + } + } + } + + Component { + id: signingExpandedComponent + + SigningKeyManager { + } + } +} diff --git a/src/QmlControls/VehicleMessageList.qml b/src/UI/toolbar/VehicleMessageList.qml similarity index 100% rename from src/QmlControls/VehicleMessageList.qml rename to src/UI/toolbar/VehicleMessageList.qml diff --git a/src/Utilities/QGCLogging.cc b/src/Utilities/QGCLogging.cc index 442a8f64af15..3aaca642ab62 100644 --- a/src/Utilities/QGCLogging.cc +++ b/src/Utilities/QGCLogging.cc @@ -6,15 +6,26 @@ #include #include +#include +#include #include #include +#include + QGC_LOGGING_CATEGORY(QGCLoggingLog, "Utilities.QGCLogging") Q_GLOBAL_STATIC(QGCLogging, _qgcLogging) static QtMessageHandler defaultHandler = nullptr; +// --------------------------------------------------------------------------- +// Test‐log‐capture storage (thread‐safe, lightweight when disabled) +// --------------------------------------------------------------------------- +static std::atomic s_captureEnabled{false}; +static QMutex s_captureMutex; +static QList s_capturedMessages; + static void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { // Call the previous handler FIRST to ensure QTest::ignoreMessage works correctly. @@ -29,6 +40,9 @@ static void msgHandler(QtMsgType type, const QMessageLogContext &context, const // Filter out Qt Quick internals if (QGCLogging::instance() && !QString(context.category).startsWith("qt.quick")) { + // Capture for unit-test introspection + QGCLogging::captureIfEnabled(type, context, msg); + QGCLogging::instance()->log(message); } } @@ -71,6 +85,84 @@ void QGCLogging::installHandler() defaultHandler = qInstallMessageHandler(msgHandler); } +// --------------------------------------------------------------------------- +// Test‑log‑capture API +// --------------------------------------------------------------------------- + +void QGCLogging::setCaptureEnabled(bool enabled) +{ + s_captureEnabled.store(enabled, std::memory_order_relaxed); +} + +void QGCLogging::clearCapturedMessages() +{ + const QMutexLocker locker(&s_captureMutex); + s_capturedMessages.clear(); +} + +QList QGCLogging::capturedMessages(const QString &category) +{ + const QMutexLocker locker(&s_captureMutex); + + if (category.isEmpty()) { + return s_capturedMessages; + } + + QList filtered; + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category == category) { + filtered.append(msg); + } + } + return filtered; +} + +bool QGCLogging::hasCapturedMessage(const QString &category, QtMsgType type) +{ + const QMutexLocker locker(&s_captureMutex); + + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category == category && msg.type == type) { + return true; + } + } + return false; +} + +bool QGCLogging::hasCapturedWarning(const QString &category) +{ + return hasCapturedMessage(category, QtWarningMsg); +} + +bool QGCLogging::hasCapturedCritical(const QString &category) +{ + return hasCapturedMessage(category, QtCriticalMsg); +} + +bool QGCLogging::hasCapturedUncategorizedMessage() +{ + const QMutexLocker locker(&s_captureMutex); + + for (const auto &msg : std::as_const(s_capturedMessages)) { + if (msg.category.isEmpty() || msg.category == QStringLiteral("default")) { + return true; + } + } + return false; +} + +void QGCLogging::captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + if (!s_captureEnabled.load(std::memory_order_relaxed)) { + return; + } + + const QMutexLocker locker(&s_captureMutex); + s_capturedMessages.append({type, + context.category ? QString::fromLatin1(context.category) : QString(), + msg}); +} + void QGCLogging::log(const QString &message) { // Emit the signal so threadsafeLog runs in the correct thread diff --git a/src/Utilities/QGCLogging.h b/src/Utilities/QGCLogging.h index 9262c473c141..74af2e481f76 100644 --- a/src/Utilities/QGCLogging.h +++ b/src/Utilities/QGCLogging.h @@ -1,12 +1,22 @@ #pragma once #include +#include #include +#include #include #include Q_DECLARE_LOGGING_CATEGORY(QGCLoggingLog) +/// A single captured log message for test introspection. +struct CapturedLogMessage +{ + QtMsgType type = QtDebugMsg; + QString category; + QString message; +}; + class QGCLogging : public QStringListModel { Q_OBJECT @@ -27,6 +37,39 @@ class QGCLogging : public QStringListModel /// Enqueue a log message (thread-safe) void log(const QString &message); + // ======================================================================== + // Test Log Capture + // ======================================================================== + + /// Enable or disable log capture (for unit testing). + /// When enabled, every message passing through the Qt message handler is + /// recorded with its category, type and raw text. + static void setCaptureEnabled(bool enabled); + + /// Discard all previously captured messages. + static void clearCapturedMessages(); + + /// Return captured messages, optionally filtered to a single category. + /// Pass an empty string to retrieve all messages. + [[nodiscard]] static QList capturedMessages(const QString &category = {}); + + /// Return true if at least one message of the given @a type was captured + /// for @a category. + [[nodiscard]] static bool hasCapturedMessage(const QString &category, QtMsgType type); + + /// Convenience: captured warning in @a category? + [[nodiscard]] static bool hasCapturedWarning(const QString &category); + + /// Convenience: captured critical in @a category? + [[nodiscard]] static bool hasCapturedCritical(const QString &category); + + /// Return true if any uncategorized message was captured (e.g. raw qDebug/qWarning). + [[nodiscard]] static bool hasCapturedUncategorizedMessage(); + + /// Record a message into the capture buffer (if capture is enabled). + /// Intended for external handler wrappers (e.g. the unit-test capture handler). + static void captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg); + signals: /// Emitted when a log message is enqueued void emitLog(const QString &message); diff --git a/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc b/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc index a8f18f372f33..9aa1b8216354 100644 --- a/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc +++ b/src/Utilities/StateMachine/Helpers/StateMachineProfiler.cc @@ -67,18 +67,23 @@ QString StateMachineProfiler::summary() const return a.totalTimeMs > b.totalTimeMs; }); - lines << QStringLiteral("%-30s %8s %10s %10s %10s %10s") - .arg("State", "Count", "Total(ms)", "Avg(ms)", "Min(ms)", "Max(ms)"); + lines << QStringLiteral("%1 %2 %3 %4 %5 %6") + .arg("State", -30) + .arg("Count", 8) + .arg("Total(ms)", 10) + .arg("Avg(ms)", 10) + .arg("Min(ms)", 10) + .arg("Max(ms)", 10); lines << QString(80, '-'); for (const auto& p : sortedProfiles) { - lines << QStringLiteral("%-30s %8d %10lld %10.1f %10lld %10lld") - .arg(p.name.left(30)) - .arg(p.entryCount) - .arg(p.totalTimeMs) - .arg(p.averageTimeMs()) - .arg(p.minTimeMs == std::numeric_limits::max() ? 0 : p.minTimeMs) - .arg(p.maxTimeMs); + lines << QStringLiteral("%1 %2 %3 %4 %5 %6") + .arg(p.name.left(30), -30) + .arg(p.entryCount, 8) + .arg(p.totalTimeMs, 10) + .arg(p.averageTimeMs(), 10, 'f', 1) + .arg(p.minTimeMs == std::numeric_limits::max() ? 0 : p.minTimeMs, 10) + .arg(p.maxTimeMs, 10); } return lines.join('\n'); diff --git a/src/Utilities/StateMachine/QGCStateMachine.cc b/src/Utilities/StateMachine/QGCStateMachine.cc index 6f764dc735db..88215eb91314 100644 --- a/src/Utilities/StateMachine/QGCStateMachine.cc +++ b/src/Utilities/StateMachine/QGCStateMachine.cc @@ -63,7 +63,7 @@ QString QGCStateMachine::currentStateName() const void QGCStateMachine::start() { if (isRunning()) { - qCWarning(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections"; + qCCritical(QGCStateMachineLog) << objectName() << "start() called but already running - check signal connections"; } QStateMachine::start(); } @@ -318,13 +318,13 @@ void QGCStateMachine::clearError(bool restart) bool QGCStateMachine::resetToState(QAbstractState* state) { if (!state) { - qCWarning(QGCStateMachineLog) << objectName() << "resetToState: null state"; + qCCritical(QGCStateMachineLog) << objectName() << "resetToState: null state"; return false; } // Verify the state belongs to this machine if (state->parentState() != this && state->parent() != this) { - qCWarning(QGCStateMachineLog) << objectName() << "resetToState: state does not belong to this machine"; + qCCritical(QGCStateMachineLog) << objectName() << "resetToState: state does not belong to this machine"; return false; } diff --git a/src/Utilities/StateMachine/States/AsyncFunctionState.cc b/src/Utilities/StateMachine/States/AsyncFunctionState.cc index deb5a30c275f..16d792490941 100644 --- a/src/Utilities/StateMachine/States/AsyncFunctionState.cc +++ b/src/Utilities/StateMachine/States/AsyncFunctionState.cc @@ -27,7 +27,7 @@ void AsyncFunctionState::onWaitEntered() } if (!_completionConnection && timeoutMsecs() == 0) { - qCWarning(QGCStateMachineLog) << stateName() + qCCritical(QGCStateMachineLog) << stateName() << "has no completion connection and no timeout - state may hang indefinitely"; } } diff --git a/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc b/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc index 4a020e6e167e..6c0d5ec46675 100644 --- a/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc +++ b/src/Utilities/StateMachine/States/SendMavlinkCommandState.cc @@ -42,7 +42,7 @@ void SendMavlinkCommandState::disconnectWaitSignal() void SendMavlinkCommandState::onWaitEntered() { if (!_configured) { - qCWarning(QGCStateMachineLog) << "SendMavlinkCommandState not configured"; + qCCritical(QGCStateMachineLog) << "SendMavlinkCommandState not configured"; waitFailed(); return; } diff --git a/src/Utilities/StateMachine/States/SubMachineState.cc b/src/Utilities/StateMachine/States/SubMachineState.cc index ce347e74821b..5277a65600c5 100644 --- a/src/Utilities/StateMachine/States/SubMachineState.cc +++ b/src/Utilities/StateMachine/States/SubMachineState.cc @@ -13,14 +13,14 @@ void SubMachineState::onEntry(QEvent* event) QGCState::onEntry(event); if (!_factory) { - qCWarning(QGCStateMachineLog) << stateName() << "no machine factory provided"; + qCCritical(QGCStateMachineLog) << stateName() << "no machine factory provided"; emit error(); return; } _childMachine = _factory(this); if (!_childMachine) { - qCWarning(QGCStateMachineLog) << stateName() << "factory returned null machine"; + qCCritical(QGCStateMachineLog) << stateName() << "factory returned null machine"; emit error(); return; } diff --git a/src/Vehicle/CMakeLists.txt b/src/Vehicle/CMakeLists.txt index 93d8016584d6..1da3b700f5f0 100644 --- a/src/Vehicle/CMakeLists.txt +++ b/src/Vehicle/CMakeLists.txt @@ -31,6 +31,8 @@ target_sources(${CMAKE_PROJECT_NAME} VehicleLinkManager.h VehicleObjectAvoidance.cc VehicleObjectAvoidance.h + VehicleSupports.cc + VehicleSupports.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/src/Vehicle/ComponentInformation/CompInfoGeneral.cc b/src/Vehicle/ComponentInformation/CompInfoGeneral.cc index f362e68be851..18483e8907b6 100644 --- a/src/Vehicle/ComponentInformation/CompInfoGeneral.cc +++ b/src/Vehicle/ComponentInformation/CompInfoGeneral.cc @@ -1,4 +1,5 @@ #include "CompInfoGeneral.h" +#include "ComponentInformationManager.h" #include "JsonHelper.h" #include "JsonParsing.h" #include "QGCLoggingCategory.h" @@ -72,7 +73,7 @@ void CompInfoGeneral::setJson(const QString& metadataJsonFileName) if (uris.uriMetaData.isEmpty() || !uris.crcMetaDataValid) { // The CRC is optional for dynamically updated metadata, and once we want to support that this logic needs // to be updated. - qCDebug(CompInfoGeneralLog) << "Metadata missing fields: type:uri:crcValid" << metadataType << + qCDebug(ComponentInformationManagerLog) << "Metadata missing fields: type:uri:crcValid" << metadataType << uris.uriMetaData << uris.crcMetaDataValid; continue; } diff --git a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc index a141ac81616d..cd5a512d8832 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationManager.cc +++ b/src/Vehicle/ComponentInformation/ComponentInformationManager.cc @@ -21,6 +21,7 @@ ComponentInformationManager::ComponentInformationManager(Vehicle *vehicle, QObje , _translation(new ComponentInformationTranslation(this, _cachedFileDownload)) { qCDebug(ComponentInformationManagerLog) << this; + qCDebug(ComponentInformationManagerLog) << "Cache location:" << QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/QGCCompInfoCache"); _compInfoMap[MAV_COMP_ID_AUTOPILOT1][COMP_METADATA_TYPE_GENERAL] = new CompInfoGeneral (MAV_COMP_ID_AUTOPILOT1, vehicle, this); _compInfoMap[MAV_COMP_ID_AUTOPILOT1][COMP_METADATA_TYPE_PARAMETER] = new CompInfoParam (MAV_COMP_ID_AUTOPILOT1, vehicle, this); @@ -178,7 +179,13 @@ void ComponentInformationManager::requestAllComponentInformation(RequestAllCompl _requestAllCompleteFn = requestAllCompletFn; _requestAllCompleteFnData = requestAllCompleteFnData; - start(); + // Guard against double-start: when InitialConnectStateMachine's CompInfo + // state times out, the retry callback re-invokes this method while the CIM + // state machine is still running. Only start if not already in progress; + // the updated callback pointers above are sufficient for the retry path. + if (!isRunning()) { + start(); + } emit progressUpdate(progress()); } diff --git a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc index f7160ca1c5c3..bd2d14d5266a 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc +++ b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.cc @@ -19,12 +19,18 @@ ComponentInformationTranslation::ComponentInformationTranslation(QObject* parent } bool ComponentInformationTranslation::downloadAndTranslate(const QString& summaryJsonFile, - const QString& toTranslateJsonFile, int maxCacheAgeSec) + const QString& toTranslateJsonFile, int maxCacheAgeSec, const QString& componentName) { + // Metadata is authored in English, no translation needed + const QString locale = QLocale::system().name(); + if (locale.startsWith(QLatin1String("en"))) { + qCDebug(ComponentInformationTranslationLog) << "Skipping translation for English locale" << locale << "for" << componentName; + return false; + } + // Parse summary: find url for current locale _toTranslateJsonFile = toTranslateJsonFile; - QString locale = QLocale::system().name(); - QString url = getUrlFromSummaryJson(summaryJsonFile, locale); + QString url = getUrlFromSummaryJson(summaryJsonFile, locale, componentName); if (url.isEmpty()) { return false; } @@ -39,26 +45,26 @@ bool ComponentInformationTranslation::downloadAndTranslate(const QString& summar return true; } -QString ComponentInformationTranslation::getUrlFromSummaryJson(const QString &summaryJsonFile, const QString &locale) +QString ComponentInformationTranslation::getUrlFromSummaryJson(const QString &summaryJsonFile, const QString &locale, const QString &componentName) { QString errorString; QJsonDocument jsonDoc; if (!JsonParsing::isJsonFile(summaryJsonFile, jsonDoc, errorString)) { - qCWarning(ComponentInformationTranslationLog) << "Metadata translation summary json file open failed:" << errorString; + qCWarning(ComponentInformationTranslationLog) << "Metadata translation summary json file open failed for" << componentName << ":" << errorString; return ""; } QJsonObject jsonObj = jsonDoc.object(); QJsonObject localeObj = jsonObj[locale].toObject(); if (localeObj.isEmpty()) { - qCWarning(ComponentInformationTranslationLog) << "Locale " << locale << " not found in json"; + qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "not found in translation json for" << componentName; return ""; } QString url = localeObj["url"].toString(); if (url.isEmpty()) { - qCWarning(ComponentInformationTranslationLog) << "Locale " << locale << ": no url set"; + qCWarning(ComponentInformationTranslationLog) << "Locale" << locale << "has no url in translation json for" << componentName; } return url; } @@ -98,7 +104,7 @@ void ComponentInformationTranslation::onDownloadCompleted(bool success, const QS QString ComponentInformationTranslation::translateJsonUsingTS(const QString &toTranslateJsonFile, const QString &tsFile) { - qCInfo(ComponentInformationTranslationLog) << "Translating" << toTranslateJsonFile << "using" << tsFile; + qCDebug(ComponentInformationTranslationLog) << "Translating" << toTranslateJsonFile << "using" << tsFile; // Open JSON and get the 'translation' object QString errorString; @@ -200,7 +206,7 @@ QString ComponentInformationTranslation::translateJsonUsingTS(const QString &toT translatedFile.write(jsonDoc.toJson()); translatedFile.close(); - qCInfo(ComponentInformationTranslationLog) << "JSON file" << toTranslateJsonFile << "successfully translated to" << translatedFileName; + qCDebug(ComponentInformationTranslationLog) << "JSON file" << toTranslateJsonFile << "successfully translated to" << translatedFileName; return translatedFileName; } diff --git a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h index 94c853df28ef..480ec25a407f 100644 --- a/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h +++ b/src/Vehicle/ComponentInformation/ComponentInformationTranslation.h @@ -26,7 +26,7 @@ class ComponentInformationTranslation : public QObject /// @param toTranslateJsonFile json file to be translated /// @param maxCacheAgeSec Maximum age of cached item in seconds /// @return true: Asynchronous download has started, false: Download initialization failed - bool downloadAndTranslate(const QString& summaryJsonFile, const QString& toTranslateJsonFile, int maxCacheAgeSec); + bool downloadAndTranslate(const QString& summaryJsonFile, const QString& toTranslateJsonFile, int maxCacheAgeSec, const QString& componentName); QString translateJsonUsingTS(const QString& toTranslateJsonFile, const QString& tsFile); @@ -36,7 +36,7 @@ class ComponentInformationTranslation : public QObject private slots: void onDownloadCompleted(bool success, const QString &localFile, QString errorMsg, bool fromCache); private: - QString getUrlFromSummaryJson(const QString& summaryJsonFile, const QString& locale); + QString getUrlFromSummaryJson(const QString& summaryJsonFile, const QString& locale, const QString& componentName); static QJsonObject translate(const QJsonObject& translationObj, const QHash& translations, QJsonObject doc); diff --git a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc index c388dc90c039..04e7ad3b8597 100644 --- a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc +++ b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.cc @@ -168,14 +168,17 @@ void RequestMetaDataTypeStateMachine::_wireTimeoutHandling() void RequestMetaDataTypeStateMachine::request(CompInfo* compInfo) { + _compInfo = compInfo; qCDebug(RequestMetaDataTypeStateMachineLog) << Q_FUNC_INFO << typeToString(); - _compInfo = compInfo; _jsonMetadataFileName.clear(); _jsonMetadataTranslatedFileName.clear(); _jsonTranslationFileName.clear(); _activeAsyncState = nullptr; _activeSkippableState = nullptr; + _metadataSource = MetadataSource::None; + _metadataUri.clear(); + _metadataIsFallback = false; start(); } @@ -335,14 +338,17 @@ void RequestMetaDataTypeStateMachine::_requestMetaDataJson() const QString uri = compInfo->uriMetaData(); _jsonMetadataCrcValid = compInfo->crcMetaDataValid(); + qCDebug(ComponentInformationManagerLog) << typeToString() << ": requesting metadata (primary) from" << uri; + _activeAsyncState = _stateRequestMetaDataJson; _activeSkippableState = nullptr; - _requestFile(fileTag, compInfo->crcMetaDataValid(), uri, _jsonMetadataFileName); + _requestFile(fileTag, compInfo->crcMetaDataValid(), uri, _jsonMetadataFileName, true); } void RequestMetaDataTypeStateMachine::_requestMetaDataJsonFallback() { - qCDebug(RequestMetaDataTypeStateMachineLog) << "Trying fallback download for" << typeToString(); + qCDebug(ComponentInformationManagerLog) << typeToString() << ": primary failed, requesting metadata (fallback) from" << _compInfo->uriMetaDataFallback(); + _metadataIsFallback = true; CompInfo* compInfo = _compInfo; const QString fileTag = ComponentInformationManager::_getFileCacheTag(compInfo->type, compInfo->crcMetaDataFallback(), false); @@ -351,7 +357,7 @@ void RequestMetaDataTypeStateMachine::_requestMetaDataJsonFallback() _activeAsyncState = nullptr; _activeSkippableState = _stateRequestMetaDataJsonFallback; - _requestFile(fileTag, compInfo->crcMetaDataFallbackValid(), uri, _jsonMetadataFileName); + _requestFile(fileTag, compInfo->crcMetaDataFallbackValid(), uri, _jsonMetadataFileName, true); } void RequestMetaDataTypeStateMachine::_requestTranslationJson() @@ -359,9 +365,15 @@ void RequestMetaDataTypeStateMachine::_requestTranslationJson() CompInfo* compInfo = _compInfo; const QString uri = compInfo->uriTranslation(); + if (uri.isEmpty()) { + qCDebug(RequestMetaDataTypeStateMachineLog) << "No translation URI for" << typeToString(); + _stateRequestTranslationJson->complete(); + return; + } + _activeAsyncState = _stateRequestTranslationJson; _activeSkippableState = nullptr; - _requestFile("", false, uri, _jsonTranslationFileName); + _requestFile("", false, uri, _jsonTranslationFileName, false); } void RequestMetaDataTypeStateMachine::_requestTranslate() @@ -371,7 +383,8 @@ void RequestMetaDataTypeStateMachine::_requestTranslate() if (!_compMgr->translation()->downloadAndTranslate(_jsonTranslationFileName, _jsonMetadataFileName, - ComponentInformationManager::cachedFileMaxAgeSec)) { + ComponentInformationManager::cachedFileMaxAgeSec, + typeToString())) { disconnect(_compMgr->translation(), &ComponentInformationTranslation::downloadComplete, this, &RequestMetaDataTypeStateMachine::_downloadAndTranslationComplete); qCDebug(RequestMetaDataTypeStateMachineLog) << "downloadAndTranslate() failed"; @@ -394,11 +407,14 @@ void RequestMetaDataTypeStateMachine::_downloadAndTranslationComplete(QString tr void RequestMetaDataTypeStateMachine::_completeRequest() { - if (_jsonMetadataTranslatedFileName.isEmpty()) { - _compInfo->setJson(_jsonMetadataFileName); - } else { + const bool success = !_jsonMetadataFileName.isEmpty(); + const bool translated = !_jsonMetadataTranslatedFileName.isEmpty(); + + if (translated) { _compInfo->setJson(_jsonMetadataTranslatedFileName); QFile(_jsonMetadataTranslatedFileName).remove(); + } else { + _compInfo->setJson(_jsonMetadataFileName); } // If we don't have a CRC we didn't cache the file and need to delete it @@ -408,9 +424,35 @@ void RequestMetaDataTypeStateMachine::_completeRequest() if (!_jsonMetadataCrcValid && !_jsonTranslationFileName.isEmpty()) { QFile(_jsonTranslationFileName).remove(); } + + // Summary log for easy filtering + const char* sourceLabel = _metadataIsFallback ? "(fallback)" : "(primary)"; + if (success) { + if (translated) { + qCDebug(ComponentInformationManagerLog) << typeToString() << ":" << _metadataSourceToString(_metadataSource) + << sourceLabel << "(translated)" << _metadataUri; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ":" << _metadataSourceToString(_metadataSource) + << sourceLabel << _metadataUri; + } + } else { + qCWarning(ComponentInformationManagerLog) << typeToString() << ": failed to load metadata (primary and fallback)" + << (_metadataUri.isEmpty() ? _compInfo->uriMetaData() : _metadataUri); + } } -void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName) +const char* RequestMetaDataTypeStateMachine::_metadataSourceToString(MetadataSource source) +{ + switch (source) { + case MetadataSource::Cache: return "loaded from cache"; + case MetadataSource::FTP: return "downloaded via FTP"; + case MetadataSource::HTTP: return "downloaded via HTTP"; + case MetadataSource::None: return "not available"; + } + return "unknown"; +} + +void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName, bool trackMetadataSource) { FTPManager* ftpManager = _compInfo->vehicle->ftpManager(); _currentCacheFileTag = cacheFileTag; @@ -427,7 +469,7 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, }; if (!_compInfo->available() || uri.isEmpty()) { - qCDebug(RequestMetaDataTypeStateMachineLog) << "Skipping download. Component information not available for" << _currentCacheFileTag; + qCDebug(ComponentInformationManagerLog) << typeToString() << ": metadata not available, skipping download"; completeCurrentState(); return; } @@ -436,14 +478,28 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, if (!cachedFile.isEmpty()) { qCDebug(RequestMetaDataTypeStateMachineLog) << "Using cached file" << cachedFile; + if (trackMetadataSource) { + _metadataSource = MetadataSource::Cache; + _metadataUri = uri; + } outputFileName = cachedFile; completeCurrentState(); return; } + if (!crcValid) { + qCDebug(RequestMetaDataTypeStateMachineLog) << typeToString() << ": CRC not available, cache bypassed"; + } else { + qCDebug(RequestMetaDataTypeStateMachineLog) << typeToString() << ": not found in cache, downloading"; + } + qCDebug(RequestMetaDataTypeStateMachineLog) << "Downloading json" << uri; if (_uriIsMAVLinkFTP(uri)) { + if (trackMetadataSource) { + _metadataSource = MetadataSource::FTP; + _metadataUri = uri; + } connect(ftpManager, &FTPManager::downloadComplete, this, &RequestMetaDataTypeStateMachine::_ftpDownloadComplete); if (ftpManager->download(MAV_COMP_ID_AUTOPILOT1, uri, QStandardPaths::writableLocation(QStandardPaths::TempLocation))) { _downloadStartTime.start(); @@ -454,6 +510,10 @@ void RequestMetaDataTypeStateMachine::_requestFile(const QString& cacheFileTag, completeCurrentState(); } } else { + if (trackMetadataSource) { + _metadataSource = MetadataSource::HTTP; + _metadataUri = uri; + } connect(_compMgr->_cachedFileDownload, &QGCCachedFileDownload::finished, this, &RequestMetaDataTypeStateMachine::_httpDownloadComplete); if (_compMgr->_cachedFileDownload->download(uri, crcValid ? 0 : ComponentInformationManager::cachedFileMaxAgeSec)) { @@ -493,8 +553,8 @@ void RequestMetaDataTypeStateMachine::_ftpDownloadComplete(const QString& fileNa if (_currentFileName) { *_currentFileName = _downloadCompleteJsonWorker(fileName); } - } else if (qgcApp()->runningUnitTests()) { - qCWarning(RequestMetaDataTypeStateMachineLog) << "_ftpDownloadComplete failed filename:errorMsg" << fileName << errorMsg; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ": FTP download failed:" << errorMsg; } if (_activeAsyncState) { @@ -529,9 +589,8 @@ void RequestMetaDataTypeStateMachine::_httpDownloadComplete(bool success, const if (_currentFileName) { *_currentFileName = _downloadCompleteJsonWorker(localFile); } - } else if (qgcApp()->runningUnitTests()) { - qCWarning(RequestMetaDataTypeStateMachineLog) << "_httpDownloadComplete failed localFile:errorMsg:fromCache" - << localFile << errorMsg << fromCache; + } else { + qCDebug(ComponentInformationManagerLog) << typeToString() << ": HTTP download failed:" << errorMsg; } if (_activeAsyncState) { diff --git a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h index 2aed8736e63f..34099faede4e 100644 --- a/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h +++ b/src/Vehicle/ComponentInformation/RequestMetaDataTypeStateMachine.h @@ -53,10 +53,18 @@ class RequestMetaDataTypeStateMachine : public QGCStateMachine bool _shouldSkipTranslation() const; // Download helpers - void _requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName); + void _requestFile(const QString& cacheFileTag, bool crcValid, const QString& uri, QString& outputFileName, bool trackMetadataSource); QString _downloadCompleteJsonWorker(const QString& jsonFileName); static bool _uriIsMAVLinkFTP(const QString& uri); + enum class MetadataSource { + None, + Cache, + FTP, + HTTP, + }; + static const char* _metadataSourceToString(MetadataSource source); + // Message result handlers void _handleCompMetadataResult(MAV_RESULT result, const mavlink_message_t& message); void _handleCompInfoResult(MAV_RESULT result, Vehicle::RequestMessageResultHandlerFailureCode_t failureCode, const mavlink_message_t& message); @@ -82,6 +90,9 @@ private slots: bool _currentFileValidCrc = false; QElapsedTimer _downloadStartTime; + MetadataSource _metadataSource = MetadataSource::None; + QString _metadataUri; + bool _metadataIsFallback = false; // State pointers AsyncFunctionState* _stateRequestCompInfo = nullptr; diff --git a/src/Vehicle/FTPManager.cc b/src/Vehicle/FTPManager.cc index 328db8d558c4..935f59646177 100644 --- a/src/Vehicle/FTPManager.cc +++ b/src/Vehicle/FTPManager.cc @@ -663,6 +663,12 @@ void FTPManager::_mavlinkMessageReceived(const mavlink_message_t& message) MavlinkFTP::Request* request = (MavlinkFTP::Request*)&data.payload[0]; + // Reject messages where hdr.size exceeds data array bounds to prevent over-reads + if (request->hdr.size > sizeof(request->data)) { + qCWarning(FTPManagerLog) << "_mavlinkMessageReceived: hdr.size exceeds data array, discarding." << request->hdr.size; + return; + } + // Ignore old/reordered packets (handle wrap-around properly) uint16_t actualIncomingSeqNumber = request->hdr.seqNumber; if ((uint16_t)((_expectedIncomingSeqNumber - 1) - actualIncomingSeqNumber) < (std::numeric_limits::max()/2)) { diff --git a/src/Vehicle/InitialConnectStateMachine.cc b/src/Vehicle/InitialConnectStateMachine.cc index db4dd05b8b10..f2b732180428 100644 --- a/src/Vehicle/InitialConnectStateMachine.cc +++ b/src/Vehicle/InitialConnectStateMachine.cc @@ -331,18 +331,22 @@ void InitialConnectStateMachine::_requestStandardModes(AsyncFunctionState* state vehicle()->_standardModes->request(); } -void InitialConnectStateMachine::_requestCompInfo(AsyncFunctionState* /*state*/) +void InitialConnectStateMachine::_requestCompInfo(AsyncFunctionState* state) { qCDebug(InitialConnectStateMachineLog) << "_stateRequestCompInfo"; connect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, this, &InitialConnectStateMachine::_onSubProgressUpdate, Qt::UniqueConnection); + // Ensure progress tracking is always cleaned up, including timeout/skip paths. + state->setOnExit([this]() { + disconnect(vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, + this, &InitialConnectStateMachine::_onSubProgressUpdate); + }); + vehicle()->_componentInformationManager->requestAllComponentInformation( [](void* requestAllCompleteFnData) { auto* self = static_cast(requestAllCompleteFnData); - disconnect(self->vehicle()->_componentInformationManager, &ComponentInformationManager::progressUpdate, - self, &InitialConnectStateMachine::_onSubProgressUpdate); if (self->_stateCompInfo) { self->_stateCompInfo->complete(); } diff --git a/src/Vehicle/RemoteIDManager.cc b/src/Vehicle/RemoteIDManager.cc index d9502d96819f..47bd23c671c4 100644 --- a/src/Vehicle/RemoteIDManager.cc +++ b/src/Vehicle/RemoteIDManager.cc @@ -256,66 +256,60 @@ void RemoteIDManager::_sendOperatorID() } } +void RemoteIDManager::_updateGcsGpsStatus(bool gpsGood, const QString& error) +{ + if (!error.isEmpty() && _gcsGPSError != error) { + _gcsGPSError = error; + qCWarning(RemoteIDManagerLog) << "GCS GPS error:" << error; + } + if (_gcsGPSGood != gpsGood) { + _gcsGPSGood = gpsGood; + if (gpsGood) { + _gcsGPSError.clear(); + } + emit gcsGPSGoodChanged(); + } +} + void RemoteIDManager::_sendSystem() { - QGeoCoordinate gcsPosition; - QGeoPositionInfo geoPositionInfo; + QGeoCoordinate gcsPosition(0, 0, 0); + const uint32_t locationType = _settings->locationType()->rawValue().toUInt(); // Location types: // 0 -> TAKEOFF (not supported yet) // 1 -> LIVE GNNS // 2 -> FIXED - if (_settings->locationType()->rawValue().toUInt() == LocationTypes::FIXED) { + if (locationType == LocationTypes::FIXED) { + const double lat = _settings->latitudeFixed()->rawValue().toDouble(); + const double lon = _settings->longitudeFixed()->rawValue().toDouble(); + const double alt = _settings->altitudeFixed()->rawValue().toDouble(); + // For FIXED location, we first check that the values are valid. Then we populate our position - if (_settings->latitudeFixed()->rawValue().toFloat() >= -90 && _settings->latitudeFixed()->rawValue().toFloat() <= 90 && _settings->longitudeFixed()->rawValue().toFloat() >= -180 && _settings->longitudeFixed()->rawValue().toFloat() <= 180) { - gcsPosition = QGeoCoordinate(_settings->latitudeFixed()->rawValue().toFloat(), _settings->longitudeFixed()->rawValue().toFloat(), _settings->altitudeFixed()->rawValue().toFloat()); - geoPositionInfo = QGeoPositionInfo(gcsPosition, QDateTime::currentDateTime().currentDateTimeUtc()); - if (!_gcsGPSGood) { - _gcsGPSGood = true; - emit gcsGPSGoodChanged(); - } + if (lat >= -90.0 && lat <= 90.0 && lon >= -180.0 && lon <= 180.0) { + gcsPosition = QGeoCoordinate(lat, lon, alt); + _updateGcsGpsStatus(true); } else { - gcsPosition = QGeoCoordinate(0,0,0); - geoPositionInfo = QGeoPositionInfo(gcsPosition, QDateTime::currentDateTime().currentDateTimeUtc()); - if (_gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "The provided coordinates for FIXED position are invalid."; - } + _updateGcsGpsStatus(false, "The provided coordinates for FIXED position are invalid."); } } else { - // For Live GNSS we take QGC GPS data - gcsPosition = QGCPositionManager::instance()->gcsPosition(); - geoPositionInfo = QGCPositionManager::instance()->geoPositionInfo(); - - // GPS position needs to be valid before checking other stuff - if (geoPositionInfo.isValid()) { - // If we dont have altitude for FAA then the GPS data is no good - if ((_settings->region()->rawValue().toInt() == Region::FAA) && !(gcsPosition.altitude() >= 0) && _gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data error (no altitude): Altitude data is mandatory for GCS GPS data in FAA regions."; - return; - } - - // If the GPS data is older than ALLOWED_GPS_DELAY we cannot use this data - if (_lastGeoPositionTimeStamp.msecsTo(QDateTime::currentDateTime().currentDateTimeUtc()) > ALLOWED_GPS_DELAY) { - if (_gcsGPSGood) { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data is older than 5 seconds"; - } - } else { - if (!_gcsGPSGood) { - _gcsGPSGood = true; - emit gcsGPSGoodChanged(); - } - } + QGCPositionManager* positionManager = QGCPositionManager::instance(); + QGeoPositionInfo geoPositionInfo = positionManager->geoPositionInfo(); + gcsPosition = positionManager->gcsPosition(); + + if (!geoPositionInfo.isValid()) { + _updateGcsGpsStatus(false, "GCS GPS data is not valid."); + } else if (positionManager->gcsPositioningError() != QGeoPositionInfoSource::NoError && positionManager->gcsPositioningError() != QGeoPositionInfoSource::UpdateTimeoutError) { + _updateGcsGpsStatus(false, QString("GCS GPS data error: %1").arg(positionManager->gcsPositioningError())); + } else if (!gcsPosition.isValid() || gcsPosition.type() == QGeoCoordinate::InvalidCoordinate) { + _updateGcsGpsStatus(false, "GCS GPS data error: Invalid coordinate type."); + } else if (_settings->region()->rawValue().toInt() == Region::FAA && gcsPosition.type() != QGeoCoordinate::Coordinate3D) { + // FAA requires altitude data, or else the GPS data is not good + _updateGcsGpsStatus(false, "GCS GPS data error: Altitude data is mandatory for FAA regions."); + } else if (_lastGeoPositionTimeStamp.msecsTo(QDateTime::currentDateTime().currentDateTimeUtc()) > ALLOWED_GPS_DELAY) { + _updateGcsGpsStatus(false, "GCS GPS data is older than 5 seconds"); } else { - _gcsGPSGood = false; - emit gcsGPSGoodChanged(); - qCDebug(RemoteIDManagerLog) << "GCS GPS data is not valid."; + _updateGcsGpsStatus(true); } - } WeakLinkInterfacePtr weakLink = _vehicle->vehicleLinkManager()->primaryLink(); diff --git a/src/Vehicle/RemoteIDManager.h b/src/Vehicle/RemoteIDManager.h index 5b4ae7bcdad4..3327391f62f2 100644 --- a/src/Vehicle/RemoteIDManager.h +++ b/src/Vehicle/RemoteIDManager.h @@ -94,6 +94,9 @@ private slots: // Basic ID void _sendBasicID(); + // GCS GPS status + void _updateGcsGpsStatus(bool gpsGood, const QString& error = QString()); + bool _isEUOperatorIDValid(const QString& operatorID) const; QChar _calculateLuhnMod36(const QString& input) const; @@ -106,6 +109,7 @@ private slots: QString _armStatusError; bool _commsGood; bool _gcsGPSGood; + QString _gcsGPSError; bool _basicIDGood; bool _GCSBasicIDValid; bool _operatorIDGood; diff --git a/src/Vehicle/TerrainProtocolHandler.cc b/src/Vehicle/TerrainProtocolHandler.cc index 36d19d33d76d..c567178bce6c 100644 --- a/src/Vehicle/TerrainProtocolHandler.cc +++ b/src/Vehicle/TerrainProtocolHandler.cc @@ -1,4 +1,5 @@ #include "TerrainProtocolHandler.h" +#include "TerrainFactGroup.h" #include "TerrainQuery.h" #include "Vehicle.h" #include "MAVLinkProtocol.h" diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index cca1b13bf4ea..1a836bc0e684 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -1,5 +1,25 @@ #include "Vehicle.h" #include "Actuators.h" +#include "BatteryFactGroupListModel.h" +#include "EscStatusFactGroupListModel.h" +#include "TerrainFactGroup.h" +#include "VehicleClockFactGroup.h" +#include "VehicleDistanceSensorFactGroup.h" +#include "VehicleEFIFactGroup.h" +#include "VehicleEstimatorStatusFactGroup.h" +#include "VehicleGeneratorFactGroup.h" +#include "VehicleGPS2FactGroup.h" +#include "VehicleGPSFactGroup.h" +#include "VehicleGPSAggregateFactGroup.h" +#include "VehicleHygrometerFactGroup.h" +#include "VehicleLocalPositionFactGroup.h" +#include "VehicleLocalPositionSetpointFactGroup.h" +#include "VehicleRPMFactGroup.h" +#include "VehicleSetpointFactGroup.h" +#include "VehicleTemperatureFactGroup.h" +#include "VehicleVibrationFactGroup.h" +#include "VehicleWindFactGroup.h" +#include "VehicleSupports.h" #include "ADSBVehicleManager.h" #include "AudioOutput.h" #include "AutoPilotPlugin.h" @@ -45,6 +65,7 @@ #include "DeviceInfo.h" #include "StatusTextHandler.h" #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" #include "GimbalController.h" #include "MavlinkSettings.h" #include "APM.h" @@ -86,24 +107,6 @@ Vehicle::Vehicle(LinkInterface* link, , _trajectoryPoints (new TrajectoryPoints(this, this)) , _mavlinkStreamConfig (std::bind(&Vehicle::_setMessageInterval, this, std::placeholders::_1, std::placeholders::_2)) , _vehicleFactGroup (this) - , _gpsFactGroup (this) - , _gps2FactGroup (this) - , _gpsAggregateFactGroup (this) - , _windFactGroup (this) - , _vibrationFactGroup (this) - , _temperatureFactGroup (this) - , _clockFactGroup (this) - , _setpointFactGroup (this) - , _distanceSensorFactGroup (this) - , _localPositionFactGroup (this) - , _localPositionSetpointFactGroup(this) - , _estimatorStatusFactGroup (this) - , _hygrometerFactGroup (this) - , _generatorFactGroup (this) - , _efiFactGroup (this) - , _rpmFactGroup (this) - , _terrainFactGroup (this) - , _terrainProtocolHandler (new TerrainProtocolHandler(this, &_terrainFactGroup, this)) { connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &Vehicle::_activeVehicleChanged); @@ -186,15 +189,6 @@ Vehicle::Vehicle(MAV_AUTOPILOT firmwareType, , _trajectoryPoints (new TrajectoryPoints(this, this)) , _mavlinkStreamConfig (std::bind(&Vehicle::_setMessageInterval, this, std::placeholders::_1, std::placeholders::_2)) , _vehicleFactGroup (this) - , _gpsFactGroup (this) - , _gps2FactGroup (this) - , _gpsAggregateFactGroup (this) - , _windFactGroup (this) - , _vibrationFactGroup (this) - , _clockFactGroup (this) - , _distanceSensorFactGroup (this) - , _localPositionFactGroup (this) - , _localPositionSetpointFactGroup (this) { // This will also set the settings based firmware/vehicle types. So it needs to happen first. if (_firmwareType == MAV_AUTOPILOT_TRACK) { @@ -298,30 +292,54 @@ void Vehicle::_commonInit(LinkInterface* link) // Flight modes can differ based on advanced mode connect(QGCCorePlugin::instance(), &QGCCorePlugin::showAdvancedUIChanged, this, &Vehicle::flightModesChanged); - _gpsAggregateFactGroup.bindToGps(&_gpsFactGroup, &_gps2FactGroup); + _gpsFactGroup = new VehicleGPSFactGroup(this); + _gps2FactGroup = new VehicleGPS2FactGroup(this); + _gpsAggregateFactGroup = new VehicleGPSAggregateFactGroup(this); + _windFactGroup = new VehicleWindFactGroup(this); + _vibrationFactGroup = new VehicleVibrationFactGroup(this); + _temperatureFactGroup = new VehicleTemperatureFactGroup(this); + _clockFactGroup = new VehicleClockFactGroup(this); + _setpointFactGroup = new VehicleSetpointFactGroup(this); + _distanceSensorFactGroup = new VehicleDistanceSensorFactGroup(this); + _localPositionFactGroup = new VehicleLocalPositionFactGroup(this); + _localPositionSetpointFactGroup = new VehicleLocalPositionSetpointFactGroup(this); + _estimatorStatusFactGroup = new VehicleEstimatorStatusFactGroup(this); + _hygrometerFactGroup = new VehicleHygrometerFactGroup(this); + _generatorFactGroup = new VehicleGeneratorFactGroup(this); + _efiFactGroup = new VehicleEFIFactGroup(this); + _rpmFactGroup = new VehicleRPMFactGroup(this); + _terrainFactGroup = new TerrainFactGroup(this); + _batteryFactGroupListModel = new BatteryFactGroupListModel(this); + _escStatusFactGroupListModel = new EscStatusFactGroupListModel(this); + + if (!_offlineEditingVehicle) { + _terrainProtocolHandler = new TerrainProtocolHandler(this, _terrainFactGroup, this); + } + + _gpsAggregateFactGroup->bindToGps(_gpsFactGroup, _gps2FactGroup); _createImageProtocolManager(); _createStatusTextHandler(); _createMAVLinkLogManager(); // _addFactGroup(_vehicleFactGroup, _vehicleFactGroupName); - _addFactGroup(&_gpsFactGroup, _gpsFactGroupName); - _addFactGroup(&_gps2FactGroup, _gps2FactGroupName); - _addFactGroup(&_gpsAggregateFactGroup, _gpsAggregateFactGroupName); - _addFactGroup(&_windFactGroup, _windFactGroupName); - _addFactGroup(&_vibrationFactGroup, _vibrationFactGroupName); - _addFactGroup(&_temperatureFactGroup, _temperatureFactGroupName); - _addFactGroup(&_clockFactGroup, _clockFactGroupName); - _addFactGroup(&_setpointFactGroup, _setpointFactGroupName); - _addFactGroup(&_distanceSensorFactGroup, _distanceSensorFactGroupName); - _addFactGroup(&_localPositionFactGroup, _localPositionFactGroupName); - _addFactGroup(&_localPositionSetpointFactGroup,_localPositionSetpointFactGroupName); - _addFactGroup(&_estimatorStatusFactGroup, _estimatorStatusFactGroupName); - _addFactGroup(&_hygrometerFactGroup, _hygrometerFactGroupName); - _addFactGroup(&_generatorFactGroup, _generatorFactGroupName); - _addFactGroup(&_efiFactGroup, _efiFactGroupName); - _addFactGroup(&_rpmFactGroup, _rpmFactGroupName); - _addFactGroup(&_terrainFactGroup, _terrainFactGroupName); + _addFactGroup(_gpsFactGroup, _gpsFactGroupName); + _addFactGroup(_gps2FactGroup, _gps2FactGroupName); + _addFactGroup(_gpsAggregateFactGroup, _gpsAggregateFactGroupName); + _addFactGroup(_windFactGroup, _windFactGroupName); + _addFactGroup(_vibrationFactGroup, _vibrationFactGroupName); + _addFactGroup(_temperatureFactGroup, _temperatureFactGroupName); + _addFactGroup(_clockFactGroup, _clockFactGroupName); + _addFactGroup(_setpointFactGroup, _setpointFactGroupName); + _addFactGroup(_distanceSensorFactGroup, _distanceSensorFactGroupName); + _addFactGroup(_localPositionFactGroup, _localPositionFactGroupName); + _addFactGroup(_localPositionSetpointFactGroup,_localPositionSetpointFactGroupName); + _addFactGroup(_estimatorStatusFactGroup, _estimatorStatusFactGroupName); + _addFactGroup(_hygrometerFactGroup, _hygrometerFactGroupName); + _addFactGroup(_generatorFactGroup, _generatorFactGroupName); + _addFactGroup(_efiFactGroup, _efiFactGroupName); + _addFactGroup(_rpmFactGroup, _rpmFactGroupName); + _addFactGroup(_terrainFactGroup, _terrainFactGroupName); // Add firmware-specific fact groups, if provided QMap* fwFactGroups = _firmwarePlugin->factGroups(); @@ -342,6 +360,7 @@ void Vehicle::_commonInit(LinkInterface* link) } _gimbalController = new GimbalController(this); + _vehicleSupports = new VehicleSupports(this); _createCameraManager(); } @@ -368,6 +387,27 @@ Vehicle::~Vehicle() _autopilotPlugin = nullptr; } +FactGroup* Vehicle::gpsFactGroup() { return _gpsFactGroup; } +FactGroup* Vehicle::gps2FactGroup() { return _gps2FactGroup; } +FactGroup* Vehicle::gpsAggregateFactGroup() { return _gpsAggregateFactGroup; } +FactGroup* Vehicle::windFactGroup() { return _windFactGroup; } +FactGroup* Vehicle::vibrationFactGroup() { return _vibrationFactGroup; } +FactGroup* Vehicle::temperatureFactGroup() { return _temperatureFactGroup; } +FactGroup* Vehicle::clockFactGroup() { return _clockFactGroup; } +FactGroup* Vehicle::setpointFactGroup() { return _setpointFactGroup; } +FactGroup* Vehicle::distanceSensorFactGroup() { return _distanceSensorFactGroup; } +FactGroup* Vehicle::localPositionFactGroup() { return _localPositionFactGroup; } +FactGroup* Vehicle::localPositionSetpointFactGroup() { return _localPositionSetpointFactGroup; } +FactGroup* Vehicle::estimatorStatusFactGroup() { return _estimatorStatusFactGroup; } +FactGroup* Vehicle::terrainFactGroup() { return _terrainFactGroup; } +FactGroup* Vehicle::hygrometerFactGroup() { return _hygrometerFactGroup; } +FactGroup* Vehicle::generatorFactGroup() { return _generatorFactGroup; } +FactGroup* Vehicle::efiFactGroup() { return _efiFactGroup; } +FactGroup* Vehicle::rpmFactGroup() { return _rpmFactGroup; } + +QmlObjectListModel* Vehicle::batteries() { return _batteryFactGroupListModel; } +QmlObjectListModel* Vehicle::escs() { return _escStatusFactGroupListModel; } + void Vehicle::_deleteCameraManager() { if(_cameraManager) { @@ -469,6 +509,15 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes } } + // Try to auto-detect signing key from incoming signed packets + if (MAVLinkSigning::isMessageSigned(message) && !MAVLinkSigning::isSigningEnabled(static_cast(link->mavlinkChannel()))) { + const QString detectedKeyName = MAVLinkSigning::tryDetectKey(static_cast(link->mavlinkChannel()), message); + if (!detectedKeyName.isEmpty() && link == vehicleLinkManager()->primaryLink().lock().get()) { + _mavlinkSigningKeyName = detectedKeyName; + emit mavlinkSigningChanged(); + } + } + // We give the link manager first whack since it it reponsible for adding new links _vehicleLinkManager->mavlinkMessageReceived(link, message); @@ -519,8 +568,8 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes _waitForMavlinkMessageMessageReceivedHandler(message); // Handle creation of dynamic fact group lists - _batteryFactGroupListModel.handleMessageForFactGroupCreation(this, message); - _escStatusFactGroupListModel.handleMessageForFactGroupCreation(this, message); + _batteryFactGroupListModel->handleMessageForFactGroupCreation(this, message); + _escStatusFactGroupListModel->handleMessageForFactGroupCreation(this, message); // Let the fact groups take a whack at the mavlink traffic for (FactGroup* factGroup : factGroups()) { @@ -542,6 +591,38 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes case MAVLINK_MSG_ID_RC_CHANNELS: _handleRCChannels(message); break; + case MAVLINK_MSG_ID_SERVO_OUTPUT_RAW: + { + mavlink_servo_output_raw_t servoOutputRaw; + mavlink_msg_servo_output_raw_decode(&message, &servoOutputRaw); + + // ArduPilot commonly publishes servo1_raw..servo16_raw in a single packet (port may remain 0). + const uint16_t rawValues[16] = { + servoOutputRaw.servo1_raw, + servoOutputRaw.servo2_raw, + servoOutputRaw.servo3_raw, + servoOutputRaw.servo4_raw, + servoOutputRaw.servo5_raw, + servoOutputRaw.servo6_raw, + servoOutputRaw.servo7_raw, + servoOutputRaw.servo8_raw, + servoOutputRaw.servo9_raw, + servoOutputRaw.servo10_raw, + servoOutputRaw.servo11_raw, + servoOutputRaw.servo12_raw, + servoOutputRaw.servo13_raw, + servoOutputRaw.servo14_raw, + servoOutputRaw.servo15_raw, + servoOutputRaw.servo16_raw + }; + + for (int servoIndex = 0; servoIndex < _servoOutputRawValues.size() && servoIndex < 16; servoIndex++) { + _servoOutputRawValues[servoIndex] = (rawValues[servoIndex] == UINT16_MAX) ? -1 : static_cast(rawValues[servoIndex]); + } + + emit servoOutputsChanged(_servoOutputRawValues); + } + break; case MAVLINK_MSG_ID_BATTERY_STATUS: _handleBatteryStatus(message); break; @@ -605,7 +686,7 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes mavlink_serial_control_t ser; mavlink_msg_serial_control_decode(&message, &ser); if (static_cast(ser.count) > sizeof(ser.data)) { - qWarning() << "Invalid count for SERIAL_CONTROL, discarding." << ser.count; + qCWarning(VehicleLog) << "Invalid count for SERIAL_CONTROL, discarding." << ser.count; } else { emit mavlinkSerialControl(ser.device, ser.flags, ser.timeout, ser.baudrate, QByteArray(reinterpret_cast(ser.data), ser.count)); @@ -643,7 +724,11 @@ void Vehicle::_mavlinkMessageReceived(LinkInterface* link, mavlink_message_t mes { mavlink_log_data_t log{}; mavlink_msg_log_data_decode(&message, &log); - emit logData(log.ofs, log.id, log.count, log.data); + if (static_cast(log.count) > sizeof(log.data)) { + qCWarning(VehicleLog) << "Invalid count for LOG_DATA, discarding." << log.count; + } else { + emit logData(log.ofs, log.id, log.count, log.data); + } break; } case MAVLINK_MSG_ID_MESSAGE_INTERVAL: @@ -729,6 +814,10 @@ void Vehicle::_handleCameraImageCaptured(const mavlink_message_t& message) // TODO: VehicleFactGroup void Vehicle::_handleGpsRawInt(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_gps_raw_int_t gpsRawInt; mavlink_msg_gps_raw_int_decode(&message, &gpsRawInt); @@ -751,6 +840,10 @@ void Vehicle::_handleGpsRawInt(mavlink_message_t& message) // TODO: VehicleFactGroup void Vehicle::_handleGlobalPositionInt(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_global_position_int_t globalPositionInt; mavlink_msg_global_position_int_decode(&message, &globalPositionInt); @@ -916,6 +1009,10 @@ QString Vehicle::vehicleUIDStr() void Vehicle::_handleExtendedSysState(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_extended_sys_state_t extendedState; mavlink_msg_extended_sys_state_decode(&message, &extendedState); @@ -1060,7 +1157,7 @@ void Vehicle::_handleBatteryStatus(mavlink_message_t& message) if (!batteryMessage.isEmpty()) { QString batteryIdStr("%1"); - if (_batteryFactGroupListModel.count() > 1) { + if (_batteryFactGroupListModel->count() > 1) { batteryIdStr = batteryIdStr.arg(batteryStatus.id); } else { batteryIdStr = batteryIdStr.arg(""); @@ -1081,6 +1178,10 @@ void Vehicle::_setHomePosition(QGeoCoordinate& homeCoord) void Vehicle::_handleHomePosition(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_home_position_t homePos; mavlink_msg_home_position_decode(&message, &homePos); @@ -1335,6 +1436,10 @@ void Vehicle::_handleHeartbeat(mavlink_message_t& message) void Vehicle::_handleCurrentMode(mavlink_message_t& message) { + if (message.compid != _defaultComponentId) { + return; + } + mavlink_current_mode_t currentMode; mavlink_msg_current_mode_decode(&message, ¤tMode); if (currentMode.intended_custom_mode != 0) { // 0 == unknown/not supplied @@ -1865,36 +1970,6 @@ bool Vehicle::vtol() const return QGCMAVLink::isVTOL(vehicleType()); } -bool Vehicle::supportsThrottleModeCenterZero() const -{ - return _firmwarePlugin->supportsThrottleModeCenterZero(); -} - -bool Vehicle::supportsNegativeThrust() -{ - return _firmwarePlugin->supportsNegativeThrust(this); -} - -bool Vehicle::supportsRadio() const -{ - return _firmwarePlugin->supportsRadio(); -} - -bool Vehicle::supportsJSButton() const -{ - return _firmwarePlugin->supportsJSButton(); -} - -bool Vehicle::supportsMotorInterference() const -{ - return _firmwarePlugin->supportsMotorInterference(); -} - -bool Vehicle::supportsTerrainFrame() const -{ - return !px4Firmware(); -} - QString Vehicle::vehicleTypeString() const { return QGCMAVLink::mavTypeToString(_vehicleType); @@ -1947,41 +2022,6 @@ void Vehicle::_setLanding(bool landing) } } -bool Vehicle::guidedModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::GuidedModeCapability); -} - -bool Vehicle::pauseVehicleSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::PauseVehicleCapability); -} - -bool Vehicle::orbitModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::OrbitModeCapability); -} - -bool Vehicle::roiModeSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::ROIModeCapability); -} - -bool Vehicle::takeoffVehicleSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::TakeoffVehicleCapability); -} - -bool Vehicle::guidedTakeoffSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::GuidedTakeoffCapability); -} - -bool Vehicle::changeHeadingSupported() const -{ - return _firmwarePlugin->isCapable(this, FirmwarePlugin::ChangeHeadingCapability); -} - QString Vehicle::gotoFlightMode() const { return _firmwarePlugin->gotoFlightMode(); @@ -1989,7 +2029,7 @@ QString Vehicle::gotoFlightMode() const void Vehicle::guidedModeRTL(bool smartRTL) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -1998,7 +2038,7 @@ void Vehicle::guidedModeRTL(bool smartRTL) void Vehicle::guidedModeLand() { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2007,7 +2047,7 @@ void Vehicle::guidedModeLand() void Vehicle::guidedModeTakeoff(double altitudeRelative) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2019,9 +2059,9 @@ double Vehicle::minimumTakeoffAltitudeMeters() return _firmwarePlugin->minimumTakeoffAltitudeMeters(this); } -double Vehicle::maximumHorizontalSpeedMultirotor() +double Vehicle::maximumHorizontalSpeedMultirotorMetersSecond() { - return _firmwarePlugin->maximumHorizontalSpeedMultirotor(this); + return _firmwarePlugin->maximumHorizontalSpeedMultirotorMetersSecond(this); } @@ -2052,26 +2092,30 @@ void Vehicle::startMission() _firmwarePlugin->startMission(this); } -void Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) +bool Vehicle::guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); - return; + return false; } if (!coordinate().isValid()) { - return; + return false; + } + if (!gotoCoord.isValid()) { + return false; } double maxDistance = SettingsManager::instance()->flyViewSettings()->maxGoToLocationDistance()->rawValue().toDouble(); if (coordinate().distanceTo(gotoCoord) > maxDistance) { qgcApp()->showAppMessage(QString("New location is too far. Must be less than %1 %2.").arg(qRound(FactMetaData::metersToAppSettingsHorizontalDistanceUnits(maxDistance).toDouble())).arg(FactMetaData::appSettingsHorizontalDistanceUnitsString())); - return; + return false; } - _firmwarePlugin->guidedModeGotoLocation(this, gotoCoord, forwardFlightLoiterRadius); + + return _firmwarePlugin->guidedModeGotoLocation(this, gotoCoord, forwardFlightLoiterRadius); } void Vehicle::guidedModeChangeAltitude(double altitudeChange, bool pauseVehicle) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2081,7 +2125,7 @@ void Vehicle::guidedModeChangeAltitude(double altitudeChange, bool pauseVehicle) void Vehicle::guidedModeChangeGroundSpeedMetersSecond(double groundspeed) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2091,7 +2135,7 @@ Vehicle::guidedModeChangeGroundSpeedMetersSecond(double groundspeed) void Vehicle::guidedModeChangeEquivalentAirspeedMetersSecond(double airspeed) { - if (!guidedModeSupported()) { + if (!_vehicleSupports->guidedMode()) { qgcApp()->showAppMessage(guided_mode_not_supported_by_vehicle); return; } @@ -2100,7 +2144,7 @@ Vehicle::guidedModeChangeEquivalentAirspeedMetersSecond(double airspeed) void Vehicle::guidedModeOrbit(const QGeoCoordinate& centerCoord, double radius, double amslAltitude) { - if (!orbitModeSupported()) { + if (!_vehicleSupports->orbitMode()) { qgcApp()->showAppMessage(QStringLiteral("Orbit mode not supported by Vehicle.")); return; } @@ -2135,7 +2179,7 @@ void Vehicle::guidedModeROI(const QGeoCoordinate& centerCoord) if (!centerCoord.isValid()) { return; } - if (!roiModeSupported()) { + if (!_vehicleSupports->roiMode()) { qgcApp()->showAppMessage(QStringLiteral("ROI mode not supported by Vehicle.")); return; } @@ -2224,7 +2268,7 @@ void Vehicle::_sendROICommand(const QGeoCoordinate& coord, MAV_FRAME frame, floa void Vehicle::stopGuidedModeROI() { - if (!roiModeSupported()) { + if (!_vehicleSupports->roiMode()) { qgcApp()->showAppMessage(QStringLiteral("ROI mode not supported by Vehicle.")); return; } @@ -2258,7 +2302,7 @@ void Vehicle::stopGuidedModeROI() void Vehicle::guidedModeChangeHeading(const QGeoCoordinate &headingCoord) { - if (!changeHeadingSupported()) { + if (!_vehicleSupports->changeHeading()) { qgcApp()->showAppMessage(tr("Change Heading not supported by Vehicle.")); return; } @@ -2268,7 +2312,7 @@ void Vehicle::guidedModeChangeHeading(const QGeoCoordinate &headingCoord) void Vehicle::pauseVehicle() { - if (!pauseVehicleSupported()) { + if (!_vehicleSupports->pauseVehicle()) { qgcApp()->showAppMessage(QStringLiteral("Pause not supported by vehicle.")); return; } @@ -2544,7 +2588,7 @@ int Vehicle::_mavCommandResponseCheckTimeoutMSecs() int Vehicle::_mavCommandAckTimeoutMSecs() { // Use shorter ack timeout during unit tests for faster test execution - return qgcApp()->runningUnitTests() ? kTestMavCommandAckTimeoutMs : 3000; + return qgcApp()->runningUnitTests() ? kTestMavCommandAckTimeoutMs : 1200; } bool Vehicle::_sendMavCommandShouldRetry(MAV_CMD command) @@ -3028,6 +3072,9 @@ void Vehicle::_waitForMavlinkMessageMessageReceivedHandler(const mavlink_message auto resultHandler = pInfo->resultHandler; auto resultHandlerData = pInfo->resultHandlerData; + pInfo->messageReceived = true; + pInfo->message = message; + const mavlink_message_info_t *info = mavlink_get_message_info_by_id(message.msgid); QString msgName = info ? QString(info->name) : QString::number(message.msgid); const int activeCount = _requestMessageInfoMap.contains(message.compid) ? _requestMessageInfoMap[message.compid].count() : 0; @@ -3041,18 +3088,10 @@ void Vehicle::_waitForMavlinkMessageMessageReceivedHandler(const mavlink_message << "queueDepth" << queueDepth; - if (!pInfo->commandAckReceived) { - qCDebug(VehicleLog) << "message received before ack came back."; - int entryIndex = _findMavCommandListEntryIndex(message.compid, MAV_CMD_REQUEST_MESSAGE); - if (entryIndex != -1) { - _mavCommandList.takeAt(entryIndex); - } else { - qWarning() << "Removing request message command from list failed - not found in list"; - } + if (pInfo->commandAckReceived) { + _removeRequestMessageInfo(message.compid, message.msgid); + (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); } - _removeRequestMessageInfo(message.compid, message.msgid); - - (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); } else { // We use any incoming message as a trigger to check timeouts on message requests @@ -3093,7 +3132,6 @@ void Vehicle::_requestMessageCmdResultHandler(void* resultHandlerData_, [[maybe_ auto resultHandler = requestMessageInfo->resultHandler; auto resultHandlerData = requestMessageInfo->resultHandlerData; Vehicle* vehicle = requestMessageInfo->vehicle; // QPointer converts to raw pointer, null if Vehicle destroyed - auto message [[maybe_unused]] = requestMessageInfo->message; // Vehicle was destroyed before callback fired - clean up and return without accessing vehicle if (!vehicle) { @@ -3132,18 +3170,20 @@ void Vehicle::_requestMessageCmdResultHandler(void* resultHandlerData_, [[maybe_ vehicle->_removeRequestMessageInfo(requestMessageInfo->compId, requestMessageInfo->msgId); - (*resultHandler)(resultHandlerData, static_cast(ack.result), requestMessageFailureCode, ackMessage); + (*resultHandler)(resultHandlerData, static_cast(ack.result), requestMessageFailureCode, ackMessage); return; } if (requestMessageInfo->messageReceived) { - // This should never happen. The command should have already been removed from the list when the message was received - qWarning() << "Command result handler should now have been called if message has already been received"; - } else { - // Now that the request has been acked we start the timer to wait for the message - requestMessageInfo->messageWaitElapsedTimer.start(); + auto message = requestMessageInfo->message; + vehicle->_removeRequestMessageInfo(requestMessageInfo->compId, requestMessageInfo->msgId); + (*resultHandler)(resultHandlerData, MAV_RESULT_ACCEPTED, RequestMessageNoFailure, message); + return; } + + // We have the ack, but we are still waiting for the message. Start the timer to wait for the message + requestMessageInfo->messageWaitElapsedTimer.start(); } void Vehicle::_requestMessageWaitForMessageResultHandler(void* resultHandlerData, bool noResponsefromVehicle, const mavlink_message_t& message) @@ -3516,11 +3556,6 @@ QString Vehicle::smartRTLFlightMode() const return _firmwarePlugin->smartRTLFlightMode(); } -bool Vehicle::supportsSmartRTL() const -{ - return _firmwarePlugin->supportsSmartRTL(); -} - QString Vehicle::landFlightMode() const { return _firmwarePlugin->landFlightMode(); @@ -3708,6 +3743,20 @@ void Vehicle::_mavlinkMessageStatus(int uasId, uint64_t totalSent, uint64_t tota _mavlinkLossCount = totalLoss; _mavlinkLossPercent = lossPercent; emit mavlinkStatusChanged(); + + // Update signing status from the primary link's channel + bool signing = false; + SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); + if (sharedLink) { + signing = MAVLinkSigning::isSigningEnabled(static_cast(sharedLink->mavlinkChannel())); + } + if (signing != _mavlinkSigning) { + _mavlinkSigning = signing; + if (!signing) { + _mavlinkSigningKeyName.clear(); + } + emit mavlinkSigningChanged(); + } } } @@ -3725,9 +3774,9 @@ void Vehicle::setPIDTuningTelemetryMode(PIDTuningTelemetryMode mode) { bool liveUpdate = mode != ModeDisabled; setLiveUpdates(liveUpdate); - _setpointFactGroup.setLiveUpdates(liveUpdate); - _localPositionFactGroup.setLiveUpdates(liveUpdate); - _localPositionSetpointFactGroup.setLiveUpdates(liveUpdate); + _setpointFactGroup->setLiveUpdates(liveUpdate); + _localPositionFactGroup->setLiveUpdates(liveUpdate); + _localPositionSetpointFactGroup->setLiveUpdates(liveUpdate); switch (mode) { case ModeDisabled: @@ -4555,7 +4604,7 @@ void Vehicle::_errorMessageReceived(QString message) /* Signing */ /*===========================================================================*/ -void Vehicle::sendSetupSigning() +void Vehicle::sendSetupSigning(int keyIndex) { SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); if (!sharedLink) { @@ -4563,6 +4612,12 @@ void Vehicle::sendSetupSigning() return; } + const QByteArray keyBytes = MAVLinkSigningKeys::instance()->keyBytesAt(keyIndex); + if (keyBytes.isEmpty()) { + qCCritical(VehicleLog) << "Invalid key index:" << keyIndex; + return; + } + const mavlink_channel_t channel = static_cast(sharedLink->mavlinkChannel()); mavlink_setup_signing_t setup_signing; @@ -4571,7 +4626,17 @@ void Vehicle::sendSetupSigning() target_system.sysid = id(); target_system.compid = defaultComponentId(); - MAVLinkSigning::createSetupSigning(channel, target_system, setup_signing); + MAVLinkSigning::createSetupSigning(channel, target_system, keyBytes, setup_signing); + + // Also configure signing on our channel with this key so outgoing packets are signed + if (!MAVLinkSigning::initSigning(channel, keyBytes, MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)) { + qCCritical(VehicleLog) << "Internal error: failed to initialize signing on channel" << channel; + return; + } + + _mavlinkSigning = true; + _mavlinkSigningKeyName = MAVLinkSigningKeys::instance()->keyNameAt(keyIndex); + emit mavlinkSigningChanged(); mavlink_message_t msg; (void) mavlink_msg_setup_signing_encode_chan(MAVLinkProtocol::instance()->getSystemId(), MAVLinkProtocol::getComponentId(), channel, &msg, &setup_signing); @@ -4582,6 +4647,39 @@ void Vehicle::sendSetupSigning() } } +void Vehicle::sendDisableSigning() +{ + SharedLinkInterfacePtr sharedLink = vehicleLinkManager()->primaryLink().lock(); + if (!sharedLink) { + qCDebug(VehicleLog) << "Primary Link Gone!"; + return; + } + + const mavlink_channel_t channel = static_cast(sharedLink->mavlinkChannel()); + + mavlink_setup_signing_t setup_signing; + + mavlink_system_t target_system; + target_system.sysid = id(); + target_system.compid = defaultComponentId(); + + MAVLinkSigning::createDisableSigning(target_system, setup_signing); + + mavlink_message_t msg; + (void) mavlink_msg_setup_signing_encode_chan(MAVLinkProtocol::instance()->getSystemId(), MAVLinkProtocol::getComponentId(), channel, &msg, &setup_signing); + + for (uint8_t i = 0; i < 2; ++i) { + sendMessageOnLinkThreadSafe(sharedLink.get(), msg); + } + + // Disable signing on the local channel so we stop signing outgoing packets + MAVLinkSigning::initSigning(channel, QByteArrayView(), nullptr); + + _mavlinkSigning = false; + _mavlinkSigningKeyName.clear(); + emit mavlinkSigningChanged(); +} + /*---------------------------------------------------------------------------*/ /*===========================================================================*/ /* Image Protocol Manager */ diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index a2e3545368f3..bf8771b6bcf1 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -17,32 +17,31 @@ #include "QGCMAVLink.h" #include "QmlObjectListModel.h" #include "SysStatusSensorInfo.h" -#include "VehicleLinkManager.h" - -#include "TerrainFactGroup.h" #include "VehicleFactGroup.h" -#include "VehicleClockFactGroup.h" -#include "VehicleDistanceSensorFactGroup.h" -#include "VehicleEFIFactGroup.h" -#include "VehicleEstimatorStatusFactGroup.h" -#include "VehicleGeneratorFactGroup.h" -#include "VehicleGPS2FactGroup.h" -#include "VehicleGPSFactGroup.h" -#include "VehicleGPSAggregateFactGroup.h" -#include "VehicleHygrometerFactGroup.h" -#include "VehicleLocalPositionFactGroup.h" -#include "VehicleLocalPositionSetpointFactGroup.h" -#include "VehicleRPMFactGroup.h" -#include "VehicleSetpointFactGroup.h" -#include "VehicleTemperatureFactGroup.h" -#include "VehicleVibrationFactGroup.h" -#include "VehicleWindFactGroup.h" -#include "GimbalController.h" -#include "BatteryFactGroupListModel.h" -#include "EscStatusFactGroupListModel.h" +#include "VehicleLinkManager.h" class Actuators; class AutoPilotPlugin; +class BatteryFactGroupListModel; +class EscStatusFactGroupListModel; +class GimbalController; +class TerrainFactGroup; +class VehicleClockFactGroup; +class VehicleDistanceSensorFactGroup; +class VehicleEFIFactGroup; +class VehicleEstimatorStatusFactGroup; +class VehicleGeneratorFactGroup; +class VehicleGPS2FactGroup; +class VehicleGPSFactGroup; +class VehicleGPSAggregateFactGroup; +class VehicleHygrometerFactGroup; +class VehicleLocalPositionFactGroup; +class VehicleLocalPositionSetpointFactGroup; +class VehicleRPMFactGroup; +class VehicleSetpointFactGroup; +class VehicleTemperatureFactGroup; +class VehicleVibrationFactGroup; +class VehicleWindFactGroup; class Autotune; class ComponentInformationManager; class EventHandler; @@ -68,6 +67,7 @@ class TerrainAtCoordinateQuery; class TerrainProtocolHandler; class TrajectoryPoints; class VehicleObjectAvoidance; +class VehicleSupports; namespace events { namespace parser { @@ -91,6 +91,8 @@ class Vehicle : public VehicleFactGroup Q_MOC_INCLUDE("Actuators.h") Q_MOC_INCLUDE("MAVLinkLogManager.h") Q_MOC_INCLUDE("LinkInterface.h") + Q_MOC_INCLUDE("VehicleSupports.h") + Q_MOC_INCLUDE("GimbalController.h") friend class InitialConnectStateMachine; friend class VehicleLinkManager; @@ -159,11 +161,7 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(bool vtol READ vtol NOTIFY vehicleTypeChanged) Q_PROPERTY(bool rover READ rover NOTIFY vehicleTypeChanged) Q_PROPERTY(bool sub READ sub NOTIFY vehicleTypeChanged) - Q_PROPERTY(bool supportsThrottleModeCenterZero READ supportsThrottleModeCenterZero CONSTANT) - Q_PROPERTY(bool supportsNegativeThrust READ supportsNegativeThrust CONSTANT) - Q_PROPERTY(bool supportsJSButton READ supportsJSButton CONSTANT) - Q_PROPERTY(bool supportsRadio READ supportsRadio CONSTANT) - Q_PROPERTY(bool supportsMotorInterference READ supportsMotorInterference CONSTANT) + Q_PROPERTY(VehicleSupports* supports READ supports CONSTANT) Q_PROPERTY(QString prearmError READ prearmError WRITE setPrearmError NOTIFY prearmErrorChanged) Q_PROPERTY(int motorCount READ motorCount CONSTANT) Q_PROPERTY(bool coaxialMotors READ coaxialMotors CONSTANT) @@ -179,7 +177,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString pauseFlightMode READ pauseFlightMode CONSTANT) Q_PROPERTY(QString rtlFlightMode READ rtlFlightMode CONSTANT) Q_PROPERTY(QString smartRTLFlightMode READ smartRTLFlightMode CONSTANT) - Q_PROPERTY(bool supportsSmartRTL READ supportsSmartRTL CONSTANT) Q_PROPERTY(QString landFlightMode READ landFlightMode CONSTANT) Q_PROPERTY(QString takeControlFlightMode READ takeControlFlightMode CONSTANT) Q_PROPERTY(QString followFlightMode READ followFlightMode CONSTANT) @@ -201,7 +198,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString hobbsMeter READ hobbsMeter NOTIFY hobbsMeterChanged) Q_PROPERTY(bool inFwdFlight READ inFwdFlight NOTIFY inFwdFlightChanged) Q_PROPERTY(bool vtolInFwdFlight READ vtolInFwdFlight WRITE setVtolInFwdFlight NOTIFY vtolInFwdFlightChanged) - Q_PROPERTY(bool supportsTerrainFrame READ supportsTerrainFrame NOTIFY firmwareTypeChanged) Q_PROPERTY(quint64 mavlinkSentCount READ mavlinkSentCount NOTIFY mavlinkStatusChanged) Q_PROPERTY(quint64 mavlinkReceivedCount READ mavlinkReceivedCount NOTIFY mavlinkStatusChanged) Q_PROPERTY(quint64 mavlinkLossCount READ mavlinkLossCount NOTIFY mavlinkStatusChanged) @@ -226,13 +222,6 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(bool flying READ flying NOTIFY flyingChanged) ///< Vehicle is flying Q_PROPERTY(bool landing READ landing NOTIFY landingChanged) ///< Vehicle is in landing pattern (DO_LAND_START) Q_PROPERTY(bool guidedMode READ guidedMode WRITE setGuidedMode NOTIFY guidedModeChanged) ///< Vehicle is in Guided mode and can respond to guided commands - Q_PROPERTY(bool guidedModeSupported READ guidedModeSupported CONSTANT) ///< Guided mode commands are supported by this vehicle - Q_PROPERTY(bool pauseVehicleSupported READ pauseVehicleSupported CONSTANT) ///< Pause vehicle command is supported - Q_PROPERTY(bool orbitModeSupported READ orbitModeSupported CONSTANT) ///< Orbit mode is supported by this vehicle - Q_PROPERTY(bool roiModeSupported READ roiModeSupported CONSTANT) ///< Orbit mode is supported by this vehicle - Q_PROPERTY(bool takeoffVehicleSupported READ takeoffVehicleSupported CONSTANT) ///< Takeoff supported - Q_PROPERTY(bool guidedTakeoffSupported READ guidedTakeoffSupported CONSTANT) ///< Guided takeoff supported - Q_PROPERTY(bool changeHeadingSupported READ changeHeadingSupported CONSTANT) ///< Change Heading supported Q_PROPERTY(QString gotoFlightMode READ gotoFlightMode CONSTANT) ///< Flight mode vehicle is in while performing goto Q_PROPERTY(bool haveMRSpeedLimits READ haveMRSpeedLimits NOTIFY haveMRSpeedLimChanged) Q_PROPERTY(bool haveFWSpeedLimits READ haveFWSpeedLimits NOTIFY haveFWSpeedLimChanged) @@ -282,6 +271,7 @@ class Vehicle : public VehicleFactGroup Q_PROPERTY(QString vehicleUIDStr READ vehicleUIDStr NOTIFY vehicleUIDChanged) Q_PROPERTY(bool mavlinkSigning READ mavlinkSigning NOTIFY mavlinkSigningChanged) + Q_PROPERTY(QString mavlinkSigningKeyName READ mavlinkSigningKeyName NOTIFY mavlinkSigningChanged) /// Resets link status counters Q_INVOKABLE void resetCounters (); @@ -301,7 +291,7 @@ class Vehicle : public VehicleFactGroup Q_INVOKABLE double minimumTakeoffAltitudeMeters(); /// @return Maximum horizontal speed multirotor. - Q_INVOKABLE double maximumHorizontalSpeedMultirotor(); + Q_INVOKABLE double maximumHorizontalSpeedMultirotorMetersSecond(); /// @return Maximum equivalent airspeed. Q_INVOKABLE double maximumEquivalentAirspeed(); @@ -310,7 +300,8 @@ class Vehicle : public VehicleFactGroup Q_INVOKABLE double minimumEquivalentAirspeed(); /// Command vehicle to move to specified location (altitude is ignored) - Q_INVOKABLE void guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius = 0.0f); + /// @return true: goto command accepted, false: goto failed + Q_INVOKABLE bool guidedModeGotoLocation(const QGeoCoordinate& gotoCoord, double forwardFlightLoiterRadius = 0.0f); /// Command vehicle to change altitude /// @param altitudeChange If > 0, go up by amount specified, if < 0, go down by amount specified @@ -406,19 +397,16 @@ class Vehicle : public VehicleFactGroup /// Set home from flight map coordinate Q_INVOKABLE void doSetHome(const QGeoCoordinate& coord); - Q_INVOKABLE void sendSetupSigning(); + /// Send SETUP_SIGNING with the key at the given index in MAVLinkSigningKeys + Q_INVOKABLE void sendSetupSigning(int keyIndex); + Q_INVOKABLE void sendDisableSigning(); Q_INVOKABLE QVariant expandedToolbarIndicatorSource(const QString& indicatorName); bool isInitialConnectComplete() const; - bool guidedModeSupported () const; - bool pauseVehicleSupported () const; - bool orbitModeSupported () const; - bool roiModeSupported () const; - bool takeoffVehicleSupported () const; - bool guidedTakeoffSupported () const; - bool changeHeadingSupported () const; QString gotoFlightMode () const; + + VehicleSupports* supports() { return _vehicleSupports; } bool hasGripper () const; bool haveMRSpeedLimits() const { return _multirotor_speed_limits_available; } bool haveFWSpeedLimits() const { return _fixed_wing_airspeed_limits_available; } @@ -482,12 +470,7 @@ class Vehicle : public VehicleFactGroup bool sub() const; bool spacecraft() const; - bool supportsThrottleModeCenterZero () const; - bool supportsNegativeThrust (); - bool supportsRadio () const; - bool supportsJSButton () const; - bool supportsMotorInterference () const; - bool supportsTerrainFrame () const; + void setGuidedMode(bool guidedMode); @@ -537,7 +520,6 @@ class Vehicle : public VehicleFactGroup QString pauseFlightMode () const; QString rtlFlightMode () const; QString smartRTLFlightMode () const; - bool supportsSmartRTL () const; QString landFlightMode () const; QString takeControlFlightMode () const; QString followFlightMode () const; @@ -565,6 +547,7 @@ class Vehicle : public VehicleFactGroup bool hilMode () const { return _base_mode & MAV_MODE_FLAG_HIL_ENABLED; } Actuators* actuators () const { return _actuators; } bool mavlinkSigning () const { return _mavlinkSigning; } + QString mavlinkSigningKeyName () const { return _mavlinkSigningKeyName; } void startCalibration (QGCMAVLink::CalibrationType calType); void stopCalibration (bool showError); @@ -573,26 +556,26 @@ class Vehicle : public VehicleFactGroup void stopUAVCANBusConfig(void); FactGroup* vehicleFactGroup () { return _vehicleFactGroup; } - FactGroup* gpsFactGroup () { return &_gpsFactGroup; } - FactGroup* gps2FactGroup () { return &_gps2FactGroup; } - FactGroup* gpsAggregateFactGroup () { return &_gpsAggregateFactGroup; } - FactGroup* windFactGroup () { return &_windFactGroup; } - FactGroup* vibrationFactGroup () { return &_vibrationFactGroup; } - FactGroup* temperatureFactGroup () { return &_temperatureFactGroup; } - FactGroup* clockFactGroup () { return &_clockFactGroup; } - FactGroup* setpointFactGroup () { return &_setpointFactGroup; } - FactGroup* distanceSensorFactGroup () { return &_distanceSensorFactGroup; } - FactGroup* localPositionFactGroup () { return &_localPositionFactGroup; } - FactGroup* localPositionSetpointFactGroup() { return &_localPositionSetpointFactGroup; } - FactGroup* estimatorStatusFactGroup () { return &_estimatorStatusFactGroup; } - FactGroup* terrainFactGroup () { return &_terrainFactGroup; } - FactGroup* hygrometerFactGroup () { return &_hygrometerFactGroup; } - FactGroup* generatorFactGroup () { return &_generatorFactGroup; } - FactGroup* efiFactGroup () { return &_efiFactGroup; } - FactGroup* rpmFactGroup () { return &_rpmFactGroup; } - - QmlObjectListModel* batteries () { return &_batteryFactGroupListModel; } - QmlObjectListModel* escs () { return &_escStatusFactGroupListModel; } + FactGroup* gpsFactGroup (); + FactGroup* gps2FactGroup (); + FactGroup* gpsAggregateFactGroup (); + FactGroup* windFactGroup (); + FactGroup* vibrationFactGroup (); + FactGroup* temperatureFactGroup (); + FactGroup* clockFactGroup (); + FactGroup* setpointFactGroup (); + FactGroup* distanceSensorFactGroup (); + FactGroup* localPositionFactGroup (); + FactGroup* localPositionSetpointFactGroup(); + FactGroup* estimatorStatusFactGroup (); + FactGroup* terrainFactGroup (); + FactGroup* hygrometerFactGroup (); + FactGroup* generatorFactGroup (); + FactGroup* efiFactGroup (); + FactGroup* rpmFactGroup (); + + QmlObjectListModel* batteries (); + QmlObjectListModel* escs (); MissionManager* missionManager () { return _missionManager; } GeoFenceManager* geoFenceManager () { return _geoFenceManager; } @@ -862,6 +845,10 @@ public slots: /// @param channelValues The current values for rc channels void rcChannelsChanged(QVector channelValues); + /// New SERVO output values coming from SERVO_OUTPUT_RAW message + /// @param servoValues The current servo output values in microseconds (0-15 -> SERVO1..SERVO16). Invalid values are -1. + void servoOutputsChanged(QVector servoValues); + /// Remote control RSSI changed (0% - 100%) void remoteControlRSSIChanged (uint8_t rssi); @@ -1035,6 +1022,7 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); bool _readyToFly = false; bool _allSensorsHealthy = true; bool _mavlinkSigning = false; + QString _mavlinkSigningKeyName; SysStatusSensorInfo _sysStatusSensorInfo; @@ -1050,6 +1038,7 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); VehicleObjectAvoidance* _objectAvoidance = nullptr; Autotune* _autotune = nullptr; GimbalController* _gimbalController = nullptr; + VehicleSupports* _vehicleSupports = nullptr; bool _armed = false; ///< true: vehicle is armed uint8_t _base_mode = 0; ///< base_mode from HEARTBEAT @@ -1149,8 +1138,8 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); RequestMessageResultHandler resultHandler = nullptr; void* resultHandlerData = nullptr; bool commandAckReceived = false; // We keep track of the ack/message being received since the order in which this will come in is random - bool messageReceived = false; // We only delete the allocated RequestMessageInfo_t when both happen (or the message wait times out) - QElapsedTimer messageWaitElapsedTimer; // Elapsed time since we started waiting for the message to show up + bool messageReceived = false; // We only delete the allocated RequestMessageInfo_t when both the message is received and we get the ack + QElapsedTimer messageWaitElapsedTimer; // Elapsed time since we started waiting message to show up mavlink_message_t message; } RequestMessageInfo_t; @@ -1239,27 +1228,30 @@ void _activeVehicleChanged (Vehicle* newActiveVehicle); const QString _rpmFactGroupName = QStringLiteral("rpm"); VehicleFactGroup* _vehicleFactGroup; - VehicleGPSFactGroup _gpsFactGroup; - VehicleGPS2FactGroup _gps2FactGroup; - VehicleGPSAggregateFactGroup _gpsAggregateFactGroup; - VehicleWindFactGroup _windFactGroup; - VehicleVibrationFactGroup _vibrationFactGroup; - VehicleTemperatureFactGroup _temperatureFactGroup; - VehicleClockFactGroup _clockFactGroup; - VehicleSetpointFactGroup _setpointFactGroup; - VehicleDistanceSensorFactGroup _distanceSensorFactGroup; - VehicleLocalPositionFactGroup _localPositionFactGroup; - VehicleLocalPositionSetpointFactGroup _localPositionSetpointFactGroup; - VehicleEstimatorStatusFactGroup _estimatorStatusFactGroup; - VehicleHygrometerFactGroup _hygrometerFactGroup; - VehicleGeneratorFactGroup _generatorFactGroup; - VehicleEFIFactGroup _efiFactGroup; - VehicleRPMFactGroup _rpmFactGroup; - TerrainFactGroup _terrainFactGroup; + VehicleGPSFactGroup* _gpsFactGroup = nullptr; + VehicleGPS2FactGroup* _gps2FactGroup = nullptr; + VehicleGPSAggregateFactGroup* _gpsAggregateFactGroup = nullptr; + VehicleWindFactGroup* _windFactGroup = nullptr; + VehicleVibrationFactGroup* _vibrationFactGroup = nullptr; + VehicleTemperatureFactGroup* _temperatureFactGroup = nullptr; + VehicleClockFactGroup* _clockFactGroup = nullptr; + VehicleSetpointFactGroup* _setpointFactGroup = nullptr; + VehicleDistanceSensorFactGroup* _distanceSensorFactGroup = nullptr; + VehicleLocalPositionFactGroup* _localPositionFactGroup = nullptr; + VehicleLocalPositionSetpointFactGroup* _localPositionSetpointFactGroup = nullptr; + VehicleEstimatorStatusFactGroup* _estimatorStatusFactGroup = nullptr; + VehicleHygrometerFactGroup* _hygrometerFactGroup = nullptr; + VehicleGeneratorFactGroup* _generatorFactGroup = nullptr; + VehicleEFIFactGroup* _efiFactGroup = nullptr; + VehicleRPMFactGroup* _rpmFactGroup = nullptr; + TerrainFactGroup* _terrainFactGroup = nullptr; + + // Live SERVO_OUTPUT_RAW values (microseconds). Indexed 0..15 -> SERVO1..SERVO16. + QVector _servoOutputRawValues = QVector(16, -1); // Dynamic FactGroups - BatteryFactGroupListModel _batteryFactGroupListModel; - EscStatusFactGroupListModel _escStatusFactGroupListModel; + BatteryFactGroupListModel* _batteryFactGroupListModel = nullptr; + EscStatusFactGroupListModel* _escStatusFactGroupListModel = nullptr; TerrainProtocolHandler* _terrainProtocolHandler = nullptr; diff --git a/src/Vehicle/VehicleObjectAvoidance.cc b/src/Vehicle/VehicleObjectAvoidance.cc index d903799c1abd..28fce3ac5d14 100644 --- a/src/Vehicle/VehicleObjectAvoidance.cc +++ b/src/Vehicle/VehicleObjectAvoidance.cc @@ -1,6 +1,7 @@ #include "VehicleObjectAvoidance.h" #include "Vehicle.h" #include "ParameterManager.h" +#include "VehicleSetpointFactGroup.h" //----------------------------------------------------------------------------- VehicleObjectAvoidance::VehicleObjectAvoidance(Vehicle *vehicle, QObject* parent) diff --git a/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc b/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc index 55d28659cea7..36993d248a96 100644 --- a/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc +++ b/src/Vehicle/VehicleSetup/FirmwareUpgradeController.cc @@ -89,7 +89,8 @@ static QMap px4_board_name_map { {7000, "cuav_7-nano_default"}, {7001, "cuav_fmu-v6x_default"}, {7002, "cuav_x25-evo_default"}, - {7003, "cuav_x25-super_default"} + {7003, "cuav_x25-super_default"}, + {7004, "cuav_x25-mega_default"} }; uint qHash(const FirmwareUpgradeController::FirmwareIdentifier& firmwareId) diff --git a/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml b/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml index 1a00009b14ee..0667eee23ba7 100644 --- a/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml +++ b/src/Vehicle/VehicleSetup/JoystickComponentButtons.qml @@ -107,7 +107,7 @@ ColumnLayout { /*Column { id: buttonCol Layout.fillWidth: true - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton spacing: ScreenTools.defaultFontPixelHeight / 3 Row { @@ -123,7 +123,7 @@ ColumnLayout { } QGCLabel { width: ScreenTools.defaultFontPixelWidth * 26 - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton text: qsTr("Shift Function: ") } } @@ -133,7 +133,7 @@ ColumnLayout { Row { spacing: ScreenTools.defaultFontPixelWidth - visible: globals.activeVehicle.supportsJSButton + visible: globals.activeVehicle.supports.jsButton property var parameterName: `BTN${index}_FUNCTION` property var parameterShiftName: `BTN${index}_SFUNCTION` property bool hasFirmwareSupport: controller.parameterExists(-1, parameterName) @@ -217,7 +217,7 @@ ColumnLayout { id: repeatCheck text: qsTr("Repeat") enabled: currentAssignableAction && joystick.calibrated && currentAssignableAction.canRepeat - visible: !globals.activeVehicle.supportsJSButton + visible: !globals.activeVehicle.supports.jsButton onClicked: { joystick.setButtonRepeat(modelData, checked) diff --git a/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml b/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml index 269c9500a769..e7ba50d6d247 100644 --- a/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml +++ b/src/Vehicle/VehicleSetup/JoystickComponentSettings.qml @@ -46,7 +46,7 @@ ColumnLayout { Layout.fillWidth: true text: qsTr("Negative Thrust") fact: _joystickSettings.negativeThrust - visible: globals.activeVehicle.supportsNegativeThrust && fact.visible + visible: globals.activeVehicle.supports.negativeThrust && fact.visible } QGCCheckBoxSlider { diff --git a/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml b/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml index 388a48044df3..c76c9673ac13 100644 --- a/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml +++ b/src/Vehicle/VehicleSetup/RemoteControlCalibration.qml @@ -228,11 +228,16 @@ ColumnLayout { onClicked: { if (text === qsTr("Calibrate")) { if (controller.channelCount < controller.minChannelCount) { - QGroundControl.showMessageDialog(root, qsTr("Remote Not Ready"), - controller.channelCount == 0 ? qsTr("Please turn on remote.") : - (controller.channelCount < controller.minChannelCount ? - qsTr("%1 channels or more are needed to fly.").arg(controller.minChannelCount) : - qsTr("Ready to calibrate."))) + let errorMessage = "" + let title = "" + if (controller.joystickMode) { + title = qsTr("Joystick Not Ready") + errorMessage = qsTr("%1 axes or more are needed to fly. Joystick is reporting %2 axes.").arg(controller.minChannelCount).arg(controller.channelCount) + } else { + title = qsTr("Not Ready") + errorMessage = controller.channelCount === 0 ? qsTr("Please turn on RC transmitter.") : qsTr("%1 channels or more are needed to fly.").arg(controller.minChannelCount) + } + QGroundControl.showMessageDialog(root, title, errorMessage) return } else if (!controller.joystickMode) { QGroundControl.showMessageDialog(root, qsTr("Zero Trims"), diff --git a/src/Vehicle/VehicleSetup/VehicleSummary.qml b/src/Vehicle/VehicleSetup/VehicleSummary.qml index 305ec83a2f40..5b93bb705891 100644 --- a/src/Vehicle/VehicleSetup/VehicleSummary.qml +++ b/src/Vehicle/VehicleSetup/VehicleSummary.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import QGroundControl import QGroundControl.Controls @@ -14,6 +15,7 @@ Rectangle { property real _minSummaryW: ScreenTools.isTinyScreen ? ScreenTools.defaultFontPixelWidth * 28 : ScreenTools.defaultFontPixelWidth * 36 property real _summaryBoxWidth: _minSummaryW property real _summaryBoxSpace: ScreenTools.defaultFontPixelWidth * 2 + property real _margins: ScreenTools.defaultFontPixelHeight / 2 function computeSummaryBoxSize() { var sw = 0 @@ -85,52 +87,58 @@ Rectangle { // Outer summary item rectangle Rectangle { - width: _summaryBoxWidth - height: ScreenTools.defaultFontPixelHeight * 13 - color: qgcPal.windowShade - visible: modelData.summaryQmlSource.toString() !== "" + width: mainLayout.width + (_margins * 2) + height: mainLayout.height + (_margins * 2) + color: qgcPal.windowShade + visible: modelData.summaryQmlSource.toString() !== "" border.width: 1 border.color: qgcPal.text + Component.onCompleted: { border.color = Qt.rgba(border.color.r, border.color.g, border.color.b, 0.1) } readonly property real titleHeight: ScreenTools.defaultFontPixelHeight * 2 - // Title bar - QGCButton { - id: titleBar - width: parent.width - height: titleHeight - text: capitalizeWords(modelData.name) - - // Setup indicator - Rectangle { - anchors.rightMargin: ScreenTools.defaultFontPixelWidth - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: ScreenTools.defaultFontPixelWidth * 1.75 - height: width - radius: width / 2 - color: modelData.setupComplete ? "#00d932" : "red" - visible: modelData.requiresSetup && modelData.setupSource !== "" - } + ColumnLayout { + id: mainLayout + anchors.margins: _margins + anchors.left: parent.left + anchors.top: parent.top + spacing: ScreenTools.defaultFontPixelHeight / 2 + + // Title bar + QGCButton { + Layout.fillWidth: true + Layout.preferredHeight: titleHeight + text: capitalizeWords(modelData.name) + + // Setup indicator + Rectangle { + anchors.rightMargin: ScreenTools.defaultFontPixelWidth + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + width: ScreenTools.defaultFontPixelWidth * 1.75 + height: width + radius: width / 2 + color: modelData.setupComplete ? "#00d932" : "red" + visible: modelData.requiresSetup && modelData.setupSource !== "" + } - onClicked : { - //console.log(modelData.setupSource) - if (modelData.setupSource !== "") { - setupView.showVehicleComponentPanel(modelData) + onClicked : { + if (modelData.setupSource !== "") { + setupView.showVehicleComponentPanel(modelData) + } } } - } - // Summary Qml - Rectangle { - anchors.top: titleBar.bottom - width: parent.width + + // Summary Qml Loader { - anchors.fill: parent - anchors.margins: ScreenTools.defaultFontPixelWidth - source: modelData.summaryQmlSource + id: summaryLoader + Layout.fillWidth: true + Layout.preferredWidth: item ? item.implicitWidth : 0 + Layout.preferredHeight: item ? item.implicitHeight : 0 + source: modelData.summaryQmlSource property var vehicleComponent: modelData } diff --git a/src/Vehicle/VehicleSupports.cc b/src/Vehicle/VehicleSupports.cc new file mode 100644 index 000000000000..7529ee262185 --- /dev/null +++ b/src/Vehicle/VehicleSupports.cc @@ -0,0 +1,88 @@ +#include "VehicleSupports.h" + +#include "FirmwarePlugin/FirmwarePlugin.h" +#include "Vehicle.h" + +VehicleSupports::VehicleSupports(Vehicle *vehicle) + : QObject(vehicle) + , _vehicle(vehicle) +{ + connect(_vehicle, &Vehicle::firmwareTypeChanged, this, &VehicleSupports::terrainFrameChanged); +} + +bool VehicleSupports::throttleModeCenterZero() const +{ + return _vehicle->firmwarePlugin()->supportsThrottleModeCenterZero(); +} + +bool VehicleSupports::negativeThrust() const +{ + return _vehicle->firmwarePlugin()->supportsNegativeThrust(_vehicle); +} + +bool VehicleSupports::jsButton() const +{ + return _vehicle->firmwarePlugin()->supportsJSButton(); +} + +bool VehicleSupports::radio() const +{ + return _vehicle->firmwarePlugin()->supportsRadio(); +} + +bool VehicleSupports::motorInterference() const +{ + return _vehicle->firmwarePlugin()->supportsMotorInterference(); +} + +bool VehicleSupports::smartRTL() const +{ + return _vehicle->firmwarePlugin()->supportsSmartRTL(); +} + +bool VehicleSupports::terrainFrame() const +{ + return !_vehicle->px4Firmware(); +} + +bool VehicleSupports::guidedMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::GuidedModeCapability); +} + +bool VehicleSupports::pauseVehicle() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::PauseVehicleCapability); +} + +bool VehicleSupports::orbitMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::OrbitModeCapability); +} + +bool VehicleSupports::roiMode() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::ROIModeCapability); +} + +bool VehicleSupports::takeoffMissionCommand() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::TakeoffVehicleCapability); +} + +bool VehicleSupports::guidedTakeoffWithAltitude() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::GuidedTakeoffCapability); +} + +bool VehicleSupports::guidedTakeoffWithoutAltitude() const +{ + auto* fp = _vehicle->firmwarePlugin(); + return fp->isCapable(_vehicle, FirmwarePlugin::TakeoffVehicleCapability) + && !fp->isCapable(_vehicle, FirmwarePlugin::GuidedTakeoffCapability); +} + +bool VehicleSupports::changeHeading() const +{ + return _vehicle->firmwarePlugin()->isCapable(_vehicle, FirmwarePlugin::ChangeHeadingCapability); +} diff --git a/src/Vehicle/VehicleSupports.h b/src/Vehicle/VehicleSupports.h new file mode 100644 index 000000000000..dc7c45b4e687 --- /dev/null +++ b/src/Vehicle/VehicleSupports.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +class FirmwarePlugin; +class Vehicle; + +class VehicleSupports : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + explicit VehicleSupports(Vehicle *vehicle); + + Q_PROPERTY(bool throttleModeCenterZero READ throttleModeCenterZero CONSTANT) + Q_PROPERTY(bool negativeThrust READ negativeThrust CONSTANT) + Q_PROPERTY(bool jsButton READ jsButton CONSTANT) + Q_PROPERTY(bool radio READ radio CONSTANT) + Q_PROPERTY(bool motorInterference READ motorInterference CONSTANT) + Q_PROPERTY(bool smartRTL READ smartRTL CONSTANT) + Q_PROPERTY(bool terrainFrame READ terrainFrame NOTIFY terrainFrameChanged) + Q_PROPERTY(bool guidedMode READ guidedMode CONSTANT) + Q_PROPERTY(bool pauseVehicle READ pauseVehicle CONSTANT) + Q_PROPERTY(bool orbitMode READ orbitMode CONSTANT) + Q_PROPERTY(bool roiMode READ roiMode CONSTANT) + Q_PROPERTY(bool takeoffMissionCommand READ takeoffMissionCommand CONSTANT) + Q_PROPERTY(bool guidedTakeoffWithAltitude READ guidedTakeoffWithAltitude CONSTANT) + Q_PROPERTY(bool guidedTakeoffWithoutAltitude READ guidedTakeoffWithoutAltitude CONSTANT) + Q_PROPERTY(bool changeHeading READ changeHeading CONSTANT) + + bool throttleModeCenterZero() const; + bool negativeThrust() const; + bool jsButton() const; + bool radio() const; + bool motorInterference() const; + bool smartRTL() const; + bool terrainFrame() const; + bool guidedMode() const; + bool pauseVehicle() const; + bool orbitMode() const; + bool roiMode() const; + bool takeoffMissionCommand() const; + bool guidedTakeoffWithAltitude() const; + bool guidedTakeoffWithoutAltitude() const; + bool changeHeading() const; + +signals: + void terrainFrameChanged(); + +private: + Vehicle *_vehicle = nullptr; +}; diff --git a/src/VideoManager/VideoManager.cc b/src/VideoManager/VideoManager.cc index 553772074ab8..7f1b73a1d829 100644 --- a/src/VideoManager/VideoManager.cc +++ b/src/VideoManager/VideoManager.cc @@ -1,6 +1,6 @@ #include "VideoManager.h" #include "AppSettings.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MultiVehicleManager.h" #include "QGCApplication.h" #include "QGCCameraManager.h" @@ -11,21 +11,23 @@ #include "Vehicle.h" #include "VideoReceiver.h" #include "VideoSettings.h" -#ifdef QGC_GST_STREAMING -#include "GStreamer.h" -#include "VideoItemStub.h" -#else #include "VideoItemStub.h" -#endif #include "QtMultimediaReceiver.h" #include "UVCReceiver.h" +#ifdef QGC_GST_STREAMING +#include "GStreamer.h" +#include "GStreamerHelpers.h" +#endif #include #include +#include +#include +#include +#include #include #include #include -#include QGC_LOGGING_CATEGORY(VideoManagerLog, "Video.VideoManager") @@ -37,6 +39,11 @@ static constexpr const char *kFileExtension[VideoReceiver::FILE_FORMAT_MAX + 1] Q_APPLICATION_STATIC(VideoManager, _videoManagerInstance); +bool VideoManager::_shouldSkipGStreamerForUnitTests() +{ + return qgcApp() && qgcApp()->runningUnitTests() && !qEnvironmentVariableIsSet("QGC_TEST_ENABLE_GSTREAMER"); +} + VideoManager::VideoManager(QObject *parent) : QObject(parent) , _subtitleWriter(new SubtitleWriter(this)) @@ -46,19 +53,17 @@ VideoManager::VideoManager(QObject *parent) (void) qRegisterMetaType("STATUS"); + bool needsStub = true; #ifdef QGC_GST_STREAMING - const bool skipGStreamerForUnitTests = - qgcApp() && qgcApp()->runningUnitTests() && !qEnvironmentVariableIsSet("QGC_TEST_ENABLE_GSTREAMER"); - - if (skipGStreamerForUnitTests) { - (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); + _gstreamerDisabledForUnitTests = _shouldSkipGStreamerForUnitTests(); + needsStub = _gstreamerDisabledForUnitTests; + if (_gstreamerDisabledForUnitTests) { qCInfo(VideoManagerLog) << "Skipping GStreamer initialization for unit tests"; - } else if (!GStreamer::initialize()) { - qCCritical(VideoManagerLog) << "Failed To Initialize GStreamer"; } -#else - (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); #endif + if (needsStub) { + (void) qmlRegisterType("org.freedesktop.gstreamer.Qt6GLVideoItem", 1, 0, "GstGLQt6VideoItem"); + } } VideoManager::~VideoManager() @@ -71,6 +76,85 @@ VideoManager *VideoManager::instance() return _videoManagerInstance(); } +void VideoManager::startGStreamerInit() +{ +#ifdef QGC_GST_STREAMING + if (_gstreamerDisabledForUnitTests) { + _initState = InitState::GstReady; + qCInfo(VideoManagerLog) << "GStreamer initialization disabled for unit tests"; + return; + } + + if (_initState != InitState::NotStarted) { + qCWarning(VideoManagerLog) << "GStreamer init already started"; + return; + } + + _initState = InitState::Pending; + _gstInitFuture = GStreamer::initializeAsync(); + _gstInitFuture.then(this, [this](bool success) { + _onGstInitComplete(success); + }).onCanceled(this, [this] { + _onGstInitComplete(false); + }); +#endif +} + +bool VideoManager::waitForGStreamerInit(int timeoutMs) +{ +#ifdef QGC_GST_STREAMING + if (_gstreamerDisabledForUnitTests) { + return true; + } + + if (_initState == InitState::NotStarted) { + startGStreamerInit(); + } + + switch (_initState) { + case InitState::Failed: + return false; + case InitState::GstReady: + case InitState::Running: + return true; + default: + break; + } + + if (!_gstInitFuture.isValid()) { + qCCritical(VideoManagerLog) << "waitForGStreamerInit: no valid future"; + return false; + } + + QEventLoop loop; + QTimer timer; + timer.setSingleShot(true); + QFutureWatcher watcher; + (void) connect(&watcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit); + (void) connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit); + + watcher.setFuture(_gstInitFuture); + if (!watcher.isFinished()) { + timer.start(timeoutMs); + loop.exec(); + } + + if (!watcher.isFinished()) { + qCCritical(VideoManagerLog) << "Timed out waiting for GStreamer init"; + return false; + } + + const bool success = watcher.result(); + if (_initState == InitState::Pending || _initState == InitState::QmlReady) { + _onGstInitComplete(success); + } + return _initState != InitState::Failed; +#else + Q_UNUSED(timeoutMs); + return true; +#endif +} + void VideoManager::init(QQuickWindow *mainWindow) { if (_initialized) { @@ -95,25 +179,83 @@ void VideoManager::init(QQuickWindow *mainWindow) (void) connect(this, &VideoManager::autoStreamConfiguredChanged, this, &VideoManager::_videoSourceChanged); - _mainWindow->scheduleRenderJob(new FinishVideoInitialization(), QQuickWindow::AfterSynchronizingStage); +#ifdef QGC_GST_STREAMING + if (_initState == InitState::NotStarted) { + startGStreamerInit(); + } +#endif + + _mainWindow->scheduleRenderJob( + QRunnable::create([this] { + QMetaObject::invokeMethod(this, &VideoManager::_initAfterQmlIsReady, Qt::QueuedConnection); + }), + QQuickWindow::AfterSynchronizingStage); _initialized = true; } void VideoManager::_initAfterQmlIsReady() { - if (_initAfterQmlIsReadyDone) { - qCWarning(VideoManagerLog) << "_initAfterQmlIsReady called multiple times"; - return; - } if (!_mainWindow) { qCCritical(VideoManagerLog) << "_initAfterQmlIsReady called with NULL mainWindow"; return; } - _initAfterQmlIsReadyDone = true; qCDebug(VideoManagerLog) << "_initAfterQmlIsReady"; +#ifdef QGC_GST_STREAMING + switch (_initState) { + case InitState::Pending: + _initState = InitState::QmlReady; + qCDebug(VideoManagerLog) << "QML ready, waiting for GStreamer"; + return; + case InitState::GstReady: + _initState = InitState::Running; + qCDebug(VideoManagerLog) << "QML ready, GStreamer already done — creating receivers"; + break; + case InitState::Failed: + qCWarning(VideoManagerLog) << "QML ready but GStreamer init failed"; + return; + default: + qCWarning(VideoManagerLog) << "_initAfterQmlIsReady: unexpected state" << static_cast(_initState); + return; + } +#endif + _createVideoReceivers(); +} + +void VideoManager::_onGstInitComplete(bool success) +{ + if (!success) { + _initState = InitState::Failed; + qCCritical(VideoManagerLog) << "GStreamer initialization failed"; + return; + } + +#ifdef QGC_GST_STREAMING + const auto decoderOption = static_cast( + _videoSettings->forceVideoDecoder()->rawValue().toInt()); + GStreamer::setCodecPriorities(decoderOption); +#endif + + switch (_initState) { + case InitState::Pending: + _initState = InitState::GstReady; + qCDebug(VideoManagerLog) << "GStreamer ready, waiting for QML"; + return; + case InitState::QmlReady: + _initState = InitState::Running; + qCDebug(VideoManagerLog) << "GStreamer ready, QML already done — creating receivers"; + _createVideoReceivers(); + return; + default: + qCWarning(VideoManagerLog) << "_onGstInitComplete: unexpected state" << static_cast(_initState); + return; + } +} + +void VideoManager::_createVideoReceivers() +{ static const QStringList videoStreamList = { "videoContent", "thermalVideo" @@ -561,7 +703,7 @@ void VideoManager::_setActiveVehicle(Vehicle *vehicle) (void) disconnect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged); auto cameraManager = _activeVehicle->cameraManager(); if (cameraManager) { - MavlinkCameraControl *pCamera = cameraManager->currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = cameraManager->currentCameraInstance(); if (pCamera) { pCamera->stopStream(); } @@ -579,7 +721,7 @@ void VideoManager::_setActiveVehicle(Vehicle *vehicle) (void) connect(_activeVehicle->vehicleLinkManager(), &VehicleLinkManager::communicationLostChanged, this, &VideoManager::_communicationLostChanged); if (_activeVehicle->cameraManager()) { (void) connect(_activeVehicle->cameraManager(), &QGCCameraManager::streamChanged, this, &VideoManager::_videoSourceChanged); - MavlinkCameraControl *pCamera = _activeVehicle->cameraManager()->currentCameraInstance(); + MavlinkCameraControlInterface *pCamera = _activeVehicle->cameraManager()->currentCameraInstance(); if (pCamera) { pCamera->resumeStream(); } @@ -808,22 +950,3 @@ void VideoManager::startVideo() _restartAllVideos(); } - -/*===========================================================================*/ - -FinishVideoInitialization::FinishVideoInitialization() - : QRunnable() -{ - // qCDebug(VideoManagerLog) << this; -} - -FinishVideoInitialization::~FinishVideoInitialization() -{ - // qCDebug(VideoManagerLog) << this; -} - -void FinishVideoInitialization::run() -{ - qCDebug(VideoManagerLog) << "FinishVideoInitialization::run"; - QMetaObject::invokeMethod(VideoManager::instance(), &VideoManager::_initAfterQmlIsReady, Qt::QueuedConnection); -} diff --git a/src/VideoManager/VideoManager.h b/src/VideoManager/VideoManager.h index 04bca3349be4..dc7a4d0466b0 100644 --- a/src/VideoManager/VideoManager.h +++ b/src/VideoManager/VideoManager.h @@ -1,15 +1,14 @@ #pragma once +#include #include #include -#include #include #include Q_DECLARE_LOGGING_CATEGORY(VideoManagerLog) class QQuickWindow; -class FinishVideoInitialization; class SubtitleWriter; class Vehicle; class VideoReceiver; @@ -46,8 +45,6 @@ class VideoManager : public QObject explicit VideoManager(QObject *parent = nullptr); ~VideoManager(); - friend class FinishVideoInitialization; - static VideoManager *instance(); Q_INVOKABLE void grabImage(const QString &imageFile = QString()); @@ -57,6 +54,8 @@ class VideoManager : public QObject Q_INVOKABLE void stopVideo(); void init(QQuickWindow *mainWindow); + void startGStreamerInit(); + bool waitForGStreamerInit(int timeoutMs = 60000); void cleanup(); bool autoStreamConfigured() const; bool decoding() const { return _decoding; } @@ -101,7 +100,19 @@ private slots: void _videoSourceChanged(); private: + enum class InitState : uint8_t { + NotStarted, + Pending, + GstReady, + QmlReady, + Running, + Failed + }; + + static bool _shouldSkipGStreamerForUnitTests(); void _initAfterQmlIsReady(); + void _onGstInitComplete(bool success); + void _createVideoReceivers(); void _initVideoReceiver(VideoReceiver *receiver, QQuickWindow *window); bool _updateAutoStream(VideoReceiver *receiver); bool _updateUVC(VideoReceiver *receiver); @@ -114,30 +125,21 @@ private slots: static void _cleanupOldVideos(); QList _videoReceivers; - SubtitleWriter *_subtitleWriter = nullptr; VideoSettings *_videoSettings = nullptr; + QQuickWindow *_mainWindow = nullptr; + Vehicle *_activeVehicle = nullptr; + InitState _initState = InitState::NotStarted; + QFuture _gstInitFuture; bool _initialized = false; - bool _initAfterQmlIsReadyDone = false; + bool _gstreamerDisabledForUnitTests = false; bool _fullScreen = false; + QAtomicInteger _decoding = false; QAtomicInteger _recording = false; QAtomicInteger _streaming = false; QSize _videoSize; QString _imageFile; QString _uvcVideoSourceID; - Vehicle *_activeVehicle = nullptr; - QQuickWindow *_mainWindow = nullptr; -}; - -/*===========================================================================*/ - -class FinishVideoInitialization : public QRunnable -{ -public: - FinishVideoInitialization(); - ~FinishVideoInitialization(); - - void run() final; }; diff --git a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt index 057db82665df..ec48a788370f 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/CMakeLists.txt @@ -1,37 +1,27 @@ -# ============================================================================ -# GStreamer Video Receiver Backend -# GStreamer-based video streaming and decoding -# ============================================================================ - target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_sources(${CMAKE_PROJECT_NAME} PRIVATE VideoItemStub.h) -# ============================================================================ -# GStreamer Detection and Configuration -# ============================================================================ - if(QGC_ENABLE_GST_VIDEOSTREAMING) - if(NOT MACOS) - # NOTE: Using FindGStreamer.cmake is currently bypassed on macOS - # Using hardwired framework path as a workaround until FindGStreamer works on macOS - find_package(GStreamer - REQUIRED - COMPONENTS Core Base Video Gl GlPrototypes Rtsp - OPTIONAL_COMPONENTS GlEgl GlWayland GlX11 - ) + find_package(QGCGStreamer + REQUIRED + COMPONENTS Core Base Video Gl GlPrototypes Rtsp + OPTIONAL_COMPONENTS GlEgl GlWayland GlX11 + ) + + if(GStreamer_USE_STATIC_LIBS) + foreach(_plugin IN LISTS GSTREAMER_PLUGINS) + if(GST_PLUGIN_${_plugin}_FOUND) + target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE GST_PLUGIN_${_plugin}_FOUND) + endif() + endforeach() endif() - # Build GStreamer Qt6 QML GL plugin add_subdirectory(gstqml6gl) # TODO: Add Qt6 Direct3D11 plugin support # https://gstreamer.freedesktop.org/documentation/qt6d3d11/index.html#qml6d3d11sink-page endif() -# ============================================================================ -# GStreamer Video Receiver Sources -# ============================================================================ - if(TARGET gstqml6gl) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE gstqml6gl) @@ -41,64 +31,74 @@ if(TARGET gstqml6gl) GStreamer.h GStreamerHelpers.cc GStreamerHelpers.h + GStreamerLogging.cc + GStreamerLogging.h GstVideoReceiver.cc GstVideoReceiver.h ) - # Build custom GStreamer QGC plugin add_subdirectory(gstqgc) target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_GST_STREAMING) - # ============================================================================ - # Platform-Specific GStreamer Installation - # ============================================================================ - if(LINUX) - # ---------------------------------------------------------------------------- - # Linux: Install GStreamer Plugins and Libraries - # ---------------------------------------------------------------------------- - install( - DIRECTORY + # Fixed "lib" to match AppRun expectations (CMAKE_INSTALL_LIBDIR varies by distro) + gstreamer_install_plugins( + SOURCE_DIR "${GSTREAMER_PLUGIN_PATH}" + DEST_DIR "lib/gstreamer-1.0" + EXTENSION "so" + PREFIX "libgst" + ) + gstreamer_install_gio_modules( + SOURCE_DIR "${GSTREAMER_LIB_PATH}/gio/modules" + DEST_DIR "lib/gio/modules" + EXTENSION "so" + ) + # Helper binary path varies by distro: Debian lib//gstreamer1.0/, + # Fedora libexec/gstreamer-1.0/, Arch lib/gstreamer-1.0/. + # We install to Debian convention (lib/gstreamer1.0/gstreamer-1.0/); + # runtime env var GST_PLUGIN_SCANNER in GStreamer.cc matches this path. + find_program(_GST_PLUGIN_SCANNER gst-plugin-scanner + PATHS + "${GSTREAMER_LIB_PATH}/gstreamer1.0/gstreamer-1.0" + "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0" "${GSTREAMER_PLUGIN_PATH}" - "${GSTREAMER_LIB_PATH}/gio" - DESTINATION "${CMAKE_INSTALL_LIBDIR}" - FILES_MATCHING - PATTERN "*.so" - PATTERN "*.so.*" - PATTERN "*/include" EXCLUDE + NO_DEFAULT_PATH ) - install( - DIRECTORY "${GSTREAMER_LIB_PATH}/gstreamer1.0" - DESTINATION "${CMAKE_INSTALL_LIBDIR}" - FILE_PERMISSIONS - OWNER_READ OWNER_WRITE OWNER_EXECUTE - GROUP_READ GROUP_EXECUTE - WORLD_READ WORLD_EXECUTE - FILES_MATCHING - PATTERN "gst-*" + if(_GST_PLUGIN_SCANNER) + install(PROGRAMS "${_GST_PLUGIN_SCANNER}" + DESTINATION "lib/gstreamer1.0/gstreamer-1.0") + else() + message(WARNING "gst-plugin-scanner not found; AppImage video may not work") + endif() + find_program(_GST_PTP_HELPER gst-ptp-helper + PATHS + "${GSTREAMER_LIB_PATH}/gstreamer1.0/gstreamer-1.0" + "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0" + "${GSTREAMER_PLUGIN_PATH}" + NO_DEFAULT_PATH ) + if(_GST_PTP_HELPER) + install(PROGRAMS "${_GST_PTP_HELPER}" + DESTINATION "lib/gstreamer1.0/gstreamer-1.0") + endif() + elseif(WIN32) - # ---------------------------------------------------------------------------- - # Windows: Install GStreamer DLLs and Plugins - # ---------------------------------------------------------------------------- - install( - DIRECTORY "${GStreamer_ROOT_DIR}/bin/" - DESTINATION "${CMAKE_INSTALL_BINDIR}" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_libs( + SOURCE_DIR "${GStreamer_ROOT_DIR}/bin" + DEST_DIR "${CMAKE_INSTALL_BINDIR}" + EXTENSION "dll" ) - install( - DIRECTORY "${GSTREAMER_LIB_PATH}/gio/modules/" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/gio/modules" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_gio_modules( + SOURCE_DIR "${GSTREAMER_LIB_PATH}/gio/modules" + DEST_DIR "${CMAKE_INSTALL_LIBDIR}/gio/modules" + EXTENSION "dll" ) - install( - DIRECTORY "${GSTREAMER_PLUGIN_PATH}/" - DESTINATION "${CMAKE_INSTALL_LIBDIR}/gstreamer-1.0" - FILES_MATCHING - PATTERN "*.dll" + gstreamer_install_plugins( + SOURCE_DIR "${GSTREAMER_PLUGIN_PATH}" + DEST_DIR "${CMAKE_INSTALL_LIBDIR}/gstreamer-1.0" + EXTENSION "dll" + PREFIX "gst" ) install( DIRECTORY "${GStreamer_ROOT_DIR}/libexec/gstreamer-1.0/" @@ -110,14 +110,17 @@ if(TARGET gstqml6gl) FILES_MATCHING PATTERN "*.exe" ) + elseif(MACOS) - # ---------------------------------------------------------------------------- - # macOS: Install GStreamer Framework - # ---------------------------------------------------------------------------- if(GSTREAMER_FRAMEWORK) + # Resolve the framework real path before install. Some SDK layouts expose + # the framework as a symlink; copying through the symlink can leave an + # unsigned wrapper path inside the app bundle. + get_filename_component(_gst_framework_real "${GSTREAMER_FRAMEWORK}" REALPATH) install( - DIRECTORY "${GSTREAMER_FRAMEWORK}" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_PROJECT_NAME}.app/Contents/Frameworks" + DIRECTORY "${_gst_framework_real}/" + DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_PROJECT_NAME}.app/Contents/Frameworks/GStreamer.framework" + USE_SOURCE_PERMISSIONS PATTERN "*.la" EXCLUDE PATTERN "*.a" EXCLUDE PATTERN "*/bin" EXCLUDE @@ -127,7 +130,13 @@ if(TARGET gstqml6gl) PATTERN "*/include" EXCLUDE PATTERN "*/pkgconfig" EXCLUDE PATTERN "*/share" EXCLUDE + PATTERN "*gstpython*" EXCLUDE ) + else() + message(WARNING "macOS: Homebrew GStreamer detected. " + "Distributable builds require the GStreamer.framework SDK from " + "https://gstreamer.freedesktop.org/download/") endif() endif() + endif() diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc index 8e3c18c98cad..3f0794ce3974 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc @@ -1,43 +1,36 @@ #include "GStreamer.h" #include "GStreamerHelpers.h" +#include "GStreamerLogging.h" #include "AppSettings.h" #include "GstVideoReceiver.h" -#include "QGCLoggingCategory.h" -#include "SettingsManager.h" -#include "VideoSettings.h" +#include #include +#include +#include +#include #include #include #include -#include +#include -QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer") -QGC_LOGGING_CATEGORY(GStreamerDecoderRanksLog, "Video.GStreamerDecoderRanks") -QGC_LOGGING_CATEGORY_ON(GStreamerAPILog, "Video.GStreamerAPI") +#ifdef Q_OS_LINUX +#include +#endif + +#include -// TODO: Clean These up with Macros or CMake G_BEGIN_DECLS -GST_PLUGIN_STATIC_DECLARE(androidmedia); -GST_PLUGIN_STATIC_DECLARE(applemedia); +#ifdef QGC_GST_STATIC_BUILD GST_PLUGIN_STATIC_DECLARE(coreelements); -GST_PLUGIN_STATIC_DECLARE(d3d); -GST_PLUGIN_STATIC_DECLARE(d3d11); -GST_PLUGIN_STATIC_DECLARE(d3d12); -GST_PLUGIN_STATIC_DECLARE(dav1d); -GST_PLUGIN_STATIC_DECLARE(dxva); GST_PLUGIN_STATIC_DECLARE(isomp4); GST_PLUGIN_STATIC_DECLARE(libav); GST_PLUGIN_STATIC_DECLARE(matroska); GST_PLUGIN_STATIC_DECLARE(mpegtsdemux); -GST_PLUGIN_STATIC_DECLARE(msdk); -GST_PLUGIN_STATIC_DECLARE(nvcodec); GST_PLUGIN_STATIC_DECLARE(opengl); GST_PLUGIN_STATIC_DECLARE(openh264); GST_PLUGIN_STATIC_DECLARE(playback); -GST_PLUGIN_STATIC_DECLARE(qml6); -GST_PLUGIN_STATIC_DECLARE(qsv); GST_PLUGIN_STATIC_DECLARE(rtp); GST_PLUGIN_STATIC_DECLARE(rtpmanager); GST_PLUGIN_STATIC_DECLARE(rtsp); @@ -45,207 +38,474 @@ GST_PLUGIN_STATIC_DECLARE(sdpelem); GST_PLUGIN_STATIC_DECLARE(tcp); GST_PLUGIN_STATIC_DECLARE(typefindfunctions); GST_PLUGIN_STATIC_DECLARE(udp); -GST_PLUGIN_STATIC_DECLARE(va); +GST_PLUGIN_STATIC_DECLARE(videoconvertscale); GST_PLUGIN_STATIC_DECLARE(videoparsersbad); GST_PLUGIN_STATIC_DECLARE(vpx); + +#ifdef GST_PLUGIN_androidmedia_FOUND +GST_PLUGIN_STATIC_DECLARE(androidmedia); +#endif +#ifdef GST_PLUGIN_applemedia_FOUND +GST_PLUGIN_STATIC_DECLARE(applemedia); +#endif +#ifdef GST_PLUGIN_d3d_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d); +#endif +#ifdef GST_PLUGIN_d3d11_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d11); +#endif +#ifdef GST_PLUGIN_d3d12_FOUND +GST_PLUGIN_STATIC_DECLARE(d3d12); +#endif +#ifdef GST_PLUGIN_dav1d_FOUND +GST_PLUGIN_STATIC_DECLARE(dav1d); +#endif +#ifdef GST_PLUGIN_dxva_FOUND +GST_PLUGIN_STATIC_DECLARE(dxva); +#endif +#ifdef GST_PLUGIN_nvcodec_FOUND +GST_PLUGIN_STATIC_DECLARE(nvcodec); +#endif +#ifdef GST_PLUGIN_qsv_FOUND +GST_PLUGIN_STATIC_DECLARE(qsv); +#endif +#ifdef GST_PLUGIN_va_FOUND +GST_PLUGIN_STATIC_DECLARE(va); +#endif +#ifdef GST_PLUGIN_vulkan_FOUND GST_PLUGIN_STATIC_DECLARE(vulkan); +#endif +#endif +GST_PLUGIN_STATIC_DECLARE(qml6); GST_PLUGIN_STATIC_DECLARE(qgc); G_END_DECLS namespace GStreamer { +static std::atomic s_envPathsValid{true}; +static QMutex s_envPathsMutex; +static QString s_envPathsError; + void _registerPlugins() { #ifdef QGC_GST_STATIC_BUILD - #ifdef GST_PLUGIN_androidmedia_FOUND - GST_PLUGIN_STATIC_REGISTER(androidmedia); - #endif - #ifdef GST_PLUGIN_applemedia_FOUND - GST_PLUGIN_STATIC_REGISTER(applemedia); - #endif - GST_PLUGIN_STATIC_REGISTER(coreelements); - #ifdef GST_PLUGIN_d3d_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d); - #endif - #ifdef GST_PLUGIN_d3d11_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d11); - #endif - #ifdef GST_PLUGIN_d3d12_FOUND - GST_PLUGIN_STATIC_REGISTER(d3d12); - #endif - #ifdef GST_PLUGIN_dav1d_FOUND - GST_PLUGIN_STATIC_REGISTER(dav1d); - #endif - #ifdef GST_PLUGIN_dxva_FOUND - GST_PLUGIN_STATIC_REGISTER(dxva); - #endif - GST_PLUGIN_STATIC_REGISTER(isomp4); - GST_PLUGIN_STATIC_REGISTER(libav); - GST_PLUGIN_STATIC_REGISTER(matroska); - GST_PLUGIN_STATIC_REGISTER(mpegtsdemux); - #ifdef GST_PLUGIN_msdk_FOUND - GST_PLUGIN_STATIC_REGISTER(msdk); - #endif - #ifdef GST_PLUGIN_nvcodec_FOUND - GST_PLUGIN_STATIC_REGISTER(nvcodec); - #endif - GST_PLUGIN_STATIC_REGISTER(opengl); - GST_PLUGIN_STATIC_REGISTER(openh264); - GST_PLUGIN_STATIC_REGISTER(playback); - #ifdef GST_PLUGIN_qsv_FOUND - GST_PLUGIN_STATIC_REGISTER(qsv); - #endif - GST_PLUGIN_STATIC_REGISTER(rtp); - GST_PLUGIN_STATIC_REGISTER(rtpmanager); - GST_PLUGIN_STATIC_REGISTER(rtsp); - GST_PLUGIN_STATIC_REGISTER(sdpelem); - GST_PLUGIN_STATIC_REGISTER(tcp); - GST_PLUGIN_STATIC_REGISTER(typefindfunctions); - GST_PLUGIN_STATIC_REGISTER(udp); - #ifdef GST_PLUGIN_va_FOUND - GST_PLUGIN_STATIC_REGISTER(va); - #endif - GST_PLUGIN_STATIC_REGISTER(videoparsersbad); - GST_PLUGIN_STATIC_REGISTER(vpx); - #ifdef GST_PLUGIN_vulkan_FOUND - GST_PLUGIN_STATIC_REGISTER(vulkan); - #endif + GST_PLUGIN_STATIC_REGISTER(coreelements); + GST_PLUGIN_STATIC_REGISTER(isomp4); + GST_PLUGIN_STATIC_REGISTER(libav); + GST_PLUGIN_STATIC_REGISTER(matroska); + GST_PLUGIN_STATIC_REGISTER(mpegtsdemux); + GST_PLUGIN_STATIC_REGISTER(opengl); + GST_PLUGIN_STATIC_REGISTER(openh264); + GST_PLUGIN_STATIC_REGISTER(playback); + GST_PLUGIN_STATIC_REGISTER(rtp); + GST_PLUGIN_STATIC_REGISTER(rtpmanager); + GST_PLUGIN_STATIC_REGISTER(rtsp); + GST_PLUGIN_STATIC_REGISTER(sdpelem); + GST_PLUGIN_STATIC_REGISTER(tcp); + GST_PLUGIN_STATIC_REGISTER(typefindfunctions); + GST_PLUGIN_STATIC_REGISTER(udp); + GST_PLUGIN_STATIC_REGISTER(videoconvertscale); + GST_PLUGIN_STATIC_REGISTER(videoparsersbad); + GST_PLUGIN_STATIC_REGISTER(vpx); + +#ifdef GST_PLUGIN_androidmedia_FOUND + GST_PLUGIN_STATIC_REGISTER(androidmedia); +#endif +#ifdef GST_PLUGIN_applemedia_FOUND + GST_PLUGIN_STATIC_REGISTER(applemedia); +#endif +#ifdef GST_PLUGIN_d3d_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d); +#endif +#ifdef GST_PLUGIN_d3d11_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d11); +#endif +#ifdef GST_PLUGIN_d3d12_FOUND + GST_PLUGIN_STATIC_REGISTER(d3d12); +#endif +#ifdef GST_PLUGIN_dav1d_FOUND + GST_PLUGIN_STATIC_REGISTER(dav1d); +#endif +#ifdef GST_PLUGIN_dxva_FOUND + GST_PLUGIN_STATIC_REGISTER(dxva); +#endif +#ifdef GST_PLUGIN_nvcodec_FOUND + GST_PLUGIN_STATIC_REGISTER(nvcodec); +#endif +#ifdef GST_PLUGIN_qsv_FOUND + GST_PLUGIN_STATIC_REGISTER(qsv); +#endif +#ifdef GST_PLUGIN_va_FOUND + GST_PLUGIN_STATIC_REGISTER(va); +#endif +#ifdef GST_PLUGIN_vulkan_FOUND + GST_PLUGIN_STATIC_REGISTER(vulkan); +#endif #endif -// #if !defined(GST_PLUGIN_qml6_FOUND) && defined(QGC_GST_STATIC_BUILD) GST_PLUGIN_STATIC_REGISTER(qml6); -// #endif - GST_PLUGIN_STATIC_REGISTER(qgc); } -void _qtGstLog(GstDebugCategory *category, - GstDebugLevel level, - const gchar *file, - const gchar *function, - gint line, - GObject *object, - GstDebugMessage *message, - gpointer data) +void _resetEnvValidation() +{ + const QMutexLocker locker(&s_envPathsMutex); + s_envPathsError.clear(); + s_envPathsValid.store(true, std::memory_order_release); +} + +void _setEnvValidationError(const QString &error) +{ + const QMutexLocker locker(&s_envPathsMutex); + s_envPathsError = error; + s_envPathsValid.store(false, std::memory_order_release); + qCCritical(GStreamerLog) << error; +} + +QString _cleanJoin(const QString &base, const QString &relative) +{ + return QDir::cleanPath(QDir(base).filePath(relative)); +} + +void _setGstEnv(const char *name, const QString &value) +{ + qputenv(name, value.toUtf8()); + qCDebug(GStreamerLog) << " " << name << "=" << value; +} + +void _unsetEnv(const char *name) { - Q_UNUSED(data); + if (qEnvironmentVariableIsSet(name)) { + qunsetenv(name); + qCDebug(GStreamerLog) << " unset" << name; + } +} + +void _setGstEnvIfExists(const char *name, const QString &path) +{ + if (QFileInfo::exists(path)) { + _setGstEnv(name, path); + } +} + +bool _isExecutableFile(const QString &path) +{ + const QFileInfo fileInfo(path); + return fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable(); +} + +QString _firstExistingPath(const QStringList &paths) +{ + for (const QString &path : paths) { + if (QFileInfo::exists(path)) { + return path; + } + } + + return {}; +} + +QString _joinExistingPaths(const QStringList &paths) +{ + QStringList existing; + existing.reserve(paths.size()); - if (level > gst_debug_category_get_threshold(category)) { + for (const QString &path : paths) { + if (QFileInfo::exists(path) && !existing.contains(path)) { + existing.append(path); + } + } + + return existing.join(QDir::listSeparator()); +} + +void _clearManagedGstEnvVars() +{ + static constexpr const char *varsToUnset[] = { + "GIO_EXTRA_MODULES", + "GIO_MODULE_DIR", + "GIO_USE_VFS", + "GST_PTP_HELPER_1_0", + "GST_PTP_HELPER", + "GST_PLUGIN_SCANNER_1_0", + "GST_PLUGIN_SCANNER", + "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", + "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_PATH", + }; + + for (const char *name : varsToUnset) { + _unsetEnv(name); + } +} + +void _setGstEnvIfExecutable(const char *name, const QString &path) +{ + if (_isExecutableFile(path)) { + _setGstEnv(name, path); + } else { + _unsetEnv(name); + } +} + +void _sanitizePythonEnvForScanner() +{ + static constexpr const char *varsToUnset[] = { + "PYTHONHOME", + "PYTHONPATH", + "VIRTUAL_ENV", + "CONDA_PREFIX", + "CONDA_DEFAULT_ENV", + "PYTHONUSERBASE", + }; + + for (const char *name : varsToUnset) { + if (qEnvironmentVariableIsSet(name)) { + qunsetenv(name); + qCDebug(GStreamerLog) << " unset" << name; + } + } +} + +void _applyGstEnvVars(const QString &pluginDir, const QString &gioModDir, + const QString &scannerPath, const QString &ptpPath) +{ + qCDebug(GStreamerLog) << "Applying GStreamer environment:"; + + _clearManagedGstEnvVars(); + _sanitizePythonEnvForScanner(); + _setGstEnv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", QStringLiteral("no")); + _setGstEnvIfExists("GIO_EXTRA_MODULES", gioModDir); + _setGstEnvIfExecutable("GST_PTP_HELPER_1_0", ptpPath); + _setGstEnvIfExecutable("GST_PTP_HELPER", ptpPath); + _setGstEnvIfExecutable("GST_PLUGIN_SCANNER_1_0", scannerPath); + _setGstEnvIfExecutable("GST_PLUGIN_SCANNER", scannerPath); + _setGstEnv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir); + _setGstEnv("GST_PLUGIN_SYSTEM_PATH", pluginDir); + _setGstEnv("GST_PLUGIN_PATH_1_0", pluginDir); + _setGstEnv("GST_PLUGIN_PATH", pluginDir); +} + +#if defined(Q_OS_LINUX) +bool _systemGioIsNew() +{ + // Probe the system GIO library on disk (not the in-process symbols) for + // g_task_set_static_name, which was added in GIO 2.76. This mirrors + // AppRun's `nm -D "$SYSTEM_GIO" | grep g_task_set_static_name` check. + static constexpr const char *kGioSoPaths[] = { + "/usr/lib/x86_64-linux-gnu/libgio-2.0.so.0", + "/usr/lib/aarch64-linux-gnu/libgio-2.0.so.0", + "/usr/lib64/libgio-2.0.so.0", + "/usr/lib/libgio-2.0.so.0", + }; + + for (const char *path : kGioSoPaths) { + void *handle = dlopen(path, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) { + handle = dlopen(path, RTLD_LAZY); + } + if (!handle) { + continue; + } + const bool found = (dlsym(handle, "g_task_set_static_name") != nullptr); + dlclose(handle); + return found; + } + + return false; +} + +void _applyGioCompatOverride(const QString &gioModDir) +{ + if (gioModDir.isEmpty()) { return; } - QMessageLogger log(file, line, function); - - char *object_info = gst_info_strdup_printf("%" GST_PTR_FORMAT, static_cast(object)); - - switch (level) { - case GST_LEVEL_ERROR: - log.critical(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_WARNING: - log.warning(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_FIXME: - case GST_LEVEL_INFO: - log.info(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - case GST_LEVEL_DEBUG: -#ifdef QT_DEBUG - case GST_LEVEL_LOG: - case GST_LEVEL_TRACE: - case GST_LEVEL_MEMDUMP: + // GIO 2.76+ requires bundled modules to be loaded via GIO_MODULE_DIR with + // VFS forced to local, mirroring the AppImage launcher logic. + if (_systemGioIsNew()) { + _unsetEnv("GIO_EXTRA_MODULES"); + _setGstEnv("GIO_MODULE_DIR", gioModDir); + _setGstEnv("GIO_USE_VFS", QStringLiteral("local")); + } +} #endif - log.debug(GStreamerAPILog, "%s %s", object_info, gst_debug_message_get(message)); - break; - default: - break; + +void _warnIfScannerMissing(const QString &platformLabel, const QString &scannerPath) +{ + if (scannerPath.isEmpty()) { + qCWarning(GStreamerLog) << "GStreamer:" << platformLabel + << "bundled gst-plugin-scanner not found; GStreamer will use in-process scanning"; + } else if (!_isExecutableFile(scannerPath)) { + qCWarning(GStreamerLog) << "GStreamer:" << platformLabel + << "gst-plugin-scanner is not executable:" << scannerPath; + } +} + +bool _validateMacBundlePaths(const QString &bundleFrameworkRoot, + const QString &pluginDirs, + const QString &scannerPath) +{ + if (pluginDirs.isEmpty()) { + _setEnvValidationError(QStringLiteral( + "GStreamer: bundled macOS framework found but plugin directory is missing under %1") + .arg(bundleFrameworkRoot)); + return false; } - g_clear_pointer(&object_info, g_free); + _warnIfScannerMissing(QStringLiteral("macOS framework"), scannerPath); + return true; +} + +bool _validateBundledDesktopPaths(const QString &platformLabel, + const QString &pluginDirs, + const QString &scannerPath) +{ + if (pluginDirs.isEmpty()) { + _setEnvValidationError(QStringLiteral( + "GStreamer: %1 bundled plugin directory is missing.") + .arg(platformLabel)); + return false; + } + + _warnIfScannerMissing(platformLabel, scannerPath); + return true; } void _setGstEnvVars() { + _resetEnvValidation(); + const QString appDir = QCoreApplication::applicationDirPath(); - qCDebug(GStreamerLog) << "App Directory:" << appDir; - -#if defined(Q_OS_MACOS) && defined(QGC_GST_MACOS_FRAMEWORK) - const QString frameworkDir = QDir(appDir).filePath("../Frameworks/GStreamer.framework"); - const QString rootDir = QDir(frameworkDir).filePath("Versions/1.0"); - const QString libDir = QDir(rootDir).filePath("../lib"); - const QString pluginDir = QDir(libDir).filePath("gstreamer-1.0"); - const QString gioMod = QDir(libDir).filePath("gio/modules"); - const QString libexecDir = QDir(appDir).filePath("../libexec"); - const QString scanner = QDir(libexecDir).filePath("gstreamer-1.0/gst-plugin-scanner"); - const QString ptp = QDir(libexecDir).filePath("gstreamer-1.0/gst-ptp-helper"); - - if (QFileInfo::exists(frameworkDir)) { - qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); - qputenv("GIO_EXTRA_MODULES", gioMod.toUtf8().constData()); - qputenv("GST_PTP_HELPER_1_0", ptp.toUtf8().constData()); - qputenv("GST_PTP_HELPER", ptp.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER_1_0", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH", pluginDir.toUtf8().constData()); - qputenv("GTK_PATH", rootDir.toUtf8().constData()); + qCDebug(GStreamerLog) << "App directory:" << appDir; + +#if defined(Q_OS_MACOS) + const QString frameworkDir = _cleanJoin(appDir, "../Frameworks/GStreamer.framework"); + const QString rootDir = _firstExistingPath({ + _cleanJoin(frameworkDir, "Versions/1.0"), + _cleanJoin(frameworkDir, "Versions/Current"), + frameworkDir, + }); + +#if defined(QGC_GST_MACOS_FRAMEWORK) + // Framework builds prefer framework paths over app-relative paths + const QString pluginDirs = _joinExistingPaths({ + _cleanJoin(rootDir, "lib/gstreamer-1.0"), + _cleanJoin(appDir, "../lib/gstreamer-1.0"), + }); + const QString gioMod = _firstExistingPath({ + _cleanJoin(rootDir, "lib/gio/modules"), + _cleanJoin(appDir, "../lib/gio/modules"), + }); +#else + // Non-framework (Homebrew) builds prefer app-relative paths + const QString pluginDirs = _joinExistingPaths({ + _cleanJoin(appDir, "../lib/gstreamer-1.0"), + _cleanJoin(rootDir, "lib/gstreamer-1.0"), + }); + const QString gioMod = _firstExistingPath({ + _cleanJoin(appDir, "../lib/gio/modules"), + _cleanJoin(rootDir, "lib/gio/modules"), + }); +#endif + + const QString scanner = _firstExistingPath({ + _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-plugin-scanner"), + _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-plugin-scanner"), + }); + const QString ptp = _firstExistingPath({ + _cleanJoin(appDir, "../libexec/gstreamer-1.0/gst-ptp-helper"), + _cleanJoin(rootDir, "libexec/gstreamer-1.0/gst-ptp-helper"), + }); + const bool hasBundledFramework = QFileInfo::exists(frameworkDir); + + const bool validBundlePaths = hasBundledFramework + ? _validateMacBundlePaths(rootDir, pluginDirs, scanner) + : !pluginDirs.isEmpty(); + + if (!pluginDirs.isEmpty() && validBundlePaths) { + _applyGstEnvVars(pluginDirs, gioMod, scanner, ptp); } -#elif defined(Q_OS_WIN) - const QString binDir = appDir; - const QString libDir = QDir(binDir).filePath("../lib"); - const QString pluginDir = QDir(libDir).filePath("gstreamer-1.0"); - const QString gioMod = QDir(libDir).filePath("gio/modules"); - const QString libexecDir = QDir(binDir).filePath("../libexec"); - const QString scanner = QDir(libexecDir).filePath("gstreamer-1.0/gst-plugin-scanner"); - const QString ptp = QDir(libexecDir).filePath("gstreamer-1.0/gst-ptp-helper"); - - if (QFileInfo::exists(pluginDir)) { - qputenv("GST_REGISTRY_REUSE_PLUGIN_SCANNER", "no"); - qputenv("GIO_EXTRA_MODULES", gioMod.toUtf8().constData()); - qputenv("GST_PTP_HELPER_1_0", ptp.toUtf8().constData()); - qputenv("GST_PTP_HELPER", ptp.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER_1_0", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SCANNER", scanner.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_SYSTEM_PATH", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH_1_0", pluginDir.toUtf8().constData()); - qputenv("GST_PLUGIN_PATH", pluginDir.toUtf8().constData()); + +#if defined(QGC_GST_MACOS_FRAMEWORK) + if (hasBundledFramework) { + _setGstEnv("GTK_PATH", rootDir); } #endif -} -void _logPlugin(gpointer data, gpointer /*user_data*/) -{ - GstPlugin *plugin = static_cast(data); - if (!plugin) { - return; +#elif defined(Q_OS_WIN) + const QString libDir = _cleanJoin(appDir, "../lib"); + const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0"); + const QString gioMod = _cleanJoin(libDir, "gio/modules"); + const QString libexecDir = _cleanJoin(appDir, "../libexec"); + const QString scanner = _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner.exe"); + const QString ptp = _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper.exe"); + + if (QFileInfo::exists(pluginDir) + && _validateBundledDesktopPaths(QStringLiteral("Windows"), pluginDir, scanner)) { + _applyGstEnvVars(pluginDir, gioMod, scanner, ptp); + } + +#elif defined(Q_OS_LINUX) + // AppRun sets GStreamer env vars before launch (including GIO compatibility + // logic). Only apply fallback paths when no external override is present. + if (!qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0") + && !qEnvironmentVariableIsSet("GST_PLUGIN_PATH") + && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH_1_0") + && !qEnvironmentVariableIsSet("GST_PLUGIN_SYSTEM_PATH")) { + const QString libDir = _cleanJoin(appDir, "../lib"); + const QString libexecDir = _cleanJoin(appDir, "../libexec"); + const QString pluginDir = _cleanJoin(libDir, "gstreamer-1.0"); + const QString gioMod = _cleanJoin(libDir, "gio/modules"); + const QString scanner = _firstExistingPath({ + _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"), + _cleanJoin(libexecDir, "gstreamer-1.0/gst-plugin-scanner"), + }); + const QString ptp = _firstExistingPath({ + _cleanJoin(libDir, "gstreamer1.0/gstreamer-1.0/gst-ptp-helper"), + _cleanJoin(libexecDir, "gstreamer-1.0/gst-ptp-helper"), + }); + + if (QFileInfo::exists(pluginDir) + && _validateBundledDesktopPaths(QStringLiteral("Linux"), pluginDir, scanner)) { + _applyGstEnvVars(pluginDir, gioMod, scanner, ptp); + _applyGioCompatOverride(gioMod); + } } +#endif - const gchar *name = gst_plugin_get_name(plugin); - const gchar *version = gst_plugin_get_version(plugin); - qCDebug(GStreamerLog) << " " << name << "-" << version; } bool _verifyPlugins() { - bool result = true; - GstRegistry *registry = gst_registry_get(); + if (!registry) { + qCCritical(GStreamerLog) << "Failed to get GStreamer registry"; + return false; + } - qCDebug(GStreamerLog) << "Installed GStreamer Plugins:"; GList *plugins = gst_registry_get_plugin_list(registry); - g_list_foreach(plugins, _logPlugin, NULL); - g_list_free(plugins); + if (plugins) { + qCDebug(GStreamerLog) << "Installed GStreamer plugins:"; + for (GList *node = plugins; node != nullptr; node = node->next) { + GstPlugin *plugin = static_cast(node->data); + if (plugin) { + qCDebug(GStreamerLog) << " " << gst_plugin_get_name(plugin) + << gst_plugin_get_version(plugin); + } + } + gst_plugin_list_free(plugins); + } - static constexpr const char *pluginNames[2] = {"qml6", "qgc"}; - for (const char *name : pluginNames) { + bool result = true; + static constexpr const char *requiredPlugins[] = {"qml6", "qgc", "coreelements"}; + for (const char *name : requiredPlugins) { GstPlugin *plugin = gst_registry_find_plugin(registry, name); if (!plugin) { - qCCritical(GStreamerLog) << name << "plugin NOT found."; + qCCritical(GStreamerLog) << "Required QGC plugin not found:" << name; result = false; continue; } @@ -253,22 +513,40 @@ bool _verifyPlugins() } if (!result) { - QString pluginPath; - if (qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0")) { - pluginPath = qgetenv("GST_PLUGIN_PATH_1_0"); - } else if (qEnvironmentVariableIsSet("GST_PLUGIN_PATH")) { - pluginPath = qgetenv("GST_PLUGIN_PATH"); - } + const QByteArray pluginPath = qEnvironmentVariableIsSet("GST_PLUGIN_PATH_1_0") + ? qgetenv("GST_PLUGIN_PATH_1_0") + : qgetenv("GST_PLUGIN_PATH"); -#ifdef QGC_GST_STATIC_BUILD - qCCritical(GStreamerLog) << "Please update the list of static plugins in GStreamer.cc"; -#else if (!pluginPath.isEmpty()) { - qCCritical(GStreamerLog) << "Please check in GST_PLUGIN_PATH=" << pluginPath; + qCCritical(GStreamerLog) << "Check GST_PLUGIN_PATH=" << pluginPath; } else { - qCCritical(GStreamerLog) << "Please set GST_PLUGIN_PATH to the path of your plugin"; + qCCritical(GStreamerLog) << "GST_PLUGIN_PATH is not set"; + } + + GList *allPlugins = gst_registry_get_plugin_list(registry); + for (GList *node = allPlugins; node != nullptr; node = node->next) { + GstPlugin *p = static_cast(node->data); + if (!p) continue; + const gchar *desc = gst_plugin_get_description(p); + const gchar *filename = gst_plugin_get_filename(p); + if (desc && g_str_has_prefix(desc, "BLACKLIST")) { + qCWarning(GStreamerLog) << "Blacklisted plugin:" << gst_plugin_get_name(p) + << "file:" << (filename ? filename : "(null)"); + } + } + gst_plugin_list_free(allPlugins); + + static constexpr const char *envDiagnostics[] = { + "GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0", + "GST_REGISTRY_REUSE_PLUGIN_SCANNER", + }; + qCCritical(GStreamerLog) << "GStreamer environment diagnostics:"; + for (const char *var : envDiagnostics) { + const QByteArray val = qgetenv(var); + qCCritical(GStreamerLog) << " " << var << "=" << (val.isEmpty() ? "(unset)" : val.constData()); } -#endif } return result; @@ -276,279 +554,176 @@ bool _verifyPlugins() void _logDecoderRanks() { - GList *decoderFactories = gst_element_factory_list_get_elements( + GList *factories = gst_element_factory_list_get_elements( static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), GST_RANK_NONE); - if (!decoderFactories) { - qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found."; + if (!factories) { + qCDebug(GStreamerDecoderRanksLog) << "No video decoder factories found"; return; } - decoderFactories = g_list_sort(decoderFactories, [](gconstpointer lhs, gconstpointer rhs) -> gint { - GstElementFactory *lhsFactory = GST_ELEMENT_FACTORY(lhs); - GstElementFactory *rhsFactory = GST_ELEMENT_FACTORY(rhs); - - if (!lhsFactory && !rhsFactory) { - return 0; - } - if (!lhsFactory) { - return 1; - } - if (!rhsFactory) { - return -1; - } - - const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhsFactory)); - const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhsFactory)); - if (lhsRank == rhsRank) { - const gchar *lhsName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhsFactory)); - const gchar *rhsName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhsFactory)); - return g_strcmp0(lhsName, rhsName); + factories = g_list_sort(factories, [](gconstpointer lhs, gconstpointer rhs) -> gint { + const guint lhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(lhs)); + const guint rhsRank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(rhs)); + if (lhsRank != rhsRank) { + return (lhsRank > rhsRank) ? -1 : 1; } - - return lhsRank > rhsRank ? -1 : 1; + return g_strcmp0(gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(lhs)), + gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(rhs))); }); - qCDebug(GStreamerDecoderRanksLog) << "Video decoder plugin ranks:"; - for (GList *node = decoderFactories; node != nullptr; node = node->next) { + qCDebug(GStreamerDecoderRanksLog) << "Video decoder ranks:"; + for (GList *node = factories; node != nullptr; node = node->next) { GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); - if (!factory) { - continue; - } - - const gchar *decoderKlass = gst_element_factory_get_klass(factory); GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); const gchar *featureName = gst_plugin_feature_get_name(feature); const guint rank = gst_plugin_feature_get_rank(feature); + const gchar *klass = gst_element_factory_get_klass(factory); + const bool isHw = GStreamer::isHardwareDecoderFactory(factory); GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); + const gchar *pluginName = plugin ? gst_plugin_get_name(plugin) : "?"; + + qCDebug(GStreamerDecoderRanksLog).noquote() + << QStringLiteral(" [%1] %2/%3 rank=%4 (%5)") + .arg(isHw ? QStringLiteral("HW") : QStringLiteral("SW"), + QString::fromUtf8(pluginName), + QString::fromUtf8(featureName)) + .arg(rank) + .arg(QString::fromUtf8(klass)); + if (plugin) { - qCDebug(GStreamerDecoderRanksLog) << " " << gst_plugin_get_name(plugin) << "/" << featureName << "-" << decoderKlass << ":" << rank; gst_object_unref(plugin); - } else { - qCDebug(GStreamerDecoderRanksLog) << " " << featureName << "-" << decoderKlass << ":" << rank; } } - gst_plugin_feature_list_free(decoderFactories); + gst_plugin_feature_list_free(factories); } -void _lowerSoftwareDecoderRanks(GstRegistry *registry) +void _configureDebugLogging() { - static constexpr uint16_t NewRank = GST_RANK_NONE; - if (!registry) { - qCCritical(GStreamerLog) << "Invalid registry!"; + gst_debug_remove_log_function(gst_debug_log_default); + gst_debug_add_log_function(GStreamer::qtGstLog, nullptr, nullptr); + + if (!qEnvironmentVariableIsEmpty("GST_DEBUG")) { return; } - const char* softDecoders[] = {"avdec_h264", "avdec_h265", "avdec_mjpeg", "avdec_mpeg2video", "avdec_mpeg4", - "avdec_vp8", "avdec_vp9", "dav1ddec", "vp8dec", "vp9dec"}; - - for (const char *name : softDecoders) { - GstPluginFeature *feature = gst_registry_lookup_feature(registry, name); - if (feature) { - qCDebug(GStreamerLog) << "Setting software decoder rank low:" << name << " rank:" << NewRank; - gst_plugin_feature_set_rank(feature, NewRank); - gst_object_unref(feature); - } else { - qCDebug(GStreamerLog) << "Software decoder not found:" << name; - } + QSettings settings; + if (settings.contains(AppSettings::gstDebugLevelName)) { + const int level = qBound(0, settings.value(AppSettings::gstDebugLevelName).toInt(), + static_cast(GST_LEVEL_MEMDUMP)); + gst_debug_set_default_threshold(static_cast(level)); } } -void _changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank) +void prepareEnvironment() { - if (!registry || !featureName) { - return; - } - - GstPluginFeature *feature = gst_registry_lookup_feature(registry, featureName); - if (!feature) { - qCDebug(GStreamerLog) << "Failed to change ranking of feature. Feature does not exist:" << featureName; - return; - } - - qCDebug(GStreamerLog) << " Changing feature (" << featureName << ") to use rank:" << rank; - gst_plugin_feature_set_rank(feature, rank); - gst_clear_object(&feature); + _setGstEnvVars(); } -void _prioritizeByHardwareClass(GstRegistry *registry, uint16_t prioritizedRank, bool requireHardware) +bool _initGstRuntime() { - if (!registry) { - qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; - return; + if (!s_envPathsValid.load(std::memory_order_acquire)) { + const QMutexLocker locker(&s_envPathsMutex); + qCCritical(GStreamerLog) << "Invalid GStreamer environment configuration:" << s_envPathsError; + return false; } - GList *decoderFactories = gst_element_factory_list_get_elements( - static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), - GST_RANK_NONE); - - if (!decoderFactories) { - qCDebug(GStreamerLog) << "No decoder factories available while prioritizing" - << (requireHardware ? "hardware" : "software") << "decoders"; - return; + QByteArrayList argStorage; + for (const QString &arg : QCoreApplication::arguments()) { + argStorage.append(arg.toUtf8()); } - qCDebug(GStreamerLog) << "Prioritizing" << (requireHardware ? "hardware" : "software") - << "video decoders with rank:" << prioritizedRank; - int matchedFactories = 0; - for (GList *node = decoderFactories; node != nullptr; node = node->next) { - GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); - if (!factory) { - continue; - } - - if (GStreamer::is_hardware_decoder_factory(factory) != requireHardware) { - continue; - } - - const gchar *featureName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); - if (!featureName) { - continue; - } - - _changeFeatureRank(registry, featureName, prioritizedRank); - ++matchedFactories; + QVarLengthArray argv; + for (QByteArray &arg : argStorage) { + argv.append(arg.data()); } - if (matchedFactories == 0) { - qCWarning(GStreamerLog) << "No" << (requireHardware ? "hardware" : "software") - << "video decoder factories found to reprioritize."; - } + int argc = argv.size(); + char **argvPtr = argv.data(); + GError *error = nullptr; - // Lower software decoder rank when using hardware decoders - if(requireHardware) { - qCCritical(GstVideoReceiverLog) << "Set the software decoder rank low."; - _lowerSoftwareDecoderRanks(registry); + if (!gst_init_check(&argc, &argvPtr, &error)) { + qCCritical(GStreamerLog) << "Failed to initialize GStreamer:" + << (error ? error->message : "unknown error"); + g_clear_error(&error); + return false; } - gst_plugin_feature_list_free(decoderFactories); + return true; } -void _setCodecPriorities(GStreamer::VideoDecoderOptions option) +bool completeInit() { - GstRegistry *registry = gst_registry_get(); - - if (!registry) { - qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; - return; + if (!gst_is_initialized()) { + qCCritical(GStreamerLog) << "completeInit called but gst_init() has not been called"; + return false; } - static constexpr uint16_t PrioritizedRank = GST_RANK_PRIMARY + 1; + _configureDebugLogging(); - // TODO: ForceVideoDecoderCustom in VideoSettings with textbox in QML - - switch (option) { - case GStreamer::ForceVideoDecoderDefault: - break; - case GStreamer::ForceVideoDecoderSoftware: - _prioritizeByHardwareClass(registry, PrioritizedRank, false); - break; - case GStreamer::ForceVideoDecoderHardware: - _prioritizeByHardwareClass(registry, PrioritizedRank, true); - break; - case GStreamer::ForceVideoDecoderVAAPI: - for (const char *name : {"vaav1dec", "vah264dec", "vah265dec", "vajpegdec", "vampeg2dec", "vavp8dec", "vavp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderNVIDIA: - for (const char *name : {"nvav1dec", "nvh264dec", "nvh265dec", "nvjpegdec", "nvmpeg2videodec", "nvmpeg4videodec", "nvmpegvideodec", "nvvp8dec", "nvvp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderDirectX3D: - for (const char *name : {"d3d11av1dec", "d3d11h264dec", "d3d11h265dec", "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec", - "d3d12av1dec", "d3d12h264dec", "d3d12h265dec", "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec", - "dxvaav1decoder", "dxvah264decoder", "dxvah265decoder", "dxvampeg2decoder", "dxvavp8decoder", "dxvavp9decoder" }) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderVideoToolbox: - for (const char *name : {"vtdec_hw", "vtdec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderIntel: - for (const char *name : {"qsvh264dec", "qsvh265dec", "qsvjpegdec", "qsvvp9dec", "msdkav1dec", "msdkh264dec", "msdkh265dec", "msdkmjpegdec", "msdkmpeg2dec", "msdkvc1dec", "msdkvp8dec", "msdkvp9dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - case GStreamer::ForceVideoDecoderVulkan: - for (const char *name : {"vulkanh264dec", "vulkanh265dec"}) { - _changeFeatureRank(registry, name, PrioritizedRank); - } - break; - default: - qCWarning(GStreamerLog) << "Can't handle decode option:" << option; - break; - } -} + gchar *version = gst_version_string(); + qCDebug(GStreamerLog) << "GStreamer initialized:" << version; + g_free(version); -bool initialize() -{ - _setGstEnvVars(); + _registerPlugins(); - if (qEnvironmentVariableIsEmpty("GST_DEBUG")) { - int gstDebugLevel = 0; - QSettings settings; - if (settings.contains(AppSettings::gstDebugLevelName)) { - gstDebugLevel = settings.value(AppSettings::gstDebugLevelName).toInt(); - } - gst_debug_set_default_threshold(static_cast(gstDebugLevel)); + if (!_verifyPlugins()) { + qCCritical(GStreamerLog) << "Plugin verification failed"; + return false; } - gst_debug_remove_log_function(gst_debug_log_default); - gst_debug_add_log_function(_qtGstLog, nullptr, nullptr); - - const QStringList args = QCoreApplication::arguments(); - int gstArgc = args.size(); - - QByteArrayList argData; - argData.reserve(gstArgc); - - QVarLengthArray rawArgv; - rawArgv.reserve(gstArgc); + _logDecoderRanks(); - for (const QString &arg : args) { - argData.append(arg.toUtf8()); - rawArgv.append(argData.last().data()); + GstElementFactory *sinkFactory = gst_element_factory_find("qml6glsink"); + if (!sinkFactory) { + qCCritical(GStreamerLog) << "qml6glsink factory not found"; + return false; } + gst_object_unref(sinkFactory); - char **argvPtr = rawArgv.data(); - GError *error = nullptr; - const gboolean ok = gst_init_check(&gstArgc, &argvPtr, &error); - if (!ok) { - qCritical(GStreamerLog) << "Failed to initialize GStreamer:" << error->message; - g_clear_error(&error); + GstElementFactory *playbinFactory = gst_element_factory_find("playbin"); + if (!playbinFactory) { + qCCritical(GStreamerLog) << "playbin factory not found"; return false; } + gst_object_unref(playbinFactory); - const gchar *version = gst_version_string(); - qCDebug(GStreamerLog) << QString("GStreamer Initialized (Version: %1)").arg(version); - - _registerPlugins(); - - if (!_verifyPlugins()) { - qCCritical(GStreamerLog) << "Failed to verify plugins - Check your GStreamer setup"; + if (GStreamer::didExternalPluginLoaderFail()) { + qCCritical(GStreamerLog) + << "GStreamer external plugin loader failed. Check GST_PLUGIN_SCANNER and bundled runtime paths."; return false; } - _logDecoderRanks(); - _setCodecPriorities(static_cast(SettingsManager::instance()->videoSettings()->forceVideoDecoder()->rawValue().toInt())); + return true; +} + +bool initialize() +{ + GStreamer::resetExternalPluginLoaderFailure(); + GStreamer::redirectGLibLogging(); - GstElement *sink = gst_element_factory_make("qml6glsink", nullptr); - if (!sink) { - qCCritical(GStreamerLog) << "failed to init qml6glsink"; + if (!_initGstRuntime()) { return false; } - gst_clear_object(&sink); - return true; + return completeInit(); +} + +QFuture initializeAsync() +{ + prepareEnvironment(); + return QtConcurrent::run(&initialize); } +// Ownership protocol for the video sink element: +// createVideoSink — returns a floating-ref element (refcount conceptually 1). +// startDecoding — calls gst_object_ref (sinks float, refcount=1). +// _ensureVideoSinkInPipeline — gst_object_ref (+1=2), gst_bin_add (+1=3). +// _shutdownDecodingBranch — gst_bin_remove (-1=2), gst_clear_object (-1=1). +// releaseVideoSink — gst_clear_object (-1=0, freed). void *createVideoSink(QQuickItem *widget, QObject * /*parent*/) { GstElement *videoSinkBin = gst_element_factory_make("qgcvideosinkbin", NULL); @@ -565,6 +740,7 @@ void *createVideoSink(QQuickItem *widget, QObject * /*parent*/) void releaseVideoSink(void *sink) { + if (!sink) return; GstElement *videoSink = GST_ELEMENT(sink); gst_clear_object(&videoSink); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h index 763be20daf79..6ca777db5914 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.h @@ -1,9 +1,11 @@ #pragma once +#include #include Q_DECLARE_LOGGING_CATEGORY(GStreamerLog) Q_DECLARE_LOGGING_CATEGORY(GStreamerAPILog) +Q_DECLARE_LOGGING_CATEGORY(GStreamerDecoderRanksLog) class QQuickItem; class VideoReceiver; @@ -23,9 +25,12 @@ enum VideoDecoderOptions { ForceVideoDecoderHardware }; +QFuture initializeAsync(); +void prepareEnvironment(); bool initialize(); +bool completeInit(); void *createVideoSink(QQuickItem *widget, QObject *parent = nullptr); void releaseVideoSink(void *sink); VideoReceiver *createVideoReceiver(QObject *parent = nullptr); -}; +} diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc index bc567e081f1d..52f59412729f 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.cc @@ -1,15 +1,20 @@ #include "GStreamerHelpers.h" #include +#include +#include #include -#include namespace GStreamer { gboolean -is_valid_rtsp_uri(const gchar *uri_str) +isValidRtspUri(const gchar *uri_str) { + if (!uri_str) { + return FALSE; + } + GstRTSPUrl *url = NULL; GstRTSPResult res; @@ -19,14 +24,18 @@ is_valid_rtsp_uri(const gchar *uri_str) res = gst_rtsp_url_parse(uri_str, &url); if ((res != GST_RTSP_OK) || (url == NULL)) { + if (url) { + gst_rtsp_url_free(url); + } return FALSE; } + const gboolean hasHost = (url->host && url->host[0] != '\0'); gst_rtsp_url_free(url); - return TRUE; + return hasHost; } -bool is_hardware_decoder_factory(GstElementFactory *factory) +bool isHardwareDecoderFactory(GstElementFactory *factory) { if (!factory) { return false; @@ -37,38 +46,45 @@ bool is_hardware_decoder_factory(GstElementFactory *factory) return false; } - // Exclude Android software decoders (OMXGoogle / C2Android) - QString name = QString::fromUtf8(factoryName).toLower(); - if (name.startsWith("amcviddec-omxgoogle") || name.startsWith("amcviddec-c2android")) { + const QString nameLower = QString::fromUtf8(factoryName).toLower(); + + // Android MediaCodec: exclude software wrappers, accept remaining as hardware + if (nameLower.startsWith("amcviddec-omxgoogle") || nameLower.startsWith("amcviddec-c2android")) { return false; } + if (nameLower.startsWith("amcviddec-")) { + return true; + } const auto containsHardware = [](const gchar *value) { - return value && (g_strrstr(value, "Hardware") != nullptr || g_strrstr(value, "hardware") != nullptr); + if (!value) return false; + gchar *lower = g_ascii_strdown(value, -1); + bool found = (g_strrstr(lower, "hardware") != nullptr); + g_free(lower); + return found; }; if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_KLASS))) { return true; } - if (containsHardware(gst_element_factory_get_klass(factory))) { + if (containsHardware(gst_element_factory_get_metadata(factory, GST_ELEMENT_METADATA_DESCRIPTION))) { return true; } - const QString nameLower = QString::fromUtf8(factoryName).toLower(); - static const QStringList kHardwareTags = { - QStringLiteral("va"), // vaapi family - QStringLiteral("nv"), // nvidia nvcodec - QStringLiteral("qsv"), // intel quick sync - QStringLiteral("msdk"), // intel media sdk - QStringLiteral("vulkan"), // vulkan accelerated - QStringLiteral("d3d"), // direct3d - QStringLiteral("dxva"), // directx video accel - QStringLiteral("vtdec"), // apple video toolbox - QStringLiteral("metal") // metal-based decoders + static constexpr QLatin1String kHardwareTags[] = { + QLatin1String("va"), + QLatin1String("nv"), + QLatin1String("qsv"), + QLatin1String("msdk"), + QLatin1String("vulkan"), + QLatin1String("d3d"), + QLatin1String("dxva"), + QLatin1String("vtdec"), + QLatin1String("metal"), }; - for (const QString &tag : kHardwareTags) { + for (const auto &tag : kHardwareTags) { if (nameLower.contains(tag)) { return true; } @@ -77,4 +93,176 @@ bool is_hardware_decoder_factory(GstElementFactory *factory) return false; } +namespace { + +void changeFeatureRank(GstRegistry *registry, const char *featureName, uint16_t rank) +{ + if (!registry || !featureName) { + return; + } + + GstPluginFeature *feature = gst_registry_lookup_feature(registry, featureName); + if (!feature) { + qCDebug(GStreamerLog) << "Failed to change ranking of feature. Feature does not exist:" << featureName; + return; + } + + qCDebug(GStreamerLog) << " Changing feature (" << featureName << ") to use rank:" << rank; + gst_plugin_feature_set_rank(feature, rank); + gst_clear_object(&feature); +} + +void lowerSoftwareDecoderRanks(GstRegistry *registry) +{ + static constexpr uint16_t NewRank = GST_RANK_NONE; + if (!registry) { + qCCritical(GStreamerLog) << "Invalid registry!"; + return; + } + + GList *decoderFactories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + for (GList *node = decoderFactories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) { + continue; + } + + if (GStreamer::isHardwareDecoderFactory(factory)) { + continue; + } + + const gchar *name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + if (!name) { + continue; + } + + qCDebug(GStreamerLog) << "Setting software decoder rank low:" << name << " rank:" << NewRank; + gst_plugin_feature_set_rank(GST_PLUGIN_FEATURE(factory), NewRank); + } + + gst_plugin_feature_list_free(decoderFactories); +} + +void prioritizeByHardwareClass(GstRegistry *registry, uint16_t prioritizedRank, bool requireHardware) +{ + if (!registry) { + qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; + return; + } + + GList *decoderFactories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!decoderFactories) { + qCDebug(GStreamerLog) << "No decoder factories available while prioritizing" + << (requireHardware ? "hardware" : "software") << "decoders"; + return; + } + + qCDebug(GStreamerLog) << "Prioritizing" << (requireHardware ? "hardware" : "software") + << "video decoders with rank:" << prioritizedRank; + int matchedFactories = 0; + for (GList *node = decoderFactories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) { + continue; + } + + if (GStreamer::isHardwareDecoderFactory(factory) != requireHardware) { + continue; + } + + const gchar *featureName = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + if (!featureName) { + continue; + } + + changeFeatureRank(registry, featureName, prioritizedRank); + ++matchedFactories; + } + + if (matchedFactories == 0) { + qCWarning(GStreamerLog) << "No" << (requireHardware ? "hardware" : "software") + << "video decoder factories found to reprioritize."; + } + + if (requireHardware) { + qCDebug(GStreamerLog) << "Lowering software decoder ranks for hardware priority."; + lowerSoftwareDecoderRanks(registry); + } + + gst_plugin_feature_list_free(decoderFactories); +} + +} // anonymous namespace + +void setCodecPriorities(VideoDecoderOptions option) +{ + GstRegistry *registry = gst_registry_get(); + + if (!registry) { + qCCritical(GStreamerLog) << "Failed to get gstreamer registry."; + return; + } + + static constexpr uint16_t PrioritizedRank = GST_RANK_PRIMARY + 1; + + switch (option) { + case ForceVideoDecoderDefault: + // Android/iOS hardware decoders (amcviddec, vtdec) already have higher + // default ranks than software decoders, so decodebin tries them first. + // However, AMC decoders require GL output (GLMemory caps) which the + // current pipeline doesn't support — so they fail and decodebin falls + // back to software. Setting software to RANK_NONE here would eliminate + // that fallback entirely. Leave ranks untouched until the pipeline + // supports GL context sharing with Qt. + break; + case ForceVideoDecoderSoftware: + prioritizeByHardwareClass(registry, PrioritizedRank, false); + break; + case ForceVideoDecoderHardware: + prioritizeByHardwareClass(registry, PrioritizedRank, true); + break; + case ForceVideoDecoderVAAPI: + for (const char *name : {"vaav1dec", "vah264dec", "vah265dec", "vajpegdec", "vampeg2dec", "vavp8dec", "vavp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderNVIDIA: + for (const char *name : {"nvav1dec", "nvh264dec", "nvh265dec", "nvjpegdec", "nvmpeg2videodec", "nvmpeg4videodec", "nvmpegvideodec", "nvvp8dec", "nvvp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderDirectX3D: + for (const char *name : {"d3d11av1dec", "d3d11h264dec", "d3d11h265dec", "d3d11mpeg2dec", "d3d11vp8dec", "d3d11vp9dec", + "d3d12av1dec", "d3d12h264dec", "d3d12h265dec", "d3d12mpeg2dec", "d3d12vp8dec", "d3d12vp9dec", + "dxvaav1decoder", "dxvah264decoder", "dxvah265decoder", "dxvampeg2decoder", "dxvavp8decoder", "dxvavp9decoder"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderVideoToolbox: + for (const char *name : {"vtdec_hw", "vtdec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderIntel: + for (const char *name : {"qsvh264dec", "qsvh265dec", "qsvjpegdec", "qsvvp9dec", "msdkav1dec", "msdkh264dec", "msdkh265dec", "msdkmjpegdec", "msdkmpeg2dec", "msdkvc1dec", "msdkvp8dec", "msdkvp9dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + case ForceVideoDecoderVulkan: + for (const char *name : {"vulkanh264dec", "vulkanh265dec"}) { + changeFeatureRank(registry, name, PrioritizedRank); + } + break; + default: + qCWarning(GStreamerLog) << "Can't handle decode option:" << option; + break; + } +} + } // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h index 148128e417b4..cd55babd41c9 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerHelpers.h @@ -1,14 +1,15 @@ #pragma once #include -// Needed for GstElementFactory #include +#include "GStreamer.h" + namespace GStreamer { - gboolean is_valid_rtsp_uri(const gchar *uri_str); + gboolean isValidRtspUri(const gchar *uri_str); + + bool isHardwareDecoderFactory(GstElementFactory *factory); - // Returns true if the given factory likely represents a hardware-accelerated decoder. - // Heuristics: checks metadata/klass for "Hardware" and common vendor tags in the factory name. - bool is_hardware_decoder_factory(GstElementFactory *factory); + void setCodecPriorities(VideoDecoderOptions option); } diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc new file mode 100644 index 000000000000..81138e437b8b --- /dev/null +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.cc @@ -0,0 +1,127 @@ +#include "GStreamer.h" +#include "GStreamerLogging.h" +#include "QGCLoggingCategory.h" + +#include + +#include +#include + +QGC_LOGGING_CATEGORY(GStreamerLog, "Video.GStreamer") +QGC_LOGGING_CATEGORY(GStreamerDecoderRanksLog, "Video.GStreamerDecoderRanks") +QGC_LOGGING_CATEGORY_ON(GStreamerAPILog, "Video.GStreamerAPI") + +namespace { + +std::atomic_bool g_externalPluginLoaderFailed {false}; + +void glib_print_handler(const gchar *string) +{ + qCInfo(GStreamerLog) << string; +} + +void glib_printerr_handler(const gchar *string) +{ + qCWarning(GStreamerLog) << string; +} + +void glib_log_handler(const gchar *log_domain, GLogLevelFlags log_level, + const gchar *message, gpointer user_data) +{ + Q_UNUSED(user_data); + const QString domain = log_domain ? QString::fromUtf8(log_domain) : QStringLiteral("GLib"); + const QString msg = QString::fromUtf8(message); + + if (msg.contains(QStringLiteral("External plugin loader failed"), Qt::CaseInsensitive)) { + g_externalPluginLoaderFailed.store(true); + } + + switch (log_level & G_LOG_LEVEL_MASK) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + qCCritical(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_WARNING: + qCWarning(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_MESSAGE: + case G_LOG_LEVEL_INFO: + qCInfo(GStreamerLog) << domain << msg; + break; + case G_LOG_LEVEL_DEBUG: + default: + qCDebug(GStreamerLog) << domain << msg; + break; + } +} + +} // anonymous namespace + +namespace GStreamer +{ + +void resetExternalPluginLoaderFailure() +{ + g_externalPluginLoaderFailed.store(false); +} + +bool didExternalPluginLoaderFail() +{ + return g_externalPluginLoaderFailed.load(); +} + +void redirectGLibLogging() +{ + g_set_print_handler(glib_print_handler); + g_set_printerr_handler(glib_printerr_handler); + g_log_set_default_handler(glib_log_handler, nullptr); +} + +void qtGstLog(GstDebugCategory *category, + GstDebugLevel level, + const gchar *file, + const gchar *function, + gint line, + GObject *object, + GstDebugMessage *message, + gpointer data) +{ + Q_UNUSED(data); + + if (level > gst_debug_category_get_threshold(category)) { + return; + } + + QMessageLogger log(file, line, function); + + struct GFree { void operator()(gchar *p) const { g_free(p); } }; + const std::unique_ptr object_info( + gst_info_strdup_printf("%" GST_PTR_FORMAT, object)); + + switch (level) { + case GST_LEVEL_ERROR: + log.critical(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_WARNING: + log.warning(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_FIXME: + case GST_LEVEL_INFO: + log.info(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + case GST_LEVEL_DEBUG: +#ifdef QT_DEBUG + // In release builds LOG/TRACE/MEMDUMP are intentionally dropped to reduce + // noise. Only debug builds route these verbose levels through Qt logging. + case GST_LEVEL_LOG: + case GST_LEVEL_TRACE: + case GST_LEVEL_MEMDUMP: +#endif + log.debug(GStreamerAPILog, "%s %s", object_info.get(), gst_debug_message_get(message)); + break; + default: + break; + } +} + +} // namespace GStreamer diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h new file mode 100644 index 000000000000..6a6212ede873 --- /dev/null +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamerLogging.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace GStreamer +{ + void redirectGLibLogging(); + void resetExternalPluginLoaderFailure(); + bool didExternalPluginLoaderFail(); + + void qtGstLog(GstDebugCategory *category, + GstDebugLevel level, + const gchar *file, + const gchar *function, + gint line, + GObject *object, + GstDebugMessage *message, + gpointer data); +} diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc index e23866817b64..187f22aaaaaf 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.cc @@ -24,7 +24,7 @@ GstVideoReceiver::GstVideoReceiver(QObject *parent) : VideoReceiver(parent) , _worker(new GstVideoWorker(this)) { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; _worker->start(); (void) connect(&_watchdogTimer, &QTimer::timeout, this, &GstVideoReceiver::_watchdog); @@ -36,7 +36,7 @@ GstVideoReceiver::~GstVideoReceiver() stop(); _worker->shutdown(); - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } void GstVideoReceiver::start(uint32_t timeout) @@ -367,8 +367,11 @@ void GstVideoReceiver::startDecoding(void *sink) return; } + _ensureVideoSinkInPipeline(); + if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed" << _uri; + _shutdownDecodingBranch(); _dispatchSignal([this]() { emit onStartDecodingComplete(STATUS_FAIL); }); return; } @@ -641,7 +644,7 @@ GstElement *GstVideoReceiver::_makeSource(const QString &input) do { if (isRtsp) { - if (!GStreamer::is_valid_rtsp_uri(input.toUtf8().constData())) { + if (!GStreamer::isValidRtspUri(input.toUtf8().constData())) { qCCritical(GstVideoReceiverLog) << "Invalid RTSP URI:" << input; break; } @@ -791,15 +794,12 @@ GstElement *GstVideoReceiver::_makeSource(const QString &input) return srcbin; } -GstElement *GstVideoReceiver::_makeDecoder(GstCaps *caps, GstElement *videoSink) +GstElement *GstVideoReceiver::_makeDecoder() { - Q_UNUSED(caps); Q_UNUSED(videoSink) - GstElement *decoder = gst_element_factory_make("decodebin3", nullptr); if (!decoder) { qCCritical(GstVideoReceiverLog) << "gst_element_factory_make('decodebin3') failed"; } - return decoder; } @@ -899,8 +899,11 @@ void GstVideoReceiver::_onNewSourcePad(GstPad *pad) GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-new-source-pad"); + _ensureVideoSinkInPipeline(); + if (!_addDecoder(_decoderValve)) { qCCritical(GstVideoReceiverLog) << "_addDecoder() failed"; + _shutdownDecodingBranch(); return; } @@ -928,7 +931,7 @@ void GstVideoReceiver::_logDecodebin3SelectedCodec(GstElement *decodebin3) GstPluginFeature *feature = GST_PLUGIN_FEATURE(factory); const gchar *featureName = gst_plugin_feature_get_name(feature); const guint rank = gst_plugin_feature_get_rank(feature); - bool isHardwareDecoder = GStreamer::is_hardware_decoder_factory(factory); + bool isHardwareDecoder = GStreamer::isHardwareDecoderFactory(factory); QString pluginName = featureName; GstPlugin *plugin = gst_plugin_feature_get_plugin(feature); @@ -937,6 +940,13 @@ void GstVideoReceiver::_logDecodebin3SelectedCodec(GstElement *decodebin3) gst_object_unref(plugin); } qCDebug(GstVideoReceiverLog) << "Decodebin3 selected codec:rank -" << pluginName << "/" << featureName << "-" << decoderKlass << (isHardwareDecoder ? "(HW)" : "(SW)") << ":" << rank; + + // Disable QoS on the internal decoder to prevent cascading + // frame drops on live streams. The videodecoder base class + // aggressively advances earliest_time after the first late + // frame, causing all subsequent frames to be dropped. + g_object_set(child, "qos", FALSE, nullptr); + qCDebug(GstVideoReceiverLog) << "Disabled QoS on internal decoder" << featureName; } } g_value_reset(&value); @@ -962,32 +972,14 @@ void GstVideoReceiver::_onNewDecoderPad(GstPad *pad) bool GstVideoReceiver::_addDecoder(GstElement *src) { - GstPad *srcpad = gst_element_get_static_pad(src, "src"); - if (!srcpad) { - qCCritical(GstVideoReceiverLog) << "gst_element_get_static_pad() failed"; - return false; - } - - GstCaps *caps = gst_pad_query_caps(srcpad, nullptr); - if (!caps) { - qCCritical(GstVideoReceiverLog) << "gst_pad_query_caps() failed"; - gst_clear_object(&srcpad); - return false; - } - - gst_clear_object(&srcpad); - _decoder = _makeDecoder(); if (!_decoder) { qCCritical(GstVideoReceiverLog) << "_makeDecoder() failed"; - gst_clear_caps(&caps); return false; } (void) gst_object_ref(_decoder); - gst_clear_caps(&caps); - (void) gst_bin_add(GST_BIN(_pipeline), _decoder); (void) gst_element_sync_state_with_parent(_decoder); @@ -995,6 +987,9 @@ bool GstVideoReceiver::_addDecoder(GstElement *src) if (!gst_element_link(src, _decoder)) { qCCritical(GstVideoReceiverLog) << "Unable to link decoder"; + (void) gst_bin_remove(GST_BIN(_pipeline), _decoder); + (void) gst_element_set_state(_decoder, GST_STATE_NULL); + gst_clear_object(&_decoder); return false; } @@ -1026,18 +1021,16 @@ bool GstVideoReceiver::_addDecoder(GstElement *src) return true; } -bool GstVideoReceiver::_addVideoSink(GstPad *pad) +void GstVideoReceiver::_ensureVideoSinkInPipeline() { - GstCaps *caps = gst_pad_query_caps(pad, nullptr); - - (void) gst_object_ref(_videoSink); // gst_bin_add() will steal one reference - (void) gst_bin_add(GST_BIN(_pipeline), _videoSink); + if (!_videoSink || !_pipeline) { + return; + } - if (!gst_element_link(_decoder, _videoSink)) { - (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); - qCCritical(GstVideoReceiverLog) << "Unable to link video sink"; - gst_clear_caps(&caps); - return false; + GstObject *parent = gst_element_get_parent(_videoSink); + if (parent) { + gst_object_unref(parent); + return; } g_object_set(_videoSink, @@ -1045,8 +1038,50 @@ bool GstVideoReceiver::_addVideoSink(GstPad *pad) "sync", (_buffer >= 0), NULL); + (void) gst_object_ref(_videoSink); + (void) gst_bin_add(GST_BIN(_pipeline), _videoSink); + + // PAUSED (not READY) triggers gst_gl_ensure_element_data in qml6glsink, + // which creates GstGLDisplay/GstGLContext from Qt's EGL context and posts + // HAVE_CONTEXT on the bus. Downstream GL elements and decoders acquire + // this context via the pipeline's context store. + (void) gst_element_set_state(_videoSink, GST_STATE_PAUSED); +} + +bool GstVideoReceiver::_addVideoSink(GstPad *pad) +{ + GstCaps *caps = gst_pad_query_caps(pad, nullptr); + + _ensureVideoSinkInPipeline(); + + GstPad *sinkPad = gst_element_get_static_pad(_videoSink, "sink"); + GstPadLinkReturn linkRet = sinkPad ? gst_pad_link(pad, sinkPad) : GST_PAD_LINK_WRONG_HIERARCHY; + if (linkRet != GST_PAD_LINK_OK) { + qCCritical(GstVideoReceiverLog) << "Unable to link decoder pad to video sink, result:" << linkRet; + + // Keep retry behavior by fully detaching the sink when link fails. + // _ensureVideoSinkInPipeline() adds it before linking. + GstObject *parent = gst_element_get_parent(_videoSink); + if (parent) { + (void) gst_element_set_state(_videoSink, GST_STATE_NULL); + (void) gst_bin_remove(GST_BIN(_pipeline), _videoSink); + gst_clear_object(&parent); + } + + gst_clear_object(&sinkPad); + gst_clear_caps(&caps); + return false; + } + gst_clear_object(&sinkPad); + (void) gst_element_sync_state_with_parent(_videoSink); + // qml6glsink resets max-lateness=0 during state transitions (it renders + // on QML's paint cycle). For live streams with hardware decoders that + // have startup latency, this causes all frames to be silently dropped + // after the first one. Override after state sync so it sticks. + g_object_set(_videoSink, "sync", FALSE, "max-lateness", G_GINT64_CONSTANT(-1), nullptr); + GST_DEBUG_BIN_TO_DOT_FILE(GST_BIN(_pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "pipeline-with-videosink"); // Determine video size. Errors here are non-fatal. @@ -1472,12 +1507,12 @@ GstPadProbeReturn GstVideoReceiver::_keyframeWatch(GstPad *pad, GstPadProbeInfo GstVideoWorker::GstVideoWorker(QObject *parent) : QThread(parent) { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } GstVideoWorker::~GstVideoWorker() { - // qCDebug(GstVideoReceiverLog) << this; + qCDebug(GstVideoReceiverLog) << this; } bool GstVideoWorker::needDispatch() const diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h index 1d65365f7dab..42870f6c1dd6 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h +++ b/src/VideoManager/VideoReceiver/GStreamer/GstVideoReceiver.h @@ -66,12 +66,13 @@ private slots: private: GstElement *_makeSource(const QString &input); - GstElement *_makeDecoder(GstCaps *caps = nullptr, GstElement *videoSink = nullptr); + GstElement *_makeDecoder(); GstElement *_makeFileSink(const QString &videoFile, FILE_FORMAT format); void _onNewSourcePad(GstPad *pad); void _onNewDecoderPad(GstPad *pad); bool _addDecoder(GstElement *src); + void _ensureVideoSinkInPipeline(); bool _addVideoSink(GstPad *pad); void _noteTeeFrame(); void _noteVideoSinkFrame(); diff --git a/src/VideoManager/VideoReceiver/GStreamer/README.md b/src/VideoManager/VideoReceiver/GStreamer/README.md index a0aa741de3ff..f3e497b20cb6 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/README.md +++ b/src/VideoManager/VideoReceiver/GStreamer/README.md @@ -1,158 +1,64 @@ -# QGroundControl +# GStreamer Video Streaming -## Video Streaming +QGroundControl uses GStreamer for UDP RTP and RTSP video streaming in the Main Flight Display. -QGroundControl implements an UDP RTP and RSTP video streaming receiver in its Main Flight Display using GStreamer. -Current suggested version of GStreamer is **1.22.12** - other versions of GStreamer may break the build as some dependent libraries may change. -To build video streaming support, you will need to install the GStreamer development packages for the desired target platform. +## Build Configuration -If you do have the proper GStreamer development libraries installed where QGC looks for it, the QGC build system will automatically use it and build video streaming support. If you would like to disable GStreamer video streaming support, set the **QGC_ENABLE_GST_VIDEOSTREAMING** CMake option to **OFF**. +- **Enable/disable**: Set `QGC_ENABLE_GST_VIDEOSTREAMING` CMake option to `ON`/`OFF` +- **Version & URLs**: Defined in [`.github/build-config.json`](../../../../.github/build-config.json), parsed by [`cmake/BuildConfig.cmake`](../../../../cmake/BuildConfig.cmake) +- **SDK discovery & auto-download**: [`cmake/find-modules/FindQGCGStreamer.cmake`](../../../../cmake/find-modules/FindQGCGStreamer.cmake) +- **Plugin allowlist**: `GSTREAMER_PLUGINS` in `FindQGCGStreamer.cmake` — controls both static linking (mobile) and dynamic install (desktop) +- **Install helpers**: [`cmake/find-modules/GStreamerHelpers.cmake`](../../../../cmake/find-modules/GStreamerHelpers.cmake) -### Gstreamer logs +## Runtime Environment Setup -For cases, when it is need to have more control over gstreamer logging than is availabe via QGroundControl's UI, it is possible to configure gstreamer logging via environment variables. Please see for details. +QGC configures GStreamer runtime paths before `gst_init()`: -### UDP Pipeline +- **Desktop (Linux/macOS/Windows)**: `GStreamer::prepareEnvironment()` sets plugin paths, scanner/helper paths, and GIO module paths when bundled runtimes are detected. +- **Linux AppImage**: [`deploy/linux/AppRun`](../../../../deploy/linux/AppRun) exports `GST_PLUGIN_*`, `GST_PLUGIN_SCANNER*`, and `GST_PTP_HELPER*` only when bundled paths are valid. +- **Environment hygiene**: Python virtualenv/conda variables are cleared for scanner stability (`PYTHONHOME`, `PYTHONPATH`, `VIRTUAL_ENV`, `CONDA_*`). +- **Validation**: If bundled plugin directories are present but `gst-plugin-scanner` is missing or non-executable, QGC fails initialization early with a clear error instead of running with a broken plugin loader. -For the time being, the RTP UDP pipeline is somewhat hardcoded, using h.264 or h.265. It's best to use a camera capable of hardware encoding either h.264 (such as the Logitech C920) or h.265. On the sender end, for RTP (UDP Streaming) you would run something like this: +## Platform Setup -h.264 - -``` -gst-launch-1.0 uvch264src initial-bitrate=1000000 average-bitrate=1000000 iframe-period=1000 device=/dev/video0 name=src auto-start=true src.vidsrc ! video/x-h264,width=1920,height=1080,framerate=24/1 ! h264parse ! rtph264pay ! udpsink host=xxx.xxx.xxx.xxx port=5600 -``` - -h.265 - -``` -ffmpeg -f v4l2 -i /dev/video1 -pix_fmt yuv420p -c:v libx265 -preset ultrafast -x265-params crf=23 -strict experimental -f rtp udp://xxx.xxx.xxx.xxx:5600 -``` - -Where xxx.xxx.xxx.xxx is the IP address where QGC is running. - -To test using a test source on localhost, you can run this command: - -``` -gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 -``` - -Or this one: - -``` -gst-launch-1.0 videotestsrc ! video/x-raw,width=640,height=480 ! videoconvert ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 -``` - -On the receiving end, if you want to test it from the command line, you can use something like: - -``` -gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink fps-update-interval=1000 sync=false -``` - -Or this one: - -``` -gst-launch-1.0 udpsrc port=5600 ! application/x-rtp ! rtpjitterbuffer ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink -``` +### Linux -Or this one, note that removing rtpjitterbuffer would reduce video latency as low latency mode is doing: +Use `tools/setup/install-dependencies-debian.sh`, or manually: ``` -gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! parsebin ! decodebin ! autovideosink fps-update-interval=1000 sync=false +sudo apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev python3-gi python3-gst-1.0 ``` -### Additional Protocols - -QGC also supports RTSP, TCP-MPEG2 and MPEG-TS pipelines. +### macOS / Windows / iOS -## Setup +GStreamer SDKs are auto-downloaded during CMake configure. To use a local installation instead, set `GStreamer_ROOT_DIR`. -### Linux +### Android -For a quick setup, use `python3 qgroundcontrol/tools/setup/install_dependencies.py --platform debian`. +Auto-downloaded during CMake configure. No manual setup required. -Alternatively for a manual approach you can use apt-get to install GStreamer 1.0: +> **Windows users building for Android**: Enable Developer Mode (Settings > System > For developers) to support symbolic links during the build. -``` -list=$(apt-cache --names-only search ^gstreamer1.0-* | awk '{ print $1 }' | sed -e /-doc/d | grep -v gstreamer1.0-hybris) -``` +## Testing Pipelines -``` -sudo apt-get install $list -``` +### Sending test video (h.264 over UDP) ``` -sudo apt-get install libgstreamer-plugins-base1.0-dev -sudo apt-get install libgstreamer-plugins-bad1.0-dev +gst-launch-1.0 videotestsrc pattern=ball ! video/x-raw,width=640,height=480 ! x264enc ! rtph264pay ! udpsink host=127.0.0.1 port=5600 ``` -The build system is setup to use pkgconfig and it will find the necessary headers and libraries automatically. - -### Mac OS - -Download the gstreamer framework from here: . Supported version is 1.22.12. - -You need two packages: - -- [gstreamer-1.0-devel-1.22.12-x86_64.pkg](https://gstreamer.freedesktop.org/data/pkg/osx/1.22.12/gstreamer-1.0-devel-1.22.12-universal.pkg) -- [gstreamer-1.0-1.22.12-x86_64.pkg](https://gstreamer.freedesktop.org/data/pkg/osx/1.22.12/gstreamer-1.0-1.22.12-universal.pkg) - -The installer places them under /Library/Frameworks/GStreamer.framework, which is where the QGC build system will look for it. That's all that is needed. When you build QGC and it finds the gstreamer framework, it automatically builds video streaming support. - -:point_right: To run gstreamer commands from the command line, you can add the path to find them (either in ~/.profile or ~/.bashrc): +### Receiving test video ``` -export PATH=$PATH:/Library/Frameworks/GStreamer.framework/Commands +gst-launch-1.0 udpsrc port=5600 caps='application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)H264' ! rtpjitterbuffer ! rtph264depay ! h264parse ! avdec_h264 ! autovideosink sync=false ``` -### iOS - -Download the gstreamer framework from here: [gstreamer-1.0-devel-1.22.12-ios-universal.pkg](https://gstreamer.freedesktop.org/data/pkg/ios/1.22.12/gstreamer-1.0-devel-1.22.12-ios-universal.pkg) - -The installer places them under ~/Library/Developer/GStreamer/iPhone.sdk/GStreamer.framework, which is where the QGC build system will look for it. That's all that is needed. When you build QGC and it finds the gstreamer framework, it automatically builds video streaming support. - -### Android - -An appropriate version of GStreamer will be automatically downloaded as part of the CMake configure step. - -#### Important Note for Windows Users - -During the build process for Android on Windows, to ensure support for UNIX-like symbolic links when not using elevated privileges, **Developer Mode** must be enabled. Without enabling Developer Mode, you may encounter linking issues with the automatically downloaded prebuilt version of GStreamer. - -To enable Developer Mode: - -1. Open the **Settings** app on Windows. -2. Go to **System** > **For developers**. -3. Enable **Developer Mode** by toggling the switch. - -### Windows - -Download the gstreamer framework from here: . Supported version is 1.22.12. QGC may work with newer version, but it is untested. - -You need two packages: - -- [gstreamer-1.0-devel-msvc-x86_64-1.22.12.msi](https://gstreamer.freedesktop.org/data/pkg/windows/1.22.12/msvc/gstreamer-1.0-devel-msvc-x86_64-1.22.12.msi) -- [gstreamer-1.0-msvc-x86_64-1.22.12.msi](https://gstreamer.freedesktop.org/data/pkg/windows/1.22.12/msvc/gstreamer-1.0-msvc-x86_64-1.22.12.msi) - -Make sure you select "Complete" installation instead of "Typical" installation during the install process. +### Supported protocols -The following environment variables can be used to configure the GStreamer installation path: -GSTREAMER_1_0_ROOT_X86_64 -GSTREAMER_1_0_ROOT_MSVC_X86_64 -GSTREAMER_1_0_ROOT_MINGW_X86_64 +UDP RTP, RTSP, TCP-MPEG2, MPEG-TS. -### Gstreamer Config +## GStreamer Logging -When running QGC, you can provide the command line options to configure GStreamer: ---gst-version "Print the GStreamer version" ---gst-fatal-warnings "Make all warnings fatal" ---gst-debug-help "Print available debug categories and exit" ---gst-debug-level "Default debug level from 1 (only error) to 9 (anything) or 0 for no output" ---gst-debug "Comma-separated list of category_name:level pairs to set specific levels for the individual categories. Example: GST_AUTOPLUG:5,GST_ELEMENT_*:3" ---gst-debug-no-color "Disable colored debugging output" ---gst-debug-color-mode "Changes coloring mode of the debug log. Possible modes: off, on, disable, auto, unix" ---gst-debug-disable "Disable debugging" ---gst-plugin-path "Colon-separated paths containing plugins" ---gst-plugin-load "Comma-separated list of plugins to preload in addition to the list stored in environment variable GST_PLUGIN_PATH" ---gst-disable-segtrap "Disable trapping of segmentation faults during plugin loading" ---gst-disable-registry-update "Disable updating the registry" ---gst-disable-registry-fork "Disable spawning a helper process while scanning the registry" +- In-app: Use QGroundControl's logging settings +- Environment variables: See +- Command line options: `--gst-debug-level`, `--gst-debug`, `--gst-debug-help`, etc. diff --git a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h b/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h deleted file mode 100644 index 32a385078830..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -G_BEGIN_DECLS - -void gst_ios_pre_init(void); -void gst_ios_post_init(void); - -G_END_DECLS diff --git a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m b/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m deleted file mode 100644 index 638744391b09..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gst_ios_init.m +++ /dev/null @@ -1,80 +0,0 @@ -#include -#include -#include - -/* Declaration of static plugins */ - @PLUGINS_DECLARATION@ - -/* Declaration of static gio modules */ - @G_IO_MODULES_DECLARE@ - -#ifdef GSTREAMER_INCLUDE_CA_CERTIFICATES -static void -gst_ios_load_certificates (const gchar *resources_dir) -{ - gchar *ca_certificates; - - ca_certificates = g_build_filename (resources_dir, "ssl", "certs", "ca-certificates.crt", NULL); - g_setenv ("CA_CERTIFICATES", ca_certificates, TRUE); - - if (ca_certificates) { - GTlsBackend *backend = g_tls_backend_get_default (); - if (backend) { - GTlsDatabase *db = g_tls_file_database_new (ca_certificates, NULL); - if (db) - g_tls_backend_set_default_database (backend, db); - } - } - g_free (ca_certificates); -} - -#endif - -void -gst_ios_init (void) -{ - GstPluginFeature *plugin; - GstRegistry *reg; - NSString *resources = [[NSBundle mainBundle] resourcePath]; - NSString *tmp = NSTemporaryDirectory(); - NSString *cache = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Caches"]; - NSString *docs = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; - - const gchar *resources_dir = [resources UTF8String]; - const gchar *tmp_dir = [tmp UTF8String]; - const gchar *cache_dir = [cache UTF8String]; - const gchar *docs_dir = [docs UTF8String]; - - g_setenv ("TMP", tmp_dir, TRUE); - g_setenv ("TEMP", tmp_dir, TRUE); - g_setenv ("TMPDIR", tmp_dir, TRUE); - g_setenv ("XDG_RUNTIME_DIR", resources_dir, TRUE); - g_setenv ("XDG_CACHE_HOME", cache_dir, TRUE); - - g_setenv ("HOME", docs_dir, TRUE); - g_setenv ("XDG_DATA_DIRS", resources_dir, TRUE); - g_setenv ("XDG_CONFIG_DIRS", resources_dir, TRUE); - g_setenv ("XDG_CONFIG_HOME", cache_dir, TRUE); - g_setenv ("XDG_DATA_HOME", resources_dir, TRUE); - g_setenv ("FONTCONFIG_PATH", resources_dir, TRUE); - - @G_IO_MODULES_LOAD@ - -#ifdef GSTREAMER_INCLUDE_CA_CERTIFICATES - gst_ios_load_certificates (resources_dir); -#endif - - gst_init (NULL, NULL); - - @PLUGINS_REGISTRATION@ - - /* Lower the ranks of filesrc and giosrc so iosavassetsrc is - * tried first in gst_element_make_from_uri() for file:// */ - reg = gst_registry_get(); - plugin = gst_registry_lookup_feature(reg, "filesrc"); - if (plugin) - gst_plugin_feature_set_rank(plugin, GST_RANK_SECONDARY); - plugin = gst_registry_lookup_feature(reg, "giosrc"); - if (plugin) - gst_plugin_feature_set_rank(plugin, GST_RANK_SECONDARY-1); -} diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc index 8f4b5d269c5c..7b93bc27fa30 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc @@ -11,6 +11,7 @@ GST_DEBUG_CATEGORY_STATIC(GST_CAT_DEFAULT); #define DEFAULT_PAR_N 0 #define DEFAULT_PAR_D 1 #define DEFAULT_SYNC TRUE +#define DEFAULT_MAX_LATENESS G_GINT64_CONSTANT(-1) #define PROP_ENABLE_LAST_SAMPLE_NAME "enable-last-sample" #define PROP_LAST_SAMPLE_NAME "last-sample" @@ -18,6 +19,7 @@ GST_DEBUG_CATEGORY_STATIC(GST_CAT_DEFAULT); #define PROP_FORCE_ASPECT_RATIO_NAME "force-aspect-ratio" #define PROP_PIXEL_ASPECT_RATIO_NAME "pixel-aspect-ratio" #define PROP_SYNC_NAME "sync" +#define PROP_MAX_LATENESS_NAME "max-lateness" enum { @@ -28,6 +30,7 @@ enum PROP_FORCE_ASPECT_RATIO, PROP_PIXEL_ASPECT_RATIO, PROP_SYNC, + PROP_MAX_LATENESS, PROP_LAST }; @@ -118,6 +121,14 @@ gst_qgc_video_sink_bin_class_init(GstQgcVideoSinkBinClass *klass) (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS) ); + properties[PROP_MAX_LATENESS] = g_param_spec_int64( + "max-lateness", "Max lateness", + "Maximum number of nanoseconds a buffer can be late before it is dropped (-1 unlimited)", + -1, G_MAXINT64, + DEFAULT_MAX_LATENESS, + (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS) + ); + g_object_class_install_properties(object_class, PROP_LAST, properties); gst_qgc_video_sink_bin_signals[SIGNAL_CREATE_ELEMENT] = g_signal_new( @@ -194,38 +205,50 @@ gst_qgc_video_sink_bin_set_property(GObject *object, guint prop_id, const GValue switch (prop_id) { case PROP_ENABLE_LAST_SAMPLE: - g_object_set(self->glsinkbin, - PROP_ENABLE_LAST_SAMPLE_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_ENABLE_LAST_SAMPLE_NAME, + g_value_get_boolean(value), + NULL); break; case PROP_WIDGET: - g_object_set(self->qmlglsink, - PROP_WIDGET_NAME, - g_value_get_pointer(value), - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_WIDGET_NAME, + g_value_get_pointer(value), + NULL); break; case PROP_FORCE_ASPECT_RATIO: - g_object_set(self->glsinkbin, - PROP_FORCE_ASPECT_RATIO_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_FORCE_ASPECT_RATIO_NAME, + g_value_get_boolean(value), + NULL); break; case PROP_PIXEL_ASPECT_RATIO: { const gint num = gst_value_get_fraction_numerator(value); const gint den = gst_value_get_fraction_denominator(value); - g_object_set(self->qmlglsink, - PROP_PIXEL_ASPECT_RATIO_NAME, - num, - den, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_PIXEL_ASPECT_RATIO_NAME, + num, + den, + NULL); break; } case PROP_SYNC: - g_object_set(self->glsinkbin, - PROP_SYNC_NAME, - g_value_get_boolean(value), - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_set(self->glsinkbin, + PROP_SYNC_NAME, + g_value_get_boolean(value), + NULL); + break; + case PROP_MAX_LATENESS: + if (G_LIKELY(self->qmlglsink)) + g_object_set(self->qmlglsink, + PROP_MAX_LATENESS_NAME, + g_value_get_int64(value), + NULL); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); @@ -241,19 +264,21 @@ gst_qgc_video_sink_bin_get_property(GObject *object, guint prop_id, GValue *valu switch (prop_id) { case PROP_ENABLE_LAST_SAMPLE: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_ENABLE_LAST_SAMPLE_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_ENABLE_LAST_SAMPLE_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } case PROP_LAST_SAMPLE: { GstSample *sample = NULL; - g_object_get(self->glsinkbin, - PROP_LAST_SAMPLE_NAME, - &sample, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_LAST_SAMPLE_NAME, + &sample, + NULL); gst_value_set_sample(value, sample); if (sample) { gst_sample_unref(sample); @@ -262,40 +287,54 @@ gst_qgc_video_sink_bin_get_property(GObject *object, guint prop_id, GValue *valu } case PROP_WIDGET: { gpointer widget = NULL; - g_object_get(self->qmlglsink, - PROP_WIDGET_NAME, - &widget, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_WIDGET_NAME, + &widget, + NULL); g_value_set_pointer(value, widget); break; } case PROP_FORCE_ASPECT_RATIO: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_FORCE_ASPECT_RATIO_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_FORCE_ASPECT_RATIO_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } case PROP_PIXEL_ASPECT_RATIO: { gint num = 0, den = 1; - g_object_get(self->qmlglsink, - PROP_PIXEL_ASPECT_RATIO_NAME, - &num, &den, - NULL); + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_PIXEL_ASPECT_RATIO_NAME, + &num, &den, + NULL); gst_value_set_fraction(value, num, den); break; } case PROP_SYNC: { gboolean enable = FALSE; - g_object_get(self->glsinkbin, - PROP_SYNC_NAME, - &enable, - NULL); + if (G_LIKELY(self->glsinkbin)) + g_object_get(self->glsinkbin, + PROP_SYNC_NAME, + &enable, + NULL); g_value_set_boolean(value, enable); break; } + case PROP_MAX_LATENESS: { + gint64 lateness = DEFAULT_MAX_LATENESS; + if (G_LIKELY(self->qmlglsink)) + g_object_get(self->qmlglsink, + PROP_MAX_LATENESS_NAME, + &lateness, + NULL); + g_value_set_int64(value, lateness); + break; + } default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); break; diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt index c2ff75c707a4..78e41bf3df54 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/CMakeLists.txt @@ -5,92 +5,113 @@ qt_add_library(gstqml6gl STATIC) -# ---------------------------------------------------------------------------- -# Platform-Specific GStreamer Linking -# ---------------------------------------------------------------------------- -if(MACOS) - # macOS uses framework path directly (FindGStreamer.cmake bypassed) - find_library(GSTREAMER_FRAMEWORK GStreamer) - set(GST_REQUIRED_FRAMEWORK_VERSION ${QGC_CONFIG_GSTREAMER_MACOS_VERSION}) - set(GST_PLUGINS_VERSION ${GST_REQUIRED_FRAMEWORK_VERSION}) - set(GSTREAMER_FRAMEWORK_PATH "/Library/Frameworks/GStreamer.framework") - if(NOT EXISTS "${GSTREAMER_FRAMEWORK_PATH}") - message(FATAL_ERROR "GStreamer.framework not found at ${GSTREAMER_FRAMEWORK_PATH}. Install GStreamer using tools/setup/install_dependencies.py --platform macos") - endif() - target_link_libraries(gstqml6gl PUBLIC "$") - target_include_directories(gstqml6gl SYSTEM PUBLIC "${GSTREAMER_FRAMEWORK_PATH}/Headers") - # Verify correct framework version - execute_process( - COMMAND defaults read "${GSTREAMER_FRAMEWORK_PATH}/Versions/1.0/Resources/Info.plist" CFBundleShortVersionString - OUTPUT_VARIABLE GST_INSTALLED_FRAMEWORK_VERSION - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - if(NOT GST_INSTALLED_FRAMEWORK_VERSION STREQUAL GST_REQUIRED_FRAMEWORK_VERSION) - message(FATAL_ERROR "GStreamer.framework version (${GST_INSTALLED_FRAMEWORK_VERSION}) does not match required framework version (${GST_REQUIRED_FRAMEWORK_VERSION}). Please install the correct version using tools/setup/install_dependencies.py --platform macos") - endif() -else() - target_link_libraries(gstqml6gl PUBLIC GStreamer::GStreamer) -endif() +target_link_libraries(gstqml6gl PUBLIC GStreamer::GStreamer) # TODO: Skip custom build if system gstreamer1.0-qt6 plugin is found # if(GST_PLUGIN_qml6_FOUND) # return() # endif() -# ============================================================================ -# GStreamer Good Plugins Integration -# ============================================================================ +include(GStreamerHelpers) + +message(STATUS "gstqml6gl: GStreamer_VERSION = '${GStreamer_VERSION}'") if(GStreamer_VERSION VERSION_GREATER_EQUAL "1.22") - # Use Latest Revisions for each minor version: 1.20.7, 1.22.12, 1.24.13, 1.26.6 string(REPLACE "." ";" GST_VERSION_LIST "${GStreamer_VERSION}") list(GET GST_VERSION_LIST 0 GST_VERSION_MAJOR) list(GET GST_VERSION_LIST 1 GST_VERSION_MINOR) - list(GET GST_VERSION_LIST 2 GST_VERSION_PATCH) - - if(GST_VERSION_MINOR EQUAL 20) - set(GST_VERSION_PATCH 7) - elseif(GST_VERSION_MINOR EQUAL 22) - set(GST_VERSION_PATCH 12) - elseif(GST_VERSION_MINOR EQUAL 24) - set(GST_VERSION_PATCH 13) - elseif(GST_VERSION_MINOR EQUAL 26) - set(GST_VERSION_PATCH 6) + set(GST_VERSION_PATCH "") + if(GStreamer_VERSION MATCHES "^${GST_VERSION_MAJOR}\\.${GST_VERSION_MINOR}\\.([0-9]+)") + set(GST_VERSION_PATCH "${CMAKE_MATCH_1}") + endif() + + gstreamer_get_recommended_version( + ${GST_VERSION_MAJOR} + ${GST_VERSION_MINOR} + GST_PLUGINS_VERSION + "${GST_VERSION_PATCH}" + ) + + # Download gst-plugins-good sources for ext/qt6. + # Primary: standalone tarball from freedesktop.org (~4MB) + # Fallback: path-filtered GitLab archive of just ext/qt6 (~few KB) + gstreamer_get_package_url(good_plugins ${GST_PLUGINS_VERSION} _gst_good_url) + gstreamer_get_package_url(good_plugins_qt6 ${GST_PLUGINS_VERSION} _gst_qt6_url) + gstreamer_fetch_checksum(good_plugins ${GST_PLUGINS_VERSION} _gst_good_hash) + set(_gst_qt6_cache "${CMAKE_BINARY_DIR}/_deps/gstreamer-good-plugins") + gstreamer_resilient_download( + URLS "${_gst_good_url}" + FILENAME "gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz" + DESTINATION_DIR "${_gst_qt6_cache}" + RESULT_VAR _gst_good_archive + EXPECTED_HASH "${_gst_good_hash}" + ALLOW_FAILURE + ) + + if(NOT _gst_good_archive) + message(STATUS "gstqml6gl: Primary source tarball unavailable, trying GitLab qt6 fallback archive") + gstreamer_resilient_download( + URLS "${_gst_qt6_url}" + FILENAME "gstreamer-${GST_PLUGINS_VERSION}-qt6.tar.gz" + DESTINATION_DIR "${_gst_qt6_cache}" + RESULT_VAR _gst_good_archive + ) endif() - set(GST_PLUGINS_VERSION ${GST_VERSION_MAJOR}.${GST_VERSION_MINOR}.${GST_VERSION_PATCH}) + if(NOT _gst_good_archive) + message(FATAL_ERROR "gstqml6gl: Could not download gst-plugins-good sources for Qt6 plugin") + endif() - # Download GStreamer good plugins for Qt6 support CPMAddPackage( NAME gstreamer_good_plugins VERSION ${GST_PLUGINS_VERSION} - URL https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz - # TODO: Add URL_HASH for verification - # URL_HASH https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-${GST_PLUGINS_VERSION}.tar.xz.sha256sum + URL "file://${_gst_good_archive}" + DOWNLOAD_ONLY YES ) - set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + + # Standalone tarball: ext/qt6 at root + # GitLab path-filtered archive: subprojects/gst-plugins-good/ext/qt6 + if(gstreamer_good_plugins_SOURCE_DIR) + if(EXISTS "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/ext/qt6") + elseif(EXISTS "${gstreamer_good_plugins_SOURCE_DIR}/subprojects/gst-plugins-good/ext/qt6") + set(QGC_GST_QT6_PLUGIN_PATH "${gstreamer_good_plugins_SOURCE_DIR}/subprojects/gst-plugins-good/ext/qt6") + endif() + endif() + + if(NOT QGC_GST_QT6_PLUGIN_PATH) + message(FATAL_ERROR "Failed to locate ext/qt6 in downloaded gst-plugins-good sources") + endif() else() + message(STATUS "gstqml6gl: GStreamer < 1.22, using local Qt6 fallback sources") set(QGC_GST_QT6_PLUGIN_PATH "${CMAKE_CURRENT_SOURCE_DIR}/qt6") endif() -# ============================================================================ -# Source Code Patching -# ============================================================================ - -# Fix missing includes in GStreamer Qt6 plugin -file(READ "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h" FILE_CONTENTS) -string(FIND "${FILE_CONTENTS}" "#include " GST_FIX_INCLUDES) -if(GST_FIX_INCLUDES EQUAL -1) - string(REPLACE "#include " "#include \n#include " FILE_CONTENTS "${FILE_CONTENTS}") - file(WRITE "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h" "${FILE_CONTENTS}") +message(STATUS "gstqml6gl: QGC_GST_QT6_PLUGIN_PATH = ${QGC_GST_QT6_PLUGIN_PATH}") + +# Copy to build tree so we don't modify CPM's download cache +set(_qt6_build_sources "${CMAKE_CURRENT_BINARY_DIR}/qt6_sources") +file(REMOVE_RECURSE "${_qt6_build_sources}") +file(COPY "${QGC_GST_QT6_PLUGIN_PATH}/" DESTINATION "${_qt6_build_sources}") +set(QGC_GST_QT6_PLUGIN_PATH "${_qt6_build_sources}") + +# Upstream qt6glitem.h can miss QQuickWindow include, which breaks moc-generated +# metatype code with newer Qt. Patch the copied source tree only. +set(_qt6glitem_header "${QGC_GST_QT6_PLUGIN_PATH}/qt6glitem.h") +if(EXISTS "${_qt6glitem_header}") + file(READ "${_qt6glitem_header}" FILE_CONTENTS) + string(FIND "${FILE_CONTENTS}" "#include " GST_FIX_INCLUDES) + if(GST_FIX_INCLUDES EQUAL -1) + string(REPLACE + "#include " + "#include \n#include " + FILE_CONTENTS + "${FILE_CONTENTS}" + ) + file(WRITE "${_qt6glitem_header}" "${FILE_CONTENTS}") + endif() endif() -# ---------------------------------------------------------------------------- -# Build GStreamer QML6 GL Library -# ---------------------------------------------------------------------------- - -# NOTE: Using file(GLOB) for external GStreamer plugin sources -# CONFIGURE_DEPENDS ensures CMake re-runs if files are added/removed file(GLOB gstqml6gl_SRCS CONFIGURE_DEPENDS "${QGC_GST_QT6_PLUGIN_PATH}/*.cc" @@ -98,7 +119,6 @@ file(GLOB gstqml6gl_SRCS ) target_sources(gstqml6gl PRIVATE ${gstqml6gl_SRCS}) -# Suppress compiler warnings for third-party GStreamer plugin sources qgc_disable_dependency_warnings(gstqml6gl) target_link_libraries(gstqml6gl @@ -120,31 +140,23 @@ target_precompile_headers(gstqml6gl ) -# ============================================================================ -# Shader Compilation -# ============================================================================ +file(GLOB _shader_files + "${QGC_GST_QT6_PLUGIN_PATH}/*.vert" + "${QGC_GST_QT6_PLUGIN_PATH}/*.frag" +) +# GLES-specific shaders are handled separately via qsb-wrapper below +list(FILTER _shader_files EXCLUDE REGEX "_gles\\.frag$") -if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") +if(_shader_files) find_package(Qt6 REQUIRED COMPONENTS ShaderTools) - set(SHADERS - "${QGC_GST_QT6_PLUGIN_PATH}/vertex.vert" - "${QGC_GST_QT6_PLUGIN_PATH}/YUV_TRIPLANAR.frag" - "${QGC_GST_QT6_PLUGIN_PATH}/RGBA.frag" - ) + set(SHADERS ${_shader_files}) + set(OUTPUTS) + foreach(_shader IN LISTS SHADERS) + get_filename_component(_name "${_shader}" NAME) + list(APPEND OUTPUTS "${_name}.qsb") + endforeach() - set(OUTPUTS - vertex.vert.qsb - YUV_TRIPLANAR.frag.qsb - RGBA.frag.qsb - ) - - if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/YUV_BIPLANAR.frag") - LIST(APPEND SHADERS "${QGC_GST_QT6_PLUGIN_PATH}/YUV_BIPLANAR.frag") - LIST(APPEND OUTPUTS "YUV_BIPLANAR.frag.qsb") - endif() - - # Compile shaders for different GLSL versions qt_add_shaders(gstqml6gl "gstqml6gl_shaders" PREFIX "/org/freedesktop/gstreamer/qml6" GLSL "100 es,120,330" @@ -154,8 +166,10 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") BATCHABLE ) - # Handle additional GLES-specific RGBA shader if present + # RGBA_gles.frag generates RGBA.frag.qsb.external (GLES 100 support for the base shader) if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") + find_package(Python3 REQUIRED COMPONENTS Interpreter) + find_program(QSB_PROGRAM NAMES qsb HINTS ${QT_HOST_PATH} ${QT_ROOT_DIR} ${QTDIR} @@ -164,19 +178,21 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") REQUIRED ) - find_program(QSB_WRAPPER - NAMES qsb-wrapper.py - PATHS "${QGC_GST_QT6_PLUGIN_PATH}" - REQUIRED - ) + set(QSB_WRAPPER "${QGC_GST_QT6_PLUGIN_PATH}/qsb-wrapper.py") + if(NOT EXISTS "${QSB_WRAPPER}") + message(FATAL_ERROR "qsb-wrapper.py not found at ${QSB_WRAPPER}") + endif() set(RGBA_BASE_QSB "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb") set(RGBA_GLES_SRC "${QGC_GST_QT6_PLUGIN_PATH}/RGBA_gles.frag") set(RGBA_EXTERNAL "${CMAKE_CURRENT_BINARY_DIR}/.qsb/RGBA.frag.qsb.external") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/.qsb") + add_custom_command( OUTPUT ${RGBA_EXTERNAL} - COMMAND ${QSB_WRAPPER} + COMMAND ${Python3_EXECUTABLE} + ${QSB_WRAPPER} ${QSB_PROGRAM} ${RGBA_EXTERNAL} ${RGBA_GLES_SRC} @@ -184,22 +200,26 @@ if(EXISTS "${QGC_GST_QT6_PLUGIN_PATH}/resources.qrc") DEPENDS ${RGBA_BASE_QSB} ${RGBA_GLES_SRC} + ${QSB_WRAPPER} COMMENT "Generating external GLES shader: ${RGBA_EXTERNAL}" VERBATIM ) + add_custom_target(gstqml6gl_external_shader DEPENDS ${RGBA_EXTERNAL}) + if(gstqml6gl_shaders) + add_dependencies(gstqml6gl_external_shader ${gstqml6gl_shaders}) + endif() + set_source_files_properties("${RGBA_EXTERNAL}" PROPERTIES QT_RESOURCE_ALIAS RGBA.frag.qsb.external) qt_add_resources(gstqml6gl "gstqml6gl_shaders1" PREFIX "/org/freedesktop/gstreamer/qml6" FILES "${RGBA_EXTERNAL}" ) + + add_dependencies(gstqml6gl gstqml6gl_external_shader) endif() endif() -# ============================================================================ -# Platform-Specific OpenGL Support -# ============================================================================ - if(GStreamer_GlX11_FOUND) target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_X11) endif() @@ -212,8 +232,8 @@ if(GStreamer_GlWayland_FOUND) find_package(Qt6 COMPONENTS WaylandClient) if(TARGET Qt6::WaylandClient) target_link_libraries(gstqml6gl PRIVATE Qt6::WaylandClient) + target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_WAYLAND) endif() - target_compile_definitions(gstqml6gl PRIVATE HAVE_QT_WAYLAND) endif() if(ANDROID) diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build b/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build deleted file mode 100644 index 150b0b9f222f..000000000000 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqml6gl/qt6/meson.build +++ /dev/null @@ -1,145 +0,0 @@ -sources = [ - 'gstplugin.cc', - 'gstqt6element.cc', - 'gstqsg6glnode.cc', - 'gstqt6glutility.cc', - 'gstqml6glsink.cc', - 'qt6glitem.cc', -] - -moc_headers = [ - 'qt6glitem.h', - 'gstqsg6glnode.h', -] - -qt6qml_dep = dependency('', required: false) -qt6_option = get_option('qt6') -qt6_method = get_option('qt-method') - -if qt6_option.disabled() - subdir_done() -endif - -if not have_gstgl - if qt6_option.enabled() - error('qt6 qmlglsink plugin is enabled, but gstreamer-gl-1.0 was not found') - endif - subdir_done() -endif - -if not add_languages('cpp', native: false, required: qt6_option) - subdir_done() -endif - -qt6_mod = import('qt6') -if not qt6_mod.has_tools() - if qt6_option.enabled() - error('qt6 qmlglsink plugin is enabled, but qt specific tools were not found') - endif - subdir_done() -endif - -qt6qml_dep = dependency('qt6', modules : ['Core', 'Gui', 'Qml', 'Quick'], - method: qt6_method, required: qt6_option, static: host_system == 'ios') -if not qt6qml_dep.found() - subdir_done() -endif - -optional_deps = [] -qt_defines = [] -have_qpa_include = false -have_qt_windowing = false - -# Look for the QPA platform native interface header -qpa_header_path = join_paths(qt6qml_dep.version(), 'QtGui') -qpa_header = join_paths(qpa_header_path, 'qpa/qplatformnativeinterface.h') -if cxx.has_header(qpa_header, dependencies : qt6qml_dep) - qt_defines += '-DHAVE_QT_QPA_HEADER' - qt_defines += '-DQT_QPA_HEADER=' + '<@0@>'.format(qpa_header) - have_qpa_include = true - message('Found QtGui QPA header in ' + qpa_header_path) -endif - -# Try to come up with all the platform/winsys combinations that will work - -if gst_gl_have_window_x11 and gst_gl_have_platform_glx - # FIXME: automagic - qt_defines += ['-DHAVE_QT_X11'] - have_qt_windowing = true -endif - -if gst_gl_have_platform_egl - # Embedded linux (e.g. i.MX6) with or without windowing support - qt_defines += ['-DHAVE_QT_EGLFS'] - optional_deps += gstglegl_dep - have_qt_windowing = true - if have_qpa_include - # Wayland windowing - if gst_gl_have_window_wayland - # FIXME: automagic - qt6waylandextras = dependency('qt6', modules : ['WaylandClient'], method: qt6_method, required : false) - if qt6waylandextras.found() - optional_deps += [qt6waylandextras, gstglwayland_dep] - qt_defines += ['-DHAVE_QT_WAYLAND'] - have_qt_windowing = true - endif - endif - # Android windowing -# if gst_gl_have_window_android - # FIXME: automagic -# qt5androidextras = dependency('qt5', modules : ['AndroidExtras'], method: qt6_method, required : false) - # for gl functions in QtGui/qopenglfunctions.h - # FIXME: automagic -# glesv2_dep = cc.find_library('GLESv2', required : false) -# if glesv2_dep.found() and qt5androidextras.found() -# optional_deps += [qt5androidextras, glesv2_dep] -# qt_defines += ['-DHAVE_QT_ANDROID'] -# have_qt_windowing = true - # Needed for C++11 support in Cerbero. People building with Android - # in some other way need to add the necessary bits themselves. -# optional_deps += dependency('gnustl', required : false) -# endif -# endif - endif -endif - -#if gst_gl_have_platform_wgl and gst_gl_have_window_win32 - # for wglMakeCurrent() - # FIXME: automagic -# opengl32_dep = cc.find_library('opengl32', required : false) -# if opengl32_dep.found() -# qt_defines += ['-DHAVE_QT_WIN32'] -# optional_deps += opengl32_dep -# have_qt_windowing = true -# endif -#endif - -if gst_gl_have_window_cocoa and gst_gl_have_platform_cgl - # FIXME: automagic - if host_machine.system() == 'darwin' - qt_defines += ['-DHAVE_QT_MAC'] - have_qt_windowing = true - endif -endif - -if gst_gl_have_window_eagl and gst_gl_have_platform_eagl - if host_machine.system() == 'ios' - qt_defines += ['-DHAVE_QT_IOS'] - have_qt_windowing = true - endif -endif - -if have_qt_windowing - # Build it! - moc_files = qt6_mod.preprocess(moc_headers : moc_headers) - gstqml6gl = library('gstqml6', sources, moc_files, - cpp_args : gst_plugins_good_args + qt_defines, - link_args : noseh_link_args, - include_directories: [configinc, libsinc], - dependencies : [gst_dep, gstvideo_dep, gstgl_dep, gstglproto_dep, qt6qml_dep, optional_deps], - override_options : ['cpp_std=c++17'], - install: true, - install_dir : plugins_install_dir) - pkgconfig.generate(gstqml6gl, install_dir : plugins_pkgconfig_install_dir) - plugins += [gstqml6gl] -endif diff --git a/src/Viewer3D/Providers/Osm/CMakeLists.txt b/src/Viewer3D/Providers/Osm/CMakeLists.txt index 87f2b331ada2..e075946ead09 100644 --- a/src/Viewer3D/Providers/Osm/CMakeLists.txt +++ b/src/Viewer3D/Providers/Osm/CMakeLists.txt @@ -16,12 +16,15 @@ target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_ # ---------------------------------------------------------------------------- # earcut.hpp — polygon triangulation +# EXCLUDE_FROM_ALL prevents earcut's unconditionally-built "fixtures" object +# library (30+ test data files) from compiling as part of our build. # ---------------------------------------------------------------------------- CPMAddPackage( NAME earcut_hpp GITHUB_REPOSITORY mapbox/earcut.hpp GIT_TAG f36ced7e50254738c4e5af1a239f5fb7b1094007 + EXCLUDE_FROM_ALL YES OPTIONS "EARCUT_BUILD_TESTS OFF" "EARCUT_BUILD_BENCH OFF" diff --git a/src/Viewer3D/Providers/Osm/OsmParser.cc b/src/Viewer3D/Providers/Osm/OsmParser.cc index 10171d8c09cf..6743aaed1483 100644 --- a/src/Viewer3D/Providers/Osm/OsmParser.cc +++ b/src/Viewer3D/Providers/Osm/OsmParser.cc @@ -21,6 +21,16 @@ OsmParser::OsmParser(QObject *parent) connect(_osmParserWorker, &OsmParserThread::fileParsed, this, &OsmParser::_onOsmParserFinished); } +OsmParser::~OsmParser() +{ + // Stop the worker thread's event loop and wait for it to finish. + // Once the thread is joined, no events are being processed and + // the destructor chain is safe to run from our thread. + _osmParserWorker->thread()->quit(); + _osmParserWorker->thread()->wait(); + delete _osmParserWorker; +} + void OsmParser::setGpsRef(const QGeoCoordinate &gpsRef) { _gpsRefPoint = gpsRef; diff --git a/src/Viewer3D/Providers/Osm/OsmParser.h b/src/Viewer3D/Providers/Osm/OsmParser.h index e562f64126dc..30fbcbb73f95 100644 --- a/src/Viewer3D/Providers/Osm/OsmParser.h +++ b/src/Viewer3D/Providers/Osm/OsmParser.h @@ -22,6 +22,7 @@ class OsmParser : public Viewer3DMapProvider public: explicit OsmParser(QObject *parent = nullptr); + ~OsmParser() override; bool mapLoaded() const override { return _mapLoadedFlag; } QGeoCoordinate gpsRef() const override { return _gpsRefPoint; } diff --git a/src/Viewer3D/Providers/Osm/OsmParserThread.cc b/src/Viewer3D/Providers/Osm/OsmParserThread.cc index 28772af2b0cb..ca63c629a52e 100644 --- a/src/Viewer3D/Providers/Osm/OsmParserThread.cc +++ b/src/Viewer3D/Providers/Osm/OsmParserThread.cc @@ -214,8 +214,8 @@ void OsmParserThread::BuildingType_t::append(const std::vector &newPo // OsmParserThread // ============================================================================ -OsmParserThread::OsmParserThread(QObject *parent) - : QObject{parent} +OsmParserThread::OsmParserThread(QObject * /*parent*/) + : QObject{nullptr} , _workerThread(new QThread()) { connect(this, &OsmParserThread::startThread, this, &OsmParserThread::_parseOsmFile); diff --git a/src/main.cc b/src/main.cc index 799cfff26e44..992b7eef1347 100644 --- a/src/main.cc +++ b/src/main.cc @@ -41,6 +41,10 @@ int main(int argc, char *argv[]) return QGCUnitTest::handleTestOptions(args); #endif case AppMode::BootTest: + if (!app.bootTestPassed()) { + qCCritical(MainLog) << "Simple boot test failed during GStreamer initialization"; + return 1; + } qCInfo(MainLog) << "Simple boot test completed"; return 0; case AppMode::Gui: diff --git a/src/pch.h b/src/pch.h index 476c7d23b618..ecea7c1baf28 100644 --- a/src/pch.h +++ b/src/pch.h @@ -1,34 +1,46 @@ #pragma once -// STL - frequently used across codebase +// STL #include #include -#include #include -// Qt Core - used in nearly every file +// Qt Core - fundamentals +#include +#include #include +#include +#include #include +#include #include +#include #include #include +#include #include +#include #include +#include #include #include -#include +// Qt Core - JSON #include #include #include -// Qt Network - used in 30+ files -#include -#include -#include +// Qt Core - utilities +#include +#include -// Qt Quick - QQuickItem used in 30+ files +// Qt Positioning - used in 50+ files +#include + +// Qt Qml/Quick - QML integration macros used in ~130 headers +#include +#include #include // MAVLink - used in 400+ locations -#include "MAVLinkLib.h" \ No newline at end of file +#include "MAVLinkLib.h" diff --git a/test/AnalyzeView/CMakeLists.txt b/test/AnalyzeView/CMakeLists.txt index 072d813ffe5d..99983281b002 100644 --- a/test/AnalyzeView/CMakeLists.txt +++ b/test/AnalyzeView/CMakeLists.txt @@ -7,8 +7,20 @@ add_subdirectory(GeoTag) target_sources(${CMAKE_PROJECT_NAME} PRIVATE - LogDownloadTest.cc - LogDownloadTest.h + MAVLinkChartControllerTest.cc + MAVLinkChartControllerTest.h + MAVLinkConsoleControllerTest.cc + MAVLinkConsoleControllerTest.h + MAVLinkInspectorControllerTest.cc + MAVLinkInspectorControllerTest.h + OnboardLogDownloadTest.cc + OnboardLogDownloadTest.h + MAVLinkMessageFieldTest.cc + MAVLinkMessageFieldTest.h + MAVLinkMessageTest.cc + MAVLinkMessageTest.h + MAVLinkSystemTest.cc + MAVLinkSystemTest.h MavlinkLogTest.cc MavlinkLogTest.h ) diff --git a/test/AnalyzeView/MAVLinkChartControllerTest.cc b/test/AnalyzeView/MAVLinkChartControllerTest.cc new file mode 100644 index 000000000000..85952b729853 --- /dev/null +++ b/test/AnalyzeView/MAVLinkChartControllerTest.cc @@ -0,0 +1,105 @@ +#include "MAVLinkChartControllerTest.h" + +#include + +#include "MAVLinkChartController.h" +#include "MAVLinkInspectorController.h" + +void MAVLinkChartControllerTest::_constructionTest() +{ + MAVLinkChartController chart; + + QVERIFY(chart.inspectorController() == nullptr); + QCOMPARE(chart.chartIndex(), 0); +} + +void MAVLinkChartControllerTest::_defaultRangeValuesTest() +{ + MAVLinkChartController chart; + + QCOMPARE(chart.rangeXIndex(), static_cast(0)); + QCOMPARE(chart.rangeYIndex(), static_cast(0)); + QCOMPARE_FUZZY(chart.rangeYMin(), 0.0, 1e-9); + QCOMPARE_FUZZY(chart.rangeYMax(), 1.0, 1e-9); +} + +void MAVLinkChartControllerTest::_chartFieldsInitiallyEmptyTest() +{ + MAVLinkChartController chart; + + QVERIFY(chart.chartFields().isEmpty()); +} + +void MAVLinkChartControllerTest::_setRangeXIndexTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy xIndexSpy(&chart, &MAVLinkChartController::rangeXIndexChanged); + + // Move from default index 0 to index 1 + chart.setRangeXIndex(1); + QCOMPARE(chart.rangeXIndex(), static_cast(1)); + QCOMPARE(xIndexSpy.count(), 1); +} + +void MAVLinkChartControllerTest::_setRangeXIndexSameValueNoSignalTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy xIndexSpy(&chart, &MAVLinkChartController::rangeXIndexChanged); + + // Setting the same index (already 0) must not emit + chart.setRangeXIndex(0); + QCOMPARE(xIndexSpy.count(), 0); +} + +void MAVLinkChartControllerTest::_setRangeYIndexTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy yIndexSpy(&chart, &MAVLinkChartController::rangeYIndexChanged); + + // Index 1 corresponds to the first non-Auto range (10,000) + chart.setRangeYIndex(1); + QCOMPARE(chart.rangeYIndex(), static_cast(1)); + QCOMPARE(yIndexSpy.count(), 1); + + // Non-Auto range: rangeYMin and rangeYMax must be set symmetrically + const qreal expectedRange = controller.rangeSt()[1]->range; + QCOMPARE_FUZZY(chart.rangeYMin(), -expectedRange, 1e-9); + QCOMPARE_FUZZY(chart.rangeYMax(), expectedRange, 1e-9); +} + +void MAVLinkChartControllerTest::_setRangeYIndexSameValueNoSignalTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + chart.setInspectorController(&controller); + + QSignalSpy yIndexSpy(&chart, &MAVLinkChartController::rangeYIndexChanged); + + // Default is already 0; setting to 0 again must not emit + chart.setRangeYIndex(0); + QCOMPARE(yIndexSpy.count(), 0); +} + +void MAVLinkChartControllerTest::_setInspectorControllerTest() +{ + MAVLinkInspectorController controller; + MAVLinkChartController chart; + + chart.setInspectorController(&controller); + QCOMPARE(chart.inspectorController(), &controller); + + // Setting the same controller again must be a no-op (no crash, same pointer) + chart.setInspectorController(&controller); + QCOMPARE(chart.inspectorController(), &controller); +} + +UT_REGISTER_TEST(MAVLinkChartControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkChartControllerTest.h b/test/AnalyzeView/MAVLinkChartControllerTest.h new file mode 100644 index 000000000000..2b6c77372111 --- /dev/null +++ b/test/AnalyzeView/MAVLinkChartControllerTest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkChartControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _defaultRangeValuesTest(); + void _chartFieldsInitiallyEmptyTest(); + void _setRangeXIndexTest(); + void _setRangeXIndexSameValueNoSignalTest(); + void _setRangeYIndexTest(); + void _setRangeYIndexSameValueNoSignalTest(); + void _setInspectorControllerTest(); +}; diff --git a/test/AnalyzeView/MAVLinkConsoleControllerTest.cc b/test/AnalyzeView/MAVLinkConsoleControllerTest.cc new file mode 100644 index 000000000000..b1cf464f2345 --- /dev/null +++ b/test/AnalyzeView/MAVLinkConsoleControllerTest.cc @@ -0,0 +1,226 @@ +#include "MAVLinkConsoleControllerTest.h" + +#include +#include +#include + +#include "MAVLinkConsoleController.h" + +// --------------------------------------------------------------------------- +// Construction / initial state +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_constructionTest() +{ + // Should construct and destruct without crashing even with no active vehicle. + MAVLinkConsoleController controller; + QVERIFY(true); +} + +void MAVLinkConsoleControllerTest::_initialStateTest() +{ + MAVLinkConsoleController controller; + + // Model must start empty. + QCOMPARE(controller.rowCount(), 0); + + // text property must return an empty string when there are no rows. + const QString text = controller.property("text").toString(); + QCOMPARE(text, QString()); +} + +// --------------------------------------------------------------------------- +// CommandHistory – navigation on empty history +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpOnEmptyTest() +{ + MAVLinkConsoleController controller; + + // With no history, historyUp must echo back whatever was passed in. + const QString current = QStringLiteral("my input"); + QCOMPARE(controller.historyUp(current), current); +} + +void MAVLinkConsoleControllerTest::_historyDownOnEmptyTest() +{ + MAVLinkConsoleController controller; + + // With no history, historyDown must echo back whatever was passed in. + const QString current = QStringLiteral("my input"); + QCOMPARE(controller.historyDown(current), current); +} + +// --------------------------------------------------------------------------- +// CommandHistory – single entry +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpSingleEntryTest() +{ + MAVLinkConsoleController controller; + + // sendCommand appends non-empty lines to history even without a vehicle. + controller.sendCommand(QStringLiteral("ls")); + + // First up should yield the recorded command. + QCOMPARE(controller.historyUp(QStringLiteral("")), QStringLiteral("ls")); + + // A second up at the top boundary must return the same current string. + QCOMPARE(controller.historyUp(QStringLiteral("ls")), QStringLiteral("ls")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – multi-entry up/down round-trip +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpDownNavigationTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("cmd1")); + controller.sendCommand(QStringLiteral("cmd2")); + controller.sendCommand(QStringLiteral("cmd3")); + + // Navigate backwards: cmd3 -> cmd2 -> cmd1 + const QString r1 = controller.historyUp(QStringLiteral("")); + QCOMPARE(r1, QStringLiteral("cmd3")); + + const QString r2 = controller.historyUp(r1); + QCOMPARE(r2, QStringLiteral("cmd2")); + + const QString r3 = controller.historyUp(r2); + QCOMPARE(r3, QStringLiteral("cmd1")); + + // Navigate forward: cmd2 -> cmd3 -> empty (past end) + const QString f1 = controller.historyDown(r3); + QCOMPARE(f1, QStringLiteral("cmd2")); + + const QString f2 = controller.historyDown(f1); + QCOMPARE(f2, QStringLiteral("cmd3")); + + const QString f3 = controller.historyDown(f2); + QCOMPARE(f3, QStringLiteral("")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – duplicate suppression +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyNoDuplicatesTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("ping")); + controller.sendCommand(QStringLiteral("ping")); // duplicate – must not be appended + + // A single up should reach "ping". + const QString r1 = controller.historyUp(QStringLiteral("")); + QCOMPARE(r1, QStringLiteral("ping")); + + // Another up at the boundary must return the same string (only one entry). + const QString r2 = controller.historyUp(r1); + QCOMPARE(r2, QStringLiteral("ping")); +} + +// --------------------------------------------------------------------------- +// CommandHistory – boundary: already at top (index 0) +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyUpAtTopBoundaryTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("alpha")); + + // Move to top. + controller.historyUp(QStringLiteral("")); + + // Another up must return current unchanged (boundary protection). + const QString current = QStringLiteral("alpha"); + QCOMPARE(controller.historyUp(current), current); +} + +// --------------------------------------------------------------------------- +// CommandHistory – boundary: already at bottom (index == history.length()) +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_historyDownAtBottomBoundaryTest() +{ + MAVLinkConsoleController controller; + + controller.sendCommand(QStringLiteral("beta")); + + // At the bottom (fresh after sendCommand), down must echo current. + const QString current = QStringLiteral("beta"); + QCOMPARE(controller.historyDown(current), current); +} + +// --------------------------------------------------------------------------- +// handleClipboard – no newline in combined string +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardNoNewlineTest() +{ + MAVLinkConsoleController controller; + + // Clear clipboard so the result is purely from command_pre. + QGuiApplication::clipboard()->clear(); + + const QString prefix = QStringLiteral("prefix"); + const QString result = controller.handleClipboard(prefix); + + // No newline present, so the whole string is returned as the pending line. + QCOMPARE(result, prefix); +} + +// --------------------------------------------------------------------------- +// handleClipboard – newline at the end of clipboard text +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardWithNewlineTest() +{ + MAVLinkConsoleController controller; + + // Place a newline-terminated string in the clipboard. + QGuiApplication::clipboard()->setText(QStringLiteral("clipped\n")); + + // With an empty prefix the clipboard text has a trailing newline. + // The part before the newline is sent as a command; the remainder + // (empty string after the newline) is returned. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("")); +} + +// --------------------------------------------------------------------------- +// handleClipboard – empty prefix, clipboard with no newline +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardEmptyPrefixTest() +{ + MAVLinkConsoleController controller; + + QGuiApplication::clipboard()->setText(QStringLiteral("data")); + + // No newline -> entire combined string returned, nothing sent. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("data")); +} + +// --------------------------------------------------------------------------- +// handleClipboard – multiple newlines, last line returned +// --------------------------------------------------------------------------- + +void MAVLinkConsoleControllerTest::_handleClipboardMultilineTest() +{ + MAVLinkConsoleController controller; + + // Three lines: "a\nb\nc" — last line has no trailing newline. + QGuiApplication::clipboard()->setText(QStringLiteral("a\nb\nc")); + + // Combined string is "a\nb\nc". Last newline is between "b" and "c". + // "a\nb" is sent; "c" is returned. + const QString result = controller.handleClipboard(QStringLiteral("")); + QCOMPARE(result, QStringLiteral("c")); +} + +UT_REGISTER_TEST(MAVLinkConsoleControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkConsoleControllerTest.h b/test/AnalyzeView/MAVLinkConsoleControllerTest.h new file mode 100644 index 000000000000..4b1335ce5e91 --- /dev/null +++ b/test/AnalyzeView/MAVLinkConsoleControllerTest.h @@ -0,0 +1,25 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkConsoleController; + +class MAVLinkConsoleControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initialStateTest(); + void _historyUpOnEmptyTest(); + void _historyDownOnEmptyTest(); + void _historyUpSingleEntryTest(); + void _historyUpDownNavigationTest(); + void _historyNoDuplicatesTest(); + void _historyUpAtTopBoundaryTest(); + void _historyDownAtBottomBoundaryTest(); + void _handleClipboardNoNewlineTest(); + void _handleClipboardWithNewlineTest(); + void _handleClipboardEmptyPrefixTest(); + void _handleClipboardMultilineTest(); +}; diff --git a/test/AnalyzeView/MAVLinkInspectorControllerTest.cc b/test/AnalyzeView/MAVLinkInspectorControllerTest.cc new file mode 100644 index 000000000000..38de1393b605 --- /dev/null +++ b/test/AnalyzeView/MAVLinkInspectorControllerTest.cc @@ -0,0 +1,61 @@ +#include "MAVLinkInspectorControllerTest.h" + +#include "MAVLinkInspectorController.h" +#include "QmlObjectListModel.h" + +void MAVLinkInspectorControllerTest::_constructionTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(controller.systems() != nullptr); + QVERIFY(controller.activeSystem() == nullptr); +} + +void MAVLinkInspectorControllerTest::_timeScalesNonEmptyTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(!controller.timeScales().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_rangeListNonEmptyTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(!controller.rangeList().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_systemNamesInitiallyEmptyTest() +{ + MAVLinkInspectorController controller; + + // No vehicles connected in unit test context + QVERIFY(controller.systemNames().isEmpty()); +} + +void MAVLinkInspectorControllerTest::_activeSystemInitiallyNullTest() +{ + MAVLinkInspectorController controller; + + QVERIFY(controller.activeSystem() == nullptr); +} + +void MAVLinkInspectorControllerTest::_timeScalesCountTest() +{ + MAVLinkInspectorController controller; + + // 4 time scales: 5s, 10s, 30s, 60s + QCOMPARE(controller.timeScales().count(), 4); + QCOMPARE(controller.timeScaleSt().count(), 4); +} + +void MAVLinkInspectorControllerTest::_rangeListCountTest() +{ + MAVLinkInspectorController controller; + + // 10 range entries: Auto + 9 fixed ranges + QCOMPARE(controller.rangeList().count(), 10); + QCOMPARE(controller.rangeSt().count(), 10); +} + +UT_REGISTER_TEST(MAVLinkInspectorControllerTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkInspectorControllerTest.h b/test/AnalyzeView/MAVLinkInspectorControllerTest.h new file mode 100644 index 000000000000..b519d64e9316 --- /dev/null +++ b/test/AnalyzeView/MAVLinkInspectorControllerTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkInspectorControllerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _timeScalesNonEmptyTest(); + void _rangeListNonEmptyTest(); + void _systemNamesInitiallyEmptyTest(); + void _activeSystemInitiallyNullTest(); + void _timeScalesCountTest(); + void _rangeListCountTest(); +}; diff --git a/test/AnalyzeView/MAVLinkMessageFieldTest.cc b/test/AnalyzeView/MAVLinkMessageFieldTest.cc new file mode 100644 index 000000000000..d95fbb6bf28a --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageFieldTest.cc @@ -0,0 +1,127 @@ +#include "MAVLinkMessageFieldTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "MAVLinkMessageField.h" + +namespace { + +QGCMAVLinkMessage *makeHeartbeatMsg(QObject *parent = nullptr) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan(1, 1, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return new QGCMAVLinkMessage(msg, parent); +} + +} // namespace + +void MAVLinkMessageFieldTest::_constructionTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + // Pass nullptr as parent to avoid double-free (stack field + parent delete) + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QCOMPARE(field.name(), QStringLiteral("type")); + QCOMPARE(field.type(), QStringLiteral("uint8_t")); + + delete msg; +} + +void MAVLinkMessageFieldTest::_initialValueEmptyTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QVERIFY(field.value().isEmpty()); + + delete msg; +} + +void MAVLinkMessageFieldTest::_selectableDefaultsTrueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QVERIFY(field.selectable()); + + delete msg; +} + +void MAVLinkMessageFieldTest::_setSelectableTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QSignalSpy selectableSpy(&field, &QGCMAVLinkMessageField::selectableChanged); + + field.setSelectable(false); + QVERIFY(!field.selectable()); + QCOMPARE(selectableSpy.count(), 1); + + // Same value must not emit + field.setSelectable(false); + QCOMPARE(selectableSpy.count(), 1); + + field.setSelectable(true); + QVERIFY(field.selectable()); + QCOMPARE(selectableSpy.count(), 2); + + delete msg; +} + +void MAVLinkMessageFieldTest::_updateValueChangesValueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + QSignalSpy valueSpy(&field, &QGCMAVLinkMessageField::valueChanged); + + field.updateValue(QStringLiteral("2"), 2.0); + QCOMPARE(field.value(), QStringLiteral("2")); + QCOMPARE(valueSpy.count(), 1); + + delete msg; +} + +void MAVLinkMessageFieldTest::_updateValueNoopOnSameValueTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("type"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + field.updateValue(QStringLiteral("5"), 5.0); + + QSignalSpy valueSpy(&field, &QGCMAVLinkMessageField::valueChanged); + + // Calling with the same string value must not emit valueChanged + field.updateValue(QStringLiteral("5"), 5.0); + QCOMPARE(valueSpy.count(), 0); + + delete msg; +} + +void MAVLinkMessageFieldTest::_labelFormatTest() +{ + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + QGCMAVLinkMessageField field(QStringLiteral("autopilot"), QStringLiteral("uint8_t"), msg); + field.setParent(nullptr); + + // label() must follow the pattern ": " + const QString expected = QStringLiteral("HEARTBEAT: autopilot"); + QCOMPARE(field.label(), expected); + + delete msg; +} + +UT_REGISTER_TEST(MAVLinkMessageFieldTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkMessageFieldTest.h b/test/AnalyzeView/MAVLinkMessageFieldTest.h new file mode 100644 index 000000000000..ba21c7885a9d --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageFieldTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkMessageFieldTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initialValueEmptyTest(); + void _selectableDefaultsTrueTest(); + void _setSelectableTest(); + void _updateValueChangesValueTest(); + void _updateValueNoopOnSameValueTest(); + void _labelFormatTest(); +}; diff --git a/test/AnalyzeView/MAVLinkMessageTest.cc b/test/AnalyzeView/MAVLinkMessageTest.cc new file mode 100644 index 000000000000..41689a9cd9cd --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageTest.cc @@ -0,0 +1,128 @@ +#include "MAVLinkMessageTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "QmlObjectListModel.h" + +namespace { + +mavlink_message_t makeHeartbeat(uint8_t sysId = 1, uint8_t compId = 1) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan( + sysId, compId, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return msg; +} + +} // namespace + +void MAVLinkMessageTest::_constructionTest() +{ + const mavlink_message_t msg = makeHeartbeat(2, 3); + QGCMAVLinkMessage message(msg); + + QCOMPARE(message.id(), static_cast(MAVLINK_MSG_ID_HEARTBEAT)); + QCOMPARE(message.sysId(), static_cast(2)); + QCOMPARE(message.compId(), static_cast(3)); + QCOMPARE(message.name(), QStringLiteral("HEARTBEAT")); +} + +void MAVLinkMessageTest::_countStartsAtOneTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QCOMPARE(message.count(), static_cast(1)); +} + +void MAVLinkMessageTest::_updateIncrementsCountTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy countSpy(&message, &QGCMAVLinkMessage::countChanged); + + message.update(msg); + QCOMPARE(message.count(), static_cast(2)); + QCOMPARE(countSpy.count(), 1); + + message.update(msg); + QCOMPARE(message.count(), static_cast(3)); + QCOMPARE(countSpy.count(), 2); +} + +void MAVLinkMessageTest::_setSelectedTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy selectedSpy(&message, &QGCMAVLinkMessage::selectedChanged); + + QVERIFY(!message.selected()); + + message.setSelected(true); + QVERIFY(message.selected()); + QCOMPARE(selectedSpy.count(), 1); + + // Setting to the same value must not emit again + message.setSelected(true); + QCOMPARE(selectedSpy.count(), 1); + + message.setSelected(false); + QVERIFY(!message.selected()); + QCOMPARE(selectedSpy.count(), 2); +} + +void MAVLinkMessageTest::_fieldsPopulatedTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + const QmlObjectListModel *fields = message.fields(); + QVERIFY(fields != nullptr); + + // HEARTBEAT has 6 fields: type, autopilot, base_mode, custom_mode, system_status, mavlink_version + const mavlink_message_info_t *info = mavlink_get_message_info(&msg); + QVERIFY(info != nullptr); + QCOMPARE(fields->count(), static_cast(info->num_fields)); +} + +void MAVLinkMessageTest::_setTargetRateHzTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + QSignalSpy rateSpy(&message, &QGCMAVLinkMessage::targetRateHzChanged); + + QCOMPARE(message.targetRateHz(), static_cast(0)); + + message.setTargetRateHz(10); + QCOMPARE(message.targetRateHz(), static_cast(10)); + QCOMPARE(rateSpy.count(), 1); + + // Same value must not emit + message.setTargetRateHz(10); + QCOMPARE(rateSpy.count(), 1); +} + +void MAVLinkMessageTest::_updateFreqTest() +{ + const mavlink_message_t msg = makeHeartbeat(); + QGCMAVLinkMessage message(msg); + + // Initial frequency is zero + QCOMPARE_FUZZY(message.actualRateHz(), 0.0, 1e-9); + + // Simulate one update since last freq refresh + message.update(msg); + message.updateFreq(); + + // _count started at 1, update() made it 2, _lastCount was 0 + // msgCount = 2 - 0 = 2; EWMA = 0.2*0 + 0.8*2 = 1.6 + QCOMPARE_FUZZY(message.actualRateHz(), 1.6, 1e-9); +} + +UT_REGISTER_TEST(MAVLinkMessageTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkMessageTest.h b/test/AnalyzeView/MAVLinkMessageTest.h new file mode 100644 index 000000000000..caa223c8fcca --- /dev/null +++ b/test/AnalyzeView/MAVLinkMessageTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkMessageTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _countStartsAtOneTest(); + void _updateIncrementsCountTest(); + void _setSelectedTest(); + void _fieldsPopulatedTest(); + void _setTargetRateHzTest(); + void _updateFreqTest(); +}; diff --git a/test/AnalyzeView/MAVLinkSystemTest.cc b/test/AnalyzeView/MAVLinkSystemTest.cc new file mode 100644 index 000000000000..c76f1f0f1923 --- /dev/null +++ b/test/AnalyzeView/MAVLinkSystemTest.cc @@ -0,0 +1,168 @@ +#include "MAVLinkSystemTest.h" + +#include + +#include "MAVLinkLib.h" +#include "MAVLinkMessage.h" +#include "MAVLinkSystem.h" +#include "QmlObjectListModel.h" + +namespace { + +QGCMAVLinkMessage *makeHeartbeatMsg(uint8_t sysId = 1, uint8_t compId = 1, QObject *parent = nullptr) +{ + mavlink_message_t msg{}; + mavlink_msg_heartbeat_pack_chan(sysId, compId, MAVLINK_COMM_0, &msg, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + return new QGCMAVLinkMessage(msg, parent); +} + +} // namespace + +void MAVLinkSystemTest::_constructionTest() +{ + QGCMAVLinkSystem system(7); + + QCOMPARE(system.id(), static_cast(7)); +} + +void MAVLinkSystemTest::_initiallyEmptyMessagesTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.messages() != nullptr); + QCOMPARE(system.messages()->count(), 0); +} + +void MAVLinkSystemTest::_initiallyNoCompIDsTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.compIDs().isEmpty()); + QVERIFY(system.compIDsStr().isEmpty()); +} + +void MAVLinkSystemTest::_appendAndFindMessageTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + QCOMPARE(system.messages()->count(), 1); + + QGCMAVLinkMessage *found = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 1); + QVERIFY(found != nullptr); + QCOMPARE(found->id(), static_cast(MAVLINK_MSG_ID_HEARTBEAT)); + QCOMPARE(found->compId(), static_cast(1)); +} + +void MAVLinkSystemTest::_findMessageNotFoundTest() +{ + QGCMAVLinkSystem system(1); + + // findMessage on an empty system returns nullptr + QGCMAVLinkMessage *found = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 1); + QVERIFY(found == nullptr); + + // findMessage with a wrong compId also returns nullptr + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + QGCMAVLinkMessage *wrongComp = system.findMessage(MAVLINK_MSG_ID_HEARTBEAT, 99); + QVERIFY(wrongComp == nullptr); +} + +void MAVLinkSystemTest::_findMessageByPointerTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 1); + system.append(msg); + + const int idx = system.findMessage(msg); + QCOMPARE(idx, 0); + + // A pointer that was never appended returns -1 + QGCMAVLinkMessage fakeMsg(mavlink_message_t{}); + QCOMPARE(system.findMessage(&fakeMsg), -1); +} + +void MAVLinkSystemTest::_setSelectedAndSelectedMsgTest() +{ + QGCMAVLinkSystem system(1); + + // Two messages with different msg IDs so they sort differently + mavlink_message_t raw1{}; + mavlink_msg_heartbeat_pack_chan(1, 1, MAVLINK_COMM_0, &raw1, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + auto *msg1 = new QGCMAVLinkMessage(raw1); + + // Use a second heartbeat with a different compId so they coexist + mavlink_message_t raw2{}; + mavlink_msg_heartbeat_pack_chan(1, 2, MAVLINK_COMM_0, &raw2, + MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_PX4, 0, 0, MAV_STATE_ACTIVE); + auto *msg2 = new QGCMAVLinkMessage(raw2); + + system.append(msg1); + system.append(msg2); + + QSignalSpy selectedSpy(&system, &QGCMAVLinkSystem::selectedChanged); + + system.setSelected(0); + QVERIFY(selectedSpy.count() >= 1); + + QGCMAVLinkMessage *sel = system.selectedMsg(); + QVERIFY(sel != nullptr); + // The selected message must be the one at index 0 of the sorted list + QCOMPARE(system.findMessage(sel), 0); +} + +void MAVLinkSystemTest::_setSelectedOutOfBoundsTest() +{ + QGCMAVLinkSystem system(1); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(); + system.append(msg); + + QSignalSpy selectedSpy(&system, &QGCMAVLinkSystem::selectedChanged); + + // Index equal to count is out of bounds - must be silently ignored + system.setSelected(1); + QCOMPARE(selectedSpy.count(), 0); +} + +void MAVLinkSystemTest::_appendUpdatesCompIDsTest() +{ + QGCMAVLinkSystem system(1); + + QSignalSpy compSpy(&system, &QGCMAVLinkSystem::compIDsChanged); + + QGCMAVLinkMessage *msg = makeHeartbeatMsg(1, 42); + system.append(msg); + + QCOMPARE(compSpy.count(), 1); + QVERIFY(system.compIDs().contains(42)); + QVERIFY(!system.compIDsStr().isEmpty()); + + // Appending another message with the same compId must not emit again + QGCMAVLinkMessage *msg2 = makeHeartbeatMsg(1, 42); + // Give it a different msgid so it is a distinct entry - use a raw msg + mavlink_message_t raw{}; + mavlink_msg_heartbeat_pack_chan(1, 42, MAVLINK_COMM_1, &raw, + MAV_TYPE_FIXED_WING, MAV_AUTOPILOT_GENERIC, 0, 0, MAV_STATE_ACTIVE); + auto *msg3 = new QGCMAVLinkMessage(raw); + delete msg2; // not used + system.append(msg3); + + QCOMPARE(compSpy.count(), 1); +} + +void MAVLinkSystemTest::_selectedMsgOnEmptySystemTest() +{ + QGCMAVLinkSystem system(1); + + QVERIFY(system.selectedMsg() == nullptr); +} + +UT_REGISTER_TEST(MAVLinkSystemTest, TestLabel::Unit, TestLabel::AnalyzeView) diff --git a/test/AnalyzeView/MAVLinkSystemTest.h b/test/AnalyzeView/MAVLinkSystemTest.h new file mode 100644 index 000000000000..f4808f42da78 --- /dev/null +++ b/test/AnalyzeView/MAVLinkSystemTest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "UnitTest.h" + +class MAVLinkSystemTest : public UnitTest +{ + Q_OBJECT + +private slots: + void _constructionTest(); + void _initiallyEmptyMessagesTest(); + void _initiallyNoCompIDsTest(); + void _appendAndFindMessageTest(); + void _findMessageNotFoundTest(); + void _findMessageByPointerTest(); + void _setSelectedAndSelectedMsgTest(); + void _setSelectedOutOfBoundsTest(); + void _appendUpdatesCompIDsTest(); + void _selectedMsgOnEmptySystemTest(); +}; diff --git a/test/AnalyzeView/LogDownloadTest.cc b/test/AnalyzeView/OnboardLogDownloadTest.cc similarity index 80% rename from test/AnalyzeView/LogDownloadTest.cc rename to test/AnalyzeView/OnboardLogDownloadTest.cc index 4606f601534c..e77fe73ee3e6 100644 --- a/test/AnalyzeView/LogDownloadTest.cc +++ b/test/AnalyzeView/OnboardLogDownloadTest.cc @@ -1,18 +1,18 @@ -#include "LogDownloadTest.h" +#include "OnboardLogDownloadTest.h" #include -#include "LogDownloadController.h" -#include "LogEntry.h" +#include "OnboardLogController.h" +#include "OnboardLogEntry.h" #include "MAVLinkProtocol.h" #include "MultiSignalSpy.h" #include "MultiVehicleManager.h" #include "QmlObjectListModel.h" -void LogDownloadTest::_downloadTest() +void OnboardLogDownloadTest::_downloadTest() { // VehicleTest::init() already connects the mock link - LogDownloadController* const controller = new LogDownloadController(this); + OnboardLogController* const controller = new OnboardLogController(this); MultiSignalSpy* multiSpyLogDownloadController = new MultiSignalSpy(this); QVERIFY(multiSpyLogDownloadController->init(controller)); controller->refresh(); @@ -25,7 +25,7 @@ void LogDownloadTest::_downloadTest() multiSpyLogDownloadController->clearAllSignals(); QmlObjectListModel* const model = controller->_getModel(); QVERIFY(model); - model->value(0)->setSelected(true); + model->value(0)->setSelected(true); const QString downloadTo = QDir::currentPath(); controller->download(downloadTo); QVERIFY(multiSpyLogDownloadController->waitForSignal("downloadingLogsChanged", TestTimeout::longMs())); @@ -40,4 +40,4 @@ void LogDownloadTest::_downloadTest() (void)QFile::remove(downloadFile); } -UT_REGISTER_TEST(LogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) +UT_REGISTER_TEST(OnboardLogDownloadTest, TestLabel::Integration, TestLabel::AnalyzeView, TestLabel::Vehicle) diff --git a/test/AnalyzeView/LogDownloadTest.h b/test/AnalyzeView/OnboardLogDownloadTest.h similarity index 69% rename from test/AnalyzeView/LogDownloadTest.h rename to test/AnalyzeView/OnboardLogDownloadTest.h index 75a25edfb2e5..b0cf94849f6a 100644 --- a/test/AnalyzeView/LogDownloadTest.h +++ b/test/AnalyzeView/OnboardLogDownloadTest.h @@ -2,7 +2,7 @@ #include "BaseClasses/VehicleTest.h" -class LogDownloadTest : public VehicleTest +class OnboardLogDownloadTest : public VehicleTest { Q_OBJECT diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e4ec6e545b33..cc492e2aed87 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,7 +73,13 @@ add_qgc_test(GeoTagControllerTest LABELS Unit AnalyzeView RESOURCE_LOCK TempFile add_qgc_test(GeoTagDataTest LABELS Unit AnalyzeView) add_qgc_test(GeoTagImageModelTest LABELS Unit AnalyzeView) add_qgc_test(ULogParserTest LABELS Unit AnalyzeView RESOURCE_LOCK TempFiles) -add_qgc_test(LogDownloadTest LABELS Integration Vehicle RESOURCE_LOCK MockLink) +add_qgc_test(MAVLinkChartControllerTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkConsoleControllerTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkInspectorControllerTest LABELS Unit AnalyzeView) +add_qgc_test(OnboardLogDownloadTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) +add_qgc_test(MAVLinkMessageFieldTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkMessageTest LABELS Unit AnalyzeView) +add_qgc_test(MAVLinkSystemTest LABELS Unit AnalyzeView) add_qgc_test(MavlinkLogTest LABELS Integration AnalyzeView Vehicle RESOURCE_LOCK MockLink) # ---------------------------------------------------------------------------- @@ -85,6 +91,12 @@ add_qgc_test(VehicleCameraControlTest LABELS Integration Vehicle RESOURCE_LOCK M add_qgc_test(QGCVideoStreamInfoTest LABELS Unit Camera) add_qgc_test(VideoManagerTest LABELS Unit) +# ---------------------------------------------------------------------------- +# VideoManager +# ---------------------------------------------------------------------------- +add_subdirectory(VideoManager) +add_qgc_test(GStreamerTest LABELS Integration) + # ---------------------------------------------------------------------------- # Comms # ---------------------------------------------------------------------------- @@ -105,6 +117,7 @@ add_qgc_test(FactSystemTestGeneric LABELS Integration Vehicle RESOURCE_LOCK Mock add_qgc_test(FactSystemTestPX4 LABELS Integration Vehicle RESOURCE_LOCK MockLink) add_qgc_test(FactTest LABELS Unit) add_qgc_test(FactValueSliderListModelTest LABELS Unit) +add_qgc_test(HashCheckTest LABELS Integration Vehicle SERIAL TIMEOUT ${QGC_TEST_TIMEOUT_EXTENDED}) add_qgc_test(ParameterManagerTest LABELS Integration Vehicle SERIAL TIMEOUT ${QGC_TEST_TIMEOUT_EXTENDED}) # ---------------------------------------------------------------------------- @@ -117,7 +130,10 @@ add_qgc_test(FollowMeTest LABELS Integration Vehicle RESOURCE_LOCK MockLink) # GPS # ---------------------------------------------------------------------------- add_subdirectory(GPS) -add_qgc_test(GpsTest LABELS Unit GPS) +add_qgc_test(RTCMParserTest LABELS Unit) +add_qgc_test(NTRIPManagerTest LABELS Unit) +add_qgc_test(NTRIPHttpTransportTest LABELS Unit) +add_qgc_test(NTRIPSourceTableTest LABELS Unit) # ---------------------------------------------------------------------------- # Joystick @@ -133,6 +149,7 @@ add_qgc_test(SDLTest LABELS Unit Joystick RESOURCE_LOCK Joystick) # ---------------------------------------------------------------------------- add_subdirectory(MAVLink) add_qgc_test(SigningTest LABELS Unit MAVLink) +add_qgc_test(MockLinkSigningTest LABELS Integration Vehicle MAVLink RESOURCE_LOCK MockLink) add_qgc_test(StatusTextHandlerTest LABELS Unit MAVLink) add_qgc_test(SysStatusSensorInfoTest LABELS Unit MAVLink) @@ -149,6 +166,7 @@ add_qgc_test(MissionCommandTreeEditorTest LABELS Unit MissionManager TIMEOUT ${Q add_qgc_test(MissionCommandTreeTest LABELS Unit MissionManager) add_qgc_test(MissionControllerManagerTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) add_qgc_test(MissionControllerTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) +add_qgc_test(MissionControllerTreeTest LABELS Integration MissionManager RESOURCE_LOCK MockLink) add_qgc_test(MissionItemTest LABELS Unit MissionManager) add_qgc_test(MissionManagerTest LABELS Integration MissionManager SERIAL) add_qgc_test(MissionSettingsTest LABELS Unit MissionManager) @@ -179,7 +197,10 @@ add_qgc_test(UrlFactoryTest LABELS Unit) # QmlControls # ---------------------------------------------------------------------------- add_subdirectory(QmlControls) +add_qgc_test(ObjectItemModelBaseTest LABELS Unit) +add_qgc_test(ObjectListModelBaseTest LABELS Unit) add_qgc_test(QmlObjectListModelTest LABELS Unit) +add_qgc_test(QmlObjectTreeModelTest LABELS Unit) # ---------------------------------------------------------------------------- # Terrain diff --git a/test/Camera/VehicleCameraControlTest.cc b/test/Camera/VehicleCameraControlTest.cc index a12f42aa8758..722a3bbba7d6 100644 --- a/test/Camera/VehicleCameraControlTest.cc +++ b/test/Camera/VehicleCameraControlTest.cc @@ -4,7 +4,7 @@ #include #include "LinkManager.h" -#include "MavlinkCameraControl.h" +#include "MavlinkCameraControlInterface.h" #include "MockConfiguration.h" #include "MockLink.h" #include "MultiVehicleManager.h" @@ -158,9 +158,9 @@ void VehicleCameraControlTest::_testCameraCapFlags() // Find Camera 1 (MAV_COMP_ID_CAMERA) which has our configured flags. // Camera 2 (MAV_COMP_ID_CAMERA2) is always photo-only and not what we're testing. - MavlinkCameraControl* camera = nullptr; + MavlinkCameraControlInterface* camera = nullptr; for (int i = 0; i < cameraManager->cameras()->count(); i++) { - auto* cam = qobject_cast(cameraManager->cameras()->get(i)); + auto* cam = qobject_cast(cameraManager->cameras()->get(i)); if (cam && cam->compID() == MAV_COMP_ID_CAMERA) { camera = cam; break; diff --git a/test/Comms/Bluetooth/BluetoothConfigurationTest.cc b/test/Comms/Bluetooth/BluetoothConfigurationTest.cc index a46af70ba263..6904b9b5ce69 100644 --- a/test/Comms/Bluetooth/BluetoothConfigurationTest.cc +++ b/test/Comms/Bluetooth/BluetoothConfigurationTest.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -101,6 +102,9 @@ void BluetoothConfigurationTest::_testBleUuidGetSet() void BluetoothConfigurationTest::_testBluetoothAvailabilityConsistency() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + QCOMPARE(BluetoothConfiguration::isBluetoothAvailable(), LinkManager::isBluetoothAvailable()); } diff --git a/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc b/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc index 3a0335e00279..17cd35b6b340 100644 --- a/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc +++ b/test/Comms/Bluetooth/BluetoothLiveAdapterTest.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -45,6 +46,9 @@ static QString _firstAdapterAddressOrEmpty(const QVariantList &adapters) void BluetoothLiveAdapterTest::_testLiveAdapterSelectionAndState() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + const QList localHosts = _localAdaptersOrSkip(); if (localHosts.isEmpty()) { QSKIP("No local Bluetooth adapter detected on this host"); diff --git a/test/Comms/Bluetooth/BluetoothWorkerTest.cc b/test/Comms/Bluetooth/BluetoothWorkerTest.cc index e5d8c8f6dd1f..22126b4324e3 100644 --- a/test/Comms/Bluetooth/BluetoothWorkerTest.cc +++ b/test/Comms/Bluetooth/BluetoothWorkerTest.cc @@ -6,6 +6,7 @@ #include "BluetoothWorker.h" #include +#include #include #include @@ -22,6 +23,9 @@ static QTimer *_findServiceDiscoveryTimer(const BluetoothWorker *worker) void BluetoothWorkerTest::_testFactoryCreatesClassicWorker() { + // Qt's Bluetooth module may emit an uncategorized warning when BlueZ is not running (e.g. on headless CI). + expectLogMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Cannot find a compatible running Bluez"))); + BluetoothConfiguration config("TestBT_factoryClassic"); config.setMode(BluetoothConfiguration::BluetoothMode::ModeClassic); diff --git a/test/FactSystem/CMakeLists.txt b/test/FactSystem/CMakeLists.txt index fc01dc8e5931..f98af7a00e6d 100644 --- a/test/FactSystem/CMakeLists.txt +++ b/test/FactSystem/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(${CMAKE_PROJECT_NAME} FactTest.h FactValueSliderListModelTest.cc FactValueSliderListModelTest.h + HashCheckTest.cc + HashCheckTest.h ParameterManagerTest.cc ParameterManagerTest.h ) diff --git a/test/FactSystem/HashCheckTest.cc b/test/FactSystem/HashCheckTest.cc new file mode 100644 index 000000000000..fbce21b7765a --- /dev/null +++ b/test/FactSystem/HashCheckTest.cc @@ -0,0 +1,267 @@ +#include "HashCheckTest.h" + +#include +#include +#include +#include + +#include "LinkManager.h" +#include "MAVLinkLib.h" +#include "MockConfiguration.h" +#include "MultiVehicleManager.h" +#include "ParameterManager.h" +#include "Vehicle.h" + +void HashCheckTest::cleanup() +{ + if (_mockLink && MultiVehicleManager::instance()->activeVehicle()) { + QSignalSpy spy(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged); + _mockLink->disconnect(); + _mockLink = nullptr; + (void) UnitTest::waitForSignal(spy, TestTimeout::mediumMs(), QStringLiteral("activeVehicleChanged")); + } + VehicleTestManualConnect::cleanup(); +} + +void HashCheckTest::_deleteCacheFiles() +{ + const QDir cacheDir = ParameterManager::parameterCacheDir(); + if (cacheDir.exists()) { + const QStringList cacheFiles = cacheDir.entryList(QStringList() << QStringLiteral("*.v2"), QDir::Files); + for (const QString &file : cacheFiles) { + QFile::remove(cacheDir.filePath(file)); + } + } +} + +void HashCheckTest::_connectAndWaitForParams() +{ + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyParamsReady, TestTimeout::longMs()); + + const QList arguments = spyParamsReady.takeFirst(); + QCOMPARE(arguments.count(), 1); + QCOMPARE(arguments.at(0).toBool(), true); +} + +void HashCheckTest::_disconnectAndSettle() +{ + _mockLink->disconnect(); + _mockLink = nullptr; + QSignalSpy spyDisconnect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged); + QVERIFY(UnitTest::waitForSignal(spyDisconnect, TestTimeout::longMs(), QStringLiteral("activeVehicleChanged"))); + UnitTest::settleEventLoopForCleanup(); +} + +MockLink *HashCheckTest::_startPX4MockLinkNoIncrement(MockConfiguration::FailureMode_t failureMode) +{ + auto *const mockConfig = new MockConfiguration(QStringLiteral("PX4 MockLink")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + mockConfig->setIncrementVehicleId(false); + mockConfig->setFailureMode(failureMode); + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr config = LinkManager::instance()->addConfiguration(mockConfig); + if (LinkManager::instance()->createConnectedLink(config)) { + return qobject_cast(config->link()); + } + return nullptr; +} + +MockLink *HashCheckTest::_startPX4MockLinkHighLatency() +{ + auto *const mockConfig = new MockConfiguration(QStringLiteral("PX4 HighLatency MockLink")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + mockConfig->setHighLatency(true); + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr config = LinkManager::instance()->addConfiguration(mockConfig); + if (LinkManager::instance()->createConnectedLink(config)) { + return qobject_cast(config->link()); + } + return nullptr; +} + +// Data-driven test matrix for all _HASH_CHECK parameter cache scenarios. +// +// FirstConnect_NoCache: First PX4 connection with no cache. Sends _HASH_CHECK, gets full param list. +// Reconnect_CacheHit: Reconnect after populating cache. Hash matches so no PARAM_REQUEST_LIST needed. +// Reconnect_CacheMiss: Reconnect after a parameter changed. Hash mismatch triggers full param reload. +// HashTimeout_CacheHit: Vehicle never responds to _HASH_CHECK. Falls back to PARAM_REQUEST_LIST, cache still valid. +// HashTimeout_NoCache: Vehicle never responds to _HASH_CHECK and no cache exists. Falls back to full param list. +// HashTimeout_CacheStale: Vehicle never responds to _HASH_CHECK and cache is stale. Falls back to full param list. +// BothTimersExhaust: Vehicle responds to neither _HASH_CHECK nor PARAM_REQUEST_LIST. Params never become ready. +// CacheDeleted_Between: Cache populated then deleted before reconnect. Forces full param reload despite same vehicle. +// ManualRefresh: User-triggered refreshAllParameters() bypasses _HASH_CHECK and requests full param list. +// ArduPilot: ArduPilot uses FTP for parameters, so no _HASH_CHECK or PARAM_REQUEST_LIST traffic. +// HighLatency: High-latency links skip parameter download entirely; params marked as missing. +// LogReplay: Log replay shares the high-latency code path; params marked as missing. +// +// PARAM_REQUEST_LIST (xPRL) is only asserted for PX4 vehicles; ArduPilot uses FTP. +// missingParameters (xMiss) is only asserted when expectParametersReady (xReady) is true. +// Scenarios 11 and 12 exercise the same code path via setHighLatency(true). +// MockLink doesn't support isLogReplay(), so high latency serves as proxy for the +// shared guard: if (isHighLatency || _logReplay) { signal ready immediately }. + +void HashCheckTest::_hashCheckMatrix_data() +{ + // MockLink configuration flags + QTest::addColumn("px4"); + QTest::addColumn("highLatency"); + QTest::addColumn("populateCache"); + QTest::addColumn("changeParam"); + QTest::addColumn("deleteCache"); + QTest::addColumn("hashCheckNoResponse"); + QTest::addColumn("failNoResponse"); + QTest::addColumn("manualRefresh"); + + // Expected outcomes + QTest::addColumn("xHashCheck"); + QTest::addColumn("xPRL"); + QTest::addColumn("xReady"); + QTest::addColumn("xMissing"); + + // px4 hl popC chgP delC noResp failNR manRef xHC xPRL xReady xMiss + QTest::newRow("FirstConnect_NoCache") << true << false << false << false << false << false << false << false << true << true << true << false; + QTest::newRow("Reconnect_CacheHit") << true << false << true << false << false << false << false << false << true << false << true << false; + QTest::newRow("Reconnect_CacheMiss") << true << false << true << true << false << false << false << false << true << true << true << false; + QTest::newRow("HashTimeout_CacheHit") << true << false << true << false << false << true << false << false << true << true << true << false; + QTest::newRow("HashTimeout_NoCache") << true << false << false << false << false << true << false << false << true << true << true << false; + QTest::newRow("HashTimeout_CacheStale") << true << false << true << true << false << true << false << false << true << true << true << false; + QTest::newRow("BothTimersExhaust") << true << false << false << false << false << true << true << false << true << true << false << false; + QTest::newRow("CacheDeleted_Between") << true << false << true << false << true << false << false << false << true << true << true << false; + QTest::newRow("ManualRefresh") << true << false << false << false << false << false << false << true << false << true << true << false; + QTest::newRow("ArduPilot") << false << false << false << false << false << false << false << false << false << false << true << false; + QTest::newRow("HighLatency") << true << true << false << false << false << false << false << false << false << false << true << true; + QTest::newRow("LogReplay") << true << true << false << false << false << false << false << false << false << false << true << true; +} + +void HashCheckTest::_hashCheckMatrix() +{ + QFETCH(bool, px4); + QFETCH(bool, highLatency); + QFETCH(bool, populateCache); + QFETCH(bool, changeParam); + QFETCH(bool, deleteCache); + QFETCH(bool, hashCheckNoResponse); + QFETCH(bool, failNoResponse); + QFETCH(bool, manualRefresh); + QFETCH(bool, xHashCheck); + QFETCH(bool, xPRL); + QFETCH(bool, xReady); + QFETCH(bool, xMissing); + + const MAV_AUTOPILOT firmwareType = px4 ? MAV_AUTOPILOT_PX4 : MAV_AUTOPILOT_ARDUPILOTMEGA; + const auto failMode = failNoResponse ? MockConfiguration::FailParamNoResponseToRequestList + : MockConfiguration::FailNone; + + // Phase 1: Clean slate + _deleteCacheFiles(); + + // Phase 2: Optional preconnect to populate the parameter cache + if (populateCache) { + _mockLink = _startPX4MockLinkNoIncrement(); + _connectAndWaitForParams(); + _disconnectAndSettle(); + + if (deleteCache) { + _deleteCacheFiles(); + } + } + + // Phase 3: Create the test link and connect + if (manualRefresh) { + _connectMockLink(firmwareType); + QVERIFY(_vehicle); + QVERIFY(_vehicle->parameterManager()->parametersReady()); + + _mockLink->clearReceivedMavlinkMessageCounts(); + _vehicle->parameterManager()->refreshAllParameters(); + QTest::qWait(100); + + } else if (highLatency) { + _mockLink = _startPX4MockLinkHighLatency(); + + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyParamsReady, TestTimeout::longMs()); + + } else if (!px4) { + _connectMockLink(firmwareType); + QVERIFY(_vehicle); + + } else if (xReady) { + // PX4 normal path — params will become ready + _mockLink = _startPX4MockLinkNoIncrement(failMode); + if (changeParam) { + _mockLink->setMockParamValue(MAV_COMP_ID_AUTOPILOT1, QStringLiteral("BAT1_V_CHARGED"), 99.0f); + } + if (hashCheckNoResponse) { + _mockLink->setHashCheckNoResponse(true); + } + _connectAndWaitForParams(); + + } else { + // PX4 path where params never become ready (both timers exhaust) + _mockLink = _startPX4MockLinkNoIncrement(failMode); + if (hashCheckNoResponse) { + _mockLink->setHashCheckNoResponse(true); + } + + MultiVehicleManager *const vehicleMgr = MultiVehicleManager::instance(); + QVERIFY(vehicleMgr); + + QSignalSpy spyVehicle(vehicleMgr, &MultiVehicleManager::activeVehicleAvailableChanged); + QVERIFY_SIGNAL_WAIT(spyVehicle, TestTimeout::mediumMs()); + + Vehicle *const vehicle = vehicleMgr->activeVehicle(); + QVERIFY(vehicle); + + QSignalSpy spyParamsReady(vehicleMgr, &MultiVehicleManager::parameterReadyVehicleAvailableChanged); + const int maxWaitMs = ParameterManager::kHashCheckTimeoutMs + + ParameterManager::kTestMaxInitialRequestTimeMs + + TestTimeout::shortMs(); + QVERIFY_NO_SIGNAL_WAIT(spyParamsReady, maxWaitMs); + } + + // Phase 4: Verify outcomes + QCOMPARE(_mockLink->hashCheckRequestCount() > 0, xHashCheck); + + if (xReady) { + Vehicle *const vehicle = MultiVehicleManager::instance()->activeVehicle(); + QVERIFY(vehicle); + QVERIFY(vehicle->parameterManager()->parametersReady()); + QCOMPARE(vehicle->parameterManager()->missingParameters(), xMissing); + } + + // PARAM_REQUEST_LIST is only checked for PX4 (ArduPilot uses FTP) + if (px4) { + QCOMPARE(_mockLink->receivedMavlinkMessageCount(MAVLINK_MSG_ID_PARAM_REQUEST_LIST) > 0, xPRL); + } + + // Phase 5: Cleanup for tests that used _connectMockLink + if (manualRefresh || !px4) { + _disconnectMockLink(); + } +} + +UT_REGISTER_TEST(HashCheckTest, TestLabel::Integration, TestLabel::Vehicle, TestLabel::Serial) diff --git a/test/FactSystem/HashCheckTest.h b/test/FactSystem/HashCheckTest.h new file mode 100644 index 000000000000..6f89f22beb98 --- /dev/null +++ b/test/FactSystem/HashCheckTest.h @@ -0,0 +1,23 @@ +#pragma once + +#include "BaseClasses/VehicleTestManualConnect.h" + +/// Data-driven test matrix for the _HASH_CHECK parameter cache optimization. +/// See _hashCheckMatrix_data() for the full scenario table. +class HashCheckTest : public VehicleTestManualConnect +{ + Q_OBJECT + +private slots: + void cleanup() override; + + void _hashCheckMatrix_data(); + void _hashCheckMatrix(); + +private: + void _deleteCacheFiles(); + void _connectAndWaitForParams(); + void _disconnectAndSettle(); + MockLink *_startPX4MockLinkNoIncrement(MockConfiguration::FailureMode_t failureMode = MockConfiguration::FailNone); + MockLink *_startPX4MockLinkHighLatency(); +}; diff --git a/test/GPS/CMakeLists.txt b/test/GPS/CMakeLists.txt index 19b3fb5d9416..b2d5c51d74e2 100644 --- a/test/GPS/CMakeLists.txt +++ b/test/GPS/CMakeLists.txt @@ -5,8 +5,14 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE - GpsTest.cc - GpsTest.h + NTRIPManagerTest.cc + NTRIPManagerTest.h + NTRIPSourceTableTest.cc + NTRIPSourceTableTest.h + NTRIPHttpTransportTest.cc + NTRIPHttpTransportTest.h + RTCMParserTest.cc + RTCMParserTest.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/GPS/NTRIPHttpTransportTest.cc b/test/GPS/NTRIPHttpTransportTest.cc new file mode 100644 index 000000000000..80c926d1908c --- /dev/null +++ b/test/GPS/NTRIPHttpTransportTest.cc @@ -0,0 +1,303 @@ +#include "NTRIPHttpTransportTest.h" +#include "NTRIPHttpTransport.h" +#include "RTCMParser.h" + +namespace { + +static QByteArray buildRtcmFrame(uint16_t messageId, int extraPayloadBytes = 0) +{ + const int payloadLen = 2 + extraPayloadBytes; + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast((payloadLen >> 8) & 0x03)); + frame.append(static_cast(payloadLen & 0xFF)); + frame.append(static_cast((messageId >> 4) & 0xFF)); + frame.append(static_cast((messageId & 0x0F) << 4)); + for (int i = 0; i < extraPayloadBytes; i++) { + frame.append(static_cast(i & 0xFF)); + } + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + return frame; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Whitelist Parsing +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testWhitelistEmpty() +{ + NTRIPHttpTransport t(NTRIPTransportConfig{}); + QVERIFY(t._whitelist.isEmpty()); + + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral(""); + NTRIPHttpTransport t2(cfg); + QVERIFY(t2._whitelist.isEmpty()); +} + +void NTRIPHttpTransportTest::testWhitelistSingle() +{ + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 1); + QCOMPARE(t._whitelist[0], 1005); +} + +void NTRIPHttpTransportTest::testWhitelistMultiple() +{ + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005,1077,1087"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 3); + QVERIFY(t._whitelist.contains(1005)); + QVERIFY(t._whitelist.contains(1077)); + QVERIFY(t._whitelist.contains(1087)); +} + +void NTRIPHttpTransportTest::testWhitelistInvalidEntries() +{ + // "abc" and "" should be skipped (toInt returns 0) + NTRIPTransportConfig cfg; + cfg.whitelist = QStringLiteral("1005,abc,,1077"); + NTRIPHttpTransport t(cfg); + QCOMPARE(t._whitelist.size(), 2); + QVERIFY(t._whitelist.contains(1005)); + QVERIFY(t._whitelist.contains(1077)); +} + +// --------------------------------------------------------------------------- +// HTTP Status Line Parsing +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testParseHttpStatus200() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 200 OK"); + QVERIFY(status.valid); + QCOMPARE(status.code, 200); + QCOMPARE(status.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusICY() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("ICY 200 OK"); + QVERIFY(status.valid); + QCOMPARE(status.code, 200); + QCOMPARE(status.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus401() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.0 401 Unauthorized"); + QVERIFY(status.valid); + QCOMPARE(status.code, 401); + QCOMPARE(status.reason, QStringLiteral("Unauthorized")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus404() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 404 Not Found"); + QVERIFY(status.valid); + QCOMPARE(status.code, 404); + QCOMPARE(status.reason, QStringLiteral("Not Found")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusInvalid() +{ + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("").valid); + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("garbage data").valid); + QVERIFY(!NTRIPHttpTransport::parseHttpStatusLine("200 OK").valid); + + const auto sourceTable = NTRIPHttpTransport::parseHttpStatusLine("SOURCETABLE 200 OK"); + QVERIFY(sourceTable.valid); + QCOMPARE(sourceTable.code, 200); + QCOMPARE(sourceTable.reason, QStringLiteral("OK")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus201() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 201 Created"); + QVERIFY(status.valid); + QCOMPARE(status.code, 201); + QCOMPARE(status.reason, QStringLiteral("Created")); +} + +void NTRIPHttpTransportTest::testParseHttpStatus500() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.0 500 Internal Server Error"); + QVERIFY(status.valid); + QCOMPARE(status.code, 500); + QCOMPARE(status.reason, QStringLiteral("Internal Server Error")); +} + +void NTRIPHttpTransportTest::testParseHttpStatusNoReason() +{ + const auto status = NTRIPHttpTransport::parseHttpStatusLine("HTTP/1.1 400"); + QVERIFY(status.valid); + QCOMPARE(status.code, 400); + QVERIFY(status.reason.isEmpty()); +} + +// --------------------------------------------------------------------------- +// NMEA Checksum Repair +// --------------------------------------------------------------------------- + +static bool hasValidNmeaChecksum(const QByteArray& sentence) +{ + int star = sentence.lastIndexOf('*'); + if (star < 2 || star + 3 > sentence.size()) { + return false; + } + + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(sentence.at(i)); + } + + QByteArray expected = QByteArray::number(calc, 16).rightJustified(2, '0').toUpper(); + QByteArray actual = sentence.mid(star + 1, 2).toUpper(); + return actual == expected; +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumCorrect() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,*64"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); + QVERIFY(result.startsWith("$GPGGA,")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumWrong() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,*FF"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); + QVERIFY(!result.contains("*FF")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumMissing() +{ + const QByteArray input = "$GPGGA,120000,4723.8620,N,00832.7360,E,1,12,1.0,100.0,M,0.0,M,,"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.contains("*")); + QVERIFY(result.endsWith("\r\n")); + QVERIFY(hasValidNmeaChecksum(result.trimmed())); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumTruncated() +{ + const QByteArray input = "$GPGGA,120000,0000.0000,N,00000.0000,E,1,12,1.0,0.0,M,0.0,M,,*"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + + QVERIFY(result.endsWith("\r\n")); + const QByteArray trimmed = result.trimmed(); + int star = trimmed.lastIndexOf('*'); + QVERIFY(star > 0); + QVERIFY(star + 3 <= trimmed.size()); + QVERIFY(hasValidNmeaChecksum(trimmed)); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumAppendsCrLf() +{ + const QByteArray input = "$GPGGA,000000,0000.0000,N,00000.0000,E,1,12,1.0,0.0,M,0.0,M,,*00"; + QVERIFY(!input.endsWith("\r\n")); + + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + QVERIFY(result.endsWith("\r\n")); + + const QByteArray input2 = input + "\r\n"; + const QByteArray result2 = NTRIPHttpTransport::repairNmeaChecksum(input2); + QVERIFY(result2.endsWith("\r\n")); + QVERIFY(!result2.endsWith("\r\n\r\n")); +} + +void NTRIPHttpTransportTest::testRepairNmeaChecksumShortSentence() +{ + const QByteArray input = "$GP"; + const QByteArray result = NTRIPHttpTransport::repairNmeaChecksum(input); + QVERIFY(result.endsWith("\r\n")); + + const QByteArray empty = ""; + const QByteArray resultEmpty = NTRIPHttpTransport::repairNmeaChecksum(empty); + QVERIFY(resultEmpty.endsWith("\r\n")); +} + +// --------------------------------------------------------------------------- +// RTCM Filtering +// --------------------------------------------------------------------------- + +void NTRIPHttpTransportTest::testFilterNoWhitelist() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + NTRIPHttpTransport t(cfg); + + QVector received; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray& msg) { + received.append(msg); + }); + + QByteArray stream = buildRtcmFrame(1005, 4) + buildRtcmFrame(1077, 8) + buildRtcmFrame(1087, 2); + t._parseRtcm(stream); + + QCOMPARE(received.size(), 3); +} + +void NTRIPHttpTransportTest::testFilterWithWhitelist() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + cfg.whitelist = QStringLiteral("1005,1087"); + NTRIPHttpTransport t(cfg); + + QVector receivedIds; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray& msg) { + if (msg.size() >= 5) { + uint16_t id = (static_cast(msg[3]) << 4) | (static_cast(msg[4]) >> 4); + receivedIds.append(id); + } + }); + + QByteArray stream = buildRtcmFrame(1005, 4) + buildRtcmFrame(1077, 8) + buildRtcmFrame(1087, 2); + t._parseRtcm(stream); + + QCOMPARE(receivedIds.size(), 2); + QVERIFY(receivedIds.contains(1005)); + QVERIFY(receivedIds.contains(1087)); + QVERIFY(!receivedIds.contains(1077)); +} + +void NTRIPHttpTransportTest::testFilterRejectsBadCrc() +{ + NTRIPTransportConfig cfg; + cfg.mountpoint = QStringLiteral("TEST"); + NTRIPHttpTransport t(cfg); + + int count = 0; + connect(&t, &NTRIPHttpTransport::RTCMDataUpdate, this, [&](const QByteArray&) { + count++; + }); + + QByteArray bad = buildRtcmFrame(1005, 4); + bad[bad.size() - 1] = static_cast(bad[bad.size() - 1] ^ 0xFF); + + QByteArray good = buildRtcmFrame(1077, 2); + + t._parseRtcm(bad + good); + + QCOMPARE(count, 1); +} + +UT_REGISTER_TEST(NTRIPHttpTransportTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPHttpTransportTest.h b/test/GPS/NTRIPHttpTransportTest.h new file mode 100644 index 000000000000..09ffdb59240f --- /dev/null +++ b/test/GPS/NTRIPHttpTransportTest.h @@ -0,0 +1,38 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPHttpTransportTest : public UnitTest +{ + Q_OBJECT + +private slots: + // HTTP status line parsing + void testParseHttpStatus200(); + void testParseHttpStatusICY(); + void testParseHttpStatus401(); + void testParseHttpStatus404(); + void testParseHttpStatusInvalid(); + void testParseHttpStatus201(); + void testParseHttpStatus500(); + void testParseHttpStatusNoReason(); + + // Whitelist parsing + void testWhitelistEmpty(); + void testWhitelistSingle(); + void testWhitelistMultiple(); + void testWhitelistInvalidEntries(); + + // RTCM filtering + void testFilterNoWhitelist(); + void testFilterWithWhitelist(); + void testFilterRejectsBadCrc(); + + // NMEA checksum repair + void testRepairNmeaChecksumCorrect(); + void testRepairNmeaChecksumWrong(); + void testRepairNmeaChecksumMissing(); + void testRepairNmeaChecksumTruncated(); + void testRepairNmeaChecksumAppendsCrLf(); + void testRepairNmeaChecksumShortSentence(); +}; diff --git a/test/GPS/NTRIPManagerTest.cc b/test/GPS/NTRIPManagerTest.cc new file mode 100644 index 000000000000..d66ea0a71927 --- /dev/null +++ b/test/GPS/NTRIPManagerTest.cc @@ -0,0 +1,216 @@ +#include "NTRIPManagerTest.h" +#include "NTRIPManager.h" + +#include + +static bool verifyNmeaChecksum(const QByteArray& sentence) +{ + if (sentence.size() < 6 || sentence.at(0) != '$') { + return false; + } + + int star = sentence.lastIndexOf('*'); + if (star < 2 || star + 3 > sentence.size()) { + return false; + } + + quint8 calc = 0; + for (int i = 1; i < star; ++i) { + calc ^= static_cast(sentence.at(i)); + } + + QByteArray expected = QByteArray::number(calc, 16).rightJustified(2, '0').toUpper(); + QByteArray actual = sentence.mid(star + 1, 2).toUpper(); + return actual == expected; +} + +static int countFields(const QByteArray& gga) +{ + // Count comma-separated fields between $ and * + int star = gga.lastIndexOf('*'); + if (star < 0) star = gga.size(); + QByteArray body = gga.mid(1, star - 1); // strip $ and *XX + return body.count(',') + 1; +} + +// --------------------------------------------------------------------------- +// GGA Format and Structure +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGAFormat() +{ + QGeoCoordinate coord(47.3977, 8.5456); + QByteArray gga = NTRIPManager::makeGGA(coord, 408.0); + + QVERIFY(gga.startsWith("$GPGGA,")); + QVERIFY(gga.contains("*")); + QVERIFY(!gga.contains("\r\n")); + + QVERIFY(gga.contains(",N,")); + QVERIFY(gga.contains(",E,")); + + QVERIFY(gga.contains(",408.0,M,")); +} + +void NTRIPManagerTest::testMakeGGAFieldCount() +{ + // GGA has 15 fields: $GPGGA,hhmmss,lat,N,lon,E,qual,nsat,hdop,alt,M,geoid,M,age,refid + QGeoCoordinate coord(40.0, -74.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 10.0); + + // GPGGA sentence: the body (between $ and *) has 15 comma-separated fields + QCOMPARE(countFields(gga), 15); +} + +void NTRIPManagerTest::testMakeGGAChecksum() +{ + QGeoCoordinate coord(37.7749, -122.4194); + QByteArray gga = NTRIPManager::makeGGA(coord, 16.0); + + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Hemisphere Encoding +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGANorthEast() +{ + QGeoCoordinate coord(51.5074, 0.1278); + QByteArray gga = NTRIPManager::makeGGA(coord, 11.0); + + QVERIFY(gga.contains(",N,")); + QVERIFY(gga.contains(",E,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGASouthWest() +{ + QGeoCoordinate coord(-33.8688, -151.2093); + QByteArray gga = NTRIPManager::makeGGA(coord, 58.0); + + QVERIFY(gga.contains(",S,")); + QVERIFY(gga.contains(",W,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGAEquator() +{ + // Latitude exactly 0 should be N (>= 0.0) + QGeoCoordinate coord(0.0, 10.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 0.0); + + QVERIFY(gga.contains(",N,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGADateLine() +{ + // Longitude near 180 (Fiji) + QGeoCoordinate coordEast(17.7134, 178.065); + QByteArray ggaEast = NTRIPManager::makeGGA(coordEast, 5.0); + QVERIFY(ggaEast.contains(",E,")); + QVERIFY(verifyNmeaChecksum(ggaEast)); + + // Longitude near -180 (across the date line) + QGeoCoordinate coordWest(17.7134, -179.5); + QByteArray ggaWest = NTRIPManager::makeGGA(coordWest, 5.0); + QVERIFY(ggaWest.contains(",W,")); + QVERIFY(verifyNmeaChecksum(ggaWest)); +} + +// --------------------------------------------------------------------------- +// GGA Altitude +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGAZeroAltitude() +{ + QGeoCoordinate coord(0.0001, 0.0001); + QByteArray gga = NTRIPManager::makeGGA(coord, 0.0); + + QVERIFY(gga.contains(",0.0,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGAHighAltitude() +{ + QGeoCoordinate coord(27.9881, 86.9250); + QByteArray gga = NTRIPManager::makeGGA(coord, 8848.9); + + QVERIFY(gga.contains(",8848.9,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +void NTRIPManagerTest::testMakeGGANegativeAltitude() +{ + // Dead Sea: ~-430m + QGeoCoordinate coord(31.5, 35.5); + QByteArray gga = NTRIPManager::makeGGA(coord, -430.5); + + QVERIFY(gga.contains(",-430.5,M,")); + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Coordinate Precision +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGADMMPrecision() +{ + // 47.3977 degrees = 47 degrees, 23.8620 minutes + // 8.5456 degrees = 008 degrees, 32.7360 minutes + QGeoCoordinate coord(47.3977, 8.5456); + QByteArray gga = NTRIPManager::makeGGA(coord, 100.0); + + // Extract lat field: after "GPGGA,HHMMSS," the next field is lat + QString ggaStr = QString::fromUtf8(gga); + QStringList fields = ggaStr.mid(1, ggaStr.indexOf('*') - 1).split(','); + // fields[0] = GPGGA, [1] = time, [2] = lat, [3] = N/S, [4] = lon, [5] = E/W + QVERIFY(fields.size() >= 6); + + // Latitude: 4723.xxxx + QString lat = fields[2]; + QVERIFY2(lat.startsWith("4723"), qPrintable(QString("Lat field: %1").arg(lat))); + QCOMPARE(fields[3], QStringLiteral("N")); + + // Longitude: 00832.xxxx + QString lon = fields[4]; + QVERIFY2(lon.startsWith("00832"), qPrintable(QString("Lon field: %1").arg(lon))); + QCOMPARE(fields[5], QStringLiteral("E")); + + QVERIFY(verifyNmeaChecksum(gga)); +} + +// --------------------------------------------------------------------------- +// GGA Time Field +// --------------------------------------------------------------------------- + +void NTRIPManagerTest::testMakeGGATimeFormat() +{ + QGeoCoordinate coord(40.0, -74.0); + QByteArray gga = NTRIPManager::makeGGA(coord, 10.0); + + // Extract time field (field index 1, after $GPGGA,) + QString ggaStr = QString::fromUtf8(gga); + QStringList fields = ggaStr.mid(1, ggaStr.indexOf('*') - 1).split(','); + QVERIFY(fields.size() >= 2); + + const QString timeField = fields[1]; + // Should be HHMMSS.SS format (8 or more chars: 6 digits + dot + decimals) + QVERIFY2(timeField.size() >= 6, qPrintable(QString("Time field too short: %1").arg(timeField))); + // First 6 chars should be digits + for (int i = 0; i < 6; ++i) { + QVERIFY2(timeField[i].isDigit(), qPrintable(QString("Non-digit at pos %1: %2").arg(i).arg(timeField))); + } + // HH should be 00-23 + int hh = timeField.left(2).toInt(); + QVERIFY2(hh >= 0 && hh <= 23, qPrintable(QString("Invalid hour: %1").arg(hh))); + // MM should be 00-59 + int mm = timeField.mid(2, 2).toInt(); + QVERIFY2(mm >= 0 && mm <= 59, qPrintable(QString("Invalid minute: %1").arg(mm))); + // SS should be 00-59 + int ss = timeField.mid(4, 2).toInt(); + QVERIFY2(ss >= 0 && ss <= 59, qPrintable(QString("Invalid second: %1").arg(ss))); +} + +UT_REGISTER_TEST(NTRIPManagerTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPManagerTest.h b/test/GPS/NTRIPManagerTest.h new file mode 100644 index 000000000000..1edbc356f361 --- /dev/null +++ b/test/GPS/NTRIPManagerTest.h @@ -0,0 +1,31 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPManagerTest : public UnitTest +{ + Q_OBJECT + +private slots: + // GGA format and structure + void testMakeGGAFormat(); + void testMakeGGAFieldCount(); + void testMakeGGAChecksum(); + + // GGA hemisphere encoding + void testMakeGGANorthEast(); + void testMakeGGASouthWest(); + void testMakeGGAEquator(); + void testMakeGGADateLine(); + + // GGA altitude + void testMakeGGAZeroAltitude(); + void testMakeGGAHighAltitude(); + void testMakeGGANegativeAltitude(); + + // GGA coordinate precision + void testMakeGGADMMPrecision(); + + // GGA time field + void testMakeGGATimeFormat(); +}; diff --git a/test/GPS/NTRIPSourceTableTest.cc b/test/GPS/NTRIPSourceTableTest.cc new file mode 100644 index 000000000000..11031379a7d5 --- /dev/null +++ b/test/GPS/NTRIPSourceTableTest.cc @@ -0,0 +1,118 @@ +#include "NTRIPSourceTableTest.h" +#include "NTRIPSourceTable.h" +#include "QmlObjectListModel.h" + +#include + +void NTRIPSourceTableTest::testParseSTRLine() +{ + const QString line = "STR;MOUNT01;Mount Identifier;RTCM 3.2;1005(1),1074(1),1084(1),1094(1);2;GPS+GLO+GAL;NET01;USA;40.0000;-74.0000;0;1;QGC Test;none;B;N;4800;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp != nullptr); + + QCOMPARE(mp->mountpoint(), QStringLiteral("MOUNT01")); + QCOMPARE(mp->identifier(), QStringLiteral("Mount Identifier")); + QCOMPARE(mp->format(), QStringLiteral("RTCM 3.2")); + QCOMPARE(mp->carrier(), 2); + QCOMPARE(mp->navSystem(), QStringLiteral("GPS+GLO+GAL")); + QCOMPARE(mp->network(), QStringLiteral("NET01")); + QCOMPARE(mp->country(), QStringLiteral("USA")); + QVERIFY(qFuzzyCompare(mp->latitude(), 40.0)); + QVERIFY(qFuzzyCompare(mp->longitude(), -74.0)); + QVERIFY(!mp->nmea()); + QVERIFY(mp->solution()); + QCOMPARE(mp->generator(), QStringLiteral("QGC Test")); + QCOMPARE(mp->authentication(), QStringLiteral("B")); + QVERIFY(!mp->fee()); + QCOMPARE(mp->bitrate(), 4800); + + delete mp; +} + +void NTRIPSourceTableTest::testParseShortLine() +{ + const QString line = "STR;SHORT;Too;Few;Fields"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp == nullptr); +} + +void NTRIPSourceTableTest::testParseNonSTRLine() +{ + const QString line = "CAS;caster.example.com;2101;NTRIP Caster;Operator;0;USA;0.0;0.0;0.0;0.0;0;0;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp == nullptr); +} + +void NTRIPSourceTableTest::testParseFullTable() +{ + const QString table = + "SOURCETABLE 200 OK\r\n" + "STR;MP1;Id1;RTCM 3.2;details;2;GPS;NET;USA;40.0;-74.0;0;1;gen;none;B;N;4800;misc\r\n" + "STR;MP2;Id2;RTCM 3.3;details;0;GLO;NET;DEU;52.0;13.0;1;0;gen;none;N;Y;9600;misc\r\n" + "CAS;caster.example.com;2101;desc;op;0;USA;0;0;0;0;0;0;misc\r\n" + "ENDSOURCETABLE\r\n"; + + NTRIPSourceTableModel model; + model.parseSourceTable(table); + + QCOMPARE(model.count(), 2); +} + +void NTRIPSourceTableTest::testDistanceCalculation() +{ + const QString line = "STR;NEAR;Id;RTCM 3.2;details;2;GPS;NET;USA;40.7128;-74.0060;0;1;gen;none;B;N;4800;misc"; + auto* mp = NTRIPMountpointModel::fromSourceTableLine(line); + QVERIFY(mp != nullptr); + + QVERIFY(mp->distanceKm() < 0.0); + + QGeoCoordinate userPos(40.7128, -74.0060); + mp->updateDistance(userPos); + QVERIFY(mp->distanceKm() >= 0.0); + QVERIFY(mp->distanceKm() < 1.0); + + QGeoCoordinate farPos(51.5074, -0.1278); + mp->updateDistance(farPos); + QVERIFY(mp->distanceKm() > 5000.0); + + delete mp; +} + +void NTRIPSourceTableTest::testUpdateDistancesAll() +{ + const QString table = + "STR;NYC;Id1;RTCM 3.2;details;2;GPS;NET;USA;40.7128;-74.0060;0;1;gen;none;B;N;4800;misc\r\n" + "STR;LON;Id2;RTCM 3.3;details;0;GPS;NET;GBR;51.5074;-0.1278;1;0;gen;none;N;Y;9600;misc\r\n" + "ENDSOURCETABLE\r\n"; + + NTRIPSourceTableModel model; + model.parseSourceTable(table); + QCOMPARE(model.count(), 2); + + // Before update, distances should be negative (unset) + auto* mp0 = qobject_cast(model.mountpoints()->get(0)); + auto* mp1 = qobject_cast(model.mountpoints()->get(1)); + QVERIFY(mp0->distanceKm() < 0.0); + QVERIFY(mp1->distanceKm() < 0.0); + + // Update from NYC position + QGeoCoordinate nycPos(40.7128, -74.0060); + model.updateDistances(nycPos); + + // NYC mount should be very close, London should be far + QVERIFY(mp0->distanceKm() >= 0.0); + QVERIFY(mp0->distanceKm() < 1.0); + QVERIFY(mp1->distanceKm() > 5000.0); +} + +void NTRIPSourceTableTest::testEmptyTable() +{ + NTRIPSourceTableModel model; + model.parseSourceTable(""); + QCOMPARE(model.count(), 0); + + model.parseSourceTable("ENDSOURCETABLE\r\n"); + QCOMPARE(model.count(), 0); +} + +UT_REGISTER_TEST(NTRIPSourceTableTest, TestLabel::Unit) diff --git a/test/GPS/NTRIPSourceTableTest.h b/test/GPS/NTRIPSourceTableTest.h new file mode 100644 index 000000000000..110552539ddc --- /dev/null +++ b/test/GPS/NTRIPSourceTableTest.h @@ -0,0 +1,17 @@ +#pragma once + +#include "UnitTest.h" + +class NTRIPSourceTableTest : public UnitTest +{ + Q_OBJECT + +private slots: + void testParseSTRLine(); + void testParseShortLine(); + void testParseNonSTRLine(); + void testParseFullTable(); + void testDistanceCalculation(); + void testUpdateDistancesAll(); + void testEmptyTable(); +}; diff --git a/test/GPS/RTCMParserTest.cc b/test/GPS/RTCMParserTest.cc new file mode 100644 index 000000000000..7d2d8b259524 --- /dev/null +++ b/test/GPS/RTCMParserTest.cc @@ -0,0 +1,351 @@ +#include "RTCMParserTest.h" +#include "RTCMParser.h" + +static QByteArray buildRtcmFrame(uint16_t messageId, int extraPayloadBytes = 0) +{ + const int payloadLen = 2 + extraPayloadBytes; + QByteArray frame; + + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast((payloadLen >> 8) & 0x03)); + frame.append(static_cast(payloadLen & 0xFF)); + + frame.append(static_cast((messageId >> 4) & 0xFF)); + frame.append(static_cast((messageId & 0x0F) << 4)); + + for (int i = 0; i < extraPayloadBytes; i++) { + frame.append(static_cast(i & 0xFF)); + } + + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + + return frame; +} + +// --------------------------------------------------------------------------- +// CRC-24Q Tests +// --------------------------------------------------------------------------- + +void RTCMParserTest::testCrc24qEmpty() +{ + const uint32_t crc = RTCMParser::crc24q(nullptr, 0); + QCOMPARE(crc, 0u); +} + +void RTCMParserTest::testCrc24qSingleByte() +{ + // CRC-24Q of 0x00 with initial value 0 is 0 (no bits set to trigger polynomial) + const uint8_t zero = 0x00; + QCOMPARE(RTCMParser::crc24q(&zero, 1), 0u); + + // CRC-24Q of a non-zero byte should produce a non-zero 24-bit result + const uint8_t nonzero = 0x01; + const uint32_t crc = RTCMParser::crc24q(&nonzero, 1); + QVERIFY(crc != 0u); + QCOMPARE(crc & 0xFF000000u, 0u); +} + +void RTCMParserTest::testCrc24qKnownVector() +{ + const uint8_t data[] = { 0xD3, 0x00, 0x02, 0x3E, 0xD0 }; + const uint32_t crc1 = RTCMParser::crc24q(data, sizeof(data)); + const uint32_t crc2 = RTCMParser::crc24q(data, sizeof(data)); + QCOMPARE(crc1, crc2); + QVERIFY((crc1 & 0xFF000000u) == 0u); +} + +void RTCMParserTest::testCrc24qReferenceVector() +{ + // Standard CRC-24Q check value: "123456789" -> 0xCDE703 + const uint8_t data[] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39 }; + const uint32_t crc = RTCMParser::crc24q(data, sizeof(data)); + QCOMPARE(crc, static_cast(0xCDE703)); +} + +void RTCMParserTest::testCrc24qIncremental() +{ + QByteArray frame = buildRtcmFrame(1005, 10); + + // header(3) + payload(12) + crc(3) = 18 + QCOMPARE(frame.size(), 18); + + const uint32_t frameCrc = (static_cast(frame[15]) << 16) | + (static_cast(frame[16]) << 8) | + static_cast(frame[17]); + + const uint32_t computed = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), 15); + + QCOMPARE(computed, frameCrc); +} + +// --------------------------------------------------------------------------- +// RTCMParser State Machine Tests +// --------------------------------------------------------------------------- + +void RTCMParserTest::testParserReset() +{ + RTCMParser parser; + QCOMPARE(parser.messageLength(), static_cast(0)); + QCOMPARE(parser.messageId(), static_cast(0)); + + parser.addByte(RTCM3_PREAMBLE); + parser.addByte(0x00); + parser.reset(); + + QCOMPARE(parser.messageLength(), static_cast(0)); +} + +void RTCMParserTest::testParserValidMessage() +{ + QByteArray frame = buildRtcmFrame(1005, 4); + RTCMParser parser; + + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + complete = parser.addByte(static_cast(frame[i])); + if (i < frame.size() - 1) { + QVERIFY2(!complete, qPrintable(QString("Parser reported complete at byte %1").arg(i))); + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageLength(), static_cast(6)); + QCOMPARE(parser.messageId(), static_cast(1005)); +} + +void RTCMParserTest::testParserCrcValidation() +{ + QByteArray frame = buildRtcmFrame(1077, 8); + RTCMParser parser; + + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserInvalidCrc() +{ + QByteArray frame = buildRtcmFrame(1077, 8); + frame[frame.size() - 1] = static_cast(frame[frame.size() - 1] ^ 0xFF); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QVERIFY(!parser.validateCrc()); +} + +void RTCMParserTest::testParserMessageId() +{ + const uint16_t testIds[] = { 1001, 1005, 1006, 1033, 1077, 1087, 1097, 1127, 4094 }; + + for (uint16_t id : testIds) { + QByteArray frame = buildRtcmFrame(id, 0); + RTCMParser parser; + + for (int i = 0; i < frame.size(); i++) { + parser.addByte(static_cast(frame[i])); + } + + QCOMPARE(parser.messageId(), id); + } +} + +void RTCMParserTest::testParserGarbageBeforePreamble() +{ + QByteArray frame = buildRtcmFrame(1005, 2); + + QByteArray input; + input.append('\x00'); + input.append('\xFF'); + input.append('\xAA'); + input.append('\x55'); + input.append(frame); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < input.size(); i++) { + if (parser.addByte(static_cast(input[i]))) { + complete = true; + break; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1005)); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserInvalidLength() +{ + // Length = 0 is invalid; parser should reset + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append('\x00'); + frame.append('\x00'); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // Parser should recover and parse a valid message + QByteArray valid = buildRtcmFrame(1005, 0); + bool complete = false; + for (int i = 0; i < valid.size(); i++) { + if (parser.addByte(static_cast(valid[i]))) { + complete = true; + } + } + QVERIFY(complete); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserOverlengthRejected() +{ + // Length > 1023 (10-bit max) — set reserved bits in length field + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast(0x04)); // bit 2 set = 1024 (exceeds 1023) + frame.append(static_cast(0x00)); + + RTCMParser parser; + for (int i = 0; i < frame.size(); i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // Parser should have reset; verify recovery + QByteArray valid = buildRtcmFrame(1077, 0); + bool complete = false; + for (int i = 0; i < valid.size(); i++) { + if (parser.addByte(static_cast(valid[i]))) { + complete = true; + } + } + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1077)); +} + +void RTCMParserTest::testParserMultipleMessages() +{ + QByteArray msg1 = buildRtcmFrame(1005, 4); + QByteArray msg2 = buildRtcmFrame(1077, 8); + QByteArray msg3 = buildRtcmFrame(1087, 2); + + QByteArray stream = msg1 + msg2 + msg3; + + RTCMParser parser; + int messageCount = 0; + QVector ids; + + for (int i = 0; i < stream.size(); i++) { + if (parser.addByte(static_cast(stream[i]))) { + QVERIFY(parser.validateCrc()); + ids.append(parser.messageId()); + messageCount++; + parser.reset(); + } + } + + QCOMPARE(messageCount, 3); + QCOMPARE(ids[0], static_cast(1005)); + QCOMPARE(ids[1], static_cast(1077)); + QCOMPARE(ids[2], static_cast(1087)); +} + +void RTCMParserTest::testParserMaxLength() +{ + QByteArray frame; + frame.append(static_cast(RTCM3_PREAMBLE)); + frame.append(static_cast(0x03)); // 1023 upper + frame.append(static_cast(0xFF)); // 1023 lower + + // 1023 bytes of payload + frame.append(static_cast(0x3E)); // message ID 1005 upper + frame.append(static_cast(0xD0)); // message ID 1005 lower + for (int i = 0; i < 1021; i++) { + frame.append(static_cast(i & 0xFF)); + } + + const uint32_t crc = RTCMParser::crc24q( + reinterpret_cast(frame.constData()), + static_cast(frame.size())); + frame.append(static_cast((crc >> 16) & 0xFF)); + frame.append(static_cast((crc >> 8) & 0xFF)); + frame.append(static_cast(crc & 0xFF)); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + if (parser.addByte(static_cast(frame[i]))) { + complete = true; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageLength(), static_cast(1023)); + QCOMPARE(parser.messageId(), static_cast(1005)); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserTruncatedFrame() +{ + QByteArray frame = buildRtcmFrame(1005, 4); + + // Feed only half the frame; parser should never report complete + RTCMParser parser; + const int halfSize = frame.size() / 2; + for (int i = 0; i < halfSize; i++) { + QVERIFY(!parser.addByte(static_cast(frame[i]))); + } + + // After reset, a full valid frame should still parse + parser.reset(); + bool complete = false; + for (int i = 0; i < frame.size(); i++) { + if (parser.addByte(static_cast(frame[i]))) { + complete = true; + } + } + QVERIFY(complete); + QVERIFY(parser.validateCrc()); +} + +void RTCMParserTest::testParserCorruptedPreamble() +{ + // Bytes that look like preamble but aren't, followed by a valid frame + QByteArray input; + // Near-miss preamble bytes + input.append(static_cast(0xD2)); + input.append(static_cast(0xD4)); + input.append(static_cast(0x00)); + input.append(static_cast(0x02)); + + QByteArray valid = buildRtcmFrame(1033, 6); + input.append(valid); + + RTCMParser parser; + bool complete = false; + for (int i = 0; i < input.size(); i++) { + if (parser.addByte(static_cast(input[i]))) { + complete = true; + break; + } + } + + QVERIFY(complete); + QCOMPARE(parser.messageId(), static_cast(1033)); + QVERIFY(parser.validateCrc()); +} + +UT_REGISTER_TEST(RTCMParserTest, TestLabel::Unit) diff --git a/test/GPS/RTCMParserTest.h b/test/GPS/RTCMParserTest.h new file mode 100644 index 000000000000..3a92d1244f22 --- /dev/null +++ b/test/GPS/RTCMParserTest.h @@ -0,0 +1,30 @@ +#pragma once + +#include "UnitTest.h" + +class RTCMParserTest : public UnitTest +{ + Q_OBJECT + +private slots: + // CRC-24Q + void testCrc24qEmpty(); + void testCrc24qSingleByte(); + void testCrc24qKnownVector(); + void testCrc24qReferenceVector(); + void testCrc24qIncremental(); + + // RTCMParser state machine + void testParserReset(); + void testParserValidMessage(); + void testParserCrcValidation(); + void testParserInvalidCrc(); + void testParserMessageId(); + void testParserGarbageBeforePreamble(); + void testParserInvalidLength(); + void testParserOverlengthRejected(); + void testParserMultipleMessages(); + void testParserMaxLength(); + void testParserTruncatedFrame(); + void testParserCorruptedPreamble(); +}; diff --git a/test/MAVLink/CMakeLists.txt b/test/MAVLink/CMakeLists.txt index 6484e8214891..1f7c81b0f323 100644 --- a/test/MAVLink/CMakeLists.txt +++ b/test/MAVLink/CMakeLists.txt @@ -5,6 +5,8 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE + MockLinkSigningTest.cc + MockLinkSigningTest.h SigningTest.cc SigningTest.h StatusTextHandlerTest.cc diff --git a/test/MAVLink/MockLinkSigningTest.cc b/test/MAVLink/MockLinkSigningTest.cc new file mode 100644 index 000000000000..05d21abe0b65 --- /dev/null +++ b/test/MAVLink/MockLinkSigningTest.cc @@ -0,0 +1,125 @@ +#include "MockLinkSigningTest.h" + +#include "MAVLinkSigningKeys.h" +#include "MAVLinkSigning.h" +#include "Vehicle.h" + +#include + +MockLinkSigningTest::MockLinkSigningTest(QObject* parent) + : VehicleTest(parent) +{ +} + +void MockLinkSigningTest::init() +{ + VehicleTest::init(); + + // Ensure a clean key store before each test + auto* signingKeys = MAVLinkSigningKeys::instance(); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } +} + +void MockLinkSigningTest::cleanup() +{ + // Always clean up keys even if the test failed + auto* signingKeys = MAVLinkSigningKeys::instance(); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } + + VehicleTest::cleanup(); +} + +void MockLinkSigningTest::_testSendSetupSigning() +{ + QVERIFY(vehicle()); + QVERIFY(mockLink()); + + // Add a key + auto* signingKeys = MAVLinkSigningKeys::instance(); + signingKeys->addKey("TestKey", "TestPassphrase"); + QVERIFY(!signingKeys->keyBytesAt(0).isEmpty()); + + mockLink()->clearReceivedMavlinkMessageCounts(); + QVERIFY(!mockLink()->signingEnabled()); + + QSignalSpy signingChangedSpy(vehicle(), &Vehicle::mavlinkSigningChanged); + + // Send signing setup to vehicle with key index 0 + vehicle()->sendSetupSigning(0); + + // Verify MockLink received the SETUP_SIGNING message (sent twice for reliability) + QVERIFY_TRUE_WAIT(mockLink()->receivedMavlinkMessageCount(MAVLINK_MSG_ID_SETUP_SIGNING) >= 2, TestTimeout::mediumMs()); + QVERIFY(mockLink()->signingEnabled()); + + // Verify the signal was emitted and the vehicle tracks the active key name + QVERIFY(signingChangedSpy.count() > 0); + QCOMPARE(vehicle()->mavlinkSigningKeyName(), QStringLiteral("TestKey")); +} + +void MockLinkSigningTest::_testSendDisableSigning() +{ + QVERIFY(vehicle()); + QVERIFY(mockLink()); + + // Add a key and send to vehicle first + auto* signingKeys = MAVLinkSigningKeys::instance(); + signingKeys->addKey("TestKey2", "TestPassphrase2"); + + QSignalSpy signingChangedSpy(vehicle(), &Vehicle::mavlinkSigningChanged); + + vehicle()->sendSetupSigning(0); + QVERIFY_TRUE_WAIT(mockLink()->signingEnabled(), TestTimeout::mediumMs()); + QVERIFY(signingChangedSpy.count() > 0); + + // Now disable signing + signingChangedSpy.clear(); + mockLink()->clearReceivedMavlinkMessageCounts(); + vehicle()->sendDisableSigning(); + + // Verify MockLink received the disable message and signing is now off + QVERIFY_TRUE_WAIT(mockLink()->receivedMavlinkMessageCount(MAVLINK_MSG_ID_SETUP_SIGNING) >= 2, TestTimeout::mediumMs()); + QVERIFY(!mockLink()->signingEnabled()); + + // Verify the signal was emitted and the vehicle cleared the active key name + QVERIFY(signingChangedSpy.count() > 0); + QVERIFY(vehicle()->mavlinkSigningKeyName().isEmpty()); +} + +void MockLinkSigningTest::_testSigningKeysAddRemove() +{ + auto* signingKeys = MAVLinkSigningKeys::instance(); + + // Start with no keys + QCOMPARE(signingKeys->keys()->count(), 0); + + // Add first key + QSignalSpy spy(signingKeys, &MAVLinkSigningKeys::keysChanged); + signingKeys->addKey("Key1", "pass1"); + QCOMPARE(signingKeys->keys()->count(), 1); + QCOMPARE(signingKeys->keyNameAt(0), "Key1"); + QVERIFY(!signingKeys->keyBytesAt(0).isEmpty()); + QCOMPARE(signingKeys->keyBytesAt(0).size(), 32); + QVERIFY(spy.count() > 0); + + // Add second key + spy.clear(); + signingKeys->addKey("Key2", "pass2"); + QCOMPARE(signingKeys->keys()->count(), 2); + QCOMPARE(signingKeys->keyNameAt(1), "Key2"); + QVERIFY(spy.count() > 0); + + // Remove first key + signingKeys->removeKey(0); + QCOMPARE(signingKeys->keys()->count(), 1); + QCOMPARE(signingKeys->keyNameAt(0), "Key2"); + + // Remove remaining key + signingKeys->removeKey(0); + QCOMPARE(signingKeys->keys()->count(), 0); +} + +UT_REGISTER_TEST(MockLinkSigningTest, TestLabel::Integration, TestLabel::Vehicle) diff --git a/test/MAVLink/MockLinkSigningTest.h b/test/MAVLink/MockLinkSigningTest.h new file mode 100644 index 000000000000..e4f077bd3dca --- /dev/null +++ b/test/MAVLink/MockLinkSigningTest.h @@ -0,0 +1,18 @@ +#pragma once + +#include "BaseClasses/VehicleTest.h" + +class MockLinkSigningTest : public VehicleTest +{ + Q_OBJECT + +public: + explicit MockLinkSigningTest(QObject* parent = nullptr); + +private slots: + void init() override; + void cleanup() override; + void _testSendSetupSigning(); + void _testSendDisableSigning(); + void _testSigningKeysAddRemove(); +}; diff --git a/test/MAVLink/SigningTest.cc b/test/MAVLink/SigningTest.cc index 07a2be7091ca..9d21049ae3a3 100644 --- a/test/MAVLink/SigningTest.cc +++ b/test/MAVLink/SigningTest.cc @@ -1,30 +1,34 @@ #include "SigningTest.h" #include "MAVLinkSigning.h" +#include "MAVLinkSigningKeys.h" +#include "QmlObjectListModel.h" void SigningTest::_testInitSigning() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + // Create a 32-byte raw key + QByteArray rawKey(32, '\0'); + rawKey[0] = 'A'; + rawKey[1] = 'B'; + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); const mavlink_status_t* status = mavlink_get_channel_status(MAVLINK_COMM_0); const mavlink_signing_t* signing = status->signing; - QVERIFY(memcmp(signing->secret_key, QCryptographicHash::hash("secret_key", QCryptographicHash::Sha256).constData(), - sizeof(signing->secret_key)) == 0); - QByteArray keyData(32, '\0'); - keyData[0] = '1'; - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, keyData, - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); - QVERIFY(memcmp(signing->secret_key, - QCryptographicHash::hash(keyData, QCryptographicHash::Sha256).constData(), - sizeof(signing->secret_key)) == 0); + QVERIFY(signing); + QVERIFY(memcmp(signing->secret_key, rawKey.constData(), sizeof(signing->secret_key)) == 0); + + // Disable signing with empty key QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, QByteArrayView(), - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + QVERIFY(!mavlink_get_channel_status(MAVLINK_COMM_0)->signing); } void SigningTest::_testCheckSigningLinkId() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + QByteArray rawKey(32, '\x01'); + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); const mavlink_heartbeat_t heartbeat{}; mavlink_message_t message; (void)mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_USER1, MAVLINK_COMM_0, &message, &heartbeat); @@ -33,16 +37,101 @@ void SigningTest::_testCheckSigningLinkId() void SigningTest::_testCreateSetupSigning() { - QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, "secret_key", - MAVLinkSigning::insecureConnectionAccceptUnsignedCallback)); + QByteArray rawKey(32, '\x02'); + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_0, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); mavlink_system_t target_system{}; target_system.sysid = 1; target_system.compid = MAV_COMP_ID_AUTOPILOT1; mavlink_setup_signing_t setup_signing; - MAVLinkSigning::createSetupSigning(MAVLINK_COMM_0, target_system, setup_signing); + MAVLinkSigning::createSetupSigning(MAVLINK_COMM_0, target_system, rawKey, setup_signing); QVERIFY(setup_signing.initial_timestamp != 0); QCOMPARE(setup_signing.target_system, target_system.sysid); QCOMPARE(setup_signing.target_component, target_system.compid); + QVERIFY(memcmp(setup_signing.secret_key, rawKey.constData(), sizeof(setup_signing.secret_key)) == 0); +} + +void SigningTest::_testVerifySignature() +{ + // Set up signing on MAVLINK_COMM_1 with a known key so the C library signs outgoing messages + QByteArray rawKey(32, '\0'); + for (int i = 0; i < 32; ++i) { + rawKey[i] = static_cast(i); + } + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_1, rawKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + + // Encode a heartbeat on the signing channel — the C library will sign it + const mavlink_heartbeat_t heartbeat{}; + mavlink_message_t message; + (void) mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_AUTOPILOT1, MAVLINK_COMM_1, &message, &heartbeat); + + // The message should be signed + QVERIFY(MAVLinkSigning::isMessageSigned(message)); + + // Our verifySignature should agree with the C library's signature + QVERIFY(MAVLinkSigning::verifySignature(rawKey, message)); + + // A different key should NOT verify + QByteArray wrongKey(32, '\xFF'); + QVERIFY(!MAVLinkSigning::verifySignature(wrongKey, message)); + + // Too-short key should fail gracefully + QByteArray shortKey(16, '\x01'); + QVERIFY(!MAVLinkSigning::verifySignature(shortKey, message)); + + // Disable signing on the channel to clean up + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_1, QByteArrayView(), nullptr)); +} + +void SigningTest::_testTryDetectKey() +{ + auto* signingKeys = MAVLinkSigningKeys::instance(); + + // Clean slate — remove any leftover keys + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } + + // Add three keys with different passphrases + signingKeys->addKey(QStringLiteral("KeyAlpha"), QStringLiteral("passphrase-alpha")); + signingKeys->addKey(QStringLiteral("KeyBravo"), QStringLiteral("passphrase-bravo")); + signingKeys->addKey(QStringLiteral("KeyCharlie"), QStringLiteral("passphrase-charlie")); + QCOMPARE(signingKeys->keys()->count(), 3); + + // Sign a message using the second key ("KeyBravo") + const QByteArray bravoKey = signingKeys->keyBytesAt(1); + QCOMPARE(bravoKey.size(), 32); + + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_2, bravoKey, + MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + + const mavlink_heartbeat_t heartbeat{}; + mavlink_message_t message; + (void) mavlink_msg_heartbeat_encode_chan(1, MAV_COMP_ID_AUTOPILOT1, MAVLINK_COMM_2, &message, &heartbeat); + QVERIFY(MAVLinkSigning::isMessageSigned(message)); + + // Disable signing on COMM_2 so it was only used to produce the signed message + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_2, QByteArrayView(), nullptr)); + + // COMM_3 has no signing configured — tryDetectKey should find "KeyBravo" + QVERIFY(!MAVLinkSigning::isSigningEnabled(MAVLINK_COMM_3)); + const QString detected = MAVLinkSigning::tryDetectKey(MAVLINK_COMM_3, message); + QCOMPARE(detected, QStringLiteral("KeyBravo")); + + // Signing should now be configured on COMM_3 with the bravo key + QVERIFY(MAVLinkSigning::isSigningEnabled(MAVLINK_COMM_3)); + + // Calling tryDetectKey again on an already-configured channel should return empty + const QString again = MAVLinkSigning::tryDetectKey(MAVLINK_COMM_3, message); + QVERIFY(again.isEmpty()); + + // Clean up + QVERIFY(MAVLinkSigning::initSigning(MAVLINK_COMM_3, QByteArrayView(), nullptr)); + while (signingKeys->keys()->count() > 0) { + signingKeys->removeKey(0); + } } UT_REGISTER_TEST(SigningTest, TestLabel::Unit) diff --git a/test/MAVLink/SigningTest.h b/test/MAVLink/SigningTest.h index 65bfcdfa04f5..e33e29d6c26d 100644 --- a/test/MAVLink/SigningTest.h +++ b/test/MAVLink/SigningTest.h @@ -10,4 +10,6 @@ private slots: void _testInitSigning(); void _testCheckSigningLinkId(); void _testCreateSetupSigning(); + void _testVerifySignature(); + void _testTryDetectKey(); }; diff --git a/test/MissionManager/CMakeLists.txt b/test/MissionManager/CMakeLists.txt index d46e497396cb..57f7dab4f53c 100644 --- a/test/MissionManager/CMakeLists.txt +++ b/test/MissionManager/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources(${CMAKE_PROJECT_NAME} MissionCommandTreeTest.cc MissionCommandTreeTest.h MissionControllerManagerTest.cc MissionControllerManagerTest.h MissionControllerTest.cc MissionControllerTest.h + MissionControllerTreeTest.cc MissionControllerTreeTest.h MissionItemTest.cc MissionItemTest.h MissionManagerTest.cc MissionManagerTest.h MissionSettingsTest.cc MissionSettingsTest.h diff --git a/test/MissionManager/CameraCalcTest.cc b/test/MissionManager/CameraCalcTest.cc index 1670f74c18fb..47fb14afe382 100644 --- a/test/MissionManager/CameraCalcTest.cc +++ b/test/MissionManager/CameraCalcTest.cc @@ -55,9 +55,9 @@ void CameraCalcTest::_testDirty() _multiSpy->clearAllSignals(); } rgFacts.clear(); - _cameraCalc->setDistanceMode(_cameraCalc->distanceMode() == QGroundControlQmlGlobal::AltitudeModeRelative - ? QGroundControlQmlGlobal::AltitudeModeAbsolute - : QGroundControlQmlGlobal::AltitudeModeRelative); + _cameraCalc->setDistanceMode(_cameraCalc->distanceMode() == QGroundControlQmlGlobal::AltitudeFrameRelative + ? QGroundControlQmlGlobal::AltitudeFrameAbsolute + : QGroundControlQmlGlobal::AltitudeFrameRelative); QVERIFY(_cameraCalc->dirty()); _multiSpy->clearAllSignals(); _cameraCalc->setCameraBrand(CameraCalc::canonicalManualCameraName()); diff --git a/test/MissionManager/MissionCommandTreeEditorTestWindow.qml b/test/MissionManager/MissionCommandTreeEditorTestWindow.qml index 4b89eb13145e..c1f43b27bc9a 100644 --- a/test/MissionManager/MissionCommandTreeEditorTestWindow.qml +++ b/test/MissionManager/MissionCommandTreeEditorTestWindow.qml @@ -41,12 +41,13 @@ ApplicationWindow { QGCLabel { text: modelData.commandName; color: "black" } Loader { - id: editorLoader - source: modelData.editorQml - - property var missionItem: modelData - property var masterController: planMasterController - property real availableWidth: editorWidth + id: editorLoader + Component.onCompleted: { + editorLoader.setSource(modelData.editorQml, { + missionItem: modelData, + availableWidth: editorWidth + }) + } } } } diff --git a/test/MissionManager/MissionControllerTest.cc b/test/MissionManager/MissionControllerTest.cc index 410f6071be1f..9f30216871e6 100644 --- a/test/MissionManager/MissionControllerTest.cc +++ b/test/MissionManager/MissionControllerTest.cc @@ -12,6 +12,13 @@ using namespace TestFixtures; MissionControllerTest::~MissionControllerTest() = default; +namespace { +// Coordinate checks involve geodesic reconstruction so they use 0.5 m. Altitude +// checks are direct arithmetic, so they use a much tighter 1e-4 m tolerance. +constexpr double kCoordToleranceMeters = 0.5; +constexpr double kAltToleranceMeters = 1e-4; +} // namespace + void MissionControllerTest::cleanup() { _masterController.reset(); @@ -187,6 +194,162 @@ void MissionControllerTest::_testVehicleYawRecalc() } } +void MissionControllerTest::_testMissionReposition() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setCoordinate(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + const QGeoCoordinate newHome = home.atDistanceAndAzimuth(200.0, 65.0); + const QGeoCoordinate expectedWp1 = + newHome.atDistanceAndAzimuth(home.distanceTo(wp1), home.azimuthTo(wp1)); + const QGeoCoordinate expectedWp2 = + newHome.atDistanceAndAzimuth(home.distanceTo(wp2), home.azimuthTo(wp2)); + _missionController->repositionMission(newHome, true, true); + QVERIFY_TRUE_WAIT((settingsItem->coordinate().distanceTo(newHome) <= kCoordToleranceMeters) && + (item1->coordinate().distanceTo(expectedWp1) <= kCoordToleranceMeters) && + (item2->coordinate().distanceTo(expectedWp2) <= kCoordToleranceMeters), + TestTimeout::shortMs()); + + QCOMPARE_COORDS(settingsItem->coordinate(), newHome, kCoordToleranceMeters); + QCOMPARE_COORDS(item1->coordinate(), expectedWp1, kCoordToleranceMeters); + QCOMPARE_COORDS(item2->coordinate(), expectedWp2, kCoordToleranceMeters); + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionOffset() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setCoordinate(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(150.0, 15.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(320.0, 120.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + _missionController->offsetMission(0.0, 0.0, 12.0, true, true); + QVERIFY_TRUE_WAIT(qAbs(settingsItem->editableAlt() - oldHomeAlt) <= kAltToleranceMeters && + qAbs(item1->editableAlt() - (oldAlt1 + 12.0)) <= kAltToleranceMeters && + qAbs(item2->editableAlt() - (oldAlt2 + 12.0)) <= kAltToleranceMeters, + TestTimeout::shortMs()); + + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1 + 12.0, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2 + 12.0, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionRotate() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setCoordinate(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 20.0); + const QGeoCoordinate wp2 = home.atDistanceAndAzimuth(260.0, 135.0); + _missionController->insertSimpleMissionItem(wp1, 1); + _missionController->insertSimpleMissionItem(wp2, 2); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + SimpleMissionItem* item2 = _missionController->visualItems()->value(2); + QVERIFY(item1); + QVERIFY(item2); + + const double oldHomeAlt = settingsItem->editableAlt(); + const double oldAlt1 = item1->editableAlt(); + const double oldAlt2 = item2->editableAlt(); + + const QGeoCoordinate expectedWp1 = + home.atDistanceAndAzimuth(home.distanceTo(wp1), home.azimuthTo(wp1) + 90.0); + const QGeoCoordinate expectedWp2 = + home.atDistanceAndAzimuth(home.distanceTo(wp2), home.azimuthTo(wp2) + 90.0); + + _missionController->rotateMission(90.0, true, true); + QVERIFY_TRUE_WAIT((settingsItem->coordinate().distanceTo(home) <= kCoordToleranceMeters) && + (item1->coordinate().distanceTo(expectedWp1) <= kCoordToleranceMeters) && + (item2->coordinate().distanceTo(expectedWp2) <= kCoordToleranceMeters), + TestTimeout::shortMs()); + + QCOMPARE_COORDS(settingsItem->coordinate(), home, kCoordToleranceMeters); + QCOMPARE_COORDS(item1->coordinate(), expectedWp1, kCoordToleranceMeters); + QCOMPARE_COORDS(item2->coordinate(), expectedWp2, kCoordToleranceMeters); + QCOMPARE_FUZZY(settingsItem->editableAlt(), oldHomeAlt, kAltToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldAlt1, kAltToleranceMeters); + QCOMPARE_FUZZY(item2->editableAlt(), oldAlt2, kAltToleranceMeters); +} + +void MissionControllerTest::_testMissionTransformsInvalidHome() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + + MissionSettingsItem* settingsItem = + _missionController->visualItems()->value(0); + QVERIFY(settingsItem); + + const QGeoCoordinate home = Coord::zurich(); + settingsItem->setCoordinate(home); + + const QGeoCoordinate wp1 = home.atDistanceAndAzimuth(180.0, 45.0); + _missionController->insertSimpleMissionItem(wp1, 1); + + SimpleMissionItem* item1 = _missionController->visualItems()->value(1); + QVERIFY(item1); + + const QGeoCoordinate oldItemCoord = item1->coordinate(); + const double oldItemAlt = item1->editableAlt(); + + settingsItem->setCoordinate(QGeoCoordinate()); + QVERIFY_TRUE_WAIT(!settingsItem->coordinate().isValid(), TestTimeout::shortMs()); + + // repositionMission and rotateMission require a valid home — they should be no-ops + _missionController->repositionMission(home.atDistanceAndAzimuth(100.0, 0.0), true, true); + _missionController->rotateMission(45.0, true, true); + QCOMPARE_COORDS(item1->coordinate(), oldItemCoord, kCoordToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldItemAlt, kAltToleranceMeters); + + // offsetMission does not depend on home — it should still apply + _missionController->offsetMission(10.0, 5.0, 8.0, true, true); + QVERIFY(item1->coordinate().distanceTo(oldItemCoord) > kCoordToleranceMeters); + QCOMPARE_FUZZY(item1->editableAlt(), oldItemAlt + 8.0, kAltToleranceMeters); +} + void MissionControllerTest::_testLoadJsonSectionAvailable() { _initForFirmwareType(MAV_AUTOPILOT_PX4); @@ -208,24 +371,24 @@ void MissionControllerTest::_testLoadJsonSectionAvailable() } } -void MissionControllerTest::_testGlobalAltMode() +void MissionControllerTest::_testGlobalAltFrame() { _initForFirmwareType(MAV_AUTOPILOT_PX4); - struct _globalAltMode_s + struct _globalAltFrame_s { - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; MAV_FRAME expectedMavFrame; - } altModeTestCases[] = { - {QGroundControlQmlGlobal::AltitudeModeRelative, MAV_FRAME_GLOBAL_RELATIVE_ALT}, - {QGroundControlQmlGlobal::AltitudeModeAbsolute, MAV_FRAME_GLOBAL}, - {QGroundControlQmlGlobal::AltitudeModeCalcAboveTerrain, MAV_FRAME_GLOBAL}, - {QGroundControlQmlGlobal::AltitudeModeTerrainFrame, MAV_FRAME_GLOBAL_TERRAIN_ALT}, + } altFrameTestCases[] = { + {QGroundControlQmlGlobal::AltitudeFrameRelative, MAV_FRAME_GLOBAL_RELATIVE_ALT}, + {QGroundControlQmlGlobal::AltitudeFrameAbsolute, MAV_FRAME_GLOBAL}, + {QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain, MAV_FRAME_GLOBAL}, + {QGroundControlQmlGlobal::AltitudeFrameTerrain, MAV_FRAME_GLOBAL_TERRAIN_ALT}, }; - for (const _globalAltMode_s& testCase : altModeTestCases) { + for (const _globalAltFrame_s& testCase : altFrameTestCases) { _missionController->removeAll(); - _missionController->setGlobalAltitudeMode(testCase.altMode); + _missionController->setGlobalAltitudeFrame(testCase.altFrame); // Use coordinate fixtures const QGeoCoordinate start = Coord::zurich(); _missionController->insertTakeoffItem(start, 1); @@ -234,13 +397,13 @@ void MissionControllerTest::_testGlobalAltMode() _missionController->insertSimpleMissionItem(start.atDistanceAndAzimuth(300, 0), 4); SimpleMissionItem* si = qobject_cast(_missionController->visualItems()->value(1)); - QCOMPARE(si->altitudeMode(), QGroundControlQmlGlobal::AltitudeModeRelative); + QCOMPARE(si->altitudeFrame(), QGroundControlQmlGlobal::AltitudeFrameRelative); QCOMPARE(si->missionItem().frame(), MAV_FRAME_GLOBAL_RELATIVE_ALT); for (int i = 2; i < _missionController->visualItems()->count(); i++) { - TEST_DEBUG(QStringLiteral("Validating altitude mode index %1").arg(i)); + TEST_DEBUG(QStringLiteral("Validating altitude frame index %1").arg(i)); SimpleMissionItem* siLoop = qobject_cast(_missionController->visualItems()->value(i)); - QCOMPARE(siLoop->altitudeMode(), testCase.altMode); + QCOMPARE(siLoop->altitudeFrame(), testCase.altFrame); QCOMPARE(siLoop->missionItem().frame(), testCase.expectedMavFrame); } } diff --git a/test/MissionManager/MissionControllerTest.h b/test/MissionManager/MissionControllerTest.h index 53a1f611434e..042de6a30582 100644 --- a/test/MissionManager/MissionControllerTest.h +++ b/test/MissionManager/MissionControllerTest.h @@ -20,9 +20,13 @@ private slots: void cleanup() override; void _testLoadJsonSectionAvailable(); - void _testGlobalAltMode(); + void _testGlobalAltFrame(); void _testGimbalRecalc(); void _testVehicleYawRecalc(); + void _testMissionReposition(); + void _testMissionOffset(); + void _testMissionRotate(); + void _testMissionTransformsInvalidHome(); // Parameterized tests - runs once per autopilot type UT_PARAMETERIZED_TEST(_testEmptyVehicle); diff --git a/test/MissionManager/MissionControllerTreeTest.cc b/test/MissionManager/MissionControllerTreeTest.cc new file mode 100644 index 000000000000..5a250bb62f8f --- /dev/null +++ b/test/MissionManager/MissionControllerTreeTest.cc @@ -0,0 +1,374 @@ +#include "MissionControllerTreeTest.h" + +#include "AppSettings.h" +#include "MissionController.h" +#include "MissionSettingsItem.h" +#include "PlanMasterController.h" +#include "QmlObjectTreeModel.h" +#include "RallyPointController.h" +#include "SettingsManager.h" +#include "SimpleMissionItem.h" +#include "TestFixtures.h" + +using namespace TestFixtures; + +MissionControllerTreeTest::~MissionControllerTreeTest() = default; + +void MissionControllerTreeTest::cleanup() +{ + _masterController.reset(); + _missionController = nullptr; + MissionControllerManagerTest::cleanup(); +} + +void MissionControllerTreeTest::_initForTest() +{ + _initForFirmwareType(MAV_AUTOPILOT_PX4); + SettingsManager::instance()->appSettings()->offlineEditingFirmwareClass()->setRawValue( + QGCMAVLink::firmwareClass(MAV_AUTOPILOT_PX4)); + + _masterController = std::make_unique(); + _masterController->setFlyView(false); + _missionController = _masterController->missionController(); + + SignalSpyFixture spy(_missionController); + QVERIFY(spy.spy()); + spy.expect("visualItemsChanged"); + _masterController->start(); + QVERIFY(spy.waitAndVerify(TestTimeout::mediumMs())); + + // Sanity — visualItems has MissionSettingsItem at index 0 + QVERIFY(_missionController->visualItems()); + QCOMPARE(_missionController->visualItems()->count(), 1); +} + +// =========================================================================== +// Tree structure after init +// =========================================================================== + +void MissionControllerTreeTest::_testTreeStructureAfterInit() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + QVERIFY(tree); + + // Root should have all expected top-level groups + QCOMPARE(tree->rowCount(), kGroupCount); + + // Verify group node types + const QModelIndex planFileGroup = tree->index(kPlanFileGroupRow, 0); + const QModelIndex defaultsGroup = tree->index(kDefaultsGroupRow, 0); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); + const QModelIndex fenceGroup = tree->index(kFenceGroupRow, 0); + const QModelIndex rallyGroup = tree->index(kRallyGroupRow, 0); + QVERIFY(planFileGroup.isValid()); + QVERIFY(defaultsGroup.isValid()); + QVERIFY(missionGroup.isValid()); + QVERIFY(fenceGroup.isValid()); + QVERIFY(rallyGroup.isValid()); + + QCOMPARE(tree->data(planFileGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("planFileGroup")); + QCOMPARE(tree->data(defaultsGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("defaultsGroup")); + QCOMPARE(tree->data(missionGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionGroup")); + QCOMPARE(tree->data(fenceGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("fenceGroup")); + QCOMPARE(tree->data(rallyGroup, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("rallyGroup")); + + // Plan File group should have 1 child (planFileInfo marker) + QCOMPARE(tree->rowCount(planFileGroup), 1); + const QModelIndex planFileChild = tree->index(0, 0, planFileGroup); + QCOMPARE(tree->data(planFileChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("planFileInfo")); + + // Defaults group should have 1 child (defaultsInfo marker) + QCOMPARE(tree->rowCount(defaultsGroup), 1); + const QModelIndex defaultsChild = tree->index(0, 0, defaultsGroup); + QCOMPARE(tree->data(defaultsChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("defaultsInfo")); + + // Mission group should contain 1 child (the MissionSettingsItem) + QCOMPARE(tree->rowCount(missionGroup), 1); + + // Fence group should have 1 child (fenceEditor marker) + QCOMPARE(tree->rowCount(fenceGroup), 1); + const QModelIndex fenceChild = tree->index(0, 0, fenceGroup); + QCOMPARE(tree->data(fenceChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("fenceEditor")); + + // Rally group should have 1 child (rallyHeader marker) + QCOMPARE(tree->rowCount(rallyGroup), 1); + const QModelIndex rallyChild = tree->index(0, 0, rallyGroup); + QCOMPARE(tree->data(rallyChild, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("rallyHeader")); +} + +// =========================================================================== +// Insert waypoints → tree sync +// =========================================================================== + +void MissionControllerTreeTest::_testInsertWaypointUpdatesTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); + + // Before insert: 1 mission item (MissionSettingsItem) + const int initialMissionChildren = tree->rowCount(missionGroup); + QCOMPARE(initialMissionChildren, _missionController->visualItems()->count()); + + // Insert 3 waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + // _visualItems should have 4 items (settings + 3 waypoints) + QCOMPARE(_missionController->visualItems()->count(), 4); + + // Tree's mission group should mirror that count + QCOMPARE(tree->rowCount(missionGroup), 4); + + // Verify tree children correspond to visualItems + for (int i = 0; i < _missionController->visualItems()->count(); i++) { + QObject* listObj = _missionController->visualItems()->get(i); + const QModelIndex treeChild = tree->index(i, 0, missionGroup); + QObject* treeObj = tree->data(treeChild, Qt::UserRole).value(); + QCOMPARE(treeObj, listObj); + } +} + +// =========================================================================== +// Remove waypoints → tree sync +// =========================================================================== + +void MissionControllerTreeTest::_testRemoveWaypointUpdatesTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); + + // Insert 3 waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + QCOMPARE(tree->rowCount(missionGroup), 4); // settings + 3 waypoints + + // Remove middle waypoint (visualItem index 2) + _missionController->removeVisualItem(2); + + QCOMPARE(_missionController->visualItems()->count(), 3); + QCOMPARE(tree->rowCount(missionGroup), 3); + + // Verify correspondence + for (int i = 0; i < _missionController->visualItems()->count(); i++) { + QObject* listObj = _missionController->visualItems()->get(i); + const QModelIndex treeChild = tree->index(i, 0, missionGroup); + QObject* treeObj = tree->data(treeChild, Qt::UserRole).value(); + QCOMPARE(treeObj, listObj); + } +} + +// =========================================================================== +// removeAll → tree rebuilt +// =========================================================================== + +void MissionControllerTreeTest::_testRemoveAllRebuildsTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + // Insert waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + const QModelIndex missionGroupBefore = tree->index(kMissionGroupRow, 0); + QCOMPARE(tree->rowCount(missionGroupBefore), 4); + + // Remove all + _missionController->removeAll(); + + // After removeAll, tree should still have all groups + QCOMPARE(tree->rowCount(), kGroupCount); + + // Mission group should have 1 child (the new MissionSettingsItem) + const QModelIndex missionGroupAfter = tree->index(kMissionGroupRow, 0); + QCOMPARE(tree->rowCount(missionGroupAfter), 1); + + // That child should be the new settings item + const QModelIndex settingsChild = tree->index(0, 0, missionGroupAfter); + QObject* settingsObj = tree->data(settingsChild, Qt::UserRole).value(); + QCOMPARE(settingsObj, static_cast(_missionController->visualItems()->get(0))); +} + +// =========================================================================== +// Persistent group indexes remain valid across operations +// =========================================================================== + +void MissionControllerTreeTest::_testPersistentGroupIndexes() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + // Capture persistent indexes for each group + QPersistentModelIndex planFilePersist(tree->index(kPlanFileGroupRow, 0)); + QPersistentModelIndex defaultsPersist(tree->index(kDefaultsGroupRow, 0)); + QPersistentModelIndex missionPersist(tree->index(kMissionGroupRow, 0)); + QPersistentModelIndex fencePersist(tree->index(kFenceGroupRow, 0)); + QPersistentModelIndex rallyPersist(tree->index(kRallyGroupRow, 0)); + + // Insert waypoints + const QList waypoints = Coord::waypointPath(Coord::zurich(), 5); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + // All persistent indexes should still be valid + QVERIFY(planFilePersist.isValid()); + QVERIFY(defaultsPersist.isValid()); + QVERIFY(missionPersist.isValid()); + QVERIFY(fencePersist.isValid()); + QVERIFY(rallyPersist.isValid()); + + // They should still be at the same rows (group structure didn't change) + QCOMPARE(planFilePersist.row(), kPlanFileGroupRow); + QCOMPARE(defaultsPersist.row(), kDefaultsGroupRow); + QCOMPARE(missionPersist.row(), kMissionGroupRow); + QCOMPARE(fencePersist.row(), kFenceGroupRow); + QCOMPARE(rallyPersist.row(), kRallyGroupRow); + + // Remove some waypoints + _missionController->removeVisualItem(3); + _missionController->removeVisualItem(2); + + // Persistent indexes should still be valid + QVERIFY(planFilePersist.isValid()); + QVERIFY(defaultsPersist.isValid()); + QVERIFY(missionPersist.isValid()); + QVERIFY(fencePersist.isValid()); + QVERIFY(rallyPersist.isValid()); +} + +// =========================================================================== +// recalcChildItems is a no-op for the tree (no crash) +// =========================================================================== + +void MissionControllerTreeTest::_testRecalcChildItemsNoCrash() +{ + _initForTest(); + + // Insert enough waypoints to exercise child item recalc + const QList waypoints = Coord::waypointPath(Coord::zurich(), 4); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QModelIndex missionGroup = tree->index(kMissionGroupRow, 0); + + // Tree should be consistent — recalcChildItems happens automatically during insert + QCOMPARE(tree->rowCount(missionGroup), _missionController->visualItems()->count()); + + // Verify no crash and tree is still functional + QVERIFY(tree->index(0, 0, missionGroup).isValid()); + QCOMPARE(tree->rowCount(), kGroupCount); +} + +// =========================================================================== +// Exposed Q_PROPERTY persistent index getters match tree model indices +// =========================================================================== + +void MissionControllerTreeTest::_testExposedPersistentIndexGettersMatchTree() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + + QCOMPARE(QModelIndex(_missionController->planFileGroupIndex()), tree->index(kPlanFileGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->defaultsGroupIndex()), tree->index(kDefaultsGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->missionGroupIndex()), tree->index(kMissionGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->fenceGroupIndex()), tree->index(kFenceGroupRow, 0)); + QCOMPARE(QModelIndex(_missionController->rallyGroupIndex()), tree->index(kRallyGroupRow, 0)); +} + +// =========================================================================== +// Exposed persistent indexes survive removeAll rebuild +// =========================================================================== + +void MissionControllerTreeTest::_testExposedIndexesSurviveRemoveAll() +{ + _initForTest(); + + // Insert some waypoints then removeAll to trigger tree rebuild + const QList waypoints = Coord::waypointPath(Coord::zurich(), 3); + for (int i = 0; i < waypoints.count(); ++i) { + _missionController->insertSimpleMissionItem(waypoints[i], i + 1); + } + + _missionController->removeAll(); + + // The CONSTANT Q_PROPERTY persistent indexes must still be valid + QVERIFY(_missionController->planFileGroupIndex().isValid()); + QVERIFY(_missionController->defaultsGroupIndex().isValid()); + QVERIFY(_missionController->missionGroupIndex().isValid()); + QVERIFY(_missionController->fenceGroupIndex().isValid()); + QVERIFY(_missionController->rallyGroupIndex().isValid()); + + // They should still point to the correct rows + QCOMPARE(_missionController->planFileGroupIndex().row(), kPlanFileGroupRow); + QCOMPARE(_missionController->defaultsGroupIndex().row(), kDefaultsGroupRow); + QCOMPARE(_missionController->missionGroupIndex().row(), kMissionGroupRow); + QCOMPARE(_missionController->fenceGroupIndex().row(), kFenceGroupRow); + QCOMPARE(_missionController->rallyGroupIndex().row(), kRallyGroupRow); +} + +// =========================================================================== +// Rally header appears when no points, disappears when points added, reappears when removed +// =========================================================================== + +void MissionControllerTreeTest::_testRallyHeaderDynamicVisibility() +{ + _initForTest(); + + QmlObjectTreeModel* tree = _missionController->visualItemsTree(); + const QPersistentModelIndex rallyGroup(_missionController->rallyGroupIndex()); + auto* rallyController = _masterController->rallyPointController(); + QVERIFY(rallyController); + + // Initially: rally group has 1 child — the header marker + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyHeader")); + + // Add a rally point → header removed, replaced by rallyItem + rallyController->addPoint(QGeoCoordinate(47.3977, 8.5456)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Add a second rally point + rallyController->addPoint(QGeoCoordinate(47.3980, 8.5460)); + QCOMPARE(tree->rowCount(rallyGroup), 2); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + QCOMPARE(tree->data(tree->index(1, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Remove the first rally point — still 1 rallyItem, no header + rallyController->removePoint(rallyController->points()->get(0)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyItem")); + + // Remove the last rally point → header marker reappears + rallyController->removePoint(rallyController->points()->get(0)); + QCOMPARE(tree->rowCount(rallyGroup), 1); + QCOMPARE(tree->data(tree->index(0, 0, rallyGroup), QmlObjectTreeModel::NodeTypeRole).toString(), + QStringLiteral("rallyHeader")); +} + +#include "UnitTest.h" + +UT_REGISTER_TEST(MissionControllerTreeTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/MissionControllerTreeTest.h b/test/MissionManager/MissionControllerTreeTest.h new file mode 100644 index 000000000000..c1579a548c5d --- /dev/null +++ b/test/MissionManager/MissionControllerTreeTest.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include "MissionControllerManagerTest.h" +#include "MissionController.h" +class PlanMasterController; + +/// Tests for the MissionController incremental tree model sync (PlanTreeView branch). +/// Verifies that _visualItemsTree stays in sync with _visualItems when +/// inserting/removing waypoints, calling removeAll, and loading plans. +class MissionControllerTreeTest : public MissionControllerManagerTest +{ + Q_OBJECT + +public: + ~MissionControllerTreeTest() override; + +private slots: + void cleanup() override; + + // Tree structure after init + void _testTreeStructureAfterInit(); + + // Insert waypoints → tree sync + void _testInsertWaypointUpdatesTree(); + + // Remove waypoints → tree sync + void _testRemoveWaypointUpdatesTree(); + + // removeAll → tree rebuilt + void _testRemoveAllRebuildsTree(); + + // Multiple inserts keep group persistent indexes valid + void _testPersistentGroupIndexes(); + + // recalcChildItems is a no-op for tree (no crash) + void _testRecalcChildItemsNoCrash(); + + // Exposed Q_PROPERTY persistent index getters match tree model indices + void _testExposedPersistentIndexGettersMatchTree(); + + // Exposed persistent indexes survive removeAll rebuild + void _testExposedIndexesSurviveRemoveAll(); + + // Rally header appears/disappears as rally points are added/removed + void _testRallyHeaderDynamicVisibility(); + +private: + void _initForTest(); + + // Use canonical constants from MissionController + static constexpr int kPlanFileGroupRow = MissionController::kPlanFileGroupRow; + static constexpr int kDefaultsGroupRow = MissionController::kDefaultsGroupRow; + static constexpr int kMissionGroupRow = MissionController::kMissionGroupRow; + static constexpr int kFenceGroupRow = MissionController::kFenceGroupRow; + static constexpr int kRallyGroupRow = MissionController::kRallyGroupRow; + static constexpr int kGroupCount = MissionController::kGroupCount; + + std::unique_ptr _masterController; + MissionController* _missionController = nullptr; +}; diff --git a/test/MissionManager/PlanMasterControllerTest.cc b/test/MissionManager/PlanMasterControllerTest.cc index 5580e418b2fb..12cd9a941776 100644 --- a/test/MissionManager/PlanMasterControllerTest.cc +++ b/test/MissionManager/PlanMasterControllerTest.cc @@ -1,11 +1,19 @@ #include "PlanMasterControllerTest.h" +#include "AppSettings.h" #include "MissionManager.h" #include "MultiSignalSpy.h" #include "MultiVehicleManager.h" #include "PlanMasterController.h" +#include "SettingsManager.h" #include "Vehicle.h" +#include +#include +#include +#include +#include + void PlanMasterControllerTest::init() { UnitTest::init(); @@ -47,15 +55,413 @@ void PlanMasterControllerTest::_testActiveVehicleChanged() QVERIFY(spyMissionManager.onlyEmittedOnceByMask(missionManagerErrorSignalMask)); spyMissionManager.clearSignal("error"); QVERIFY(spyMissionManager.noneEmitted()); + _connectMockLink(MAV_AUTOPILOT_PX4); auto masterControllerMgrVehicleChanged = spyMasterController.mask("managerVehicleChanged"); QVERIFY(spyMasterController.emittedOnceByMask(masterControllerMgrVehicleChanged)); + emit outgoingManagerVehicle->missionManager()->error(0, ""); // This signal was affected by the defect - it wouldn't reach the subscriber. Here // we make sure it does. QVERIFY(spyMissionManager.onlyEmittedOnceByMask(missionManagerErrorSignalMask)); } +void PlanMasterControllerTest::_testDirtyFlagsMatrix_data() +{ + // Dirty-state transition matrix ("unchanged" means preserve prior value): + // + // | State \ Action | Upload OK | Clear | SaveDirty=true | Load plan | Save file OK | Clear save-dirty | Download w/ items | Download empty | + // |----------------|-----------|-------|----------------|-----------|--------------|------------------|-------------------|----------------| + // | dirtyForSave | unchanged | false | true | false | false | false | true | false | + // | dirtyForUpload | false | false | true | true | unchanged | unchanged | false | false | + + // Data columns: + // - scenario: DirtyScenario enum value selecting which action path to execute + // - initialDirtyForSave: initial dirtyForSave state before action (DirtyStateTrue/False) + // - initialDirtyForUpload: initial dirtyForUpload state before action (DirtyStateTrue/False) + // - expectedDirtyForSave: expected final dirtyForSave state (DirtyState) + // - expectedDirtyForUpload: expected final dirtyForUpload state (DirtyState) + + QTest::addColumn("scenario"); + QTest::addColumn("initialDirtyForSave"); + QTest::addColumn("initialDirtyForUpload"); + QTest::addColumn("expectedDirtyForSave"); + QTest::addColumn("expectedDirtyForUpload"); + + struct ScenarioExpectation { + DirtyScenario scenario; + const char* name; + DirtyState expectedDirtyForSave; + DirtyState expectedDirtyForUpload; + }; + + const QList scenarioExpectations = { + { UploadPreservesSaveDirtyFalse, "upload completion keeps save false", DirtyStateUnchanged, DirtyStateFalse }, + { UploadPreservesSaveDirtyTrue, "upload completion keeps save true", DirtyStateUnchanged, DirtyStateFalse }, + { UploadFalseOnPlanClear, "upload false on clear", DirtyStateFalse, DirtyStateFalse }, + { UploadTrueWhenSaveTrue, "upload true when save true", DirtyStateTrue, DirtyStateTrue }, + { UploadTrueOnNewPlanLoad, "upload true on new plan load", DirtyStateFalse, DirtyStateTrue }, + { SaveToFilePreservesUploadDirtyTrue, "saveToFile keeps upload true", DirtyStateFalse, DirtyStateUnchanged }, + { SaveToFilePreservesUploadDirtyFalse,"saveToFile keeps upload false", DirtyStateFalse, DirtyStateUnchanged }, + { SaveFalseOnSuccessfulLoad, "save false on successful load", DirtyStateFalse, DirtyStateTrue }, + { ClearSaveDirtyPreservesUploadTrue, "clear save dirty keeps upload true", DirtyStateFalse, DirtyStateUnchanged }, + { ClearSaveDirtyPreservesUploadFalse, "clear save dirty keeps upload false", DirtyStateFalse, DirtyStateUnchanged }, + { DownloadWithItemsDirtyForSave, "download with items marks save dirty",DirtyStateTrue, DirtyStateFalse }, + { DownloadEmptyNotDirtyForSave, "download empty keeps save clean", DirtyStateFalse, DirtyStateFalse }, + }; + + const QList initialStates = { + DirtyStateFalse, + DirtyStateTrue, + }; + + for (const ScenarioExpectation& expectation : scenarioExpectations) { + for (const DirtyState initialDirtyForSave : initialStates) { + for (const DirtyState initialDirtyForUpload : initialStates) { + DirtyState expectedDirtyForSave = expectation.expectedDirtyForSave; + DirtyState expectedDirtyForUpload = expectation.expectedDirtyForUpload; + + if ((expectation.scenario == UploadTrueWhenSaveTrue) && (initialDirtyForSave == DirtyStateTrue)) { + // _setDirtyForSave(true) only drives dirtyForUpload when dirtyForSave transitions false->true. + // If dirtyForSave already starts true, dirtyForUpload is preserved. + expectedDirtyForUpload = DirtyStateUnchanged; + } + + const QString rowName = QStringLiteral("%1 [init save=%2 upload=%3]") + .arg(expectation.name) + .arg(initialDirtyForSave == DirtyStateTrue ? QStringLiteral("true") : QStringLiteral("false")) + .arg(initialDirtyForUpload == DirtyStateTrue ? QStringLiteral("true") : QStringLiteral("false")); + QTest::newRow(rowName.toLatin1().constData()) + << +expectation.scenario + << +initialDirtyForSave + << +initialDirtyForUpload + << +expectedDirtyForSave + << +expectedDirtyForUpload; + } + } + } +} + +void PlanMasterControllerTest::_testDirtyFlagsMatrix() +{ + QFETCH(int, scenario); + QFETCH(int, initialDirtyForSave); + QFETCH(int, initialDirtyForUpload); + QFETCH(int, expectedDirtyForSave); + QFETCH(int, expectedDirtyForUpload); + + QVERIFY(initialDirtyForSave != DirtyStateUnchanged); + QVERIFY(initialDirtyForUpload != DirtyStateUnchanged); + + // Pre-load items for scenarios that need containsItems() == true + if (scenario == DownloadWithItemsDirtyForSave) { + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + } + + const auto dirtyStateToBool = [](int state) -> bool { + switch (state) { + case DirtyStateFalse: + return false; + case DirtyStateTrue: + return true; + default: + Q_ASSERT(false); // Invalid test data + return false; + } + }; + + _masterController->_setDirtyForSaveUnitTest(dirtyStateToBool(initialDirtyForSave)); + _masterController->_setDirtyForUploadUnitTest(dirtyStateToBool(initialDirtyForUpload)); + + const bool initialDirtyForSaveBool = _masterController->dirtyForSave(); + const bool initialDirtyForUploadBool = _masterController->dirtyForUpload(); + + QSignalSpy dirtyForSaveChangedSpy(_masterController, &PlanMasterController::dirtyForSaveChanged); + QSignalSpy dirtyForUploadChangedSpy(_masterController, &PlanMasterController::dirtyForUploadChanged); + + switch (scenario) { + case UploadPreservesSaveDirtyFalse: { + const bool invoked = QMetaObject::invokeMethod(_masterController, "_sendRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case UploadPreservesSaveDirtyTrue: { + const bool invoked = QMetaObject::invokeMethod(_masterController, "_sendRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case UploadFalseOnPlanClear: + _masterController->removeAll(); + break; + case UploadTrueWhenSaveTrue: + _masterController->_setDirtyForSave(true); + break; + case UploadTrueOnNewPlanLoad: + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + break; + case SaveToFilePreservesUploadDirtyTrue: { + const QString saveFile = QDir::temp().filePath(QStringLiteral("qgc_planmaster_test_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + QFile::remove(saveFile); + break; + } + case SaveToFilePreservesUploadDirtyFalse: { + const QString saveFile = QDir::temp().filePath(QStringLiteral("qgc_planmaster_test_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + QFile::remove(saveFile); + break; + } + case SaveFalseOnSuccessfulLoad: + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + break; + case ClearSaveDirtyPreservesUploadTrue: + _masterController->_setDirtyForSave(false); + break; + case ClearSaveDirtyPreservesUploadFalse: + _masterController->_setDirtyForSave(false); + break; + case DownloadWithItemsDirtyForSave: { + QVERIFY(_masterController->containsItems()); + const bool invoked = QMetaObject::invokeMethod(_masterController, "_loadRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + case DownloadEmptyNotDirtyForSave: { + QVERIFY(!_masterController->containsItems()); + const bool invoked = QMetaObject::invokeMethod(_masterController, "_loadRallyPointsComplete", Qt::DirectConnection); + QVERIFY(invoked); + break; + } + } + + const auto resolveExpected = [](int expectedState, bool unchangedValue) -> bool { + switch (expectedState) { + case DirtyStateFalse: + return false; + case DirtyStateTrue: + return true; + case DirtyStateUnchanged: + return unchangedValue; + } + return unchangedValue; + }; + + const bool expectedDirtyForSaveBool = resolveExpected(expectedDirtyForSave, initialDirtyForSaveBool); + const bool expectedDirtyForUploadBool = resolveExpected(expectedDirtyForUpload, initialDirtyForUploadBool); + const int expectedDirtyForSaveSignalCount = (expectedDirtyForSaveBool != initialDirtyForSaveBool) ? 1 : 0; + const int expectedDirtyForUploadSignalCount = (expectedDirtyForUploadBool != initialDirtyForUploadBool) ? 1 : 0; + + QCOMPARE(_masterController->dirtyForSave(), expectedDirtyForSaveBool); + QCOMPARE(_masterController->dirtyForUpload(), expectedDirtyForUploadBool); + + QCOMPARE(dirtyForSaveChangedSpy.count(), expectedDirtyForSaveSignalCount); + QCOMPARE(dirtyForUploadChangedSpy.count(), expectedDirtyForUploadSignalCount); + + if (dirtyForSaveChangedSpy.count() > 0) { + const QList& args = dirtyForSaveChangedSpy.at(dirtyForSaveChangedSpy.count() - 1); + QCOMPARE(args.count(), 1); + QCOMPARE(args.first().toBool(), _masterController->dirtyForSave()); + } + + if (dirtyForUploadChangedSpy.count() > 0) { + const QList& args = dirtyForUploadChangedSpy.at(dirtyForUploadChangedSpy.count() - 1); + QCOMPARE(args.count(), 1); + QCOMPARE(args.first().toBool(), _masterController->dirtyForUpload()); + } +} + +// =========================================================================== +// File name property tests +// =========================================================================== + +void PlanMasterControllerTest::_testFileNamesSetOnLoad() +{ + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + // Before load, names should be empty + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // After successful load, both names should be set to the base name + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("MissionPlanner")); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + // Signals should have fired + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testCurrentPlanFileNameWritable() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + // Rename via the writable property + _masterController->setCurrentPlanFileName(QStringLiteral("RenamedPlan")); + + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("RenamedPlan")); + // Original should remain unchanged + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + QCOMPARE(currentNameSpy.count(), 1); + QCOMPARE(originalNameSpy.count(), 0); + + // Setting to the same value should not emit again + _masterController->setCurrentPlanFileName(QStringLiteral("RenamedPlan")); + QCOMPARE(currentNameSpy.count(), 1); +} + +void PlanMasterControllerTest::_testPlanFileRenamed() +{ + // Before load, planFileRenamed should be false (both names empty) + QVERIFY(!_masterController->planFileRenamed()); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // After load, current == original → not renamed + QVERIFY(!_masterController->planFileRenamed()); + + // Rename + _masterController->setCurrentPlanFileName(QStringLiteral("NewName")); + QVERIFY(_masterController->planFileRenamed()); + + // Rename back to original + _masterController->setCurrentPlanFileName(QStringLiteral("MissionPlanner")); + QVERIFY(!_masterController->planFileRenamed()); +} + +void PlanMasterControllerTest::_testSaveWithCurrentName() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // First save to a real (writable) directory so _currentPlanFile points somewhere valid + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const QString initialPath = QStringLiteral("%1/MissionPlanner.%2").arg(tmpDir.path(), _masterController->fileExtension()); + QVERIFY(_masterController->saveToFile(initialPath)); + + // Rename + _masterController->setCurrentPlanFileName(QStringLiteral("TestSaveRenamed")); + + // Save with the renamed name + QVERIFY(_masterController->saveWithCurrentName()); + + // After save, original should now match the renamed name + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("TestSaveRenamed")); + QCOMPARE(_masterController->currentPlanFileName(), QStringLiteral("TestSaveRenamed")); + QVERIFY(!_masterController->planFileRenamed()); +} + +void PlanMasterControllerTest::_testSaveWithCurrentNameNoFile() +{ + // No file loaded — saveWithCurrentName with empty name should fail + QVERIFY(!_masterController->saveWithCurrentName()); + + // Set a name without loading a file first (simulates typing a name in the UI) + _masterController->setCurrentPlanFileName(QStringLiteral("BrandNewPlan")); + + // Should save to the default mission save directory + QVERIFY(_masterController->saveWithCurrentName()); + + const QString expectedDir = SettingsManager::instance()->appSettings()->missionSavePath(); + const QString expectedPath = QStringLiteral("%1/BrandNewPlan.%2").arg(expectedDir, _masterController->fileExtension()); + QCOMPARE(_masterController->currentPlanFile(), expectedPath); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("BrandNewPlan")); + + // Clean up + QFile::remove(expectedPath); +} + +void PlanMasterControllerTest::_testResolvedPlanFileExists() +{ + // Empty name → should return false + QVERIFY(!_masterController->resolvedPlanFileExists()); + + // Save a file so it exists on disk + QTemporaryDir tmpDir; + QVERIFY(tmpDir.isValid()); + const QString savePath = QStringLiteral("%1/ExistingPlan.%2").arg(tmpDir.path(), _masterController->fileExtension()); + QVERIFY(_masterController->saveToFile(savePath)); + + // Now rename to the same base name — file exists at resolved path + _masterController->setCurrentPlanFileName(QStringLiteral("ExistingPlan")); + QVERIFY(_masterController->resolvedPlanFileExists()); + + // Rename to something non-existent + _masterController->setCurrentPlanFileName(QStringLiteral("DoesNotExist")); + QVERIFY(!_masterController->resolvedPlanFileExists()); +} + +void PlanMasterControllerTest::_testFileNamesClearedOnRemoveAll() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // Verify names are set + QVERIFY(!_masterController->currentPlanFileName().isEmpty()); + QVERIFY(!_masterController->originalPlanFileName().isEmpty()); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + _masterController->removeAll(); + + // Names should be cleared + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + QVERIFY(_masterController->currentPlanFile().isEmpty()); + + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testFileNamesClearedOnRemoveAllFromVehicle() +{ + _connectMockLink(MAV_AUTOPILOT_PX4); + + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + + // Verify names are set + QVERIFY(!_masterController->currentPlanFileName().isEmpty()); + QVERIFY(!_masterController->originalPlanFileName().isEmpty()); + + QSignalSpy currentNameSpy(_masterController, &PlanMasterController::currentPlanFileNameChanged); + QSignalSpy originalNameSpy(_masterController, &PlanMasterController::originalPlanFileNameChanged); + + _masterController->removeAllFromVehicle(); + + // Names should be cleared + QVERIFY(_masterController->currentPlanFileName().isEmpty()); + QVERIFY(_masterController->originalPlanFileName().isEmpty()); + QVERIFY(_masterController->currentPlanFile().isEmpty()); + + QVERIFY(currentNameSpy.count() >= 1); + QVERIFY(originalNameSpy.count() >= 1); +} + +void PlanMasterControllerTest::_testSaveUpdatesOriginalFileName() +{ + _masterController->loadFromFile(":/unittest/MissionPlanner.waypoints"); + QCOMPARE(_masterController->originalPlanFileName(), QStringLiteral("MissionPlanner")); + + // Save to a completely different path + const QString saveFile = QDir::temp().filePath( + QStringLiteral("qgc_planmaster_rename_%1.plan").arg(QDateTime::currentMSecsSinceEpoch())); + QVERIFY(_masterController->saveToFile(saveFile)); + + // Both names should now reflect the new file base name + const QString expectedBase = QFileInfo(saveFile).completeBaseName(); + QCOMPARE(_masterController->currentPlanFileName(), expectedBase); + QCOMPARE(_masterController->originalPlanFileName(), expectedBase); + + // Clean up + QFile::remove(saveFile); +} + #include "UnitTest.h" UT_REGISTER_TEST(PlanMasterControllerTest, TestLabel::Integration, TestLabel::MissionManager) diff --git a/test/MissionManager/PlanMasterControllerTest.h b/test/MissionManager/PlanMasterControllerTest.h index 3f2a3da3a193..34f1cc1f0bf5 100644 --- a/test/MissionManager/PlanMasterControllerTest.h +++ b/test/MissionManager/PlanMasterControllerTest.h @@ -14,7 +14,41 @@ private slots: void _testMissionPlannerFileLoad(); void _testActiveVehicleChanged(); + void _testDirtyFlagsMatrix_data(); + void _testDirtyFlagsMatrix(); + + // File name property tests + void _testFileNamesSetOnLoad(); + void _testCurrentPlanFileNameWritable(); + void _testPlanFileRenamed(); + void _testSaveWithCurrentName(); + void _testSaveWithCurrentNameNoFile(); + void _testResolvedPlanFileExists(); + void _testFileNamesClearedOnRemoveAll(); + void _testFileNamesClearedOnRemoveAllFromVehicle(); + void _testSaveUpdatesOriginalFileName(); private: + enum DirtyScenario { + UploadPreservesSaveDirtyTrue, + UploadPreservesSaveDirtyFalse, + UploadFalseOnPlanClear, + UploadTrueWhenSaveTrue, + UploadTrueOnNewPlanLoad, + SaveToFilePreservesUploadDirtyTrue, + SaveToFilePreservesUploadDirtyFalse, + SaveFalseOnSuccessfulLoad, + ClearSaveDirtyPreservesUploadTrue, + ClearSaveDirtyPreservesUploadFalse, + DownloadWithItemsDirtyForSave, + DownloadEmptyNotDirtyForSave, + }; + + enum DirtyState { + DirtyStateFalse, + DirtyStateTrue, + DirtyStateUnchanged + }; + PlanMasterController* _masterController = nullptr; }; diff --git a/test/MissionManager/QGCMapPolygonTest.cc b/test/MissionManager/QGCMapPolygonTest.cc index 038c291b2559..fef01ac80d6b 100644 --- a/test/MissionManager/QGCMapPolygonTest.cc +++ b/test/MissionManager/QGCMapPolygonTest.cc @@ -252,6 +252,65 @@ void QGCMapPolygonTest::_testSegmentSplit() QVERIFY(_mapPolygon->selectedVertex() == _mapPolygon->count() - 2); } +void QGCMapPolygonTest::_testCenterRectangle() +{ + // Simple rectangle — centroid should be the geometric center + const QGeoCoordinate topLeft(47.636, -122.093); + const QGeoCoordinate topRight(47.636, -122.085); + const QGeoCoordinate bottomRight(47.630, -122.085); + const QGeoCoordinate bottomLeft(47.630, -122.093); + + _mapPolygon->appendVertices({topLeft, topRight, bottomRight, bottomLeft}); + QCoreApplication::processEvents(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + const QGeoCoordinate center = _mapPolygon->center(); + + QVERIFY(center.isValid()); + QCOMPARE_COORDS(center, expectedCenter); +} + +void QGCMapPolygonTest::_testCenterExtraVertex() +{ + // Rectangle with an extra vertex at the midpoint of one side. + // The shape is identical to the rectangle so the area centroid should be the same, + // but a naive vertex average would shift toward the side with the extra vertex. + const QGeoCoordinate topLeft(47.636, -122.093); + const QGeoCoordinate topMid(47.636, -122.089); // midpoint of top edge + const QGeoCoordinate topRight(47.636, -122.085); + const QGeoCoordinate bottomRight(47.630, -122.085); + const QGeoCoordinate bottomLeft(47.630, -122.093); + + _mapPolygon->appendVertices({topLeft, topMid, topRight, bottomRight, bottomLeft}); + QCoreApplication::processEvents(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + const QGeoCoordinate center = _mapPolygon->center(); + + QVERIFY(center.isValid()); + QCOMPARE_COORDS(center, expectedCenter); +} + +void QGCMapPolygonTest::_testCenterDegenerate() +{ + // Three collinear points — zero area polygon. + // The surveyor's formula produces zero area, so the fallback vertex average should be used. + const QGeoCoordinate left(47.633, -122.093); + const QGeoCoordinate middle(47.633, -122.089); + const QGeoCoordinate right(47.633, -122.085); + + _mapPolygon->appendVertices({left, middle, right}); + QCoreApplication::processEvents(); + + const QGeoCoordinate center = _mapPolygon->center(); + + const QGeoCoordinate expectedCenter(47.633, -122.089); + + QVERIFY(center.isValid()); + // Vertex average: latitude stays 47.633, longitude = mean of the three + QCOMPARE_COORDS(center, expectedCenter); +} + #include "UnitTest.h" UT_REGISTER_TEST(QGCMapPolygonTest, TestLabel::Unit, TestLabel::MissionManager) diff --git a/test/MissionManager/QGCMapPolygonTest.h b/test/MissionManager/QGCMapPolygonTest.h index 10fcc6295a3a..eed49f856bef 100644 --- a/test/MissionManager/QGCMapPolygonTest.h +++ b/test/MissionManager/QGCMapPolygonTest.h @@ -23,6 +23,9 @@ private slots: void _testKMLLoad(); void _testSelectVertex(); void _testSegmentSplit(); + void _testCenterRectangle(); + void _testCenterExtraVertex(); + void _testCenterDegenerate(); private: MultiSignalSpy* _multiSpyPolygon = nullptr; diff --git a/test/MissionManager/SimpleMissionItemTest.cc b/test/MissionManager/SimpleMissionItemTest.cc index 1644a3ad7d95..b75002e895d1 100644 --- a/test/MissionManager/SimpleMissionItemTest.cc +++ b/test/MissionManager/SimpleMissionItemTest.cc @@ -57,19 +57,19 @@ void SimpleMissionItemTest::_testEditorFactsWorker(QGCMAVLink::VehicleClass_t ve MAV_CMD command; MAV_FRAME frame; double altValue; - QGroundControlQmlGlobal::AltMode altMode; + QGroundControlQmlGlobal::AltitudeFrame altFrame; } TestCase_t; TestCase_t testCases[] = { {MAV_CMD_NAV_WAYPOINT, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, - QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_LOITER_UNLIM, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, + QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_LOITER_UNLIM, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, {MAV_CMD_NAV_LOITER_TURNS, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, - QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_LOITER_TIME, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, - {MAV_CMD_NAV_LAND, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, QGroundControlQmlGlobal::AltitudeModeRelative}, - {MAV_CMD_NAV_TAKEOFF, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeModeAbsolute}, - {MAV_CMD_DO_JUMP, MAV_FRAME_MISSION, qQNaN(), QGroundControlQmlGlobal::AltitudeModeRelative}, + QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_LOITER_TIME, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, + {MAV_CMD_NAV_LAND, MAV_FRAME_GLOBAL_RELATIVE_ALT, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameRelative}, + {MAV_CMD_NAV_TAKEOFF, MAV_FRAME_GLOBAL, 70.1234567, QGroundControlQmlGlobal::AltitudeFrameAbsolute}, + {MAV_CMD_DO_JUMP, MAV_FRAME_MISSION, qQNaN(), QGroundControlQmlGlobal::AltitudeFrameRelative}, }; PlanMasterController planController(MAV_AUTOPILOT_PX4, QGCMAVLink::vehicleClassToMavType(vehicleClass)); QGCMAVLink::VehicleClass_t commandVehicleClass = @@ -167,7 +167,7 @@ void SimpleMissionItemTest::_testEditorFactsWorker(QGCMAVLink::VehicleClass_t ve QCOMPARE(fact->rawValue().toDouble(), (cExpectedAdvancedNaNFieldInfo[j].first * 10.0) + 0.1234567); } if (!qIsNaN(testCase.altValue)) { - QCOMPARE(simpleMissionItem.altitudeMode(), testCase.altMode); + QCOMPARE(simpleMissionItem.altitudeFrame(), testCase.altFrame); QCOMPARE(simpleMissionItem.altitude()->rawValue().toDouble(), testCase.altValue); } } @@ -203,14 +203,14 @@ void SimpleMissionItemTest::_testSignals() // Check that actually changing coordinate signals correctly _simpleItem->setCoordinate(QGeoCoordinate(missionItem.param5() + 1, missionItem.param6(), missionItem.param7())); QVERIFY(_spyVisualItem->onlyEmittedOnceByMask( - _spyVisualItem->mask("coordinateChanged", "exitCoordinateChanged", "dirtyChanged", "amslEntryAltChanged", - "amslExitAltChanged", "terrainAltitudeChanged"))); + _spyVisualItem->mask("coordinateChanged", "entryCoordinateChanged", "exitCoordinateChanged", "dirtyChanged", + "amslEntryAltChanged", "amslExitAltChanged", "terrainAltitudeChanged"))); _spyVisualItem->clearAllSignals(); _spySimpleItem->clearAllSignals(); _simpleItem->setCoordinate(QGeoCoordinate(missionItem.param5(), missionItem.param6() + 1, missionItem.param7())); QVERIFY(_spyVisualItem->onlyEmittedOnceByMask( - _spyVisualItem->mask("coordinateChanged", "exitCoordinateChanged", "dirtyChanged", "amslEntryAltChanged", - "amslExitAltChanged", "terrainAltitudeChanged"))); + _spyVisualItem->mask("coordinateChanged", "entryCoordinateChanged", "exitCoordinateChanged", "dirtyChanged", + "amslEntryAltChanged", "amslExitAltChanged", "terrainAltitudeChanged"))); _spyVisualItem->clearAllSignals(); _spySimpleItem->clearAllSignals(); // Altitude in coordinate is not used in setCoordinate @@ -240,12 +240,12 @@ void SimpleMissionItemTest::_testSignals() missionItem.setParam1(missionItem.param4() + 1); QVERIFY(_spyVisualItem->emitted("dirtyChanged")); _spyVisualItem->clearAllSignals(); - // Changing altitude mode should emit these signals (may also emit other related signals) - _simpleItem->setAltitudeMode(_simpleItem->altitudeMode() == QGroundControlQmlGlobal::AltitudeModeRelative - ? QGroundControlQmlGlobal::AltitudeModeAbsolute - : QGroundControlQmlGlobal::AltitudeModeRelative); + // Changing altitude frame should emit these signals (may also emit other related signals) + _simpleItem->setAltitudeFrame(_simpleItem->altitudeFrame() == QGroundControlQmlGlobal::AltitudeFrameRelative + ? QGroundControlQmlGlobal::AltitudeFrameAbsolute + : QGroundControlQmlGlobal::AltitudeFrameRelative); QVERIFY(_spySimpleItem->emittedByMask( - _spySimpleItem->mask("dirtyChanged", "friendlyEditAllowedChanged", "altitudeModeChanged"))); + _spySimpleItem->mask("dirtyChanged", "friendlyEditAllowedChanged", "altitudeFrameChanged"))); _spySimpleItem->clearAllSignals(); _spyVisualItem->clearAllSignals(); // Check commandChanged signalling. Call setCommand should trigger: @@ -318,14 +318,58 @@ void SimpleMissionItemTest::_testSpeedSection() void SimpleMissionItemTest::_testAltitudePropogation() { // Make sure that changes to altitude propogate to param 7 of the mission item - _simpleItem->setAltitudeMode(QGroundControlQmlGlobal::AltitudeModeRelative); + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameRelative); _simpleItem->altitude()->setRawValue(_simpleItem->altitude()->rawValue().toDouble() + 1); QCOMPARE(_simpleItem->altitude()->rawValue().toDouble(), _simpleItem->missionItem().param7()); QCOMPARE(_simpleItem->missionItem().frame(), MAV_FRAME_GLOBAL_RELATIVE_ALT); - _simpleItem->setAltitudeMode(QGroundControlQmlGlobal::AltitudeModeAbsolute); + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameAbsolute); _simpleItem->altitude()->setRawValue(_simpleItem->altitude()->rawValue().toDouble() + 1); QCOMPARE(_simpleItem->altitude()->rawValue().toDouble(), _simpleItem->missionItem().param7()); QCOMPARE(_simpleItem->missionItem().frame(), MAV_FRAME_GLOBAL); } +void SimpleMissionItemTest::_testCalcAboveTerrainSaveLoad() +{ + // Regression test for https://github.com/mavlink/qgroundcontrol/issues/13513 + // When saving/loading a mission with CalcAboveTerrain altitude mode, the AMSL altitude + // must be preserved correctly rather than being overwritten with the above-terrain value. + + const double terrainAlt = 500.0; // Terrain elevation at the waypoint + const double aboveTerrainAlt = 250.0; // User-specified height above terrain + const double amslAlt = terrainAlt + aboveTerrainAlt; + + // Setup the item with CalcAboveTerrain mode and simulate terrain calculation having completed + _simpleItem->setAltitudeFrame(QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain); + _simpleItem->altitude()->setRawValue(aboveTerrainAlt); + _simpleItem->amslAltAboveTerrain()->setRawValue(amslAlt); + // For CalcAboveTerrain, param7 should be the AMSL altitude + _simpleItem->missionItem().setParam7(amslAlt); + + // Save + QJsonArray missionItems; + _simpleItem->save(missionItems); + QVERIFY(missionItems.count() > 0); + QJsonObject savedJson = missionItems[0].toObject(); + + // Verify saved JSON contains the correct altitude data + QVERIFY(savedJson.contains("AltitudeMode")); + QVERIFY(savedJson.contains("Altitude")); + QVERIFY(savedJson.contains("AMSLAltAboveTerrain")); + QCOMPARE(savedJson["AltitudeMode"].toInt(), static_cast(QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain)); + QCOMPARE(savedJson["Altitude"].toDouble(), aboveTerrainAlt); + QCOMPARE(savedJson["AMSLAltAboveTerrain"].toDouble(), amslAlt); + + // Load into a new SimpleMissionItem (forLoad=true matches production MissionController behavior) + QString errorString; + SimpleMissionItem loadedItem(planController(), false /* flyView */, true /* forLoad */); + QVERIFY(loadedItem.load(savedJson, 1, errorString)); + + // Verify state immediately after load — before any terrain query + QCOMPARE(loadedItem.altitudeFrame(), QGroundControlQmlGlobal::AltitudeFrameCalcAboveTerrain); + QCOMPARE(loadedItem.altitude()->rawValue().toDouble(), aboveTerrainAlt); + QCOMPARE(loadedItem.amslAltAboveTerrain()->rawValue().toDouble(), amslAlt); + QCOMPARE(loadedItem.missionItem().frame(), MAV_FRAME_GLOBAL); + QCOMPARE(loadedItem.missionItem().param7(), amslAlt); +} + UT_REGISTER_TEST(SimpleMissionItemTest, TestLabel::Unit, TestLabel::MissionManager) diff --git a/test/MissionManager/SimpleMissionItemTest.h b/test/MissionManager/SimpleMissionItemTest.h index f687e0fa6224..59d438c344af 100644 --- a/test/MissionManager/SimpleMissionItemTest.h +++ b/test/MissionManager/SimpleMissionItemTest.h @@ -28,6 +28,7 @@ private slots: void _testCameraSection(); void _testSpeedSection(); void _testAltitudePropogation(); + void _testCalcAboveTerrainSaveLoad(); private: void _testEditorFactsWorker(QGCMAVLink::VehicleClass_t vehicleClass, QGCMAVLink::VehicleClass_t vtolMode); diff --git a/test/QmlControls/CMakeLists.txt b/test/QmlControls/CMakeLists.txt index 8ce8cfd01c64..6cb5355a5fac 100644 --- a/test/QmlControls/CMakeLists.txt +++ b/test/QmlControls/CMakeLists.txt @@ -4,8 +4,14 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE + ObjectItemModelBaseTest.cc + ObjectItemModelBaseTest.h + ObjectListModelBaseTest.cc + ObjectListModelBaseTest.h QmlObjectListModelTest.cc QmlObjectListModelTest.h + QmlObjectTreeModelTest.cc + QmlObjectTreeModelTest.h ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/QmlControls/ObjectItemModelBaseTest.cc b/test/QmlControls/ObjectItemModelBaseTest.cc new file mode 100644 index 000000000000..1ce860591c4b --- /dev/null +++ b/test/QmlControls/ObjectItemModelBaseTest.cc @@ -0,0 +1,264 @@ +#include "ObjectItemModelBaseTest.h" + +#include "ObjectItemModelBase.h" + +#include + +// --------------------------------------------------------------------------- +// Concrete subclass — ObjectItemModelBase is abstract, so we need a minimal +// implementation to test the base-class logic. +// --------------------------------------------------------------------------- +namespace { + +class ConcreteItemModel : public ObjectItemModelBase +{ + Q_OBJECT + +public: + explicit ConcreteItemModel(QObject* parent = nullptr) + : ObjectItemModelBase(parent) + { + } + + // -- ObjectItemModelBase pure-virtuals -- + int count() const override { return _count; } + void setDirty(bool dirty) override + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + void clear() override + { + beginResetModel(); + _count = 0; + endResetModel(); + } + + // -- QAbstractItemModel pure-virtuals -- + QModelIndex index(int row, int column = 0, const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + if (row < 0 || row >= _count || column != 0) return {}; + return createIndex(row, 0); + } + QModelIndex parent(const QModelIndex& child) const override + { + Q_UNUSED(child); + return {}; + } + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return _count; + } + int columnCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent); + return 1; + } + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + Q_UNUSED(index); Q_UNUSED(role); + return {}; + } + + // Helper to manipulate count from tests + void setCount(int c) { _count = c; } + + // Expose protected helpers for testing + using ObjectItemModelBase::_signalCountChangedIfNotNested; + using ObjectItemModelBase::_childDirtyChanged; + using ObjectItemModelBase::roleNames; + +private: + int _count = 0; +}; + +class DirtyObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + +public: + explicit DirtyObject(QObject* parent = nullptr) : QObject(parent) {} + + bool dirty() const { return _dirty; } + void setDirty(bool dirty) + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + +signals: + void dirtyChanged(bool dirty); + +private: + bool _dirty = false; +}; + +} // namespace + +// =========================================================================== +// Nested beginResetModel / endResetModel +// =========================================================================== + +void ObjectItemModelBaseTest::_singleResetCallsBaseOnce() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_nestedResetTwoDeep() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); // nested — should NOT emit again + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); // back to depth 1 — should NOT emit yet + QCOMPARE(resetEndSpy.count(), 0); + + model.endResetModel(); // depth 0 — now emit + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_nestedResetThreeDeep() +{ + ConcreteItemModel model; + + QSignalSpy resetBeginSpy(&model, &QAbstractItemModel::modelAboutToBeReset); + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); + model.beginResetModel(); // depth 3 + QCOMPARE(resetBeginSpy.count(), 1); + + model.endResetModel(); // depth 2 + model.endResetModel(); // depth 1 + QCOMPARE(resetEndSpy.count(), 0); + + model.endResetModel(); // depth 0 — emit + QCOMPARE(resetEndSpy.count(), 1); +} + +void ObjectItemModelBaseTest::_endResetWithoutBeginLogsWarning() +{ + ConcreteItemModel model; + + QSignalSpy resetEndSpy(&model, &QAbstractItemModel::modelReset); + + // Should just log a warning and not crash + model.endResetModel(); + QCOMPARE(resetEndSpy.count(), 0); +} + +// =========================================================================== +// Count signal suppression during reset +// =========================================================================== + +void ObjectItemModelBaseTest::_countChangedSuppressedInsideReset() +{ + ConcreteItemModel model; + model.setCount(5); + + QSignalSpy countSpy(&model, &ObjectItemModelBase::countChanged); + + model.beginResetModel(); + + // _signalCountChangedIfNotNested should be suppressed while in reset + model._signalCountChangedIfNotNested(); + QCOMPARE(countSpy.count(), 0); + + model.endResetModel(); + // endResetModel itself emits countChanged + QCOMPARE(countSpy.count(), 1); + QCOMPARE(countSpy.takeFirst().at(0).toInt(), 5); +} + +void ObjectItemModelBaseTest::_countChangedEmittedOnEndReset() +{ + ConcreteItemModel model; + model.setCount(3); + + model.beginResetModel(); + model.setCount(7); + + QSignalSpy countSpy(&model, &ObjectItemModelBase::countChanged); + + model.endResetModel(); + QCOMPARE(countSpy.count(), 1); + QCOMPARE(countSpy.takeFirst().at(0).toInt(), 7); +} + +// =========================================================================== +// Dirty tracking via _childDirtyChanged +// =========================================================================== + +void ObjectItemModelBaseTest::_childDirtySetsDirtyTrue() +{ + ConcreteItemModel model; + QCOMPARE(model.dirty(), false); + + model._childDirtyChanged(true); + QCOMPARE(model.dirty(), true); +} + +void ObjectItemModelBaseTest::_childDirtyDoesNotClearDirty() +{ + ConcreteItemModel model; + model._childDirtyChanged(true); + QCOMPARE(model.dirty(), true); + + // _childDirtyChanged(false) should NOT reset the model's dirty flag + // because _dirty |= dirty means it's sticky + model._childDirtyChanged(false); + QCOMPARE(model.dirty(), true); +} + +void ObjectItemModelBaseTest::_childDirtyEmitsDirtyChanged() +{ + ConcreteItemModel model; + + QSignalSpy dirtySpy(&model, &ObjectItemModelBase::dirtyChanged); + + model._childDirtyChanged(true); + QCOMPARE(dirtySpy.count(), 1); + QCOMPARE(dirtySpy.takeFirst().at(0).toBool(), true); + + // Calling again with true — dirty is already true, |= true is still true, + // but the signal is still emitted each time _childDirtyChanged is called + model._childDirtyChanged(true); + QCOMPARE(dirtySpy.count(), 1); +} + +// =========================================================================== +// roleNames +// =========================================================================== + +void ObjectItemModelBaseTest::_roleNamesContainObjectAndText() +{ + ConcreteItemModel model; + const auto roles = model.roleNames(); + + QVERIFY(roles.values().contains("object")); + QVERIFY(roles.values().contains("text")); +} + +UT_REGISTER_TEST(ObjectItemModelBaseTest, TestLabel::Unit) + +#include "ObjectItemModelBaseTest.moc" diff --git a/test/QmlControls/ObjectItemModelBaseTest.h b/test/QmlControls/ObjectItemModelBaseTest.h new file mode 100644 index 000000000000..f5c82ac4d63b --- /dev/null +++ b/test/QmlControls/ObjectItemModelBaseTest.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "UnitTest.h" + +class ObjectItemModelBaseTest : public UnitTest +{ + Q_OBJECT + +private slots: + // Nested beginResetModel / endResetModel + void _singleResetCallsBaseOnce(); + void _nestedResetTwoDeep(); + void _nestedResetThreeDeep(); + void _endResetWithoutBeginLogsWarning(); + + // Count signal suppression during reset + void _countChangedSuppressedInsideReset(); + void _countChangedEmittedOnEndReset(); + + // Dirty tracking via _childDirtyChanged + void _childDirtySetsDirtyTrue(); + void _childDirtyDoesNotClearDirty(); + void _childDirtyEmitsDirtyChanged(); + + // roleNames + void _roleNamesContainObjectAndText(); +}; diff --git a/test/QmlControls/ObjectListModelBaseTest.cc b/test/QmlControls/ObjectListModelBaseTest.cc new file mode 100644 index 000000000000..6d10ad25a9e8 --- /dev/null +++ b/test/QmlControls/ObjectListModelBaseTest.cc @@ -0,0 +1,139 @@ +#include "ObjectListModelBaseTest.h" + +#include "QmlObjectListModel.h" + +// =========================================================================== +// index() overrides +// =========================================================================== + +void ObjectListModelBaseTest::_indexValidRow() +{ + QmlObjectListModel model; + QObject a, b; + model.append(&a); + model.append(&b); + + const QModelIndex idx0 = model.index(0, 0); + QVERIFY(idx0.isValid()); + QCOMPARE(idx0.row(), 0); + QCOMPARE(idx0.column(), 0); + + const QModelIndex idx1 = model.index(1, 0); + QVERIFY(idx1.isValid()); + QCOMPARE(idx1.row(), 1); +} + +void ObjectListModelBaseTest::_indexInvalidRow() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Negative row + QVERIFY(!model.index(-1, 0).isValid()); + + // Beyond count + QVERIFY(!model.index(1, 0).isValid()); + QVERIFY(!model.index(100, 0).isValid()); +} + +void ObjectListModelBaseTest::_indexNonZeroColumnInvalid() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Column must be 0 for a list model + QVERIFY(!model.index(0, 1).isValid()); + QVERIFY(!model.index(0, 2).isValid()); +} + +void ObjectListModelBaseTest::_indexWithValidParentInvalid() +{ + QmlObjectListModel model; + QObject a, b; + model.append(&a); + model.append(&b); + + const QModelIndex parentIdx = model.index(0, 0); + QVERIFY(parentIdx.isValid()); + + // A flat list model should return invalid for any child of a valid parent + QVERIFY(!model.index(0, 0, parentIdx).isValid()); +} + +// =========================================================================== +// parent() override +// =========================================================================== + +void ObjectListModelBaseTest::_parentAlwaysInvalid() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + const QModelIndex idx = model.index(0, 0); + QVERIFY(idx.isValid()); + + // parent() should always return invalid for a flat list + QVERIFY(!model.parent(idx).isValid()); +} + +// =========================================================================== +// columnCount() override +// =========================================================================== + +void ObjectListModelBaseTest::_columnCountAlwaysOne() +{ + QmlObjectListModel model; + + // Empty model + QCOMPARE(model.columnCount(), 1); + QCOMPARE(model.columnCount(QModelIndex()), 1); + + // With items + QObject a; + model.append(&a); + QCOMPARE(model.columnCount(), 1); + + // With a valid parent index — still 1 + const QModelIndex idx = model.index(0, 0); + QCOMPARE(model.columnCount(idx), 1); +} + +// =========================================================================== +// hasChildren() override +// =========================================================================== + +void ObjectListModelBaseTest::_hasChildrenRootEmpty() +{ + QmlObjectListModel model; + + // Root of an empty list has no children + QVERIFY(!model.hasChildren(QModelIndex())); +} + +void ObjectListModelBaseTest::_hasChildrenRootWithItems() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + // Root of a non-empty list has children + QVERIFY(model.hasChildren(QModelIndex())); +} + +void ObjectListModelBaseTest::_hasChildrenNonRootAlwaysFalse() +{ + QmlObjectListModel model; + QObject a; + model.append(&a); + + const QModelIndex idx = model.index(0, 0); + QVERIFY(idx.isValid()); + + // Non-root item should never have children in a flat list + QVERIFY(!model.hasChildren(idx)); +} + +UT_REGISTER_TEST(ObjectListModelBaseTest, TestLabel::Unit) diff --git a/test/QmlControls/ObjectListModelBaseTest.h b/test/QmlControls/ObjectListModelBaseTest.h new file mode 100644 index 000000000000..0bcf270207e4 --- /dev/null +++ b/test/QmlControls/ObjectListModelBaseTest.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include "UnitTest.h" + +class ObjectListModelBaseTest : public UnitTest +{ + Q_OBJECT + +private slots: + // index() overrides + void _indexValidRow(); + void _indexInvalidRow(); + void _indexNonZeroColumnInvalid(); + void _indexWithValidParentInvalid(); + + // parent() override + void _parentAlwaysInvalid(); + + // columnCount() override + void _columnCountAlwaysOne(); + + // hasChildren() override + void _hasChildrenRootEmpty(); + void _hasChildrenRootWithItems(); + void _hasChildrenNonRootAlwaysFalse(); +}; diff --git a/test/QmlControls/QmlObjectTreeModelTest.cc b/test/QmlControls/QmlObjectTreeModelTest.cc new file mode 100644 index 000000000000..b31d5c29fb1d --- /dev/null +++ b/test/QmlControls/QmlObjectTreeModelTest.cc @@ -0,0 +1,764 @@ +#include "QmlObjectTreeModelTest.h" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// DirtyObject — minimal QObject with a dirty property for signal testing +// --------------------------------------------------------------------------- +namespace { + +class DirtyObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool dirty READ dirty WRITE setDirty NOTIFY dirtyChanged) + +public: + explicit DirtyObject(QObject* parent = nullptr) : QObject(parent) {} + + bool dirty() const { return _dirty; } + void setDirty(bool dirty) + { + if (_dirty == dirty) return; + _dirty = dirty; + emit dirtyChanged(_dirty); + } + +signals: + void dirtyChanged(bool dirty); + +private: + bool _dirty = false; +}; + +} // namespace + +// =========================================================================== +// Construction & empty state +// =========================================================================== + +void QmlObjectTreeModelTest::_emptyModelDefaults() +{ + QmlObjectTreeModel model; + QCOMPARE(model.count(), 0); + QCOMPARE(model.rowCount(), 0); + QVERIFY(model.isEmpty()); + QCOMPARE(model.dirty(), false); +} + +void QmlObjectTreeModelTest::_roleNamesIncludeNodeType() +{ + QmlObjectTreeModel model; + const auto roles = model.roleNames(); + QVERIFY(roles.values().contains("object")); + QVERIFY(roles.values().contains("text")); + QVERIFY(roles.values().contains("nodeType")); + QVERIFY(roles.values().contains("separator")); +} + +// =========================================================================== +// Insert / Append +// =========================================================================== + +void QmlObjectTreeModelTest::_appendToRoot() +{ + QmlObjectTreeModel model; + QObject obj; + obj.setObjectName("item1"); + + const QModelIndex idx = model.appendItem(&obj); + QVERIFY(idx.isValid()); + QCOMPARE(idx.row(), 0); + QCOMPARE(model.count(), 1); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.getObject(idx), &obj); +} + +void QmlObjectTreeModelTest::_appendWithNodeType() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.appendItem(&obj, QModelIndex(), QStringLiteral("myType")); + QVERIFY(idx.isValid()); + QCOMPARE(model.data(idx, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("myType")); +} + +void QmlObjectTreeModelTest::_insertAtPositions() +{ + QmlObjectTreeModel model; + QObject a, b, c; + a.setObjectName("a"); + b.setObjectName("b"); + c.setObjectName("c"); + + model.appendItem(&b); // [b] + model.insertItem(0, &a); // [a, b] + model.insertItem(2, &c); // [a, b, c] + + QCOMPARE(model.rowCount(), 3); + QCOMPARE(model.getObject(model.index(0, 0)), &a); + QCOMPARE(model.getObject(model.index(1, 0)), &b); + QCOMPARE(model.getObject(model.index(2, 0)), &c); +} + +void QmlObjectTreeModelTest::_appendToChild() +{ + QmlObjectTreeModel model; + QObject parent, child; + + const QModelIndex parentIdx = model.appendItem(&parent); + const QModelIndex childIdx = model.appendItem(&child, parentIdx); + + QVERIFY(childIdx.isValid()); + QCOMPARE(model.rowCount(parentIdx), 1); + QCOMPARE(model.getObject(childIdx), &child); + QCOMPARE(model.count(), 2); // parent + child +} + +void QmlObjectTreeModelTest::_insertInvalidParentLogsWarning() +{ + QmlObjectTreeModel model; + QObject obj; + + // Create a stale index from another model — will be invalid for this model + const QModelIndex badIndex = model.index(999, 0); + QVERIFY(!badIndex.isValid()); + + // appendItem with invalid index -> inserts at root (valid behavior) + const QModelIndex idx = model.appendItem(&obj, badIndex); + // Should still succeed since invalid parent = root + QVERIFY(idx.isValid()); +} + +void QmlObjectTreeModelTest::_insertOutOfRangeLogsWarning() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.insertItem(5, &obj); // row 5, but 0 children + QVERIFY(!idx.isValid()); // should fail gracefully + QCOMPARE(model.count(), 0); +} + +// =========================================================================== +// Remove +// =========================================================================== + +void QmlObjectTreeModelTest::_removeItemReturnsObject() +{ + QmlObjectTreeModel model; + QObject obj; + + const QModelIndex idx = model.appendItem(&obj); + QObject* removed = model.removeItem(idx); + + QCOMPARE(removed, &obj); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_removeSubtreeDecrementsCount() +{ + QmlObjectTreeModel model; + QObject parentObj, child1, child2, grandchild; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + const QModelIndex child1Idx = model.appendItem(&child1, parentIdx); + model.appendItem(&child2, parentIdx); + model.appendItem(&grandchild, child1Idx); + + QCOMPARE(model.count(), 4); + + // Remove parentObj — should remove parent + 2 children + 1 grandchild = 4 + model.removeItem(parentIdx); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_removeAtConvenience() +{ + QmlObjectTreeModel model; + QObject a, b; + + model.appendItem(&a); + model.appendItem(&b); + + QObject* removed = model.removeAt(QModelIndex(), 0); + QCOMPARE(removed, &a); + QCOMPARE(model.count(), 1); +} + +void QmlObjectTreeModelTest::_removeChildrenKeepsParent() +{ + QmlObjectTreeModel model; + QObject parentObj, child1, child2; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + model.appendItem(&child1, parentIdx); + model.appendItem(&child2, parentIdx); + + QCOMPARE(model.count(), 3); + model.removeChildren(parentIdx); + QCOMPARE(model.rowCount(parentIdx), 0); + QCOMPARE(model.count(), 1); // parent still there + QVERIFY(model.contains(&parentObj)); +} + +void QmlObjectTreeModelTest::_removeInvalidIndexReturnsNull() +{ + QmlObjectTreeModel model; + QObject* removed = model.removeItem(QModelIndex()); + QVERIFY(removed == nullptr); +} + +// =========================================================================== +// Count cache +// =========================================================================== + +void QmlObjectTreeModelTest::_countStaysConsistentAfterMixedOps() +{ + QmlObjectTreeModel model; + QObject objects[10]; + + // Insert 10 + for (int i = 0; i < 10; i++) { + model.appendItem(&objects[i]); + } + QCOMPARE(model.count(), 10); + + // Remove 5 (from end to avoid index shifting) + for (int i = 9; i >= 5; i--) { + model.removeAt(QModelIndex(), i); + } + QCOMPARE(model.count(), 5); + + // Add 3 more + QObject extras[3]; + for (int i = 0; i < 3; i++) { + model.appendItem(&extras[i]); + } + QCOMPARE(model.count(), 8); + + // Clear + model.clear(); + QCOMPARE(model.count(), 0); +} + +void QmlObjectTreeModelTest::_countReflectsNestedChildren() +{ + QmlObjectTreeModel model; + QObject root, c1, c2, gc1, gc2, gc3; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex c1Idx = model.appendItem(&c1, rootIdx); + model.appendItem(&c2, rootIdx); + model.appendItem(&gc1, c1Idx); + model.appendItem(&gc2, c1Idx); + model.appendItem(&gc3, c1Idx); + + QCOMPARE(model.count(), 6); // root + 2 children + 3 grandchildren +} + +// =========================================================================== +// Clear +// =========================================================================== + +void QmlObjectTreeModelTest::_clearResetsToEmpty() +{ + QmlObjectTreeModel model; + QObject a, b, c; + model.appendItem(&a); + model.appendItem(&b); + model.appendItem(&c); + + model.clear(); + QCOMPARE(model.count(), 0); + QCOMPARE(model.rowCount(), 0); + QVERIFY(model.isEmpty()); +} + +void QmlObjectTreeModelTest::_clearAndDeleteContentsDeletesObjects() +{ + QmlObjectTreeModel model; + + auto* obj1 = new QObject; + auto* obj2 = new QObject; + + model.appendItem(obj1); + model.appendItem(obj2); + + QSignalSpy destroyed1(obj1, &QObject::destroyed); + QSignalSpy destroyed2(obj2, &QObject::destroyed); + + model.clearAndDeleteContents(); + QCOMPARE(model.count(), 0); + + // deleteLater requires the event loop to process deferred deletions + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + QCOMPARE(destroyed1.count(), 1); + QCOMPARE(destroyed2.count(), 1); +} + +void QmlObjectTreeModelTest::_clearOnEmptyIsNoop() +{ + QmlObjectTreeModel model; + QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset); + model.clear(); + QCOMPARE(resetSpy.count(), 0); // no-op — no reset emitted +} + +// =========================================================================== +// Tree navigation +// =========================================================================== + +void QmlObjectTreeModelTest::_parentOfRootChildIsInvalid() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + QVERIFY(!model.parent(idx).isValid()); +} + +void QmlObjectTreeModelTest::_parentOfNestedChild() +{ + QmlObjectTreeModel model; + QObject parentObj, childObj; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + const QModelIndex childIdx = model.appendItem(&childObj, parentIdx); + + const QModelIndex retrievedParent = model.parent(childIdx); + QVERIFY(retrievedParent.isValid()); + QCOMPARE(model.getObject(retrievedParent), &parentObj); +} + +void QmlObjectTreeModelTest::_depthValues() +{ + QmlObjectTreeModel model; + QObject root, child, grandchild; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex childIdx = model.appendItem(&child, rootIdx); + const QModelIndex gcIdx = model.appendItem(&grandchild, childIdx); + + QCOMPARE(model.depth(rootIdx), 0); + QCOMPARE(model.depth(childIdx), 1); + QCOMPARE(model.depth(gcIdx), 2); + QCOMPARE(model.depth(QModelIndex()), -1); // invalid +} + +void QmlObjectTreeModelTest::_indexForObjectFindsNested() +{ + QmlObjectTreeModel model; + QObject root, child, grandchild; + + const QModelIndex rootIdx = model.appendItem(&root); + const QModelIndex childIdx = model.appendItem(&child, rootIdx); + model.appendItem(&grandchild, childIdx); + + const QModelIndex found = model.indexForObject(&grandchild); + QVERIFY(found.isValid()); + QCOMPARE(model.getObject(found), &grandchild); +} + +void QmlObjectTreeModelTest::_indexForObjectNotFound() +{ + QmlObjectTreeModel model; + QObject notInModel; + QVERIFY(!model.indexForObject(¬InModel).isValid()); + QVERIFY(!model.indexForObject(nullptr).isValid()); +} + +void QmlObjectTreeModelTest::_containsWorks() +{ + QmlObjectTreeModel model; + QObject inModel, notInModel; + model.appendItem(&inModel); + + QVERIFY(model.contains(&inModel)); + QVERIFY(!model.contains(¬InModel)); +} + +void QmlObjectTreeModelTest::_columnCountAlwaysOne() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QCOMPARE(model.columnCount(), 1); + QCOMPARE(model.columnCount(idx), 1); +} + +void QmlObjectTreeModelTest::_hasChildrenLeafVsParent() +{ + QmlObjectTreeModel model; + QObject parentObj, childObj; + + const QModelIndex parentIdx = model.appendItem(&parentObj); + QVERIFY(!model.hasChildren(parentIdx)); // no children yet + + const QModelIndex childIdx = model.appendItem(&childObj, parentIdx); + QVERIFY(model.hasChildren(parentIdx)); // now has children + QVERIFY(!model.hasChildren(childIdx)); // leaf + QVERIFY(model.hasChildren()); // root has children +} + +// =========================================================================== +// Data & SetData +// =========================================================================== + +void QmlObjectTreeModelTest::_dataObjectRole() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + const QVariant val = model.data(idx, Qt::UserRole); // ObjectRole + QCOMPARE(val.value(), &obj); +} + +void QmlObjectTreeModelTest::_dataTextRole() +{ + QmlObjectTreeModel model; + QObject obj; + obj.setObjectName("hello"); + const QModelIndex idx = model.appendItem(&obj); + + QCOMPARE(model.data(idx, Qt::UserRole + 1).toString(), QStringLiteral("hello")); // TextRole +} + +void QmlObjectTreeModelTest::_dataNodeTypeRole() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj, QModelIndex(), QStringLiteral("missionItem")); + + QCOMPARE(model.data(idx, QmlObjectTreeModel::NodeTypeRole).toString(), QStringLiteral("missionItem")); +} + +void QmlObjectTreeModelTest::_dataSeparatorRole() +{ + QmlObjectTreeModel model; + + // Build a parent with three leaf children + QObject parent; + QObject child1, child2, child3; + const QModelIndex parentIdx = model.appendItem(&parent, QModelIndex(), QStringLiteral("group")); + const QModelIndex c1 = model.appendItem(&child1, parentIdx, QStringLiteral("item")); + const QModelIndex c2 = model.appendItem(&child2, parentIdx, QStringLiteral("item")); + const QModelIndex c3 = model.appendItem(&child3, parentIdx, QStringLiteral("item")); + + // First two children are not last → separator = true + QCOMPARE(model.data(c1, QmlObjectTreeModel::SeparatorRole).toBool(), true); + QCOMPARE(model.data(c2, QmlObjectTreeModel::SeparatorRole).toBool(), true); + // Last child → separator = false + QCOMPARE(model.data(c3, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Parent node has children → separator = false regardless of position + QCOMPARE(model.data(parentIdx, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Remove last child — c2 is now last and should emit dataChanged with SeparatorRole + QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged); + model.removeItem(c3); + + // c2 should now be the last child → separator = false + const QModelIndex c2New = model.index(1, 0, parentIdx); + QCOMPARE(model.data(c2New, QmlObjectTreeModel::SeparatorRole).toBool(), false); + + // Verify dataChanged was emitted with SeparatorRole + bool foundSeparatorChange = false; + for (const auto& call : dataChangedSpy) { + const QList roles = call.at(2).value>(); + if (roles.contains(QmlObjectTreeModel::SeparatorRole)) { + foundSeparatorChange = true; + break; + } + } + QVERIFY(foundSeparatorChange); + + // Insert a new child at the end — c2 should flip back to separator = true + QObject child4; + model.appendItem(&child4, parentIdx, QStringLiteral("item")); + QCOMPARE(model.data(c2New, QmlObjectTreeModel::SeparatorRole).toBool(), true); +} + +void QmlObjectTreeModelTest::_dataInvalidIndexReturnsEmpty() +{ + QmlObjectTreeModel model; + QVERIFY(!model.data(QModelIndex(), Qt::UserRole).isValid()); +} + +void QmlObjectTreeModelTest::_setDataReplacesObject() +{ + QmlObjectTreeModel model; + QObject obj1, obj2; + + const QModelIndex idx = model.appendItem(&obj1); + QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged); + + bool ok = model.setData(idx, QVariant::fromValue(&obj2), Qt::UserRole); + QVERIFY(ok); + QCOMPARE(model.getObject(idx), &obj2); + QCOMPARE(dataChangedSpy.count(), 1); +} + +void QmlObjectTreeModelTest::_setDataNonObjectRoleReturnsFalse() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QVERIFY(!model.setData(idx, QVariant("test"), Qt::DisplayRole)); +} + +// =========================================================================== +// Dirty tracking +// =========================================================================== + +void QmlObjectTreeModelTest::_insertSetsDirty() +{ + QmlObjectTreeModel model; + model.setDirty(false); + + QObject obj; + model.appendItem(&obj); + QVERIFY(model.dirty()); +} + +void QmlObjectTreeModelTest::_childDirtyPropagates() +{ + QmlObjectTreeModel model; + DirtyObject obj; + + QSignalSpy dirtySpy(&model, &QmlObjectTreeModel::dirtyChanged); + QVERIFY(dirtySpy.isValid()); + + model.appendItem(&obj); + model.setDirty(false); + dirtySpy.clear(); + + obj.setDirty(true); + QCOMPARE(dirtySpy.count(), 1); + QCOMPARE(dirtySpy.takeFirst().at(0).toBool(), true); + QVERIFY(model.dirty()); +} + +void QmlObjectTreeModelTest::_removeDisconnectsDirty() +{ + QmlObjectTreeModel model; + DirtyObject obj; + + model.appendItem(&obj); + const QModelIndex idx = model.indexForObject(&obj); + model.removeItem(idx); + model.setDirty(false); + + QSignalSpy dirtySpy(&model, &QmlObjectTreeModel::dirtyChanged); + obj.setDirty(true); + + QCOMPARE(dirtySpy.count(), 0); + QVERIFY(!model.dirty()); +} + +// =========================================================================== +// Model signals +// =========================================================================== + +void QmlObjectTreeModelTest::_insertEmitsRowsInserted() +{ + QmlObjectTreeModel model; + QSignalSpy spy(&model, &QAbstractItemModel::rowsInserted); + + QObject obj; + model.appendItem(&obj); + + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(1).toInt(), 0); // first = 0 + QCOMPARE(spy.at(0).at(2).toInt(), 0); // last = 0 +} + +void QmlObjectTreeModelTest::_removeEmitsRowsRemoved() +{ + QmlObjectTreeModel model; + QObject obj; + const QModelIndex idx = model.appendItem(&obj); + + QSignalSpy spy(&model, &QAbstractItemModel::rowsRemoved); + model.removeItem(idx); + + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(1).toInt(), 0); // first = 0 + QCOMPARE(spy.at(0).at(2).toInt(), 0); // last = 0 +} + +void QmlObjectTreeModelTest::_clearEmitsModelReset() +{ + QmlObjectTreeModel model; + QObject obj; + model.appendItem(&obj); + + QSignalSpy spy(&model, &QAbstractItemModel::modelReset); + model.clear(); + + QCOMPARE(spy.count(), 1); +} + +void QmlObjectTreeModelTest::_countChangedEmitted() +{ + QmlObjectTreeModel model; + QSignalSpy spy(&model, &QmlObjectTreeModel::countChanged); + + QObject obj; + model.appendItem(&obj); + QVERIFY(spy.count() >= 1); + + spy.clear(); + model.removeItem(model.index(0, 0)); + QVERIFY(spy.count() >= 1); +} + +// =========================================================================== +// Nested reset +// =========================================================================== + +void QmlObjectTreeModelTest::_nestedResetOnlyEmitsOnce() +{ + QmlObjectTreeModel model; + QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset); + + model.beginResetModel(); + model.beginResetModel(); // nested — should NOT emit again + + QObject a, b; + model.appendItem(&a); + model.appendItem(&b); + + model.endResetModel(); // inner — no-op + model.endResetModel(); // outer — actually emits + + QCOMPARE(resetSpy.count(), 1); +} + +void QmlObjectTreeModelTest::_insertDuringResetNoSignals() +{ + QmlObjectTreeModel model; + model.beginResetModel(); + + QSignalSpy insertSpy(&model, &QAbstractItemModel::rowsInserted); + QSignalSpy countSpy(&model, &QmlObjectTreeModel::countChanged); + + QObject obj; + model.appendItem(&obj); + + QCOMPARE(insertSpy.count(), 0); // suppressed during reset + QCOMPARE(countSpy.count(), 0); // suppressed during reset + + model.endResetModel(); + QCOMPARE(model.count(), 1); // but count is correct +} + +// =========================================================================== +// insertRows / removeRows guard +// =========================================================================== + +void QmlObjectTreeModelTest::_insertRowsReturnsFalse() +{ + QmlObjectTreeModel model; + QVERIFY(!model.insertRows(0, 1)); +} + +void QmlObjectTreeModelTest::_removeRowsReturnsFalse() +{ + QmlObjectTreeModel model; + QVERIFY(!model.removeRows(0, 1)); +} + +// =========================================================================== +// QPersistentModelIndex stability +// =========================================================================== + +void QmlObjectTreeModelTest::_persistentIndexSurvivesInsertBefore() +{ + QmlObjectTreeModel model; + QObject a, b; + + model.appendItem(&a); + QPersistentModelIndex persistentIdx(model.index(0, 0)); + QCOMPARE(model.getObject(persistentIdx), &a); + + // Insert before — persistent index should shift to row 1 + model.insertItem(0, &b); + QVERIFY(persistentIdx.isValid()); + QCOMPARE(persistentIdx.row(), 1); + QCOMPARE(model.getObject(persistentIdx), &a); +} + +void QmlObjectTreeModelTest::_persistentIndexSurvivesRemoveOther() +{ + QmlObjectTreeModel model; + QObject a, b, c; + + model.appendItem(&a); + model.appendItem(&b); + model.appendItem(&c); + + QPersistentModelIndex persistentIdx(model.index(2, 0)); // points to c + QCOMPARE(model.getObject(persistentIdx), &c); + + // Remove a (row 0) — persistent should shift to row 1 + model.removeAt(QModelIndex(), 0); + QVERIFY(persistentIdx.isValid()); + QCOMPARE(persistentIdx.row(), 1); + QCOMPARE(model.getObject(persistentIdx), &c); +} + +// =========================================================================== +// Null object edge case +// =========================================================================== + +void QmlObjectTreeModelTest::_insertNullObject() +{ + QmlObjectTreeModel model; + const QModelIndex idx = model.appendItem(nullptr); + + QVERIFY(idx.isValid()); + QCOMPARE(model.count(), 1); + QVERIFY(model.getObject(idx) == nullptr); + QVERIFY(!model.data(idx, Qt::UserRole).isValid()); // null object → invalid variant +} + +// --------------------------------------------------------------------------- +// Destructor safety: external objects outlive the model +// --------------------------------------------------------------------------- + +void QmlObjectTreeModelTest::_destructorSafeWithExternalObjects() +{ + QObject externalObj; + { + QmlObjectTreeModel model; + model.appendItem(&externalObj, QModelIndex(), QStringLiteral("test")); + // model destroyed here — must not crash or double-free externalObj + } + // externalObj still alive — reaching here means no crash + QVERIFY(true); +} + +// --------------------------------------------------------------------------- +// Destructor safety: objects destroyed before the model (shutdown scenario) +// --------------------------------------------------------------------------- + +void QmlObjectTreeModelTest::_destructorSafeWithDestroyedObjects() +{ + QmlObjectTreeModel model; + { + QObject tempObj; + model.appendItem(&tempObj, QModelIndex(), QStringLiteral("test")); + // tempObj destroyed here while model still holds a pointer to it + } + // model destroyed after tempObj — must not crash during ~QmlObjectTreeModel + QVERIFY(true); +} + +UT_REGISTER_TEST(QmlObjectTreeModelTest, TestLabel::Unit) + +#include "QmlObjectTreeModelTest.moc" diff --git a/test/QmlControls/QmlObjectTreeModelTest.h b/test/QmlControls/QmlObjectTreeModelTest.h new file mode 100644 index 000000000000..e7203771a544 --- /dev/null +++ b/test/QmlControls/QmlObjectTreeModelTest.h @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include "QmlObjectTreeModel.h" +#include "UnitTest.h" + +class QmlObjectTreeModelTest : public UnitTest +{ + Q_OBJECT + +private slots: + // Construction & empty state + void _emptyModelDefaults(); + void _roleNamesIncludeNodeType(); + + // Insert / Append + void _appendToRoot(); + void _appendWithNodeType(); + void _insertAtPositions(); + void _appendToChild(); + void _insertInvalidParentLogsWarning(); + void _insertOutOfRangeLogsWarning(); + + // Remove + void _removeItemReturnsObject(); + void _removeSubtreeDecrementsCount(); + void _removeAtConvenience(); + void _removeChildrenKeepsParent(); + void _removeInvalidIndexReturnsNull(); + + // Count cache + void _countStaysConsistentAfterMixedOps(); + void _countReflectsNestedChildren(); + + // Clear + void _clearResetsToEmpty(); + void _clearAndDeleteContentsDeletesObjects(); + void _clearOnEmptyIsNoop(); + + // Tree navigation + void _parentOfRootChildIsInvalid(); + void _parentOfNestedChild(); + void _depthValues(); + void _indexForObjectFindsNested(); + void _indexForObjectNotFound(); + void _containsWorks(); + void _columnCountAlwaysOne(); + void _hasChildrenLeafVsParent(); + + // Data & SetData + void _dataObjectRole(); + void _dataTextRole(); + void _dataNodeTypeRole(); + void _dataSeparatorRole(); + void _dataInvalidIndexReturnsEmpty(); + void _setDataReplacesObject(); + void _setDataNonObjectRoleReturnsFalse(); + + // Dirty tracking + void _insertSetsDirty(); + void _childDirtyPropagates(); + void _removeDisconnectsDirty(); + + // Model signals + void _insertEmitsRowsInserted(); + void _removeEmitsRowsRemoved(); + void _clearEmitsModelReset(); + void _countChangedEmitted(); + + // Nested reset + void _nestedResetOnlyEmitsOnce(); + void _insertDuringResetNoSignals(); + + // insertRows / removeRows guard + void _insertRowsReturnsFalse(); + void _removeRowsReturnsFalse(); + + // QPersistentModelIndex stability + void _persistentIndexSurvivesInsertBefore(); + void _persistentIndexSurvivesRemoveOther(); + + // Null object edge case + void _insertNullObject(); + + // Destructor safe when external objects outlive the model + void _destructorSafeWithExternalObjects(); + + // Destructor safe when objects destroyed before the model + void _destructorSafeWithDestroyedObjects(); +}; diff --git a/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc b/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc index 56f9d84dd49a..36515a754425 100644 --- a/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc +++ b/test/QtLocationPlugin/QGCTileCacheDatabaseTest.cc @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -59,6 +60,9 @@ void QGCTileCacheDatabaseTest::_testInitWithValidPath() void QGCTileCacheDatabaseTest::_testInitWithEmptyPath() { + // Expected: critical about missing cache directory + expectLogMessage(QtCriticalMsg, QRegularExpression("Could not find suitable cache directory")); + const QString emptyPath; QGCTileCacheDatabase db(emptyPath); QVERIFY(!db.init()); diff --git a/test/UnitTestFramework/Fixtures/RAIIFixtures.cc b/test/UnitTestFramework/Fixtures/RAIIFixtures.cc index 7380adff5a15..54c5d9f3d4b4 100644 --- a/test/UnitTestFramework/Fixtures/RAIIFixtures.cc +++ b/test/UnitTestFramework/Fixtures/RAIIFixtures.cc @@ -150,11 +150,6 @@ void SettingsFixture::setOfflineVehicleType(MAV_TYPE vehicleType) appSettings->offlineEditingVehicleClass()->setRawValue(QGCMAVLink::vehicleClass(vehicleType)); } -void SettingsFixture::setAltitudeMode(int altitudeMode) -{ - Q_UNUSED(altitudeMode); -} - void SettingsFixture::setFactValue(Fact* fact, const QVariant& value) { if (!fact) { diff --git a/test/UnitTestFramework/Fixtures/RAIIFixtures.h b/test/UnitTestFramework/Fixtures/RAIIFixtures.h index 34d8a7269dcc..18c7f790f749 100644 --- a/test/UnitTestFramework/Fixtures/RAIIFixtures.h +++ b/test/UnitTestFramework/Fixtures/RAIIFixtures.h @@ -113,9 +113,6 @@ class SettingsFixture /// Set offline editing vehicle type void setOfflineVehicleType(MAV_TYPE vehicleType); - /// Set global altitude mode - void setAltitudeMode(int altitudeMode); - /// Set a Fact value (will be restored on destruction) void setFactValue(Fact* fact, const QVariant& value); diff --git a/test/UnitTestFramework/Tests/TestBaseClassesTest.h b/test/UnitTestFramework/Tests/TestBaseClassesTest.h index c3ecd7342cbf..2a8c876e93d9 100644 --- a/test/UnitTestFramework/Tests/TestBaseClassesTest.h +++ b/test/UnitTestFramework/Tests/TestBaseClassesTest.h @@ -30,12 +30,12 @@ class SimpleVehicleTest : public VehicleTest bool wasInitCalled() const { - return _initCalled; + return _initWasCalled; } bool wasCleanupCalled() const { - return _cleanupCalled; + return _cleanupWasCalled; } bool hadVehicle() const @@ -43,17 +43,14 @@ class SimpleVehicleTest : public VehicleTest return _hadVehicle; } - // Public wrappers for testing void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } public slots: @@ -61,26 +58,19 @@ public slots: void init() override { VehicleTest::init(); - _initCalled = true; + _initWasCalled = true; _hadVehicle = (vehicle() != nullptr); } void cleanup() override { - _cleanupCalled = true; + _cleanupWasCalled = true; VehicleTest::cleanup(); } -private slots: - - void _dummyTest() - { - // Just a placeholder test method - } - private: - bool _initCalled = false; - bool _cleanupCalled = false; + bool _initWasCalled = false; + bool _cleanupWasCalled = false; bool _hadVehicle = false; }; @@ -98,17 +88,14 @@ class SimpleOfflineMissionTest : public OfflineMissionTest return missionController() != nullptr; } - // Public wrappers for testing void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } public slots: @@ -122,12 +109,6 @@ public slots: { OfflineMissionTest::cleanup(); } - -private slots: - - void _dummyTest() - { - } }; /// Simple derived class to test CommsTest base @@ -144,17 +125,14 @@ class SimpleCommsTest : public CommsTest return linkManager() != nullptr; } - // Public wrappers for testing - expose protected methods void doInit() { - UnitTest::initTestCase(); init(); } void doCleanup() { cleanup(); - UnitTest::cleanupTestCase(); } SharedLinkInterfacePtr doCreateMockLink(const QString& name) @@ -178,10 +156,4 @@ public slots: { CommsTest::cleanup(); } - -private slots: - - void _dummyTest() - { - } }; diff --git a/test/UnitTestFramework/UnitTest.cc b/test/UnitTestFramework/UnitTest.cc index b97ebb35157c..006df0227537 100644 --- a/test/UnitTestFramework/UnitTest.cc +++ b/test/UnitTestFramework/UnitTest.cc @@ -19,6 +19,7 @@ #include "MultiVehicleManager.h" #include "QGC.h" #include "QGCApplication.h" +#include "QGCLogging.h" #include "QGCLoggingCategory.h" #include "QmlObjectListModel.h" #include "SettingsManager.h" @@ -626,21 +627,61 @@ int UnitTest::run(QStringView singleTest, const QString& outputFile, TestLabels return ret; } +// ============================================================================ +// Test‑time capture handler +// ============================================================================ +// QTest::qExec() replaces the application message handler with its own. +// Our QGCLogging::msgHandler is therefore NOT called during test execution. +// We work around this by installing a thin wrapper ON TOP of QTest's handler +// inside initTestCase() and restoring it in cleanupTestCase(). +// The wrapper captures messages for the test log‑capture API and then chains +// to QTest's handler so QWARN / QCRITICAL output keeps working. + +static QtMessageHandler s_qtestHandler = nullptr; + +static void testCaptureHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + // Capture for unit‑test introspection + QGCLogging::captureIfEnabled(type, context, msg); + + // Chain to QTest's handler for normal test output + if (s_qtestHandler) { + s_qtestHandler(type, context, msg); + } +} + void UnitTest::initTestCase() { // Reset test tracking state at start of each test class _resetTestState(); + + // Install the capture wrapper on top of QTest's handler + s_qtestHandler = qInstallMessageHandler(testCaptureHandler); } void UnitTest::cleanupTestCase() { - // Override in derived classes for one-time teardown + // Restore QTest's handler so qExec teardown stays consistent + if (s_qtestHandler) { + (void) qInstallMessageHandler(s_qtestHandler); + s_qtestHandler = nullptr; + } +} + +void UnitTest::expectLogMessage(QtMsgType type, const QRegularExpression &pattern) +{ + _expectedLogMessages.append({type, pattern}); } void UnitTest::init() { _initCalled = true; _failureContextDumped = false; + _expectedLogMessages.clear(); + + // Start capturing log messages for this test (cleared from previous test) + QGCLogging::clearCapturedMessages(); + QGCLogging::setCaptureEnabled(true); // Force offline vehicle back to defaults AppSettings* const appSettings = SettingsManager::instance()->appSettings(); @@ -655,10 +696,58 @@ void UnitTest::cleanup() _cleanupCalled = true; dumpFailureContextIfTestFailed(QStringLiteral("cleanup")); + // Stop capturing log messages after the test finishes + QGCLogging::setCaptureEnabled(false); + _cleanupTempFiles(); // Process any lingering events to prevent cross-test contamination settleEventLoopForCleanup(3, 0); + + // Fail the test if any uncategorized or critical log messages were captured. + // Skip if the test already failed to avoid noisy double-failure reports. + if (!QTest::currentTestFailed()) { + QString uncategorizedDetails; + QString criticalDetails; + + auto isExpected = [this](const CapturedLogMessage &m) { + for (const auto &e : _expectedLogMessages) { + if (e.type == m.type && e.pattern.match(m.message).hasMatch()) { + return true; + } + } + return false; + }; + + const auto allMsgs = QGCLogging::capturedMessages(); + for (const auto &m : allMsgs) { + if (isExpected(m)) { + continue; + } + if (m.category.isEmpty() || m.category == QStringLiteral("default")) { + const char *level = (m.type == QtDebugMsg) ? "debug" + : (m.type == QtWarningMsg) ? "warning" + : (m.type == QtInfoMsg) ? "info" + : "other"; + uncategorizedDetails += QStringLiteral(" [%1] %2\n").arg(QLatin1String(level), m.message); + } + if (m.type == QtCriticalMsg) { + criticalDetails += QStringLiteral(" [%1] %2\n").arg(m.category, m.message); + } + } + + if (!uncategorizedDetails.isEmpty() || !criticalDetails.isEmpty()) { + QString msg; + if (!uncategorizedDetails.isEmpty()) { + msg += QStringLiteral("Uncategorized log messages (use qCDebug/qCWarning with a category):\n%1") + .arg(uncategorizedDetails); + } + if (!criticalDetails.isEmpty()) { + msg += QStringLiteral("Critical log messages:\n%1").arg(criticalDetails); + } + QFAIL(qPrintable(msg)); + } + } } void UnitTest::dumpFailureContextIfTestFailed(QStringView reason) diff --git a/test/UnitTestFramework/UnitTest.h b/test/UnitTestFramework/UnitTest.h index b47e1f0ade88..42f233c0a10f 100644 --- a/test/UnitTestFramework/UnitTest.h +++ b/test/UnitTestFramework/UnitTest.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -436,7 +437,17 @@ protected slots: /// Allows derived fixtures to append state to failure dumps. virtual QString failureContextSummary() const; + /// Declare that a captured log message matching @a pattern at level @a type + /// is expected and should not cause a test failure in cleanup(). + /// Call this before the code that emits the message. + void expectLogMessage(QtMsgType type, const QRegularExpression &pattern); + private: + struct ExpectedLogMessage { + QtMsgType type; + QRegularExpression pattern; + }; + void _cleanupTempFiles(); void _resetTestState(); @@ -445,6 +456,7 @@ protected slots: QList _tempFiles; QList _tempDirs; + QList _expectedLogMessages; TestLabels _labels; bool _unitTestRun = false; diff --git a/test/Utilities/Compression/QGCStreamingDecompressionTest.cc b/test/Utilities/Compression/QGCStreamingDecompressionTest.cc index bfe8cafab980..73167f9043b9 100644 --- a/test/Utilities/Compression/QGCStreamingDecompressionTest.cc +++ b/test/Utilities/Compression/QGCStreamingDecompressionTest.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "QGCArchiveFile.h" @@ -123,6 +124,7 @@ void QGCStreamingDecompressionTest::_testDecompressDeviceErrors() // Test writeData returns -1 QGCDecompressDevice device4(":/unittest/manifest.json.gz"); QVERIFY(device4.open(QIODevice::ReadOnly)); + expectLogMessage(QtWarningMsg, QRegularExpression("ReadOnly device")); QCOMPARE(device4.write("test", 4), qint64(-1)); device4.close(); } @@ -238,6 +240,7 @@ void QGCStreamingDecompressionTest::_testArchiveFileErrors() // Test writeData returns -1 QGCArchiveFile device4(":/unittest/manifest.json.zip", "manifest.json"); QVERIFY(device4.open(QIODevice::ReadOnly)); + expectLogMessage(QtWarningMsg, QRegularExpression("ReadOnly device")); QCOMPARE(device4.write("test", 4), qint64(-1)); device4.close(); } diff --git a/test/Utilities/StateMachine/QGCStateMachineTest.cc b/test/Utilities/StateMachine/QGCStateMachineTest.cc index 62c59059b554..dbc8afc8a589 100644 --- a/test/Utilities/StateMachine/QGCStateMachineTest.cc +++ b/test/Utilities/StateMachine/QGCStateMachineTest.cc @@ -4,6 +4,7 @@ #include "QGCStateMachine.h" #include "WaitStateBase.h" +#include #include #include #include @@ -75,6 +76,9 @@ void QGCStateMachineTest::_testGlobalErrorState() }); machine.setGlobalErrorState(errorState); + // The async state has no completion connection and no timeout — expected critical + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = machine.addAsyncFunctionState(QStringLiteral("Async"), [](AsyncFunctionState* state) { QTimer::singleShot(50, state, [state]() { state->fail(); }); }); @@ -141,6 +145,9 @@ void QGCStateMachineTest::_testLocalErrorState() localErrorHandled = true; }); + // The async state has no completion connection and no timeout — expected critical + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState(QStringLiteral("Async"), &machine, [](AsyncFunctionState* state) { QTimer::singleShot(50, state, [state]() { state->fail(); }); }); @@ -319,6 +326,10 @@ void QGCStateMachineTest::_testErrorHandlerFactories() auto* errorState = machine.addLogAndContinueErrorState(QStringLiteral("ErrorContinue"), finalState); machine.setGlobalErrorState(errorState); + // Expected: async state critical (no completion connection) + error handler warning + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + expectLogMessage(QtWarningMsg, QRegularExpression("error handled in")); + auto* failingState = machine.addAsyncFunctionState(QStringLiteral("Failing"), [](AsyncFunctionState* state) { QTimer::singleShot(0, state, [state]() { state->fail(); }); }); @@ -336,6 +347,10 @@ void QGCStateMachineTest::_testErrorHandlerFactories() auto* errorState = machine.addLogAndStopErrorState(QStringLiteral("ErrorStop")); machine.setGlobalErrorState(errorState); + // Expected: async state critical (no completion connection) + error handler warning + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + expectLogMessage(QtWarningMsg, QRegularExpression("stopping due to error in")); + auto* failingState = machine.addAsyncFunctionState(QStringLiteral("Failing"), [](AsyncFunctionState* state) { QTimer::singleShot(0, state, [state]() { state->fail(); }); }); diff --git a/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc b/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc index ed5e259716a8..a69bfef2da11 100644 --- a/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc +++ b/test/Utilities/StateMachine/states/AsyncFunctionStateTest.cc @@ -1,6 +1,8 @@ #include "AsyncFunctionStateTest.h" #include "StateTestCommon.h" +#include + void AsyncFunctionStateTest::_testAsyncFunctionState() { @@ -8,6 +10,9 @@ void AsyncFunctionStateTest::_testAsyncFunctionState() bool setupCalled = false; AsyncFunctionState* capturedState = nullptr; + // Expected: async state has no completion connection and no timeout + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState( QStringLiteral("TestAsync"), &machine, @@ -76,6 +81,9 @@ void AsyncFunctionStateTest::_testErrorTransition() QStateMachine machine; bool errorHandled = false; + // Expected: async state has no completion connection and no timeout + expectLogMessage(QtCriticalMsg, QRegularExpression("has no completion connection")); + auto* asyncState = new AsyncFunctionState( QStringLiteral("TestError"), &machine, diff --git a/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc b/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc index 844e4f0d562e..dd9c2d8c0e33 100644 --- a/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc +++ b/test/Utilities/StateMachine/states/SendMavlinkCommandStateTest.cc @@ -1,6 +1,8 @@ #include "SendMavlinkCommandStateTest.h" #include "StateTestCommon.h" +#include + #include "SendMavlinkCommandState.h" @@ -40,6 +42,10 @@ void SendMavlinkCommandStateTest::_testUnconfiguredStateFails() { QStateMachine machine; + // Expected: unconfigured state emits critical + Qt warns about null connect + expectLogMessage(QtWarningMsg, QRegularExpression("invalid nullptr parameter")); + expectLogMessage(QtCriticalMsg, QRegularExpression("SendMavlinkCommandState not configured")); + // Create without configuration auto* state = new SendMavlinkCommandState(&machine); auto* errorState = new FunctionState(QStringLiteral("Error"), &machine, []() {}); diff --git a/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc b/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc index 3d63461fb42f..13726c3b20ac 100644 --- a/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc +++ b/test/Vehicle/ComponentInformation/ComponentInformationTranslationTest.cc @@ -42,6 +42,12 @@ void ComponentInformationTranslationTest::_downloadAndTranslateFromSummary_test( { const QString summaryPath = tempPath(QStringLiteral("summary.json")); const QString locale = QLocale::system().name(); + + // Skip test on English locales since translation is intentionally skipped + if (locale.startsWith(QLatin1String("en"))) { + QSKIP("Translation is skipped for English locales"); + } + QFile summaryFile(summaryPath); QVERIFY(summaryFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)); QTextStream summaryStream(&summaryFile); @@ -58,7 +64,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateFromSummary_test( QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); QVERIFY(completeSpy.isValid()); - QVERIFY(translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QVERIFY_SIGNAL_WAIT(completeSpy, TestTimeout::mediumMs()); QCOMPARE(completeSpy.count(), 1); @@ -93,7 +99,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateMissingLocale_tes ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } @@ -116,7 +122,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateMissingUrl_test() ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } @@ -132,7 +138,7 @@ void ComponentInformationTranslationTest::_downloadAndTranslateInvalidSummaryJso ComponentInformationTranslation translation(this, &cachedDownloader); QSignalSpy completeSpy(&translation, &ComponentInformationTranslation::downloadComplete); - QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600)); + QVERIFY(!translation.downloadAndTranslate(summaryPath, QStringLiteral(":/unittest/TranslationTest.json"), 3600, QStringLiteral("TEST"))); QCOMPARE(completeSpy.count(), 0); } diff --git a/test/Vehicle/InitialConnectTest.cc b/test/Vehicle/InitialConnectTest.cc index c038cb9ab2b8..781aaf0e49cf 100644 --- a/test/Vehicle/InitialConnectTest.cc +++ b/test/Vehicle/InitialConnectTest.cc @@ -10,6 +10,7 @@ #include "MAVLinkProtocol.h" #include "MultiVehicleManager.h" #include "MockConfiguration.h" +#include "MockLink.h" #include "MockLinkMissionItemHandler.h" #include "MissionManager.h" #include "ParameterManager.h" @@ -21,11 +22,6 @@ #include -void InitialConnectTest::init() -{ - VehicleTestManualConnect::init(); -} - void InitialConnectTest::_performTestCases_data() { QTest::addColumn("failureMode"); @@ -247,6 +243,133 @@ void InitialConnectTest::_rallyTimeoutPathDoesNotLeakCompletionHandler() _disconnectMockLink(); } +void InitialConnectTest::_stateTimeoutFallsThrough_data() +{ + QTest::addColumn>("blockedMessageIds"); + QTest::addColumn("configFailureMode"); + QTest::addColumn("blockMissionProtocolImmediately"); + QTest::addColumn("blockMissionProtocolAfterMissionLoad"); + QTest::addColumn("timeoutOverrideStates"); + QTest::addColumn("expectParametersReady"); + QTest::addColumn("expectPlanRequestComplete"); + + // Timeout matrix: + // +----------------+-------------------+------------------+---------+--------+ + // | State | Failure Injection | Timeout States | Params? | Plans? | + // +----------------+-------------------+------------------+---------+--------+ + // | StandardModes | Block AVAIL_MODES | StdModes | Yes | Yes | + // | CompInfo | Block COMP_META | CompInfo | Yes | Yes | + // | Parameters | No param response | Parameters | No | Yes | + // | Mission | Block mission req | Msn+Fence+Rally | Yes | No | + // | GeoFence | Block after msn | Fence+Rally | Yes | No | + // +----------------+-------------------+------------------+---------+--------+ + + QTest::addRow("StandardModes") + << QList{MAVLINK_MSG_ID_AVAILABLE_MODES} + << static_cast(MockConfiguration::FailNone) + << false << false + << QStringList{QStringLiteral("RequestStandardModes")} + << true << true; + + QTest::addRow("CompInfo") + << QList{MAVLINK_MSG_ID_COMPONENT_METADATA} + << static_cast(MockConfiguration::FailNone) + << false << false + << QStringList{QStringLiteral("RequestCompInfo")} + << true << true; + + QTest::addRow("Parameters") + << QList{} + << static_cast(MockConfiguration::FailParamNoResponseToRequestList) + << false << false + << QStringList{QStringLiteral("RequestParameters")} + << false << true; + + QTest::addRow("Mission") + << QList{} + << static_cast(MockConfiguration::FailNone) + << true << false + << QStringList{QStringLiteral("RequestMission"), + QStringLiteral("RequestGeoFence"), + QStringLiteral("RequestRallyPoints")} + << true << false; + + QTest::addRow("GeoFence") + << QList{} + << static_cast(MockConfiguration::FailNone) + << false << true + << QStringList{QStringLiteral("RequestGeoFence"), + QStringLiteral("RequestRallyPoints")} + << true << false; +} + +void InitialConnectTest::_stateTimeoutFallsThrough() +{ + QFETCH(QList, blockedMessageIds); + QFETCH(int, configFailureMode); + QFETCH(bool, blockMissionProtocolImmediately); + QFETCH(bool, blockMissionProtocolAfterMissionLoad); + QFETCH(QStringList, timeoutOverrideStates); + QFETCH(bool, expectParametersReady); + QFETCH(bool, expectPlanRequestComplete); + + LinkManager::instance()->setConnectionsAllowed(); + + auto* mvm = MultiVehicleManager::instance(); + QSignalSpy activeVehicleSpy{mvm, &MultiVehicleManager::activeVehicleChanged}; + auto* mockConfig = new MockConfiguration(QStringLiteral("TimeoutFallthroughMock")); + mockConfig->setFirmwareType(MAV_AUTOPILOT_PX4); + mockConfig->setVehicleType(MAV_TYPE_QUADROTOR); + if (configFailureMode != static_cast(MockConfiguration::FailNone)) { + mockConfig->setFailureMode(static_cast(configFailureMode)); + } + mockConfig->setDynamic(true); + + SharedLinkConfigurationPtr linkConfig = LinkManager::instance()->addConfiguration(mockConfig); + QVERIFY(LinkManager::instance()->createConnectedLink(linkConfig)); + + _mockLink = qobject_cast(linkConfig->link()); + QVERIFY(_mockLink); + + for (const uint32_t messageId : blockedMessageIds) { + _mockLink->setRequestMessageNoResponse(messageId); + } + + if (blockMissionProtocolImmediately) { + _mockLink->setMissionItemFailureMode( + MockLinkMissionItemHandler::FailReadRequestListNoResponse, MAV_MISSION_ACCEPTED); + } + + QVERIFY(activeVehicleSpy.wait(TestTimeout::longMs())); + _vehicle = mvm->activeVehicle(); + QVERIFY(_vehicle); + + auto* initialConnectStateMachine = _vehicle->findChild(); + QVERIFY(initialConnectStateMachine); + + for (const QString& stateName : timeoutOverrideStates) { + initialConnectStateMachine->setTimeoutOverride(stateName, 100); + } + + if (blockMissionProtocolAfterMissionLoad) { + auto* missionManager = _vehicle->findChild(); + QVERIFY(missionManager); + connect(missionManager, &MissionManager::newMissionItemsAvailable, this, [this]() { + _mockLink->setMissionItemFailureMode( + MockLinkMissionItemHandler::FailReadRequestListNoResponse, MAV_MISSION_ACCEPTED); + }); + } + + QSignalSpy initialConnectCompleteSpy{_vehicle, &Vehicle::initialConnectComplete}; + if (!_vehicle->isInitialConnectComplete()) { + QVERIFY(initialConnectCompleteSpy.wait(60000)); + } + QCOMPARE(_vehicle->parameterManager()->parametersReady(), expectParametersReady); + QCOMPARE(_vehicle->initialPlanRequestComplete(), expectPlanRequestComplete); + + _disconnectMockLink(); +} + void InitialConnectTest::_stateRunMatrix_data() { QTest::addColumn("highLatency"); diff --git a/test/Vehicle/InitialConnectTest.h b/test/Vehicle/InitialConnectTest.h index 9bbd17d159ed..4f1764c6d351 100644 --- a/test/Vehicle/InitialConnectTest.h +++ b/test/Vehicle/InitialConnectTest.h @@ -7,7 +7,6 @@ class InitialConnectTest : public VehicleTestManualConnect Q_OBJECT private slots: - void init() override; void _performTestCases_data(); void _performTestCases(); void _boardVendorProductId(); @@ -16,6 +15,8 @@ private slots: void _genericAutopilotVersionFailureSkipsUnsupportedPlanTypes(); void _multipleReconnects(); void _rallyTimeoutPathDoesNotLeakCompletionHandler(); + void _stateTimeoutFallsThrough_data(); + void _stateTimeoutFallsThrough(); void _stateRunMatrix_data(); void _stateRunMatrix(); }; diff --git a/test/VideoManager/CMakeLists.txt b/test/VideoManager/CMakeLists.txt new file mode 100644 index 000000000000..5b13ca02bc99 --- /dev/null +++ b/test/VideoManager/CMakeLists.txt @@ -0,0 +1,5 @@ +# ============================================================================ +# VideoManager Tests +# ============================================================================ + +add_subdirectory(GStreamer) diff --git a/test/VideoManager/GStreamer/CMakeLists.txt b/test/VideoManager/GStreamer/CMakeLists.txt new file mode 100644 index 000000000000..fb0a5e84e485 --- /dev/null +++ b/test/VideoManager/GStreamer/CMakeLists.txt @@ -0,0 +1,12 @@ +# ============================================================================ +# GStreamer Integration Tests +# Requires GStreamer runtime; tests skip gracefully when GST is disabled. +# ============================================================================ + +target_sources(${CMAKE_PROJECT_NAME} + PRIVATE + GStreamerTest.cc + GStreamerTest.h +) + +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/test/VideoManager/GStreamer/GStreamerTest.cc b/test/VideoManager/GStreamer/GStreamerTest.cc new file mode 100644 index 000000000000..60194f0f5f99 --- /dev/null +++ b/test/VideoManager/GStreamer/GStreamerTest.cc @@ -0,0 +1,252 @@ +#include "GStreamerTest.h" + +#ifdef QGC_GST_STREAMING + +#include "GStreamer.h" +#include "GStreamerHelpers.h" +#include "GStreamerLogging.h" + +#include +#include +#include + +void GStreamerTest::init() +{ + UnitTest::init(); + + if (!gst_is_initialized()) { + GStreamer::prepareEnvironment(); + GStreamer::redirectGLibLogging(); + + GError *error = nullptr; + if (!gst_init_check(nullptr, nullptr, &error)) { + const QString msg = error ? QString::fromUtf8(error->message) : QStringLiteral("unknown error"); + g_clear_error(&error); + QSKIP(qPrintable(QStringLiteral("GStreamer unavailable: %1").arg(msg))); + } + } +} + +void GStreamerTest::_testIsValidRtspUri() +{ + QVERIFY(GStreamer::isValidRtspUri("rtsp://127.0.0.1:8554/test")); + QVERIFY(GStreamer::isValidRtspUri("rtsp://user:pass@10.0.0.1/stream")); + QVERIFY(GStreamer::isValidRtspUri("rtspu://192.168.1.1:554/video")); + QVERIFY(GStreamer::isValidRtspUri("rtspt://example.com/live")); + + QVERIFY(!GStreamer::isValidRtspUri(nullptr)); + QVERIFY(!GStreamer::isValidRtspUri("")); + QVERIFY(!GStreamer::isValidRtspUri("not-a-uri")); + QVERIFY(!GStreamer::isValidRtspUri("http://example.com")); + QVERIFY(!GStreamer::isValidRtspUri("udp://127.0.0.1:5600")); + QVERIFY(!GStreamer::isValidRtspUri("rtsp://")); +} + +void GStreamerTest::_testIsHardwareDecoderFactory() +{ + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + int total = 0; + int hwCount = 0; + int swCount = 0; + + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + QVERIFY(factory != nullptr); + + const gchar *name = gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(factory)); + QVERIFY(name != nullptr); + + if (GStreamer::isHardwareDecoderFactory(factory)) { + ++hwCount; + } else { + ++swCount; + } + ++total; + } + + qCDebug(GStreamerLog) << "Decoder factory classification:" << total << "total," + << hwCount << "hardware," << swCount << "software"; + + QVERIFY(total > 0); + QVERIFY(swCount > 0); + QVERIFY(!GStreamer::isHardwareDecoderFactory(nullptr)); + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testSetCodecPrioritiesDefault() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderDefault); + + GstRegistry *registry = gst_registry_get(); + QVERIFY(registry != nullptr); +} + +void GStreamerTest::_testSetCodecPrioritiesSoftware() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderSoftware); + + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + bool foundPrioritizedSoftware = false; + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) continue; + + if (!GStreamer::isHardwareDecoderFactory(factory)) { + const guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)); + if (rank > GST_RANK_MARGINAL) { + foundPrioritizedSoftware = true; + break; + } + } + } + + QVERIFY(foundPrioritizedSoftware); + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testSetCodecPrioritiesHardware() +{ + GStreamer::setCodecPriorities(GStreamer::ForceVideoDecoderHardware); + + GList *factories = gst_element_factory_list_get_elements( + static_cast(GST_ELEMENT_FACTORY_TYPE_DECODER | GST_ELEMENT_FACTORY_TYPE_MEDIA_VIDEO), + GST_RANK_NONE); + + if (!factories) { + QSKIP("No video decoder factories available on this system"); + } + + for (GList *node = factories; node != nullptr; node = node->next) { + GstElementFactory *factory = GST_ELEMENT_FACTORY(node->data); + if (!factory) continue; + + if (!GStreamer::isHardwareDecoderFactory(factory)) { + const guint rank = gst_plugin_feature_get_rank(GST_PLUGIN_FEATURE(factory)); + QCOMPARE(rank, static_cast(GST_RANK_NONE)); + } + } + + gst_plugin_feature_list_free(factories); +} + +void GStreamerTest::_testRedirectGLibLogging() +{ + GStreamer::redirectGLibLogging(); + + g_log("TestDomain", G_LOG_LEVEL_DEBUG, "GStreamerTest debug message"); + g_log("TestDomain", G_LOG_LEVEL_WARNING, "GStreamerTest warning message"); +} + +void GStreamerTest::_testVerifyRequiredPlugins() +{ + GstRegistry *registry = gst_registry_get(); + QVERIFY(registry != nullptr); + + // coreelements comes from system/bundled GStreamer and should always be + // available after gst_init_check(). + GstPlugin *corePlugin = gst_registry_find_plugin(registry, "coreelements"); + QVERIFY2(corePlugin, "Required plugin not found: coreelements"); + gst_clear_object(&corePlugin); + + // qml6 and qgc are QGC-built static plugins only registered by + // _registerPlugins() during GStreamer::completeInit(). They aren't in the + // registry from a bare gst_init_check(), so just verify the registry + // loaded *some* plugins and that playbin is available (from system GStreamer). + GList *plugins = gst_registry_get_plugin_list(registry); + const int pluginCount = g_list_length(plugins); + gst_plugin_list_free(plugins); + QVERIFY2(pluginCount > 0, "GStreamer registry contains no plugins at all"); + + GstElementFactory *playbinFactory = gst_element_factory_find("playbin"); + QVERIFY2(playbinFactory, "Required factory not found: playbin"); + gst_object_unref(playbinFactory); +} + +void GStreamerTest::_testEnvironmentSetup() +{ + // Save and clear relevant env vars + struct EnvBackup { + const char *name; + QByteArray value; + bool wasSet; + }; + static constexpr const char *envVars[] = { + "GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0", + "GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0", + "PYTHONHOME", "PYTHONPATH", "PYTHONUSERBASE", + "VIRTUAL_ENV", "CONDA_PREFIX", "CONDA_DEFAULT_ENV", + }; + QList backups; + for (const char *var : envVars) { + backups.append({var, qgetenv(var), qEnvironmentVariableIsSet(var)}); + qunsetenv(var); + } + + GStreamer::prepareEnvironment(); + + // On desktop builds with bundled plugins, env vars should be set. + // On dev builds without bundles, they remain unset (which is correct). + // Either way, if any plugin path is set, it should point to an existing directory. + for (const char *var : {"GST_PLUGIN_PATH", "GST_PLUGIN_PATH_1_0", + "GST_PLUGIN_SYSTEM_PATH", "GST_PLUGIN_SYSTEM_PATH_1_0"}) { + if (qEnvironmentVariableIsSet(var)) { + const QString path = qEnvironmentVariable(var); + // Paths may be colon-separated; check each component + const QStringList parts = path.split(QDir::listSeparator(), Qt::SkipEmptyParts); + for (const QString &part : parts) { + QVERIFY2(QDir(part).exists(), + qPrintable(QStringLiteral("%1 contains non-existent path: %2").arg(var, part))); + } + } + } + + for (const char *var : {"GST_PLUGIN_SCANNER", "GST_PLUGIN_SCANNER_1_0"}) { + if (qEnvironmentVariableIsSet(var)) { + const QString path = qEnvironmentVariable(var); + QVERIFY2(QFileInfo(path).isExecutable(), + qPrintable(QStringLiteral("%1 is not executable: %2").arg(var, path))); + } + } + + // Restore env vars + for (const EnvBackup &backup : backups) { + if (backup.wasSet) { + qputenv(backup.name, backup.value); + } else { + qunsetenv(backup.name); + } + } +} + +#else + +void GStreamerTest::init() { UnitTest::init(); QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testIsValidRtspUri() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testIsHardwareDecoderFactory() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesDefault() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesSoftware() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testSetCodecPrioritiesHardware() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testRedirectGLibLogging() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testVerifyRequiredPlugins() { QSKIP("GStreamer not enabled"); } +void GStreamerTest::_testEnvironmentSetup() { QSKIP("GStreamer not enabled"); } + +#endif + +UT_REGISTER_TEST(GStreamerTest, TestLabel::Integration) diff --git a/test/VideoManager/GStreamer/GStreamerTest.h b/test/VideoManager/GStreamer/GStreamerTest.h new file mode 100644 index 000000000000..43f01e4a9ff3 --- /dev/null +++ b/test/VideoManager/GStreamer/GStreamerTest.h @@ -0,0 +1,20 @@ +#pragma once + +#include "UnitTest.h" + +class GStreamerTest : public UnitTest +{ + Q_OBJECT + +private slots: + void init() override; + + void _testIsValidRtspUri(); + void _testIsHardwareDecoderFactory(); + void _testSetCodecPrioritiesDefault(); + void _testSetCodecPrioritiesSoftware(); + void _testSetCodecPrioritiesHardware(); + void _testRedirectGLibLogging(); + void _testVerifyRequiredPlugins(); + void _testEnvironmentSetup(); +}; diff --git a/tools/README.md b/tools/README.md index 629a3e953a3f..d4c26961e46b 100644 --- a/tools/README.md +++ b/tools/README.md @@ -7,13 +7,13 @@ This directory contains development tools, scripts, and configuration files for ``` tools/ ├── analyze.py # Static analysis and formatting (clang-format, clang-tidy, cppcheck, clazy) -├── check-deps.sh # Check for outdated dependencies +├── check_deps.py # Check for outdated dependencies ├── clean.sh # Clean build artifacts and caches ├── configure.py # CMake configuration wrapper -├── coverage.sh # Code coverage reports -├── generate-docs.sh # Generate API docs (Doxygen) +├── coverage.py # Code coverage reports +├── generate_docs.py # Generate API docs (Doxygen) ├── param-docs.py # Generate parameter documentation -├── pre-commit.sh # Pre-commit hook runner +├── pre_commit.py # Pre-commit hook runner ├── release.sh # Semantic versioning and release automation ├── run_tests.py # Qt unit test runner ├── configs/ # Tool configuration files @@ -106,15 +106,15 @@ Clean build artifacts and caches. ./tools/clean.sh --dry-run # Show what would be removed ``` -### coverage.sh +### coverage.py Generate code coverage reports. Wrapper around CMake coverage targets. ```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 +python3 ./tools/coverage.py # Build with coverage, run tests, generate report +python3 ./tools/coverage.py --report # Generate report only (after tests) +python3 ./tools/coverage.py --open # Generate and open in browser +python3 ./tools/coverage.py --clean # Clean coverage data ``` Requires: `gcovr` (`pip install gcovr`) @@ -143,26 +143,26 @@ Profile QGC for performance and memory issues. ./tools/debuggers/profile.sh --sanitize # Build with AddressSanitizer ``` -### check-deps.sh +### check_deps.py 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 +python3 ./tools/check_deps.py # Check all dependencies +python3 ./tools/check_deps.py --submodules # Check git submodules only +python3 ./tools/check_deps.py --qt # Check Qt version +python3 ./tools/check_deps.py --update # Update submodules to latest ``` -### generate-docs.sh +### generate_docs.py 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 +python3 ./tools/generate_docs.py # Generate HTML docs +python3 ./tools/generate_docs.py --open # Generate and open in browser +python3 ./tools/generate_docs.py --pdf # Generate PDF (requires LaTeX) +python3 ./tools/generate_docs.py --clean # Clean generated docs ``` Requires: `doxygen`, `graphviz` @@ -190,7 +190,7 @@ Scripts in `setup/` help configure development environments. They read configura | `install_dependencies.py --platform windows` | Windows | Install GStreamer (Vulkan SDK optional) | | `install_python.py` | All | Install Python tools via uv or pip | | `build-gstreamer.py` | All | Build GStreamer from source (optional) | -| `download_artifacts.py` | All | Download build artifacts from GitHub Actions | +| `download_artifacts.py` | All | Download build artifacts (in `.github/scripts/`) | | `read_config.py` | All | Read `.github/build-config.json` (Python, cross-platform) | ### Usage Examples @@ -206,9 +206,9 @@ python3 ./tools/setup/install_dependencies.py --platform macos python .\tools\setup\install_dependencies.py --platform windows # Install Python tooling (pre-commit, test, coverage, etc.) -python3 ./tools/setup/install_python.py --groups precommit test coverage +python3 ./tools/setup/install_python.py precommit,test,coverage -# Build GStreamer from source (Linux, optional) +# Build GStreamer from source (optional — CMake auto-downloads pre-built SDKs) python3 ./tools/setup/build-gstreamer.py --platform linux --prefix /opt/gstreamer # Read build config values @@ -285,7 +285,7 @@ Common utilities in `common/` are used by multiple tools: - `patterns.py` - QGC-specific regex patterns (Fact, FactGroup, MAVLink) - `file_traversal.py` - File discovery with proper filtering -- `gh_actions.py` - Shared `gh api` helpers for workflow runs and artifacts +- `gh_actions.py` - GitHub API helpers (httpx with `gh` CLI fallback) for workflow runs and artifacts See [common/README.md](common/README.md) for API documentation. @@ -441,7 +441,7 @@ Version numbers and build settings are centralized in `.github/build-config.json { "qt_version": "6.10.1", "qt_modules": "qtcharts qtlocation ...", - "gstreamer_version": "1.24.12", + "gstreamer_default_version": "1.24.13", "ndk_version": "r27c", ... } diff --git a/tools/_bootstrap.py b/tools/_bootstrap.py new file mode 100644 index 000000000000..22dd025181d7 --- /dev/null +++ b/tools/_bootstrap.py @@ -0,0 +1,18 @@ +"""Shared import bootstrap for Python entrypoints under tools/.""" + +from __future__ import annotations + +import sys +from pathlib import Path + + +def ensure_tools_dir(start: str | Path) -> Path: + """Ensure the top-level ``tools`` directory is importable.""" + path = Path(start).resolve() + current = path if path.is_dir() else path.parent + for candidate in [current] + list(current.parents): + if candidate.name == "tools": + if str(candidate) not in sys.path: + sys.path.insert(0, str(candidate)) + return candidate + raise RuntimeError(f"Could not locate tools directory from {start}") diff --git a/tools/analyze.py b/tools/analyze.py index 6920d973cc2d..9443fed478f8 100755 --- a/tools/analyze.py +++ b/tools/analyze.py @@ -199,7 +199,8 @@ def run(self, files: list[Path], fix: bool = False) -> AnalysisResult: capture_output=True, text=True, ) - version_match = version_result.stdout.split()[2] if version_result.stdout else "unknown" + parts = version_result.stdout.split() if version_result.stdout else [] + version_match = parts[2] if len(parts) > 2 else "unknown" log_info(f"Using clang-format version {version_match}") if not files: diff --git a/tools/check-deps.sh b/tools/check-deps.sh deleted file mode 100755 index 33c0e63df85a..000000000000 --- a/tools/check-deps.sh +++ /dev/null @@ -1,261 +0,0 @@ -#!/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 - # Extract Qt 6.x versions (portable grep) - latest_info=$(curl -s "https://download.qt.io/official_releases/qt/" 2>/dev/null | \ - grep -Eo '6\.[0-9]+' | 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 -Eo '[0-9]+\.[0-9]+\.[0-9]+' || 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 -Eo '[0-9]+\.[0-9]+\.[0-9]+' || 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/check_deps.py b/tools/check_deps.py new file mode 100644 index 000000000000..82961db05a2e --- /dev/null +++ b/tools/check_deps.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Check dependency and tool versions used by QGroundControl development.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from pathlib import Path +from urllib.error import URLError +from urllib.request import urlopen + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.build_config import get_build_config_value +from common.logging import log_error, log_info, log_ok, log_warn + +QT_RELEASES_URL = "https://download.qt.io/official_releases/qt/" +REQ_FILES = [ + Path("requirements.txt"), + Path("docs/requirements.txt"), + Path("tools/pyproject.toml"), +] +BUILD_TOOLS = ["cmake", "ninja", "ccache", "clang-format", "clang-tidy"] + + +def run_git(args: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + """Run a git command and capture stdout/stderr.""" + return subprocess.run( + ["git", *args], + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + + +def parse_submodule_paths(repo_root: Path) -> list[Path]: + """Return existing submodule paths from ``git submodule status``.""" + result = run_git(["submodule", "status", "--recursive"], repo_root) + if result.returncode != 0: + return [] + + paths: list[Path] = [] + for line in result.stdout.splitlines(): + parts = line.strip().split() + if len(parts) < 2: + continue + path = repo_root / parts[1] + if path.is_dir(): + paths.append(path) + return paths + + +def check_submodules(repo_root: Path, *, update: bool) -> None: + """Check whether git submodules are behind their upstream branch.""" + log_info("Checking git submodules...") + run_git(["submodule", "update", "--init", "--recursive"], repo_root) + + outdated: list[tuple[Path, int]] = [] + for submodule in parse_submodule_paths(repo_root): + run_git(["fetch", "--quiet"], submodule) + upstream = run_git(["rev-parse", "@{u}"], submodule) + if upstream.returncode != 0: + print(f" - {submodule.relative_to(repo_root)} (no upstream)") + continue + + behind_result = run_git(["rev-list", "--count", "HEAD..@{u}"], submodule) + behind = int(behind_result.stdout.strip() or "0") if behind_result.returncode == 0 else 0 + if behind > 0: + outdated.append((submodule, behind)) + log_warn(f"{submodule.relative_to(repo_root)}: {behind} commits behind upstream") + else: + print(f" - {submodule.relative_to(repo_root)} (up to date)") + + if not outdated: + log_ok("All submodules up to date") + return + + log_warn(f"{len(outdated)} submodule(s) have updates available") + if update: + log_info("Updating submodules...") + result = run_git(["submodule", "update", "--remote", "--merge"], repo_root) + if result.returncode == 0: + log_ok("Submodules updated") + else: + log_error("Submodule update failed") + if result.stderr: + print(result.stderr.strip(), file=sys.stderr) + else: + log_info("Run with --update to update submodules") + + +def fetch_latest_qt_minor() -> str | None: + """Return the latest Qt 6 minor version available on download.qt.io.""" + try: + with urlopen(QT_RELEASES_URL, timeout=15) as response: + body = response.read().decode("utf-8", errors="ignore") + except (OSError, URLError): + return None + + versions = re.findall(r">6\.(\d+)/<", body) + if not versions: + versions = re.findall(r"6\.(\d+)", body) + if not versions: + return None + latest_minor = max(int(version) for version in versions) + return f"6.{latest_minor}" + + +def check_qt_version() -> None: + """Check configured and installed Qt versions.""" + log_info("Checking Qt version...") + current_version = get_build_config_value("qt_version", "unknown", start=Path(__file__).resolve()) + print(f" Current: Qt {current_version}") + + latest_minor = fetch_latest_qt_minor() + if latest_minor: + print(f" Latest minor: Qt {latest_minor}.x") + if current_version and current_version != "unknown": + current_minor = ".".join(current_version.split(".")[:2]) + if current_minor != latest_minor: + log_warn(f"Newer Qt minor version available: {latest_minor}") + else: + log_ok("Using latest Qt minor version") + + for candidate in (["qmake", "--version"], ["qtpaths", "--qt-version"]): + result = subprocess.run(candidate, capture_output=True, text=True, check=False) + if result.returncode != 0: + continue + match = re.search(r"\d+\.\d+\.\d+", result.stdout) + if match: + print(f" Installed: Qt {match.group(0)}") + return + + +def check_gstreamer_version() -> None: + """Check configured and installed GStreamer versions.""" + log_info("Checking GStreamer version...") + current_version = get_build_config_value( + "gstreamer_version", + "unknown", + start=Path(__file__).resolve(), + ) + print(f" Configured: GStreamer {current_version}") + + result = subprocess.run( + ["gst-launch-1.0", "--version"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + return + match = re.search(r"\d+\.\d+\.\d+", result.stdout) + if match: + print(f" Installed: GStreamer {match.group(0)}") + + +def find_python_manifests(repo_root: Path) -> list[Path]: + """Return dependency manifest files present in the repo.""" + return [path for path in REQ_FILES if (repo_root / path).exists()] + + +def check_python_deps(repo_root: Path) -> None: + """Check installed Python packages for available updates.""" + log_info("Checking Python dependencies...") + manifests = find_python_manifests(repo_root) + if not manifests: + log_warn("No Python dependency manifests found") + return + + print(" Manifests:") + for manifest in manifests: + print(f" - {manifest}") + + result = subprocess.run( + [sys.executable, "-m", "pip", "list", "--outdated", "--format=json"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + log_warn("Could not query installed Python package updates") + return + + try: + outdated = json.loads(result.stdout or "[]") + except json.JSONDecodeError: + log_warn("Could not parse pip outdated output") + return + + if not outdated: + log_ok("No outdated installed Python packages reported") + return + + print(" Outdated installed packages:") + for package in outdated: + name = package.get("name", "unknown") + current = package.get("version", "?") + latest = package.get("latest_version", "?") + print(f" - {name}: {current} -> {latest}") + + +def check_build_tools() -> None: + """Print installed build tool versions.""" + log_info("Checking build tools...") + for tool in BUILD_TOOLS: + result = subprocess.run([tool, "--version"], capture_output=True, text=True, check=False) + if result.returncode != 0: + print(f" - {tool}: not installed") + continue + first_line = result.stdout.splitlines()[0] if result.stdout else "unknown" + print(f" - {tool}: {first_line}") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Check dependency versions and tool availability.") + parser.add_argument("--submodules", action="store_true", help="Check only git submodules") + parser.add_argument("--qt", action="store_true", help="Check only Qt version") + parser.add_argument("--update", action="store_true", help="Update submodules to latest upstream") + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested dependency checks.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + + check_all = not args.submodules and not args.qt + if check_all or args.submodules: + check_submodules(repo_root, update=args.update) + if check_all: + print() + check_qt_version() + print() + check_gstreamer_version() + print() + check_python_deps(repo_root) + print() + check_build_tools() + elif args.qt: + check_qt_version() + + print() + log_ok("Dependency check complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/clean.sh b/tools/clean.sh index 9dde8c0dd343..37592722d7ea 100755 --- a/tools/clean.sh +++ b/tools/clean.sh @@ -18,18 +18,7 @@ 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} $*"; } +source "$REPO_ROOT/tools/common/shell-utils.sh" # Defaults CLEAN_ALL=false diff --git a/tools/common/__init__.py b/tools/common/__init__.py index 7f47d8497c68..5798fdcfe815 100644 --- a/tools/common/__init__.py +++ b/tools/common/__init__.py @@ -34,6 +34,22 @@ parse_json_documents, list_workflow_runs_for_sha, list_run_artifacts, + write_github_output, +) + +from .build_config import ( + find_build_config, + load_build_config, + get_build_config_value, + export_build_config_values, + derive_ios_qt_modules, +) + +from .github_runs import ( + parse_created_at, + is_newer_run, + select_latest_runs_by_name, + group_runs_by_name, ) from .logging import ( @@ -89,6 +105,16 @@ 'parse_json_documents', 'list_workflow_runs_for_sha', 'list_run_artifacts', + 'write_github_output', + 'find_build_config', + 'load_build_config', + 'get_build_config_value', + 'export_build_config_values', + 'derive_ios_qt_modules', + 'parse_created_at', + 'is_newer_run', + 'select_latest_runs_by_name', + 'group_runs_by_name', # Logging 'Color', 'Colors', diff --git a/tools/common/build_config.py b/tools/common/build_config.py new file mode 100644 index 000000000000..9af1553b61d6 --- /dev/null +++ b/tools/common/build_config.py @@ -0,0 +1,127 @@ +"""Shared helpers for reading and exporting .github/build-config.json.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + +CONFIG_ENV_VAR = "CONFIG_FILE" +CONFIG_RELATIVE_PATH = Path(".github") / "build-config.json" +DEFAULT_EXPORT_KEYS = [ + "qt_version", + "qt_minimum_version", + "qt_modules", + "gstreamer_minimum_version", + "gstreamer_default_version", + "gstreamer_macos_version", + "gstreamer_android_version", + "gstreamer_windows_version", + "xcode_version", + "xcode_ios_version", + "ndk_version", + "ndk_full_version", + "java_version", + "android_platform", + "android_min_sdk", + "android_build_tools", + "android_cmdline_tools", + "cmake_minimum_version", + "macos_deployment_target", + "ios_deployment_target", + "platform_workflows", +] +IOS_QT_MODULE_EXCLUDES = {"qtserialport", "qtscxml"} + + +def find_build_config( + start: Path | None = None, + *, + extra_candidates: list[Path] | None = None, +) -> Path: + """Find build-config.json by env override, extra candidates, or repo search.""" + if env_path := os.environ.get(CONFIG_ENV_VAR): + return Path(env_path) + + for candidate in extra_candidates or []: + if candidate.exists(): + return candidate + + current = (start or Path(__file__).resolve()).resolve() + if current.is_file(): + current = current.parent + + while current != current.parent: + config_path = current / CONFIG_RELATIVE_PATH + if config_path.exists(): + return config_path + current = current.parent + + raise FileNotFoundError(f"Could not find {CONFIG_RELATIVE_PATH}") + + +def load_build_config( + config_file: Path | None = None, + *, + start: Path | None = None, + extra_candidates: list[Path] | None = None, +) -> dict[str, Any]: + """Load and parse the build config JSON file.""" + path = config_file or find_build_config(start, extra_candidates=extra_candidates) + with path.open(encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + raise ValueError(f"Expected JSON object in {path}") + return data + + +def get_build_config_value( + key: str, + default: str = "", + *, + config_file: Path | None = None, + start: Path | None = None, + extra_candidates: list[Path] | None = None, +) -> str: + """Return a string value from the build config or *default*.""" + try: + config = load_build_config(config_file, start=start, extra_candidates=extra_candidates) + except (FileNotFoundError, OSError, json.JSONDecodeError, ValueError): + return default + value = config.get(key, default) + return str(value) if value is not None else default + + +def derive_ios_qt_modules(qt_modules: str) -> str: + """Return the iOS-safe Qt module list.""" + modules = [module for module in qt_modules.split() if module not in IOS_QT_MODULE_EXCLUDES] + return " ".join(modules) + + +_EXPORT_KEY_ALIASES: dict[str, str] = { + "gstreamer_default_version": "GSTREAMER_VERSION", +} + + +def export_build_config_values( + config: dict[str, Any], + *, + keys: list[str] | None = None, +) -> dict[str, str]: + """Return uppercase env-style values exported from the config.""" + exported: dict[str, str] = {} + for key in keys or DEFAULT_EXPORT_KEYS: + if key in config: + export_name = _EXPORT_KEY_ALIASES.get(key, key.upper()) + exported[export_name] = str(config[key]) + return exported + + +def format_github_output(values: dict[str, str]) -> str: + """Format values for writing to $GITHUB_OUTPUT.""" + lines = [f"{key.lower()}={value}" for key, value in sorted(values.items())] + qt_modules = values.get("QT_MODULES", "") + if qt_modules: + lines.append(f"qt_modules_ios={derive_ios_qt_modules(qt_modules)}") + return "\n".join(lines) diff --git a/tools/common/gh_actions.py b/tools/common/gh_actions.py index f30aab74e132..e4253addec68 100644 --- a/tools/common/gh_actions.py +++ b/tools/common/gh_actions.py @@ -8,10 +8,6 @@ import subprocess import time from typing import Any -from urllib import error as urllib_error -from urllib import parse as urllib_parse -from urllib import request as urllib_request - def gh(*args: str, check: bool = True) -> subprocess.CompletedProcess: """Run a gh CLI command and return the process result.""" @@ -32,62 +28,65 @@ def _should_use_http_api() -> bool: return mode == "http" and bool(_github_token()) -def _extract_next_link(link_header: str) -> str: - for part in link_header.split(","): - section = part.strip() - if 'rel="next"' not in section: - continue - start = section.find("<") - end = section.find(">", start + 1) - if start != -1 and end != -1: - return section[start + 1:end] - return "" +def _build_http_client(): + import httpx + transport = httpx.HTTPTransport(retries=3) + base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/") + return httpx.Client( + base_url=base_url, + transport=transport, + timeout=30.0, + headers={ + "Accept": "application/vnd.github+json", + "User-Agent": "qgc-ci-gh-actions/1.0", + "X-GitHub-Api-Version": "2022-11-28", + "Authorization": f"Bearer {_github_token()}", + }, + ) + + +def _retry_after_seconds(value: str, fallback: float) -> float: + """Parse a Retry-After header value, falling back to a simple backoff.""" + try: + retry_after = float(value) + except (TypeError, ValueError): + return fallback + return retry_after if retry_after > 0 else fallback + + +def _http_get_with_retries(client, url: str, **kwargs) -> Any: + """GET a GitHub API endpoint with retries for transient status failures.""" + import httpx + retryable_statuses = {429, 500, 502, 503, 504} -def _http_get_json(url: str, headers: dict[str, str], retries: int = 3) -> tuple[dict[str, Any], str]: - for attempt in range(1, retries + 1): - req = urllib_request.Request(url, headers=headers, method="GET") + for attempt in range(1, 4): try: - with urllib_request.urlopen(req, timeout=30) as resp: - payload = resp.read().decode("utf-8") - doc = json.loads(payload) - if not isinstance(doc, dict): - raise ValueError(f"Unexpected non-object response from GitHub API at {url}") - next_url = _extract_next_link(resp.headers.get("Link", "")) - return doc, next_url - except urllib_error.HTTPError as exc: - if attempt < retries and exc.code in {429, 500, 502, 503, 504}: - time.sleep(attempt) - continue - raise RuntimeError(f"GitHub API request failed ({exc.code}) for {url}: {exc.reason}") from exc - except (urllib_error.URLError, TimeoutError, json.JSONDecodeError, ValueError) as exc: - if attempt < retries: - time.sleep(attempt) - continue - raise RuntimeError(f"GitHub API request failed for {url}: {exc}") from exc - - raise RuntimeError(f"GitHub API request failed for {url}: exhausted retries") + resp = client.get(url, **kwargs) + resp.raise_for_status() + return resp + except httpx.HTTPStatusError as exc: + status_code = exc.response.status_code + if attempt >= 3 or status_code not in retryable_statuses: + raise + delay = _retry_after_seconds(exc.response.headers.get("Retry-After", ""), float(attempt)) + except httpx.RequestError: + if attempt >= 3: + raise + delay = float(attempt) + + time.sleep(delay) def _http_paginated_docs(path: str, params: dict[str, str]) -> list[dict[str, Any]]: - base_url = os.environ.get("GITHUB_API_URL", "https://api.github.com").rstrip("/") - headers = { - "Accept": "application/vnd.github+json", - "User-Agent": "qgc-ci-gh-actions/1.0", - "X-GitHub-Api-Version": "2022-11-28", - "Authorization": f"Bearer {_github_token()}", - } - - query = urllib_parse.urlencode(params) - url = f"{base_url}/{path.lstrip('/')}" - if query: - url = f"{url}?{query}" - docs: list[dict[str, Any]] = [] - while url: - doc, next_url = _http_get_json(url, headers=headers) - docs.append(doc) - url = next_url + with _build_http_client() as client: + resp = _http_get_with_retries(client, f"/{path.lstrip('/')}", params=params) + docs.append(resp.json()) + + while "next" in resp.links: + resp = _http_get_with_retries(client, resp.links["next"]["url"]) + docs.append(resp.json()) return docs @@ -181,3 +180,74 @@ def list_run_artifacts(repo: str, run_id: int | str) -> list[dict[str, Any]]: if isinstance(items, list): artifacts.extend(item for item in items if isinstance(item, dict)) return artifacts + + +def parse_csv_list(value: str) -> list[str]: + """Parse comma-separated values into a trimmed non-empty list.""" + return [item.strip() for item in value.split(",") if item.strip()] + + +def is_fork_pr() -> bool: + """Check if the current event is a PR from a fork repository.""" + event = os.environ.get("EVENT_NAME", os.environ.get("GITHUB_EVENT_NAME", "")) + if event != "pull_request": + return False + pr_repo = os.environ.get("PR_REPO", "").strip() + this_repo = os.environ.get("THIS_REPO", os.environ.get("GITHUB_REPOSITORY", "")).strip() + return bool(pr_repo and this_repo and pr_repo != this_repo) + + +def resolve_cache_policy(requested: str) -> str: + """Resolve cache save policy. + + Args: + requested: "auto", "true", or "false" + + Returns: + "true" or "false" + """ + if requested != "auto": + return requested + return "false" if is_fork_pr() else "true" + + +def write_github_output(outputs: dict[str, str]) -> None: + """Write key=value pairs to $GITHUB_OUTPUT for GitHub Actions. + + Handles multiline values using hash-based heredoc delimiters. + """ + import hashlib + + github_output = os.environ.get('GITHUB_OUTPUT') + if not github_output: + return + + with open(github_output, 'a', encoding="utf-8") as f: + for key, value in outputs.items(): + if "\n" in value: + value_hash = hashlib.sha256(value.encode("utf-8")).hexdigest()[:12] + delim = f"EOF_{key}_{value_hash}" + while delim in value: + delim = f"{delim}_X" + f.write(f"{key}<<{delim}\n{value}\n{delim}\n") + else: + f.write(f"{key}={value}\n") + + +def write_step_summary(markdown: str) -> None: + """Append markdown content to $GITHUB_STEP_SUMMARY.""" + path = os.environ.get("GITHUB_STEP_SUMMARY") + if not path: + return + with open(path, "a", encoding="utf-8") as f: + f.write(markdown) + + +def append_github_env(values: dict[str, str]) -> None: + """Append environment variables to $GITHUB_ENV.""" + path = os.environ.get("GITHUB_ENV") + if not path: + return + with open(path, "a", encoding="utf-8") as f: + for key, value in values.items(): + f.write(f"{key}={value}\n") diff --git a/tools/common/github_runs.py b/tools/common/github_runs.py new file mode 100644 index 000000000000..f29093708015 --- /dev/null +++ b/tools/common/github_runs.py @@ -0,0 +1,90 @@ +"""Shared helpers for selecting and grouping GitHub workflow runs.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + + +def parse_created_at(created_at: Any) -> datetime | None: + """Parse GitHub ISO-8601 timestamps into datetimes.""" + value = str(created_at).strip() + if not value: + return None + if value.endswith("Z"): + value = f"{value[:-1]}+00:00" + try: + return datetime.fromisoformat(value) + except ValueError: + return None + + +def is_newer_run(candidate: dict[str, Any], existing: dict[str, Any]) -> bool: + """Return True when *candidate* is newer than *existing*.""" + candidate_created_at = str(candidate.get("created_at", "")) + existing_created_at = str(existing.get("created_at", "")) + candidate_dt = parse_created_at(candidate_created_at) + existing_dt = parse_created_at(existing_created_at) + if candidate_dt is not None and existing_dt is not None: + return candidate_dt > existing_dt + return candidate_created_at > existing_created_at + + +def select_latest_runs_by_name( + runs: list[dict[str, Any]], + names: set[str], + *, + event: str = "", + status: str = "", + conclusion: str = "", +) -> dict[str, dict[str, Any]]: + """Return the latest workflow run per name after optional filtering.""" + latest: dict[str, dict[str, Any]] = {} + for run in runs: + name = str(run.get("name", "")) + if name not in names: + continue + if event and str(run.get("event", "")) != event: + continue + if status and str(run.get("status", "")) != status: + continue + if conclusion and str(run.get("conclusion", "")) != conclusion: + continue + existing = latest.get(name) + if existing is None or is_newer_run(run, existing): + latest[name] = run + return latest + + +def group_runs_by_name( + runs: list[dict[str, Any]], + names: list[str], + *, + event: str = "", + status: str = "", + conclusion: str = "", +) -> dict[str, list[dict[str, Any]]]: + """Group workflow runs by name after optional filtering, newest first.""" + target = set(names) + grouped: dict[str, list[dict[str, Any]]] = {name: [] for name in names} + for run in runs: + name = str(run.get("name", "")) + if name not in target: + continue + if event and str(run.get("event", "")) != event: + continue + if status and str(run.get("status", "")) != status: + continue + if conclusion and str(run.get("conclusion", "")) != conclusion: + continue + grouped[name].append(run) + + for name in grouped: + grouped[name].sort( + key=lambda run: ( + parse_created_at(run.get("created_at")) is not None, + str(run.get("created_at", "")), + ), + reverse=True, + ) + return grouped diff --git a/tools/common/shell-utils.sh b/tools/common/shell-utils.sh new file mode 100644 index 000000000000..591741656f04 --- /dev/null +++ b/tools/common/shell-utils.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Shared shell utilities for QGC developer tools. +# Source this file at the top of shell scripts: +# source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/common/shell-utils.sh" + +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; } diff --git a/tools/coverage.py b/tools/coverage.py new file mode 100644 index 000000000000..01b004cdb34a --- /dev/null +++ b/tools/coverage.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Generate code coverage reports for QGroundControl.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.logging import log_error, log_info, log_ok + +LINE_COVERAGE_RE = re.compile(r"lines:\s*(.+)") +BRANCH_COVERAGE_RE = re.compile(r"branches:\s*(.+)") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Build and report test coverage.") + parser.add_argument( + "--mode", + choices=["full", "report-only"], + default="full", + help="Coverage generation mode (default: full)", + ) + parser.add_argument("-r", "--report", action="store_true", help="Generate report only") + parser.add_argument("-o", "--open", dest="open_report", action="store_true", help="Open HTML report") + parser.add_argument("-c", "--clean", action="store_true", help="Clean coverage data") + parser.add_argument("--xml", action="store_true", help="Generate coverage report and highlight XML output path") + parser.add_argument("--log-file", default="", help="Write coverage target output to a file") + parser.add_argument( + "--step-summary", + action="store_true", + help="Append a coverage summary to GITHUB_STEP_SUMMARY if available", + ) + parser.add_argument( + "-b", + "--build-dir", + default="build-coverage", + help="Coverage build directory (default: build-coverage)", + ) + return parser.parse_args(argv) + + +def run_command( + cmd: list[str], + *, + cwd: Path | None = None, + capture_output: bool = False, +) -> subprocess.CompletedProcess[str]: + """Run a subprocess and fail fast on errors.""" + return subprocess.run(cmd, cwd=cwd, check=True, capture_output=capture_output, text=True) + + +def check_dependencies() -> None: + """Ensure required tooling is installed.""" + if shutil.which("gcovr") is None: + raise RuntimeError("gcovr not found. Install with: pip install gcovr") + + +def clean_coverage(build_dir: Path) -> None: + """Remove coverage build artifacts.""" + log_info("Cleaning coverage data...") + shutil.rmtree(build_dir, ignore_errors=True) + log_ok("Coverage data cleaned") + + +def configure_build(repo_root: Path, build_dir: Path) -> None: + """Configure the coverage build directory.""" + cache_path = build_dir / "CMakeCache.txt" + if cache_path.exists(): + cache_text = cache_path.read_text(encoding="utf-8", errors="ignore") + if "QGC_ENABLE_COVERAGE:BOOL=ON" in cache_text: + log_info("Using existing coverage build configuration") + return + + log_info("Configuring build with coverage...") + run_command( + [ + "cmake", + "-B", + str(build_dir), + "-S", + str(repo_root), + "-DCMAKE_BUILD_TYPE=Debug", + "-DQGC_ENABLE_COVERAGE=ON", + "-DQGC_BUILD_TESTING=ON", + "-G", + "Ninja", + ] + ) + log_ok("Build configured") + + +def build_project(build_dir: Path) -> None: + """Build the coverage target.""" + log_info("Building project...") + run_command(["cmake", "--build", str(build_dir), "--parallel"]) + log_ok("Build complete") + + +def run_tests(build_dir: Path) -> None: + """Run tests and fail if any test fails.""" + log_info("Running tests...") + run_command(["ctest", "--test-dir", str(build_dir), "--output-on-failure", "--timeout", "300"]) + + +def build_step_summary(log_text: str, mode: str) -> str: + """Build a GitHub Step Summary section from gcovr output.""" + lines = ["## Code Coverage", "", f"Mode: `{mode}`", ""] + line_match = LINE_COVERAGE_RE.search(log_text) + branch_match = BRANCH_COVERAGE_RE.search(log_text) + if line_match: + lines.extend( + [ + "| Metric | Coverage |", + "|--------|----------|", + f"| Lines | {line_match.group(1).strip()} |", + f"| Branches | {(branch_match.group(1).strip() if branch_match else 'N/A')} |", + "", + "[View detailed report](coverage.html)", + ] + ) + else: + lines.append("Coverage data not available") + return "\n".join(lines) + "\n" + + +def maybe_write_step_summary(log_text: str, mode: str) -> None: + """Append coverage markdown to GITHUB_STEP_SUMMARY when requested.""" + summary_path = os.environ.get("GITHUB_STEP_SUMMARY") + if not summary_path: + return + with open(summary_path, "a", encoding="utf-8") as handle: + handle.write(build_step_summary(log_text, mode)) + + +def generate_report(build_dir: Path, *, xml_only: bool, log_file: Path | None = None, mode: str = "full") -> str: + """Generate coverage artifacts from existing coverage data.""" + log_info("Generating coverage report...") + result = run_command( + ["cmake", "--build", str(build_dir), "--target", "coverage-report"], + capture_output=True, + ) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + if log_file is not None: + log_file.write_text((result.stdout or "") + (result.stderr or ""), encoding="utf-8") + print() + log_ok("Coverage report generated") + log_info(f" XML: {build_dir / 'coverage.xml'}") + if not xml_only: + log_info(f" HTML: {build_dir / 'coverage.html'}") + return (result.stdout or "") + (result.stderr or "") + + +def open_report(build_dir: Path) -> None: + """Open the generated HTML coverage report.""" + report = build_dir / "coverage.html" + if not report.exists(): + raise FileNotFoundError(f"Report not found: {report}") + + log_info("Opening coverage report...") + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError(f"Could not find a browser opener. Report at: {report}") + subprocess.run([opener, str(report)], check=False) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested coverage workflow.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + build_dir = Path(args.build_dir) + mode = "report-only" if args.report else args.mode + if not build_dir.is_absolute(): + build_dir = repo_root / build_dir + log_file = Path(args.log_file) if args.log_file else None + if log_file is not None and not log_file.is_absolute(): + log_file = repo_root / log_file + + try: + check_dependencies() + if args.clean: + clean_coverage(build_dir) + return 0 + + if mode == "report-only": + report_output = generate_report(build_dir, xml_only=args.xml, log_file=log_file, mode=mode) + else: + configure_build(repo_root, build_dir) + build_project(build_dir) + run_tests(build_dir) + report_output = generate_report(build_dir, xml_only=args.xml, log_file=log_file, mode=mode) + + if args.open_report: + open_report(build_dir) + if args.step_summary: + maybe_write_step_summary(report_output, mode) + return 0 + except (RuntimeError, FileNotFoundError, subprocess.CalledProcessError) as exc: + log_error(str(exc)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/coverage.sh b/tools/coverage.sh deleted file mode 100755 index 3f8df907a96f..000000000000 --- a/tools/coverage.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/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 report only (after tests) -# ./tools/coverage.sh --open # Generate and open report in browser -# ./tools/coverage.sh --clean # Clean coverage data -# ./tools/coverage.sh --xml # Generate XML only (for CI tools) -# -# Requirements: -# - gcovr (pip install gcovr) -# -# This script wraps CMake coverage targets defined in cmake/modules/Coverage.cmake - -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" -REPORT_ONLY=false -OPEN_REPORT=false -CLEAN_ONLY=false -XML_ONLY=false - -show_help() { - head -14 "$0" | tail -12 - 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 - ;; - --xml) - XML_ONLY=true - shift - ;; - -b|--build-dir) - BUILD_DIR="$2" - shift 2 - ;; - *) - log_error "Unknown option: $1" - exit 1 - ;; - esac -done - -check_dependencies() { - if ! command -v gcovr &> /dev/null; then - log_error "gcovr not found" - log_info "Install with: pip install gcovr" - exit 1 - fi -} - -clean_coverage() { - log_info "Cleaning coverage data..." - rm -rf "$BUILD_DIR" - log_ok "Coverage data cleaned" -} - -configure_build() { - if [[ -f "$BUILD_DIR/CMakeCache.txt" ]]; then - # Check if already configured with coverage - if grep -q "QGC_ENABLE_COVERAGE:BOOL=ON" "$BUILD_DIR/CMakeCache.txt" 2>/dev/null; then - log_info "Using existing coverage build configuration" - return - fi - fi - - log_info "Configuring build with coverage..." - cmake -B "$BUILD_DIR" -S "$REPO_ROOT" \ - -DCMAKE_BUILD_TYPE=Debug \ - -DQGC_ENABLE_COVERAGE=ON \ - -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..." - # Run via ctest for proper test discovery - ctest --test-dir "$BUILD_DIR" --output-on-failure --timeout 300 || true - log_ok "Tests complete" -} - -generate_report() { - log_info "Generating coverage report..." - - if [[ "$XML_ONLY" == true ]]; then - cmake --build "$BUILD_DIR" --target coverage-report - else - cmake --build "$BUILD_DIR" --target coverage-report - fi - - echo "" - log_ok "Coverage report generated:" - log_info " HTML: $BUILD_DIR/coverage.html" - log_info " XML: $BUILD_DIR/coverage.xml" -} - -open_report() { - local report="$BUILD_DIR/coverage.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/debuggers/profile.sh b/tools/debuggers/profile.sh index 1fa225bdac53..2cdbea93f0aa 100755 --- a/tools/debuggers/profile.sh +++ b/tools/debuggers/profile.sh @@ -23,19 +23,8 @@ 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; } +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +source "$REPO_ROOT/tools/common/shell-utils.sh" # Defaults BUILD_DIR="$REPO_ROOT/build" @@ -122,8 +111,8 @@ run_memcheck() { --track-origins=yes \ --verbose \ --log-file="$output" \ - --suppressions="$REPO_ROOT/tools/debuggers/valgrind.supp" 2>/dev/null || true \ - "$EXECUTABLE" $EXTRA_ARGS + --suppressions="$REPO_ROOT/tools/debuggers/valgrind.supp" \ + "$EXECUTABLE" $EXTRA_ARGS || true log_ok "Memcheck complete. Results: $output" diff --git a/tools/generate-docs.sh b/tools/generate-docs.sh deleted file mode 100755 index a879d9f8493a..000000000000 --- a/tools/generate-docs.sh +++ /dev/null @@ -1,239 +0,0 @@ -#!/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/generate_docs.py b/tools/generate_docs.py new file mode 100644 index 000000000000..b15aafeba7c7 --- /dev/null +++ b/tools/generate_docs.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Generate API documentation using Doxygen.""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.logging import log_error, log_info, log_ok, log_warn + +DEFAULT_DOXYFILE = """# Doxyfile for QGroundControl + +PROJECT_NAME = "QGroundControl" +PROJECT_BRIEF = "Ground Control Station for MAVLink Drones" +OUTPUT_DIRECTORY = docs/api +INPUT = src +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h *.cc *.cpp *.hpp *.qml *.md +RECURSIVE = YES +EXCLUDE_PATTERNS = */test/* */libs/* */_deps/* */build/* +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_LATEX = NO +GENERATE_XML = NO +HAVE_DOT = YES +QUIET = NO +WARNINGS = YES +""" + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description="Generate QGroundControl API documentation.") + parser.add_argument("-o", "--open", dest="open_docs", action="store_true", help="Open docs after generating") + parser.add_argument("--pdf", action="store_true", help="Generate PDF output") + parser.add_argument("-c", "--clean", action="store_true", help="Clean generated docs") + parser.add_argument("--output-dir", default="docs/api", help="Output directory (default: docs/api)") + parser.add_argument("--doxyfile", default="Doxyfile", help="Path to Doxyfile (default: Doxyfile)") + return parser.parse_args(argv) + + +def build_doxyfile_text(base_text: str, output_dir: Path, *, generate_pdf: bool) -> str: + """Return a Doxygen config with safe per-run overrides appended.""" + latex_value = "YES" if generate_pdf else "NO" + override = ( + "\n" + f'OUTPUT_DIRECTORY = "{output_dir.as_posix()}"\n' + f"GENERATE_LATEX = {latex_value}\n" + ) + return base_text.rstrip() + override + + +def check_dependencies(*, generate_pdf: bool) -> None: + """Ensure the required tooling is installed.""" + if shutil.which("doxygen") is None: + raise RuntimeError("doxygen not found. Install with: sudo apt install doxygen") + if shutil.which("dot") is None: + log_warn("graphviz not found. Diagrams will be disabled.") + if generate_pdf and shutil.which("pdflatex") is None: + raise RuntimeError("pdflatex not found. Install with: sudo apt install texlive-latex-base") + + +def clean_docs(output_dir: Path) -> None: + """Remove generated documentation.""" + log_info("Cleaning generated documentation...") + shutil.rmtree(output_dir, ignore_errors=True) + log_ok("Documentation cleaned") + + +def load_base_doxyfile(doxyfile_path: Path) -> str: + """Load the repo Doxyfile or fall back to a generated default.""" + if doxyfile_path.exists(): + return doxyfile_path.read_text(encoding="utf-8") + log_warn(f"{doxyfile_path.name} not found. Using generated default configuration.") + return DEFAULT_DOXYFILE + + +def generate_docs(repo_root: Path, output_dir: Path, doxyfile_path: Path, *, generate_pdf: bool) -> None: + """Generate HTML docs and optionally PDF docs without mutating tracked files.""" + log_info("Generating documentation...") + output_dir.mkdir(parents=True, exist_ok=True) + + base_text = load_base_doxyfile(doxyfile_path) + effective_text = build_doxyfile_text(base_text, output_dir, generate_pdf=generate_pdf) + + with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".doxyfile", delete=False) as handle: + handle.write(effective_text) + temp_doxyfile = Path(handle.name) + + try: + subprocess.run(["doxygen", str(temp_doxyfile)], cwd=repo_root, check=True) + finally: + temp_doxyfile.unlink(missing_ok=True) + + log_ok(f"Documentation generated: {output_dir / 'html' / 'index.html'}") + + if generate_pdf: + latex_dir = output_dir / "latex" + log_info("Generating PDF...") + subprocess.run(["make", "pdf"], cwd=latex_dir, check=True) + log_ok(f"PDF generated: {latex_dir / 'refman.pdf'}") + + +def open_docs(output_dir: Path) -> None: + """Open the generated HTML documentation.""" + index = output_dir / "html" / "index.html" + if not index.exists(): + raise FileNotFoundError(f"Documentation not found: {index}") + + log_info("Opening documentation...") + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError(f"Could not find a browser opener. Docs at: {index}") + subprocess.run([opener, str(index)], check=False) + + +def main(argv: list[str] | None = None) -> int: + """Run the requested documentation workflow.""" + args = parse_args(argv) + repo_root = Path(__file__).resolve().parent.parent + output_dir = Path(args.output_dir) + doxyfile_path = Path(args.doxyfile) + if not output_dir.is_absolute(): + output_dir = repo_root / output_dir + if not doxyfile_path.is_absolute(): + doxyfile_path = repo_root / doxyfile_path + + try: + if args.clean: + clean_docs(output_dir) + return 0 + + check_dependencies(generate_pdf=args.pdf) + generate_docs(repo_root, output_dir, doxyfile_path, generate_pdf=args.pdf) + if args.open_docs: + open_docs(output_dir) + return 0 + except (RuntimeError, FileNotFoundError, subprocess.CalledProcessError) as exc: + log_error(str(exc)) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/param-docs.py b/tools/param-docs.py index be95aecabf9f..d294bc401b74 100755 --- a/tools/param-docs.py +++ b/tools/param-docs.py @@ -285,7 +285,7 @@ def generate_json_output(params: list[Parameter]) -> str: return json.dumps(output, indent=2) -def main(): +def main() -> int: parser = argparse.ArgumentParser( description="Generate parameter documentation from FactMetaData JSON files", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/tools/pre-commit.sh b/tools/pre-commit.sh deleted file mode 100755 index 04a0b906e151..000000000000 --- a/tools/pre-commit.sh +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env bash -# -# Run pre-commit checks with formatted output -# -# Usage: -# ./tools/pre-commit.sh # Run on all files -# ./tools/pre-commit.sh --changed # Run only on changed files (vs master) -# ./tools/pre-commit.sh --install # Install pre-commit hooks -# ./tools/pre-commit.sh --update # Update hook versions -# ./tools/pre-commit.sh --ci # CI mode (machine-readable output) -# -# Environment: -# PRE_COMMIT_OUTPUT - File to write output (default: stdout) -# GITHUB_STEP_SUMMARY - GitHub Actions step summary file -# - -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 -MODE="all" -CI_MODE=false -OUTPUT_FILE="" - -show_help() { - head -13 "$0" | tail -11 - exit 0 -} - -# Parse arguments -while [[ $# -gt 0 ]]; do - case $1 in - -h|--help) - show_help - ;; - -c|--changed) - MODE="changed" - shift - ;; - -i|--install) - MODE="install" - shift - ;; - -u|--update) - MODE="update" - shift - ;; - --ci) - CI_MODE=true - shift - ;; - -o|--output) - OUTPUT_FILE="$2" - shift 2 - ;; - *) - log_error "Unknown option: $1" - show_help - ;; - esac -done - -cd "$REPO_ROOT" - -# Check for pre-commit -if ! command -v pre-commit &> /dev/null; then - log_error "pre-commit not found" - log_info "Install with: pip install pre-commit" - log_info "Or run: ./tools/pre-commit.sh --install" - exit 1 -fi - -# Handle install/update modes -if [[ "$MODE" == "install" ]]; then - log_info "Installing pre-commit and hooks..." - pip install pre-commit - pre-commit install - pre-commit install --hook-type commit-msg - log_ok "Pre-commit hooks installed" - exit 0 -fi - -if [[ "$MODE" == "update" ]]; then - log_info "Updating pre-commit hooks..." - pre-commit autoupdate - log_ok "Hooks updated. Review changes in .pre-commit-config.yaml" - exit 0 -fi - -# Build pre-commit arguments -ARGS=( - "--show-diff-on-failure" - "--color=always" -) - -if [[ "$MODE" == "changed" ]]; then - # Check if we can compare against master - # SECURITY: Branch name "master" is hardcoded to prevent command injection. - # If making this dynamic, validate: [[ "$branch" =~ ^[a-zA-Z0-9/_-]+$ ]] - if git rev-parse --verify master &>/dev/null || git rev-parse --verify origin/master &>/dev/null; then - log_info "Running on files changed vs master..." - ARGS+=("--from-ref" "master" "--to-ref" "HEAD") - else - log_warn "master branch not available, running on all files" - ARGS+=("--all-files") - fi -else - ARGS+=("--all-files") -fi - -# Create temp file for output capture -TEMP_OUTPUT=$(mktemp) -trap 'rm -f "$TEMP_OUTPUT"' EXIT - -log_info "Running pre-commit checks..." -echo "" - -# Run pre-commit and capture output -set +e -pre-commit run "${ARGS[@]}" 2>&1 | tee "$TEMP_OUTPUT" -EXIT_CODE=${PIPESTATUS[0]} -set -e - -echo "" - -# Parse results - single awk pass for efficiency -read -r PASSED FAILED SKIPPED < <(awk '/Passed/{p++} /Failed/{f++} /Skipped/{s++} END{print p+0, f+0, s+0}' "$TEMP_OUTPUT" 2>/dev/null || echo "0 0 0") - -# Summary -if [[ $EXIT_CODE -eq 0 ]]; then - log_ok "All checks passed ($PASSED passed)" -else - log_error "Some checks failed ($PASSED passed, $FAILED failed)" -fi - -# Write to output file if specified -if [[ -n "$OUTPUT_FILE" ]] || [[ -n "${PRE_COMMIT_OUTPUT:-}" ]]; then - OUTPUT_FILE="${OUTPUT_FILE:-$PRE_COMMIT_OUTPUT}" - cp "$TEMP_OUTPUT" "$OUTPUT_FILE" - log_info "Output written to: $OUTPUT_FILE" -fi - -# CI mode: write structured output -if [[ "$CI_MODE" == true ]]; then - # Write to GITHUB_OUTPUT if available - if [[ -n "${GITHUB_OUTPUT:-}" ]]; then - { - echo "exit_code=$EXIT_CODE" - echo "passed=$PASSED" - echo "failed=$FAILED" - echo "skipped=$SKIPPED" - } >> "$GITHUB_OUTPUT" - - # Write summary to GITHUB_OUTPUT (multiline with unique delimiter) - DELIMITER="PRECOMMIT_SUMMARY_$(date +%s)" - { - echo "summary<<${DELIMITER}" - grep -E '\.\.\.\.\.\.\.\.\.\.' "$TEMP_OUTPUT" 2>/dev/null | head -40 | sed 's/\x1b\[[0-9;]*m//g' || echo "No results" - echo "${DELIMITER}" - } >> "$GITHUB_OUTPUT" - fi - - # Write to GITHUB_STEP_SUMMARY if available - if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then - { - echo "## Pre-commit Results" - echo "" - if [[ $EXIT_CODE -eq 0 ]]; then - echo "**All checks passed**" - else - echo "**Some checks failed**" - fi - echo "" - echo "| Status | Count |" - echo "|--------|-------|" - echo "| Passed | $PASSED |" - echo "| Failed | $FAILED |" - if [[ $SKIPPED -gt 0 ]]; then - echo "| Skipped | $SKIPPED |" - fi - echo "" - echo "
" - echo "Hook Results" - echo "" - echo '```' - grep -E '\.\.\.\.\.\.\.\.\.\.' "$TEMP_OUTPUT" 2>/dev/null | head -40 | sed 's/\x1b\[[0-9;]*m//g' || echo "No results" - echo '```' - echo "
" - - # Show modified files if any - if ! git diff --quiet 2>/dev/null; then - echo "" - echo "
" - echo "Files Modified by Hooks" - echo "" - echo '```' - git diff --stat 2>/dev/null || true - echo '```' - echo "
" - fi - } >> "$GITHUB_STEP_SUMMARY" - fi -fi - -# Show fix instructions on failure -if [[ $EXIT_CODE -ne 0 ]]; then - echo "" - log_info "To fix issues locally:" - echo " 1. Run: pre-commit run --all-files" - echo " 2. Review and stage changes: git add -u" - echo " 3. Amend your commit: git commit --amend --no-edit" -fi - -exit $EXIT_CODE diff --git a/tools/pre_commit.py b/tools/pre_commit.py new file mode 100644 index 000000000000..85b7c57a6208 --- /dev/null +++ b/tools/pre_commit.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Run pre-commit checks with CI-friendly summaries.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +from _bootstrap import ensure_tools_dir + +ensure_tools_dir(__file__) + +from common.gh_actions import write_github_output as _write_github_output, write_step_summary as _write_step_summary +from common.logging import log_error, log_info, log_ok, log_warn + +HOOK_RESULT_RE = re.compile(r"\b(Passed|Failed|Skipped)\b") +ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run pre-commit checks.") + parser.add_argument("-c", "--changed", action="store_true", help="Run only files changed vs master") + parser.add_argument("-i", "--install", action="store_true", help="Install pre-commit hooks") + parser.add_argument("-u", "--update", action="store_true", help="Update hook versions") + parser.add_argument("--ci", action="store_true", help="Enable GitHub Actions outputs") + parser.add_argument("-o", "--output", default="", help="Write raw output to file") + return parser.parse_args(argv) + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def ensure_precommit_available() -> bool: + return shutil.which("pre-commit") is not None + + +def strip_ansi(value: str) -> str: + return ANSI_ESCAPE_RE.sub("", value) + + +def summarize_output(output: str) -> tuple[int, int, int]: + passed = failed = skipped = 0 + for line in output.splitlines(): + match = HOOK_RESULT_RE.search(line) + if not match: + continue + state = match.group(1) + if state == "Passed": + passed += 1 + elif state == "Failed": + failed += 1 + elif state == "Skipped": + skipped += 1 + return passed, failed, skipped + + +def extract_hook_lines(output: str, *, limit: int = 40) -> list[str]: + lines = [strip_ansi(line) for line in output.splitlines() if ".........." in line] + return lines[:limit] or ["No results"] + + +def git_has_master_ref() -> bool: + commands = [ + ["git", "rev-parse", "--verify", "master"], + ["git", "rev-parse", "--verify", "origin/master"], + ] + return any(subprocess.run(cmd, capture_output=True, check=False).returncode == 0 for cmd in commands) + + +def build_precommit_args(args: argparse.Namespace) -> list[str]: + result = ["pre-commit", "run", "--show-diff-on-failure", "--color=always"] + if args.changed: + if git_has_master_ref(): + log_info("Running on files changed vs master...") + result.extend(["--from-ref", "master", "--to-ref", "HEAD"]) + else: + log_warn("master branch not available, running on all files") + result.append("--all-files") + else: + result.append("--all-files") + return result + + +def run_command(cmd: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run(cmd, capture_output=True, text=True, check=False) + + +def write_github_output(exit_code: int, passed: int, failed: int, skipped: int, summary_lines: list[str]) -> None: + _write_github_output({ + "exit_code": str(exit_code), + "passed": str(passed), + "failed": str(failed), + "skipped": str(skipped), + "summary": "\n".join(summary_lines), + }) + + +def write_step_summary(exit_code: int, passed: int, failed: int, skipped: int, output: str) -> None: + parts = ["## Pre-commit Results\n"] + parts.append("**All checks passed**\n" if exit_code == 0 else "**Some checks failed**\n") + parts.append("| Status | Count |\n|--------|-------|") + parts.append(f"| Passed | {passed} |") + parts.append(f"| Failed | {failed} |") + if skipped > 0: + parts.append(f"| Skipped | {skipped} |") + hook_lines = "\n".join(extract_hook_lines(output)) + parts.append(f"\n
\nHook Results\n\n```\n{hook_lines}\n```\n
") + + diff = subprocess.run(["git", "diff", "--stat"], capture_output=True, text=True, check=False) + if diff.stdout.strip(): + parts.append(f"\n
\nFiles Modified by Hooks\n\n```\n{diff.stdout}```\n
") + + _write_step_summary("\n".join(parts) + "\n") + + +def handle_install() -> int: + log_info("Installing pre-commit and hooks...") + subprocess.run([sys.executable, "-m", "pip", "install", "pre-commit"], check=True) + subprocess.run(["pre-commit", "install"], check=True) + subprocess.run(["pre-commit", "install", "--hook-type", "commit-msg"], check=True) + log_ok("Pre-commit hooks installed") + return 0 + + +def handle_update() -> int: + log_info("Updating pre-commit hooks...") + subprocess.run(["pre-commit", "autoupdate"], check=True) + log_ok("Hooks updated. Review changes in .pre-commit-config.yaml") + return 0 + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + os.chdir(repo_root()) + + try: + if args.install: + return handle_install() + if args.update: + if not ensure_precommit_available(): + log_error("pre-commit not found") + return 1 + return handle_update() + + if not ensure_precommit_available(): + log_error("pre-commit not found") + log_info("Install with: pip install pre-commit") + log_info("Or run: python3 ./tools/pre_commit.py --install") + return 1 + + command = build_precommit_args(args) + log_info("Running pre-commit checks...") + print() + result = run_command(command) + if result.stdout: + print(result.stdout, end="") + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + print() + + passed, failed, skipped = summarize_output(result.stdout + result.stderr) + if args.output or os.environ.get("PRE_COMMIT_OUTPUT"): + output_path = Path(args.output or os.environ["PRE_COMMIT_OUTPUT"]) + output_path.write_text(result.stdout + result.stderr, encoding="utf-8") + log_info(f"Output written to: {output_path}") + + if result.returncode == 0: + log_ok(f"All checks passed ({passed} passed)") + else: + log_error(f"Some checks failed ({passed} passed, {failed} failed)") + + if args.ci: + summary_lines = extract_hook_lines(result.stdout + result.stderr) + write_github_output(result.returncode, passed, failed, skipped, summary_lines) + write_step_summary(result.returncode, passed, failed, skipped, result.stdout + result.stderr) + + if result.returncode != 0: + print() + log_info("To fix issues locally:") + print(" 1. Run: pre-commit run --all-files") + print(" 2. Review and stage changes: git add -u") + print(" 3. Amend your commit: git commit --amend --no-edit") + + return result.returncode + except subprocess.CalledProcessError as exc: + log_error(str(exc)) + return exc.returncode or 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/pyproject.toml b/tools/pyproject.toml new file mode 100644 index 000000000000..aa70bb12ca55 --- /dev/null +++ b/tools/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "qgc-tools" +version = "0.0.0" +requires-python = ">=3.12" + +[project.optional-dependencies] +scripts = [ + "defusedxml>=0.7.1", + "httpx>=0.28", + "jinja2>=3.1", +] +precommit = [ + "pre-commit", +] +test = [ + "pytest", + "jinja2", + "pyyaml", +] +ci = [ + "pre-commit", + "meson", + "ninja", +] +qt = [ + "aqtinstall", +] +coverage = [ + "gcovr", +] +dev = [ + "jinja2", + "pyyaml", + "pymavlink", +] +lsp = [ + "pygls", + "lsprotocol", +] + +[tool.pytest.ini_options] +pythonpath = ["."] +testpaths = ["tests"] diff --git a/tools/release.sh b/tools/release.sh index 5bcd86a515c6..8f7bad1e337c 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -17,17 +17,7 @@ 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; } +source "$REPO_ROOT/tools/common/shell-utils.sh" DRY_RUN=true INSTALL_DEPS=false diff --git a/tools/run_tests.py b/tools/run_tests.py index 4e5960e2d5a3..afca33ce8783 100755 --- a/tools/run_tests.py +++ b/tools/run_tests.py @@ -27,6 +27,7 @@ from typing import Sequence from common import find_repo_root, Logger +from common.gh_actions import write_github_output as _write_github_output @dataclass @@ -281,20 +282,15 @@ def _build_command(self, binary: Path, test_args: list[str]) -> tuple[list[str], def write_github_output(self, result: TestResult) -> None: """Write results to GITHUB_OUTPUT for CI integration.""" - github_output = os.environ.get("GITHUB_OUTPUT") - if not github_output: - return - - try: - with open(github_output, "a") as f: - f.write(f"exit_code={result.exit_code}\n") - f.write(f"passed={'true' if result.exit_code == 0 else 'false'}\n") - if result.xml_path: - f.write(f"output_file={result.xml_path}\n") - if result.log_path: - f.write(f"log_file={result.log_path}\n") - except OSError as e: - self.log.warn(f"Failed to write GITHUB_OUTPUT: {e}") + outputs: dict[str, str] = { + "exit_code": str(result.exit_code), + "passed": "true" if result.exit_code == 0 else "false", + } + if result.xml_path: + outputs["output_file"] = str(result.xml_path) + if result.log_path: + outputs["log_file"] = str(result.log_path) + _write_github_output(outputs) def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: diff --git a/tools/setup/build-gstreamer.py b/tools/setup/build-gstreamer.py index 250147d1a539..c881d33890b0 100755 --- a/tools/setup/build-gstreamer.py +++ b/tools/setup/build-gstreamer.py @@ -39,6 +39,12 @@ from dataclasses import dataclass, field from pathlib import Path +_tools_dir = Path(__file__).resolve().parents[1] +if str(_tools_dir) not in sys.path: + sys.path.insert(0, str(_tools_dir)) + +from common.logging import log_info, log_ok, log_warn, log_error + # ============================================================================ # Shared Utilities @@ -47,25 +53,6 @@ MESON_VERSION = "1.10.1" NINJA_VERSION = "1.13.0" -class Colors: - RED = '\033[0;31m' - GREEN = '\033[0;32m' - YELLOW = '\033[1;33m' - BLUE = '\033[0;34m' - NC = '\033[0m' - -def log_info(msg: str) -> None: - print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}") - -def log_ok(msg: str) -> None: - print(f"{Colors.GREEN}[OK]{Colors.NC} {msg}") - -def log_warn(msg: str) -> None: - print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}", file=sys.stderr) - -def log_error(msg: str) -> None: - print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}", file=sys.stderr) - def run_cmd(cmd: list, cwd: Path | None = None, check: bool = True, env: dict | None = None) -> subprocess.CompletedProcess: """Run a command with optional working directory and environment.""" @@ -112,8 +99,11 @@ def read_config(key: str, default: str = '') -> str: with open(config_file) as f: config = json.load(f) return config.get(key, default) - except Exception: + except FileNotFoundError: return default + except json.JSONDecodeError as error: + log_error(f"Failed to parse JSON config '{config_file}': {error}") + raise def write_github_output(outputs: dict) -> None: """Write outputs for GitHub Actions.""" @@ -632,7 +622,7 @@ def main() -> int: args = parse_args() # Resolve defaults - version = args.version or read_config('gstreamer_version', '1.24.10') + version = args.version or read_config('gstreamer_default_version', '1.24.13') arch = args.arch or get_default_arch(args.platform) prefix = Path(args.prefix) if args.prefix else None work_dir = Path(args.work_dir) diff --git a/tools/setup/install_dependencies.py b/tools/setup/install_dependencies.py index e72466407b41..2ac7575f20cf 100644 --- a/tools/setup/install_dependencies.py +++ b/tools/setup/install_dependencies.py @@ -16,6 +16,7 @@ import argparse import json import os +import re import shlex import shutil import subprocess @@ -155,6 +156,7 @@ "-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3", ] +PACKAGE_NAME_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9.+-]*$") def get_repo_root() -> Path: @@ -285,6 +287,21 @@ def check_apt_package_available(package: str) -> bool: return result.returncode == 0 +def get_available_debian_packages(category: str) -> list[str]: + """Return packages in *category* that exist in apt metadata.""" + return [pkg for pkg in get_debian_packages(category) if check_apt_package_available(pkg)] + + +def validate_extra_packages(packages: list[str]) -> list[str]: + """Validate extra package names passed from CI inputs.""" + validated: list[str] = [] + for package in packages: + if not PACKAGE_NAME_RE.match(package): + raise ValueError(f"Invalid package name '{package}'") + validated.append(package) + return validated + + def get_gstreamer_macos_urls(version: str) -> tuple[str, str]: """Get GStreamer download URLs for macOS.""" base_url = f"https://gstreamer.freedesktop.org/data/pkg/osx/{version}" @@ -769,6 +786,17 @@ def parse_args(args: list[str] | None = None) -> argparse.Namespace: action="store_true", help="Install Vulkan SDK (Windows only)", ) + parser.add_argument( + "--print-available-packages", + action="store_true", + help="Print available apt packages in the selected Debian category", + ) + parser.add_argument( + "--validate-extra-packages", + nargs="*", + default=None, + help="Validate extra apt package names and print them back", + ) return parser.parse_args(args) @@ -787,6 +815,24 @@ def main() -> int: print_packages(platform or "debian", args.category) return 0 + if args.print_available_packages: + if (platform or "debian") != "debian": + print("Error: --print-available-packages is only supported for debian", file=sys.stderr) + return 1 + if not args.category: + print("Error: --category is required with --print-available-packages", file=sys.stderr) + return 1 + print(" ".join(get_available_debian_packages(args.category))) + return 0 + + if args.validate_extra_packages is not None: + try: + print(" ".join(validate_extra_packages(args.validate_extra_packages))) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + return 0 + if platform is None: print("Error: Could not detect platform", file=sys.stderr) print("Use --platform to specify: debian, macos, windows", file=sys.stderr) diff --git a/tools/setup/install_python.py b/tools/setup/install_python.py index ce2e277b3ff8..cce6366e51fa 100755 --- a/tools/setup/install_python.py +++ b/tools/setup/install_python.py @@ -14,61 +14,44 @@ from __future__ import annotations import argparse +import functools +import os import shutil import subprocess import sys +import tomllib from pathlib import Path -PACKAGE_GROUPS: dict[str, list[str]] = { - "precommit": [ - "pre-commit", - ], - "test": [ - "pytest", - "jinja2", - "pyyaml", - ], - "ci": [ - "pre-commit", - "meson", - "ninja", - ], - "qt": [ - "aqtinstall", - ], - "coverage": [ - "gcovr", - ], - "dev": [ - "jinja2", - "pyyaml", - "pymavlink", - ], - "lsp": [ - "pygls", - "lsprotocol", - ], - "all": [], # Populated below -} - -# 'all' includes everything from other groups -PACKAGE_GROUPS["all"] = list( - set( - pkg - for group, packages in PACKAGE_GROUPS.items() - if group != "all" - for pkg in packages +_tools_dir = Path(__file__).resolve().parents[1] +if str(_tools_dir) not in sys.path: + sys.path.insert(0, str(_tools_dir)) + +from common.file_traversal import find_repo_root + + +@functools.lru_cache(maxsize=1) +def load_package_groups() -> dict[str, list[str]]: + """Load dependency groups from tools/pyproject.toml.""" + pyproject_path = find_repo_root() / "tools" / "pyproject.toml" + data = tomllib.loads(pyproject_path.read_text(encoding="utf-8")) + optional = data.get("project", {}).get("optional-dependencies", {}) + if not isinstance(optional, dict): + raise ValueError("project.optional-dependencies is missing from tools/pyproject.toml") + + groups: dict[str, list[str]] = {} + for group, packages in optional.items(): + if isinstance(packages, list): + groups[group] = [str(pkg) for pkg in packages] + + groups["all"] = sorted( + { + package + for group, packages in groups.items() + if group != "all" + for package in packages + } ) -) - - -def get_repo_root() -> Path: - """Find repository root directory.""" - current = Path(__file__).resolve() - for parent in [current] + list(current.parents): - if (parent / ".git").exists(): - return parent - return Path.cwd() + return groups def has_uv() -> bool: @@ -76,9 +59,19 @@ def has_uv() -> bool: return shutil.which("uv") is not None +def get_lockfile_path() -> Path: + """Return the uv lockfile path for the tools project.""" + return find_repo_root() / "tools" / "uv.lock" + + +def get_project_path() -> Path: + """Return the tools project path.""" + return find_repo_root() / "tools" + + def get_venv_path() -> Path: """Get path to virtual environment.""" - return get_repo_root() / ".venv" + return find_repo_root() / ".venv" def get_activate_script(venv_path: Path) -> Path: @@ -132,25 +125,52 @@ def install_packages(venv_path: Path, packages: list[str]) -> None: subprocess.run(cmd, check=True) +def sync_groups_with_uv(venv_path: Path, group_spec: str) -> None: + """Sync dependency groups from the locked tools project into the repo venv.""" + lockfile_path = get_lockfile_path() + if not lockfile_path.exists(): + raise FileNotFoundError(f"uv lockfile not found at {lockfile_path}") + + groups = [group.strip() for group in group_spec.split(",") if group.strip()] + cmd = [ + "uv", + "sync", + "--project", + str(get_project_path()), + "--active", + "--no-install-project", + "--frozen", + ] + + if "all" in groups: + cmd.append("--all-extras") + else: + for group in groups: + cmd.extend(["--extra", group]) + + env = os.environ.copy() + env["VIRTUAL_ENV"] = str(venv_path) + scripts_dir = venv_path / ("Scripts" if sys.platform == "win32" else "bin") + env["PATH"] = f"{scripts_dir}{os.pathsep}{env.get('PATH', '')}" + subprocess.run(cmd, check=True, env=env) + + def list_packages(venv_path: Path) -> None: """List installed packages.""" python = get_python_executable(venv_path) - - if has_uv(): - subprocess.run(["uv", "pip", "list", "--python", str(python)]) - else: - subprocess.run([str(python), "-m", "pip", "list"]) + subprocess.run([str(python), "-m", "pip", "list"]) def get_packages_for_groups(group_spec: str) -> list[str]: """Get packages for comma-separated group specification.""" + package_groups = load_package_groups() groups = [g.strip() for g in group_spec.split(",")] packages: set[str] = set() for group in groups: - if group not in PACKAGE_GROUPS: - raise ValueError(f"Unknown group: {group}. Valid groups: {', '.join(PACKAGE_GROUPS.keys())}") - packages.update(PACKAGE_GROUPS[group]) + if group not in package_groups: + raise ValueError(f"Unknown group: {group}. Valid groups: {', '.join(package_groups.keys())}") + packages.update(package_groups[group]) return sorted(packages) @@ -206,8 +226,9 @@ def main() -> int: args = parse_args() if args.list_groups: + package_groups = load_package_groups() print("Available package groups:") - for group, packages in sorted(PACKAGE_GROUPS.items()): + for group, packages in sorted(package_groups.items()): print(f" {group}:") for pkg in sorted(packages): print(f" - {pkg}") @@ -230,10 +251,17 @@ def main() -> int: try: create_venv(venv_path) - install_packages(venv_path, packages) + if has_uv(): + print("Using uv sync (locked mode)") + sync_groups_with_uv(venv_path, args.group) + else: + install_packages(venv_path, packages) except subprocess.CalledProcessError as e: print(f"Error: Command failed with exit code {e.returncode}", file=sys.stderr) return 1 + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 print() print("Done! Activate the environment with:") diff --git a/tools/setup/install_qt.py b/tools/setup/install_qt.py new file mode 100644 index 000000000000..31f1af146d5d --- /dev/null +++ b/tools/setup/install_qt.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python3 +""" +Install Qt SDK using aqtinstall with architecture resolution. + +Wraps aqtinstall with QGC-specific arch-directory mapping, cache key +generation, and path resolution. Used by the qt-install GitHub Action. + +Usage: + python tools/setup/install_qt.py --version 6.8.3 --host linux --arch linux_gcc_64 + python tools/setup/install_qt.py --version 6.8.3 --host mac --arch clang_64 --modules "qtcharts qtlocation" + python tools/setup/install_qt.py cache-key --arch linux_gcc_64 --modules "qtcharts" + python tools/setup/install_qt.py resolve-arch --arch win64_msvc2022_64 +""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import shutil +import subprocess +import sys +from pathlib import Path + +_tools_dir = str(Path(__file__).resolve().parent.parent) +if _tools_dir not in sys.path: + sys.path.insert(0, _tools_dir) + +from common.gh_actions import write_github_output + +# aqtinstall creates directories that differ from the arch parameter. +# This mapping resolves the actual on-disk directory name. +_ARCH_DIR_MAP: dict[str, str] = { + "win64_msvc2022_arm64_cross_compiled": "msvc2022_arm64", +} + +_ARCH_DIR_PREFIXES = [ + ("linux_", ""), + ("win64_", ""), +] + + +def resolve_arch_dir(arch: str) -> str: + """Map a Qt arch identifier to the on-disk directory name aqtinstall creates.""" + if arch in _ARCH_DIR_MAP: + return _ARCH_DIR_MAP[arch] + for prefix, replacement in _ARCH_DIR_PREFIXES: + if arch.startswith(prefix): + return replacement + arch[len(prefix):] + if arch == "clang_64": + return "macos" + return arch + + +def compute_cache_digest(modules: str, archives: str) -> str: + """Generate a SHA-256 digest for cache key differentiation.""" + content = f"{modules}\n{archives}" + return hashlib.sha256(content.encode()).hexdigest() + + +def resolve_qt_root(outdir: Path, version: str, arch_dir: str) -> Path: + """Resolve and validate the Qt root directory after installation.""" + qt_root = outdir / version / arch_dir + if not qt_root.is_dir(): + available = "none" + version_dir = outdir / version + if version_dir.is_dir(): + available = ", ".join(sorted(p.name for p in version_dir.iterdir() if p.is_dir())) or "none" + print(f"::error::Qt root not found at {qt_root}") + print(f"Expected arch_dir '{arch_dir}' from arch, available: {available}") + sys.exit(1) + return qt_root + + +def install_qt( + host: str, + target: str, + version: str, + arch: str, + outdir: Path, + modules: str = "", + archives: str = "", +) -> Path: + """Install Qt using aqtinstall and return the resolved root directory.""" + aqt = shutil.which("aqt") + if not aqt: + subprocess.run( + [sys.executable, "-m", "pip", "install", "aqtinstall", "--quiet"], + check=True, + ) + aqt = shutil.which("aqt") + if not aqt: + print("::error::aqtinstall not found after pip install") + sys.exit(1) + + args = [aqt, "install-qt", host, target, version, arch, "--outputdir", str(outdir)] + + if modules: + args.extend(["--modules", *modules.split()]) + if archives: + args.extend(["--archives", *archives.split()]) + + print(f"Running: {' '.join(args)}") + subprocess.run(args, check=True) + + arch_dir = resolve_arch_dir(arch) + return resolve_qt_root(outdir, version, arch_dir) + + +# Android ABI → aqtinstall arch mapping (priority order for resolution) +_ANDROID_ABI_ORDER = [ + ("arm64-v8a", "arm64"), + ("armeabi-v7a", "armv7"), + ("x86_64", "x86_64"), + ("x86", "x86"), +] + + +def resolve_android_qt_root(abis: str, roots: dict[str, str]) -> str: + """Pick the primary Android Qt root from installed ABIs. + + Args: + abis: Semicolon-separated ABI list (e.g. "arm64-v8a;x86_64") + roots: Mapping of short ABI key → Qt root path (e.g. {"arm64": "/path/to/qt"}) + + Returns: + The Qt root path for the highest-priority installed ABI. + + Raises: + SystemExit: If no matching ABI is found. + """ + abi_set = {a.strip() for a in abis.split(";") if a.strip()} + for abi, key in _ANDROID_ABI_ORDER: + if abi in abi_set and roots.get(key): + return roots[key] + print(f"::error::Failed to resolve an installed Android Qt root for ABIs: {abis}") + sys.exit(1) + + +def _add_arch_args(p: argparse.ArgumentParser) -> None: + p.add_argument("--arch", required=True) + p.add_argument("--modules", default="") + p.add_argument("--archives", default="") + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Install Qt SDK using aqtinstall.") + sub = parser.add_subparsers(dest="command", required=True) + + install_p = sub.add_parser("install", help="Install Qt") + install_p.add_argument("--version", required=True) + install_p.add_argument("--host", default="linux") + install_p.add_argument("--target", default="desktop") + install_p.add_argument("--outdir", type=Path, default=Path(".qt")) + _add_arch_args(install_p) + + cache_p = sub.add_parser("cache-key", help="Output arch_dir and cache digest") + _add_arch_args(cache_p) + + resolve_p = sub.add_parser("resolve-arch", help="Print resolved arch directory name") + resolve_p.add_argument("--arch", required=True) + + android_p = sub.add_parser("resolve-android-root", help="Pick primary Android Qt root from installed ABIs") + android_p.add_argument("--abis", required=True, help="Semicolon-separated ABI list") + android_p.add_argument("--arm64", default="") + android_p.add_argument("--armv7", default="") + android_p.add_argument("--x86-64", default="", dest="x86_64") + android_p.add_argument("--x86", default="") + + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + + if args.command == "resolve-arch": + print(resolve_arch_dir(args.arch)) + return 0 + + if args.command == "resolve-android-root": + roots = {"arm64": args.arm64, "armv7": args.armv7, "x86_64": args.x86_64, "x86": args.x86} + qt_root = resolve_android_qt_root(args.abis, roots) + write_github_output({"qt_root_dir": qt_root}) + print(f"qt_root_dir={qt_root}") + return 0 + + if args.command == "cache-key": + arch_dir = resolve_arch_dir(args.arch) + digest = compute_cache_digest(args.modules, args.archives) + write_github_output({"arch_dir": arch_dir, "digest": digest}) + print(f"arch_dir={arch_dir}") + print(f"digest={digest}") + return 0 + + # Default: install + arch_dir = resolve_arch_dir(args.arch) + qt_root = install_qt( + host=args.host, + target=args.target, + version=args.version, + arch=args.arch, + outdir=args.outdir, + modules=args.modules, + archives=args.archives, + ) + + write_github_output({ + "arch_dir": arch_dir, + "qt_root_dir": str(qt_root), + "qt_bin_dir": str(qt_root / "bin"), + }) + print(f"Qt installed at {qt_root}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/setup/read_config.py b/tools/setup/read_config.py index 3799e817e9e9..e9a90fe81114 100755 --- a/tools/setup/read_config.py +++ b/tools/setup/read_config.py @@ -62,8 +62,9 @@ def load_config(config_file: Path) -> dict[str, Any]: "qt_minimum_version", "qt_modules", "gstreamer_minimum_version", - "gstreamer_version", + "gstreamer_default_version", "gstreamer_macos_version", + "gstreamer_ios_version", "gstreamer_android_version", "gstreamer_windows_version", "xcode_version", @@ -88,6 +89,8 @@ def get_export_values(config: dict[str, Any]) -> dict[str, str]: if key in config: env_name = key.upper() result[env_name] = str(config[key]) + if key == "gstreamer_default_version": + result["GSTREAMER_VERSION"] = str(config[key]) return result @@ -181,6 +184,9 @@ def main() -> int: # Handle --get for single value if args.get: key = args.get.lower() # Normalize to lowercase + if key == "gstreamer_version" and "gstreamer_default_version" in config: + print(config["gstreamer_default_version"]) + return 0 if key in config: print(config[key]) return 0 diff --git a/tools/simulation/mock_ntrip_caster.py b/tools/simulation/mock_ntrip_caster.py new file mode 100755 index 000000000000..7932a746f3d9 --- /dev/null +++ b/tools/simulation/mock_ntrip_caster.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 +""" +Mock NTRIP Caster for QGroundControl Testing + +Simulates an NTRIP caster that serves a source table and streams RTCM3 +corrections. Useful for testing the QGC NTRIP client without a real caster +or internet connection. + +Usage: + ./mock_ntrip_caster.py # Default: localhost:2101 + ./mock_ntrip_caster.py --port 2102 # Custom port + ./mock_ntrip_caster.py --auth user:pass # Require basic auth + ./mock_ntrip_caster.py --rate 5 # 5 RTCM messages/sec + ./mock_ntrip_caster.py --error bad_crc # Inject bad CRC every 10th msg + ./mock_ntrip_caster.py --error drop # Drop connection after 10 msgs + ./mock_ntrip_caster.py --error http_401 # Always return 401 + ./mock_ntrip_caster.py --error http_500 # Always return 500 + +Real caster for comparison: + Host: rtk2go.com Port: 2101 Password: ntrip@ (or your email) +""" + +import argparse +import base64 +import logging +import select +import socket +import struct +import threading +import time + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger("mock-ntrip") + +# --------------------------------------------------------------------------- +# RTCM3 frame builder +# --------------------------------------------------------------------------- + +# CRC-24Q lookup table (same algorithm as RTCMParser::crc24q) +_CRC24Q_TABLE = [0] * 256 + + +def _init_crc_table(): + for i in range(256): + crc = i << 16 + for _ in range(8): + crc <<= 1 + if crc & 0x1000000: + crc ^= 0x1864CFB + _CRC24Q_TABLE[i] = crc & 0xFFFFFF + + +_init_crc_table() + + +def crc24q(data: bytes) -> int: + crc = 0 + for b in data: + crc = ((crc << 8) & 0xFFFFFF) ^ _CRC24Q_TABLE[((crc >> 16) ^ b) & 0xFF] + return crc + + +def make_rtcm_frame(message_id: int, payload: bytes = b"") -> bytes: + """Build a complete RTCM3 frame with preamble, length, message ID, payload, and CRC.""" + # Message body = 12-bit message ID (big-endian in first 2 bytes) + payload + id_high = (message_id >> 4) & 0xFF + id_low = (message_id & 0x0F) << 4 + if payload: + # Merge low nibble of ID with first nibble of payload + body = bytes([id_high, id_low | (payload[0] >> 4)]) + payload[1:] + else: + body = bytes([id_high, id_low]) + + length = len(body) + if length > 1023: + raise ValueError(f"RTCM payload too large: {length}") + + # Header: preamble (0xD3) + 6 reserved bits (0) + 10-bit length + header = bytes([0xD3, (length >> 8) & 0x03, length & 0xFF]) + + frame_without_crc = header + body + crc = crc24q(frame_without_crc) + crc_bytes = struct.pack(">I", crc)[1:] # 3 bytes big-endian + + return frame_without_crc + crc_bytes + + +def make_rtcm_1005() -> bytes: + """RTCM 1005 - Stationary RTK Reference Station ARP (compact, 19 bytes payload). + Uses a fixed position near 0,0,0 for testing.""" + # Reference Station ID (12 bits) = 1 + # ITRF Realization Year (6 bits) = 0 + # GPS indicator (1) = 1, GLONASS (1) = 0, Galileo (1) = 0, Reference-Station (1) = 0 + # ECEF-X (38 bits signed, 0.0001m) = 0 + # Single Receiver Oscillator (1) = 0, Reserved (1) = 0 + # ECEF-Y (38 bits signed) = 0 + # Quarter Cycle Indicator (2) = 0 + # ECEF-Z (38 bits signed) = 0 + # Total payload: 152 bits = 19 bytes + payload = bytearray(19) + # Station ID = 1 in bits 0..11 -> byte 0 high nibble will be merged with msg ID + payload[0] = 0x00 # station ID high 4 bits (will merge with msg id low nibble) + payload[1] = 0x11 # station ID low 8 bits = 0x01, shifted: 0001 + GPS=1,GLO=0,GAL=0,REF=0 + # Rest is zeros (ECEF 0,0,0) + return make_rtcm_frame(1005, bytes(payload)) + + +def make_rtcm_1077() -> bytes: + """RTCM 1077 - GPS MSM7 (minimal/empty observation for testing).""" + # Minimal MSM header: station ID + epoch + flags, rest zeros + payload = bytearray(25) # minimum MSM7 header size + return make_rtcm_frame(1077, bytes(payload)) + + +def make_bad_crc_frame() -> bytes: + """Build a valid-looking RTCM3 frame with intentionally bad CRC.""" + frame = bytearray(make_rtcm_1005()) + # Corrupt the last CRC byte + frame[-1] ^= 0xFF + return bytes(frame) + + +# --------------------------------------------------------------------------- +# Source table +# --------------------------------------------------------------------------- + +SOURCE_TABLE = """\ +STR;MOCK1;MOCK1;RTCM 3.3;1005(1),1077(1),1087(1);2;GPS+GLO;MOCK;USA;0.00;0.00;0;0;sNTRIP;none;N;N;0; +STR;MOCK2;MOCK2;RTCM 3.3;1005(1),1077(1);2;GPS;MOCK;USA;37.77;-122.42;0;0;sNTRIP;none;N;N;0; +STR;MOCK_AUTH;MOCK_AUTH;RTCM 3.3;1005(1);2;GPS;MOCK;USA;0.00;0.00;0;0;sNTRIP;none;B;N;0; +ENDSOURCETABLE\r\n""" + + +# --------------------------------------------------------------------------- +# Client handler +# --------------------------------------------------------------------------- + + +class ClientHandler(threading.Thread): + def __init__(self, conn, addr, args): + super().__init__(daemon=True) + self.conn = conn + self.addr = addr + self.args = args + self.running = True + + def run(self): + try: + self._handle() + except (ConnectionResetError, BrokenPipeError): + log.info("%s disconnected", self.addr) + except Exception as e: + log.error("%s error: %s", self.addr, e) + finally: + self.conn.close() + + def _handle(self): + # Read HTTP request + buf = b"" + while b"\r\n\r\n" not in buf: + chunk = self.conn.recv(4096) + if not chunk: + return + buf += chunk + if len(buf) > 16384: + log.warning("%s request too large", self.addr) + return + + header_end = buf.index(b"\r\n\r\n") + header = buf[:header_end].decode("utf-8", errors="replace") + lines = header.split("\r\n") + request_line = lines[0] if lines else "" + log.info("%s >> %s", self.addr, request_line) + + # Parse headers + headers = {} + for line in lines[1:]: + if ":" in line: + k, v = line.split(":", 1) + headers[k.strip().lower()] = v.strip() + + # Error injection: HTTP errors + if self.args.error == "http_401": + self._send(b"HTTP/1.1 401 Unauthorized\r\n\r\n") + return + if self.args.error == "http_500": + self._send(b"HTTP/1.1 500 Internal Server Error\r\n\r\n") + return + + # Auth check + if self.args.auth: + expected = base64.b64encode(self.args.auth.encode()).decode() + auth = headers.get("authorization", "") + if not auth or auth.split()[-1] != expected: + log.warning("%s auth failed", self.addr) + self._send(b"HTTP/1.1 401 Unauthorized\r\n\r\n") + return + + # Parse request path + parts = request_line.split() + if len(parts) < 2: + self._send(b"HTTP/1.1 400 Bad Request\r\n\r\n") + return + + path = parts[1] + + if path == "/": + # Source table request + log.info("%s serving source table", self.addr) + body = SOURCE_TABLE.encode() + resp = ( + f"HTTP/1.1 200 OK\r\n" + f"Content-Type: text/plain\r\n" + f"Content-Length: {len(body)}\r\n" + f"\r\n" + ).encode() + body + self._send(resp) + return + + # Mountpoint stream + mountpoint = path.lstrip("/") + valid_mounts = {"MOCK1", "MOCK2", "MOCK_AUTH"} + if mountpoint not in valid_mounts: + log.warning("%s unknown mount: %s", self.addr, mountpoint) + self._send(b"HTTP/1.1 404 Not Found\r\n\r\n") + return + + log.info("%s streaming %s @ %s msg/sec", self.addr, mountpoint, self.args.rate) + # ICY 200 OK (like real NTRIP casters) + self._send(b"ICY 200 OK\r\n\r\n") + + self._stream_rtcm(mountpoint) + + def _stream_rtcm(self, mountpoint): + interval = 1.0 / self.args.rate + msg_count = 0 + + while self.running: + msg_count += 1 + + # Error injection: bad CRC every 10th message + if self.args.error == "bad_crc" and msg_count % 10 == 0: + log.info("%s injecting bad CRC (msg #%d)", self.addr, msg_count) + self._send(make_bad_crc_frame()) + time.sleep(interval) + continue + + # Error injection: drop connection + if self.args.error == "drop" and msg_count > 10: + log.info("%s dropping connection after %d msgs", self.addr, msg_count) + return + + # Alternate between 1005 and 1077 + if msg_count % 2 == 1: + frame = make_rtcm_1005() + else: + frame = make_rtcm_1077() + + try: + self._send(frame) + except (ConnectionResetError, BrokenPipeError): + return + + # Check for incoming NMEA from client (GGA) + try: + ready, _, _ = select.select([self.conn], [], [], 0) + if ready: + nmea = self.conn.recv(4096) + if nmea: + log.info("%s << NMEA: %s", self.addr, nmea.decode("ascii", errors="replace").strip()) + else: + log.info("%s client closed", self.addr) + return + except Exception: + pass + + time.sleep(interval) + + def _send(self, data: bytes): + self.conn.sendall(data) + + +# --------------------------------------------------------------------------- +# Server +# --------------------------------------------------------------------------- + + +def run_server(args): + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((args.host, args.port)) + srv.listen(5) + + log.info("Mock NTRIP caster listening on %s:%d", args.host, args.port) + log.info("Mountpoints: MOCK1, MOCK2, MOCK_AUTH") + if args.auth: + log.info("Auth required: %s", args.auth) + if args.error: + log.info("Error injection: %s", args.error) + log.info("RTCM rate: %s msg/sec", args.rate) + log.info("") + log.info("QGC settings:") + log.info(" Host: %s", args.host if args.host != "0.0.0.0" else "127.0.0.1") + log.info(" Port: %d", args.port) + log.info(" Mountpoint: MOCK1") + if args.auth: + user, passwd = args.auth.split(":", 1) + log.info(" Username: %s", user) + log.info(" Password: %s", passwd) + log.info("") + + try: + while True: + conn, addr = srv.accept() + log.info("Connection from %s:%d", *addr) + handler = ClientHandler(conn, addr, args) + handler.start() + except KeyboardInterrupt: + log.info("Shutting down") + finally: + srv.close() + + +def main(): + parser = argparse.ArgumentParser( + description="Mock NTRIP caster for QGroundControl testing" + ) + parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)") + parser.add_argument("--port", type=int, default=2101, help="Port (default: 2101)") + parser.add_argument("--auth", metavar="USER:PASS", help="Require basic auth (e.g. user:pass)") + parser.add_argument("--rate", type=float, default=1.0, help="Messages per second (default: 1)") + parser.add_argument( + "--error", + choices=["bad_crc", "drop", "http_401", "http_500"], + help="Inject errors: bad_crc (every 10th), drop (after 10), http_401, http_500", + ) + args = parser.parse_args() + run_server(args) + + +if __name__ == "__main__": + main() diff --git a/tools/simulation/mock_vehicle.py b/tools/simulation/mock_vehicle.py index d1e112d27dc6..582358c731d5 100755 --- a/tools/simulation/mock_vehicle.py +++ b/tools/simulation/mock_vehicle.py @@ -389,7 +389,7 @@ def run(self, rate=10): self.running = False -def main(): +def main() -> None: parser = argparse.ArgumentParser( description="Mock MAVLink vehicle for QGroundControl testing", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/tools/simulation/run-arducopter-sitl.sh b/tools/simulation/run-arducopter-sitl.sh index a2710d9d4a15..ad790806eab3 100755 --- a/tools/simulation/run-arducopter-sitl.sh +++ b/tools/simulation/run-arducopter-sitl.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # ArduCopter SITL Test Environment # Runs ArduCopter in Docker for testing QGC without hardware # @@ -7,7 +7,7 @@ # Options: # --with-latency Simulate 100ms round-trip latency (Herelink-like conditions) -set -e +set -euo pipefail COPTER_VERSION="Copter-4.5.6" IMAGE_NAME="ardupilot-sitl-4.5.6" @@ -36,7 +36,7 @@ DOCKER_CMD="docker run -d --name $CONTAINER_NAME -p 5760:5760" ARDUPILOT_CMD="/ardupilot/build/sitl/bin/arducopter -S --model + --speedup 1 --defaults /ardupilot/Tools/autotest/default_params/copter.parm --home 42.3898,-71.1476,14.0,270.0 --serial0 tcp:0:5760:wait" # Check for latency simulation flag -if [[ "$1" == "--with-latency" ]]; then +if [[ "${1:-}" == "--with-latency" ]]; then echo "" echo "Starting SITL with simulated latency (100ms round-trip)..." echo "This simulates Herelink-like network conditions." diff --git a/tools/tests/conftest.py b/tools/tests/conftest.py new file mode 100644 index 000000000000..7bb0b2fa7e98 --- /dev/null +++ b/tools/tests/conftest.py @@ -0,0 +1,11 @@ +"""Shared pytest setup for tools tests.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# Ensure the tools directory is importable (for common.*, setup.*, etc.) +tools_dir = str(Path(__file__).resolve().parent.parent) +if tools_dir not in sys.path: + sys.path.insert(0, tools_dir) diff --git a/tools/tests/test_common.py b/tools/tests/test_common.py index 386d3418f57c..ec4d5342a56c 100644 --- a/tools/tests/test_common.py +++ b/tools/tests/test_common.py @@ -1,16 +1,10 @@ #!/usr/bin/env python3 """Tests for common utilities.""" -import sys from pathlib import Path import pytest -TOOLS_DIR = Path(__file__).parent.parent - -# Add tools to path for imports -sys.path.insert(0, str(TOOLS_DIR)) - from common.patterns import ( FACT_MEMBER_PATTERN, FACTGROUP_CLASS_PATTERN, diff --git a/tools/tests/test_coverage.py b/tools/tests/test_coverage.py new file mode 100644 index 000000000000..6e2be0f2627b --- /dev/null +++ b/tools/tests/test_coverage.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +"""Tests for tools/coverage.py.""" + +from __future__ import annotations + +from coverage import build_step_summary + + +def test_build_step_summary_includes_metrics() -> None: + markdown = build_step_summary("lines: 42.0% (42 out of 100)\nbranches: 10.0% (1 out of 10)\n", "report-only") + assert "| Lines | 42.0% (42 out of 100) |" in markdown + assert "| Branches | 10.0% (1 out of 10) |" in markdown diff --git a/tools/tests/test_factgroup_generator.py b/tools/tests/test_factgroup_generator.py index 50142b18df27..aac66c37fb9e 100644 --- a/tools/tests/test_factgroup_generator.py +++ b/tools/tests/test_factgroup_generator.py @@ -8,12 +8,8 @@ import pytest -TOOLS_DIR = Path(__file__).parent.parent FIXTURES_DIR = Path(__file__).parent / "fixtures" -# Add tools to path for imports -sys.path.insert(0, str(TOOLS_DIR)) - from generators.factgroup.generator import ( FactSpec, MavlinkMessageSpec, @@ -281,7 +277,8 @@ class TestCLI: def run_cli(self, *args): """Run the CLI and return result.""" cmd = [sys.executable, "-m", "tools.generators.factgroup.cli"] + list(args) - return subprocess.run(cmd, capture_output=True, text=True, cwd=TOOLS_DIR.parent) + repo_root = Path(__file__).resolve().parent.parent.parent + return subprocess.run(cmd, capture_output=True, text=True, cwd=repo_root) def test_cli_help(self): """--help should show usage.""" diff --git a/tools/tests/test_generate_docs.py b/tools/tests/test_generate_docs.py new file mode 100644 index 000000000000..53e7daf390ab --- /dev/null +++ b/tools/tests/test_generate_docs.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Tests for tools/generate_docs.py.""" + +from __future__ import annotations + +from generate_docs import build_doxyfile_text + + +def test_build_doxyfile_text_overrides_output_and_pdf(tmp_path) -> None: + base = "OUTPUT_DIRECTORY = docs/api\nGENERATE_LATEX = NO\n" + rendered = build_doxyfile_text(base, tmp_path / "api", generate_pdf=True) + + assert 'OUTPUT_DIRECTORY = "' in rendered + assert (tmp_path / "api").as_posix() in rendered + assert "GENERATE_LATEX = YES" in rendered + diff --git a/tools/tests/test_gh_actions.py b/tools/tests/test_gh_actions.py index bd75e5fa1887..aff63550f98a 100644 --- a/tools/tests/test_gh_actions.py +++ b/tools/tests/test_gh_actions.py @@ -6,12 +6,9 @@ import json import os import subprocess -import sys -from pathlib import Path from unittest.mock import patch -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) +import httpx from common import gh_actions as mod @@ -20,19 +17,20 @@ def _cp(stdout: str = "", stderr: str = "", returncode: int = 0) -> subprocess.C return subprocess.CompletedProcess(args=["gh"], returncode=returncode, stdout=stdout, stderr=stderr) -class _FakeHttpResponse: - def __init__(self, payload: dict[str, object], link: str = "") -> None: - self._payload = payload - self.headers = {"Link": link} - - def read(self) -> bytes: - return json.dumps(self._payload).encode("utf-8") - - def __enter__(self) -> _FakeHttpResponse: - return self - - def __exit__(self, exc_type, exc, tb) -> None: - return None +def _mock_response( + data: dict, + next_url: str = "", + status_code: int = 200, + extra_headers: dict[str, str] | None = None, +) -> httpx.Response: + headers = {} + if next_url: + headers["link"] = f'<{next_url}>; rel="next"' + if extra_headers: + headers.update(extra_headers) + request = httpx.Request("GET", "https://api.github.com/test") + resp = httpx.Response(status_code, json=data, headers=headers, request=request) + return resp def test_parse_json_documents_handles_paginated_stream() -> None: @@ -68,19 +66,43 @@ def test_list_workflow_runs_for_sha_uses_get_method() -> None: def test_list_workflow_runs_for_sha_http_mode() -> None: - first = _FakeHttpResponse( + first = _mock_response( {"workflow_runs": [{"id": 1, "name": "Linux"}]}, - '; rel="next"', + next_url="https://api.github.com/next", ) - second = _FakeHttpResponse({"workflow_runs": [{"id": 2, "name": "Windows"}]}) + second = _mock_response({"workflow_runs": [{"id": 2, "name": "Windows"}]}) + with patch.dict(os.environ, {"QGC_GH_API_MODE": "http", "GH_TOKEN": "token"}, clear=False): - with patch.object(mod.urllib_request, "urlopen", side_effect=[first, second]), patch.object(mod, "gh") as gh_mock: - runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") + with patch.object(mod, "_build_http_client") as mock_client: + client = mock_client.return_value.__enter__.return_value + client.get.side_effect = [first, second] + with patch.object(mod, "gh") as gh_mock: + runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") assert [run["id"] for run in runs] == [1, 2] gh_mock.assert_not_called() +def test_list_workflow_runs_for_sha_http_mode_retries_retryable_status() -> None: + first = _mock_response( + {"message": "try again"}, + status_code=503, + extra_headers={"Retry-After": "0"}, + ) + second = _mock_response({"workflow_runs": [{"id": 7, "name": "Linux"}]}) + + with patch.dict(os.environ, {"QGC_GH_API_MODE": "http", "GH_TOKEN": "token"}, clear=False): + with patch.object(mod, "_build_http_client") as mock_client: + client = mock_client.return_value.__enter__.return_value + client.get.side_effect = [first, second] + with patch.object(mod.time, "sleep") as sleep_mock: + runs = mod.list_workflow_runs_for_sha("owner/repo", "abc123") + + assert runs == [{"id": 7, "name": "Linux"}] + assert client.get.call_count == 2 + sleep_mock.assert_called_once_with(1.0) + + def test_list_run_artifacts_parses_paginated_stream() -> None: payload = ( json.dumps({"artifacts": [{"name": "QGroundControl", "size_in_bytes": 1}]}) @@ -101,3 +123,98 @@ def test_list_run_artifacts_rejects_invalid_run_id() -> None: assert "run_id must be an integer" in str(exc) else: raise AssertionError("Expected ValueError for invalid run_id") + + +class TestIsForkPr: + def test_not_pr_event(self) -> None: + with patch.dict(os.environ, {"EVENT_NAME": "push"}, clear=False): + assert mod.is_fork_pr() is False + + def test_same_repo_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "owner/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is False + + def test_fork_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "fork/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is True + + def test_empty_pr_repo(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.is_fork_pr() is False + + +class TestResolveCachePolicy: + def test_explicit_true(self) -> None: + assert mod.resolve_cache_policy("true") == "true" + + def test_explicit_false(self) -> None: + assert mod.resolve_cache_policy("false") == "false" + + def test_auto_non_pr(self) -> None: + with patch.dict(os.environ, {"EVENT_NAME": "push"}, clear=False): + assert mod.resolve_cache_policy("auto") == "true" + + def test_auto_same_repo_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "owner/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.resolve_cache_policy("auto") == "true" + + def test_auto_fork_pr(self) -> None: + env = {"EVENT_NAME": "pull_request", "PR_REPO": "fork/repo", "THIS_REPO": "owner/repo"} + with patch.dict(os.environ, env, clear=False): + assert mod.resolve_cache_policy("auto") == "false" + + +class TestWriteGithubOutput: + def test_simple_values(self, tmp_path) -> None: + out = tmp_path / "output" + out.touch() + with patch.dict(os.environ, {"GITHUB_OUTPUT": str(out)}, clear=False): + mod.write_github_output({"key1": "val1", "key2": "val2"}) + content = out.read_text() + assert "key1=val1\n" in content + assert "key2=val2\n" in content + + def test_multiline_value(self, tmp_path) -> None: + out = tmp_path / "output" + out.touch() + with patch.dict(os.environ, {"GITHUB_OUTPUT": str(out)}, clear=False): + mod.write_github_output({"body": "line1\nline2"}) + content = out.read_text() + assert "body< None: + with patch.dict(os.environ, {}, clear=True): + mod.write_github_output({"key": "val"}) + + +class TestWriteStepSummary: + def test_writes_markdown(self, tmp_path) -> None: + out = tmp_path / "summary" + out.touch() + with patch.dict(os.environ, {"GITHUB_STEP_SUMMARY": str(out)}, clear=False): + mod.write_step_summary("## Hello\n") + assert out.read_text() == "## Hello\n" + + def test_noop_without_env(self) -> None: + with patch.dict(os.environ, {}, clear=True): + mod.write_step_summary("test") + + +class TestAppendGithubEnv: + def test_writes_env_vars(self, tmp_path) -> None: + out = tmp_path / "env" + out.touch() + with patch.dict(os.environ, {"GITHUB_ENV": str(out)}, clear=False): + mod.append_github_env({"FOO": "bar", "BAZ": "qux"}) + content = out.read_text() + assert "FOO=bar\n" in content + assert "BAZ=qux\n" in content + + def test_noop_without_env(self) -> None: + with patch.dict(os.environ, {}, clear=True): + mod.append_github_env({"FOO": "bar"}) diff --git a/tools/tests/test_install_dependencies.py b/tools/tests/test_install_dependencies.py index e8f1e1d7ff49..a213132453b4 100644 --- a/tools/tests/test_install_dependencies.py +++ b/tools/tests/test_install_dependencies.py @@ -3,26 +3,24 @@ from __future__ import annotations -import sys from pathlib import Path from unittest.mock import MagicMock, call, patch import pytest -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - from setup.install_dependencies import ( DEBIAN_PACKAGES, MACOS_PACKAGES, PIPX_PACKAGES, detect_platform, + get_available_debian_packages, get_apt_install_command, get_apt_update_command, get_debian_packages, get_macos_packages, parse_args, run_apt_install_with_retry, + validate_extra_packages, ) @@ -75,6 +73,11 @@ def test_get_debian_packages_no_duplicates() -> None: assert len(pkgs) == len(set(pkgs)) +def test_get_available_debian_packages_filters_unavailable() -> None: + with patch("setup.install_dependencies.check_apt_package_available", side_effect=lambda pkg: pkg == "cmake"): + assert get_available_debian_packages("core") == ["cmake"] + + def test_get_macos_packages() -> None: pkgs = get_macos_packages() assert "cmake" in pkgs @@ -145,6 +148,11 @@ def test_parse_args_category() -> None: assert args.category == "qt" +def test_parse_args_validate_extra_packages() -> None: + args = parse_args(["--validate-extra-packages", "foo", "bar"]) + assert args.validate_extra_packages == ["foo", "bar"] + + def test_parse_args_gstreamer_version() -> None: args = parse_args(["--platform", "windows", "--gstreamer-version", "1.24.0"]) assert args.gstreamer_version == "1.24.0" @@ -160,6 +168,15 @@ def test_parse_args_vulkan() -> None: assert args.vulkan is True +def test_validate_extra_packages_accepts_valid_names() -> None: + assert validate_extra_packages(["foo", "libbar-dev", "baz+1"]) == ["foo", "libbar-dev", "baz+1"] + + +def test_validate_extra_packages_rejects_invalid_names() -> None: + with pytest.raises(ValueError, match="Invalid package name"): + validate_extra_packages(["good", "bad;rm"]) + + # --------------------------------------------------------------------------- # download_file (network call mocked) # --------------------------------------------------------------------------- diff --git a/tools/tests/test_install_python.py b/tools/tests/test_install_python.py index 606c55b11e9d..98ab77db5b31 100644 --- a/tools/tests/test_install_python.py +++ b/tools/tests/test_install_python.py @@ -3,15 +3,12 @@ from __future__ import annotations -import sys from pathlib import Path +from unittest.mock import patch import pytest -TOOLS_DIR = Path(__file__).parent.parent -sys.path.insert(0, str(TOOLS_DIR)) - -from setup.install_python import get_packages_for_groups +from setup.install_python import get_packages_for_groups, sync_groups_with_uv def test_test_group_contains_pytest() -> None: @@ -21,6 +18,11 @@ def test_test_group_contains_pytest() -> None: assert "pyyaml" in packages +def test_scripts_group_contains_defusedxml() -> None: + packages = get_packages_for_groups("scripts") + assert "defusedxml>=0.7.1" in packages + + def test_multiple_groups_are_merged() -> None: packages = get_packages_for_groups("precommit,test") assert "pre-commit" in packages @@ -39,3 +41,18 @@ def test_all_group_includes_test_and_ci_tools() -> None: def test_unknown_group_raises() -> None: with pytest.raises(ValueError, match="Unknown group"): get_packages_for_groups("nope") + + +def test_sync_groups_with_uv_uses_locked_active_env(tmp_path: Path) -> None: + venv = tmp_path / ".venv" + (tmp_path / "tools").mkdir() + lockfile = tmp_path / "tools" / "uv.lock" + lockfile.write_text("", encoding="utf-8") + with patch("setup.install_python.find_repo_root", return_value=tmp_path), \ + patch("setup.install_python.subprocess.run") as mock_run: + sync_groups_with_uv(venv, "scripts,test") + + args = mock_run.call_args.args[0] + assert args[:5] == ["uv", "sync", "--project", str(tmp_path / "tools"), "--active"] + assert "--frozen" in args + assert args.count("--extra") == 2 diff --git a/tools/tests/test_install_qt.py b/tools/tests/test_install_qt.py new file mode 100644 index 000000000000..3b64cf7ffa22 --- /dev/null +++ b/tools/tests/test_install_qt.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Tests for tools/setup/install_qt.py.""" + +from __future__ import annotations + +import pytest + +from setup.install_qt import compute_cache_digest, resolve_android_qt_root, resolve_arch_dir, resolve_qt_root + + +class TestResolveArchDir: + def test_linux_gcc_64(self) -> None: + assert resolve_arch_dir("linux_gcc_64") == "gcc_64" + + def test_linux_arm64(self) -> None: + assert resolve_arch_dir("linux_arm64") == "arm64" + + def test_win64_msvc2022_64(self) -> None: + assert resolve_arch_dir("win64_msvc2022_64") == "msvc2022_64" + + def test_win64_msvc2022_arm64_cross_compiled(self) -> None: + assert resolve_arch_dir("win64_msvc2022_arm64_cross_compiled") == "msvc2022_arm64" + + def test_clang_64_maps_to_macos(self) -> None: + assert resolve_arch_dir("clang_64") == "macos" + + def test_android_arm64_v8a_unchanged(self) -> None: + assert resolve_arch_dir("android_arm64_v8a") == "android_arm64_v8a" + + def test_ios_unchanged(self) -> None: + assert resolve_arch_dir("ios") == "ios" + + +class TestComputeCacheDigest: + def test_deterministic(self) -> None: + a = compute_cache_digest("qtcharts qtlocation", "") + b = compute_cache_digest("qtcharts qtlocation", "") + assert a == b + + def test_different_modules_differ(self) -> None: + a = compute_cache_digest("qtcharts", "") + b = compute_cache_digest("qtlocation", "") + assert a != b + + def test_archives_affect_digest(self) -> None: + a = compute_cache_digest("qtcharts", "") + b = compute_cache_digest("qtcharts", "icu") + assert a != b + + def test_returns_hex_string(self) -> None: + d = compute_cache_digest("", "") + assert len(d) == 64 + assert all(c in "0123456789abcdef" for c in d) + + +class TestResolveQtRoot: + def test_valid_path(self, tmp_path: "pytest.TempPathFactory") -> None: + qt_root = tmp_path / "6.8.3" / "gcc_64" + qt_root.mkdir(parents=True) + result = resolve_qt_root(tmp_path, "6.8.3", "gcc_64") + assert result == qt_root + + def test_missing_path_exits(self, tmp_path: "pytest.TempPathFactory") -> None: + with pytest.raises(SystemExit): + resolve_qt_root(tmp_path, "6.8.3", "gcc_64") + + +class TestResolveAndroidQtRoot: + def test_arm64_preferred(self) -> None: + roots = {"arm64": "/qt/arm64", "armv7": "/qt/armv7"} + assert resolve_android_qt_root("arm64-v8a;armeabi-v7a", roots) == "/qt/arm64" + + def test_armv7_fallback(self) -> None: + roots = {"arm64": "", "armv7": "/qt/armv7"} + assert resolve_android_qt_root("arm64-v8a;armeabi-v7a", roots) == "/qt/armv7" + + def test_x86_64_only(self) -> None: + roots = {"x86_64": "/qt/x86_64"} + assert resolve_android_qt_root("x86_64", roots) == "/qt/x86_64" + + def test_x86_only(self) -> None: + roots = {"x86": "/qt/x86"} + assert resolve_android_qt_root("x86", roots) == "/qt/x86" + + def test_no_match_exits(self) -> None: + with pytest.raises(SystemExit): + resolve_android_qt_root("mips", {}) + + def test_empty_root_skipped(self) -> None: + roots = {"arm64": "", "x86_64": "/qt/x86_64"} + assert resolve_android_qt_root("arm64-v8a;x86_64", roots) == "/qt/x86_64" + + def test_semicolon_parsing(self) -> None: + roots = {"armv7": "/qt/armv7"} + assert resolve_android_qt_root("armeabi-v7a", roots) == "/qt/armv7" diff --git a/tools/tests/test_pre_commit.py b/tools/tests/test_pre_commit.py new file mode 100644 index 000000000000..4f3ecbdbe001 --- /dev/null +++ b/tools/tests/test_pre_commit.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Tests for tools/pre_commit.py.""" + +from __future__ import annotations + +from unittest.mock import patch + +from pre_commit import build_precommit_args, extract_hook_lines, parse_args, summarize_output + + +def test_summarize_output_counts_states() -> None: + passed, failed, skipped = summarize_output( + "hook-a........................Passed\nhook-b........................Failed\nhook-c........................Skipped\n" + ) + assert (passed, failed, skipped) == (1, 1, 1) + + +def test_extract_hook_lines_strips_ansi() -> None: + lines = extract_hook_lines("\x1b[31mhook-a........................Failed\x1b[0m\n") + assert lines == ["hook-a........................Failed"] + + +def test_build_precommit_args_all_files() -> None: + args = parse_args([]) + with patch("pre_commit.git_has_master_ref", return_value=True): + assert build_precommit_args(args)[-1] == "--all-files" + + +def test_build_precommit_args_changed_mode() -> None: + args = parse_args(["--changed"]) + with patch("pre_commit.git_has_master_ref", return_value=True): + built = build_precommit_args(args) + assert built[-4:] == ["--from-ref", "master", "--to-ref", "HEAD"] diff --git a/tools/tests/test_read_config.py b/tools/tests/test_read_config.py index 7463bf986811..5668f6d92eee 100644 --- a/tools/tests/test_read_config.py +++ b/tools/tests/test_read_config.py @@ -32,6 +32,7 @@ def _write_config(path: Path) -> None: "qt_version": "6.10.2", "qt_minimum_version": "6.8.0", "qt_modules": "qtpositioning qtserialport qtscxml", + "gstreamer_default_version": "1.28.0", "gstreamer_windows_version": "1.26.6", "android_platform": "35", } @@ -58,6 +59,16 @@ def test_missing_key_returns_error(tmp_path: Path) -> None: assert "not found" in result.stderr +def test_legacy_gstreamer_version_alias_returns_default_version(tmp_path: Path) -> None: + config = tmp_path / "build-config.json" + config.write_text(json.dumps({"gstreamer_default_version": "1.28.0"}), encoding="utf-8") + + result = _run_read_config("--get", "gstreamer_version", env={"CONFIG_FILE": str(config)}) + + assert result.returncode == 0 + assert result.stdout.strip() == "1.28.0" + + def test_export_bash_format(tmp_path: Path) -> None: config = tmp_path / "build-config.json" _write_config(config) @@ -67,6 +78,7 @@ def test_export_bash_format(tmp_path: Path) -> None: assert result.returncode == 0 assert 'export QT_VERSION="6.10.2"' in result.stdout assert 'export QT_MODULES="qtpositioning qtserialport qtscxml"' in result.stdout + assert 'export GSTREAMER_VERSION="1.28.0"' in result.stdout def test_export_bash_preserves_bang_character(tmp_path: Path) -> None: @@ -101,6 +113,7 @@ def test_github_output_includes_ios_modules(tmp_path: Path) -> None: output_text = github_output.read_text(encoding="utf-8") assert "qt_version=6.10.2" in output_text assert "qt_minimum_version=6.8.0" in output_text + assert "gstreamer_version=1.28.0" in output_text assert "gstreamer_windows_version=1.26.6" in output_text # Derived value excludes qtserialport and qtscxml and normalizes spacing. assert "qt_modules_ios=qtpositioning" in output_text diff --git a/tools/translations/README.md b/tools/translations/README.md index b079356ad469..f21f5c0215c7 100644 --- a/tools/translations/README.md +++ b/tools/translations/README.md @@ -10,9 +10,9 @@ Crowdin is configured to automatically sychronize the qgc.ts file once a day. So Add the new language from the CrowdIn settings as the first step. -### Periodically update the base transation files during the release cycle +### Periodically update the base translation files during the release cycle -You do this by running the `source tools/translations/qgc-lupdate.sh` script to update the translations files for both Qt and Json. Crowdin will automatically pull these up and submit a pull request back when new translations are available. +The `lupdate.yml` workflow runs automatically every Sunday to regenerate the `.ts` source files and open a PR with any changes. You can also trigger it manually from the Actions tab or run it locally with `./tools/translations/qgc-lupdate.sh` from the repository root. Crowdin will automatically pull these up and submit a pull request back when new translations are available. ## C++ and Qml code strings diff --git a/tools/translations/qgc-lupdate-json.py b/tools/translations/qgc-lupdate-json.py index e9665f2c4bed..27fa5cdd7583 100755 --- a/tools/translations/qgc-lupdate-json.py +++ b/tools/translations/qgc-lupdate-json.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import json import os import sys @@ -108,7 +108,7 @@ def walkDirectoryTreeForJsonFiles(dir, multiFileLocArray): walkDirectoryTreeForJsonFiles(path, multiFileLocArray) -def writeJsonTSFile(multiFileLocArray): +def writeJsonTSFile(output_path, multiFileLocArray): ts_root = ET.Element("TS", version="2.1") for entry in multiFileLocArray: @@ -146,7 +146,7 @@ def writeJsonTSFile(multiFileLocArray): ET.SubElement(message, "source").text = sourceStr ET.SubElement(message, "translation", type="unfinished") - with open("translations/qgc-json.ts", "w", encoding="utf-8") as jsonTSFile: + with open(output_path, "w", encoding="utf-8") as jsonTSFile: jsonTSFile.write('\n') jsonTSFile.write("\n") jsonTSFile.write(ET.tostring(ts_root, encoding="unicode")) @@ -154,9 +154,13 @@ def writeJsonTSFile(multiFileLocArray): def main(): + # Resolve repo root from script location so CWD doesn't matter + script_dir = os.path.dirname(os.path.abspath(__file__)) + repo_root = os.path.normpath(os.path.join(script_dir, "..", "..")) + multiFileLocArray = [] - walkDirectoryTreeForJsonFiles("../src", multiFileLocArray) - writeJsonTSFile(multiFileLocArray) + walkDirectoryTreeForJsonFiles(os.path.join(repo_root, "src"), multiFileLocArray) + writeJsonTSFile(os.path.join(repo_root, "translations", "qgc-json.ts"), multiFileLocArray) if __name__ == "__main__": diff --git a/tools/uv.lock b/tools/uv.lock new file mode 100644 index 000000000000..f869054d19d7 --- /dev/null +++ b/tools/uv.lock @@ -0,0 +1,1310 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version < '3.13'", +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "aqtinstall" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bs4" }, + { name = "defusedxml" }, + { name = "humanize" }, + { name = "patch-ng" }, + { name = "py7zr" }, + { name = "requests" }, + { name = "semantic-version" }, + { name = "texttable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/19/24a588de6c25d43169d172dab47e63a63cd0d8f90e98cf86487acbf00ac7/aqtinstall-3.3.0.tar.gz", hash = "sha256:9c7d85fbe7258be2d7d23fda33f8aff2e8b7536817255eaeaaf4226da8546a31", size = 374818, upload-time = "2025-06-02T11:10:53.913Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/70/849fa1005fa197fc28a744fd69598ab9da883b737a1ab9461d88721ddd0b/aqtinstall-3.3.0-py3-none-any.whl", hash = "sha256:e88dbd87226f276fdd5d05347a44578d390d93a7f176e9476fbda0a7c9635f69", size = 71789, upload-time = "2025-06-02T11:10:51.994Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-zstd" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" }, + { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" }, + { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" }, + { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" }, + { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" }, + { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" }, + { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" }, + { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" }, + { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" }, + { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" }, + { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, +] + +[[package]] +name = "brotlicffi" +version = "1.2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, +] + +[[package]] +name = "bs4" +version = "0.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/aa/4acaf814ff901145da37332e05bb510452ebed97bc9602695059dd46ef39/bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", size = 698, upload-time = "2024-01-17T18:15:47.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/bb/bf7aab772a159614954d84aa832c129624ba6c32faa559dfb200a534e50b/bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc", size = 1189, upload-time = "2024-01-17T18:15:48.613Z" }, +] + +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "colorlog" +version = "6.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "fastcrc" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/79/0afaff8ff928ce9990ca883998c4ea7a7f07f2dfea3ebd6d65ba2aadfd4e/fastcrc-0.3.5.tar.gz", hash = "sha256:3705cbad6b3f283a04256f97ae899404794395090ff5966eac79fe303c13e93e", size = 11979, upload-time = "2025-12-31T18:23:09.579Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/6e/d57d65fb5a36fcbf6d35f759172ebf18646c2abdc3ce5300d4b1c010337a/fastcrc-0.3.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6d0a131010c608f46ad2ab1aea2b4ec2a01be90f18af8ff94b58ded7043d123e", size = 255680, upload-time = "2025-12-31T18:21:47.841Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/56a568a74f35bbd0b6b4b983b39de06ba7a21bc0502545a63d9eca8004a3/fastcrc-0.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3302c42d6356da9b0cad35e9cebff262c4542a5cdace98857a16bf7203166ed", size = 251091, upload-time = "2025-12-31T18:21:42.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/bf/a7412fef1676e98ba07cb64b8a7b5b721a5b63f8fc6cccad215e52b4ac67/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8883e1ad3e530f9c97f41fbee35ae555876186475918fd42da5f78e6da674322", size = 282008, upload-time = "2025-12-31T18:19:48.897Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e1/a70192cccd292a977998ff44150cf12680dc82b9f706df89f757903d275f/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15ada82abfc4d6e775b61d4591b42ce2c95994ab9139bc31ba2085ba05b32135", size = 290131, upload-time = "2025-12-31T18:20:06.695Z" }, + { url = "https://files.pythonhosted.org/packages/e2/16/5d1ac72c26494a7eb9ced6034bbdde1efbbbfbb1275c70e70748188e622b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:402c4663ecb5eb00d2ddb58407796cfb731f72e9e348f152809d6292c5229ba7", size = 407328, upload-time = "2025-12-31T18:20:26.344Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/c54608230fede639d3d443cd6fd08cf53457fe347f13649112816d94fd66/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4270ff2603b6d5bdac43c92e317605cb921f605a05d383917222ada402ed0b8e", size = 307459, upload-time = "2025-12-31T18:21:03.396Z" }, + { url = "https://files.pythonhosted.org/packages/c3/37/379fae277f2b73d0703996c5b78e5ebdde1a346d8c4d5bb9a6fb2fa4df6b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9088dc6f0ff21535fd29ff4639ce5e7b5cb4666fe30040fbfe29614c46ef6c7", size = 286617, upload-time = "2025-12-31T18:21:32.338Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/e389d565cac63d620e9e501ee3b297b01144165eaef9fdd616fbfa1fdbd0/fastcrc-0.3.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4b5860aaf5e1114731b63e924be35179b570016ac3fcdd051265c5665c34efa9", size = 290784, upload-time = "2025-12-31T18:20:44.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/f8/2857b9e0076d4d5838f9393e5d47624fe28b9c6f154697c8e420249f3c4e/fastcrc-0.3.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a69b53da8ffbfe60099555a9f38ebb05519ba76233caf0f866ac5943efd1df3", size = 299395, upload-time = "2025-12-31T18:21:21.254Z" }, + { url = "https://files.pythonhosted.org/packages/fb/60/e69d182d150f41ca8db07c0ba5d77d371ee52ebce13537d1d487a45980aa/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a9d2b2afcfab28dbc9fa4aa516a4570524cb74d0aa734f0cf612bc9070c360d", size = 464295, upload-time = "2025-12-31T18:21:55.355Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/60f3379f11f55e2bda0f46e660b1ae772f3031e751649474c9ba7ad5e52d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c96dbf431f7214d22329650121a5f0c41377722833f255f2d926d7df0d4b1143", size = 560169, upload-time = "2025-12-31T18:22:15.081Z" }, + { url = "https://files.pythonhosted.org/packages/76/5c/8bd19ba855624aea12a6c2da5cef2cf3d12119c55acd048349583c218a7d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:186e3f5fdfa912b43cd9700dc6be5c5c126fe8e159eb65f0757a912e0db671d4", size = 518877, upload-time = "2025-12-31T18:22:33.91Z" }, + { url = "https://files.pythonhosted.org/packages/12/54/50b211283dc54f5af3517dee0b94797f04848c844a3765fd56a5aa899a0c/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4680abf2b254599d6d21bb58c1140e4a8142d951d94240c91cc30362c26c54c5", size = 489218, upload-time = "2025-12-31T18:22:52.785Z" }, + { url = "https://files.pythonhosted.org/packages/dd/af/b77460dbe826793fc65a39b4959efa677d12be6d6680cab6b24035b82da2/fastcrc-0.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:aa8614b0340be45b280c2c4f0330a546c71a2167c4414625096254969750b17b", size = 146970, upload-time = "2025-12-31T18:23:14.477Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/f13eb8e644f18c621d1003d1e34209759ed28ace2eb698809558f45f831e/fastcrc-0.3.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:846aca923cc96965f41a9ebb5c2a4172d948d67285b2e6f2af495fda55d2d624", size = 255919, upload-time = "2025-12-31T18:21:49.158Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ab/72a8a7881f1ac1fd5ab8679fe29a57dbf0523d9c5ee9538da744d5e10d95/fastcrc-0.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe5d56b33a68bc647242345484281e9df7818adb7c6f690393e318a413597872", size = 251366, upload-time = "2025-12-31T18:21:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3a/605dda593c0e089b9eaf8a6b614fd3da174ecd746c7ea1212033f1ff897a/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:953093ed390ad1601288d349a0f08a48562b6b547ee184c8396b88dff50a6a5f", size = 282465, upload-time = "2025-12-31T18:19:50.36Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/f0bb9501c96471ec415319b342995d823a2c9482bcebff705fead1e23224/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50679ec92e0c668b76bb1b9f5d48f7341fecc593419a60cce245f05e3631be10", size = 290304, upload-time = "2025-12-31T18:20:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6e/a59baef04c2e991e9a07494b585906aa23017d2262580c453cca29448270/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d377bb515176d08167daa5784f37f0372280042bde0974d6cdcf0874ce33fdc", size = 409004, upload-time = "2025-12-31T18:20:27.895Z" }, + { url = "https://files.pythonhosted.org/packages/86/97/9931fbc57226a83a76ee31fd197e5fb39979cb02cd975a598ab9c9a4e13d/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89e6f4df265d4443c45329857ddee4b445546c39d839e107dc471ba9398cde1d", size = 307402, upload-time = "2025-12-31T18:21:05.107Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2b/aea9bd3694fa323737cf6db7ac63a3fe21cc8987fe6d2b9f97531801688b/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65d6210030a84ed3aaec0736e18908b2e499784c51f6ffd0d8f7d96de90eea1", size = 286802, upload-time = "2025-12-31T18:21:34.406Z" }, + { url = "https://files.pythonhosted.org/packages/15/03/6667737b2a24bd48a15bea1924bed3d7cd322813bc61f5ee3ea7632417fa/fastcrc-0.3.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a1be96b4090518cd6718c24a0f012f7f385fabbd255ce6a7b0d8ec025c9fb377", size = 291114, upload-time = "2025-12-31T18:20:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/983934a2a56078a022e6f82f3fd6baf40d7e85871658590b57274179dc85/fastcrc-0.3.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63cf91059d8ab4fdb8875296cff961f7e30af81d1263ba11de4feabea980f932", size = 299618, upload-time = "2025-12-31T18:21:22.499Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/59cfae39201db940a1f97bb0fd8f5508c7065394a19a77cd6c5d6cbf2f6b/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5ad026ab9afe90effe55d2237d0357cc0edcfbb1b7cd7fa6c9c12b8649d30a9", size = 464682, upload-time = "2025-12-31T18:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4c/b129f316ddcbf4bf1c0745b209a45a5f2bf5bfd4ccd528893d3d25ce53f6/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2e4e0f053f9c683d11dea4282f9e0a9c5f0299883a4dd83e36eb0da83716f4f9", size = 560357, upload-time = "2025-12-31T18:22:16.694Z" }, + { url = "https://files.pythonhosted.org/packages/10/d3/c7342e8a966fb3bff7672b5727e08c54e509a470e4f96623cc5c6ff8679c/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:80fb2879c0e0bb1d20ea599e4033f48a50d3565550a484a4f3f020847d929569", size = 519044, upload-time = "2025-12-31T18:22:35.399Z" }, + { url = "https://files.pythonhosted.org/packages/02/b6/e65ba338709e49d205a612c69996b66af1bfd78e9c9278fffc0bea8613c3/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ff342e97ff48f9f3c8aa12c48d87ed90b50b753d9cf65d2fecdb8a67cef20d", size = 489513, upload-time = "2025-12-31T18:22:54.496Z" }, + { url = "https://files.pythonhosted.org/packages/58/dd/a0338c32d8e404f32b26b6948d0b60cedc07e1fa76661c331931473ead71/fastcrc-0.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:6d59f686f253478626b9befa4dfff723d7ae5509d2aa507a1bf26cfd4ec05ae4", size = 147181, upload-time = "2025-12-31T18:23:16.014Z" }, + { url = "https://files.pythonhosted.org/packages/59/39/1a656464fca9c32722b36065cbf3ae989da8783e0d012102ade5e968b385/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2b9f060e14b291e35783372afb87227c7f1dc29f876b76f06c39552103b6545", size = 282367, upload-time = "2025-12-31T18:19:51.761Z" }, + { url = "https://files.pythonhosted.org/packages/fa/35/d9efe66a001f989e84e3d1d196f9cc8764dcead87f43e9f22b3f1ea6d1e1/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93e2761f46567334690c4becc8adcd33c3c8bd40c0f05b1a2b151265d57aff73", size = 289402, upload-time = "2025-12-31T18:20:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ca/c10d7fc598528052463d892c4e71c01c00985cfdb86500da3550fb0b6e75/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f07abfb775b6b7c4a55819847f9f9ddd6b710ebc5e822989860771f77a08bf9c", size = 408177, upload-time = "2025-12-31T18:20:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/c5/72/59bbfe8c6bdb17eb86a43a958268da3fefe3d0da67496e3112dfb84f276a/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5c57c4f70c59f6adafa8b3ae8ac1f60da42a87479d6e0eea04dbaa3c00aca6e", size = 307543, upload-time = "2025-12-31T18:21:06.85Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/904760e09f5768ecbf25da8fddf57fb4fb1599b92780294f7e6172e2a398/fastcrc-0.3.5-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:03f50409fbcb118e66739012a7debcfd55dd409d6d87c32986080efdf0a70144", size = 290841, upload-time = "2025-12-31T18:20:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bd/340e3d9525d442189883fce613134a5acf167d7f3e48d95036f1e0c9a2dc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89e6247432e481c420daeb670751a496680484c0c6f69e2198522d6f0f6a5b3a", size = 464580, upload-time = "2025-12-31T18:21:58.673Z" }, + { url = "https://files.pythonhosted.org/packages/4c/97/e2a90908069c89ea41b1cf7ae956fb77219d293ebe62cca32d6c2a077c16/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9d40dcef226068888898c30ef4005c22e697360685d7e40945365bee63e15d26", size = 559567, upload-time = "2025-12-31T18:22:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/91/93/b8652fabe301faf050cd19d2f6ae655a61f743687fb8dfc8e614fbf9c493/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:edabc8ee13f7581e3fb42344999a35a6a253cb65ac019969dc38aa45dac32ee8", size = 518538, upload-time = "2025-12-31T18:22:36.82Z" }, + { url = "https://files.pythonhosted.org/packages/98/c9/585216c6a93b398b3c911653eacaf05c5dc731db39b55f99415025af90bc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:430ae0104f9eafe893f232e81834144ba31041bcc63a3eb379d22d0978c6e926", size = 489898, upload-time = "2025-12-31T18:22:55.903Z" }, + { url = "https://files.pythonhosted.org/packages/57/aa/845d6257bac319b9c1fe16648f2e3d0efa1bbbaf8a5b7b4977851d8102ae/fastcrc-0.3.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b6507652630bc1076f57fe56f265634df21f811d6882a2723be854c58664318c", size = 255565, upload-time = "2025-12-31T18:21:50.502Z" }, + { url = "https://files.pythonhosted.org/packages/f7/66/f67a5e6bf89fa7de858fd055b5a69f00029c16cabf5dcf9bc750db120257/fastcrc-0.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a4c39b9c4a8a37f2fb0a255e3b63b640a5426c0daf3d790399ea85aaabad7f6", size = 251112, upload-time = "2025-12-31T18:21:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/74/a1/c7568f21ad284e68faed0093ccb68bb5d5b248bd08f6241dedfe69ff000b/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d0621722fc4c17bdd7e772fb3fb5384d7c985bb1c8f66256a1dba374e2d12a5", size = 282301, upload-time = "2025-12-31T18:19:53.016Z" }, + { url = "https://files.pythonhosted.org/packages/35/57/da0342f2702e6b50b4d4e5befb2fcd127e82762fe30925b9160eed2184a1/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:648d63f41da1216ef1143573fef35ad2eb34913496ccec58138c2619b10ea469", size = 289811, upload-time = "2025-12-31T18:20:11.584Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d6/94e24eb87bb02546b2b809a8c05034e1e90df3179a44260451e640533a9c/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2be6196f5c4a40b7fca04ef0cc176aa30ac2e19206f362b953fe0db00ea76080", size = 406010, upload-time = "2025-12-31T18:20:30.67Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/c4d71a07bcba4761db0c8ab70b4cb2d1fbd803f72d97e8cde188bd1cb658/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c70e985afa6ec16eebaeb0f3d6bfacb46826636f59853f1169e2eb2a336a2c5", size = 307190, upload-time = "2025-12-31T18:21:08.204Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/bba64d12b0c22d839ddb8cfafae49bb332ae925e839fff2b7752bb20d8dc/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562820b556d9d2070948a89cb76c34d6ec777bbcd3f70bdb89638a16b6072964", size = 286376, upload-time = "2025-12-31T18:21:35.986Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/4fab9f16bb9e6eb6d0c74f913691c25c6f332761c255edd5f3e22a57bd65/fastcrc-0.3.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:99a333fa78aa32f2a024528828cfad71aa39533f210d121ec3962e70542d857b", size = 290450, upload-time = "2025-12-31T18:20:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/41/2c/d4e4072c39f40c8a8550499722ab2539d1de1f30feb97f637d48d33325c7/fastcrc-0.3.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d752dc2285dc446fdf8e3d6e349a3428f46f7b8f059842bdabbb989332d1f3e", size = 299070, upload-time = "2025-12-31T18:21:23.761Z" }, + { url = "https://files.pythonhosted.org/packages/0d/37/12fe830bdfe3b39367645d5b2f8fb2359dc46e67346e91fdd7b9253ad501/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c996c9273c4d3b4c2902852a51b7142bd14a6c750f84bec47778225f7f8952c3", size = 464591, upload-time = "2025-12-31T18:22:00.931Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/edda45991f71b1ec6022c1649397c1b3d82d45cf3c6323684f9a97a4a9ce/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d595809fd266b037c22a22c2d72375c3f572d89200827ecaaa7c002b015b8a2e", size = 559904, upload-time = "2025-12-31T18:22:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d8/aaeb37ebc079ad945dd3aca3fae0f358d5c05af760305e6b6fef63d1c4c7/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6ee18383827caee641d22f60be89e0a0220c1d5a00c5e8cbb744aac1d5bc876", size = 518525, upload-time = "2025-12-31T18:22:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/0d/b5/7479aadffc83bb152884f65c8d543e61d2e95592d4ed1e902019fe5b80f2/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:276591e6828cd4dba70cdfc16450607d742c78119d46060a6cf90ef006a13c71", size = 489362, upload-time = "2025-12-31T18:22:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/4c3af4d4410aff8eef2077425406ddb20ccd3eb8b0fb5d6b6bd5fd2510a3/fastcrc-0.3.5-cp314-cp314-win32.whl", hash = "sha256:08ade4c88109a21ad098b99a9dc51bb3b689f9079828b5e390373414d0868828", size = 139114, upload-time = "2025-12-31T18:23:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/c2564781aeb0c7ae8a6554329b2f05b8a84a2376d2c0ba170ed44ddcc78c/fastcrc-0.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:01565a368ce5fe8a8578992601575313027fb16f55cf3e62c339ae89ccd6ccd2", size = 147063, upload-time = "2025-12-31T18:23:17.152Z" }, + { url = "https://files.pythonhosted.org/packages/33/79/277500a3ac37b3adf2b1c7ab59ddb72587104b7edb5d1a44f9b0a5af4704/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d169f76f8f6330ef4741eadda4cba8f0254c3ec472ed80ebb98d35fc50227d7c", size = 281969, upload-time = "2025-12-31T18:19:54.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/d3/fe850eaf2e4b9be41fa4ae76c4d02bdf809a382d0d7b63cf71d84b47ecca/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d45e1940d5439f2f6fa1f8f1e4202861fb77335c7432f3fc197960af0c6f335d", size = 289745, upload-time = "2025-12-31T18:20:13.831Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/ec4f384a15a95bbad15e359d9d63294c4842eee08f5c576ee22ff3c45035/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8527fded11884789b0ecb9b7d93d9093e38dbfc47d4daefa948447e2397d10b", size = 407620, upload-time = "2025-12-31T18:20:31.987Z" }, + { url = "https://files.pythonhosted.org/packages/98/b8/feb49bf3a2539df193481915653da52a442395c35ffeaf1deb0a649bae87/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6530a28d45ca0754bbeca3a820ae0cce3ded7f3428ed67b587d3ac8ea45fc4aa", size = 307050, upload-time = "2025-12-31T18:21:09.566Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/93fe977525ccb06a491490f53042b3682f15e626263917e3e94cd66d877a/fastcrc-0.3.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4f1229f339b32987e4ad35ae500564a782ce4e62f260150928c0233f32bb6e83", size = 290287, upload-time = "2025-12-31T18:20:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e5/0b6ae9ca6cc8ae599ca298e8a42d96febbc69b52d285464bb994f0a682ff/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a56aa52c40d4293230d2751f136346d6a2b392fa2a38fe342754a6d9c238e", size = 464193, upload-time = "2025-12-31T18:22:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/4a/97/7d4ed6342b244c30b81daabaa6eac591426e216455205e5c85b8566fcd19/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5816694e6aac595cca7a4331c503ed00a22889e0f97a0fa82779ed27316c90ee", size = 559728, upload-time = "2025-12-31T18:22:21.143Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4e/b9cac563d0692345bc9e6dfdc7db92695dd1b3b431ac8fe61ec1dbd6d637/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b598bb000c4290e1eb847ae20a1e07f4ad064d364c2471864fa4c8ccca0f22f6", size = 518422, upload-time = "2025-12-31T18:22:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/a3/58/ef3751447b173ae84d046f90a7dac869b9ff4e11639694f60d8c04f784ea/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:56d564c7ec3dc8116c291963834f0925b4a32df1ea53d239fd470fcdde6b80e4", size = 488965, upload-time = "2025-12-31T18:22:59.006Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, +] + +[[package]] +name = "gcovr" +version = "8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorlog" }, + { name = "jinja2" }, + { name = "lxml" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/37/b4a87dff166dc0a5002e9d03fcb6ca8eeff048247b011b67f047e31122c9/gcovr-8.6.tar.gz", hash = "sha256:b2e7042abca9321cadbab8a06eb34d19f801b831557b28cdc30a029313de8b9e", size = 199997, upload-time = "2026-01-13T20:04:30.019Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/be/f722c843e7875c7cf92cf0e0c1604cddda55a70278c768c6327a78fdba79/gcovr-8.6-py3-none-any.whl", hash = "sha256:dbf9d87c38042752ad6f530aa8210427e22b526611bb7b7bfed0e81977d1f1ef", size = 254618, upload-time = "2026-01-13T20:04:28.15Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "identify" +version = "2.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "inflate64" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/f3/41bb2901543abe7aad0b0b0284ae5854bb75f848cf406bf8a046bf525f67/inflate64-1.0.4.tar.gz", hash = "sha256:b398c686960c029777afc0ed281a86f66adb956cfc3fbf6667cc6453f7b407ce", size = 902542, upload-time = "2025-11-28T10:55:52.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/33/5cfa7468960de1be0833e7e41adf5b7804a0aef2fb46f3679df3876bf3ab/inflate64-1.0.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c8009e4a4918ee6c8cbc49e58fe159464895064cfdf0565fed3f49ca81e45272", size = 58619, upload-time = "2025-11-28T10:54:48.315Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0a/583c7c2832da36e986c5758d0afb6f5944599e55c5b798b066a9ef63e581/inflate64-1.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d173a7a0e865bb7d19685c5b1ad2994712b8361b24136d7e94abeff58505647", size = 35865, upload-time = "2025-11-28T10:54:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/9c6acfbe900e5c8698132244c68036b0455bd2169f46e356c83dc0366f11/inflate64-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8bad992f2d034f5f7e36208e54502d1b0829ce772c898e5dc59109833420148a", size = 36019, upload-time = "2025-11-28T10:54:50.813Z" }, + { url = "https://files.pythonhosted.org/packages/7f/66/c0c3d3b4b863aab2c2ce631d219a8eb3b95b78acd5f40d3212f071e693db/inflate64-1.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bfcf806912ced77a21394f7363805ecacd626b79f93cba87d505a48e88ede78", size = 98765, upload-time = "2025-11-28T10:54:52.273Z" }, + { url = "https://files.pythonhosted.org/packages/37/00/1a2351a85d36b26c5b2b8cfbb37ad86084c98f592dd7590f8577d8b33993/inflate64-1.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62d1aac3aba094ae42e27ce7581b414c90f218248be0953b6aeb11a127225e5d", size = 100594, upload-time = "2025-11-28T10:54:53.684Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3e/5d18ff5b86aaaf54117e1bb6ce15cb17163f56035f9c480e609d35f258ab/inflate64-1.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8065166f355122484f004225b379d403346bdae69ec624786a9334f025580675", size = 96745, upload-time = "2025-11-28T10:54:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/306d5d6aca1e04e596d2a504a59ff9a900623a6ec852f38aab99f384562d/inflate64-1.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94a95f32087d223d2e119ff5c7c264109e8d4cb7e421e7a688a899a6fe021b38", size = 99795, upload-time = "2025-11-28T10:54:56.485Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ec/d4caa4bf3c9e520c15e900fee5a00fc523953843e14aa378ca1abfb2b4ea/inflate64-1.0.4-cp312-cp312-win32.whl", hash = "sha256:ad4fa490bb7dc2a4640a3adaa2d5950f4a465ba034bbcf184c2103646e58ad97", size = 32956, upload-time = "2025-11-28T10:54:57.642Z" }, + { url = "https://files.pythonhosted.org/packages/33/c4/c0de4e9bdf12e449360b710e9ab5b5248804610f382b538773cbd07b72bb/inflate64-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:2c6befdf83d088a6e0d10d0873a9d4bfde2ce00ad7a52c8189cf303306f98030", size = 35577, upload-time = "2025-11-28T10:54:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/03a5ef74ad3869b3c5af3b09216321f5a1a5a45265f7bd6d5abc669c7622/inflate64-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:2b263c619469f90a75f29c421c53d31b208ad494a078235a8f6db2bc96583fdc", size = 33465, upload-time = "2025-11-28T10:55:00.568Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/b7de7ae318a4f233f892c4f7c8b7e0e8643abe3fdcc53ed35020a9fe3f47/inflate64-1.0.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3f37540d0e64884a935fd62a7d17e40ab69f05ec63e815483b6513675d01bef", size = 58633, upload-time = "2025-11-28T10:55:01.931Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5f/6f89c8524503fd7a9ca2bd91fe60d7291b3f684e9d41edb38ef49e10fc3d/inflate64-1.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d24112180c95d12f279cade9a1e21f8be7f4790c4109c293292edf87d061992", size = 35864, upload-time = "2025-11-28T10:55:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/7d/14/9eadd59244b38cf85ccd0ca43d1296c50b3a33aa37be4fb68a1928efa58c/inflate64-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5c098dab17821f466fc6e6a3d78fc6e0295bb51458015f03416b1d58d6a8df4f", size = 36019, upload-time = "2025-11-28T10:55:04.126Z" }, + { url = "https://files.pythonhosted.org/packages/ca/04/399e82d8f5003dd92c8a0c5c1a9a8ce0919114710a496cbe88848bff3a72/inflate64-1.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a984b9287ff0fb596eb058d66a9e94530556afd2b7c054b44f2e0aeeff894e8f", size = 98973, upload-time = "2025-11-28T10:55:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c5/038dc2593bbc4272d87eac8c9f75692267d47f834ced888f6d81995df606/inflate64-1.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62a13d0327631778fa2a47c308ae2b07b2659b7bb8564783259ac65949f8c0c", size = 100777, upload-time = "2025-11-28T10:55:06.41Z" }, + { url = "https://files.pythonhosted.org/packages/74/4a/f6d3031dd3578510894a41bfe1ac149228970ce1629a43f20e0c5abbe8d1/inflate64-1.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:513201336fb3b0b7e2aee5dbbbe30a9f1b23291738b5ceb80076fc285f2ec2f1", size = 96967, upload-time = "2025-11-28T10:55:07.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/ef/7ab3bde4e176609ae0e607a8a9bb38d201885275664a6d574299f5bf7850/inflate64-1.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84ce3a97272ba745fce52b38363855c7201968f6402a794bbade774e64c657b9", size = 99997, upload-time = "2025-11-28T10:55:08.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/59/8256b3e802e203c8645e0b32b25e6bd94508ad572593f0cdf8234db3879a/inflate64-1.0.4-cp313-cp313-win32.whl", hash = "sha256:332051a9d7e50579b90a3f555d68f53414b06f636c9ffe82e97c0baae3c8fbcc", size = 32958, upload-time = "2025-11-28T10:55:10.343Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4f/5784ee1eb8260f2310e24ef2883f1f494f9332bcfde4ed14ee780372149e/inflate64-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:3983f53b590ff7d0ba243f664ce852aca882482f30f7a8eab33e10d769336d0c", size = 35578, upload-time = "2025-11-28T10:55:11.389Z" }, + { url = "https://files.pythonhosted.org/packages/39/e8/8d927770ce25dc9764c8104207a80653d65471d0a6a8f9ead350016e4586/inflate64-1.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:118d8286f085e99a14341c76ef9fbffd56619ccc80318a9a204aea3dbfa71470", size = 33465, upload-time = "2025-11-28T10:55:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fb/ec9d10f44f2fc7666ad5d70acae2b8a1941e8e08ccae1fad0820f7796be3/inflate64-1.0.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4f61925b2d4248eac2ebb15350a80aaa0d1f7f1dc770bd5ebbbb3b0db4a6a416", size = 58727, upload-time = "2025-11-28T10:55:13.834Z" }, + { url = "https://files.pythonhosted.org/packages/81/80/24ba0d2ee14e07e275e9c5b058e59a8a58f8ef42dd51a78ebbfd7c857ac4/inflate64-1.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c1acf18b08b32981a4a11ec5a112b8ad5d7c7a5b15cb5bdbdb5b19201e9aa180", size = 35945, upload-time = "2025-11-28T10:55:15.225Z" }, + { url = "https://files.pythonhosted.org/packages/70/b8/073a79716e093db973b8823bdfb02e10fbdf65642dbe1fa3cda24832aeb2/inflate64-1.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abddae8920b2eaef824254e14b8d4ff54afbe6194a1bbe9816584859f0c1244d", size = 36060, upload-time = "2025-11-28T10:55:16.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f0/87d3c317ed0acd94f5e9b3b1b9e9000228ea2af0cb4618c62cbfc816da34/inflate64-1.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b303132cc562a906543a56f35c4e164e3880da6ff041cb4a7b1df9f9d2b4bb69", size = 98995, upload-time = "2025-11-28T10:55:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/0b6035302e9c33f004240a50cb6e2e1fc7bb1f2b415b02d939c551bdd06b/inflate64-1.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f0993214dea0738c557fa56c13cd9083aef0097a201d726c21984ad7f577514", size = 100781, upload-time = "2025-11-28T10:55:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/2b/05/5f383c615ec0f01bcbbc699a71da167623e494083ab7ed0df86b4bddf125/inflate64-1.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a6baedc3288d7a4ff588951d3a9a97a5391dceed6255ff5b16e42cae7274bfa9", size = 97038, upload-time = "2025-11-28T10:55:20.156Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/c1482718c717c49c67490c42b4fdced9476e894eaebd52193e488c12e188/inflate64-1.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a846ce1f38845b20bef2625af1b512be83416d97824539524c5a34e7a729aec7", size = 100016, upload-time = "2025-11-28T10:55:21.337Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7f/700ede7474e72a1c0e2e8fe36624cd0225ca8b2875eca33d64aa2de75f4d/inflate64-1.0.4-cp314-cp314-win32.whl", hash = "sha256:eef87908c780439393d577a155868317f0a275b47b417db9f47d8633ec791745", size = 33691, upload-time = "2025-11-28T10:55:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/f2972df8cceecc9bf3afa3353d517ffc7125285198c844588e9aaf98f5d0/inflate64-1.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:fb2fdd63ef3933b67af98b3f2ee2f57e7787278041d7ba4821382fedd729b68a", size = 36304, upload-time = "2025-11-28T10:55:24.005Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/16200aced67215119fb30ec9a5889b48289ee2fa5ce5137623a9ad41b2c4/inflate64-1.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:2e129669a0243ac7816fd526946ee01c25688fe81623a6d6bc95b3156d80f4fb", size = 34491, upload-time = "2025-11-28T10:55:25.068Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/b466ec7666c40912ea81a305a8a2b75f5998e6cec1d3d75e067d76203731/inflate64-1.0.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:b17bf665d948dc4edeea0cd17752415d0cd7240c882b9c7e136ad4cc4321e9d4", size = 59300, upload-time = "2025-11-28T10:55:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/16e1168ac80f39894e6cb18b439eec63fec42cbced239aebfe12081b6ec7/inflate64-1.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6751758301936fbb38fa38eb5312e14e27b6a1abf568f83c17557fab2694373d", size = 36258, upload-time = "2025-11-28T10:55:27.295Z" }, + { url = "https://files.pythonhosted.org/packages/25/42/c463b42fd8a7947b4445fbaf57c265bb7f3114362fb7aee6884ffd8b5341/inflate64-1.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d6a4136752aa2a544301059d8f13780aeb88c34d60770258436a87dacd3fc304", size = 36296, upload-time = "2025-11-28T10:55:28.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/f1/cf6121926e405020e9e7bccb78ec7781fbc87500ec67368e7d9e866758be/inflate64-1.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:938ebc6b28578bfd365d1a9fdb18b7faab08321babeb2198e8025d07d8dc7fb5", size = 106788, upload-time = "2025-11-28T10:55:29.797Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/b653f9962497cf4d3520d69272894c37fd76f86a0e04bb3bd9f32827dc2f/inflate64-1.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61f51f80fa6f367288343c1a2cd20a42af454883087064e9274fd2a8c3a5a200", size = 107959, upload-time = "2025-11-28T10:55:31.306Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/65d88109b63611c9b6c29008201107127cf2603d186ab99beec39d85f38e/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:172b51da7bbfa66b33f0a5405e944807b9949e92cf4cd9f983c07af8152766df", size = 104213, upload-time = "2025-11-28T10:55:32.506Z" }, + { url = "https://files.pythonhosted.org/packages/f9/94/c17de2f55b9fb1269bca4657f9089efe4ba0f3d4b652f07b34dfc69f69a2/inflate64-1.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ca9a2985afd5a14fb48cd126a67e5944ccb7a0a6bdec58c4f796c8c88a84539", size = 106629, upload-time = "2025-11-28T10:55:33.735Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/8bbc587bb2ad09ab7edf1f9215b2c5faf4fa7ee7c071daefe9ed55e28814/inflate64-1.0.4-cp314-cp314t-win32.whl", hash = "sha256:f8964ceaabea294bc20abc9ef408c6aae978a75c25c83168a76cd87a37c38938", size = 33865, upload-time = "2025-11-28T10:55:35.335Z" }, + { url = "https://files.pythonhosted.org/packages/91/87/8bf6f412f93c8b6dc14866a021b83321331fbdd17f6ab902a24dbf88773d/inflate64-1.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:7d13b04cba65c12d21e65eaa77da9484e265e8e821b26e0761d1455ad3a878d9", size = 36943, upload-time = "2025-11-28T10:55:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/33447bb3c4e3c0ae7b1fde3aadc52a18b2b0193cfcf4f585977e924c6463/inflate64-1.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:9ae3ee727235a06dc3cd353ee5761fdd8e3b56ad119c711f61680528972a6ced", size = 34846, upload-time = "2025-11-28T10:55:38.433Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "lsprotocol" +version = "2025.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, +] + +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, + { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, + { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, + { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, + { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, + { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, + { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "meson" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/d3/8c43e758cf456273c32652bb8b7a4ec2d74327d8849856b0b714ad671da7/meson-1.10.1.tar.gz", hash = "sha256:c42296f12db316a4515b9375a5df330f2e751ccdd4f608430d41d7d6210e4317", size = 2413969, upload-time = "2026-01-18T14:45:08.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d5/582789135863eec7c8c1fa31fbde401b3d5d82dbbb4a0973351a1698f738/meson-1.10.1-py3-none-any.whl", hash = "sha256:fe43d1cc2e6de146fbea78f3a062194bcc0e779efc8a0f0d7c35544dfb86731f", size = 1057724, upload-time = "2026-01-18T14:45:02.584Z" }, +] + +[[package]] +name = "multivolumefile" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/f0/a7786212b5a4cb9ba05ae84a2bbd11d1d0279523aea0424b6d981d652a14/multivolumefile-0.2.3.tar.gz", hash = "sha256:a0648d0aafbc96e59198d5c17e9acad7eb531abea51035d08ce8060dcad709d6", size = 77984, upload-time = "2021-04-29T12:18:39.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/31/ec5f46fd4c83185b806aa9c736e228cb780f13990a9cf4da0beb70025fcc/multivolumefile-0.2.3-py3-none-any.whl", hash = "sha256:237f4353b60af1703087cf7725755a1f6fcaeeea48421e1896940cd1c920d678", size = 17037, upload-time = "2021-04-29T12:18:38.886Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "patch-ng" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/bb/ebd7c6058dcfbf634986f9a8b3fb638f3269501c73701a48b7530042da5b/patch-ng-1.19.0.tar.gz", hash = "sha256:27484792f4ac1c15fe2f3e4cecf74bb9833d33b75c715b71d199f7e1e7d1f786", size = 18488, upload-time = "2025-10-08T15:17:19.616Z" } + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "py7zr" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-zstd", marker = "python_full_version < '3.14'" }, + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation == 'PyPy'" }, + { name = "inflate64" }, + { name = "multivolumefile" }, + { name = "psutil", marker = "sys_platform != 'cygwin'" }, + { name = "pybcj" }, + { name = "pycryptodomex" }, + { name = "pyppmd" }, + { name = "texttable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/e6/01fb15361ca75ee5d01df6361825a49816a836c99980c5481da0e40c6877/py7zr-1.1.0.tar.gz", hash = "sha256:087b1a94861ad9eb4d21604f6aaa0a8986a7e00580abd79fedd6f82fecf0592c", size = 70855, upload-time = "2025-12-21T03:27:44.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/9c/762284710ead9076eeecd55fb60509c19cd1f4bea811df5f3603725b44cb/py7zr-1.1.0-py3-none-any.whl", hash = "sha256:5921bc30fb72b5453aafe3b2183664c08ef508cde2655988d5e9bd6078353ef7", size = 71257, upload-time = "2025-12-21T03:27:42.881Z" }, +] + +[[package]] +name = "pybcj" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/2670b672655b18454841b8e88f024b9159d637a4c07f6ce6db85accf8467/pybcj-1.0.7.tar.gz", hash = "sha256:72d64574069ffb0a800020668376b7ebd7adea159adbf4d35f8effc62f0daa67", size = 31282, upload-time = "2025-11-29T00:53:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/60/39b51114e3e740b61844448c3b61be146781a5c0ffabcd473a17ba7f4336/pybcj-1.0.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d39787b85678d2ab1c67e2f21dd2e71be851f08e5c9fe619c605877b57dd529d", size = 31858, upload-time = "2025-11-29T00:52:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/ee/15/df4cd94bdf6a73c2b6ecf5e99dd9dcfe654215992a0114860ff1c94752b5/pybcj-1.0.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8cd5dd166093a1fb146fb78859aac0f00b45db6c11074705517bc72a940a1c8e", size = 23746, upload-time = "2025-11-29T00:52:46.947Z" }, + { url = "https://files.pythonhosted.org/packages/15/1a/f8bbe5f9ad95a0c2d1853006a93021aa1c2851b25a6bccc0894b1d72c0f4/pybcj-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82152e8641f5ce68638f3504227065f27b6b1efe96479ffbf20d81530c220062", size = 24088, upload-time = "2025-11-29T00:52:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/53675b56f9dbcb7a4dd681af8c05b1abf95fea3cdf7bf64872b9e0fdc8c8/pybcj-1.0.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2095b45d05f8d19430167b7df52ebd920df854ab8d064bae879df0a4611374b3", size = 52407, upload-time = "2025-11-29T00:52:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/3008e748c7d35c407db97b77af52ade07756033250d0e208a6af231131ca/pybcj-1.0.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b400c9f48faed01edb7f0df54b4354270325c886e785f31c866c581a46023b", size = 51462, upload-time = "2025-11-29T00:52:51.016Z" }, + { url = "https://files.pythonhosted.org/packages/83/f4/8e8b079af7ac6a51b2edcb8bed6040a9748542cb1daf55387f769f9571d0/pybcj-1.0.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8210e51a2d4e5ccb4fdb75a1e692dd8c121858b589026bb28988ed7ffdb7ed00", size = 50320, upload-time = "2025-11-29T00:52:52.835Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/c2277eeb083029313f5822a491c39d7af91ebd1e717f42c772d56b8c3c4e/pybcj-1.0.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6c3fe420083186ae2e5f75c23aa6563dcb030b8fc188d00778ce374d1df1984", size = 49987, upload-time = "2025-11-29T00:52:53.904Z" }, + { url = "https://files.pythonhosted.org/packages/a0/52/711a94d5ae634ff3dd51324a40885158f819ba660b4601653bd78bbd33cd/pybcj-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:c435062d66364f85674a639541980000e37657b98367a2ce2699514e44b8ab05", size = 24940, upload-time = "2025-11-29T00:52:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d4/02e3ca25deca3359e63b70e5804bdbae9f400d03f93d0c66341e0471bb39/pybcj-1.0.7-cp312-cp312-win_arm64.whl", hash = "sha256:3f74fd70b08092e58b1ee13c67fbf9de63d73eb1c61ab06670a0d7161efeb252", size = 23410, upload-time = "2025-11-29T00:52:55.944Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6f/b08d5be15209b584858981b44a447ae7a6d8c591487e502e212b5420f94e/pybcj-1.0.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d5e0feeeee3a659b30d7afbd89bf41da84e8c8fe13e5b997457e799a70fa550", size = 31869, upload-time = "2025-11-29T00:52:56.973Z" }, + { url = "https://files.pythonhosted.org/packages/47/e5/68bffbc87581ea96bb4aea623d8cd085786f36d5b912ed8d9bade3265110/pybcj-1.0.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:60baaf9f0da31438515a401145f920f75f2ec7d511165bbf57475467af72a3e6", size = 23750, upload-time = "2025-11-29T00:52:57.986Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/718d6daa0d5e15e2131301220eaefbf6bc8cd0c90791c5ac18c893be111a/pybcj-1.0.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c6e726618c3d43c730df5a4067fc19653b360f89c2f72f4323dae10d324552", size = 24089, upload-time = "2025-11-29T00:52:59.457Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a6/7df6b55b64b370e2d42aa51b88402336d7f17c97eda284755f404d8d6047/pybcj-1.0.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a5fcd40a4ce8f0c5428032ec5db9f03abb42214b993886cdf558e5644de636e", size = 52454, upload-time = "2025-11-29T00:53:00.468Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/2ce282501a39f32f27cef2a3ee11c621f9d5348da4441d2326f2fcc9b17e/pybcj-1.0.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:029112255c22de66e0117bec932c8be341ed20c56dcf6a961c14689f7f0ce772", size = 51522, upload-time = "2025-11-29T00:53:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2a/f75ea5f09c91e01456a13759a325e007663855fa16af28830ce7d44d5427/pybcj-1.0.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6492bcef5cb6883506b9dce5e48cb81217407305957b0e602c6c689c60097c5e", size = 50398, upload-time = "2025-11-29T00:53:02.839Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/6d49e0c62a0ab68aa3325e6f141c33f37e5bc9d61cc9a1186c0a2d324fbe/pybcj-1.0.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22a7f4a51d36a1abb67a61e93248f997eb2be278f788d681096f5044ae18b4f9", size = 50087, upload-time = "2025-11-29T00:53:03.966Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/a0d58633236783028c08e585b3c4f4715a4286970b4ec4a25acfe23794f1/pybcj-1.0.7-cp313-cp313-win_amd64.whl", hash = "sha256:ebcce9b419fe5d3109150a1fab0fc93a64d5cd812ca44c5ddb7d4f7128ea369f", size = 24939, upload-time = "2025-11-29T00:53:05.363Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/8856cd8bc07e66322a55750ab87a264829cdfbc6cd85c5844340cf06bc53/pybcj-1.0.7-cp313-cp313-win_arm64.whl", hash = "sha256:bc6acf0320976b4e31bdc0e59b16689083d5c346a6c62ac4f799685d1cc5cf27", size = 23409, upload-time = "2025-11-29T00:53:06.749Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9e/eb50f11ea7fb6342167bb8353d48966b490afa8ae47c98917e9acd045b71/pybcj-1.0.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:293f951eb3877840acab79f0c4dcfc06eab03e087cb9e4c004ec058e093acb1d", size = 32409, upload-time = "2025-11-29T00:53:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f6/b55f4a5faf9bd162426f49de84873fbf813698d1b018234c3c99816a9662/pybcj-1.0.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3ae64960904f362d33ffca10715803afd9f9a6a2a592f871dcb335acf82edf29", size = 23818, upload-time = "2025-11-29T00:53:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/4c/24/78cf0973b3deded1e072479ac35d387083a626b3dc0b29e2e099cf73e082/pybcj-1.0.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:70aa4476910f982025f878e598c136559a6d78b59fc20ba8b4b592306cde6051", size = 24115, upload-time = "2025-11-29T00:53:10.241Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/e044d7127018158c67b119e6ed26d1a4b34b1e1defd9b0d3de029a782b9f/pybcj-1.0.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79ce3ce9b380b1b75c5e490abc3888ee3b5b2d28c22b59618674bf410b9cee16", size = 52484, upload-time = "2025-11-29T00:53:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/98/05/55337b0a9807887967b2cf49dd6f3bf47c7745786b0bda51a5cbfda66c78/pybcj-1.0.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc73ee1bc064d6f97dfd66051d3859b32e1b6a4cf89b077f5c8ef6c2dccb71af", size = 51471, upload-time = "2025-11-29T00:53:12.86Z" }, + { url = "https://files.pythonhosted.org/packages/59/09/6c57cbbf4931ac304f822ef78f478dd6b34b00b08e9530d308737959f064/pybcj-1.0.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5b0d13f41a9f85b3f95dd5dc7bfaa9539e80f8ae60a96db7f34c07ed732e4a82", size = 50435, upload-time = "2025-11-29T00:53:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/bc/93/26415ca1b96456e27550dae67092023af3e8454621b1efa701079d8acb64/pybcj-1.0.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:597d7e9a8cbb30a6ed54d552fd3436edb32bbb821a7ac2fa8e5c7ebd1f7e0e93", size = 50010, upload-time = "2025-11-29T00:53:15.396Z" }, + { url = "https://files.pythonhosted.org/packages/91/f7/d4e55aede85143adff3ded89a1ae87bbf29060ddc2249fb861d78b103d4a/pybcj-1.0.7-cp314-cp314-win_amd64.whl", hash = "sha256:4603cc41ceb1236abe9169e2ead344140be5d2c3ac01bbc5e44cb1b13078a009", size = 25283, upload-time = "2025-11-29T00:53:16.472Z" }, + { url = "https://files.pythonhosted.org/packages/f6/19/9939ba437140d7375aecf8063d8695dfeea56ca40f57ddaa6ca3b828c131/pybcj-1.0.7-cp314-cp314-win_arm64.whl", hash = "sha256:adf985e816ddd59f3bf6d1066b7fa89de7424a4f19f3725f9976284cabe54e28", size = 23622, upload-time = "2025-11-29T00:53:17.994Z" }, + { url = "https://files.pythonhosted.org/packages/62/6e/5e14c70f3ddc268f28e7ac912510c8bc2b5430f18bb7f326bbb9d1878955/pybcj-1.0.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bbd835873de147481d62c11ba91a75d26a72df1142de3516b384b04e5a1db6d", size = 33016, upload-time = "2025-11-29T00:53:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/49/c1/25a1a8d5811c5d2a2ca3439c548abbba882fa72eba9a6eb41040206fdc26/pybcj-1.0.7-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b7576b25d7b01a953e2f987e77cef93c001db7b95924a5541d5a55f9195a7e89", size = 24114, upload-time = "2025-11-29T00:53:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/35/66/09567564cdc6cc2713d729bd29794367ebb67fa9ea5c10313c49608c08fa/pybcj-1.0.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57b36920498f82ca6a325a98b13e0fbff8fc29bade7aaaddc7d284640bffd87d", size = 24428, upload-time = "2025-11-29T00:53:21.522Z" }, + { url = "https://files.pythonhosted.org/packages/c5/49/4f6793624eb418cc2536ef39e67c8783d7ca542f6139a051e9d2d76fdc80/pybcj-1.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aac2a46faf41e373939f6d3e6a5aa2121bf09e2446972c14a8e5d1ca3b0f8130", size = 58806, upload-time = "2025-11-29T00:53:22.568Z" }, + { url = "https://files.pythonhosted.org/packages/be/1c/8604f7fe360a9340bbf798b826a88f8e9d186fc031eb531bcd11ac9e684b/pybcj-1.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c7d6156ef2b4e8ecd450b62dc4cc3a89e8dda307cb26288b670952ef0df3a37", size = 56975, upload-time = "2025-11-29T00:53:23.691Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/d706b4220f60fa4e6a5756dc0051fb76c32a5615958fc12f0dcacef3f86b/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0fe306213de1e764abae63c06ae5a4e9a83632f62612805f1f883b8d74431901", size = 56023, upload-time = "2025-11-29T00:53:25.135Z" }, + { url = "https://files.pythonhosted.org/packages/3e/28/ad306f5acb1226241960bb86e9021b4e32bb7da426ccdc824d9d36d61261/pybcj-1.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00448182d535cca37e8f24d892d480fa86f80ff20c79385f6eca75f118efcbb4", size = 55194, upload-time = "2025-11-29T00:53:26.209Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d8/70c5e5701926fc67dae02101f0298d21c0b89ff83fa705712c3b4da252e5/pybcj-1.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:7e94aa712d0fa5fda9875828441755ece7121fc3f8c5cc3bc8ee92d05b853590", size = 25826, upload-time = "2025-11-29T00:53:27.655Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/7a87ba32c0c0756f36000fafe642fa4609be2c26a50a7913a057a47eabdf/pybcj-1.0.7-cp314-cp314t-win_arm64.whl", hash = "sha256:16fd4e51a5556d1f38d7ba5d1fab588bfb60ae23d2299b5179779bf9900adf71", size = 24049, upload-time = "2025-11-29T00:53:28.679Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodomex" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/85/e24bf90972a30b0fcd16c73009add1d7d7cd9140c2498a68252028899e41/pycryptodomex-3.23.0.tar.gz", hash = "sha256:71909758f010c82bc99b0abf4ea12012c98962fbf0583c2164f8b84533c2e4da", size = 4922157, upload-time = "2025-05-17T17:23:41.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/00/10edb04777069a42490a38c137099d4b17ba6e36a4e6e28bdc7470e9e853/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:7b37e08e3871efe2187bc1fd9320cc81d87caf19816c648f24443483005ff886", size = 2498764, upload-time = "2025-05-17T17:22:21.453Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3f/2872a9c2d3a27eac094f9ceaa5a8a483b774ae69018040ea3240d5b11154/pycryptodomex-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:91979028227543010d7b2ba2471cf1d1e398b3f183cb105ac584df0c36dac28d", size = 1643012, upload-time = "2025-05-17T17:22:23.702Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/774c2e2b4f6570fbf6a4972161adbb183aeeaa1863bde31e8706f123bf92/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8962204c47464d5c1c4038abeadd4514a133b28748bcd9fa5b6d62e3cec6fa", size = 2187643, upload-time = "2025-05-17T17:22:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/71065b24cb889d537954cedc3ae5466af00a2cabcff8e29b73be047e9a19/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a33986a0066860f7fcf7c7bd2bc804fa90e434183645595ae7b33d01f3c91ed8", size = 2273762, upload-time = "2025-05-17T17:22:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0b/ff6f43b7fbef4d302c8b981fe58467b8871902cdc3eb28896b52421422cc/pycryptodomex-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7947ab8d589e3178da3d7cdeabe14f841b391e17046954f2fbcd941705762b5", size = 2313012, upload-time = "2025-05-17T17:22:30.57Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/9d4772c0506ab6da10b41159493657105d3f8bb5c53615d19452afc6b315/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c25e30a20e1b426e1f0fa00131c516f16e474204eee1139d1603e132acffc314", size = 2186856, upload-time = "2025-05-17T17:22:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/8b30efcd6341707a234e5eba5493700a17852ca1ac7a75daa7945fcf6427/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:da4fa650cef02db88c2b98acc5434461e027dce0ae8c22dd5a69013eaf510006", size = 2347523, upload-time = "2025-05-17T17:22:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/0f/02/16868e9f655b7670dbb0ac4f2844145cbc42251f916fc35c414ad2359849/pycryptodomex-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58b851b9effd0d072d4ca2e4542bf2a4abcf13c82a29fd2c93ce27ee2a2e9462", size = 2272825, upload-time = "2025-05-17T17:22:37.632Z" }, + { url = "https://files.pythonhosted.org/packages/ca/18/4ca89ac737230b52ac8ffaca42f9c6f1fd07c81a6cd821e91af79db60632/pycryptodomex-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:a9d446e844f08299236780f2efa9898c818fe7e02f17263866b8550c7d5fb328", size = 1772078, upload-time = "2025-05-17T17:22:40Z" }, + { url = "https://files.pythonhosted.org/packages/73/34/13e01c322db027682e00986873eca803f11c56ade9ba5bbf3225841ea2d4/pycryptodomex-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bc65bdd9fc8de7a35a74cab1c898cab391a4add33a8fe740bda00f5976ca4708", size = 1803656, upload-time = "2025-05-17T17:22:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/54/68/9504c8796b1805d58f4425002bcca20f12880e6fa4dc2fc9a668705c7a08/pycryptodomex-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c885da45e70139464f082018ac527fdaad26f1657a99ee13eecdce0f0ca24ab4", size = 1707172, upload-time = "2025-05-17T17:22:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9c/1a8f35daa39784ed8adf93a694e7e5dc15c23c741bbda06e1d45f8979e9e/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:06698f957fe1ab229a99ba2defeeae1c09af185baa909a31a5d1f9d42b1aaed6", size = 2499240, upload-time = "2025-05-17T17:22:46.953Z" }, + { url = "https://files.pythonhosted.org/packages/7a/62/f5221a191a97157d240cf6643747558759126c76ee92f29a3f4aee3197a5/pycryptodomex-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2c2537863eccef2d41061e82a881dcabb04944c5c06c5aa7110b577cc487545", size = 1644042, upload-time = "2025-05-17T17:22:49.098Z" }, + { url = "https://files.pythonhosted.org/packages/8c/fd/5a054543c8988d4ed7b612721d7e78a4b9bf36bc3c5ad45ef45c22d0060e/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43c446e2ba8df8889e0e16f02211c25b4934898384c1ec1ec04d7889c0333587", size = 2186227, upload-time = "2025-05-17T17:22:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a9/8862616a85cf450d2822dbd4fff1fcaba90877907a6ff5bc2672cafe42f8/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f489c4765093fb60e2edafdf223397bc716491b2b69fe74367b70d6999257a5c", size = 2272578, upload-time = "2025-05-17T17:22:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/bda9c49a7c1842820de674ab36c79f4fbeeee03f8ff0e4f3546c3889076b/pycryptodomex-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdc69d0d3d989a1029df0eed67cc5e8e5d968f3724f4519bd03e0ec68df7543c", size = 2312166, upload-time = "2025-05-17T17:22:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/870b9bf8ca92866ca0186534801cf8d20554ad2a76ca959538041b7a7cf4/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bbcb1dd0f646484939e142462d9e532482bc74475cecf9c4903d4e1cd21f003", size = 2185467, upload-time = "2025-05-17T17:22:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/96/e3/ce9348236d8e669fea5dd82a90e86be48b9c341210f44e25443162aba187/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:8a4fcd42ccb04c31268d1efeecfccfd1249612b4de6374205376b8f280321744", size = 2346104, upload-time = "2025-05-17T17:23:02.112Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e869bcee87beb89040263c416a8a50204f7f7a83ac11897646c9e71e0daf/pycryptodomex-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55ccbe27f049743a4caf4f4221b166560d3438d0b1e5ab929e07ae1702a4d6fd", size = 2271038, upload-time = "2025-05-17T17:23:04.872Z" }, + { url = "https://files.pythonhosted.org/packages/8d/67/09ee8500dd22614af5fbaa51a4aee6e342b5fa8aecf0a6cb9cbf52fa6d45/pycryptodomex-3.23.0-cp37-abi3-win32.whl", hash = "sha256:189afbc87f0b9f158386bf051f720e20fa6145975f1e76369303d0f31d1a8d7c", size = 1771969, upload-time = "2025-05-17T17:23:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/69/96/11f36f71a865dd6df03716d33bd07a67e9d20f6b8d39820470b766af323c/pycryptodomex-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:52e5ca58c3a0b0bd5e100a9fbc8015059b05cffc6c66ce9d98b4b45e023443b9", size = 1803124, upload-time = "2025-05-17T17:23:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, +] + +[[package]] +name = "pygls" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "lsprotocol" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/94/cce3560f6c7296be43bd67ba342f8972b8adddfe407b62b25d1fb90c514b/pygls-2.0.1.tar.gz", hash = "sha256:2f774a669fbe2ece977d302786f01f9b0c5df7d0204ea0fa371ecb08288d6b86", size = 55796, upload-time = "2026-01-26T23:04:33.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/8b/d0d7e51f928ec6e555c943f7c07a712b992e9cf061d52cbf37fbf8622c05/pygls-2.0.1-py3-none-any.whl", hash = "sha256:d29748042cea5bedc98285eb3e2c0c60bf3fc73786319519001bf72bbe8f36cc", size = 69536, upload-time = "2026-01-26T23:04:35.197Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymavlink" +version = "2.4.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastcrc" }, + { name = "lxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a2/0a4ce323178f60f869e42a2ed3844bead7b685807674bef966a39661606e/pymavlink-2.4.49.tar.gz", hash = "sha256:d7cf10d5592d038a18aa972711177ebb88be2143efcc258df630b0513e9da2c2", size = 6172115, upload-time = "2025-08-01T23:33:10.372Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/81/062427da96311d359ed799af8569b7b3ffa25c333fb4a961478ce5a4735f/pymavlink-2.4.49-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b7925330a4bb30bcc732971cfeb1aa54515efd28f4588d7abc942967d7a2298b", size = 6291309, upload-time = "2025-08-01T23:32:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/8bfed758e2efc2d6f28259634d94c09b10e50c62cd5914ac888ce268378d/pymavlink-2.4.49-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf4c13703bc6dcbc70083a12aaec71f3a36a6b607290e93f59f2b004ebd02784", size = 6226353, upload-time = "2025-08-01T23:32:06.311Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/78419c2ae5489fdd996f6af0c1e4bd6dceaa5a5b155a367851926da7b05f/pymavlink-2.4.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8522d652fef8fb03c7ee50abd2686ffe0262cbec06136ae230f3a88cccdff21c", size = 6222943, upload-time = "2025-08-01T23:32:07.609Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/c9ae05436ed219bb9f2b505d7c82474173c8ebcd28ff8f55833213d732a2/pymavlink-2.4.49-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1b6e29972792a1da3fafde911355631b8a62735297a2f3c5209aa985919917a", size = 6376049, upload-time = "2025-08-01T23:32:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/36/6f/eb93cc44e2653044eb5bbfa7ce0f808611e42d56106a4d6d5de4db8bb211/pymavlink-2.4.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8b13b9ac195e9fa8f01cda21638e26af9c5a90e3475ddb43fd2b9e396913f6b", size = 6388174, upload-time = "2025-08-01T23:32:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/34099ab9e4e41db4b2ec9f05c3d8e7726ef3d5a2ae8cfb6f90596c4d82fb/pymavlink-2.4.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a87b508a9e9215afdb809189224731b4b34153f3879226fd94b8f485ac626ab", size = 6390472, upload-time = "2025-08-01T23:32:11.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/51/0146c0008feb5d8a7721870489b4c19fd30a1e49433be7a83624dc961f90/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31ca1c1e60a21f240abf35258df30e7b5ee954a055bbe7584f0ebabb48dd8c40", size = 6376189, upload-time = "2025-08-01T23:32:12.921Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/aa4b51cd9948eca7b63359ad392d8cd69b393bd781830c4a518a98aede33/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f854d1d730f40d4efa52d8901413af1b23d16187e941b76d55f0dcc0208d641d", size = 6378697, upload-time = "2025-08-01T23:32:14.471Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b6/dec8f9f7e1769894b7b11c8900b0a13cf13fb9cee2c45d7f9f5a785b3f39/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16c915365a21b7734c794ba97fa804ae6db52411bf62d21b877a51df2183dfab", size = 6384644, upload-time = "2025-08-01T23:32:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2e/3db53612dab0bfa31eca8e9162489f44c9f9e23c183a2b263d707eb5ddc7/pymavlink-2.4.49-cp312-cp312-win32.whl", hash = "sha256:af7e84aec82f00fd574c2a0dbe11fb1a4c3cbf26f294ca0ef3807dcc5670567e", size = 6230813, upload-time = "2025-08-01T23:32:17.732Z" }, + { url = "https://files.pythonhosted.org/packages/bf/47/fe857933a464b5a07bf72e2a1d2e92a87ad9d96915f48f86c9475333a63d/pymavlink-2.4.49-cp312-cp312-win_amd64.whl", hash = "sha256:246e227ca8535de98a4b93876a14b9ced7bfc82c70458e480725a715aa6b6bf3", size = 6242451, upload-time = "2025-08-01T23:32:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/25/ca/995d1201925ad49fb6b174a9d488f1d90b77256b1088ebd3d7f192b0f65a/pymavlink-2.4.49-cp312-cp312-win_arm64.whl", hash = "sha256:c7415592166d9cbd4434775828b00c71bebf292c8367744d861e3ccd2dab9f3e", size = 6231742, upload-time = "2025-08-01T23:32:20.707Z" }, + { url = "https://files.pythonhosted.org/packages/26/10/67756d987b1aefd991664ce0a996ee3bf69ed7aaf8c7319ff6012a4dc8a2/pymavlink-2.4.49-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f7cfaf3cc1abd611c0757d4b7e56eaf5b4cfa54510a3178b26ebbd9d3443b9d7", size = 6290269, upload-time = "2025-08-01T23:32:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/c2/02/9e63467d65da78fed03981c86e5b7877fcf163a98372ba5ef03015e3798c/pymavlink-2.4.49-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db9c0d00e79946ecf1ac89847f32712ef546994342f44b3e9a68e59cfbc85bef", size = 6225761, upload-time = "2025-08-01T23:32:23.419Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/46a5512964043ada02914657610c885b083375dd169dea172870f4dd73b0/pymavlink-2.4.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37937d5dfd2ddc2a64ea64687380278ac9c49e1644ea125f1e8a5caf4e1f2ebd", size = 6222450, upload-time = "2025-08-01T23:32:24.803Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/1b63a8c4d35887edc979805b324240ff4b847e9d912b323d71613e8f1971/pymavlink-2.4.49-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8100f2f1f53b094611531df2cfb25f1c8e8fdee01f095eb8ee18976994663cf6", size = 6368072, upload-time = "2025-08-01T23:32:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/29/69/94348757424a94c5a3e87f41d4c05a168bc5de2549afdbea1d4424a318dc/pymavlink-2.4.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2db4b88f38aa1ba4c0653a8c5938364bfe78a008e8d02627534015142bf774", size = 6379869, upload-time = "2025-08-01T23:32:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/792925eadc046ae580ab444181a06e8d51d38204a81a9274460f90009b88/pymavlink-2.4.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7fe9286fd5b2db05277d30d1ea6b9b3a9ea010a99aff04d451705cc4be6a7e6", size = 6382786, upload-time = "2025-08-01T23:32:28.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/d7b1d280dda800ac386fd54dcded6344b518a8266a918729512e46e39f6b/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d49309e00d4d434f2e414c166b18ef18496987a13a613864f89a19ca190ef0d0", size = 6368732, upload-time = "2025-08-01T23:32:29.794Z" }, + { url = "https://files.pythonhosted.org/packages/23/89/b75ef8eea1e31ec07f13fe71883b08cdc2bce0c33418218cebb03e55124a/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7104eef554b01d6c180e1a532dc494c4b1d74e48b0b725328ec39f042982e172", size = 6370950, upload-time = "2025-08-01T23:32:31.041Z" }, + { url = "https://files.pythonhosted.org/packages/f6/57/3cb77e3f593e27dc63bd74357b3c3b57075af74771c4446275097f0865f2/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:795e6628f9ecf0b06e3b7b65f8fcf477ec1603971d590cffd4640cff1852da23", size = 6376423, upload-time = "2025-08-01T23:32:32.345Z" }, + { url = "https://files.pythonhosted.org/packages/41/bb/49b83c6d212751c88a29cebe413c940ee1d0b7991a667710689eb0cd648e/pymavlink-2.4.49-cp313-cp313-win32.whl", hash = "sha256:9f14bbe1ce3d5c0af4994f0f76d1a8d0c2f915d7dcb7645c1ecba42eeff89536", size = 6230635, upload-time = "2025-08-01T23:32:33.613Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c4/d3e9e414dd7ba0124ef07d33d9492cc01db1b76ae3cec45443ec4d6a7935/pymavlink-2.4.49-cp313-cp313-win_amd64.whl", hash = "sha256:9777a0375ebcda0efda3f4eae6d8d2e5ce6de8e26c2f0ac7be1a016d0d386b82", size = 6242260, upload-time = "2025-08-01T23:32:35.256Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/52616b4fdd076177f1ba22e6ef40782b48e14efb47fce2c3bd4f8496ec23/pymavlink-2.4.49-cp313-cp313-win_arm64.whl", hash = "sha256:712ee4240a9489c6dab6158882c7e1f37516c5951db5841cd408ad7b4c6db0d4", size = 6231575, upload-time = "2025-08-01T23:32:36.845Z" }, +] + +[[package]] +name = "pyppmd" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/d7/803232913cab9163a1a97ecf2236cd7135903c46ac8d49613448d88e8759/pyppmd-1.3.1.tar.gz", hash = "sha256:ced527f08ade4408c1bfc5264e9f97ffac8d221c9d13eca4f35ec1ec0c7b6b2e", size = 1351815, upload-time = "2025-11-27T22:08:44.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/89/696046e53c7aea98bb563aed3f15c3e2fa20c33e3d6c9de3c20992c586cc/pyppmd-1.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3faa58ab2ebe3b13ec23b1904639d687fb727270d2962fd2d239ca00fd6eb865", size = 77796, upload-time = "2025-11-27T22:07:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/a708cdb58e76889bc65201eda12211486548ffe00ecddc5dfbdce6d4d252/pyppmd-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27703f041ee96912a5410fd3ce31c5cde32f9323bd67f72f100bd960ee67bf13", size = 48185, upload-time = "2025-11-27T22:07:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/f299535c18009addb866d89a7044f1086e4eecf50542e57ff5bb15840195/pyppmd-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e773d8353b36f7e7973a43526993fb276b98a97839cb5dc8f4e6465ad873f41a", size = 48496, upload-time = "2025-11-27T22:07:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/4a59b73ea8e305f9192ee26ceb7c3d57e17ccb9bcca0e99ef335db29fcf7/pyppmd-1.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37b1883accf840cb0b711785d353f8548853a1401d381da007c0aec362f3ffac", size = 142620, upload-time = "2025-11-27T22:07:54.411Z" }, + { url = "https://files.pythonhosted.org/packages/88/d7/fe32c2a4f8539365e6292aed25545830a5e718a510cdb4caddd6fd8d8056/pyppmd-1.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bd6d179ad39b6191ca0cbe62fb9592f33f49277b4384ad7bc5eb0e6ca27ebee", size = 144306, upload-time = "2025-11-27T22:07:55.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/e3907cf263b58f4ed4c5315bc1ac91721c85332fd0d73a89e3e2752904ef/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:806cf8d33606e44bf5ff5786c57891f57993f1eef1c763da3c58ea97de3a13c8", size = 139522, upload-time = "2025-11-27T22:07:57.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/22d5f93251e481f23bf741dde7f59cfc3e315f60b32b63f215a2d7bb8944/pyppmd-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1826cbce9a2c944aa08df79310a7e6d4a61fd20636b6dff64a77ea4bc43da30f", size = 142340, upload-time = "2025-11-27T22:07:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/34/88/38d36c1394d5e331db9708dae08863c87c059bd5b6e43b20234be02a0503/pyppmd-1.3.1-cp312-cp312-win32.whl", hash = "sha256:d3ff96671319318d941dd34300d641745048e8a3251b077bddf98652d6ddc513", size = 42102, upload-time = "2025-11-27T22:08:00.124Z" }, + { url = "https://files.pythonhosted.org/packages/ee/29/1398c6f67dfb66367babeed2980caee837d0a488705e3dff7ff159e017e7/pyppmd-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:c8c1ad39e7ebde71bf5a54cf61f489bf4790f1dd0beb70dc2e8f5ad3329d7ca7", size = 46957, upload-time = "2025-11-27T22:08:01.587Z" }, + { url = "https://files.pythonhosted.org/packages/d2/41/ee3193c16472a5a1c5f1965abb8fa87a7ad455c39688e9a0c2d87d6a7027/pyppmd-1.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:391b2bf76d7dc45b343781754d0b734dcbf539b92667986a343f5488c4bf9ca0", size = 45214, upload-time = "2025-11-27T22:08:02.721Z" }, + { url = "https://files.pythonhosted.org/packages/f0/01/7cc3854b0e1304b90d2435e946db5e1c42e103adf840a68400002fc838b4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4b4edb3e9619fd0bc39c1a07eb03e8731db833a93b23134f36c7ef581a94b37a", size = 77800, upload-time = "2025-11-27T22:08:03.856Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/caf6305224b9432ea7373670d101392f620c1ce741d6f95458b1fb9a16a4/pyppmd-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8b5c813e462c91048b88e2adfbcc0c69f2c905f70097001d32066f86f675bd4", size = 48195, upload-time = "2025-11-27T22:08:05.323Z" }, + { url = "https://files.pythonhosted.org/packages/45/82/8c5d3f0f738a3cf5c209d1471efda105a16417c0aaa91a2ad39efb3d4efb/pyppmd-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e8d372d9fac382183e0371cf0c2d736b494b1857a1befe98d563342b1205265b", size = 48482, upload-time = "2025-11-27T22:08:06.441Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/09ab8b4f00473f210284dacf837cae8355ad900f3fd8fd98b2bba12de4fa/pyppmd-1.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b765ae21f7ed2f4ea8f32bfd9e3a4a8d738e73fc8f8dcddec9cbe2c898d60be", size = 142828, upload-time = "2025-11-27T22:08:07.611Z" }, + { url = "https://files.pythonhosted.org/packages/64/fd/673d429df719affa1445611288aab6111c0bc87e51eb4677fe2421a1dd64/pyppmd-1.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd00522ddfcc292304577386b6c217758c0c10e1fb9ce7877ad7d3b7b821a808", size = 144502, upload-time = "2025-11-27T22:08:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0f/64dad213f56eaf23210ac7d3a5dedb19ad8f186f8b8682f65e7d51d9fa4a/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3de62099ff2ca876c2d39bc547bcba6f7b878988663abd782a5bad4edac3bb44", size = 139718, upload-time = "2025-11-27T22:08:10.114Z" }, + { url = "https://files.pythonhosted.org/packages/74/61/bd5dbe8d7374748687d48f0bc44e6b930470e09552b1bbc65ef899ab673e/pyppmd-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:011f845de195d60fe973a635a1f4be981b7d80f357a8acb1b2d83bdf5087c808", size = 142578, upload-time = "2025-11-27T22:08:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/ae/0af00a9b1cd59b6821eb182413033d5ddbe05dfcde7511e3a6b1a5b83f6d/pyppmd-1.3.1-cp313-cp313-win32.whl", hash = "sha256:7d61bd01f25289b6ae54832db4254602fb0c6d105f6e6bf0aee39b803b698b98", size = 42102, upload-time = "2025-11-27T22:08:13.297Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/311d8faca142f39b4d158c94ed7eb237a197d41dd7eb67f53442a7904e04/pyppmd-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:df8d84ab72381058a964ba66e5e81ed52dbd0b5ad734a5ef8353452983506098", size = 46962, upload-time = "2025-11-27T22:08:14.759Z" }, + { url = "https://files.pythonhosted.org/packages/bb/42/24b59b6bb73a3fec96c7f81521d146c2ac89ffcb89cf9e848f62e0660381/pyppmd-1.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:124a04aab6936ba011f9ad57067798c7f052fdb1848b0cc4318606eea55475e6", size = 45209, upload-time = "2025-11-27T22:08:15.933Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/ac2a011af1c7c40b6482e60d85d267ac5923fb8794b51bfddbb56b04d1ec/pyppmd-1.3.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ea53f71ac16e113599b8441a9d8b6dcd71cfdf15cdb33ba5151810b8e656c5ec", size = 77897, upload-time = "2025-11-27T22:08:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/20/13/737f4dac06685865d23f51b7061dbe9373c7bcc60b5b6456cd08301bc807/pyppmd-1.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:985c8703b53e5f68fe17f653e96748d60b1f855676c852a6e67cd472eb853671", size = 48268, upload-time = "2025-11-27T22:08:18.263Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d3/c50ebaee8b8a064fe9a534e98df5051e309efb705728aadff4e6893cd87f/pyppmd-1.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:33daa996ad5203c665c0b55aff6329817b2cb7fa95f2c33a2e83ed0121b400cb", size = 48521, upload-time = "2025-11-27T22:08:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/80/6c/4a97c0a0ab7640aeddde4843cb6381b80ecb71fe2c6c48b9032d60586b11/pyppmd-1.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b49870c6d7194f6eb80f30335ca03596d153e02fcde2c222e4f1202ac25f7fcf", size = 142892, upload-time = "2025-11-27T22:08:20.972Z" }, + { url = "https://files.pythonhosted.org/packages/8f/22/744c32c7da3de171d9e62a1b2cb4104544ac778358565a3dbc06a2253324/pyppmd-1.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:385e92c97c42e8a6f0bfc0e4acfc6c074cb1ba3a2f650f292696dd9f19e2e603", size = 144550, upload-time = "2025-11-27T22:08:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/18/84af6808e0754923062122d3ee9f0532050f1612069d558bb5d53978e67a/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:017a1e2903f1c3147a1046db486990d401e8a25eb52c320b1fc2fb3e7b83cbeb", size = 139850, upload-time = "2025-11-27T22:08:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/20/5e/dc6166ea7929d442625301289d99313a5b358e3cb2e927a6bd913047e8ee/pyppmd-1.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2770a4b777c0c5236b3d9294b7bf4bc15538c95d45b2079eb8ebc1298e62e37", size = 142589, upload-time = "2025-11-27T22:08:25.228Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/adc6b12ddf11173a02fd631010f88ad4fe4c532363959638d5d53583a3d9/pyppmd-1.3.1-cp314-cp314-win32.whl", hash = "sha256:b9d54cd59ce97f2ba57be1da91b3d874d129faca21c9565d7afec111f942e6a1", size = 42761, upload-time = "2025-11-27T22:08:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/5003a413d9f2743298a070df6713a75dedccc470e7adeeced5b43cad7418/pyppmd-1.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0e64247618cb150d2909beb0137da3084fef1d3479b4cc73b5b47fda7611abf9", size = 47806, upload-time = "2025-11-27T22:08:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/07/b7/db927e1df7fc3132cb57960f4ea23bf174c7e86ccec22857e6a35cccdae7/pyppmd-1.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:d354d6e551d2630b0ac98f27e3ad63e86cdcac9ce2115b5dfe46e2c9d3f4e82c", size = 46122, upload-time = "2025-11-27T22:08:29.191Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/3150227624f374b06e331a7f67ae3383e796dd90973ee17ec3e35d30524c/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2d77ed79662c32e2551748d59763cfe3dcd10855bf3495937e3d5e5917507818", size = 78876, upload-time = "2025-11-27T22:08:30.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/11a3bba62d1d92050f4dd86db810062b506ec76d20b6e00b4ab567ff15e4/pyppmd-1.3.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f92b94635c23d85270bb26db25cc0db544e436af86efc1cf58302d71d5af1", size = 48830, upload-time = "2025-11-27T22:08:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/e8f50cf89c689543d5a16c7be3df3c4afd43cbeb1a019b84ff9e82f13415/pyppmd-1.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:610f214f2405e27eb5a3dad7fa15f385cfc42141a01cda71995d9c1e0b09fab9", size = 48956, upload-time = "2025-11-27T22:08:33.567Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0f/944b30679a8dea3ea95a66531ee30cb6feb5d011ec7fd6832069d9130bf3/pyppmd-1.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae81a14895498d9a23429d92114c98da478b74b8e33251527d7cff3e01c09de0", size = 152872, upload-time = "2025-11-27T22:08:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/ae/87/1e48ea92994f4c72dc9b5520a6a386b21c524d3d2969c2c2a5c325929ad7/pyppmd-1.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1683e6d1ba09e377e0ae02de3a518191a3d63ccdb0b6037c74e6ddf577b5644", size = 154210, upload-time = "2025-11-27T22:08:36.368Z" }, + { url = "https://files.pythonhosted.org/packages/3f/21/650911f98c4cb360442724bbdade27cb3679b0587ea77e73512693d2918d/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e4d74fa0f3531e9dadc56e0ace41bce82d3c0babed47b3f224101dc0dbde7287", size = 147636, upload-time = "2025-11-27T22:08:37.657Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/4f8cfc1dd67b04ced8c10669fa14c4e35a42cf4a84e1101793735a82d5f0/pyppmd-1.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f8d6375b18b9c79127fee0885cfd52e2e983edb67041464309426571d38dcd4", size = 150998, upload-time = "2025-11-27T22:08:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/c4/da/dc9e5e10bc56056bd8c33e533517cb327752cbaf172688bba3c23f6b2318/pyppmd-1.3.1-cp314-cp314t-win32.whl", hash = "sha256:de87f7acd575fb07a4ff42d41bcc071570fe759a36f345f1f54f574ecfccfc5b", size = 43016, upload-time = "2025-11-27T22:08:40.234Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a0/46dc15549ad2c0427a9825730e6bdf342045ad410543368afd4389a16f36/pyppmd-1.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:76e4800aa67292b4cc80058fd29b39e02a5dded721af9fe5654f356ef24307f4", size = 48636, upload-time = "2025-11-27T22:08:41.45Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7b/7e9a83848de928c26f0ab3ee105c0da63a932a0804a94807205e6313e73a/pyppmd-1.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:e066cbf1d335fe20480cd8ecc10848ba78d99fe6d1e44ea00def48feaf46afdf", size = 46402, upload-time = "2025-11-27T22:08:42.804Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "qgc-tools" +version = "0.0.0" +source = { virtual = "." } + +[package.optional-dependencies] +ci = [ + { name = "meson" }, + { name = "ninja" }, + { name = "pre-commit" }, +] +coverage = [ + { name = "gcovr" }, +] +dev = [ + { name = "jinja2" }, + { name = "pymavlink" }, + { name = "pyyaml" }, +] +lsp = [ + { name = "lsprotocol" }, + { name = "pygls" }, +] +precommit = [ + { name = "pre-commit" }, +] +qt = [ + { name = "aqtinstall" }, +] +scripts = [ + { name = "defusedxml" }, + { name = "httpx" }, + { name = "jinja2" }, +] +test = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "aqtinstall", marker = "extra == 'qt'" }, + { name = "defusedxml", marker = "extra == 'scripts'", specifier = ">=0.7.1" }, + { name = "gcovr", marker = "extra == 'coverage'" }, + { name = "httpx", marker = "extra == 'scripts'", specifier = ">=0.28" }, + { name = "jinja2", marker = "extra == 'dev'" }, + { name = "jinja2", marker = "extra == 'scripts'", specifier = ">=3.1" }, + { name = "jinja2", marker = "extra == 'test'" }, + { name = "lsprotocol", marker = "extra == 'lsp'" }, + { name = "meson", marker = "extra == 'ci'" }, + { name = "ninja", marker = "extra == 'ci'" }, + { name = "pre-commit", marker = "extra == 'ci'" }, + { name = "pre-commit", marker = "extra == 'precommit'" }, + { name = "pygls", marker = "extra == 'lsp'" }, + { name = "pymavlink", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'test'" }, + { name = "pyyaml", marker = "extra == 'dev'" }, + { name = "pyyaml", marker = "extra == 'test'" }, +] +provides-extras = ["scripts", "precommit", "test", "ci", "qt", "coverage", "dev", "lsp"] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "texttable" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/dc/0aff23d6036a4d3bf4f1d8c8204c5c79c4437e25e0ae94ffe4bbb55ee3c2/texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638", size = 12831, upload-time = "2023-10-03T09:48:12.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917", size = 10768, upload-time = "2023-10-03T09:48:10.434Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, +]