diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index f220c1f..6dd3f22 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -2,18 +2,41 @@ name: Desktop binaries on: push: - tags: ['**'] + tags: ['v*'] workflow_dispatch: + inputs: + packages: + description: Packages to build (manual runs do not publish a release) + type: choice + default: all + options: [all, adapter, gcs, qgroundcontrol, mission-planner] + gcs-target: + description: QGroundControl platform (manual builds only) + type: choice + default: all + options: [all, linux-amd64, windows-amd64, macos-universal] permissions: contents: read concurrency: - group: desktop-${{ github.ref }} + group: desktop-${{ github.ref }}-${{ inputs.packages || 'all' }}-${{ inputs.gcs-target || 'all' }} cancel-in-progress: true jobs: + gcs: + name: GCS packages + if: inputs.packages != 'adapter' + uses: ./.github/workflows/gcs.yml + with: + component: ${{ (inputs.packages == 'qgroundcontrol' || inputs.packages == 'mission-planner') && inputs.packages || 'all' }} + target: ${{ inputs.gcs-target || 'all' }} + permissions: + contents: read + actions: read + build: + if: inputs.packages == '' || inputs.packages == 'all' || inputs.packages == 'adapter' name: ${{ matrix.target }} strategy: fail-fast: false @@ -57,6 +80,8 @@ jobs: run: bash build/ci/install-linux-deps.sh - name: Install pinned Wails CLI run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.34 + - name: Validate version metadata + run: node scripts/check-version.mjs - name: Backend tests and vet run: | go test -race ./internal/... @@ -80,6 +105,22 @@ jobs: run: | wails3 build GOARCH=amd64 tar -czf bin/utm-adapter-linux-amd64.tar.gz -C bin utm-adapter + - name: Package Linux AppImage + if: runner.os == 'Linux' + run: | + wails3 task linux:create:appimage + image=$(find bin -maxdepth 1 -name '*.AppImage' -print -quit) + test -n "$image" + mv "$image" bin/utm-adapter-linux-amd64.AppImage + - name: Package Windows installer with WebView2 bootstrapper + if: runner.os == 'Windows' + run: | + choco install nsis --yes --no-progress + export PATH="/c/Program Files (x86)/NSIS:$PATH" + wails3 task windows:create:nsis:installer INSTALL_SCOPE=user + installer=$(find bin -maxdepth 1 -name '*-installer.exe' -print -quit) + test -n "$installer" + mv "$installer" bin/utm-adapter-windows-amd64-installer.exe - name: Check Svelte and TypeScript run: npm --prefix frontend run check - name: Generate download checksum @@ -87,10 +128,11 @@ jobs: TARGET: ${{ matrix.target }} run: | node --input-type=module <<'JS' - import { readFileSync, writeFileSync } from 'node:fs'; + import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; import { createHash } from 'node:crypto'; - const name = `utm-adapter-${process.env.TARGET}.${process.env.TARGET.startsWith('linux') ? 'tar.gz' : 'zip'}`; - writeFileSync(`bin/${name}.sha256`, `${createHash('sha256').update(readFileSync(`bin/${name}`)).digest('hex')} ${name}\n`); + for (const name of readdirSync('bin').filter(n => n.startsWith('utm-adapter-') && /\.(zip|tar\.gz|exe|AppImage)$/.test(n))) { + writeFileSync(`bin/${name}.sha256`, `${createHash('sha256').update(readFileSync(`bin/${name}`)).digest('hex')} ${name}\n`); + } JS - uses: actions/upload-artifact@v4 with: @@ -98,6 +140,39 @@ jobs: path: | bin/utm-adapter-${{ matrix.target }}.zip bin/utm-adapter-${{ matrix.target }}.tar.gz - bin/utm-adapter-${{ matrix.target }}.*.sha256 + bin/utm-adapter-${{ matrix.target }}.AppImage + bin/utm-adapter-${{ matrix.target }}-installer.exe + bin/utm-adapter-${{ matrix.target }}*.sha256 if-no-files-found: error retention-days: 30 + + release: + name: Verify release downloads + if: (inputs.packages == '' || inputs.packages == 'all') && (inputs.gcs-target == '' || inputs.gcs-target == 'all') + needs: [build, gcs] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: utm-adapter-* + merge-multiple: true + path: release + - name: Verify checksums and assemble integration sources + run: | + cd release + sha256sum --check ./*.sha256 + cp ../COMPATIBILITY.md release-notes.md + tar -czf utm-adapter-gcs-integrations.tar.gz -C .. integrations scripts/prepare-qgc.py build/gcs-versions.json + sha256sum utm-adapter-gcs-integrations.tar.gz > utm-adapter-gcs-integrations.tar.gz.sha256 + - name: Publish verified downloads + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + cd release + gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --draft --title "UTM Adapter $RELEASE_TAG" --notes-file release-notes.md ./*.zip ./*.tar.gz ./*.exe ./*.AppImage ./*.dmg ./*.build.json ./*.sha256 + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false diff --git a/.github/workflows/gcs.yml b/.github/workflows/gcs.yml new file mode 100644 index 0000000..defaa94 --- /dev/null +++ b/.github/workflows/gcs.yml @@ -0,0 +1,235 @@ +name: GCS builds + +on: + workflow_call: + inputs: + component: + type: string + default: all + target: + type: string + default: all + workflow_dispatch: + inputs: + component: + description: GCS packages to build + type: choice + default: all + options: [all, qgroundcontrol, mission-planner] + target: + description: QGroundControl platform + type: choice + default: all + options: [all, linux-amd64, windows-amd64, macos-universal] + pull_request: + paths: + - '.github/workflows/gcs.yml' + - '.github/workflows/desktop.yml' + - 'build/gcs-versions.json' + - 'cmd/mission-planner-plugin/**' + - 'go.mod' + - 'go.sum' + - 'frontend/package.json' + - 'integrations/**' + - 'internal/adapter/**' + - 'scripts/*gcs*' + - 'scripts/*qgc*' + - 'scripts/*mission-planner*' + - 'scripts/*mission_planner*' + +permissions: + contents: read + actions: read + +jobs: + pins: + if: inputs.component != 'mission-planner' + runs-on: ubuntu-24.04 + outputs: + qgc-ref: ${{ steps.pins.outputs.qgc-ref }} + qgc-repository: ${{ steps.pins.outputs.qgc-repository }} + qgc-matrix: ${{ steps.pins.outputs.qgc-matrix }} + steps: + - uses: actions/checkout@v4 + - id: pins + env: + QGC_TARGET: ${{ inputs.target || 'all' }} + run: | + echo "qgc-ref=$(jq -r .qgroundcontrol.commit build/gcs-versions.json)" >> "$GITHUB_OUTPUT" + echo "qgc-repository=$(jq -r .qgroundcontrol.repository build/gcs-versions.json)" >> "$GITHUB_OUTPUT" + matrix=$(jq -cn --arg target "$QGC_TARGET" '[ + {runner:"ubuntu-24.04",host:"linux",arch:"linux_gcc_64",target:"linux-amd64"}, + {runner:"windows-2022",host:"windows",arch:"win64_msvc2022_64",target:"windows-amd64"}, + {runner:"macos-15",host:"mac",arch:"clang_64",target:"macos-universal"} + ] | map(select($target == "all" or .target == $target)) | {include: .}') + test "$(echo "$matrix" | jq '.include | length')" -gt 0 + echo "qgc-matrix=$matrix" >> "$GITHUB_OUTPUT" + + qgroundcontrol: + name: QGroundControl (${{ matrix.target }}) + needs: pins + if: inputs.component != 'mission-planner' + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.pins.outputs.qgc-matrix) }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 180 + defaults: + run: + shell: bash + steps: + # Upstream composite actions expect QGC at GITHUB_WORKSPACE, including nested actions. + - uses: actions/checkout@v4 + with: + repository: ${{ needs.pins.outputs.qgc-repository }} + ref: ${{ needs.pins.outputs.qgc-ref }} + submodules: recursive + fetch-depth: 0 + persist-credentials: false + - uses: actions/checkout@v4 + with: + path: adapter + persist-credentials: false + - name: Verify pinned upstream and prepare minimal overlay + env: + QGC_REF: ${{ needs.pins.outputs.qgc-ref }} + run: | + test "$(git rev-parse HEAD)" = "$QGC_REF" + python3 adapter/scripts/prepare-qgc.py . + python3 -m unittest discover -s adapter/scripts -p 'test_*.py' + - name: Set up upstream Qt, compiler and build tools + uses: ./.github/actions/build-setup + with: + qt-host: ${{ matrix.host }} + qt-arch: ${{ matrix.arch }} + build-type: Release + cache-variant: openutm + aqt-source: git+https://github.com/miurahr/aqtinstall.git@8d961e61720b3c06583517fbc68d57ec0a4e9f95 + - uses: ./.github/actions/install-dependencies + with: + nsis: ${{ runner.os == 'Windows' && 'true' || 'false' }} + # Dependency setup may create a venv containing only CI tools. CMake + # reuses it, so install generator dependencies explicitly before configure. + - name: Install upstream source generator dependencies + run: | + python3 tools/setup/install_python.py scripts,ci + if [[ "$RUNNER_OS" == "Windows" ]]; then + venv_python=.venv/Scripts/python.exe + else + venv_python=.venv/bin/python + fi + "$venv_python" -c 'import defusedxml, httpx, jinja2, ninja' + "$venv_python" -m pip --version + ninja --version + - name: Test native restriction map + env: + QSG_RHI_BACKEND: software + run: | + qmllint adapter/integrations/qgroundcontrol/AdapterRestrictions.qml + qmltestrunner -platform offscreen -input adapter/integrations/qgroundcontrol/tests + - uses: ./.github/actions/cmake-configure + # The pinned upstream's automatic check ignores Python version markers, + # then syncs only scripts and removes Ninja. We install and check both + # dependency groups above, so keep that environment intact in CMake. + with: + build-dir: ${{ runner.temp }}/build + stable: 'true' + extra-args: >- + -DQGC_CUSTOM_DIR=custom-openutm-adapter + -DQGC_BUILD_TESTING=OFF + -DQGC_MACOS_SIGN_WITH_IDENTITY=OFF + -DQGC_AUTO_PYTHON_VENV=OFF + - uses: ./.github/actions/cmake-build + with: + build-dir: ${{ runner.temp }}/build + parallel-jobs: '3' + - uses: ./.github/actions/cmake-install + with: + build-dir: ${{ runner.temp }}/build + - name: Install Windows package for smoke test + if: runner.os == 'Windows' + shell: pwsh + run: | + $installers = @(Get-ChildItem "$env:RUNNER_TEMP/build" -Filter '*installer*.exe') + if ($installers.Count -ne 1) { throw 'Expected exactly one QGC installer' } + # NSIS /D requires a native path; forward slashes can be discarded. + $destination = Join-Path $env:RUNNER_TEMP 'qgc-install-test' + $process = Start-Process $installers[0].FullName -ArgumentList '/S', "/D=$destination" -Wait -PassThru + if ($process.ExitCode -ne 0) { throw "Installer failed: $($process.ExitCode)" } + if (!(Test-Path (Join-Path $destination 'bin\QGroundControl.exe'))) { throw "Installed QGC executable missing from $destination" } + - name: Mount macOS package for smoke test + if: runner.os == 'macOS' + run: hdiutil attach "$RUNNER_TEMP/build/QGroundControl.dmg" -mountpoint "$RUNNER_TEMP/qgc-dmg" -nobrowse + - name: Boot packaged QGC and verify custom restriction feed + env: + QGC_BINARY: ${{ runner.os == 'macOS' && format('{0}/qgc-dmg/QGroundControl.app/Contents/MacOS/QGroundControl', runner.temp) || runner.os == 'Windows' && format('{0}/qgc-install-test/bin/QGroundControl.exe', runner.temp) || format('{0}/build/QGroundControl-x86_64.AppImage', runner.temp) }} + QGC_ARCHS: ${{ runner.os == 'macOS' && 'x86_64h arm64' || '' }} + run: python3 adapter/scripts/test_qgc_package.py --qgc-source . --binary "$QGC_BINARY" --expect-archs "$QGC_ARCHS" + - name: Stage packages, provenance and checksums + env: + TARGET: ${{ matrix.target }} + ADAPTER_COMMIT: ${{ github.sha }} + run: | + case "$RUNNER_OS" in + macOS) package="$RUNNER_TEMP/build/QGroundControl.dmg" ;; + Linux) package="$RUNNER_TEMP/build/QGroundControl-x86_64.AppImage" ;; + Windows) package=$(find "$RUNNER_TEMP/build" -maxdepth 1 -iname '*installer*.exe' -print -quit) ;; + esac + python3 adapter/scripts/package-gcs.py --component qgroundcontrol --target "$TARGET" --input "$package" --output adapter/bin/gcs --adapter-commit "$ADAPTER_COMMIT" + - name: Unmount macOS package + if: always() && runner.os == 'macOS' + run: hdiutil detach "$RUNNER_TEMP/qgc-dmg" || true + - name: Verify portable download checksums + run: cd adapter/bin/gcs && sha256sum --check ./*.sha256 + - uses: actions/upload-artifact@v4 + with: + name: utm-adapter-gcs-qgroundcontrol-${{ matrix.target }} + path: adapter/bin/gcs/* + if-no-files-found: error + retention-days: 30 + + mission-planner: + name: Mission Planner plugin (Windows x64) + if: inputs.component != 'qgroundcontrol' + runs-on: windows-2025 + timeout-minutes: 30 + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Test package generation + run: python -m unittest discover -s scripts -p 'test_*.py' + - name: Download and verify pinned Mission Planner release + run: | + $pin = (Get-Content build/gcs-versions.json -Raw | ConvertFrom-Json).missionPlanner + $archive = "$env:RUNNER_TEMP/MissionPlanner.zip" + Invoke-WebRequest $pin.url -OutFile $archive + if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLower() -ne $pin.sha256) { throw 'Mission Planner checksum mismatch' } + Expand-Archive $archive -DestinationPath "$env:RUNNER_TEMP/MissionPlanner" + - name: Build release plugin using production generator + run: ./scripts/build-mission-planner.ps1 -MissionPlannerDirectory "$env:RUNNER_TEMP/MissionPlanner" -OutputDirectory bin/mission-planner + - name: Package plugin and source with provenance + env: + ADAPTER_COMMIT: ${{ github.sha }} + run: | + python scripts/package-gcs.py --component missionPlanner --target windows-amd64 --input bin/mission-planner --output bin/gcs --adapter-commit "$env:ADAPTER_COMMIT" + if ($LASTEXITCODE -ne 0) { throw 'Plugin packaging failed' } + - name: Load and initialize the packaged DLL against Mission Planner + run: | + $archive = @(Get-ChildItem bin/gcs -Filter '*.zip') + if ($archive.Count -ne 1) { throw 'Expected exactly one plugin archive' } + powershell.exe -NoProfile -File scripts/test_mission_planner_package.ps1 -Archive $archive[0].FullName -MissionPlannerDirectory "$env:RUNNER_TEMP/MissionPlanner" + if ($LASTEXITCODE -ne 0) { throw 'Packaged plugin integration test failed' } + - name: Verify portable download checksums + shell: bash + run: cd bin/gcs && sha256sum --check ./*.sha256 + - uses: actions/upload-artifact@v4 + with: + name: utm-adapter-gcs-mission-planner-windows-amd64 + path: bin/gcs/* + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 76b14cc..596b8bc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,6 +5,15 @@ on: push: branches: [main] workflow_dispatch: + inputs: + flight-blender-ref: + description: 'Optional Flight Blender branch/ref containing the companion adapter API' + type: string + default: '' + dji-android-ref: + description: 'Optional DJI Android branch/ref containing the shared archive client (requires Flight Blender ref)' + type: string + default: '' permissions: contents: read @@ -32,11 +41,85 @@ jobs: node-version: '24' cache: npm cache-dependency-path: frontend/package-lock.json + - name: Verify release version + run: node scripts/check-version.mjs - name: Backend tests and vet run: | go test -race ./internal/... go vet ./internal/... + - name: Compile generated Mission Planner plugin against release 1.3.83 + if: runner.os == 'Windows' + shell: pwsh + run: | + $archive = Join-Path $env:RUNNER_TEMP 'MissionPlanner.zip' + Invoke-WebRequest 'https://github.com/ArduPilot/MissionPlanner/releases/download/MissionPlanner1.3.83/MissionPlanner-1.3.83.zip' -OutFile $archive + if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLower() -ne 'ff3694f7a9038caf756545049dd7aef74e3d0d8fe3586e87aa881045704dd6f9') { throw 'Mission Planner checksum mismatch' } + $env:UTM_TEST_MP_DIRECTORY = Join-Path $env:RUNNER_TEMP 'MissionPlanner' + Expand-Archive $archive -DestinationPath $env:UTM_TEST_MP_DIRECTORY + $env:UTM_TEST_MP_CSC = Join-Path $env:WINDIR 'Microsoft.NET/Framework64/v4.0.30319/csc.exe' + $framework = Get-ChildItem "${env:ProgramFiles(x86)}/Reference Assemblies/Microsoft/Framework/.NETFramework" -Directory | Where-Object { Test-Path (Join-Path $_.FullName 'Facades/netstandard.dll') } | Sort-Object Name -Descending | Select-Object -First 1 + if (-not $framework) { throw '.NET Framework reference assemblies with netstandard facade are required' } + $env:UTM_TEST_MP_FRAMEWORK = $framework.FullName + go test ./internal/adapter -run '^TestMissionPlannerPluginCompilation$' -count=1 -v + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - name: Install locked frontend dependencies run: npm --prefix frontend ci - name: Check Svelte and TypeScript run: npm --prefix frontend run check + + native-map: + name: Native QGC restriction rendering + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: jurplel/install-qt-action@v4 + with: + version: '6.8.3' + modules: 'qtlocation qtpositioning' + cache: true + - name: Verify map rendering and custom-build wiring + env: + QSG_RHI_BACKEND: software + run: | + qmllint integrations/qgroundcontrol/AdapterRestrictions.qml + qmltestrunner -platform offscreen -input integrations/qgroundcontrol/tests + python3 -m unittest discover -s scripts -p 'test_*.py' + + flight-blender-contract: + name: Real Flight Blender session contract + if: github.event_name == 'workflow_dispatch' && inputs.flight-blender-ref != '' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + path: adapter + - uses: actions/checkout@v4 + with: + repository: openutm/flight-blender + ref: ${{ inputs.flight-blender-ref }} + path: flight-blender + - uses: actions/checkout@v4 + if: inputs.dji-android-ref != '' + with: + repository: openutm-labs/dji-android-gcs + ref: ${{ inputs.dji-android-ref }} + path: dji-android-gcs + - uses: actions/setup-java@v4 + if: inputs.dji-android-ref != '' + with: + distribution: temurin + java-version: '17' + - uses: gradle/actions/setup-gradle@v4 + if: inputs.dji-android-ref != '' + - uses: actions/setup-go@v5 + with: + go-version-file: adapter/go.mod + - uses: astral-sh/setup-uv@v6 + - name: Install server dependencies and test actual application + env: + DJI_ANDROID_ROOT: ${{ inputs.dji-android-ref != '' && format('{0}/dji-android-gcs', github.workspace) || '' }} + run: | + cd flight-blender + uv sync --locked + cd ../adapter + env -u ANDROID_HOME -u ANDROID_SDK_ROOT scripts/test-flight-blender.sh diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md new file mode 100644 index 0000000..80e45de --- /dev/null +++ b/COMPATIBILITY.md @@ -0,0 +1,26 @@ +# UTM Adapter 0.2.0 compatibility + +This release implements the adapter workflow in [Flight Blender #218](https://github.com/openutm/flight-blender/issues/218). Planning and declaration creation remain in Flight Spotlight / Flight Blender. + +| Component | Integration | Verification | +| --- | --- | --- | +| Flight Blender | Existing telemetry/declaration/geofence APIs plus `/adapter_ops/sessions/{UUID}` | Go client tested against the actual FastAPI/SQLAlchemy application, including real UDP reception, restart, upload acknowledgement, duplicate uploads and exported-report readback. Deploy the companion `a218_session_records` migration before enabling archive synchronization. | +| QGroundControl 5.1.4 | MAVLink forwarding; custom FlyView layer for restriction polygons | CI builds a pinned upstream commit for macOS universal, Windows x64 and Linux x64, tests native restriction rendering, and boots the packaged application before upload. The minimal custom plugin preserves upstream firmware support and UI. Flight trials remain required. | +| Mission Planner 1.3.83 | Generated C# plugin: all connection telemetry and Flight Data restriction overlays | CI compiles the production generator's source against the checksum-pinned official release, then extracts the release ZIP and loads, initializes and unloads the DLL using .NET Framework. Connected-vehicle and Flight Data UI trials remain required. | +| Aerobridge | Optional bearer-authenticated aircraft/pilot/operation registry and flight-log API | Fixture tests cover stable batch keys and recovery after a stored upload loses its response. An isolated actual Aerobridge application also passed registry, upload and duplicate-retry checks with upstream authentication enforcement disabled. Production authentication/deployment remains unverified. | +| MAVLink 1/2 | Autopilot component 1; distinct system IDs for concurrent aircraft | UDP/TCP and two-aircraft session tests; macOS virtual serial; SITL through MAVProxy to the adapter's parser configuration produced GPS/global-position frames with zero parse errors over 60 seconds. This is not a full SITL-to-server flight test. Live delivery requires fresh MAVLink 2 ellipsoid altitude. Physical autopilot/serial hardware remains unverified. | +| macOS | Wails desktop application | Local native build and UI start/end/export smoke test; CI also builds the universal application. Ad-hoc signing, without notarization. | +| Windows x64 | Portable executable and NSIS installer | Desktop builds/packages passed prior CI. Interactive installer, tray, credential-store and vehicle/GCS acceptance trials remain pending. | +| Linux x64 | Ubuntu 24.04 archive and AppImage | Desktop builds/packages passed prior CI. The archive executable booted and served restrictions in an emulated Ubuntu 24.04 desktop container. Local adapter AppImage launch was inconclusive under emulation; native desktop acceptance remains pending. GTK4, WebKitGTK 6 and a credential-store service are required. | + +Local validation recorded on 2026-10-25 used isolated services and synthetic telemetry on an Apple Silicon Mac. Flight Blender's real-application contract used its development JWT-signature bypass; separate server tests covered RSA verification, scopes, owner isolation and migration upgrade/downgrade. macOS UI validation covered recording/export, restart persistence and Keychain save/remove. The SITL/parser follow-up used MAVProxy to separate raw console startup text from telemetry; direct console-bearing UDP is not a clean MAVLink source. + +Prior desktop/GCS CI runs passed all platform builds and package checks, including native QGC boot/restriction-fetch and Windows Mission Planner DLL initialization. These results are evidence for those tested revisions, not proof that every later documentation or code revision has run on CI. Release publication was skipped in those validation runs. Production OAuth/identity-provider flows, deployed services, physical hardware, interactive Windows use, native Linux adapter AppImage acceptance and actual release publication remain separate checks. Use the [developer verification matrix](DEVELOPING.md#verification-matrix) to reproduce coverage. + +GCS downloads use the prefix `utm-adapter-gcs-`: QGroundControl ships as a macOS DMG, Windows installer and Linux AppImage; Mission Planner ships as a Windows plugin ZIP with installation instructions and inspection source. Mission Planner itself is not redistributed. Each artifact includes a checksum and a manifest recording the adapter version/commit and exact upstream build inputs in `build/gcs-versions.json`. These QGC builds retain the standard application/settings name, so install them in place of stock QGC. + +The integration sources also ship as `utm-adapter-gcs-integrations.tar.gz`. Prepare a new QGC custom directory with `python3 scripts/prepare-qgc.py /path/to/qgroundcontrol`, then build QGC using its supported custom-build process. For custom Mission Planner endpoints, use the adapter's Ground stations screen instead of installing the release DLL. Install only one copy of the plugin (source or DLL). Both map integrations use `http://127.0.0.1:14552/v1/restrictions` by default; adjust the QML endpoint if the local bridge address changes. + +Archives and installers include SHA-256 checksum files. Pushing a matching semantic-version tag, such as `v0.2.0`, triggers desktop and GCS builds and publishes a GitHub Release only after all builds and package checks pass. Uploads go to a draft before publication. The Desktop binaries workflow can be dispatched manually to build all artifacts without creating a release; GCS builds can also be dispatched independently. Releases are not automatically installed. macOS packages use ad-hoc signing without notarization. + +Local records contain pilot contact details and flight positions. They are stored alongside `product.json` in `records.sqlite`; exports go in `exports/`. Protect and back up this directory. Start/end audit events and declaration history provide operational records, without cryptographic certification or tamper-proof guarantees. diff --git a/DEVELOPING.md b/DEVELOPING.md index b609fc3..00fce8d 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -1,8 +1,8 @@ # Developing UTM Adapter -This guide covers building, testing, and changing UTM Adapter. For installation and everyday use, see the [user guide](README.md). +This guide covers building, testing, and changing UTM Adapter. For installation and everyday use, see the [user guide](README.md). Read the [architecture and integration contracts](docs/ARCHITECTURE.md) for system diagrams, concurrency, SQLite schema, retry semantics, API contracts and extension rules. The [configuration reference](docs/CONFIGURATION.md) documents all settings. -The desktop preview implements the initial MAVLink-to-Flight Blender connection work described in [flight-blender#218](https://github.com/openutm/flight-blender/issues/218). It uses Go, Wails v3, and Svelte 5. A separate Python prototype is outside this repository. +The desktop implements the session, telemetry and GCS integration work described in [flight-blender#218](https://github.com/openutm/flight-blender/issues/218). It uses Go, Wails v3, and Svelte 5. A separate Python prototype is outside this repository. ## Prerequisites @@ -55,7 +55,7 @@ The build tasks install locked frontend dependencies, generate TypeScript bindin macOS packaging ad-hoc signs and verifies the bundle. This is not Developer ID signing or notarization. Platform-specific tasks live under `build/darwin/`, `build/windows/`, and `build/linux/`; the root [Taskfile.yml](Taskfile.yml) dispatches to them. Docker and server-mode task scaffolding also exists, but the supported user workflow documented here is the desktop app. -The Go entry point embeds `frontend/dist`, so a raw `go build` from a fresh checkout is insufficient. Use the Wails build tasks to prepare the frontend first. +The Go entry point embeds `frontend/dist`, so a raw `go build` from a fresh checkout is insufficient. Use the Wails build tasks to prepare the frontend first. Finish frontend production builds before compiling or testing packages that embed it: Vite replaces the asset directory during its build, so concurrent Go compilation can fail with missing embedded files. ## Isolate local settings @@ -68,10 +68,10 @@ The Go entry point embeds `frontend/dist`, so a raw `go build` from a fresh chec For example, on macOS or Linux: ```sh -UTM_ADAPTER_CONFIG="$PWD/product.json" wails3 dev +UTM_ADAPTER_CONFIG="${TMPDIR:-/tmp}/utm-adapter-dev/product.json" wails3 dev ``` -The root `product.json` is ignored by Git. Configuration uses an atomic temporary-file replacement and excludes tokens and client secrets. Secrets are stored under credential service `org.openutm.adapter`, keyed by the configuration path. Choosing another path also chooses another credential-store entry. +The root `product.json` is ignored by Git. The example uses a separate temporary directory because `records.sqlite` and `exports/` belong to the configuration directory; different filenames within one directory share the same database. Temporary directories can be removed by the OS; use a persistent private directory when you need to retain test records. Keep local test data out of commits. Configuration uses an atomic temporary-file replacement and excludes tokens and client secrets. Secrets are stored under credential service `org.openutm.adapter`, keyed by the configuration path. Choosing another path also chooses another credential-store entry. A second application launch opens the existing instance, even when a different config path is supplied. Quit the first instance before switching configurations. The application starts connections on launch using its saved configuration; for local testing, leave delivery disabled and select **Direct MAVLink** to avoid automatic ground station changes. @@ -96,6 +96,35 @@ go run ./cmd/telemetry-demo --address 127.0.0.1:14550 Synthetic data can satisfy the delivery guards. Keep delivery off for UI-only tests; use a dedicated test server and declaration when testing publishing. +The telemetry card shows the parse-error count and the latest parser diagnostic. +An invalid magic byte means the input did not begin with a MAVLink header; +ArduPilot SITL can produce this when console startup text shares the telemetry +output. A skipped-byte diagnostic reports unread data from the previous UDP +datagram, which can contain console text or additional frames. A checksum error +reports a rejected MAVLink frame. Valid telemetry continues to be processed after +these errors; reconnecting clears the count and diagnostic. For SITL validation, +keep its console output separate from the MAVLink feed. + +For a built ArduPilot checkout, set `ARDUPILOT_ROOT` to its absolute path and +start SITL from a scratch directory so its parameter storage stays isolated: + +```sh +mkdir -p /tmp/utm-adapter-sitl +cd /tmp/utm-adapter-sitl +"$ARDUPILOT_ROOT/build/sitl/bin/arducopter" --model quad --home 47.5,19.0,120,0 --serial0 tcp:5770 --defaults "$ARDUPILOT_ROOT/Tools/autotest/default_params/copter.parm" +``` + +In another terminal, use MAVProxy's dedicated MAVLink output and explicitly +request telemetry streams. Configure the adapter to listen on UDP port 14551: + +```sh +mavproxy.py --master tcp:127.0.0.1:5770 --out udp:127.0.0.1:14551 --streamrate 5 --mav20 --non-interactive --no-state +``` + +Allow time for simulated GPS initialization. MAVProxy parses the console-facing +connection and forwards MAVLink messages to the adapter. This avoids feeding +SITL's raw console output directly into its UDP listener. + ## Automated checks These commands match the checks in the [Tests workflow](.github/workflows/tests.yml): @@ -127,11 +156,14 @@ Before changing a native integration, also exercise it on the relevant OS. Compi | [internal/adapter/blender.go](internal/adapter/blender.go) | Flight Blender HTTP client, bearer/OAuth authentication, access checks, and telemetry payloads. | | [internal/adapter/discovery.go](internal/adapter/discovery.go) | Ground station process detection and known installation/settings locations. | | [internal/adapter/gcsconfigure.go](internal/adapter/gcsconfigure.go) | QGC forwarding configuration, backups, and generated Mission Planner C# plugin installation/removal. | -| [frontend/src/App.svelte](frontend/src/App.svelte) | Overview, ground station options, connection settings, and service calls. | +| [internal/adapter/records.go](internal/adapter/records.go) | SQLite schema, sample sequences, durable batches, cursors and document history. | +| [internal/adapter/sessions.go](internal/adapter/sessions.go), [sync.go](internal/adapter/sync.go) | Session lifecycle, export, archive orchestration, declaration cache and Aerobridge integration. | +| [internal/adapter/restrictions.go](internal/adapter/restrictions.go) | Restriction conversion/cache and loopback GeoJSON bridge. | +| [frontend/src/App.svelte](frontend/src/App.svelte) | Overview, flight sessions, ground station options, settings, and service calls. | | [cmd/telemetry-demo/main.go](cmd/telemetry-demo/main.go) | Synthetic local UDP sender. | | `build/` and `.github/workflows/` | Platform build tasks, application assets, CI checks, and binary artifacts. | -The Wails service owns a gomavlib node and a background loop. Incoming frames update vehicle state keyed by MAVLink system ID. The frontend polls `GetSnapshot()` once per second; snapshots contain status and credential-presence flags, never secret values. `ApplyConfig()` validates and saves settings and restarts connections. `Start()` and `Stop()` control connections; `ClearCredentials()` pauses them and disables publishing. +The Wails service owns a gomavlib node and a background loop. Incoming frames update vehicle state keyed by MAVLink system ID. The frontend polls `GetSnapshot()` once per second; snapshots contain status and credential-presence flags, never secret values. `ApplyConfig()` validates and saves settings and restarts connections. `Start()` and `Stop()` control connections; `ClearCredentials()` pauses them and disables legacy publishing and Aerobridge; it does not clear the session-delivery flag. Ground station discovery, setup, server health, and API read checks run roughly every ten loop iterations. Publishing is attempted on a one-second ticker. HTTP calls and discovery can delay that cadence, so these are approximate intervals. @@ -142,7 +174,7 @@ Ground station discovery, setup, server health, and API read checks run roughly - `HEARTBEAT` supplies armed state and heartbeat freshness. `GPS_RAW_INT` supplies the delivery position, GPS freshness, fix, speed, course, and MAVLink 2 ellipsoid altitude. `GLOBAL_POSITION_INT` supplies displayed MSL/relative altitude and a position fallback before GPS arrives. - Delivery requires heartbeat and GPS no more than five seconds old, a new GPS sample after the last successful send, and populated position, WGS84 altitude, speed, and course. GPS fix types 3–8 are accepted. Unknown speed/course sentinels and missing or zero ellipsoid altitude block delivery; MSL is never substituted for WGS84 altitude. - Delivery sends one observation with one current state for the configured declaration, vehicle serial, and operator. Operational status is `Undeclared`; unknown accuracy and unavailable vertical-speed/pressure-altitude values use the sentinels in `Blender.Publish`. Changing these mappings requires checking the receiving Flight Blender contract. -- A successful send advances the last-sent sample and accepted count. A failed send can be retried while the sample remains fresh. There is no durable queue or historical replay. Vehicle state and counters reset when connections restart. +- A successful send advances the last-sent sample and accepted count. A failed send can be retried while the sample remains fresh. This legacy live path has no historical replay. Session recording has an independent durable archive queue. Vehicle state and counters reset when connections restart. The HTTP client uses these endpoints relative to the configured base URL: @@ -153,7 +185,7 @@ The HTTP client uses these endpoints relative to the configured base URL: | `GET /flight_declaration_ops/flight_declaration/{UUID}` | Authenticated read access check for the selected declaration. | | `PUT /flight_stream/set_telemetry` | Telemetry delivery; successful read access alone does not prove write permission. | -Requests have a six-second timeout, reject redirects, and limit response reads to 1 MiB. OAuth uses client credentials with HTTP Basic client authentication and caches access tokens until 30 seconds before expiry. A missing expiry defaults to 300 seconds; HTTP 401 clears the cached token for a subsequent retry. +Requests have a six-second timeout, reject redirects, and limit response reads to 8 MiB. OAuth uses client credentials with HTTP Basic client authentication and caches access tokens until 30 seconds before expiry. A missing expiry defaults to 300 seconds; HTTP 401 clears the cached token for a subsequent retry. ## Ground station changes @@ -161,7 +193,7 @@ Automatic setup only configures local UDP forwarding, skips direct mode, and wai QGC configuration updates `forwardMavlink=true` and `forwardMavlinkHostName` under `[General]` in `QGroundControl.ini`. The adjacent `.utm-adapter-target` file records an adapter-managed destination so future updates can preserve unrelated forwarding settings. -Mission Planner plugin installation requires explicit opt-in. The generated `plugins/OpenUTMAdapter.cs` forwards received packets to the adapter and follows the primary connection. Files without the managed-plugin marker are preserved. Removal disables the opt-in and renames the managed plugin to an unloaded backup. +Mission Planner plugin installation requires explicit opt-in. The generated `plugins/OpenUTMAdapter.cs` forwards received packets to the adapter and follows all Mission Planner connections. Files without the managed-plugin marker are preserved. Removal disables the opt-in and renames the managed plugin to an unloaded backup. Changed existing files are backed up with `.utm-adapter-backup-`. Disabling automatic configuration does not undo earlier changes. See the [user guide's rollback instructions](README.md#settings-credentials-and-undoing-setup). @@ -175,14 +207,134 @@ Changed existing files are backed up with `.utm-adapter-backup-`. Dis gh workflow run desktop.yml --ref ``` -GitHub exposes manual dispatch once the workflow is present on the default branch. The binary workflow tests the backend, builds the app, checks the frontend, and uploads macOS universal, Windows amd64, and Linux amd64 archives with SHA-256 checksums. Artifacts are retained for 30 days; they are not published release installers. +GitHub exposes manual dispatch once the workflow is present on the default branch. The binary workflow tests the backend, builds the app, checks the frontend, and uploads macOS universal, Windows amd64, and Linux amd64 archives with SHA-256 checksums. Preview artifacts are retained for 30 days. Matching `vX.Y.Z` tags publish verified archives, Windows NSIS installers, Linux AppImages, integration sources, checksums and the compatibility matrix as a GitHub Release. Linux CI installs native dependencies through [build/ci/install-linux-deps.sh](build/ci/install-linux-deps.sh), using Ubuntu's official HTTPS repositories. Package downloads are cached, indexes refresh each run, and cache keys rotate weekly or when the script changes. ## Validation boundaries -The existing preview notes record successful QGroundControl 5.1.4 automatic setup and forwarding on macOS with synthetic MAVLink, direct UDP, TCP integration tests, an HTTP Flight Blender test server, and checks against Flight Blender's telemetry dataclass parser. These are separate from live operational validation. +Local validation includes actual Flight Blender and Aerobridge application contracts, macOS UI/Keychain/session export, virtual serial, Linux executable startup under emulation, and a clean SITL → MAVProxy → parser GPS stream. See [COMPATIBILITY.md](COMPATIBILITY.md) for scope and remaining gaps. Isolated local services do not establish production authentication or deployment readiness. + +Physical autopilots, serial hardware, interactive Windows/GCS operation, native Linux adapter AppImage launch and production identity-provider behavior still need trials. Windows CI compiles, loads, initializes and unloads the packaged Mission Planner DLL; this does not prove connected-aircraft or map interaction. Record the adapter/server commit, OS, GCS version, transport, authentication mode and test procedure when extending coverage. + +Declaration creation and planning remain external. Publisher signing, auto-updates and login startup remain outside this release. Session archives, optional Aerobridge registry/log integration, map overlays, compiled custom QGC distribution and installer packaging are implemented in 0.2.0. + +## Session workflow and integration verification (0.2.0) + +`records.go` owns the private SQLite database beside `product.json`. Each immutable session assignment binds a MAVLink system to an aircraft, declaration and pilot. The receiver records every incoming GPS sample while that session is active. `sessions.go` handles start/end, streamed JSON export and synchronization. `sync.go` caches declaration versions and integrates the optional Aerobridge registry/log APIs. `restrictions.go` caches geofences and serves a credential-free loopback GeoJSON feed. + +Archive workers persist exact pending payloads before sending, then advance separate Flight Blender / Aerobridge cursors after acknowledgement. Flight Blender stores sequences idempotently. Aerobridge lacks native idempotency keys, so the adapter reconciles `raw_log.adapterBatchKey` before retrying POSTs. This relies on read-after-write visibility and a single adapter owning a session. Active sessions persist through a restart. Ended sessions remain exportable; there is no automatic retention/deletion policy. + +Deploy the companion Flight Blender changes and apply `alembic upgrade head`. Its owner-isolated endpoints require read scope for GET and write scope for PUT. Records are owned by token issuer and subject, so use the same service identity for retries. Then run: + +```sh +# Two real UDP aircraft -> durable sessions -> outage -> restart -> +# authenticated archive/log services -> restriction bridge and JSON export. +go test -race ./internal/adapter -run '^TestFlightSessionIntegration$' -count=1 -v + +# Real UDP -> actual Flight Blender FastAPI + SQLAlchemy -> exported report, isolated +# in a temporary directory. Requires the sibling server's installed Python environment and companion API. +FLIGHT_BLENDER_ROOT=../flight-blender scripts/test-flight-blender.sh + +node scripts/check-version.mjs +``` + +The cross-repository script uses a local JWT-verification bypass only in its isolated server process; Flight Blender's own tests separately enforce real RSA token verification, missing-token failures and wrong-scope failures. It creates schema from ORM metadata; the migration is tested separately in the Flight Blender suite. + +For QGC 5.1.4, prepare a **new** custom directory inside its source checkout: + +```sh +python3 scripts/prepare-qgc.py /path/to/qgroundcontrol +# Configure/build QGC following its native instructions, adding: +# -DQGC_CUSTOM_DIR=custom-openutm-adapter +``` + +This keeps the GCS map native and reads only the adapter's local GeoJSON feed. The preparation script refuses to overwrite an existing directory. To test its native QML component with Qt 6.8+ and Qt Location / Positioning installed: + +```sh +qmllint integrations/qgroundcontrol/AdapterRestrictions.qml +qmltestrunner -platform offscreen -input integrations/qgroundcontrol/tests +python3 -m unittest discover -s scripts -p 'test_*.py' +``` + +Changing the bridge address requires updating the QML `endpoint` or regenerating the installed Mission Planner plugin through the adapter. The generated Mission Planner source contains its forwarding and map integration; connected-vehicle and interactive Windows map validation remains required. See [COMPATIBILITY.md](COMPATIBILITY.md) for exact coverage. + +The Tests workflow accepts an optional `flight-blender-ref` on manual dispatch to run the same real-server contract against a branch containing the companion API. Default checks run the two-aircraft UDP/restart integration test on Linux and Windows, plus native Qt map tests on Linux and generated Mission Planner plugin compilation on Windows. + +## GCS build and release CI + +The [GCS workflow](.github/workflows/gcs.yml) builds QGroundControl from the commit +in `build/gcs-versions.json` using that commit's own Qt/dependency/build actions. +It packages and boots a macOS universal DMG, Windows x64 installer and Linux x64 +AppImage. Upstream is checked out at the workspace root because its composite +actions resolve nested actions and scripts relative to that directory; this +adapter is checked out into `adapter/`. + +The packaged-app integration test first runs upstream's boot verification, then +starts the real application event loop with temporary configuration directories +and requires the custom overlay to fetch a restriction polygon from a local HTTP +fixture. This catches missing custom resources in otherwise bootable packages. + +The Windows job downloads the checksum-pinned Mission Planner ZIP, compiles the +same C# source used by the application, creates a plugin ZIP, then loads that +ZIP's DLL and checks its version, initialization and unload using Windows +PowerShell/.NET Framework. This does not replace a connected-aircraft/UI trial. +To reproduce its packaging from a Windows developer shell: + +```powershell +./scripts/build-mission-planner.ps1 -MissionPlannerDirectory C:/MissionPlanner -OutputDirectory bin/mission-planner +python scripts/package-gcs.py --component missionPlanner --target windows-amd64 --input bin/mission-planner --output bin/gcs --adapter-commit (git rev-parse HEAD) +powershell.exe -NoProfile -File scripts/test_mission_planner_package.ps1 -Archive (Get-ChildItem bin/gcs/*.zip).FullName -MissionPlannerDirectory C:/MissionPlanner +``` + +Dispatch **GCS builds** for integration packages alone, or **Desktop binaries** +for the complete release set without publishing. Its `packages` input also accepts +`adapter`, `gcs`, `qgroundcontrol` or `mission-planner` for focused builds. +QGC builds default to every platform; `gcs-target` selects one platform for manual +retests (for example `-f packages=qgroundcontrol -f gcs-target=windows-amd64`). +Full manual runs verify the combined release checksums and assemble integration sources. +Relevant pull requests also run +GCS builds. A matching `v` tag runs both matrices and publishes only +after every job succeeds. All packages and their provenance manifests have +SHA-256 checksums. No signing secrets are required; macOS uses ad-hoc signing. +Update the upstream pins and compatibility matrix together when upgrading a GCS. + +## Verification matrix + +Run the checks appropriate to the changed boundary. Default `go test` runs skip environment-dependent checks when their prerequisites are absent; a green default suite does not imply those checks ran. + +| Boundary | Check | Prerequisites / limits | +| --- | --- | --- | +| Core backend | `go test -race ./internal/...` and `go vet ./internal/...` | No live services/hardware. Covers auth failures, conversion guards, GCS preservation and parser recovery. | +| Durable store | `go test -race ./internal/adapter -run 'TestRecordsRestartAndCursors\|TestDocumentHistoryAndExport' -count=1` | Temporary SQLite; sequences, retry cursors, declaration history and export. | +| Multi-aircraft integration | `go test -race ./internal/adapter -run '^TestFlightSessionIntegration$' -count=1 -v` | Real UDP, two aircraft, fixture HTTP services, outage/restart, archive/log recovery, bridge and export. | +| Actual Flight Blender | `FLIGHT_BLENDER_ROOT=../flight-blender scripts/test-flight-blender.sh` | Installed sibling `.venv`, companion API, Go on PATH; optional `FLIGHT_BLENDER_PYTHON` overrides Python. Temporary SQLite/server, loopback port, `curl`; no production endpoint needed. | +| Frontend types | `npm --prefix frontend ci` then `npm --prefix frontend run check` | Includes Svelte/TypeScript; not interaction coverage. | +| Version consistency | `node scripts/check-version.mjs` | Keep release/application/frontend versions aligned. | +| Packaging helpers | `python3 -m unittest discover -s scripts -p 'test_*.py'` | Use a current Python; package-smoke tooling requires modern Python annotations. | +| QGC native map | `qmllint` and `qmltestrunner` commands above | Qt Location/Positioning, Qt 6.8+; use `QSG_RHI_BACKEND=software` if offscreen graphics need it. | +| Mission Planner compilation | `TestMissionPlannerPluginCompilation` | Windows CI supplies `UTM_TEST_MP_DIRECTORY`, `UTM_TEST_MP_CSC`, `UTM_TEST_MP_FRAMEWORK`; without them this test skips. | +| Packaged GCS | GCS workflow | Boots packaged QGC and requires real event-loop feed fetch; Windows loads/init/unloads the packaged plugin DLL. | +| Desktop/hardware acceptance | Manual target-OS trial | Install, tray, restart, credential store, real telemetry, map interaction and server authentication. Record results separately. | + +The real Flight Blender script starts only an isolated local server, uses temporary data, and cleans it up on exit. It intentionally bypasses JWT signature verification in that process. Run the companion server's API tests for RSA verification, scope/owner isolation and Alembic upgrade/downgrade; ORM metadata creation in the adapter script does not exercise migrations. Actual Aerobridge was additionally checked locally with authentication enforcement disabled; fixture tests remain the repeatable default and do not replace production identity-provider validation. + +## Maintaining releases and documentation + +For a focused GCS build or the full package set: + +```sh +gh workflow run gcs.yml --ref -f component=mission-planner +gh workflow run desktop.yml --ref -f packages=qgroundcontrol -f gcs-target=windows-amd64 +gh workflow run desktop.yml --ref -f packages=all +# Optional actual-server contract against companion changes: +gh workflow run tests.yml --ref -f flight-blender-ref= +``` + +Manual dispatch builds artifacts without publishing. Full manual Desktop runs also assemble and verify the complete release set; focused selections do not run that aggregate release job. GCS artifacts include provenance manifests and checksums. Update [build/gcs-versions.json](build/gcs-versions.json), compatibility notes, integration installation instructions and expected package tests together when changing an upstream pin. + +Before tagging, align versions using the version check, verify both matrices and aggregate checksum checks, review runtime gaps in the compatibility matrix, and confirm the companion server migration is available to users. A matching `v` tag triggers the publish path: create a draft, upload verified packages, then publish. A passing manual build does not validate actual publication permissions or the tag-triggered publish condition. Developer ID/notarization and publisher signing are not configured by this workflow. -Live authenticated Flight Blender delivery, real autopilots, serial hardware, and Mission Planner runtime integration still need trials. The Mission Planner plugin has been checked against a local source API but not validated at runtime on Windows. Record the OS, GCS version, transport, and receiving Flight Blender version when extending that coverage. +When changing options, update [CONFIGURATION.md](docs/CONFIGURATION.md) and the user workflow. When changing persistence, concurrency, payloads or retries, update [ARCHITECTURE.md](docs/ARCHITECTURE.md), receiving-service contracts and relevant integration tests. Keep observed validation results distinct from configured CI checks and pending hardware trials. -Declaration creation, mapping/planning, Aerobridge/fleet synchronization, release signing and installers, auto-updates, login startup, and the custom OpenUTM QGC build remain outside the implemented preview. +See the [shared Go/DJI session archive contract](docs/SESSION_ARCHIVE.md) for schema version 2, completion receipts, compatibility and the cross-client integration harness. diff --git a/README.md b/README.md index a2eb809..ced9975 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,26 @@ UTM Adapter connects your vehicle's MAVLink telemetry to Flight Blender. Use it The adapter runs on your computer and keeps working in the system tray when you close its window. It receives telemetry passively and sends no vehicle commands. -**Early access:** this preview supports telemetry monitoring and delivery. It does not create flight declarations or provide flight planning. See [current limitations](#current-limitations) before use. +**Version 0.2.0:** records pilot-assigned flight sessions locally, synchronizes their telemetry and audit records, and supplies restrictions to native GCS map integrations. Planning and declaration creation remain in Flight Spotlight / Flight Blender. See the [compatibility matrix](COMPATIBILITY.md) for tested versions and validation boundaries. + +## Documentation + +| Guide | Use it for | +| --- | --- | +| This page | Installation, first connection, and everyday controls. | +| [Configuration reference](docs/CONFIGURATION.md) | Every option, default, identifier, credential and environment setting. | +| [Operations and recovery](docs/OPERATIONS.md) | Workflow choices, pre/post-flight checks, offline behavior, maps, backups and troubleshooting. | +| [Developer guide](DEVELOPING.md) | Builds, tests, integration verification and release CI. | +| [Architecture](docs/ARCHITECTURE.md) | Component/sequence/data diagrams, concurrency, persistence, APIs and extension rules. | +| [Compatibility and validation](COMPATIBILITY.md) | Supported package targets, tested paths and remaining runtime trials. | + +## Choose what to enable + +Monitoring, local session recording, live delivery and archive synchronization are separate workflows. Both live-delivery switches default to off. Starting a session records received GPS samples and queues its archive for automatic synchronization while connections run, even if live delivery is off. Legacy delivery sends one aircraft's fresh positions without creating a durable session archive. See the [workflow comparison](docs/OPERATIONS.md#choose-a-workflow) before enabling delivery. ## Get the app -Download a build from a successful run of the [Desktop binaries workflow](https://github.com/openutm/utm-adapter/actions/workflows/desktop.yml). Open the run, scroll to **Artifacts**, and select the download for your computer. Downloading requires a GitHub account. Each artifact contains an application archive and a SHA-256 checksum file. +Download tagged builds from [GitHub Releases](https://github.com/openutm/utm-adapter/releases), or a preview build from a successful run of the [Desktop binaries workflow](https://github.com/openutm/utm-adapter/actions/workflows/desktop.yml). Open the run, scroll to **Artifacts**, and select the download for your computer. Downloading requires a GitHub account. Each artifact contains an application archive and a SHA-256 checksum file. | Your computer | Artifact | How to open it | | --- | --- | --- | @@ -16,7 +31,7 @@ Download a build from a successful run of the [Desktop binaries workflow](https: | Windows, x64 | `utm-adapter-windows-amd64` | Extract the archive and run `utm-adapter.exe`. Microsoft Edge WebView2 Runtime is required. | | Linux, x64 | `utm-adapter-linux-amd64` | Extract the archive, make `utm-adapter` executable if necessary, and launch it from your desktop session. | -These are preview builds, not signed release installers. The macOS app is ad-hoc signed and is not notarized, so macOS may require approval in **System Settings → Privacy & Security** before opening it. +Windows releases include an NSIS installer with WebView2 bootstrapping; Linux releases include an AppImage. Builds are not publisher-signed. The macOS app is ad-hoc signed and is not notarized, so macOS may require approval in **System Settings → Privacy & Security** before opening it. The Linux executable is built on Ubuntu 24.04 and requires compatible GTK4 and WebKitGTK 6.0 libraries, a desktop session, a tray host, and a running Secret Service provider for credential storage. On Ubuntu 24.04, install the runtime libraries with: @@ -34,14 +49,14 @@ Have the following ready: - QGroundControl or Mission Planner connected to your vehicle, or a direct MAVLink source. - To send telemetry: an existing Flight Blender declaration UUID, the vehicle serial number, the operator ID, and the vehicle's MAVLink system ID. -You can monitor incoming telemetry with **Enable telemetry delivery** switched off. Delivery is off by default. +For monitoring only, leave both **Enable telemetry delivery** and **Send session telemetry to Flight Blender** off, and do not start a session. Both switches are off by default. ## Connect for the first time 1. Open UTM Adapter and select **Connection settings**. 2. Enter your **Server URL** and choose **Authentication**. For a bearer token, paste the token. For OAuth, enter the **Token URL**, **Client ID**, **Client secret**, and **Scopes** provided by your administrator. The default scopes are `flightblender.read flightblender.write`. Choose **None** only if your server permits unauthenticated requests. 3. Under **Ground station & telemetry**, choose your **Telemetry source**. For a local ground station, keep **UDP** and the default **Listen address**, `127.0.0.1:14551`. -4. Leave telemetry delivery off while setting up. Click **Save & connect**. +4. Leave both live-delivery switches off while setting up. Click **Save & connect**. 5. Follow the instructions below for [QGroundControl](#qgroundcontrol), [Mission Planner](#mission-planner), or [Direct MAVLink](#direct-mavlink). 6. Open **Overview**. Confirm that Flight Blender is reachable, API access is verified, and your vehicle appears under **Vehicle telemetry**. @@ -75,9 +90,9 @@ You can use Mission Planner's built-in MAVLink mirror or install the optional Op 3. In **Ground stations**, choose **Install OpenUTM plugin** and wait for **File installed**. 4. Reopen Mission Planner and connect your vehicle. Confirm incoming telemetry in **Overview**. -The plugin forwards received telemetry automatically when Mission Planner starts and follows changes to its primary vehicle connection. Automatic configuration alone never installs it; you must select the plugin explicitly. The adapter does not request administrator access, so installation can fail if the Mission Planner directory is not writable. Use the built-in mirror in that case. +The plugin forwards received telemetry automatically when Mission Planner starts and follows changes to all vehicle connections and displays restriction polygons on the Flight Data map. Automatic configuration alone never installs it; you must select the plugin explicitly. The adapter does not request administrator access, so installation can fail if the Mission Planner directory is not writable. Use the built-in mirror in that case. -**File installed** confirms installation on disk. Incoming telemetry confirms forwarding is working. Windows runtime validation of the plugin remains pending in this preview. +**File installed** confirms installation on disk. Incoming telemetry confirms forwarding is working. Windows CI verifies packaged DLL loading, initialization and unload. Connected-vehicle forwarding and interactive map trials remain pending. ### Direct MAVLink @@ -91,7 +106,23 @@ Choose **Direct MAVLink** in **Connection settings**, then select a connection t Click **Save & connect**. Direct mode does not change ground station settings. The source must already emit heartbeat and GPS data; the adapter does not request telemetry streams. If a ground station already uses the vehicle's serial port, use its forwarding feature. -**MAVLink system ID** selects a vehicle: `0` displays all received vehicles, while `1`–`255` filters to that system. Sending to Flight Blender requires an explicit, nonzero ID. +**MAVLink system ID** selects a vehicle: `0` displays all received vehicles, while `1`–`255` filters to that system. Each flight session requires an explicit, nonzero ID; use filter `0` to record multiple aircraft. + +## Record and synchronize flight sessions + +Deploy the companion Flight Blender adapter API and run `alembic upgrade head` on the server first. The session archive requires `/adapter_ops/sessions/{UUID}` and the `a218_session_records` migration. Existing live telemetry APIs remain supported. + +1. Connect your telemetry source and select **Flight sessions**. +2. Enter the aircraft's MAVLink system ID, declaration UUID, vehicle serial, operator, and pilot name, licence and contact details. Pilot ID is optional. Select system filter `0` in settings when receiving multiple aircraft. +3. Start the session before flight. Each aircraft has one active session; its assignment stays fixed until the session ends. +4. Enable **Send session telemetry** in settings to send fresh positions for each active session. Archive synchronization runs independently and retains failed deliveries for retry. +5. End the session and select **Export** for a JSON report containing pilot/aircraft details, GPS positions, altitudes, speed, declaration history and start/end audit events. The app displays the export's path. + +Sessions and delivery cursors survive restarts. A paused receiver records no new samples. After a crash, the existing session remains active until you end it; receiving telemetry resumes into that assignment. Session records upload in batches of at most 200 samples, with duplicate-safe Flight Blender retries. Historical samples go to the archive API rather than being replayed as live positions. The adapter caches declarations from the configured Flight Blender server, including observed changes to historical declarations. See [operations and recovery](docs/OPERATIONS.md) for offline behavior, synchronization status, multi-aircraft checks and backups. + +For Aerobridge, enable the optional registry in settings and provide its URL and bearer token. **Refresh choices** loads aircraft, pilots and operations. Enter the matching operation UUID when starting a session; the adapter attaches the session batch to that operation's flight log. Disabling Aerobridge keeps its pending records locally. Pending deliveries remain associated with their original server URL and resume when that server is selected again. + +The Mission Planner plugin shows Flight Blender restriction boundaries directly on its map. For QGC map overlays, install the OpenUTM QGC build from the release's `utm-adapter-gcs-qgroundcontrol-*` downloads. CI also provides a compiled Mission Planner plugin ZIP; follow its README and install only one plugin copy (DLL or generated source). Source-build instructions are in [DEVELOPING.md](DEVELOPING.md). Both use the read-only loopback feed at `http://127.0.0.1:14552/v1/restrictions`, without credentials or pilot details. Stale/unavailable feeds retain the cached boundaries and show a warning. Check published altitude and validity limits; the overlay fills polygon exteriors, including any holes. ## Send telemetry to Flight Blender @@ -105,7 +136,7 @@ The declaration must cover the current time and be in a state that accepts telem Delivery requires a vehicle heartbeat and GPS sample no more than five seconds old, a 3D GPS fix, WGS84 ellipsoid altitude from MAVLink 2, speed, and course. An MSL altitude displayed in the app does not satisfy the WGS84 altitude requirement. Missing ellipsoid altitude, including a reported zero value, blocks delivery. -The adapter sends new GPS samples about once per second. It has no offline queue or historical replay, so interrupted delivery does not backfill missed samples. Switch off **Enable telemetry delivery** and save to stop sending while continuing local monitoring. +This legacy single-declaration mode sends fresh GPS samples about once per second. Use Flight sessions for durable recording and archive synchronization; legacy mode alone has no offline queue. Switch off **Enable telemetry delivery** and save to stop sending while continuing local monitoring. ## Understand the overview @@ -127,7 +158,7 @@ Launching the app again opens the existing instance. The app does not configure ## Settings, credentials, and undoing setup -Tokens and client secrets are stored in macOS Keychain, Windows Credential Manager, or Linux Secret Service. Leave a password field blank to keep its saved value. **Remove saved credentials** deletes saved credentials, disables telemetry delivery, and pauses connections. +Tokens and client secrets are stored in macOS Keychain, Windows Credential Manager, or Linux Secret Service. Leave a password field blank to keep its saved value. **Remove saved credentials** deletes all saved secrets, disables legacy telemetry delivery and Aerobridge, and pauses connections. The session-delivery setting remains unchanged; review it before resuming. See the [credential reference](docs/CONFIGURATION.md#credential-presence-fields). Other settings are saved in `OpenUTM/utm-adapter/product.json` under your OS configuration directory. Typical locations are: @@ -137,12 +168,12 @@ Other settings are saved in `OpenUTM/utm-adapter/product.json` under your OS con | Windows | `%AppData%\OpenUTM\utm-adapter\product.json` | | Linux | `$XDG_CONFIG_HOME/OpenUTM/utm-adapter/product.json`, or `~/.config/OpenUTM/utm-adapter/product.json` when unset | -Use the path shown in **Recent activity** to locate your actual file. +Use the path shown in **Recent activity** to locate your actual file. `records.sqlite` beside it contains flight sessions, cached declarations and delivery queues. Reports are written in the adjacent `exports/` directory. These contain personal and flight data; protect them and back them up with your configuration. Automatic setup backs up existing files before changing them, beside the original with the suffix `.utm-adapter-backup-`. It does not rewrite unchanged settings. Turning automatic setup off leaves existing forwarding and installed plugins in place. - **Undo QGroundControl setup:** disable automatic configuration in the adapter, close QGroundControl, restore its settings backup, and remove the adjacent `.utm-adapter-target` file. Reopen QGroundControl. If no backup is available, disable forwarding manually in QGroundControl. -- **Remove the Mission Planner plugin:** close Mission Planner and choose **Ground stations → Remove OpenUTM plugin**. This disables future installation and moves the plugin to a backup that Mission Planner will not load. +- **Remove the Mission Planner plugin:** close Mission Planner and choose **Ground stations → Remove OpenUTM plugin**. This disables future installation and moves the managed source plugin to a backup that Mission Planner will not load. A compiled release DLL must be removed manually; see its [installation guide](integrations/mission-planner/README.md). ## Troubleshooting @@ -162,12 +193,14 @@ If you need help, [open an issue](https://github.com/openutm/utm-adapter/issues) ## Current limitations -- Existing flight declarations only; no declaration creation, maps, flight planning, or fleet synchronization. +- Existing flight declarations only; declaration creation and flight planning remain in external tools. Native GCS maps show restriction boundaries through the supplied integrations. - MAVLink 1 and 2 are supported for monitoring, using autopilot component 1. Delivery requires MAVLink 2 ellipsoid altitude. UDP input expects one MAVLink frame per datagram. - Armed state is displayed locally; delivered operational status is **Undeclared**, because armed state alone does not establish whether a vehicle is airborne. -- No automatic updates, login startup, or signed release installers. The optional OpenUTM custom QGroundControl build is planned and has no downloadable release in this preview. -- Real autopilots, serial hardware, live authenticated Flight Blender connections, and Mission Planner still need user trials. QGroundControl forwarding has been exercised on macOS with synthetic telemetry. +- No automatic updates, login startup, or publisher signing. QGC packages and the Mission Planner plugin are built alongside adapter releases; see [COMPATIBILITY.md](COMPATIBILITY.md) for platforms and verification coverage. +- Physical autopilots, serial hardware, production authentication/deployments and interactive Mission Planner vehicle/map use still need trials. Local tests exercise the actual Flight Blender and Aerobridge applications; virtual serial and SITL/parser tests do not establish hardware compatibility. See [COMPATIBILITY.md](COMPATIBILITY.md). For source builds, development setup, architecture, and testing, see the separate [developer guide](DEVELOPING.md). Licensed under [Apache 2.0](LICENSE). + +See the [shared Go/DJI session archive contract](docs/SESSION_ARCHIVE.md) for schema version 2, completion receipts, compatibility and the cross-client integration harness. diff --git a/build/config.yml b/build/config.yml index fb8255a..a341289 100644 --- a/build/config.yml +++ b/build/config.yml @@ -11,7 +11,7 @@ info: description: "Ground control to Flight Blender" # The application description copyright: "(c) 2026 OpenUTM" # Copyright text comments: "Desktop connection preview" # Comments - version: "0.1.0" # The application version + version: "0.2.0" # The application version # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) # # Should match the name of your .icon file without the extension # # If not set and Assets.car exists, defaults to "appicon" @@ -24,7 +24,7 @@ info: # # The display name shown under the app icon (CFBundleDisplayName/CFBundleName) # displayName: "UTM Adapter" # # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion) -# version: "0.1.0" +# version: "0.2.0" # # The company/organisation name for templates and project settings # company: "OpenUTM" # # Additional comments to embed in Info.plist metadata diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 95d686c..81bd475 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -10,11 +10,11 @@ CFBundleIdentifier org.openutm.adapter CFBundleVersion - 0.1.0 + 0.2.0 CFBundleGetInfoString This is a comment CFBundleShortVersionString - 0.1.0 + 0.2.0 CFBundleIconFile icons CFBundleIconName diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index eed6b50..dbf6959 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -10,11 +10,11 @@ CFBundleIdentifier org.openutm.adapter CFBundleVersion - 0.1.0 + 0.2.0 CFBundleGetInfoString Ground control to Flight Blender CFBundleShortVersionString - 0.1.0 + 0.2.0 CFBundleIconFile icons CFBundleIconName diff --git a/build/gcs-versions.json b/build/gcs-versions.json new file mode 100644 index 0000000..0f2010e --- /dev/null +++ b/build/gcs-versions.json @@ -0,0 +1,12 @@ +{ + "qgroundcontrol": { + "repository": "mavlink/qgroundcontrol", + "version": "5.1.4", + "commit": "4568b585648e8c6065cca6c650b52731c1d3baa3" + }, + "missionPlanner": { + "version": "1.3.83", + "url": "https://github.com/ArduPilot/MissionPlanner/releases/download/MissionPlanner1.3.83/MissionPlanner-1.3.83.zip", + "sha256": "ff3694f7a9038caf756545049dd7aef74e3d0d8fe3586e87aa881045704dd6f9" + } +} diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index 57946d7..b0916e0 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "utm-adapter" arch: ${GOARCH} platform: "linux" -version: "0.1.0" +version: "0.2.0" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> diff --git a/build/windows/info.json b/build/windows/info.json index 3f8532a..cf69048 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "0.1.0" + "file_version": "0.2.0" }, "info": { "0409": { - "ProductVersion": "0.1.0", + "ProductVersion": "0.2.0", "CompanyName": "OpenUTM", "FileDescription": "Ground control to Flight Blender", "LegalCopyright": "© 2026, OpenUTM", diff --git a/build/windows/msix/app_manifest.xml b/build/windows/msix/app_manifest.xml index e228282..3c1fc1a 100644 --- a/build/windows/msix/app_manifest.xml +++ b/build/windows/msix/app_manifest.xml @@ -10,7 +10,7 @@ diff --git a/build/windows/msix/template.xml b/build/windows/msix/template.xml index 0e3bf45..6a296e2 100644 --- a/build/windows/msix/template.xml +++ b/build/windows/msix/template.xml @@ -17,7 +17,7 @@ PackageDisplayName="UTM Adapter" PublisherName="CN=OpenUTM" PublisherDisplayName="OpenUTM" - Version="0.1.0.0" + Version="0.2.0.0" PackageDescription="Ground control to Flight Blender"> diff --git a/build/windows/nsis/project.nsi b/build/windows/nsis/project.nsi index c13cf05..b545947 100644 --- a/build/windows/nsis/project.nsi +++ b/build/windows/nsis/project.nsi @@ -22,7 +22,7 @@ Unicode true ## !define INFO_PROJECTNAME "my-project" # Default "utm-adapter" ## !define INFO_COMPANYNAME "OpenUTM" # Default "OpenUTM" ## !define INFO_PRODUCTNAME "My Product Name" # Default "UTM Adapter" -## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.2.0" ## !define INFO_COPYRIGHT "(c) Now, OpenUTM" # Default "© 2026, OpenUTM" ### ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 1a831a6..0947df7 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -14,7 +14,7 @@ !define INFO_PRODUCTNAME "UTM Adapter" !endif !ifndef INFO_PRODUCTVERSION - !define INFO_PRODUCTVERSION "0.1.0" + !define INFO_PRODUCTVERSION "0.2.0" !endif !ifndef INFO_COPYRIGHT !define INFO_COPYRIGHT "© 2026, OpenUTM" diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest index 44d2b5b..466f6d4 100644 --- a/build/windows/wails.exe.manifest +++ b/build/windows/wails.exe.manifest @@ -1,6 +1,6 @@ - + diff --git a/cmd/mission-planner-plugin/main.go b/cmd/mission-planner-plugin/main.go new file mode 100644 index 0000000..5a35882 --- /dev/null +++ b/cmd/mission-planner-plugin/main.go @@ -0,0 +1,22 @@ +// Generate the same source used by the adapter's Mission Planner installer. +package main + +import ( + "flag" + "log" + "os" + "path/filepath" + + "github.com/openutm/utm-adapter/internal/adapter" +) + +func main() { + output := flag.String("out", "OpenUTMAdapter.cs", "generated plugin source path") + flag.Parse() + if err := os.MkdirAll(filepath.Dir(*output), 0755); err != nil { + log.Fatal(err) + } + if err := os.WriteFile(*output, []byte(adapter.MissionPlannerPluginSource()), 0644); err != nil { + log.Fatal(err) + } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..4d0756b --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,227 @@ +# Architecture and integration contracts + +UTM Adapter is a single desktop process built with Go, Wails v3 and Svelte 5. It passively receives MAVLink, maintains live vehicle state, records assigned sessions in SQLite, and connects to Flight Blender and optional Aerobridge APIs. Native GCS integrations consume its local restriction feed. Planning, declaration creation, aircraft control, and flight authorization remain external. + +Read [DEVELOPING.md](../DEVELOPING.md) for build/test commands, [CONFIGURATION.md](CONFIGURATION.md) for settings, and [COMPATIBILITY.md](../COMPATIBILITY.md) for evidence and remaining runtime trials. Diagrams below use Mermaid and describe the implemented desktop workflow. + +## System context + +```mermaid +flowchart LR + Vehicle["Autopilot / SITL"] -->|MAVLink| GCS["QGC / Mission Planner / router"] + Vehicle -->|Direct UDP / TCP / serial| Receiver + GCS -->|Forwarded UDP| Receiver + subgraph Desktop["UTM Adapter process"] + UI["Svelte UI + Wails bindings"] <-->|"Calls / redacted snapshots"| Service["Go Service"] + Receiver["gomavlib receiver"] --> Service + Service <-->|"Sessions / cache / pending batches"| DB[("records.sqlite")] + Service --> Bridge["Loopback HTTP bridge"] + Service <--> Keys["OS credential store"] + end + Service <-->|"HTTPS: live telemetry, archives, declarations, geofences"| FB["Flight Blender"] + Service <-->|"HTTPS: registry + operation flight logs"| AB["Aerobridge, optional"] + Bridge -->|"GeoJSON restrictions"| Map["Native GCS map integration"] +``` + +There is no remote control listener or MAVLink command path. The Wails service supplies desktop RPC, not a public web API. The separate HTTP bridge exposes only health and cached restrictions on a loopback IP. Default ports are UDP `14551` for telemetry and TCP `14552` for bridge HTTP; neither port is a Flight Blender listener. + +## Modules and ownership + +| Source | Responsibility | +| --- | --- | +| [main.go](../main.go) | Embeds frontend/assets, registers service, creates tray/window, handles single-instance and shutdown lifecycle. | +| [service.go](../internal/adapter/service.go) | Owns config, receiver, live state, worker lifecycle, credentials and redacted snapshots; applies delivery guards. | +| [config.go](../internal/adapter/config.go) | Defaults, validation and atomic, secret-free settings persistence. | +| [records.go](../internal/adapter/records.go) | SQLite schema, assignment validation, transactions, sample sequencing, batches, acknowledgements and document history. | +| [sessions.go](../internal/adapter/sessions.go) | Session service methods, streamed export and orchestration of destination synchronization. | +| [blender.go](../internal/adapter/blender.go) | Flight Blender transport, bearer/OAuth, access checks and live telemetry mapping. | +| [sync.go](../internal/adapter/sync.go) | Declaration caching, archive HTTP requests, Aerobridge registry and flight-log reconciliation. | +| [restrictions.go](../internal/adapter/restrictions.go) | Geofence conversion/cache refresh and local HTTP bridge. | +| [discovery.go](../internal/adapter/discovery.go), [gcsconfigure.go](../internal/adapter/gcsconfigure.go) | Known GCS discovery, conservative forwarding edits, backups and managed Mission Planner source generation. | +| [App.svelte](../frontend/src/App.svelte) | Four screens, forms, session actions, status display; polls snapshots once per second without overlapping polls. | +| [QGC integration](../integrations/qgroundcontrol), [Mission Planner integration](../integrations/mission-planner) | Native restriction rendering; Mission Planner also forwards all connected vehicle links. | +| [build](../build), [scripts](../scripts), [workflows](../.github/workflows) | Native packaging, pinned GCS builds, contract tests and release publication. | + +Generated `frontend/bindings/` and `frontend/dist/` derive from service types and frontend sources. Regenerate with Wails build tasks after changing exported Go types; do not maintain a parallel handwritten RPC model. + +## Process, concurrency, and lifecycle + +The process creates a service, opens the records database independently of telemetry, and starts saved connections on launch. Closing the window hides it; Quit calls `Close`, which stops workers and closes SQLite. A second launch focuses the existing instance, including when a different config path is provided. + +`Start` stops the old receiver, validates configuration, starts the bridge and MAVLink node, and resets transient vehicle state/counters. `ApplyConfig` validates and persists settings/credentials before restarting. `Stop` cancels connections and waits for workers; it preserves the database and active assignments. `Close` is the process-shutdown operation, not a session-end operation. + +```mermaid +flowchart TD + Start["Start / ApplyConfig"] --> Stop["Stop previous connections and wait"] + Stop --> Init["Validate / open records / start node and bridge"] + Init --> Frames["Frame-consumer goroutine"] + Init --> Loop["Main periodic worker"] + Loop --> Archive["Archive-sync goroutine"] + Frames --> State["Vehicle map + GPS sample transaction"] + Loop --> Health["Discovery / managed setup / health and read checks"] + Loop --> Live["Session live delivery, then eligible legacy delivery"] + Archive --> Sync["Cache refresh + destination batches"] + Sync --> Wait["Wait 10 seconds after each pass"] + Wait --> Sync + Cancel["Stop / Quit cancellation"] --> Join["Close node and bridge; wait for workers"] +``` + +The operation mutex `op` serializes lifecycle mutations, session start/end, export, manual synchronization and plugin removal. `mu` protects shared configuration, snapshot state, vehicle maps and records access points; frame consumption appends samples under this lock. `syncMu` prevents manual/background synchronization passes from overlapping. SQLite uses one connection. Avoid nested database queries while rows hold that connection, and do not hold `mu` across remote HTTP requests. + +The main worker uses a one-second ticker; discovery, setup, health and API access checks run approximately every ten iterations. HTTP duration can delay ticks. The archive worker has a 45-second request budget per pass and waits ten seconds afterward. Each HTTP request has its own six-second timeout. These intervals are scheduling choices, not throughput or latency guarantees. + +## Telemetry pipeline and live-delivery rules + +1. gomavlib decodes ArduPilotMega MAVLink 1/2. Heartbeats and stream requests from the adapter are disabled. UDP expects one frame per datagram; TCP/serial are streams. +2. The frame count includes successfully parsed frames before aircraft filtering. GCS heartbeats and components other than autopilot component `1` are ignored for vehicle tracking; an optional system filter further limits vehicles. +3. `HEARTBEAT` updates armed state and heartbeat receipt time. `GLOBAL_POSITION_INT` updates MSL/relative altitude and provides a display position fallback until the first GPS message. +4. Each `GPS_RAW_INT` updates receipt time and GPS fields, clearing unavailable optional values instead of retaining stale ones. Active sessions append the resulting vehicle snapshot even when it is unsuitable for live delivery. +5. Live delivery requires heartbeat and GPS no older than five seconds, GPS newer than the last successful send, valid coordinates and fix type 3–8, ellipsoid altitude, speed and course. `GPS_RAW_INT` MAVLink 2 extension `alt_ellipsoid` must be nonzero. MSL altitude never substitutes for it. + +Latitude/longitude convert from degrees × 10^7, altitude from millimetres to metres, speed from centimetres/second to metres/second, and course from centidegrees to degrees. Unknown speed `65535` and course values outside the valid range remain unavailable. Receipt timestamps use the desktop clock; vehicle boot time is not used as authoritative sample time. + +Live payloads contain one observation/current state. Session live delivery uses immutable assignment identifiers. Legacy mode uses configuration identifiers, and an active session suppresses it for the same system even when session delivery is disabled. Both paths send operational status `Undeclared`: armed state alone does not establish airborne state. Accuracy and unavailable vertical-speed/pressure-altitude fields use the receiving API's sentinels in `Blender.Publish`. + +Successful live sends advance in-memory sample markers and the accepted counter. Failures retry only while data remains fresh. Restart resets these markers; live delivery is not an exactly-once archive. Historical samples travel exclusively through the durable session API. Parser errors increment a separate count and retain the latest classified diagnostic; subsequent valid frames continue processing, and restart clears the diagnostics. + +## Session model and persistence + +```mermaid +stateDiagram-v2 + [*] --> Active: BeginSession validates assignment and commits + Active --> Active: GPS sample appends sequence and increments revision + Active --> Active: Pause / restart preserves assignment + Active --> Ended: EndSession commits end time and audit event + Ended --> Ended: Synchronize / export +``` + +A session has a random UUID, immutable system/declaration/vehicle/operator/pilot assignment, UTC start/end times, original destination URLs, and a monotonic revision. At most one session can be active per system ID, enforced by a partial unique database index. It can start without a connected aircraft; server existence/authorization checks are separate. Pilot name, licence and contact are required; pilot ID is optional. An Aerobridge operation UUID is required when Aerobridge is enabled at creation. + +The database is beside the selected configuration, not named after its filename. It uses `modernc.org/sqlite`, schema `user_version=1`, foreign keys, DELETE journaling, FULL synchronous mode and a five-second busy timeout. New data directories request mode `0700`; the database and reports request `0600`. OS access controls still determine effective protection. Opening a newer schema is rejected. Add explicit migration and upgrade/rollback coverage before evolving this schema. + +```mermaid +erDiagram + SESSIONS ||--o{ SAMPLES : contains + SESSIONS ||--o{ DELIVERIES : queues + SESSIONS ||--o{ AUDIT : records + SESSIONS { + string id PK + string data "Assignment, times and original URLs as JSON" + int system_id + int ended + int revision + int fb_revision + int fb_sequence + int ab_revision + int ab_sequence + } + SAMPLES { + string session_id PK,FK + int sequence PK + string data "Vehicle snapshot JSON" + } + DELIVERIES { + string session_id PK + string destination PK + string data "Exact pending SessionBatch JSON" + } + AUDIT { + int sequence PK + string at + string session_id + string action + } + DOCUMENTS { + int observation PK + string server + string kind + string id + string digest + string captured + string data + } +``` + +Only `samples.session_id` has a SQL foreign-key constraint; the diagram's delivery/audit relationships are logical. Documents are associated by server/kind/ID, not by a session FK. Declaration observations retain changes using a SHA-256 digest of raw JSON; formatting changes can therefore count as a new observation. Restriction cache refreshes update freshness even if content is unchanged. The cache is scoped to the configured server URL. + +Starting/ending a session and appending a sequenced sample use transactions. Start/end audit events are committed with their session changes. Missing GPS fixes produce samples with unavailable fields rather than invented positions. There is no automatic sample deletion, retention limit, database encryption or cryptographic audit certification. + +## Archive synchronization and crash recovery + +Flight Blender and Aerobridge have independent acknowledged revision/sequence cursors. Each pass considers all sessions, not just the latest 100 displayed by the UI, and attempts at most one batch of up to 200 samples per destination per session. Metadata-only revisions, including session end, must also be acknowledged. Destination URLs are captured at session creation and compared with current configuration; server changes do not migrate records. + +```mermaid +sequenceDiagram + participant Worker as Sync worker + participant DB as SQLite + participant Remote as Archive destination + Worker->>DB: Load existing pending batch, or select next samples/revision + DB-->>Worker: Exact batch persisted before network request + Worker->>Remote: Upload batch + alt Response acknowledges success + Remote-->>Worker: Success + Worker->>DB: Transaction: advance cursor and remove pending batch + else Timeout, failure, or process crash + Note over DB: Pending JSON and old cursor survive restart + Worker->>Remote: Retry the same batch on a later pass + end +``` + +Persisting the exact payload closes the gap between a remote commit and a lost response. Acknowledgement advances cursors transactionally; samples appended during an upload remain pending. Flight Blender's companion API stores sequences idempotently. Its records are owner-isolated by token issuer/subject; retries with a different identity may be rejected even for the same URL. + +Aerobridge has no native idempotency key for this operation. The adapter adds `raw_log.adapterBatchKey` built from session ID, revision and last sample sequence (or zero). Before posting it lists operation logs and looks for that key and matching operation. A lost response can then reconcile to the existing log. This relies on read-after-write visibility and one adapter owning the session; concurrent cloned datasets or delayed list visibility can create duplicates. Do not describe this destination as an unconditional exactly-once delivery guarantee. + +Cache refresh failures do not delete prior data. Synchronization reports destination errors through status; a completed manual `Synchronize` call does not imply all destinations accepted every batch. Disabled Aerobridge leaves its queue intact, and enabling it later does not retrofit earlier sessions with an Aerobridge destination. + +## External API contracts + +Paths are relative to configured base URLs. URL construction, pagination, request/response structures and status handling live in the linked clients; coordinate changes with the receiving service. + +| Service | Method/path | Purpose | +| --- | --- | --- | +| Flight Blender | `GET /ping` | Unauthenticated reachability. | +| Flight Blender | `GET /flight_declaration_ops/flight_declaration?page_size=1` | Read-access check without a selected declaration. | +| Flight Blender | `GET /flight_declaration_ops/flight_declaration/{UUID}` | Read-access check for a selected declaration. | +| Flight Blender | `GET /flight_declaration_ops/flight_declaration` | Paginated declaration cache, broad start/end date range and page size 100. | +| Flight Blender | `GET /geo_fence_ops/geo_fence` | Geofence cache, limit/offset pagination with limit 100. | +| Flight Blender | `PUT /flight_stream/set_telemetry` | Fresh live position payload. | +| Flight Blender | `PUT /adapter_ops/sessions/{UUID}` | Shared `ArchiveBatch` version 2 containing session metadata and sequenced samples. Requires companion API/migration. | +| Aerobridge | `GET /registry/aircraft/` | Health/read check and aircraft choices. | +| Aerobridge | `GET /registry/pilots/` | Pilot choices. | +| Aerobridge | `GET /gcs/flight-operations` | Operation choices. | +| Aerobridge | `GET /gcs/flight-logs?operation={UUID}` | Existing-log reconciliation. | +| Aerobridge | `POST /gcs/flight-logs` | `{operation, raw_log: {adapterBatchKey, schemaVersion: 1, batch}}`. | + +The common HTTP format, completion rules and source mappings are documented in [Shared session archive API](SESSION_ARCHIVE.md). Local and Aerobridge `SessionBatch` JSON is defined by `Session`, `Assignment`, `Pilot`, `Sample` and `Vehicle` in source. Samples have monotonically increasing per-session sequence numbers; optional vehicle values encode absence rather than zero substitutes. Reports have top-level `schemaVersion: 1`, `session`, `declarationHistory`, `samples` and `audit`. GPS rows stream from SQLite during export, so a whole flight need not fit in memory. Export requires an ended session and writes a private temporary file, syncs it, then renames it into `exports/` with a unique filename. Declaration history contains observations captured by this adapter, not a complete authoritative server change log. + +All HTTP clients reject redirects and bound response reads (8 MiB for general API responses; 1 MiB for OAuth token decoding). OAuth uses client credentials with HTTP Basic authentication, caches until 30 seconds before expiry, defaults missing expiry to 300 seconds and rejects negative expiry. HTTP 401 invalidates the cached token for the next attempt; it does not immediately replay the failed request. Response bodies are not echoed into user-facing HTTP errors. + +Pagination is bounded. Aerobridge accepts array or paginated `results` shapes and reconstructs same-origin page requests rather than following arbitrary `next` URLs. Declaration pagination uses server counts; geofence pagination stops at a short page. If upstream response formats or limits change, update contract tests alongside the client. + +## Restriction bridge and native GCS integration + +`GET /v1/health` returns local bridge status/version. `GET /v1/restrictions` returns `application/geo+json` with `Cache-Control: no-store`: a FeatureCollection plus `fetchedAt` and `stale`. No cached data produces an empty stale collection; invalid cached data/database errors produce HTTP 503. Data older than two minutes is stale. + +Flight Blender `raw_geo_fence` may be an object or JSON string containing a FeatureCollection. Features retain their original properties with restriction ID/name/start/end metadata added. QGC and Mission Planner poll the feed and retain their last boundaries on failure, showing stale state. They render Polygon/MultiPolygon outer rings; holes are filled. These are display integrations, not altitude/time filtering or flight-permission enforcement. + +Automatic forwarding edits are conservative: only local UDP, outside direct mode, and only while the relevant GCS is closed. QGC writes its `[General]` forwarding keys and an adjacent managed-target marker, preserving other destinations. Changed files receive timestamped backups. Mission Planner source installation is explicit opt-in and preserves files without the managed marker. Removing the managed source renames it to an unloaded backup; release DLL installation/removal is manual. Turning auto-configuration off does not undo earlier edits. + +Custom QGC is built from pinned upstream with a minimal custom layer, preserving the standard application/settings identity. The Mission Planner release DLL is compiled from the same production generator as the installed source, but with default endpoints. Never install source and DLL copies together. + +## Security, failure boundaries, and extension rules + +Secrets use the OS credential store under `org.openutm.adapter`, keyed by configuration path; snapshots expose only presence flags. Config persistence strips secrets. Removing credentials pauses connections and disables legacy/Aerobridge delivery, but leaves the session-delivery flag unchanged. Records and exports include personal and flight data; protect backups and avoid exposing them in diagnostics. + +Loopback HTTP is readable by other local processes. MAVLink reception has no adapter-level remote-sender authentication, and UDP/TCP telemetry is not encrypted by this application. Use trusted interfaces and external transport security where needed. Single-instance behavior prevents two normal desktop launches, but does not provide a distributed lock across copied databases or machines. + +When changing the implementation, preserve these invariants: + +- Pause/reconfigure/shutdown must not end sessions or delete pending work. +- Sample sequencing, revision changes, audit events and acknowledgements must retain their transaction boundaries. +- Pending retries must reuse the exact persisted payload; live freshness markers must stay separate from archive cursors. +- Server/identity binding must not silently redirect historical records. +- Unknown telemetry must remain unavailable; MSL must not become ellipsoid altitude. +- GCS setup must preserve unmanaged targets/plugins and never introduce vehicle commands. +- Export/API/schema changes must be versioned and tested against the real receiving application as well as fixtures. + +Verification is layered: focused Go tests for conversion/configuration/authentication, SQLite restart/cursor/export tests, real-UDP multi-aircraft integration with outages, optional actual Flight Blender contract tests, native Qt map tests, packaged GCS boot/fetch tests, and Windows DLL load/init/unload tests. See the [developer test matrix](../DEVELOPING.md#verification-matrix). Hardware and connected GCS trials remain separate from those checks. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..ea0b19e --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,124 @@ +# Configuration reference + +Use **Connection settings → Save & connect** to apply changes. Saving restarts connections and resets live counters, but preserves sessions and queued archives. See the [user guide](../README.md) for installation and the [operations guide](OPERATIONS.md) for workflows and recovery. + +## Connection settings + +The JSON keys below correspond to `product.json`. Omitted fields inherit the defaults in [config.go](../internal/adapter/config.go). Prefer the UI; quit the application before editing this file manually. Never put credentials in it: credentials read from JSON are discarded. + +### Flight Blender and authentication + +| UI option | JSON key | Default | Meaning | +| --- | --- | --- | --- | +| Server URL | `baseURL` | `http://127.0.0.1:8000` | Flight Blender base URL, optionally with a path prefix. | +| Authentication | `authMode` | `bearer` | `bearer`, `oauth` (client credentials), or `none`. Use the mode your server accepts. | +| Bearer token | `token` | Empty | Secret supplied through the UI and stored in the OS credential store. | +| Token URL | `tokenURL` | Empty | OAuth token endpoint; required in OAuth mode. | +| Client ID | `clientID` | Empty | Required in OAuth mode. | +| Client secret | `clientSecret` | Empty | OAuth secret supplied through the UI and stored in the OS credential store. | +| Scopes | `scope` | `flightblender.read flightblender.write` | Requested OAuth scopes. A bearer token must already carry the required permissions. | + +HTTP(S) URLs must have a host, with no embedded username/password, query, or fragment. Redirects are rejected. Use the server's final URL. HTTPS uses the operating system's trust configuration; there is no custom CA or client-certificate setting in the UI. HTTP is available for local test servers; use HTTPS for remote credentials and flight data. + +The connection check verifies reachability and read access. Live delivery and archive uploads need write access too. Session archives require the companion Flight Blender API and `a218_session_records` migration. Archive ownership is based on token issuer and subject: renew credentials using the same service identity to continue accessing that identity's records. + +### Sessions, Aerobridge, and maps + +| UI option | JSON key | Default | Meaning | +| --- | --- | --- | --- | +| Send session telemetry to Flight Blender | `sessionDelivery` | `false` | Sends fresh positions for active sessions. Does not control local recording or durable archive synchronization. | +| Local GCS map address | `bridgeAddress` | `127.0.0.1:14552` | Local HTTP listener for restrictions. Empty disables it. Must use a loopback IP literal and port, such as `127.0.0.1:14552` or `[::1]:14552`; `localhost` is not accepted. | +| Connect Aerobridge | `aerobridgeEnabled` | `false` | Enables registry choices and flight-log synchronization. | +| Aerobridge URL | `aerobridgeURL` | `http://127.0.0.1:8080` | Required when enabled; same URL rules as Flight Blender. | +| Aerobridge bearer token | `aerobridgeToken` | Empty | Separate credential stored in the OS credential store. The adapter requires a nonempty token when making registry/log requests, including against a development server without authentication enforcement. | + +Archive synchronization is automatic while connections run, even when both live-delivery switches are off. **Flight sessions → Synchronize now** can attempt synchronization while connections are paused. There is no separate archive-off setting. To monitor without creating flight records, do not start a session. To record during an outage, leave the server configuration intact; uploads retry when connectivity returns. + +The map listener runs only while connections run. Release GCS integrations use the default IPv4 map address. Changing it requires updating the QGC QML endpoint or regenerating the Mission Planner source plugin; the compiled release DLL has fixed default endpoints. + +### Ground station and telemetry + +| UI option | JSON key | Default | Meaning | +| --- | --- | --- | --- | +| Telemetry source | `source` | `auto` | `auto`, `qgroundcontrol`, `mission-planner`, or `direct`. Controls discovery/setup policy; the transport below determines reception. | +| Set up ground station forwarding automatically | `autoConfigureGCS` | `true` | Allows managed local UDP setup while the GCS is closed. Direct mode skips setup. Does not install the Mission Planner plugin without opt-in. | +| Install/remove OpenUTM plugin in Ground stations | `missionPlannerPlugin` | `false` | Persists explicit opt-in to the managed Mission Planner source plugin. | +| Connection type | `transport` | `udp` | `udp` listener, `tcp` client, or `serial` connection. | +| Listen address / Remote address | `address` | `127.0.0.1:14551` | UDP bind address or TCP server address, in `host:port` form. Ignored for serial. | +| Device | `serialDevice` | Empty | Serial device path or Windows COM port; required for serial. | +| Baud rate | `baud` | `57600` | Match the device; minimum accepted value is 1200. | +| MAVLink system ID | `systemID` | `0` | `0` tracks all aircraft; `1`–`255` filters to one aircraft. Legacy delivery requires a nonzero filter. Sessions require explicit IDs matching this filter, or filter `0`. | + +The default UDP listener accepts traffic only from the local computer. Listening on an interface address exposes reception to that network; incoming MAVLink does not have adapter-level sender authentication. Use a trusted telemetry network. Automatic GCS setup accepts only local UDP destinations. TCP and serial sources must already stream telemetry: the adapter sends no heartbeat, stream requests, or vehicle commands. + +### Legacy single-declaration live delivery + +| UI option | JSON key | Default | Meaning | +| --- | --- | --- | --- | +| Enable telemetry delivery | `publish` | `false` | Enables live delivery for the configured system and declaration. No durable queue. | +| Flight declaration ID | `declarationID` | Empty | Existing declaration UUID. Any nonempty value must have UUID format, even with delivery off. | +| Vehicle serial number | `vehicleID` | Empty | Vehicle identifier expected by Flight Blender's live telemetry API. | +| Operator ID | `operatorID` | `utm-adapter` | Operator identifier expected by the receiving service. | + +Enabling this mode requires all identifiers and a nonzero `systemID`. An active session for that aircraft takes precedence: it suppresses legacy delivery even if session live delivery is off. End that session before returning to legacy delivery. Use sessions for concurrent aircraft and durable archives. + +### Credential presence fields + +`hasToken`, `hasClientSecret`, and `hasAerobridgeToken` are snapshot indicators for the UI, not configuration controls. The backend redacts secrets from snapshots and clears these flags when saving configuration. Leaving a password field blank preserves its saved secret. **Remove saved credentials** deletes all three secrets, pauses connections, and disables legacy delivery and Aerobridge. It leaves the session-delivery setting intact; review it before resuming, especially with authentication set to None. + +## Session assignment + +Assignments are fixed once recording starts. End the session and create another to change them. Starting a session validates its fields locally; it does not prove the declaration, aircraft, pilot, or operation exists on a server. + +| Field | Required | Meaning | +| --- | --- | --- | +| MAVLink system ID | Yes | Aircraft ID `1`–`255`; unique among active sessions and distinguishable in the incoming telemetry. | +| Declaration UUID | Yes | Existing Flight Blender declaration. | +| Aircraft ID | Yes | Assignment's vehicle identifier, also used as the serial number in session live delivery. Use the value expected by Flight Blender; up to 512 characters. | +| Operator ID | Yes | Operator responsible for this flight; up to 512 characters. | +| Pilot ID | No | Optional registry/reference identifier, up to 128 characters. | +| Pilot name | Yes | Pilot's name, up to 512 characters. | +| Pilot licence | Yes | Licence/reference, up to 512 characters. | +| Pilot contact | Yes | Contact details, up to 512 characters. | +| Aerobridge operation UUID | When Aerobridge is enabled | Operation to receive the flight logs. Optional otherwise; must be a UUID when supplied. | + +**Refresh choices** offers cached declarations and, when enabled, Aerobridge aircraft, pilots, and operations. Suggestions do not automatically fill or validate all assignment metadata. A registry UUID and a vehicle serial are not necessarily interchangeable; confirm the identifier accepted by your Flight Blender deployment. + +## Files and environment + +The default directory is `OpenUTM/utm-adapter` under the OS user configuration directory. See [platform paths](../README.md#settings-credentials-and-undoing-setup). + +| Location | Contents | +| --- | --- | +| `product.json` | Non-secret connection settings. | +| `records.sqlite` in the same directory | Sessions, GPS samples, delivery cursors, pending payloads, declaration/restriction cache, and audit events. | +| `exports/` in the same directory | JSON reports created explicitly for ended sessions. | +| OS credential store | Flight Blender token, OAuth client secret, Aerobridge token. | + +| Environment variable | Meaning | +| --- | --- | +| `UTM_ADAPTER_CONFIG` | Overrides the configuration filename. Use an absolute path. The directory also determines the database and export location. Different filenames in one directory still share one database; use separate directories for isolated datasets. | +| `UTM_ADAPTER_START_HIDDEN=1` | Opens in the tray with the window hidden. Does not install startup-at-login. | +| `WAILS_VITE_PORT` | Development frontend port; task default `9245`. Not a packaged-app connection setting. | + +Credential-store service is `org.openutm.adapter`; entries are keyed by configuration path. Moving the configuration can require entering credentials again. A second launch opens the existing application instance, even with a different path: quit before switching datasets. + +Example configuration for **local synthetic monitoring**, using an unauthenticated test server: + +```json +{ + "baseURL": "http://127.0.0.1:8000", + "authMode": "none", + "source": "direct", + "autoConfigureGCS": false, + "transport": "udp", + "address": "127.0.0.1:14551", + "systemID": 0, + "publish": false, + "sessionDelivery": false, + "aerobridgeEnabled": false, + "bridgeAddress": "127.0.0.1:14552" +} +``` + +This example still attempts server checks and restriction refreshes. Save it in a separate test directory and use the [synthetic sender](../DEVELOPING.md#test-without-a-vehicle). Never point synthetic delivery at an operational declaration. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..7b59b32 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,88 @@ +# Operating UTM Adapter + +Start with the [installation and connection guide](../README.md). The [configuration reference](CONFIGURATION.md) explains every option. This guide covers choosing a workflow, checking results, and recovering records. + +## Choose a workflow + +| Goal | Required actions | Result | +| --- | --- | --- | +| Monitor telemetry | Connect a source; leave both delivery switches off; do not start a session. | Live display; no session archive. Server checks and restriction refreshes still run. | +| Record a flight | Start a session with an immutable assignment. | Durable GPS records and start/end audit events. Archive synchronization runs automatically while connections run. | +| Send live positions for sessions | Enable **Send session telemetry to Flight Blender** and start assigned sessions. | Fresh positions go to the live API; recorded history independently goes to the archive API. | +| Send live positions without a session | Enable legacy **Enable telemetry delivery**, with explicit system/declaration/vehicle/operator IDs. | Single-aircraft live updates; no historical queue or session report. | +| Attach records to Aerobridge | Enable Aerobridge before starting the session; assign an operation UUID. | Session batches also become operation flight logs. | +| Display restrictions in a GCS | Run the adapter bridge and install the custom QGC or Mission Planner integration. | Cached Flight Blender restriction boundaries appear on the native map. Stock QGC forwarding alone does not add overlays. | + +An active session suppresses legacy live delivery for its system. Turning off session live delivery does not stop recording or archive uploads. Pausing connections stops reception, periodic synchronization, and the map listener; **Synchronize now** remains an explicit way to attempt uploads while paused. + +## Before recording + +1. Obtain an existing declaration and the server permissions for the intended workflow. For archives, have the administrator deploy the companion Flight Blender session API and apply `alembic upgrade head`, including `a218_session_records`. +2. Verify **Overview** shows incoming vehicle telemetry and the intended MAVLink system IDs. Each aircraft must have a distinct system ID; the adapter cannot distinguish aircraft sharing one ID. +3. Use system filter `0` for concurrent aircraft. Confirm the correct declaration, aircraft identifier, operator and pilot details for each assignment. If Aerobridge is enabled, select each operation UUID before starting. +4. Check writable local storage and available disk space. Recording and queues have no automatic retention or size limit. +5. Start a session for each aircraft before the flight. Verify its sample count increases. Starting a session does not require a currently connected aircraft, so a successful start alone does not prove recording is receiving data. +6. If live delivery is required, enable the appropriate switch and check accepted updates. A recent heartbeat and 3D GPS fix, speed, course, and nonzero MAVLink 2 ellipsoid altitude are required. A live vehicle card or MSL altitude alone is insufficient. + +The adapter records received `GPS_RAW_INT` messages during active sessions, including samples with missing fix/position values. Timestamps reflect reception on the adapter computer, so keep its clock correct. It does not infer takeoff, landing, or flight completion from armed state. + +## After recording + +1. End each session explicitly. Pausing, closing the window, quitting, and restarting do not end sessions. +2. Use **Synchronize now** and inspect the session's Flight Blender and Aerobridge statuses, along with Records/Aerobridge status messages. The success toast means the attempt completed; individual services can still be unavailable or queues pending. +3. Retry until the required destinations show synchronized. A live accepted count confirms live API acceptance only; it does not confirm archive synchronization. +4. Select **Export** for an ended session. The app reports a JSON file path. Reports contain assignment, GPS samples, observed declaration history, and session audit events. Protect pilot details and positions when sharing them. +5. Back up the data directory after quitting. Exports are independent snapshots; a later export may include additional observed declaration history or updated delivery metadata. + +The UI shows the latest 100 sessions. Older records remain in SQLite and are still considered by synchronization, but there is no full-history browser or bulk export UI. Export reports while sessions are accessible and retain protected backups. + +## Outages, restarts, and server changes + +Recording continues during server outages while telemetry and local storage remain available. Exact pending archive batches and separate destination cursors are persisted before upload. Sessions survive a restart; reconnecting resumes recording into active assignments. Samples missed while the app is stopped, paused, or disconnected cannot be reconstructed. + +Synchronization runs in background passes, with up to 200 samples per session per destination in each pass. Each pass is followed by a ten-second wait; HTTP requests, backlog, and time budgets can extend catch-up time. Large queues may require many passes. Ending a session also produces metadata that must synchronize. + +Sessions retain their original Flight Blender URL and, if enabled when created, Aerobridge URL. Changing servers does not reroute existing records. Select the original URL again to resume those deliveries, and use the original Flight Blender service identity. Enabling Aerobridge later does not attach earlier sessions to it; disabling it retains pending work. Avoid running cloned datasets concurrently: Aerobridge reconciliation assumes one adapter owns a session. + +If storage fails, inspect the Records status immediately and resolve disk space or permissions before relying on further recording. Keep the database intact when reporting a problem. Do not clear pending-delivery rows to force a retry; doing so removes the persisted retry contract. + +## Native map integrations + +QGroundControl users need the OpenUTM custom QGC package for overlays. Install it in place of stock QGC: it uses the same application/settings name. Mission Planner users can install the compiled release plugin ZIP or the adapter-generated source plugin. Follow the [plugin installation guide](../integrations/mission-planner/README.md); install one copy only. The generated source supports custom forwarding and map addresses, while the release DLL uses defaults. + +With connections running, these URLs help diagnose the bridge: + +```sh +curl --fail http://127.0.0.1:14552/v1/health +curl --fail http://127.0.0.1:14552/v1/restrictions +``` + +Health confirms the local listener, not the upstream server or telemetry. Restrictions return a GeoJSON FeatureCollection with `fetchedAt` and `stale`. An empty, stale collection can mean no cache has been obtained. Data older than two minutes is stale; failed refreshes retain the last cache. A paused adapter does not serve these URLs, and the GCS integration retains its own last displayed boundaries with a warning. + +Overlays fill Polygon/MultiPolygon exteriors, including holes. They do not enforce altitude, validity windows, airspace compliance, or authorization. Read the published restriction details separately. The unauthenticated bridge binds only to loopback and exposes restrictions, not tokens, pilot details, or flight samples; other local processes can read it. + +## Backup, restore, upgrade, and removal + +Quit the adapter before copying its data directory so SQLite has no active writer. Copy `product.json`, `records.sqlite`, and `exports/` together to protected storage. Back up managed GCS settings separately if needed. Credentials are in the OS credential store and are not included in this copy. Local records are not encrypted or cryptographically tamper-proof. + +To restore, quit the adapter, preserve the current directory as a backup, and restore the saved files to the intended configuration directory. Restore normal owner-only access permissions. Start with the same configuration path where possible; re-enter credentials if needed. Verify active sessions and queues before connecting aircraft. Do not combine databases by copying individual rows or run the restored copy alongside the original. + +To upgrade, end flights, synchronize, quit, back up, then replace the application using the package for your OS. Check the [compatibility matrix](../COMPATIBILITY.md) and server requirements before starting it. A database with a newer schema version is rejected; retain backups for rollback rather than opening an upgraded database with an older binary. The app does not install updates automatically. + +To remove the adapter, first undo managed GCS forwarding/plugins using the [rollback instructions](../README.md#settings-credentials-and-undoing-setup), remove saved credentials if appropriate, quit, and uninstall/delete the app. The in-app plugin removal handles only the managed source file; manually remove a release DLL. Retain or securely remove the separate data directory according to your record-retention needs. Uninstalling the executable does not itself erase flight records. + +## Diagnose a problem + +| Symptom | Interpretation and next action | +| --- | --- | +| Parse errors but valid telemetry continues | Read the latest diagnostic. Invalid magic often means console text on a MAVLink feed; skipped bytes can mean leftover text or multiple frames in a UDP datagram; checksum errors mean a rejected frame. Use a clean MAVLink output with one frame per UDP datagram. For SITL, use the [MAVProxy procedure](../DEVELOPING.md#test-without-a-vehicle). Reconnect resets diagnostics. | +| Frame count increases but no aircraft appears | Frames may belong to GCS/component IDs that are ignored or a filtered system. Check component 1 and the system filter. | +| Aircraft looks live but cannot deliver | Vehicle freshness and GPS readiness are separate. Check heartbeat/GPS ages, fix, ellipsoid altitude, speed/course, and the selected assignment. | +| Live delivery stopped after starting a session | Active sessions take precedence over legacy mode. Enable session delivery or end the session as appropriate. | +| Archive returns 404 | Check the base URL and companion server API deployment/migration. Existing live telemetry support does not imply archive support. | +| Archive returns 401/403 or disappears for new credentials | Check scopes and the original issuer/subject identity. Read-access success does not prove write access. | +| Synchronize completes but session remains queued | Inspect destination errors, original URL binding, enabled Aerobridge, and backlog; repeat after correcting the cause. | +| Map is empty/stale | Check connections running, bridge address, cached restrictions, server access, and whether the custom GCS integration is installed. Check both bridge URLs above. | +| Cannot open database after downgrade | Restore the matching pre-upgrade backup or use the matching application version. Do not change SQLite's schema-version field manually. | + +For other connection/install issues, see [README troubleshooting](../README.md#troubleshooting). When filing an issue, include OS, app/GCS versions, source/transport, relevant statuses, and reproduction steps. Redact tokens, contact details, positions, registry data, and URLs containing private infrastructure names. Do not attach the full database by default. diff --git a/docs/SESSION_ARCHIVE.md b/docs/SESSION_ARCHIVE.md new file mode 100644 index 0000000..b8d8bb8 --- /dev/null +++ b/docs/SESSION_ARCHIVE.md @@ -0,0 +1,117 @@ +# Shared session archive API + +The Go UTM adapter and DJI Android GCS use one vendor-neutral Flight Blender archive API. Its payload version is `session.schemaVersion: 2`. This is an OpenUTM application contract, not a new ASTM standard. Live traffic continues through `/flight_stream/set_telemetry`; its receipt window and processing acknowledgement do not establish durable historical storage. + +## Deploy and use + +Deploy Flight Blender's adapter router and apply `alembic upgrade head` (including `a218_session_records`) before upgrading clients. Configure the server base URL and a bearer identity with `flightblender.write` for uploads and `flightblender.read` for retrieval. Retain the same token issuer and subject when renewing credentials. HTTPS is required outside local development. + +Go: start a flight session with its existing declaration, aircraft, operator and pilot. Local recording and automatic archive synchronization run independently of the live-delivery switches. End the session to publish its final count. Keep the application running or use Synchronize now until the archive is acknowledged. Local SQLite sequence numbers and exported reports remain version 1; the HTTP boundary converts samples to the common zero-based archive sequence. + +DJI: import device setup, select the pilot and aircraft, plan and fly normally, then Finish (or Cancel before takeoff). Device journals retain samples during outages. Uploads continue after the operation closes, after the live receipt window expires, and after the app restarts. A closed operation with an outstanding archive displays an uploading status. Keep local records until archive completion; an operation being Ended does not itself acknowledge the archive. Battery changes within an unfinished flight keep the same archive. Starting another flight creates a new archive UUID. + +DJI admins may explicitly provision `pilots` entries with `id`, `name`, and optional `license` and `contact`. For example, `{"id":"pilot-1","name":"Test Pilot","license":"TEST-123","contact":"pilot@example.test"}`. Only include personal information permitted by the operator's data-sharing agreement. Omitted fields remain absent/null, including for older journals; the clients never invent licence numbers or contacts. A stored archive with missing pilot metadata is not evidence that every operational or legal reporting requirement has been met. + +## Request and receipt + +`PUT /adapter_ops/sessions/{archiveUUID}` accepts at most 200 ordered samples. The path UUID must equal `session.id`; it identifies a recording, while `assignment.declarationID` identifies the existing declaration. Multiple recordings can reference one declaration. + +```json +{ + "session": { + "schemaVersion": 2, + "id": "f1807e86-8faa-4801-b143-88c380f66648", + "source": "dji", + "sourceData": {"registrationID": "TEST-REG"}, + "assignment": { + "declarationID": "4895d690-46d6-4d0d-9ceb-38f0dfc7c788", + "vehicleID": "TEST-SERIAL", + "operatorID": "TEST-OPERATOR", + "pilot": {"id": "pilot-1", "name": "Test Pilot"} + }, + "startedAt": "2026-10-25T10:00:00Z", + "endedAt": "2026-10-25T10:00:01Z", + "revision": 2, + "finalSampleCount": 1 + }, + "samples": [{ + "sequence": 0, + "recordedAt": "2026-10-25T10:00:00Z", + "position": {"latitude": 47.5, "longitude": 19.0, "relativeAltitude": 30}, + "sourceData": {"gps": 5} + }] +} +``` + +Common fields use degrees, metres, metres/second and UTC timestamps with offsets. Position requires latitude/longitude; optional fields are `relativeAltitude`, `altitudeWGS84`, `altitudeSource`, `altitudeSigma`, `velocityNorth`, `velocityEast`, `velocityDown`, `speed`, `heading`, `flying`, `motorsOn`, `satellites`. Unknown values remain absent/null. Heading is clockwise degrees in [0,360). Altitude WGS84 is ellipsoidal height, not MSL. DJI GPS signal level remains source evidence and is never treated as a MAVLink fix type. + +`sourceData` preserves vendor-specific evidence without new endpoints. Go uses source `mavlink` and preserves the original Vehicle plus system ID, configured URLs and Aerobridge operation ID. DJI uses source `dji` and preserves TelemetrySample, including DJI GPS level, flight mode and available pilot coordinates. These source fields are evidence; they are not an authentication or hardware-attestation mechanism. + +A successful PUT returns HTTP 200, after the database transaction commits: + +```json +{"id":"f1807e86-8faa-4801-b143-88c380f66648","revision":2,"archive":{"receivedSampleCount":1,"finalSampleCount":1,"complete":true}} +``` + +While recording, send `endedAt: null`, `finalSampleCount: null`. Closing publishes both an end time and the final count, using a higher revision. An empty flight closes with count 0 and no samples. Completion requires precisely the unique sequences 0 through count−1. Samples may arrive in separate batches with gaps; the archive remains incomplete until every sequence is stored. Time must be within the session's start/end, independently of when the server receives it. The declaration must exist, but may already have ended or expired. + +Assignments, source metadata, start time and sample contents are immutable. Reusing a sequence with different evidence is a conflict. Identical retries are accepted without duplicates. A stale metadata revision cannot reopen a closed session. Closed metadata cannot change apart from a revision increase carrying identical values. A final count cannot truncate samples already stored. + +Persist the exact outbox payload before sending. Advance the archive cursor only after a valid receipt for this UUID; final acknowledgement must report completion and the expected count. Retain and replay the outbox when a response is lost, including across process restarts. Archive and live cursors must remain independent. + +## Retrieval, failures and ownership + +`GET /adapter_ops/sessions/{UUID}?after=-1&limit=200` returns `session`, `samples`, and `archive`. Continue with `after` equal to the last sequence received until a short page. The default cursor is −1 to include sequence 0. Each page is a current view, not a frozen snapshot of an active recording. + +| Status | Meaning and recovery | +|---|---| +| 200, incomplete | Batch stored; upload remaining samples and closing metadata. | +| 401 / 403 | Check credentials/scopes; writes to another identity's archive are forbidden. | +| 404 | Archive unavailable to this identity, or endpoint missing on an old deployment. | +| 409 | Missing declaration, changed assignment/evidence, invalid closing bounds, or concurrent insert conflict. Retry identical payloads for concurrency; inspect persistent conflicts without discarding local data. | +| 422 | Invalid UUID, timestamps, values, sequence order or batch size. Correct the producer. | +| Network / 5xx | Keep outbox and retry with backoff. | + +Archive ownership is the JWT `(iss, sub)` pair. Reads hide another owner's archive with 404; writes return 403. The declaration existence check does not prove ownership of the declaration: the declaration model currently has no matching authenticated owner field. Deployment access and credential provisioning must account for that limitation. Pilot attribution is separate from the device/service identity. + +## Architecture and compatibility + +```mermaid +flowchart LR + MAV[MAVLink aircraft] --> GO[Go adapter] + DJI[DJI SDK aircraft] --> AND[Android core] + GO --> SQL[(Local SQLite + outbox)] + AND --> JOURNAL[(CRC journal + archive outbox)] + SQL --> COMMON[Shared schemaVersion 2 archive API] + JOURNAL --> COMMON + COMMON --> AUTH[JWT scopes and archive owner] + AUTH --> TX[Validate immutable metadata and sequences] + TX --> DB[(Flight Blender archive tables)] + DB --> RECEIPT[Committed count and completion receipt] + RECEIPT --> SQL + RECEIPT --> JOURNAL + GO -. fresh traffic .-> LIVE[Live telemetry API] + AND -. fresh traffic .-> LIVE +``` + +Flight Blender keeps HTTP validation in `schemas/adapter.py`, transaction wiring in `api/routers/adapter_api.py`, service access in `services/adapter_svc.py` and persistence in `repositories/adapter_repo.py`. The router uses a function-scoped database dependency so commit failure cannot produce a successful receipt. Existing session/sample tables store the common JSON without vendor-specific columns; sample uniqueness is enforced by `(session_id, sequence)`. + +The old Go wire payload remains accepted on this same endpoint for existing clients. An archive UUID cannot switch payload protocols: finish existing legacy server archives with the legacy client before upgrading. New Go and Android archives use version 2. Local Go reports and Aerobridge payloads keep their existing formats. Old Android journals remain readable; pending archives are reconstructed from their recorded metadata and available samples, with missing personal information left absent. There is no silent fallback from a failed archive upload to live telemetry. + +## Validation and CI + +Flight Blender: `uv run pytest tests/test_adapter_fastapi.py tests/test_import_architecture.py` covers scope enforcement, ownership, migration, both source types, deduplication, stale retries, bounds, gaps and final counts. The full Python suite remains applicable. + +Go: `go test -race ./internal/...` includes multi-batch conversion across restart, exact retry bytes, and final receipt validation. Android: `./gradlew :core:test :adapter-fake:test` exercises journal recovery and expired-window archival without Android tooling. Android app assembly additionally needs JDK 17 and the Android SDK; the repository's existing Android CI builds the sim app. + +Run both clients against the actual FastAPI application from the Go checkout: + +```sh +FLIGHT_BLENDER_ROOT=../flight-blender \ +DJI_ANDROID_ROOT=../dji-android-gcs \ +scripts/test-flight-blender.sh +``` + +Install Flight Blender's locked dependencies first (`uv sync --locked`), put Go on PATH and configure JDK 17 in JAVA_HOME. The harness creates a temporary SQLite database, seeds an expired declaration, listens on an ephemeral loopback port, and removes it afterward. It runs real Go HTTP workflows and Android's `ArchiveContractIntegrationTest`. Android uploads over 400 historical samples, loses a response after server commit, reopens the journal, retries identical bytes, and verifies every sequence through paginated HTTP reads. It does not require Redis, Celery, a drone or credentials to external services. Its authentication bypass is confined to that temporary server. + +The Go Tests workflow exposes manual Flight Blender and optional DJI ref inputs for companion branches until the server/client changes are merged. Supply both refs to run both clients. A missing or incompatible requested server/client fails the job; the Android contract test is skipped in ordinary JVM runs only when no integration harness URL was supplied. CI configuration and local tests do not constitute physical aircraft qualification or a published release. diff --git a/frontend/bindings/encoding/json/index.ts b/frontend/bindings/encoding/json/index.ts new file mode 100644 index 0000000..0533417 --- /dev/null +++ b/frontend/bindings/encoding/json/index.ts @@ -0,0 +1,6 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export type { + RawMessage +} from "./models.js"; diff --git a/frontend/bindings/encoding/json/models.ts b/frontend/bindings/encoding/json/models.ts new file mode 100644 index 0000000..9fcec0d --- /dev/null +++ b/frontend/bindings/encoding/json/models.ts @@ -0,0 +1,9 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * RawMessage is a raw encoded JSON value. + * It implements [Marshaler] and [Unmarshaler] and can + * be used to delay JSON decoding or precompute a JSON encoding. + */ +export type RawMessage = any; diff --git a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/index.ts b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/index.ts index 6ff5e96..a133876 100644 --- a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/index.ts +++ b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/index.ts @@ -8,9 +8,16 @@ export { export type { Activity, + Assignment, Config, + Document, + Fleet, GCS, Link, + Pilot, + RecordSummary, + RegistryEntity, + Session, Snapshot, Vehicle } from "./models.js"; diff --git a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/models.ts b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/models.ts index 19b7582..9fce172 100644 --- a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/models.ts +++ b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/models.ts @@ -1,12 +1,31 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as json$0 from "../../../../../encoding/json/models.js"; + export interface Activity { "at": string; "message": string; } +export interface Assignment { + "systemID": number; + "declarationID": string; + "vehicleID": string; + "operatorID": string; + "pilot": Pilot; + "aerobridgeOperationID": string; +} + export interface Config { + "sessionDelivery": boolean; + "aerobridgeEnabled": boolean; + "aerobridgeURL": string; + "aerobridgeToken"?: string; + "hasAerobridgeToken": boolean; + "bridgeAddress": string; "baseURL": string; "authMode": string; "token"?: string; @@ -30,6 +49,20 @@ export interface Config { "hasClientSecret": boolean; } +export interface Document { + "id": string; + "kind": string; + "server": string; + "capturedAt": string; + "data": json$0.RawMessage; +} + +export interface Fleet { + "aircraft": RegistryEntity[] | null; + "pilots": RegistryEntity[] | null; + "operations": RegistryEntity[] | null; +} + export interface GCS { "id": string; "name": string; @@ -49,7 +82,42 @@ export interface Link { "checkedAt": string; } +export interface Pilot { + "id": string; + "name": string; + "license": string; + "contact": string; +} + +export interface RecordSummary { + "session": Session; + "samples": number; + "blenderPending": boolean; + "aerobridgePending": boolean; +} + +export interface RegistryEntity { + "id": string; + "data": json$0.RawMessage; +} + +export interface Session { + "id": string; + "assignment": Assignment; + "startedAt": string; + "endedAt": string | null; + "blenderURL": string; + "aerobridgeURL": string; + "revision": number; +} + export interface Snapshot { + "version": string; + "aerobridge": Link; + "records": Link; + "restrictions": Link; + "sessions": RecordSummary[] | null; + "bridgeURL": string; "config": Config; "configPath": string; "running": boolean; @@ -62,6 +130,7 @@ export interface Snapshot { "events": Activity[] | null; "frames": number; "parseErrors": number; + "lastParseError": string; "sent": number; "now": string; } diff --git a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/service.ts b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/service.ts index df8fb43..ebf1f5b 100644 --- a/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/service.ts +++ b/frontend/bindings/github.com/openutm/utm-adapter/internal/adapter/service.ts @@ -13,10 +13,40 @@ export function ApplyConfig(c: $models.Config): $CancellablePromise { return $Call.ByID(2974556148, c); } +export function BeginSession(a: $models.Assignment): $CancellablePromise<$models.Session> { + return $Call.ByID(3265027169, a); +} + export function ClearCredentials(): $CancellablePromise { return $Call.ByID(682087843); } +/** + * Close is for process shutdown. Stop only pauses connections. + */ +export function Close(): $CancellablePromise { + return $Call.ByID(682221518); +} + +export function EndSession(id: string): $CancellablePromise { + return $Call.ByID(1813009769, id); +} + +/** + * Export is streamed from SQLite, so a long flight does not have to fit in RAM. + */ +export function ExportSession(id: string): $CancellablePromise { + return $Call.ByID(4174278844, id); +} + +export function GetDeclarations(): $CancellablePromise<$models.Document[] | null> { + return $Call.ByID(2212004943); +} + +export function GetFleet(): $CancellablePromise<$models.Fleet> { + return $Call.ByID(1940079994); +} + export function GetSnapshot(): $CancellablePromise<$models.Snapshot> { return $Call.ByID(3543970594); } @@ -35,3 +65,10 @@ export function Start(): $CancellablePromise { export function Stop(): $CancellablePromise { return $Call.ByID(1890959658); } + +/** + * Synchronize retries durable deliveries without starting a telemetry receiver. + */ +export function Synchronize(): $CancellablePromise { + return $Call.ByID(3073840824); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5e16834..93a2179 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "utm-adapter-ui", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "utm-adapter-ui", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@wailsio/runtime": "3.0.0-beta.34" }, diff --git a/frontend/package.json b/frontend/package.json index 32feb8a..2986570 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "utm-adapter-ui", "private": true, - "version": "0.1.0", + "version": "0.2.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index c10e5c6..40df258 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,11 +1,16 @@