diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 54208213a..0fcc4c194 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -137,239 +137,13 @@ jobs:
republish: "true"
file: "*.deb"
windows:
- runs-on: windows-latest
-
- defaults:
- run:
- working-directory: src
-
- steps:
- - uses: actions/checkout@v5
-
- - name: Install Qt
- id: qt-install
- uses: jurplel/install-qt-action@v4
- with:
- version: '5.15.2'
- aqtversion: '==3.1.*'
- host: windows
- target: desktop
- arch: win32_mingw81
- setup-python: false
- tools: tools_cmake tools_mingw81,qt.tools.win32_mingw810
-
- - name: Download and Extract OpenSSL
- shell: pwsh
- working-directory: ${{ github.workspace }}
- run: |
- Invoke-WebRequest `
- -Uri "https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1w.zip" `
- -OutFile openssl-1.1.1w.zip
- Expand-Archive openssl-1.1.1w.zip -DestinationPath openssl111
-
- - name: Copy OpenSSL to Qt MinGW path
- shell: pwsh
- working-directory: ${{ github.workspace }}
- run: |
- dir ${{ github.workspace }}\..\Qt\
- dir ${{ github.workspace }}\..\Qt\Tools\
- dir ${{ github.workspace }}\..\Qt\Tools\mingw810_32\
-
- $openssl = (Get-ChildItem -Directory -Path "${{ github.workspace }}\openssl111" | Where-Object { $_.Name -like "openssl-*" } | Select-Object -First 1).FullName + "\x86"
- $mingwRoot = "${{ github.workspace }}\..\Qt\Tools\mingw810_32"
-
- Write-Host "Resolved OpenSSL path: $openssl"
- if (Test-Path $openssl) {
- Get-ChildItem -Recurse "$openssl" | ForEach-Object { Write-Host $_.FullName }
- } else {
- Write-Error "OpenSSL path not found: $openssl"
- }
-
- Copy-Item "$openssl\bin\libcrypto-1_1.dll" "$mingwRoot\bin\" -Force
- Copy-Item "$openssl\bin\libssl-1_1.dll" "$mingwRoot\bin\" -Force
- Copy-Item "$openssl\lib\libcrypto.lib" "$mingwRoot\lib\" -Force
- Copy-Item "$openssl\lib\libssl.lib" "$mingwRoot\lib\" -Force
- New-Item -ItemType Directory -Force -Path "$mingwRoot\include\openssl"
- Write-Host "Copying OpenSSL headers from: $openssl\include\openssl\."
- Copy-Item "$openssl\include\openssl\." "$mingwRoot\include\" -Recurse -Force -Verbose
- dir "$mingwRoot\include\openssl\"
-
- - name: Install NSIS
- run: choco install nsis --x86 --force -y
-
- - name: Show Qt & CMake Info
- run: |
- where gcc
- where g++
- cmake --version
-
- - name: Debug OpenSSL include headers
- shell: pwsh
- working-directory: ${{ github.workspace }}
- run: |
- $opensslInclude = "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\include\openssl"
- $openssl = (Get-ChildItem -Directory -Path "${{ github.workspace }}\openssl111" | Where-Object { $_.Name -like "openssl-*" } | Select-Object -First 1).FullName + "\x86"
-
- Write-Host "Looking in: $opensslInclude"
- if (Test-Path $opensslInclude) {
- Get-ChildItem $opensslInclude | Select-Object -First 20
- } else {
- Write-Error "OpenSSL include directory not found!"
- }
-
- dir "$openssl\include\openssl\"
-
-
- - name: Configure CMake
- shell: cmd
- working-directory: src
- run: |
- dir "%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\include"
- mkdir build
- cd build
- cmake .. -G "MinGW Makefiles" ^
- -DCMAKE_BUILD_TYPE=Release ^
- -DCMAKE_C_COMPILER="%GITHUB_WORKSPACE%\..\Qt\Tools\mingw810_32\bin\gcc.exe" ^
- -DCMAKE_CXX_COMPILER="%GITHUB_WORKSPACE%\..\Qt\Tools\mingw810_32\bin\g++.exe" ^
- -DOPENSSL_INCLUDE_DIR="%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\include" ^
- -DOPENSSL_LIBRARIES="%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\lib"
-
-
- - name: Check CMake Cache
- run: |
- dir build
- type build\CMakeCache.txt || echo "Cache missing"
-
- - name: Build
- shell: pwsh
- run: |
- $env:PATH = "$Env:GITHUB_WORKSPACE\..\Qt\5.15.2\mingw81_32\bin;$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin;$env:PATH"
- cd build
- cmake --build . --verbose
-
- - name: Copy OpenSSL DLLs to build folder
- shell: pwsh
- run: |
- Copy-Item "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin\libssl-1_1.dll" -Destination "$Env:GITHUB_WORKSPACE\src\build\"
- Copy-Item "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin\libcrypto-1_1.dll" -Destination "$Env:GITHUB_WORKSPACE\src\build\"
-
- - name: Validate Windows signing configuration
- shell: pwsh
- working-directory: ${{ github.workspace }}
- env:
- WINDOWS_SIGNING_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
- WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
- run: |
- $hasCertificate = -not [string]::IsNullOrWhiteSpace($Env:WINDOWS_SIGNING_CERTIFICATE_BASE64)
- $hasPassword = -not [string]::IsNullOrWhiteSpace($Env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD)
- if ($hasCertificate -xor $hasPassword) {
- throw "Configure both Windows signing secrets or neither of them"
- }
- if (-not $hasCertificate) {
- Write-Warning "Windows signing secrets are not configured; this CI artifact will be unsigned"
- }
-
- # Sign the executable before NSIS packages it. The two repository secrets
- # are intentionally optional so pull requests and forks can still build.
- - name: Sign Windows application
- shell: pwsh
- working-directory: ${{ github.workspace }}
- env:
- WINDOWS_SIGNING_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
- WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
- run: |
- if ([string]::IsNullOrWhiteSpace($Env:WINDOWS_SIGNING_CERTIFICATE_BASE64)) {
- Write-Host "Windows signing is not configured; skipping application signing"
- exit 0
- }
-
- $certificatePath = Join-Path $Env:RUNNER_TEMP "openhd-imagewriter-signing.pfx"
- [IO.File]::WriteAllBytes(
- $certificatePath,
- [Convert]::FromBase64String($Env:WINDOWS_SIGNING_CERTIFICATE_BASE64)
- )
-
- $signTool = Get-ChildItem "${Env:ProgramFiles(x86)}\Windows Kits\10\bin" `
- -Filter signtool.exe -Recurse |
- Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
- Sort-Object FullName -Descending |
- Select-Object -First 1 -ExpandProperty FullName
- if (-not $signTool) {
- throw "signtool.exe was not found"
- }
-
- & $signTool sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com `
- /f $certificatePath /p $Env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD `
- /d "OpenHD ImageWriter" /du "https://openhd.tech" `
- "src\build\deploy\OpenHDImageWriter.exe"
- if ($LASTEXITCODE -ne 0) {
- throw "Signing OpenHDImageWriter.exe failed with exit code $LASTEXITCODE"
- }
- & $signTool verify /pa /v "src\build\deploy\OpenHDImageWriter.exe"
- if ($LASTEXITCODE -ne 0) {
- throw "Signature verification for OpenHDImageWriter.exe failed with exit code $LASTEXITCODE"
- }
-
- - name: Build NSIS Installer
- shell: pwsh
- working-directory: src\build
- run: |
- & "${Env:ProgramFiles(x86)}\NSIS\makensis.exe" "openhdimagewriter.nsi"
-
- - name: Sign Windows installer
- shell: pwsh
- working-directory: ${{ github.workspace }}
- env:
- WINDOWS_SIGNING_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }}
- WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }}
- run: |
- if ([string]::IsNullOrWhiteSpace($Env:WINDOWS_SIGNING_CERTIFICATE_BASE64)) {
- Write-Host "Windows signing is not configured; skipping installer signing"
- exit 0
- }
-
- $certificatePath = Join-Path $Env:RUNNER_TEMP "openhd-imagewriter-signing.pfx"
- $signTool = Get-ChildItem "${Env:ProgramFiles(x86)}\Windows Kits\10\bin" `
- -Filter signtool.exe -Recurse |
- Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
- Sort-Object FullName -Descending |
- Select-Object -First 1 -ExpandProperty FullName
- if (-not $signTool) {
- throw "signtool.exe was not found"
- }
-
- $installer = Get-ChildItem "src\build\OpenHD-ImageWriter-*.exe" |
- Select-Object -First 1
- if (-not $installer) {
- throw "NSIS installer was not found"
- }
-
- & $signTool sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com `
- /f $certificatePath /p $Env:WINDOWS_SIGNING_CERTIFICATE_PASSWORD `
- /d "OpenHD ImageWriter" /du "https://openhd.tech" `
- $installer.FullName
- if ($LASTEXITCODE -ne 0) {
- throw "Signing the NSIS installer failed with exit code $LASTEXITCODE"
- }
- & $signTool verify /pa /v $installer.FullName
- if ($LASTEXITCODE -ne 0) {
- throw "Signature verification for the NSIS installer failed with exit code $LASTEXITCODE"
- }
-
- - name: Remove temporary signing certificate
- if: always()
- shell: pwsh
- working-directory: ${{ github.workspace }}
- run: |
- Remove-Item -LiteralPath (Join-Path $Env:RUNNER_TEMP "openhd-imagewriter-signing.pfx") `
- -Force -ErrorAction SilentlyContinue
-
- - name: Upload Installer
- uses: actions/upload-artifact@v7
- with:
- name: OpenHDImageWriter-Build-Full
- path: src/build/OpenHD-ImageWriter-*
- if-no-files-found: error
+ permissions:
+ contents: read
+ actions: read
+ uses: ./.github/workflows/windows.yml
+ with:
+ source-repository: ${{ github.repository }}
+ source-ref: ${{ github.sha }}
macos:
name: Build (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
diff --git a/.github/workflows/sign-windows.yml b/.github/workflows/sign-windows.yml
new file mode 100644
index 000000000..ed3a3fae5
--- /dev/null
+++ b/.github/workflows/sign-windows.yml
@@ -0,0 +1,30 @@
+name: Sign Windows ImageWriter
+
+on:
+ workflow_dispatch:
+ inputs:
+ signing_mode:
+ description: Test for SignPath onboarding, release for public distribution
+ type: choice
+ options: [test, release]
+ default: test
+ required: true
+
+permissions:
+ contents: read
+ actions: read
+
+jobs:
+ imagewriter:
+ if: github.repository == 'OpenHD/OpenHD-ImageWriter'
+ uses: ./.github/workflows/windows.yml
+ with:
+ source-ref: ${{ github.sha }}
+ sign: true
+ signing-mode: ${{ inputs.signing_mode }}
+ organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
+ project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
+ signing-policy-slug: ${{ inputs.signing_mode == 'test' && vars.SIGNPATH_TEST_SIGNING_POLICY_SLUG || vars.SIGNPATH_RELEASE_SIGNING_POLICY_SLUG }}
+ test-certificate-base64: ${{ vars.SIGNPATH_TEST_CERTIFICATE_BASE64 }}
+ secrets:
+ SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 000000000..28f0b8ee0
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,339 @@
+name: Windows ImageWriter build and SignPath signing
+
+on:
+ workflow_call:
+ inputs:
+ source-repository:
+ description: Source repository; signed builds use the canonical ImageWriter repository
+ type: string
+ default: OpenHD/OpenHD-ImageWriter
+ source-ref:
+ description: Full OpenHD-ImageWriter source commit SHA
+ type: string
+ required: true
+ sign:
+ description: Sign using ImageWriter's approved SignPath project
+ type: boolean
+ default: false
+ signing-mode:
+ description: Test or release signing; test artifacts are not for distribution
+ type: string
+ default: release
+ test-certificate-base64:
+ description: Public self-signed onboarding certificate (DER, base64)
+ type: string
+ default: ''
+ organization-id:
+ type: string
+ default: ''
+ project-slug:
+ type: string
+ default: ''
+ signing-policy-slug:
+ type: string
+ default: ''
+ secrets:
+ SIGNPATH_API_TOKEN:
+ required: false
+
+permissions:
+ contents: read
+ actions: read
+
+env:
+ BUILD_TYPE: Release
+
+jobs:
+ windows:
+ runs-on: windows-latest
+
+ # Signing is requested only by the manual workflow in the ImageWriter origin.
+ # Pull requests and normal ImageWriter builds always remain unsigned.
+
+ defaults:
+ run:
+ working-directory: src
+
+ steps:
+ - name: Validate SignPath configuration
+ if: inputs.sign
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ env:
+ SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
+ SIGNPATH_ORGANIZATION_ID: ${{ inputs.organization-id }}
+ SIGNPATH_PROJECT_SLUG: ${{ inputs.project-slug }}
+ SIGNPATH_SIGNING_POLICY_SLUG: ${{ inputs.signing-policy-slug }}
+ SOURCE_REPOSITORY: ${{ inputs.source-repository }}
+ SOURCE_REF: ${{ inputs.source-ref }}
+ SIGNING_MODE: ${{ inputs.signing-mode }}
+ TEST_CERTIFICATE_BASE64: ${{ inputs.test-certificate-base64 }}
+ run: |
+ if ($Env:GITHUB_REPOSITORY -ne 'OpenHD/OpenHD-ImageWriter' -or $Env:GITHUB_EVENT_NAME -ne 'workflow_dispatch') {
+ throw "Signing must be requested by the manual ImageWriter workflow"
+ }
+ if ($Env:SOURCE_REF -ne $Env:GITHUB_SHA) {
+ throw 'Signed source must match the workflow revision for origin verification'
+ }
+ if ($Env:SIGNING_MODE -notin @('test', 'release')) {
+ throw 'signing-mode must be test or release'
+ }
+ if ($Env:SIGNING_MODE -eq 'test' -and [string]::IsNullOrWhiteSpace($Env:TEST_CERTIFICATE_BASE64)) {
+ throw 'Test signing requires the public SignPath test certificate'
+ }
+ if ($Env:SOURCE_REPOSITORY -ne 'OpenHD/OpenHD-ImageWriter') {
+ throw "Signed builds must use the canonical ImageWriter source repository"
+ }
+ foreach ($name in @('SIGNPATH_API_TOKEN', 'SIGNPATH_ORGANIZATION_ID', 'SIGNPATH_PROJECT_SLUG', 'SIGNPATH_SIGNING_POLICY_SLUG')) {
+ if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
+ throw "Missing required SignPath setting: $name"
+ }
+ }
+
+ - name: Validate source revision
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ env:
+ SOURCE_REF: ${{ inputs.source-ref }}
+ run: |
+ if ($Env:SOURCE_REF -notmatch '^[0-9a-fA-F]{40}$') {
+ throw "source-ref must be the full ImageWriter commit SHA"
+ }
+
+ - uses: actions/checkout@v5
+ with:
+ repository: ${{ inputs.source-repository }}
+ ref: ${{ inputs.source-ref }}
+ persist-credentials: false
+
+ - name: Trust the pinned onboarding certificate on this test runner
+ if: inputs.sign && inputs.signing-mode == 'test'
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ env:
+ TEST_CERTIFICATE_BASE64: ${{ inputs.test-certificate-base64 }}
+ run: |
+ & scripts/trust_signpath_test_certificate.ps1 -CertificateBase64 $Env:TEST_CERTIFICATE_BASE64
+
+ - name: Install Qt
+ id: qt-install
+ uses: jurplel/install-qt-action@v4
+ with:
+ version: '5.15.2'
+ aqtversion: '==3.1.*'
+ host: windows
+ target: desktop
+ arch: win32_mingw81
+ setup-python: false
+ tools: tools_cmake tools_mingw81,qt.tools.win32_mingw810
+
+ - name: Download and Extract OpenSSL
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ run: |
+ Invoke-WebRequest `
+ -Uri "https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1w.zip" `
+ -OutFile openssl-1.1.1w.zip
+ Expand-Archive openssl-1.1.1w.zip -DestinationPath openssl111
+
+ - name: Copy OpenSSL to Qt MinGW path
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ run: |
+ dir ${{ github.workspace }}\..\Qt\
+ dir ${{ github.workspace }}\..\Qt\Tools\
+ dir ${{ github.workspace }}\..\Qt\Tools\mingw810_32\
+
+ $openssl = (Get-ChildItem -Directory -Path "${{ github.workspace }}\openssl111" | Where-Object { $_.Name -like "openssl-*" } | Select-Object -First 1).FullName + "\x86"
+ $mingwRoot = "${{ github.workspace }}\..\Qt\Tools\mingw810_32"
+
+ Write-Host "Resolved OpenSSL path: $openssl"
+ if (Test-Path $openssl) {
+ Get-ChildItem -Recurse "$openssl" | ForEach-Object { Write-Host $_.FullName }
+ } else {
+ Write-Error "OpenSSL path not found: $openssl"
+ }
+
+ Copy-Item "$openssl\bin\libcrypto-1_1.dll" "$mingwRoot\bin\" -Force
+ Copy-Item "$openssl\bin\libssl-1_1.dll" "$mingwRoot\bin\" -Force
+ Copy-Item "$openssl\lib\libcrypto.lib" "$mingwRoot\lib\" -Force
+ Copy-Item "$openssl\lib\libssl.lib" "$mingwRoot\lib\" -Force
+ New-Item -ItemType Directory -Force -Path "$mingwRoot\include\openssl"
+ Write-Host "Copying OpenSSL headers from: $openssl\include\openssl\."
+ Copy-Item "$openssl\include\openssl\." "$mingwRoot\include\" -Recurse -Force -Verbose
+ dir "$mingwRoot\include\openssl\"
+
+ - name: Install NSIS
+ run: choco install nsis --x86 --force -y
+
+ - name: Show Qt & CMake Info
+ run: |
+ where gcc
+ where g++
+ cmake --version
+
+ - name: Debug OpenSSL include headers
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ run: |
+ $opensslInclude = "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\include\openssl"
+ $openssl = (Get-ChildItem -Directory -Path "${{ github.workspace }}\openssl111" | Where-Object { $_.Name -like "openssl-*" } | Select-Object -First 1).FullName + "\x86"
+
+ Write-Host "Looking in: $opensslInclude"
+ if (Test-Path $opensslInclude) {
+ Get-ChildItem $opensslInclude | Select-Object -First 20
+ } else {
+ Write-Error "OpenSSL include directory not found!"
+ }
+
+ dir "$openssl\include\openssl\"
+
+
+ - name: Configure CMake
+ shell: cmd
+ working-directory: src
+ run: |
+ dir "%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\include"
+ mkdir build
+ cd build
+ cmake .. -G "MinGW Makefiles" ^
+ -DCMAKE_BUILD_TYPE=Release ^
+ -DCMAKE_C_COMPILER="%GITHUB_WORKSPACE%\..\Qt\Tools\mingw810_32\bin\gcc.exe" ^
+ -DCMAKE_CXX_COMPILER="%GITHUB_WORKSPACE%\..\Qt\Tools\mingw810_32\bin\g++.exe" ^
+ -DOPENSSL_INCLUDE_DIR="%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\include" ^
+ -DOPENSSL_LIBRARIES="%GITHUB_WORKSPACE%\openssl111\openssl-1.1\x86\lib"
+
+
+ - name: Check CMake Cache
+ run: |
+ dir build
+ type build\CMakeCache.txt || echo "Cache missing"
+
+ - name: Build
+ shell: pwsh
+ run: |
+ $env:PATH = "$Env:GITHUB_WORKSPACE\..\Qt\5.15.2\mingw81_32\bin;$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin;$env:PATH"
+ cd build
+ cmake --build . --verbose
+
+ - name: Copy OpenSSL DLLs to build folder
+ shell: pwsh
+ run: |
+ Copy-Item "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin\libssl-1_1.dll" -Destination "$Env:GITHUB_WORKSPACE\src\build\"
+ Copy-Item "$Env:GITHUB_WORKSPACE\..\Qt\Tools\mingw810_32\bin\libcrypto-1_1.dll" -Destination "$Env:GITHUB_WORKSPACE\src\build\"
+
+ - name: Prepare application and uninstaller for signing
+ if: inputs.sign
+ shell: pwsh
+ working-directory: src/build
+ run: |
+ $stage = Join-Path $Env:RUNNER_TEMP 'imagewriter-unsigned-application'
+ New-Item -ItemType Directory -Force -Path $stage | Out-Null
+ & "${Env:ProgramFiles(x86)}\NSIS\makensis.exe" /DINNER "/DUNINSTALLER_OUTPUT=$stage\uninstall.exe" openhdimagewriter.nsi
+ if ($LASTEXITCODE -ne 0) { throw 'Building the uninstaller generator failed' }
+ $generator = Start-Process -FilePath (Join-Path $PWD 'ImageWriter-uninstaller-generator.exe') -WindowStyle Hidden -Wait -PassThru
+ if ($generator.ExitCode -ne 0 -or -not (Test-Path -LiteralPath "$stage\uninstall.exe")) {
+ throw 'Generating the uninstaller failed'
+ }
+ Copy-Item deploy/OpenHDImageWriter.exe, deploy/fat32format.exe -Destination $stage
+
+ - name: Upload unsigned application
+ if: inputs.sign
+ id: unsigned-application
+ uses: actions/upload-artifact@v4
+ with:
+ name: ImageWriter-unsigned-application
+ path: ${{ runner.temp }}/imagewriter-unsigned-application/*
+ if-no-files-found: error
+ retention-days: 7
+
+ - name: Sign application, formatter and uninstaller with SignPath
+ if: inputs.sign
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ inputs.organization-id }}
+ project-slug: ${{ inputs.project-slug }}
+ signing-policy-slug: ${{ inputs.signing-policy-slug }}
+ artifact-configuration-slug: imagewriter-application
+ github-artifact-id: ${{ steps.unsigned-application.outputs.artifact-id }}
+ wait-for-completion: true
+ wait-for-completion-timeout-in-seconds: 1800
+ output-artifact-directory: ${{ runner.temp }}/imagewriter-signed-application
+
+ - name: Verify and stage signed application
+ if: inputs.sign
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ env:
+ SIGNING_MODE: ${{ inputs.signing-mode }}
+ run: |
+ foreach ($file in @('OpenHDImageWriter.exe', 'fat32format.exe', 'uninstall.exe')) {
+ $source = Join-Path $Env:RUNNER_TEMP "imagewriter-signed-application/$file"
+ & scripts/verify_windows_signature.ps1 -Path $source -SigningMode $Env:SIGNING_MODE -ExpectedSignerThumbprint $Env:SIGNPATH_TEST_CERTIFICATE_THUMBPRINT
+ Copy-Item -LiteralPath $source -Destination "src/build/deploy/$file" -Force
+ }
+
+ - name: Build NSIS installer
+ shell: pwsh
+ working-directory: src/build
+ env:
+ SIGN_BUILD: ${{ inputs.sign }}
+ run: |
+ $arguments = @('openhdimagewriter.nsi')
+ if ($Env:SIGN_BUILD -eq 'true') {
+ $arguments = @('/DSIGNED_UNINSTALLER=deploy\uninstall.exe') + $arguments
+ }
+ & "${Env:ProgramFiles(x86)}\NSIS\makensis.exe" @arguments
+ if ($LASTEXITCODE -ne 0) { throw 'Building the NSIS installer failed' }
+
+ - name: Upload unsigned installer
+ if: inputs.sign
+ id: unsigned-installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: ImageWriter-unsigned-installer
+ path: src/build/OpenHD-ImageWriter-*.exe
+ if-no-files-found: error
+ retention-days: 7
+
+ - name: Sign installer with SignPath
+ if: inputs.sign
+ uses: signpath/github-action-submit-signing-request@v3
+ with:
+ api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
+ organization-id: ${{ inputs.organization-id }}
+ project-slug: ${{ inputs.project-slug }}
+ signing-policy-slug: ${{ inputs.signing-policy-slug }}
+ artifact-configuration-slug: imagewriter-installer
+ github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }}
+ wait-for-completion: true
+ wait-for-completion-timeout-in-seconds: 1800
+ output-artifact-directory: ${{ runner.temp }}/imagewriter-signed-installer
+
+ - name: Verify and stage signed installer
+ if: inputs.sign
+ shell: pwsh
+ working-directory: ${{ github.workspace }}
+ env:
+ SIGNING_MODE: ${{ inputs.signing-mode }}
+ run: |
+ $installers = @(Get-ChildItem "$Env:RUNNER_TEMP/imagewriter-signed-installer/OpenHD-ImageWriter-*.exe")
+ if ($installers.Count -ne 1) { throw 'Expected exactly one signed installer' }
+ & scripts/verify_windows_signature.ps1 -Path $installers[0].FullName -SigningMode $Env:SIGNING_MODE -ExpectedSignerThumbprint $Env:SIGNPATH_TEST_CERTIFICATE_THUMBPRINT
+ Copy-Item -LiteralPath $installers[0].FullName -Destination src/build -Force
+
+ - name: Upload installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: OpenHDImageWriter-Build-Full${{ inputs.sign && (inputs.signing-mode == 'test' && '-Test-Signed' || '-Signed') || '-Unsigned' }}
+ path: src/build/OpenHD-ImageWriter-*.exe
+ if-no-files-found: error
+
+ - name: Upload portable application
+ uses: actions/upload-artifact@v4
+ with:
+ name: OpenHDImageWriter-Windows-Portable${{ inputs.sign && (inputs.signing-mode == 'test' && '-Test-Signed' || '-Signed') || '-Unsigned' }}
+ path: src/build/deploy/
+ if-no-files-found: error
+
diff --git a/doc/openhd_settings_map.json b/doc/openhd_settings_map.json
index 04a7cef8e..7a3fb816a 100644
--- a/doc/openhd_settings_map.json
+++ b/doc/openhd_settings_map.json
@@ -28,25 +28,82 @@
"sbc": {
"_comment": "Detected SBC marker files written as empty files.",
"options": [
- { "id": "rpi", "file": "openhd/rpi.txt" },
- { "id": "rock-5a", "file": "openhd/rock-5a.txt" },
- { "id": "rock-5b", "file": "openhd/rock-5b.txt" },
- { "id": "zero3w", "file": "openhd/zero3w.txt" },
- { "id": "radxa-cm3", "file": "openhd/rock-rk3566.txt" },
- { "id": "radxa-cm5", "file": "openhd/radxa-cm5.txt" },
- { "id": "x20", "file": "openhd/x20.txt" },
- { "id": "x21", "file": "openhd/x21.txt" },
- { "id": "cubie", "file": "openhd/cubie.txt" },
- { "id": "x86", "file": "openhd/x86.txt" },
- { "id": "qcs405", "file": "openhd/qcs405.txt" },
- { "id": "qrb5165", "file": "openhd/qrb5165.txt" },
- { "id": "openipc", "file": "openhd/openipc.txt" },
- { "id": "rockchip-rv", "file": "openhd/rockchip-rv.txt" },
- { "id": "orqa", "file": "openhd/orqa.txt" },
- { "id": "nvidia-xavier", "file": "openhd/nvidia-xavier.txt" },
- { "id": "uvx", "file": "openhd/uvx.txt" },
- { "id": "nxp-imx8", "file": "openhd/nxp-imx8.txt" },
- { "id": "unknown", "file": "openhd/unknown.txt" }
+ {
+ "id": "rpi",
+ "file": "openhd/rpi.txt"
+ },
+ {
+ "id": "rock-5a",
+ "file": "openhd/rock-5a.txt"
+ },
+ {
+ "id": "rock-5b",
+ "file": "openhd/rock-5b.txt"
+ },
+ {
+ "id": "zero3w",
+ "file": "openhd/zero3w.txt"
+ },
+ {
+ "id": "radxa-cm3",
+ "file": "openhd/rock-rk3566.txt"
+ },
+ {
+ "id": "radxa-cm5",
+ "file": "openhd/radxa-cm5.txt"
+ },
+ {
+ "id": "x20",
+ "file": "openhd/x20.txt"
+ },
+ {
+ "id": "x21",
+ "file": "openhd/x21.txt"
+ },
+ {
+ "id": "cubie",
+ "file": "openhd/cubie.txt"
+ },
+ {
+ "id": "x86",
+ "file": "openhd/x86.txt"
+ },
+ {
+ "id": "qcs405",
+ "file": "openhd/qcs405.txt"
+ },
+ {
+ "id": "qrb5165",
+ "file": "openhd/qrb5165.txt"
+ },
+ {
+ "id": "openipc",
+ "file": "openhd/openipc.txt"
+ },
+ {
+ "id": "rockchip-rv",
+ "file": "openhd/rockchip-rv.txt"
+ },
+ {
+ "id": "orqa",
+ "file": "openhd/orqa.txt"
+ },
+ {
+ "id": "nvidia-xavier",
+ "file": "openhd/nvidia-xavier.txt"
+ },
+ {
+ "id": "uvx",
+ "file": "openhd/uvx.txt"
+ },
+ {
+ "id": "nxp-imx8",
+ "file": "openhd/nxp-imx8.txt"
+ },
+ {
+ "id": "unknown",
+ "file": "openhd/unknown.txt"
+ }
]
},
"mode": {
@@ -63,28 +120,69 @@
"_comment": "Camera selection depends on SBC and vendor/category shown in the UI.",
"valueEncoding": "stringified OpenHD camera type integers written to openhd/settings.json",
"secondaryOptions": [
- { "id": "USB", "displayName": "USB", "valueWritten": "10" },
- { "id": "IP-CAMERA", "displayName": "IP CAMERA", "valueWritten": "3" },
- { "id": "TESTPATTERN", "displayName": "DEV CAMERA", "valueWritten": "0" },
- { "id": "INFIRAY", "displayName": "INFIRAY", "valueWritten": "11" },
- { "id": "INFIRAY_T2", "displayName": "INFIRAY T2", "valueWritten": "12" },
- { "id": "INFIRAY_X2", "displayName": "INFIRAY X2", "valueWritten": "13" },
- { "id": "INFIRAY_P2_PRO", "displayName": "INFIRAY P2 PRO", "valueWritten": "14" },
- { "id": "FLIR_VUE", "displayName": "FLIR VUE", "valueWritten": "15" },
- { "id": "FLIR_BOSON", "displayName": "FLIR BOSON", "valueWritten": "16" }
+ {
+ "id": "USB",
+ "displayName": "USB",
+ "valueWritten": "10"
+ },
+ {
+ "id": "IP-CAMERA",
+ "displayName": "IP CAMERA",
+ "valueWritten": "3"
+ },
+ {
+ "id": "TESTPATTERN",
+ "displayName": "DEV CAMERA",
+ "valueWritten": "0"
+ },
+ {
+ "id": "INFIRAY",
+ "displayName": "INFIRAY",
+ "valueWritten": "11"
+ },
+ {
+ "id": "INFIRAY_T2",
+ "displayName": "INFIRAY T2",
+ "valueWritten": "12"
+ },
+ {
+ "id": "INFIRAY_X2",
+ "displayName": "INFIRAY X2",
+ "valueWritten": "13"
+ },
+ {
+ "id": "INFIRAY_P2_PRO",
+ "displayName": "INFIRAY P2 PRO",
+ "valueWritten": "14"
+ },
+ {
+ "id": "FLIR_VUE",
+ "displayName": "FLIR VUE",
+ "valueWritten": "15"
+ },
+ {
+ "id": "FLIR_BOSON",
+ "displayName": "FLIR BOSON",
+ "valueWritten": "16"
+ }
],
"commonVendors": [
{
"id": "network",
"displayName": "Network",
"options": [
- { "id": "IP-CAMERA", "valueWritten": "3" }
+ {
+ "id": "IP-CAMERA",
+ "valueWritten": "3"
+ }
]
}
],
"sbcGroups": [
{
- "sbc": ["rpi"],
+ "sbc": [
+ "rpi"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"secondaryCsi": true,
@@ -93,52 +191,122 @@
"id": "raspberry",
"displayName": "Raspberry",
"options": [
- { "id": "HDMI", "valueWritten": "20" },
- { "id": "OV5647", "valueWritten": "30" },
- { "id": "IMX219", "valueWritten": "31" },
- { "id": "IMX708", "valueWritten": "32" },
- { "id": "IMX477", "valueWritten": "33" }
+ {
+ "id": "HDMI",
+ "valueWritten": "20"
+ },
+ {
+ "id": "OV5647",
+ "valueWritten": "30"
+ },
+ {
+ "id": "IMX219",
+ "valueWritten": "31"
+ },
+ {
+ "id": "IMX708",
+ "valueWritten": "32"
+ },
+ {
+ "id": "IMX477",
+ "valueWritten": "33"
+ }
]
},
{
"id": "arducam",
"displayName": "Arducam",
"options": [
- { "id": "SkyMasterHDR708", "valueWritten": "40" },
- { "id": "SkyVisionPro519", "valueWritten": "41" },
- { "id": "IMX477m", "valueWritten": "42" },
- { "id": "IMX462", "valueWritten": "43" },
- { "id": "IMX327", "valueWritten": "44" },
- { "id": "IMX290", "valueWritten": "45" },
- { "id": "IMX462MINI", "valueWritten": "46" },
- { "id": "IMX662", "valueWritten": "47" }
+ {
+ "id": "SkyMasterHDR708",
+ "valueWritten": "40"
+ },
+ {
+ "id": "SkyVisionPro519",
+ "valueWritten": "41"
+ },
+ {
+ "id": "IMX477m",
+ "valueWritten": "42"
+ },
+ {
+ "id": "IMX462",
+ "valueWritten": "43"
+ },
+ {
+ "id": "IMX327",
+ "valueWritten": "44"
+ },
+ {
+ "id": "IMX290",
+ "valueWritten": "45"
+ },
+ {
+ "id": "IMX462MINI",
+ "valueWritten": "46"
+ },
+ {
+ "id": "IMX662",
+ "valueWritten": "47"
+ }
]
},
{
"id": "veye",
"displayName": "Veye",
"options": [
- { "id": "2MPCAMERAS", "valueWritten": "60" },
- { "id": "CSIMX307", "valueWritten": "61" },
- { "id": "CSSC137", "valueWritten": "62" },
- { "id": "MVCAM", "valueWritten": "63" },
- { "id": "GX/GXC IMX662 (ISP)", "valueWritten": "64" }
+ {
+ "id": "2MPCAMERAS",
+ "valueWritten": "60"
+ },
+ {
+ "id": "CSIMX307",
+ "valueWritten": "61"
+ },
+ {
+ "id": "CSSC137",
+ "valueWritten": "62"
+ },
+ {
+ "id": "MVCAM",
+ "valueWritten": "63"
+ },
+ {
+ "id": "GX/GXC IMX662 (ISP)",
+ "valueWritten": "64"
+ }
]
},
{
"id": "advanced",
"displayName": "Advanced",
"options": [
- { "id": "USB", "valueWritten": "10" },
- { "id": "FILESRC", "valueWritten": "4" },
- { "id": "EXTERNAL", "valueWritten": "2" },
- { "id": "TESTPATTERN", "valueWritten": "0" }
+ {
+ "id": "USB",
+ "valueWritten": "10"
+ },
+ {
+ "id": "FILESRC",
+ "valueWritten": "4"
+ },
+ {
+ "id": "EXTERNAL",
+ "valueWritten": "2"
+ },
+ {
+ "id": "TESTPATTERN",
+ "valueWritten": "0"
+ }
]
}
]
},
{
- "sbc": ["rock-5a", "rock-5b", "radxa-cm5"],
+ "sbc": [
+ "rock-5a",
+ "rock-5b",
+ "radxa-cm5"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -146,21 +314,51 @@
"id": "rock-default",
"displayName": "Rock5",
"options": [
- { "id": "HDMI", "valueWritten": "80" },
- { "id": "OV5647", "valueWritten": "81" },
- { "id": "IMX219", "valueWritten": "82" },
- { "id": "IMX708", "valueWritten": "83" },
- { "id": "IMX462", "valueWritten": "84" },
- { "id": "IMX415", "valueWritten": "85" },
- { "id": "IMX477", "valueWritten": "86" },
- { "id": "IMX519", "valueWritten": "87" },
- { "id": "OHD-JAGUAR", "valueWritten": "88" }
+ {
+ "id": "HDMI",
+ "valueWritten": "80"
+ },
+ {
+ "id": "OV5647",
+ "valueWritten": "81"
+ },
+ {
+ "id": "IMX219",
+ "valueWritten": "82"
+ },
+ {
+ "id": "IMX708",
+ "valueWritten": "83"
+ },
+ {
+ "id": "IMX462",
+ "valueWritten": "84"
+ },
+ {
+ "id": "IMX415",
+ "valueWritten": "85"
+ },
+ {
+ "id": "IMX477",
+ "valueWritten": "86"
+ },
+ {
+ "id": "IMX519",
+ "valueWritten": "87"
+ },
+ {
+ "id": "OHD-JAGUAR",
+ "valueWritten": "88"
+ }
]
}
]
},
{
- "sbc": ["zero3w", "radxa-cm3"],
+ "sbc": [
+ "zero3w",
+ "radxa-cm3"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -168,20 +366,47 @@
"id": "rock3-default",
"displayName": "Zero3W",
"options": [
- { "id": "HDMI", "valueWritten": "90" },
- { "id": "OV5647", "valueWritten": "91" },
- { "id": "IMX219", "valueWritten": "92" },
- { "id": "IMX708", "valueWritten": "93" },
- { "id": "IMX462", "valueWritten": "94" },
- { "id": "IMX519", "valueWritten": "95" },
- { "id": "OHD-JAGUAR", "valueWritten": "96" },
- { "id": "VEYE", "valueWritten": "97" }
+ {
+ "id": "HDMI",
+ "valueWritten": "90"
+ },
+ {
+ "id": "OV5647",
+ "valueWritten": "91"
+ },
+ {
+ "id": "IMX219",
+ "valueWritten": "92"
+ },
+ {
+ "id": "IMX708",
+ "valueWritten": "93"
+ },
+ {
+ "id": "IMX462",
+ "valueWritten": "94"
+ },
+ {
+ "id": "IMX519",
+ "valueWritten": "95"
+ },
+ {
+ "id": "OHD-JAGUAR",
+ "valueWritten": "96"
+ },
+ {
+ "id": "VEYE",
+ "valueWritten": "97"
+ }
]
}
]
},
{
- "sbc": ["x86", "unknown"],
+ "sbc": [
+ "x86",
+ "unknown"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -189,16 +414,30 @@
"id": "generic",
"displayName": "Generic",
"options": [
- { "id": "USB", "valueWritten": "10" },
- { "id": "EXTERNAL", "valueWritten": "2" },
- { "id": "FILESRC", "valueWritten": "4" },
- { "id": "TESTPATTERN", "valueWritten": "0" }
+ {
+ "id": "USB",
+ "valueWritten": "10"
+ },
+ {
+ "id": "EXTERNAL",
+ "valueWritten": "2"
+ },
+ {
+ "id": "FILESRC",
+ "valueWritten": "4"
+ },
+ {
+ "id": "TESTPATTERN",
+ "valueWritten": "0"
+ }
]
}
]
},
{
- "sbc": ["x20"],
+ "sbc": [
+ "x20"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -206,18 +445,38 @@
"id": "x20",
"displayName": "X20",
"options": [
- { "id": "HDZERO", "valueWritten": "70" },
- { "id": "RUNCAM_V1", "valueWritten": "71" },
- { "id": "RUNCAM_V2", "valueWritten": "72" },
- { "id": "RUNCAM_V3", "valueWritten": "73" },
- { "id": "RUNCAM_NANO_90", "valueWritten": "74" },
- { "id": "OHD-JAGUAR", "valueWritten": "75" }
+ {
+ "id": "HDZERO",
+ "valueWritten": "70"
+ },
+ {
+ "id": "RUNCAM_V1",
+ "valueWritten": "71"
+ },
+ {
+ "id": "RUNCAM_V2",
+ "valueWritten": "72"
+ },
+ {
+ "id": "RUNCAM_V3",
+ "valueWritten": "73"
+ },
+ {
+ "id": "RUNCAM_NANO_90",
+ "valueWritten": "74"
+ },
+ {
+ "id": "OHD-JAGUAR",
+ "valueWritten": "75"
+ }
]
}
]
},
{
- "sbc": ["x21"],
+ "sbc": [
+ "x21"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -225,13 +484,19 @@
"id": "x21",
"displayName": "X21",
"options": [
- { "id": "OHD-JAGUAR-X21", "valueWritten": "76" }
+ {
+ "id": "OHD-JAGUAR-X21",
+ "valueWritten": "76"
+ }
]
}
]
},
{
- "sbc": ["qcs405", "qrb5165"],
+ "sbc": [
+ "qcs405",
+ "qrb5165"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -239,14 +504,22 @@
"id": "qualcomm",
"displayName": "Qualcomm",
"options": [
- { "id": "IMX577", "valueWritten": "120" },
- { "id": "OV9282", "valueWritten": "121" }
+ {
+ "id": "IMX577",
+ "valueWritten": "120"
+ },
+ {
+ "id": "OV9282",
+ "valueWritten": "121"
+ }
]
}
]
},
{
- "sbc": ["openipc"],
+ "sbc": [
+ "openipc"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -254,13 +527,18 @@
"id": "integrated",
"displayName": "Integrated camera",
"options": [
- { "id": "OPENIPC", "valueWritten": "110" }
+ {
+ "id": "OPENIPC",
+ "valueWritten": "110"
+ }
]
}
]
},
{
- "sbc": ["orqa"],
+ "sbc": [
+ "orqa"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -268,15 +546,26 @@
"id": "orqa",
"displayName": "Orqa",
"options": [
- { "id": "ORQA-HORNET", "valueWritten": "122" },
- { "id": "ORQA-JAGUAR", "valueWritten": "123" },
- { "id": "ORQA-REKINDLE", "valueWritten": "124" }
+ {
+ "id": "ORQA-HORNET",
+ "valueWritten": "122"
+ },
+ {
+ "id": "ORQA-JAGUAR",
+ "valueWritten": "123"
+ },
+ {
+ "id": "ORQA-REKINDLE",
+ "valueWritten": "124"
+ }
]
}
]
},
{
- "sbc": ["rockchip-rv"],
+ "sbc": [
+ "rockchip-rv"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -284,13 +573,18 @@
"id": "integrated",
"displayName": "Integrated camera",
"options": [
- { "id": "ROCKCHIP-RV", "valueWritten": "125" }
+ {
+ "id": "ROCKCHIP-RV",
+ "valueWritten": "125"
+ }
]
}
]
},
{
- "sbc": ["nvidia-xavier"],
+ "sbc": [
+ "nvidia-xavier"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": [
@@ -298,13 +592,20 @@
"id": "nvidia",
"displayName": "Nvidia",
"options": [
- { "id": "XAVIER-IMX577", "valueWritten": "101" }
+ {
+ "id": "XAVIER-IMX577",
+ "valueWritten": "101"
+ }
]
}
]
},
{
- "sbc": ["cubie", "uvx", "nxp-imx8"],
+ "sbc": [
+ "cubie",
+ "uvx",
+ "nxp-imx8"
+ ],
"bootTypeRequired": "Air",
"file": "openhd/camera1.txt",
"vendors": []
@@ -315,62 +616,265 @@
"_comment": "Resolution/fps choices keyed by OpenHD camera type value (widthxheight@fps).",
"valueEncoding": "string written to openhd/settings.json as camera_resolution_fps and camera2_resolution_fps",
"byCameraValue": {
- "0": ["640x480@30", "1280x720@30", "1280x720@60"],
- "4": ["848x480@60", "1280x720@60", "1920x1080@60"],
- "10": ["640x480@30", "0x0@0"],
- "11": ["384x292@25"],
- "12": ["256x192@25", "0x0@0"],
- "13": ["384x292@50"],
- "14": ["256x192@25"],
- "15": ["640x512@30"],
- "16": ["640x512@60"],
- "20": ["1280x720@30", "1920x1080@25", "1280x720@60"],
- "30": ["1280x720@30", "1920x1080@30"],
- "31": ["640x480@47", "896x504@47", "1280x720@47", "1920x1080@30"],
- "32": ["640x480@60", "896x504@60", "1280x720@60", "1920x1080@30"],
- "33": ["640x480@50", "896x504@50", "1280x720@50", "1920x1080@30"],
- "40": ["640x480@60", "896x504@60", "1280x720@60", "1920x1080@30"],
- "41": ["640x480@60", "896x504@60", "1280x720@60", "1920x1080@30"],
- "42": ["640x480@50", "896x504@50", "1280x720@50", "1920x1080@30"],
- "43": ["640x480@60", "896x504@30", "896x504@60", "1280x720@30", "1280x720@60", "1920x1080@30"],
- "44": ["640x480@60", "896x504@60", "1280x720@60", "1920x1080@30"],
- "46": ["640x480@60", "896x504@30", "896x504@60", "1280x720@30", "1280x720@60", "1920x1080@30"],
- "47": ["1280x720@60", "1920x1080@30"],
- "60": ["1920x1080@30"],
- "61": ["1280x720@60", "1920x1080@30"],
- "62": ["1920x1080@30"],
- "63": ["1920x1080@30"],
- "70": ["1280x720@60"],
- "71": ["1280x720@60"],
- "72": ["1280x720@60"],
- "73": ["1280x720@60"],
- "74": ["1280x720@60"],
- "75": ["1280x720@60"],
- "76": ["1280x720@60"],
- "80": ["1280x720@30", "1280x720@60", "1920x1080@30", "1920x1080@60", "3840x2160@30", "3840x2160@60"],
- "81": ["640x480@60", "1280x720@30", "1920x1080@30"],
- "82": ["640x480@30", "848x480@30", "1280x720@30", "1920x1080@30"],
- "83": ["1280x720@30", "1280x720@60", "1920x1080@60", "4608x2592@14"],
- "84": ["1280x720@30", "1280x720@60", "1920x1080@30", "1920x1080@60"],
- "85": ["1280x720@30", "1280x720@30", "1920x1080@30", "3864x2192@30"],
- "86": ["1280x720@60", "1920x1080@60", "3840x2160@20", "4056x3040@10"],
- "87": ["1280x720@60", "1920x1080@60", "3840x2160@20", "4056x3040@9"],
- "88": ["1280x720@120", "1920x1080@100", "3840x2160@90"],
- "90": ["1280x720@30", "1280x720@60"],
- "91": ["640x480@60", "1280x720@30", "1920x1080@30"],
- "92": ["640x480@30", "848x480@30", "1280x720@30", "1920x1080@30"],
- "93": ["1280x720@30", "1280x720@60", "1920x1080@60"],
- "94": ["1280x720@60", "1280x720@30", "1920x1080@30", "1920x1080@60"],
- "95": ["1280x720@60", "1280x720@30", "1920x1080@30", "1920x1080@60"],
- "96": ["1280x720@60", "1280x720@120", "1920x1080@60", "1920x1080@100"],
- "101": ["1280x720@60", "1920x1080@60"],
- "110": ["1280x720@60"],
- "120": ["1280x720@30", "1920x1080@30"],
- "121": ["1280x720@30"],
- "122": ["960x720@120"],
- "123": ["1280x720@120"],
- "124": ["1280x720@60"],
- "125": ["1280x720@60", "1296x968@60", "1920x1080@25", "2592x1944@25"]
+ "0": [
+ "640x480@30",
+ "1280x720@30",
+ "1280x720@60"
+ ],
+ "4": [
+ "848x480@60",
+ "1280x720@60",
+ "1920x1080@60"
+ ],
+ "10": [
+ "640x480@30",
+ "0x0@0"
+ ],
+ "11": [
+ "384x292@25"
+ ],
+ "12": [
+ "256x192@25",
+ "0x0@0"
+ ],
+ "13": [
+ "384x292@50"
+ ],
+ "14": [
+ "256x192@25"
+ ],
+ "15": [
+ "640x512@30"
+ ],
+ "16": [
+ "640x512@60"
+ ],
+ "20": [
+ "1280x720@30",
+ "1920x1080@25",
+ "1280x720@60"
+ ],
+ "30": [
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "31": [
+ "640x480@47",
+ "896x504@47",
+ "1280x720@47",
+ "1920x1080@30"
+ ],
+ "32": [
+ "640x480@60",
+ "896x504@60",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "33": [
+ "640x480@50",
+ "896x504@50",
+ "1280x720@50",
+ "1920x1080@30"
+ ],
+ "40": [
+ "640x480@60",
+ "896x504@60",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "41": [
+ "640x480@60",
+ "896x504@60",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "42": [
+ "640x480@50",
+ "896x504@50",
+ "1280x720@50",
+ "1920x1080@30"
+ ],
+ "43": [
+ "640x480@60",
+ "896x504@30",
+ "896x504@60",
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "44": [
+ "640x480@60",
+ "896x504@60",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "46": [
+ "640x480@60",
+ "896x504@30",
+ "896x504@60",
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "47": [
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "60": [
+ "1920x1080@30"
+ ],
+ "61": [
+ "1280x720@60",
+ "1920x1080@30"
+ ],
+ "62": [
+ "1920x1080@30"
+ ],
+ "63": [
+ "1920x1080@30"
+ ],
+ "70": [
+ "1280x720@60"
+ ],
+ "71": [
+ "1280x720@60"
+ ],
+ "72": [
+ "1280x720@60"
+ ],
+ "73": [
+ "1280x720@60"
+ ],
+ "74": [
+ "1280x720@60"
+ ],
+ "75": [
+ "1280x720@60"
+ ],
+ "76": [
+ "1280x720@60"
+ ],
+ "80": [
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@30",
+ "1920x1080@60",
+ "3840x2160@30",
+ "3840x2160@60"
+ ],
+ "81": [
+ "640x480@60",
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "82": [
+ "640x480@30",
+ "848x480@30",
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "83": [
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@60",
+ "4608x2592@14"
+ ],
+ "84": [
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@30",
+ "1920x1080@60"
+ ],
+ "85": [
+ "1280x720@30",
+ "1280x720@30",
+ "1920x1080@30",
+ "3864x2192@30"
+ ],
+ "86": [
+ "1280x720@60",
+ "1920x1080@60",
+ "3840x2160@20",
+ "4056x3040@10"
+ ],
+ "87": [
+ "1280x720@60",
+ "1920x1080@60",
+ "3840x2160@20",
+ "4056x3040@9"
+ ],
+ "88": [
+ "1280x720@120",
+ "1920x1080@100",
+ "3840x2160@90"
+ ],
+ "90": [
+ "1280x720@30",
+ "1280x720@60"
+ ],
+ "91": [
+ "640x480@60",
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "92": [
+ "640x480@30",
+ "848x480@30",
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "93": [
+ "1280x720@30",
+ "1280x720@60",
+ "1920x1080@60"
+ ],
+ "94": [
+ "1280x720@60",
+ "1280x720@30",
+ "1920x1080@30",
+ "1920x1080@60"
+ ],
+ "95": [
+ "1280x720@60",
+ "1280x720@30",
+ "1920x1080@30",
+ "1920x1080@60"
+ ],
+ "96": [
+ "1280x720@60",
+ "1280x720@120",
+ "1920x1080@60",
+ "1920x1080@100"
+ ],
+ "101": [
+ "1280x720@60",
+ "1920x1080@60"
+ ],
+ "110": [
+ "1280x720@60"
+ ],
+ "120": [
+ "1280x720@30",
+ "1920x1080@30"
+ ],
+ "121": [
+ "1280x720@30"
+ ],
+ "122": [
+ "960x720@120"
+ ],
+ "123": [
+ "1280x720@120"
+ ],
+ "124": [
+ "1280x720@60"
+ ],
+ "125": [
+ "1280x720@60",
+ "1296x968@60",
+ "1920x1080@25",
+ "2592x1944@25"
+ ]
}
},
"ipCamera": {
@@ -382,14 +886,20 @@
"reservedBitrateKey": "ip_camera_bitrate_mbits",
"defaultAddress": "192.168.144.108",
"defaultPipeline": "rtspsrc location=rtsp://{IP}:554/stream=0 latency=0 ! rtph264depay",
- "reservedBitrateRangeMbits": [1, 20],
+ "reservedBitrateRangeMbits": [
+ 1,
+ 20
+ ],
"defaultReservedBitrateMbits": 2
},
"rpi5CameraConnectors": {
"_comment": "For Raspberry Pi CSI cameras, ImageWriter asks which Pi 5 connector is used. Sysutils applies ,cam0 for CAM0; CAM1 uses the overlay default.",
"primaryKey": "camera_port",
"secondaryKey": "camera2_port",
- "options": ["cam0", "cam1"],
+ "options": [
+ "cam0",
+ "cam1"
+ ],
"primaryDefault": "cam1",
"secondaryDefault": "cam0"
},
@@ -401,9 +911,18 @@
"refreshKey": "display_refresh_hz",
"connectorKey": "display_connector",
"defaultConnector": "HDMI-A-1",
- "widthRange": [320, 7680],
- "heightRange": [240, 4320],
- "refreshRangeHz": [20, 240]
+ "widthRange": [
+ 320,
+ 7680
+ ],
+ "heightRange": [
+ 240,
+ 4320
+ ],
+ "refreshRangeHz": [
+ 20,
+ 240
+ ]
},
"qopenhdConf": {
"_comment": "Optional full configuration file copied verbatim when provided by the user.",
@@ -428,5 +947,471 @@
"eject": {
"_comment": "Stored in settings but not written to FAT32 partition.",
"options": []
- }
+ },
+ "platforms": [
+ {
+ "id": "x86",
+ "label": "PC / x86_64",
+ "platformId": 1,
+ "sbc": "x86",
+ "aliases": [
+ "PC / x86_64 Ground Station",
+ "x86_64"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi3",
+ "label": "Raspberry Pi 3",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi4",
+ "label": "Raspberry Pi 4",
+ "platformId": 11,
+ "sbc": "rpi",
+ "aliases": [
+ "raspberry-pi-4"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "cm4",
+ "label": "Raspberry Pi CM4",
+ "platformId": 12,
+ "sbc": "rpi",
+ "aliases": [
+ "raspberry-pi-cm4"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi5",
+ "label": "Raspberry Pi 5",
+ "platformId": 13,
+ "sbc": "rpi",
+ "aliases": [
+ "raspberry-pi-5"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "zero3w",
+ "label": "Radxa Zero 3W",
+ "platformId": 20,
+ "sbc": "zero3w",
+ "aliases": [
+ "radxa-zero3w",
+ "radxa-zero-3w"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rock5a",
+ "label": "Radxa Rock 5A",
+ "platformId": 21,
+ "sbc": "rock-5a",
+ "aliases": [
+ "radxa-rock5a",
+ "rock-5a"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rock5b",
+ "label": "Radxa Rock 5B",
+ "platformId": 22,
+ "sbc": "rock-5b",
+ "aliases": [
+ "radxa-rock5b",
+ "rock-5b"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "cm5",
+ "label": "Radxa CM5",
+ "platformId": 23,
+ "sbc": "radxa-cm5",
+ "aliases": [
+ "radxa-cm5"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "cm3",
+ "label": "Radxa CM3",
+ "platformId": 24,
+ "sbc": "radxa-cm3",
+ "aliases": [
+ "radxa-cm3"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "luckfox-pico",
+ "label": "Luckfox Pico / RV110X",
+ "platformId": 25,
+ "sbc": "rockchip-rv",
+ "aliases": [
+ "rockchip-rv",
+ "rv110x",
+ "luckfox-pico"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "luckfox-lyra",
+ "label": "Luckfox Lyra",
+ "platformId": 27,
+ "sbc": "rockchip-rv",
+ "aliases": [
+ "luckfox-lyra"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "x21b",
+ "label": "OpenHD Core X21",
+ "platformId": 28,
+ "sbc": "x21",
+ "aliases": [
+ "x21",
+ "OpenHD X21"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "luckfox-aura",
+ "label": "Luckfox Aura",
+ "platformId": 29,
+ "sbc": "rockchip-rv",
+ "aliases": [
+ "luckfox-aura"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "x20",
+ "label": "OpenHD Core X20",
+ "platformId": 30,
+ "sbc": "x20",
+ "aliases": [
+ "OpenHD X20",
+ "x20"
+ ],
+ "roles": [
+ "air"
+ ]
+ },
+ {
+ "id": "cubie-a7s",
+ "label": "Radxa Cubie A7S",
+ "platformId": 31,
+ "sbc": "cubie",
+ "aliases": [
+ "cubie-a7s"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "cubie-a7z",
+ "label": "Radxa Cubie A7Z",
+ "platformId": 32,
+ "sbc": "cubie",
+ "aliases": [
+ "cubie-a7z"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "openipc",
+ "label": "OpenIPC SigmaStar",
+ "platformId": 36,
+ "sbc": "openipc",
+ "aliases": [
+ "openipc"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "nvidia-xavier",
+ "label": "NVIDIA Xavier",
+ "platformId": 40,
+ "sbc": "nvidia-xavier",
+ "aliases": [
+ "nvidia-xavier"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "qrb5165",
+ "label": "Qualcomm QRB5165",
+ "platformId": 46,
+ "sbc": "qrb5165",
+ "aliases": [
+ "qrb5165"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "qcs405",
+ "label": "Qualcomm QCS405",
+ "platformId": 47,
+ "sbc": "qcs405",
+ "aliases": [
+ "qcs405"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "orqa",
+ "label": "Orqa",
+ "platformId": 51,
+ "sbc": "orqa",
+ "aliases": [
+ "orqa"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "uvx",
+ "label": "UVX Mod",
+ "platformId": 52,
+ "sbc": "uvx",
+ "aliases": [
+ "uvx"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "nxp-imx8",
+ "label": "NXP i.MX8",
+ "platformId": 61,
+ "sbc": "nxp-imx8",
+ "aliases": [
+ "nxp-imx8"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi-zero",
+ "label": "Raspberry Pi Zero",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi-zero-w",
+ "label": "Raspberry Pi Zero W",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi-zero2w",
+ "label": "Raspberry Pi Zero 2W",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [
+ "raspberry-pi-zero2w"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi2",
+ "label": "Raspberry Pi 2",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi-cm3",
+ "label": "Raspberry Pi CM3",
+ "platformId": 10,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rpi-cm5",
+ "label": "Raspberry Pi CM5",
+ "platformId": 13,
+ "sbc": "rpi",
+ "aliases": [],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ },
+ {
+ "id": "rock3",
+ "label": "Radxa Rock 3",
+ "platformId": 20,
+ "sbc": "zero3w",
+ "aliases": [
+ "radxa-rock3"
+ ],
+ "roles": [
+ "air",
+ "ground"
+ ]
+ }
+ ],
+ "_settingsContract": {
+ "version": 1,
+ "catalogHash": "6171a437338e86373c8ccd5372172af5e2609ab2351856803ca7a98668303e15"
+ },
+ "initialFields": [
+ {
+ "choices": [
+ {
+ "label": "DISABLE",
+ "value": 0
+ },
+ {
+ "label": "ENABLE",
+ "value": 1
+ },
+ {
+ "label": "AUTO(armed)",
+ "value": 2
+ }
+ ],
+ "description": "Record video data locally on your air unit. You can find the files under /home/openhd/Videos on the SD card and/or download them via the web ui.When AUTO is set, air recording automatically starts (and stops) when you arm/disarm your drone (requires inav / ardupilot FC).",
+ "group": "General",
+ "hidden": false,
+ "id": "AIR_RECORDING_E",
+ "label": "Air recording",
+ "maximum": 2,
+ "minimum": 0,
+ "readOnly": false,
+ "requiresReboot": false,
+ "type": "int32",
+ "provision": {
+ "componentId": 100,
+ "roles": [
+ "air"
+ ]
+ }
+ },
+ {
+ "choices": [
+ {
+ "label": "Disable",
+ "value": 0
+ },
+ {
+ "label": "Enable",
+ "value": 1
+ }
+ ],
+ "description": "Enable / disable streaming for this camera. Note that this setting is persistent at run time - once you disable streaming for a camera, you won't have video until you re-enable streaming or reboot your air unit. On by default",
+ "group": "General",
+ "hidden": true,
+ "id": "STREAMING_E",
+ "label": "Video streaming",
+ "maximum": 1,
+ "minimum": 0,
+ "readOnly": false,
+ "requiresReboot": false,
+ "type": "int32",
+ "provision": {
+ "componentId": 100,
+ "roles": [
+ "air"
+ ]
+ }
+ }
+ ]
}
diff --git a/doc/shared-settings.md b/doc/shared-settings.md
new file mode 100644
index 000000000..6804e7856
--- /dev/null
+++ b/doc/shared-settings.md
@@ -0,0 +1,26 @@
+# Initial configuration and FleetControl
+
+Select a craft in FleetControl, configure its image options, then use **Save
+configuration** to save a version-1 initial snapshot on that craft. **Export JSON**
+saves the initial `settings.json` locally. The snapshot uses the image-writing
+builder and excludes FleetControl login credentials and enrollment tokens.
+
+Selecting the craft again restores the editable settings and typed
+`parameter_defaults`. A profile without a plan clears the previous craft's plan.
+Open initial-default controls follow profile changes; Ground removes camera
+component defaults. OpenHD seeds new persistent owners only, preserving existing
+device preferences.
+
+Hardware selectors use generated platform IDs and labels from OpenHD's
+`settings/platforms.json`. The online craft editor refreshes the platform catalog
+from `/api/imagewriter/platforms`; offline use retains the bundled list. Combined
+legacy board names require a concrete selection. Image availability and flashing
+transport still depend on the selected image.
+
+FleetControl's Hangar editor shows and downloads the captured initial file.
+Ordinary craft edits preserve that snapshot until it is explicitly saved again.
+The full contract is documented in OpenHD's `OpenHD/settings/README.md`.
+
+Tests: `node src/test/fleet_profiles_test.js`, `node src/test/catalog_test.js`,
+`node src/test/fleetcontrol_catalog_test.js`, and the CMake targets
+`openhd_backend_tests` / `initialsettings_qml_test` under `src/test`.
diff --git a/doc/signpath.md b/doc/signpath.md
new file mode 100644
index 000000000..cde506ee0
--- /dev/null
+++ b/doc/signpath.md
@@ -0,0 +1,68 @@
+# Windows ImageWriter signing with SignPath
+
+SignPath Foundation's confirmed arrangement is a separate ImageWriter project
+inside the existing OpenHD OSS organization, sharing its production certificate.
+The project's source repository and GitHub Actions origin are
+`https://github.com/OpenHD/OpenHD-ImageWriter`. An OpenHD repository caller is
+not required.
+
+Ordinary pushes, pull requests and fork builds call `windows.yml` with signing
+disabled. Run **Sign Windows ImageWriter** (`sign-windows.yml`) manually in the
+canonical ImageWriter repository, selecting the branch containing the reviewed
+workflow and the `test` or `release` mode. The workflow builds its own exact
+commit (`github.sha`), so the source matches the build's origin metadata.
+
+## Configure the ImageWriter project
+
+1. Wait for SignPath to add ImageWriter as a separate project, then link its
+ GitHub trusted build system and enable SignPath GitHub App access to the
+ ImageWriter repository as required by their configuration.
+2. Add these artifact configurations to the **ImageWriter** project:
+ - `imagewriter-application`: [application XML](signpath/imagewriter-application.xml)
+ - `imagewriter-installer`: [installer XML](signpath/imagewriter-installer.xml)
+ Both expect ZIP artifacts from `actions/upload-artifact@v4`. Configurations
+ previously added to the OpenHD project do not configure the new project.
+3. In **OpenHD/OpenHD-ImageWriter**, configure these repository variables with
+ the exact values from the new project's settings:
+ - `SIGNPATH_ORGANIZATION_ID` (the existing OpenHD OSS organization ID)
+ - `SIGNPATH_PROJECT_SLUG` (the new ImageWriter project's slug)
+ - `SIGNPATH_TEST_SIGNING_POLICY_SLUG` (its test policy)
+ - `SIGNPATH_RELEASE_SIGNING_POLICY_SLUG` (its release policy)
+ - `SIGNPATH_TEST_CERTIFICATE_BASE64` (the public test certificate exported
+ as DER and base64 encoded; never include a private key or PFX)
+4. Add repository secret `SIGNPATH_API_TOKEN` for the CI submitter permitted
+ by the ImageWriter project's signing policies. Never put it in a variable,
+ source file or workflow input.
+
+## Test signing and production review
+
+Run with `test` first. Only this ephemeral GitHub-hosted runner imports the
+configured public self-signed test certificate into its current-user trust
+stores. Each signed file must have a valid timestamped signature from exactly
+that certificate. Test outputs are labelled `-Test-Signed`; they are for
+onboarding verification and are not publicly trusted distribution packages.
+Do not install this certificate on end-user computers.
+
+Once integration and origin verification work, send SignPath the successful
+workflow/signing request links for their technical review. Their onboarding
+email states that they then order and import the production certificate.
+
+Run with `release` only once its policy is valid. Release builds never import
+the test certificate; they require normal Windows trust and timestamping.
+
+## Signed artifacts
+
+All compilation and signing run on GitHub-hosted Windows. The application,
+locally built FAT32 formatter and generated NSIS uninstaller are signed first.
+The installer embeds those files, then the outer installer is signed. Third-party
+Qt/OpenSSL/UUU binaries are redistributed as supplied.
+
+The run publishes `OpenHDImageWriter-Build-Full-Signed` and
+`OpenHDImageWriter-Windows-Portable-Signed` for release mode. Test mode publishes
+those names with `-Test-Signed` instead; ordinary builds use `-Unsigned`.
+The portable archive includes the application and its runtime dependencies.
+Missing settings, failed requests, invalid signatures or missing timestamps
+fail the build without falling back to unsigned publication as signed.
+
+See the [SignPath GitHub integration](https://docs.signpath.io/trusted-build-systems/github)
+and [artifact configuration syntax](https://docs.signpath.io/artifact-configuration/syntax).
diff --git a/doc/signpath/imagewriter-application.xml b/doc/signpath/imagewriter-application.xml
new file mode 100644
index 000000000..3d008a61a
--- /dev/null
+++ b/doc/signpath/imagewriter-application.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/doc/signpath/imagewriter-installer.xml b/doc/signpath/imagewriter-installer.xml
new file mode 100644
index 000000000..4683de355
--- /dev/null
+++ b/doc/signpath/imagewriter-installer.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/scripts/trust_signpath_test_certificate.ps1 b/scripts/trust_signpath_test_certificate.ps1
new file mode 100644
index 000000000..0d67baf21
--- /dev/null
+++ b/scripts/trust_signpath_test_certificate.ps1
@@ -0,0 +1,39 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string] $CertificateBase64
+)
+
+$ErrorActionPreference = 'Stop'
+if ($Env:GITHUB_ACTIONS -ne 'true' -or $Env:RUNNER_ENVIRONMENT -ne 'github-hosted') {
+ throw 'Test certificate trust is restricted to ephemeral GitHub-hosted Actions runners'
+}
+$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new(
+ [Convert]::FromBase64String($CertificateBase64)
+)
+try {
+ if ($certificate.HasPrivateKey -or $certificate.Subject -ne $certificate.Issuer) {
+ throw 'Expected the public self-signed SignPath test certificate'
+ }
+ if ($certificate.NotBefore -gt (Get-Date) -or $certificate.NotAfter -lt (Get-Date)) {
+ throw 'The test certificate is outside its validity period'
+ }
+ $eku = $certificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.37' }
+ if (-not $eku -or '1.3.6.1.5.5.7.3.3' -notin @($eku.EnhancedKeyUsages | ForEach-Object { $_.Value })) {
+ throw 'The test certificate must have the code signing EKU'
+ }
+ # Only the ephemeral GitHub-hosted test job trusts this public certificate.
+ # Release jobs never execute this script and retain normal Windows trust.
+ foreach ($storeName in @('Root', 'TrustedPublisher')) {
+ $store = [System.Security.Cryptography.X509Certificates.X509Store]::new($storeName, 'CurrentUser')
+ try {
+ $store.Open('ReadWrite')
+ $store.Add($certificate)
+ } finally {
+ $store.Dispose()
+ }
+ }
+ "SIGNPATH_TEST_CERTIFICATE_THUMBPRINT=$($certificate.Thumbprint)" >> $Env:GITHUB_ENV
+ Write-Host "Configured onboarding certificate: $($certificate.Thumbprint)"
+} finally {
+ $certificate.Dispose()
+}
diff --git a/scripts/verify_windows_signature.ps1 b/scripts/verify_windows_signature.ps1
new file mode 100644
index 000000000..cbf7fe759
--- /dev/null
+++ b/scripts/verify_windows_signature.ps1
@@ -0,0 +1,20 @@
+param(
+ [Parameter(Mandatory = $true)]
+ [string] $Path,
+ [ValidateSet('test', 'release')]
+ [string] $SigningMode = 'release',
+ [string] $ExpectedSignerThumbprint
+)
+
+$ErrorActionPreference = 'Stop'
+if ($SigningMode -eq 'test' -and [string]::IsNullOrWhiteSpace($ExpectedSignerThumbprint)) {
+ throw 'Test verification requires the configured test certificate thumbprint'
+}
+$signature = Get-AuthenticodeSignature -LiteralPath $Path
+if ($signature.Status -ne 'Valid' -or -not $signature.SignerCertificate -or -not $signature.TimeStamperCertificate) {
+ throw "Expected a valid, timestamped Authenticode signature on $Path; status: $($signature.Status)"
+}
+if ($SigningMode -eq 'test' -and $signature.SignerCertificate.Thumbprint -ne $ExpectedSignerThumbprint) {
+ throw "Unexpected test signing certificate on $Path"
+}
+Write-Host "Verified timestamped signature: $Path"
diff --git a/src/ImageOptionsPage.qml b/src/ImageOptionsPage.qml
index c51ade2ac..a53308064 100644
--- a/src/ImageOptionsPage.qml
+++ b/src/ImageOptionsPage.qml
@@ -11,6 +11,7 @@ import Qt.labs.settings 1.0
import QtQuick.Dialogs 1.3
import "qmlcomponents"
import "qmlcomponents/SettingsLabels.js" as SettingsLabels
+import "qmlcomponents/FleetProfilesHelper.js" as FleetProfilesHelper
Rectangle {
id: page
@@ -29,10 +30,14 @@ Rectangle {
property var settingsMap: ({})
property bool settingsMapLoaded: false
+ onVisibleChanged: {
+ if (visible && settingsMapLoaded) settingsMap = JSON.parse(JSON.stringify(FleetProfilesHelper.getSettingsMap(imageWriter)))
+ }
// refactored settings
property string bootType
property string fileName
+ property string platform: imageWriter.getValue("platform") || ""
property string sbc
property string camera
property string camera2
@@ -165,6 +170,33 @@ Rectangle {
width: deviceRoleCard.availableWidth
spacing: 10
+ Label { text: qsTr("Hardware platform"); Layout.fillWidth: true }
+ ModernComboBox {
+ Layout.fillWidth: true
+ model: settingsMap.platforms || []
+ textRole: "label"
+ currentIndex: {
+ for (var n=0; n= 0 ? currentText : qsTr("Choose hardware platform")
+ onActivated: {
+ var board = model[currentIndex]
+ page.platform = board.id
+ sbc = board.sbc
+ imageWriter.setSetting("platform", board.id)
+ imageWriter.setSetting("sbc", board.sbc)
+ if (board.roles.indexOf(bootType.toLowerCase()) < 0) bootType = board.roles[0] === "air" ? "Air" : "Ground"
+ rpi = sbc === "rpi"; rock5 = sbc === "rock-5a" || sbc === "rock-5b" || sbc === "radxa-cm5"; rock3 = sbc === "zero3w" || sbc === "radxa-cm3"; page.refreshCameraOptions()
+ }
+ }
+ InitialSettingsPanel {
+ id: initialDefaults
+ Layout.fillWidth: true
+ settingsOwner: imageWriter
+ fields: settingsMap.initialFields || []
+ role: String(bootType).toLowerCase()
+ }
ModernComboBox {
id: bootTypeSelector
Layout.fillWidth: true
@@ -1074,86 +1106,15 @@ Rectangle {
else if (!supportsAir)
bootType = "Ground"
- if (normalizedPlatform.indexOf("raspberry-pi") === 0) {
- imageWriter.setSetting("sbc", "rpi");
- sbc = "rpi"
- rpi=true;
- rock5=false;
- rock3=false;
- }
- else if (normalizedFileName.includes("rock5a") || normalizedFileName.includes("rock-5a")) {
- imageWriter.setSetting("sbc", "rock-5a");
- sbc = "rock-5a"
- rpi=false; rock5=true; rock3=false;
- }
- else if (normalizedFileName.includes("rock5b") || normalizedFileName.includes("rock-5b")) {
- imageWriter.setSetting("sbc", "rock-5b");
- sbc = "rock-5b"
- rpi=false; rock5=true; rock3=false;
- }
- else if (normalizedFileName.includes("zero3") || normalizedFileName.includes("zero-3")) {
- imageWriter.setSetting("sbc", "zero3w");
- sbc = "zero3w"
- rpi=false; rock5=false; rock3=true;
- }
- else if (normalizedFileName.includes("radxa-cm5") || normalizedFileName.includes("radxa_cm5")) {
- sbc = "radxa-cm5"; rpi=false; rock5=true; rock3=false;
- }
- else if (normalizedFileName.includes("radxa-cm3") || normalizedFileName.includes("radxa_cm3")) {
- imageWriter.setSetting("sbc", "radxa-cm3");
- sbc = "radxa-cm3"
- rpi=false; rock5=false; rock3=true;
- }
- else if (normalizedFileName.includes("qrb5165")) {
- sbc = "qrb5165"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("qcs405")) {
- sbc = "qcs405"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("openipc")) {
- sbc = "openipc"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("rv1103") || normalizedFileName.includes("rv1106") || normalizedFileName.includes("rv1126") || normalizedFileName.includes("luckfox")) {
- sbc = "rockchip-rv"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("orqa")) {
- sbc = "orqa"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("x20")) {
- sbc = "x20"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("x21")) {
- sbc = "x21"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("cubie") || normalizedFileName.includes("a733")) {
- sbc = "cubie"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("xavier") || normalizedFileName.includes("jetson")) {
- sbc = "nvidia-xavier"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("uvx") || normalizedFileName.includes("uvx-mod")) {
- sbc = "uvx"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("imx8") || normalizedFileName.includes("nxp")) {
- sbc = "nxp-imx8"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("x86") || normalizedFileName.includes("amd64")) {
- sbc = "x86"; rpi=false; rock5=false; rock3=false;
- }
- else if (normalizedFileName.includes("raspberry") || normalizedFileName.includes("rpi") || normalizedFileName.includes("image-pi") || normalizedFileName.includes("image_pi")) {
- imageWriter.setSetting("sbc", "rpi");
- sbc = "rpi"
- rpi=true;
- rock5=false;
- rock3=false;
- }
- else{
- imageWriter.setSetting("sbc", "unknown");
- sbc = "unknown"
- rpi=false;
- rock5=false;
- rock3=false;
- }
+ FleetProfilesHelper.getSettingsMap(imageWriter)
+ var board = FleetProfilesHelper.platformForHint(normalizedPlatform) || FleetProfilesHelper.platformForHint(normalizedFileName) ||
+ FleetProfilesHelper.platformForHardware(imageWriter.getValue("platform"))
+ platform = board ? board.id : ""
+ sbc = board ? board.sbc : "unknown"
+ rpi = sbc === "rpi"
+ rock5 = sbc === "rock-5a" || sbc === "rock-5b" || sbc === "radxa-cm5"
+ rock3 = sbc === "zero3w" || sbc === "radxa-cm3"
+ imageWriter.setSetting("platform", platform)
imageWriter.setSetting("sbc", sbc)
imageWriter.setSetting("bootType", bootType)
@@ -1241,8 +1202,10 @@ Rectangle {
function availableBootOptions() {
if (!settingsMap.bootType || !settingsMap.bootType.options)
return []
+ var board = FleetProfilesHelper.platformForHardware(platform)
return settingsMap.bootType.options.filter(function(option) {
return option.id && option.id.length > 0 &&
+ (!board || board.roles.indexOf(option.id.toLowerCase()) >= 0) &&
((option.id === "Air" && supportsAir) ||
(option.id === "Ground" && supportsGround))
})
@@ -1269,6 +1232,8 @@ Rectangle {
}
}
+ imageWriter.setSetting("platform", platform)
+ imageWriter.setSetting("initialParameterDefaults", JSON.stringify(initialDefaults.exportDefaults()))
imageWriter.setSetting("bootType", bootType)
imageWriter.setSetting("camera", camera)
imageWriter.setSetting("camera2", camera2)
@@ -1323,7 +1288,7 @@ Rectangle {
return
try {
- settingsMap = JSON.parse(imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
+ settingsMap = FleetProfilesHelper.getSettingsMap(imageWriter)
settingsMapLoaded = true
} catch (e) {
console.log("Failed to load OpenHD settings map: " + e)
diff --git a/src/configure.qml b/src/configure.qml
index 0454bb56a..c393f1df9 100644
--- a/src/configure.qml
+++ b/src/configure.qml
@@ -35,7 +35,10 @@ Rectangle {
// Settings map and values shared with the image configuration page.
property var settingsMap: ({})
property bool settingsMapLoaded: false
+ property string settingsMapSourceRoot: ""
+ property bool hasImageSettingsManifest: false
property string bootType: ""
+ property string platform: imageWriter.getValue("platform") || ""
property string sbc: ""
property string camera: ""
property string camera2: ""
@@ -66,6 +69,11 @@ Rectangle {
property bool returnHomeAfterPopupClose: false
property string language: ""
property string token: ""
+ function platformSupportsRole(role) {
+ var boards = settingsMap.platforms || []
+ for (var n=0; n= 0
+ return true
+ }
Component.onCompleted: {
qopenhdConfPath = normalizeLocalFilePath(imageWriter.getValue("qopenhdConfPath"))
@@ -376,9 +384,36 @@ ImButton {
}
ColumnLayout {
id: settingsBody
+ InitialSettingsPanel {
+ id: initialDefaults
+ settingsOwner: imageWriter
+ Layout.fillWidth: true
+ fields: hasImageSettingsManifest ? (settingsMap.initialFields || []) : []
+ role: bootType.toLowerCase()
+ }
width: settingsScroll.width - settingsScroll.leftPadding - settingsScroll.rightPadding
spacing: 16
+ Label { text: qsTr("Hardware platform"); Layout.fillWidth: true }
+ ModernComboBox {
+ Layout.fillWidth: true
+ model: settingsMap.platforms || []
+ textRole: "label"
+ currentIndex: {
+ for (var n=0; n= 0 ? currentText : qsTr("Choose hardware platform")
+ onActivated: {
+ var board = model[currentIndex]
+ window.platform = board.id
+ sbc = board.sbc
+ imageWriter.setSetting("platform", board.id)
+ imageWriter.setSetting("sbc", board.sbc)
+ if (board.roles.indexOf(bootType.toLowerCase()) < 0) bootType = board.roles[0] === "air" ? "Air" : "Ground"
+ rebuildCameraSelectors()
+ }
+ }
SettingsSection {
title: qsTr("Boot Mode")
Layout.fillWidth: true
@@ -386,7 +421,7 @@ ImButton {
ColumnLayout {
spacing: 0
Repeater {
- model: settingsMap.bootType && settingsMap.bootType.options ? settingsMap.bootType.options.filter(function(option) { return option.id && option.id.length > 0 }) : []
+ model: settingsMap.bootType && settingsMap.bootType.options ? settingsMap.bootType.options.filter(function(option) { return option.id && option.id.length > 0 && platformSupportsRole(option.id) }) : []
delegate: ImCheckBox {
property var option: modelData
text: option ? SettingsLabels.translated(option.id) : ""
@@ -576,7 +611,7 @@ ImButton {
}
var targetIndex = 0
- if (cameraResolution && cameraResolution.length > 0) {
+ if (cameraResolution && cameraResolution.length > 0) {
for (var idx = 0; idx < cameraResolutionOptionsModel.count; idx++) {
if (cameraResolutionOptionsModel.get(idx).value === cameraResolution) {
targetIndex = idx
@@ -1299,11 +1334,19 @@ ImButton {
}
function loadSettingsMap() {
- if (settingsMapLoaded)
+ if (settingsMapLoaded && settingsMapSourceRoot === openhdRoot)
return
try {
- settingsMap = JSON.parse(imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
+ var manifestText = openhdRoot ? imageWriter.readTextFile(drivePath("settings-manifest.json")) : ""
+ hasImageSettingsManifest = manifestText.length > 0
+ settingsMap = JSON.parse(hasImageSettingsManifest ? manifestText : imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
+ if (hasImageSettingsManifest && (!settingsMap._settingsContract || settingsMap._settingsContract.version !== 1)) {
+ settingsMap = {}
+ hasImageSettingsManifest = false
+ throw new Error("Unsupported image settings manifest")
+ }
+ settingsMapSourceRoot = openhdRoot
settingsMapLoaded = true
} catch (e) {
console.log("[Configure] Failed to load OpenHD settings map: " + e)
@@ -1470,9 +1513,11 @@ ImButton {
var settingsJson = imageWriter.readTextFile(drivePath("settings.json"))
var settingsObj = {}
+ if (platform) settingsObj.platform = platform
if (settingsJson && settingsJson.length > 0) {
try {
settingsObj = JSON.parse(settingsJson)
+ initialDefaults.defaults = settingsObj.parameter_defaults || {version: 1, components: {}}
} catch (e) {
console.log("[Configure] Error parsing settings.json:", e)
}
@@ -1491,6 +1536,7 @@ ImButton {
} else if (settingsMap.sbc && settingsMap.sbc.options && settingsMap.sbc.options.length > 0) {
sbc = settingsMap.sbc.options[0].id
}
+ platform = settingsObj.platform || ""
if (settingsObj.debug) {
mode = "debug"
@@ -1597,6 +1643,7 @@ ImButton {
console.log("[Configure] Writing settings to", openhdRoot)
var settingsObj = {}
+ settingsObj.parameter_defaults = initialDefaults.exportDefaults()
if (bootType === "Air") {
settingsObj.role = "air"
} else if (bootType === "Ground") {
@@ -1709,11 +1756,13 @@ ImButton {
premiumCertificatePresent = imageWriter.fileExists(drivePath(premiumCertificateRelativePath()))
}
+ imageWriter.setSetting("platform", platform)
imageWriter.setSetting("bootType", bootType)
imageWriter.setSetting("sbc", sbc)
imageWriter.setSetting("camera", camera)
imageWriter.setSetting("camera2", camera2)
imageWriter.setSetting("cameraResolution", cameraResolution)
+ imageWriter.setSetting("initialParameterDefaults", JSON.stringify(initialDefaults.exportDefaults()))
imageWriter.setSetting("camera2Resolution", camera2Resolution)
imageWriter.setSetting("cameraPort", cameraPort)
imageWriter.setSetting("camera2Port", camera2Port)
diff --git a/src/imagewriter.cpp b/src/imagewriter.cpp
index c35189a6c..dcf94ae91 100644
--- a/src/imagewriter.cpp
+++ b/src/imagewriter.cpp
@@ -1635,9 +1635,15 @@ void ImageWriter::setSetting(const QString &key, const QVariant &value)
_settings.setValue(key, value);
_settings.sync();
+ emit settingChanged(key, value);
}
+QVariantMap ImageWriter::initialConfiguration() const
+{
+ return OpenHDImageCustomizer::initialConfiguration(_settings).toVariantMap();
+}
+
QString ImageWriter::crypt(const QByteArray &password)
{
QByteArray salt = "$5$";
diff --git a/src/imagewriter.h b/src/imagewriter.h
index 8631aa5fa..d5c441562 100644
--- a/src/imagewriter.h
+++ b/src/imagewriter.h
@@ -166,6 +166,7 @@ class ImageWriter : public QObject
Q_INVOKABLE bool getBoolSetting(const QString &key);
Q_INVOKABLE QString getValue(const QString &key);
Q_INVOKABLE void setSetting(const QString &key, const QVariant &value);
+ Q_INVOKABLE QVariantMap initialConfiguration() const;
Q_INVOKABLE QVariantMap getSavedCustomizationSettings();
Q_INVOKABLE void clearSavedCustomizationSettings();
Q_INVOKABLE bool hasSavedCustomizationSettings();
@@ -190,6 +191,7 @@ class ImageWriter : public QObject
Q_INVOKABLE bool hasMouse();
signals:
+ void settingChanged(const QString &key, const QVariant &value);
/* We are emiting signals with QVariant as parameters because QML likes it that way */
void downloadProgress(QVariant dlnow, QVariant dltotal);
diff --git a/src/openhdimagecustomizer.cpp b/src/openhdimagecustomizer.cpp
index 78135b73e..a4f095ac2 100644
--- a/src/openhdimagecustomizer.cpp
+++ b/src/openhdimagecustomizer.cpp
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -20,85 +21,25 @@
QString OpenHDImageCustomizer::cameraTypeForName(const QString &cameraName, const QString &sbc)
{
- if (cameraName.isEmpty())
- return QString();
-
- qDebug() << "Camera found" << cameraName;
-
- QString cameraType;
- if (sbc == "rpi")
- {
- if (cameraName == "OV5647") cameraType = "30";
- else if (cameraName == "IMX219") cameraType = "31";
- else if (cameraName == "IMX708") cameraType = "32";
- else if (cameraName == "IMX477") cameraType = "33";
- else if (cameraName == "HDMI") cameraType = "20";
- else if (cameraName == "SkyMasterHDR708") cameraType = "40";
- else if (cameraName == "SkyVisionPro519") cameraType = "41";
- else if (cameraName == "IMX477m") cameraType = "42";
- else if (cameraName == "IMX462") cameraType = "43";
- else if (cameraName == "IMX327") cameraType = "44";
- else if (cameraName == "IMX290") cameraType = "45";
- else if (cameraName == "IMX462MINI") cameraType = "46";
- else if (cameraName == "IMX662") cameraType = "47";
- else if (cameraName == "2MPCAMERAS") cameraType = "60";
- else if (cameraName == "CSIMX307") cameraType = "61";
- else if (cameraName == "CSSC137") cameraType = "62";
- else if (cameraName == "MVCAM") cameraType = "63";
- else if (cameraName == "GX/GXC IMX662 (ISP)") cameraType = "64";
- }
- else if (sbc == "zero3w")
- {
- if (cameraName == "HDMI") cameraType = "90";
- else if (cameraName == "IMX462") cameraType = "94";
- else if (cameraName == "IMX519") cameraType = "95";
- else if (cameraName == "IMX219") cameraType = "92";
- else if (cameraName == "OV5647") cameraType = "91";
- else if (cameraName == "IMX708") cameraType = "93";
- else if (cameraName == "VEYE") cameraType = "97";
- else if (cameraName == "OHD-JAGUAR") cameraType = "96";
- }
- else if (sbc == "rock-5b" || sbc == "rock-5a" || sbc == "radxa-cm5")
- {
- if (cameraName == "HDMI") cameraType = "80";
- else if (cameraName == "IMX219") cameraType = "82";
- else if (cameraName == "OV5647") cameraType = "81";
- else if (cameraName == "IMX708") cameraType = "83";
- else if (cameraName == "IMX462") cameraType = "84";
- else if (cameraName == "IMX415") cameraType = "85";
- else if (cameraName == "IMX477") cameraType = "86";
- else if (cameraName == "IMX519") cameraType = "87";
- else if (cameraName == "OHD-JAGUAR") cameraType = "88";
+ QFile file(":/doc/openhd_settings_map.json");
+ if (!file.open(QIODevice::ReadOnly)) return {};
+ const auto camera=QJsonDocument::fromJson(file.readAll()).object().value("camera").toObject();
+ const auto search=[&](const QJsonArray& vendors) -> QString {
+ for (const auto& vendor:vendors) for (const auto& option:vendor.toObject().value("options").toArray()) {
+ const auto object=option.toObject();
+ if (object.value("id").toString()==cameraName) return object.value("valueWritten").toVariant().toString();
+ }
+ return {};
+ };
+ for (const auto& group:camera.value("sbcGroups").toArray()) {
+ const auto object=group.toObject();
+ if (object.value("sbc").toArray().contains(sbc)) {
+ const auto result=search(object.value("vendors").toArray());
+ if (!result.isEmpty()) return result;
+ }
}
+ return search(camera.value("commonVendors").toArray());
- if (cameraName == "FILESRC") cameraType = "4";
- else if (cameraName == "IP-CAMERA") cameraType = "3";
- else if (cameraName == "EXTERNAL") cameraType = "2";
- else if (cameraName == "USB") cameraType = "10";
- else if (cameraName == "TESTPATTERN") cameraType = "0";
- else if (cameraName == "INFIRAY") cameraType = "11";
- else if (cameraName == "INFIRAY_T2") cameraType = "12";
- else if (cameraName == "INFIRAY_X2") cameraType = "13";
- else if (cameraName == "INFIRAY_P2_PRO") cameraType = "14";
- else if (cameraName == "FLIR_VUE" || cameraName == "FLIR VUE") cameraType = "15";
- else if (cameraName == "FLIR_BOSON" || cameraName == "FLIR BOSON") cameraType = "16";
- else if (cameraName == "HDZERO") cameraType = "70";
- else if (cameraName == "RUNCAM_V1") cameraType = "71";
- else if (cameraName == "RUNCAM_V2") cameraType = "72";
- else if (cameraName == "RUNCAM_V3") cameraType = "73";
- else if (cameraName == "RUNCAM_NANO_90") cameraType = "74";
- else if (cameraName == "OHD-JAGUAR-X21") cameraType = "76";
- else if (cameraName == "OPENIPC") cameraType = "110";
- else if (cameraName == "XAVIER-IMX577") cameraType = "101";
- else if (cameraName == "ORQA-HORNET") cameraType = "122";
- else if (cameraName == "ORQA-JAGUAR") cameraType = "123";
- else if (cameraName == "ORQA-REKINDLE") cameraType = "124";
- else if (cameraName == "ROCKCHIP-RV") cameraType = "125";
- else if (sbc == "x20" && cameraName == "OHD-JAGUAR") cameraType = "75";
- else if ((sbc == "qcs405" || sbc == "qrb5165") && cameraName == "IMX577") cameraType = "120";
- else if ((sbc == "qcs405" || sbc == "qrb5165") && cameraName == "OV9282") cameraType = "121";
-
- return cameraType;
}
QJsonObject OpenHDImageCustomizer::buildSettings(const QSettings &settings)
@@ -175,6 +116,8 @@ QJsonObject OpenHDImageCustomizer::buildSettings(const QSettings &settings)
if (!sbc.isEmpty())
openhdSettings.insert("sbc", sbc);
+ if (!settings.value("platform").toString().isEmpty())
+ openhdSettings.insert("platform", settings.value("platform").toString());
if (mode == "debug")
openhdSettings.insert("debug", true);
openhdSettings.insert("disable_ethernet_link", disableEthernetLink);
@@ -205,9 +148,28 @@ QJsonObject OpenHDImageCustomizer::buildSettings(const QSettings &settings)
openhdSettings.insert("fleetcontrol_username", fleetControlUsername);
openhdSettings.insert("fleetcontrol_password", fleetControlPassword);
}
+ const auto defaults=QJsonDocument::fromJson(settings.value("initialParameterDefaults").toString().toUtf8()).object();
+ if (defaults.value("version").toInt()==1 && defaults.value("components").isObject())
+ openhdSettings.insert("parameter_defaults",defaults);
return openhdSettings;
}
+QJsonObject OpenHDImageCustomizer::initialConfiguration(const QSettings &settings)
+{
+ auto file = buildSettings(settings);
+ for (const auto &key : {"fleetcontrol_username", "fleetcontrol_password", "token"}) file.remove(key);
+ QJsonObject selections;
+ for (const auto &key : {"bootType", "platform", "sbc", "camera", "camera2", "cameraResolution", "camera2Resolution",
+ "cameraPort", "camera2Port", "ipCameraAddress", "ipCameraPipeline", "camera2IpCameraAddress", "camera2IpCameraPipeline",
+ "ipCameraBitrate", "displayForceMode", "displayWidth", "displayHeight", "displayRefreshHz", "mode", "hotSpot",
+ "useSettings", "mapboxApiKey", "disableEthernetLink", "initialParameterDefaults", "language"})
+ if (settings.contains(key)) selections.insert(key, QJsonValue::fromVariant(settings.value(key)));
+ selections.insert("mode", file.value("role"));
+ selections.insert("bootType", file.value("role").toString() == "ground" ? "Ground" : "Air");
+ selections.insert("debug", file.value("debug").toBool());
+ return {{"version", 1}, {"settings", file}, {"imageWriterSettings", selections}};
+}
+
OpenHDImageCustomizer::Result OpenHDImageCustomizer::prepareBootSplash(const QString &bootPartition)
{
const QDir boot(bootPartition);
diff --git a/src/openhdimagecustomizer.h b/src/openhdimagecustomizer.h
index b7067e099..41245bdd3 100644
--- a/src/openhdimagecustomizer.h
+++ b/src/openhdimagecustomizer.h
@@ -46,6 +46,7 @@ class OpenHDImageCustomizer final
};
static QJsonObject buildSettings(const QSettings &settings);
+ static QJsonObject initialConfiguration(const QSettings &settings);
static Result prepareBootSplash(const QString &bootPartition);
static Result apply(const QString &bootPartition, const QSettings &settings);
diff --git a/src/qml.qrc b/src/qml.qrc
index acee417c3..f0c84f41f 100644
--- a/src/qml.qrc
+++ b/src/qml.qrc
@@ -58,6 +58,7 @@
qmlcomponents/ImageCardGrid.qml
qmlcomponents/DeviceSelectionPage.qml
qmlcomponents/SettingsSection.qml
+ qmlcomponents/InitialSettingsPanel.qml
qmlcomponents/OperationReviewPage.qml
qmlcomponents/LanguageSettingsPage.qml
qmlcomponents/CacheSettingsSection.qml
diff --git a/src/qmlcomponents/FleetControlPage.qml b/src/qmlcomponents/FleetControlPage.qml
index 3622760d9..609ff3620 100644
--- a/src/qmlcomponents/FleetControlPage.qml
+++ b/src/qmlcomponents/FleetControlPage.qml
@@ -1,6 +1,7 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
+import QtQuick.Dialogs 1.3
import "FleetProfilesHelper.js" as FleetProfilesHelper
Item {
@@ -123,6 +124,9 @@ Item {
"icon": item.craftIcon,
"role": item.craftRole,
"hardware": item.craftHardware,
+ "platform": item.craftPlatform || "",
+ "writerSettings": item.craftWriterSettings || "{}",
+ "initialParameterDefaults": item.craftInitialDefaults || "{\"version\":1,\"components\":{}}",
"description": item.craftDescription,
"cameraVendor": item.craftCameraVendor || "",
"camera": item.craftCamera || "",
@@ -222,12 +226,16 @@ Item {
}
function loadCrafts() {
+ request("GET", "/api/imagewriter/platforms", undefined, function(xhr, response) {
+ if (xhr.status >= 200 && xhr.status < 300 && response && response.version === 1 && FleetProfilesHelper.installPlatforms(response.platforms)) {
+ var copy = JSON.parse(JSON.stringify(root.settingsMap))
+ copy.platforms = response.platforms
+ root.settingsMap = copy
+ }
+ })
var selectedCraftId = imageWriter.getValue("fleetcontrol_craft_id") || ""
loadingCrafts = true
message = ""
- craftModel.clear()
- imageWriter.setSetting(getCacheKey(), "[]")
- activeCraftName = ""
// FleetControl is the source of crafts. The local list is refreshed only after a successful fetch.
request("GET", "/api/imagewriter/profiles", undefined, function(xhr, response) {
@@ -253,6 +261,9 @@ Item {
craftModel.append({
"craftId": profile.id || ("craft_" + i),
+ "craftPlatform": s.platform || "",
+ "craftWriterSettings": JSON.stringify(s),
+ "craftInitialDefaults": s.initialParameterDefaults || "{\"version\":1,\"components\":{}}",
"craftName": profile.name || "",
"craftCategory": category,
"craftIcon": icon,
@@ -296,7 +307,9 @@ Item {
var tempId = (editIdx >= 0 && editIdx < craftModel.count)
? craftModel.get(editIdx).craftId
: ("craft_" + Date.now())
- var sbcKey = sbcKeyFromHardware(hardware)
+ var board = FleetProfilesHelper.platformForHardware(hardware)
+ if (!board) { message = qsTr("Choose a concrete hardware platform."); return }
+ var sbcKey = board.sbc
var craftObj = {
"craftId": tempId,
@@ -333,6 +346,7 @@ Item {
"mode": role,
"bootType": role === "ground" ? "Ground" : "Air",
"sbc": sbcKey,
+ "platform": board.id,
"useSettings": true,
"craftCategory": category,
"craftIcon": icon,
@@ -358,6 +372,12 @@ Item {
}
}
+ if (editIdx >= 0 && editIdx < craftModel.count) {
+ var previousSettings = JSON.parse(craftModel.get(editIdx).craftWriterSettings || "{}");
+ Object.keys(previousSettings).forEach(function(key) {
+ if (payload.openhdSettings[key] === undefined) payload.openhdSettings[key] = previousSettings[key];
+ });
+ }
var existingCloudProfile = editIdx >= 0 && editIdx < craftModel.count &&
String(tempId).indexOf("craft_") !== 0
var method = existingCloudProfile ? "PUT" : "POST"
@@ -377,6 +397,32 @@ Item {
})
}
+ property bool savingConfiguration: false
+ function saveInitialConfiguration() {
+ var craftId = imageWriter.getValue("fleetcontrol_craft_id")
+ if (!signedIn || !craftId || savingConfiguration) return
+ var snapshot = imageWriter.initialConfiguration()
+ savingConfiguration = true
+ request("PUT", "/api/imagewriter/profiles/" + encodeURIComponent(craftId) + "/initial-configuration", snapshot, function(xhr, response) {
+ savingConfiguration = false
+ if (xhr.status >= 200 && xhr.status < 300) {
+ loadCrafts()
+ message = qsTr("Initial configuration saved on this craft.")
+ } else message = response && response.message ? response.message : qsTr("Could not save the initial configuration.")
+ })
+ }
+ FileDialog {
+ id: exportConfigurationDialog
+ title: qsTr("Save initial OpenHD configuration")
+ selectExisting: false
+ nameFilters: ["JSON files (*.json)"]
+ onAccepted: {
+ var localPath = decodeURIComponent(String(fileUrl).replace(/^file:\/\//, ""))
+ if (Qt.platform.os === "windows" && localPath.charAt(0) === "/") localPath = localPath.substring(1)
+ message = imageWriter.writeTextFile(localPath, JSON.stringify(imageWriter.initialConfiguration().settings, null, 2) + "\n")
+ ? qsTr("Initial configuration exported.") : qsTr("Could not export the initial configuration.")
+ }
+ }
function deleteCraft(index) {
if (index >= 0 && index < craftModel.count) {
var item = craftModel.get(index)
@@ -741,7 +787,8 @@ Item {
editorSelectedCategory = craft.craftCategory || "craft"
editorSelectedIcon = craft.craftIcon || (editorSelectedCategory === "craft" ? "survey" : "station-military-gcs")
editorSelectedFamily = familyForIcon(editorSelectedIcon)
- editorSelectedHardware = craft.craftHardware || "Raspberry Pi 4"
+ var board = FleetProfilesHelper.platformForHardware(craft.craftPlatform || craft.craftHardware)
+ editorSelectedHardware = board ? board.label : craft.craftHardware || ""
craftNameField.text = craft.craftName || ""
craftDescField.text = craft.craftDescription || ""
@@ -1216,6 +1263,17 @@ Item {
elide: Text.ElideRight
}
+ ModernActionButton {
+ text: root.savingConfiguration ? qsTr("Saving…") : qsTr("Save configuration")
+ enabled: root.signedIn && !root.savingConfiguration
+ implicitHeight: 26
+ onClicked: root.saveInitialConfiguration()
+ }
+ ModernActionButton {
+ text: qsTr("Export JSON")
+ implicitHeight: 26
+ onClicked: exportConfigurationDialog.open()
+ }
ModernActionButton {
text: qsTr("Clear Selection")
implicitHeight: 26
@@ -2001,19 +2059,7 @@ Item {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
- model: [
- "Raspberry Pi 4",
- "Raspberry Pi 5",
- "Raspberry Pi CM4",
- "Raspberry Pi Zero 2W",
- "Radxa Zero 3W",
- "Radxa Rock 3",
- "Radxa Rock 5B",
- "Radxa CM5",
- "OpenHD Core X20",
- "OpenHD Core X21",
- "PC / x86_64 Ground Station"
- ]
+ model: (root.settingsMap.platforms || []).map(function(p) { return p.label })
background: Rectangle { color: "transparent" }
onCurrentTextChanged: {
root.editorSelectedHardware = currentText
diff --git a/src/qmlcomponents/FleetProfilesHelper.js b/src/qmlcomponents/FleetProfilesHelper.js
index 6a7ec6f13..183717701 100644
--- a/src/qmlcomponents/FleetProfilesHelper.js
+++ b/src/qmlcomponents/FleetProfilesHelper.js
@@ -46,17 +46,49 @@ function iconSource(key) {
return map[key] || "../icons/ui/hub.svg";
}
+function platformForHardware(hw) {
+ var key = String(hw || "").toLowerCase();
+ var platforms = _cachedSettingsMap ? _cachedSettingsMap.platforms || [] : [];
+ for (var i=0; i 512) return false;
+ var seen = {};
+ for (var i=0; i= 0) return "x21";
- if (hw.indexOf("x20") >= 0) return "x20";
- if (hw.indexOf("rock 5") >= 0 || hw.indexOf("rock-5") >= 0 || hw.indexOf("cm5") >= 0 || hw.indexOf("rk3588") >= 0) return "rock-5b";
- if (hw.indexOf("zero 3") >= 0 || hw.indexOf("zero3") >= 0 || hw.indexOf("rock 3") >= 0 || hw.indexOf("cm3") >= 0 || hw.indexOf("rk3566") >= 0) return "zero3w";
- if (hw.indexOf("x86") >= 0 || hw.indexOf("pc") >= 0 || hw.indexOf("laptop") >= 0) return "x86";
- if (hw.indexOf("qualcomm") >= 0 || hw.indexOf("qcs") >= 0 || hw.indexOf("qrb") >= 0) return "qcs405";
- if (hw.indexOf("openipc") >= 0) return "openipc";
- if (hw.indexOf("raspberry") >= 0 || hw.indexOf("rpi") >= 0) return "rpi";
- return "rpi";
+ var platform = platformForHardware(hw);
+ return platform ? platform.sbc : "";
}
function getVendorsForSbc(settingsMap, sbcKey) {
@@ -220,6 +252,9 @@ function loadProfiles(imageWriter) {
if (p && p.name) {
list.push({
craftId: p.id || ("craft_" + i),
+ craftPlatform: p.platform || "",
+ craftWriterSettings: p.writerSettings || "{}",
+ craftInitialDefaults: p.initialParameterDefaults || "{\"version\":1,\"components\":{}}",
craftName: p.name,
craftCategory: p.category || "craft",
craftIcon: p.icon || (p.category === "station" ? "station-military-gcs" : "survey"),
@@ -258,11 +293,15 @@ function loadProfiles(imageWriter) {
function applyProfile(imageWriter, p) {
if (!imageWriter || !p) return;
var role = p.craftRole || (p.craftCategory === "station" ? "ground" : "air");
- var sbcKey = sbcKeyFromHardware(p.craftHardware);
+ var board = platformForHardware(p.craftPlatform || p.craftHardware);
+ var sbcKey = board ? board.sbc : "";
imageWriter.setSetting("mode", role);
imageWriter.setSetting("bootType", role === "ground" ? "Ground" : "Air");
imageWriter.setSetting("sbc", sbcKey);
+ imageWriter.setSetting("platform", board ? board.id : "");
+ // Clear a previous craft plan when this profile has no defaults.
+ imageWriter.setSetting("initialParameterDefaults", p.craftInitialDefaults || "{\"version\":1,\"components\":{}}");
imageWriter.setSetting("useSettings", true);
imageWriter.setSetting("fleetcontrol_craft_id", p.craftId || "");
imageWriter.setSetting("fleetcontrol_craft_name", p.craftName || "");
@@ -286,6 +325,13 @@ function applyProfile(imageWriter, p) {
imageWriter.setSetting("camera2IpCameraPipeline", role === "air" ? (p.craftCamera2IpPipeline || "") : "");
imageWriter.setSetting("ipCameraBitrate", p.craftIpCameraBitrate || 2);
+ try {
+ var extra = JSON.parse(p.craftWriterSettings || "{}");
+ ["disableEthernetLink", "language"].forEach(function(key) {
+ if (extra[key] !== undefined) imageWriter.setSetting(key, extra[key]);
+ });
+ imageWriter.setSetting("mode", extra.debug ? "debug" : role);
+ } catch(e) {}
imageWriter.setSetting("hotSpot", p.craftHotSpot || "");
if (role === "ground") {
diff --git a/src/qmlcomponents/InitialSettingsPanel.qml b/src/qmlcomponents/InitialSettingsPanel.qml
new file mode 100644
index 000000000..a307460b2
--- /dev/null
+++ b/src/qmlcomponents/InitialSettingsPanel.qml
@@ -0,0 +1,93 @@
+import QtQuick 2.9
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.0
+import Qt.labs.settings 1.0
+
+GroupBox {
+ id: panel
+ title: qsTr("Initial OpenHD parameters")
+ property var fields: []
+ property var settingsOwner: null
+ property string role: ""
+ property var defaults: ({version: 1, components: {}})
+ readonly property var visibleFields: fields.filter(function(f) {
+ return f.provision && f.provision.roles.indexOf(role) >= 0
+ })
+ visible: visibleFields.length > 0
+ Settings { id: store; property string initialParameterDefaults: "{}" }
+ Component.onCompleted: {
+ loadDefaults(settingsOwner ? settingsOwner.getValue("initialParameterDefaults") : store.initialParameterDefaults)
+ }
+ function loadDefaults(raw) {
+ try { var d = JSON.parse(raw); if (d.version === 1 && d.components) { defaults = d; return } } catch(e) {}
+ defaults = {version: 1, components: {}}
+ }
+ Connections {
+ target: panel.settingsOwner
+ function onSettingChanged(key, value) { if (key === "initialParameterDefaults") panel.loadDefaults(String(value)) }
+ }
+ function hasValue(field) {
+ var values = defaults.components[String(field.provision.componentId)]
+ return !!(values && values[field.id] !== undefined)
+ }
+ function value(field) {
+ var values = defaults.components[String(field.provision.componentId)]
+ return hasValue(field) ? values[field.id] : field.choices && field.choices.length ? field.choices[0].value : field.type === "int32" ? 0 : ""
+ }
+ function setValue(field, value, enabled) {
+ var copy = JSON.parse(JSON.stringify(defaults))
+ var component = String(field.provision.componentId)
+ if (!copy.components[component]) copy.components[component] = {}
+ if (enabled) copy.components[component][field.id] = value
+ else delete copy.components[component][field.id]
+ defaults = copy
+ store.initialParameterDefaults = JSON.stringify(copy)
+ if (settingsOwner) settingsOwner.setSetting("initialParameterDefaults", JSON.stringify(copy))
+ }
+ function exportDefaults() {
+ // Preserve explicit file parameters that a newer firmware knows about.
+ var result = JSON.parse(JSON.stringify(defaults))
+ if (role === "ground") { delete result.components["100"]; delete result.components["101"] }
+ fields.forEach(function(field) {
+ if (!field.provision || field.provision.roles.indexOf(role) >= 0) return
+ var values = result.components[String(field.provision.componentId)]
+ if (values) delete values[field.id]
+ })
+ return result
+ }
+ ColumnLayout {
+ anchors.fill: parent
+ Label { text: qsTr("Applied only when OpenHD creates new settings. Existing user settings are preserved."); wrapMode: Text.WordWrap; Layout.fillWidth: true }
+ Repeater {
+ model: panel.visibleFields
+ delegate: ColumnLayout {
+ Layout.fillWidth: true
+ property var field: modelData
+ CheckBox {
+ text: field.label
+ checked: panel.hasValue(field)
+ onClicked: panel.setValue(field, panel.value(field), checked)
+ }
+ ComboBox {
+ visible: field.choices && field.choices.length > 0
+ enabled: panel.hasValue(field)
+ Layout.fillWidth: true
+ model: field.choices || []
+ textRole: "label"
+ currentIndex: {
+ for (var n=0; nJSON.stringify(map),getValue:key=>settings[key] || '',setSetting:(key,value)=>settings[key]=value};
+context.getSettingsMap(writer);
+for(const board of map.platforms) {
+ assert.strictEqual(context.platformForHardware(board.id).sbc,board.sbc);
+ context.applyProfile(writer,{craftId:'craft-1',craftRole:board.roles[0],craftPlatform:board.id,craftHardware:board.label,
+ craftCamera:'USB',craftInitialDefaults:'{"version":1,"components":{"100":{"AIR_RECORDING_E":2}}}',craftWriterSettings:'{"disableEthernetLink":false}'});
+ assert.strictEqual(settings.platform,board.id);
+ assert.strictEqual(settings.sbc,board.sbc);
+ assert.strictEqual(JSON.parse(settings.initialParameterDefaults).components['100'].AIR_RECORDING_E,2);
+ assert.strictEqual(settings.disableEthernetLink,false);
+}
+assert.strictEqual(context.platformForHardware('Raspberry Pi (Pi 4 / Pi 5 / CM4 / Zero 2W)'),null);
+assert.strictEqual(context.platformForHint('openhd-rpi5-bullseye.img').id,'rpi5');
+assert.strictEqual(context.platformForHint('openhd-rpi-bullseye.img'),null);
+context.applyProfile(writer,{craftRole:'ground',craftPlatform:'rpi4',craftCamera:'USB',craftCamera2:'USB'});
+assert.deepStrictEqual(JSON.parse(settings.initialParameterDefaults),{version:1,components:{}});
+assert.strictEqual(settings.camera,''); assert.strictEqual(settings.camera2,'');
+console.log(`PROFILE ROUND TRIP AND ${map.platforms.length} SHARED PLATFORMS PASSED`);
diff --git a/src/test/initialsettings_qml_test.cpp b/src/test/initialsettings_qml_test.cpp
new file mode 100644
index 000000000..33a03fc2b
--- /dev/null
+++ b/src/test/initialsettings_qml_test.cpp
@@ -0,0 +1,43 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+class Owner : public QObject {
+ Q_OBJECT
+public:
+ QString raw = R"({"version":1,"components":{"100":{"AIR_RECORDING_E":2},"191":{"CUSTOM_NEW":7}}})";
+ Q_INVOKABLE QString getValue(const QString&) const { return raw; }
+ Q_INVOKABLE void setSetting(const QString &key,const QVariant &value) { raw=value.toString();emit settingChanged(key,value); }
+signals:
+ void settingChanged(const QString &key,const QVariant &value);
+};
+int main(int argc,char**argv) {
+ QGuiApplication app(argc,argv); app.setOrganizationName("OpenHDSettingsQmlTests");app.setApplicationName("Defaults");
+ QFile catalog(":/doc/openhd_settings_map.json");if(!catalog.open(QIODevice::ReadOnly)) return 1;
+ const auto map=QJsonDocument::fromJson(catalog.readAll()).object();
+ Owner owner; QQmlEngine engine;
+ QQmlComponent component(&engine,QUrl::fromLocalFile(QStringLiteral(INITIAL_SETTINGS_QML_PATH)));
+ QObject* panel=component.beginCreate(engine.rootContext());
+ if(!panel) {qCritical()<setProperty("settingsOwner",QVariant::fromValue(static_cast(&owner)));
+ panel->setProperty("fields",map.value("initialFields").toArray().toVariantList());panel->setProperty("role","air");
+ component.completeCreate();engine.globalObject().setProperty("panel",engine.newQObject(panel));
+ panel->setProperty("fields",map.value("initialFields").toArray().toVariantList());panel->setProperty("role","air");
+ const auto exported=[&] { const auto value=engine.evaluate("JSON.stringify(panel.exportDefaults())");
+ if(value.isError()) std::cerr<setProperty("role","ground");values=exported().value("components").toObject().value("100").toObject();
+ if(!values.isEmpty() || exported().value("components").toObject().value("191").toObject().value("CUSTOM_NEW").toInt()!=7) return 5;
+ delete panel;std::cout<<"INITIAL SETTINGS QML PROFILE REFRESH AND ROLE FILTER PASSED"<../../doc/openhd_settings_map.json
diff --git a/src/windows/openhdimagewriter.nsi.in b/src/windows/openhdimagewriter.nsi.in
index abed32421..6b76aa894 100644
--- a/src/windows/openhdimagewriter.nsi.in
+++ b/src/windows/openhdimagewriter.nsi.in
@@ -34,7 +34,12 @@ VIAddVersionKey "FileVersion" "${VERSION}"
SetCompressor LZMA
Name "${APP_NAME}"
Caption "${APP_NAME}"
+!ifdef INNER
+RequestExecutionLevel user
+OutFile "ImageWriter-uninstaller-generator.exe"
+!else
OutFile "${INSTALLER_NAME}"
+!endif
BrandingText "${APP_NAME}"
XPStyle on
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
@@ -181,6 +186,7 @@ LangString win7Msg ${LANG_ITALIAN} "Per l'esecuzione del programma è richiesto
######################################################################
+!ifndef INNER
Section -MainProgram
${INSTALL_TYPE}
!insertmacro TerminateApp
@@ -285,8 +291,12 @@ SectionEnd
Section -Icons_Reg
!ifndef INNER
SetOutPath "$INSTDIR"
+!ifdef SIGNED_UNINSTALLER
+File /oname=uninstall.exe "${SIGNED_UNINSTALLER}"
+!else
WriteUninstaller "$INSTDIR\uninstall.exe"
!endif
+!endif
!ifdef REG_START_MENU
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
@@ -332,6 +342,12 @@ SectionEnd
######################################################################
+!else
+; The generator needs only the uninstaller code, not the application payload.
+Section -GenerateUninstaller
+SectionEnd
+!endif
+
Section Uninstall
${INSTALL_TYPE}
Delete "$INSTDIR\D3Dcompiler_47.dll"
@@ -446,8 +462,17 @@ Function .onInit
; the uninstaller. This is better than processing a command line option as it means
; this entire code path is not present in the final (real) installer.
SetSilent silent
- WriteUninstaller "$%TEMP%\uninstall.exe"
+ !ifndef UNINSTALLER_OUTPUT
+ !error "INNER requires UNINSTALLER_OUTPUT"
+ !endif
+ ClearErrors
+ WriteUninstaller "${UNINSTALLER_OUTPUT}"
+ IfErrors generator_failed
+ SetErrorLevel 0
Quit ; just bail out quickly when running the "inner" installer
+generator_failed:
+ SetErrorLevel 1
+ Quit
!else
${IfNot} ${AtLeastWin7}
MessageBox MB_OK "$(win7Msg)"