Skip to content

[feature] Implement FlexRadio profile import/export#2641

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/flex-profile-import-export
May 14, 2026
Merged

[feature] Implement FlexRadio profile import/export#2641
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/flex-profile-import-export

Conversation

@jensenpat

@jensenpat jensenpat commented May 13, 2026

Copy link
Copy Markdown
Collaborator
Screenshot 2026-05-13 at 6 13 44 PM

Summary

Adds SmartSDR-compatible profile import/export for AetherSDR using the FlexRadio radio-side database transfer flow.

  • Replaces the empty Profiles -> Import/Export Profiles... action with a non-modal import/export dialog.
  • Adds an async ProfileTransfer backend for .ssdr_cfg export/import using FlexRadio file upload / file download database package commands.
  • Builds SmartSDR-compatible meta_subset requests for selected categories: Global Profiles, TX Profiles, MIC Profiles, Memories, Preferences, TNF, XVTR, and USB Cables.
  • Imports .ssdr_cfg backup packages by preparing the import manifest wrapper expected by the radio while leaving the radio database payload untouched.
  • Adds version-risk and destructive-import warnings, transfer cancel/timeout handling, and profile refresh after import.
  • Updates help text and adds a manual integration checklist.

Why

The previous menu entry was a TODO, and the closed JSON/name-list approach would not produce or restore real SmartSDR-compatible backups. SmartSDR profile backup/restore is a radio database package transfer; this implementation lets the radio create and apply those packages instead of parsing or rewriting the database locally.

Fixes #1415. Addresses duplicate reports #2319, #2581, and #2625.

Validation

  • Built locally with cmake --build build -j8.
  • Ran ctest --test-dir build -R profile_transfer_test --output-on-failure.
  • Live-tested export/import with a radio: select-all export produced a zipped .ssdr_cfg containing flex_payload, memories.csv, and meta_data; import flow restored the tested package after switching to the SmartSDR-style import wrapper.

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat

@jensenpat jensenpat changed the title Implement FlexRadio profile import/export [feature] Implement FlexRadio profile import/export May 13, 2026
@jensenpat jensenpat marked this pull request as ready for review May 14, 2026 01:15

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jensenpat — this is a substantial feature and the radio-driven approach (let the radio build/apply the package, don't rewrite it locally) is the right architectural call. The async state machine in ProfileTransfer is well factored, the dialog uses AppSettings correctly, and frameless support is wired through setFramelessMode. A few findings worth addressing before merge:

1. Dead status flags on RadioModel — the import-watch signal never fires (real bug)

src/models/RadioModel.h adds:

void profileDatabaseImportingChanged(bool importing);
void profileDatabaseExportingChanged(bool exporting);
bool        m_profileDatabaseImporting{false};
bool        m_profileDatabaseExporting{false};

…and ProfileTransfer relies on them:

// ProfileTransfer.cpp
connect(m_model, &RadioModel::profileDatabaseImportingChanged, this, [this](bool importing) {
    if (m_phase == Phase::WaitingForImport && !importing)
        scheduleImportCompletion();
});
...
if (m_model && m_model->profileDatabaseImporting()) {
    m_timeout->start(kOverallTimeoutMs / 2);
    return;
}
scheduleImportCompletion();

But grepping RadioModel.cpp for either flag shows the only place they're touched is onDisconnected() — nothing ever sets them to true or emits the signal during a real upload/import. So:

  • ProfileTransfer::waitForImportCompletion() always immediately falls through to the kImportSettleMs = 5000 ms timer (5 s blanket wait, regardless of actual radio progress), and
  • the profileDatabaseImportingChanged connection is dead code that will never fire on a healthy import.

Either remove the unused infrastructure and lean on the timer, or wire up an actual setter that's flipped from the appropriate radio status message (e.g., the same place that ultimately tells the GUI the radio is busy applying the package). Right now it looks like the integration is half-finished.

2. Response-callback lifetime captures bare this

requestUploadPort / requestPackageDownload pass lambdas that capture this into RadioModel::sendCmd, which stores them in m_pendingCallbacks keyed by sequence number. The dialog's closeEvent does ask before closing while busy, but if the radio never replies, or the parent goes away during connection teardown for another reason, the callback can fire on a freed ProfileTransfer. Consider guarding with a QPointer<ProfileTransfer> (or a QPointer-style watcher) so the lambda no-ops on dead receivers — this is cheap and matches how the Qt-side code already uses QPointer<ProfileImportExportDialog>.

3. CI/installer scope (out of stated PR scope, but justified)

.github/workflows/ci.yml and windows-installer.yml install zlib:x64-windows via vcpkg and copy zlib*.dll into deploy\. That's outside the headline feature but is a direct dep of the new ZIP code on Windows; Linux/macOS are already covered by the new find_package(ZLIB REQUIRED) and system zlib. Worth calling out in the PR body so reviewers (or whoever inherits a Windows CI break) understand why those workflows changed.

4. Minor

  • validateImportFile opens the file just to confirm it's readable, then importDatabase opens it again to read it. Functionally fine (QFile destructor closes), but you can return true on the first successful info.isReadable() check without an open call.
  • commitExportFile reads the first 4 bytes, checks for "PK", and only qCWarnings on mismatch — given the radio is contractually returning a ZIP and the rest of the pipeline assumes that, consider promoting this to a hard fail so a corrupt download doesn't get saved silently as .ssdr_cfg.
  • appendProfileSubsetLine emits the tag line even when names is empty (e.g., the radio reports zero global profiles but the user ticked the box). If FlexRadio interprets "GLOBAL_PROFILES^\r\n" as "all global profiles," that's intentional — worth a one-line code comment so future readers don't second-guess it.

Test coverage of buildMetaSubset is a nice touch; expanding it to round-trip a ZIP through writeStoredZipreadZipEntries would catch regressions in the framing code without needing a radio.

Nothing on the AetherSDR conventions side stood out (AppSettings, Qt parent ownership, frameless wiring, RAII for QSaveFile/QTcpSocket look right). Item 1 is the one I'd want resolved before this lands.

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. FlexLib protocol cited and verified (db_meta_subset upload, db_package download, fallback port 42607 all match Radio.cs). Solid async state machine with multi-layer timeouts, atomic file writes, MOX/TUNE/PTT guards. Tests pass locally on merged tree. Filing two follow-ups (dialog geometry persistence; consider bundling zlib instead of vcpkg).

@ten9876 ten9876 merged commit e54843c into aethersdr:main May 14, 2026
5 checks passed
ten9876 added a commit that referenced this pull request May 14, 2026
Resolves MainWindow.cpp conflict in setFramelessWindow().  The PR
removes the m_profileManagerDialog->setFramelessMode(on) call (Profile
Manager is now a PersistentDialog subclass and auto-propagates via
m_persistentDialogs).  Main added m_profileImportExportDialog->
setFramelessMode(on) from PR #2641 — Profile Import/Export is NOT yet
migrated to PersistentDialog, so its explicit cast stays.
ten9876 pushed a commit that referenced this pull request May 14, 2026
…2644)

Introduces PersistentDialog — base class that consolidates the lazy-construct + non-modal + geometry-persist + frameless-chrome boilerplate that 7+ dialogs (Profile Manager, Memory, SpotHub, DSP, MultiFlex, MidiMapping, PanLayout, NetworkDiagnostics) had been hand-rolling. Migrates ProfileManagerDialog as the proof-of-concept; other dialogs land as follow-up issues.

PersistentDialog owns:
- Outer layout (FramelessWindowTitleBar + bodyWidget content slot)
- FramelessResizer::install
- setFramelessMode with wasVisible-guarded geometry preserve and body margin nudge
- Geometry persistence (base64 in AppSettings) with crash-resilient save semantics — in-memory on move/resize, disk-flush on close. Restore deferred to first showEvent so subclasses can call setMinimumSize in their own ctor without clipping.
- m_restoringGeometry guard during restore, m_geometryRestored gate against pre-show native-window move/resize events overwriting saved geometry.

MainWindow::showOrRaisePersistent template helper collapses the per-menu-callback boilerplate to one line. Auto-registers each instance into m_persistentDialogs (QPointer list, auto-prunes nulls on iteration) so setFramelessWindow() can propagate the toggle without explicit qobject_cast branches.

Profile Manager migration is strictly subtractive: 99 LOC removed from ProfileManagerDialog.cpp, 27 from the header. All hand-rolled setFramelessMode / closeEvent / moveEvent / resizeEvent / saveGeometry / restoreGeometry / FramelessResizer::install / m_titleBar / m_bodyLayout members removed.

Required three iterations to land: QTabWidget forward-decl needed to be outside the AetherSDR namespace; first merge from main brought in PR #2641's setFramelessWindow conflict; second merge picked up PR #2662's m_daxBridge guard so Windows builds. Final state has all five CI checks green.

Closes #2605.

Co-Authored-By: AetherClaude <aethersdr-agent@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 15, 2026
…III. (#2698)

zlib was added by PR #2641 as a runtime dependency for raw-deflate
decompression of .ssdr_cfg ZIP packages.  The original implementation
linked the system zlib on Linux/macOS and pulled vcpkg's zlib on
Windows + copied zlib1.dll into the installer.  That split-source
pattern deviates from the constitution's Technology Constraint:

  > Third-party: vendor under third_party/ with THIRD_PARTY_LICENSES
  > updated. Prefer bundled libraries over package-manager dependencies
  > for portability (see libmosquitto bundling for MQTT, #699).

This change bundles zlib 1.3.1 in third_party/zlib/ (vendored from
madler/zlib's GitHub release; sha256
9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23) and
links zlibstatic via the upstream CMakeLists.txt:

  CMakeLists.txt
    - find_package(ZLIB REQUIRED)
    + set(ZLIB_BUILD_EXAMPLES OFF) / SKIP_INSTALL_ALL ON
    + add_subdirectory(third_party/zlib EXCLUDE_FROM_ALL)
    - ZLIB::ZLIB
    + zlibstatic

  .github/workflows/ci.yml
    - "Install zlib via vcpkg" step (sets VCPKG_ROOT, runs vcpkg install)
    + small comment pointing at the bundled tree

  .github/workflows/windows-installer.yml
    - "Install zlib via vcpkg" step (same as ci.yml)
    - zlib*.dll copy into deploy/ (no DLL needed; statically linked)

  THIRD_PARTY_LICENSES — adds the zlib entry (entry #13).

Trimmed from the vendored tree (kept lean while still building):
amiga/ contrib/ doc/ examples/ msdos/ nintendods/ old/ os400/ qnx/
watcom/.  Kept win32/ because zlib's own CMakeLists.txt references
win32/zlib1.rc for the shared-library target unconditionally.

Verified:
  * cmake configure clean, AetherSDR builds + links clean (no
    direct NEEDED on libz in objdump -p; only Qt's transitive
    use of system zlib remains, unrelated to our code).
  * profile_transfer_test passes.
  * Single source-of-truth zlib version across Linux, macOS, and
    Windows.  No vcpkg cache miss → Windows CI no longer waits on
    vcpkg's first-build slow path.  Self-contained Windows installer
    artifact (no zlib1.dll to ship separately).

Closes #2651.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 22, 2026
… PR 8) (#2953)

Eight docs leave the top-level docs/ tree, partitioned by purpose:

docs/archive/ — historical planning/session notes, kept for grep
context:
  - issue-2136-feasibility-report.md     (PR #2759, shipped)
  - issue-2136-implementation-plan.md    (PR #2759, shipped)
  - TX_SYNC_FIX_REPORT.md                 (PR #353, RadioModel.cpp still
    references the report by name)
  - aether-dsp-session-notes.md          (#796 maintainer notebook)

docs/data/ — reference fixtures (not bundled, not runtime):
  - SSDR.settings + SmartSDR.exe.config   (SmartSDR parity reference)
  - rxapplet_mode_settings.csv            (per-mode RX control research)
  - vfo_mode_filters.csv                  (VFO filter-preset research;
    VfoWidget.cpp comment updated to new path)

Kept in docs/ (still active):
  - audio_test_plan.md  (17 KB, edited 2026-04-29, ongoing test plan)
  - profile-import-export-manual-checklist.md (PR #2641 QA checklist)

Both new dirs include a one-paragraph README explaining the convention
so a future maintainer doesn't have to guess what belongs where.

Cross-references audited:
  - AppSettings.h:9 mentions "SmartSDR's SSDR.settings" as a concept,
    not a path — no change.
  - RadioModel.cpp:3991 references TX_SYNC_FIX_REPORT by name — no
    change, file is findable via grep/find.
  - VfoWidget.cpp:3324 carried a literal "docs/vfo_mode_filters.csv"
    path — updated to docs/data/.

Verified with a clean local Release build (cmake --build … --target
AetherSDR exits 0).

After this PR, docs/ contains 18 markdown files (architecture/user
docs), the assets/ subdir from PR 5, and the new archive/ + data/
subdirs — matches the acceptance criteria in the cleanup plan.

Principle I.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…sdr#2933, PR 8) (aethersdr#2953)

Eight docs leave the top-level docs/ tree, partitioned by purpose:

docs/archive/ — historical planning/session notes, kept for grep
context:
  - issue-2136-feasibility-report.md     (PR aethersdr#2759, shipped)
  - issue-2136-implementation-plan.md    (PR aethersdr#2759, shipped)
  - TX_SYNC_FIX_REPORT.md                 (PR aethersdr#353, RadioModel.cpp still
    references the report by name)
  - aether-dsp-session-notes.md          (aethersdr#796 maintainer notebook)

docs/data/ — reference fixtures (not bundled, not runtime):
  - SSDR.settings + SmartSDR.exe.config   (SmartSDR parity reference)
  - rxapplet_mode_settings.csv            (per-mode RX control research)
  - vfo_mode_filters.csv                  (VFO filter-preset research;
    VfoWidget.cpp comment updated to new path)

Kept in docs/ (still active):
  - audio_test_plan.md  (17 KB, edited 2026-04-29, ongoing test plan)
  - profile-import-export-manual-checklist.md (PR aethersdr#2641 QA checklist)

Both new dirs include a one-paragraph README explaining the convention
so a future maintainer doesn't have to guess what belongs where.

Cross-references audited:
  - AppSettings.h:9 mentions "SmartSDR's SSDR.settings" as a concept,
    not a path — no change.
  - RadioModel.cpp:3991 references TX_SYNC_FIX_REPORT by name — no
    change, file is findable via grep/find.
  - VfoWidget.cpp:3324 carried a literal "docs/vfo_mode_filters.csv"
    path — updated to docs/data/.

Verified with a clean local Release build (cmake --build … --target
AetherSDR exits 0).

After this PR, docs/ contains 18 markdown files (architecture/user
docs), the assets/ subdir from PR 5, and the new archive/ + data/
subdirs — matches the acceptance criteria in the cleanup plan.

Principle I.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Import/Export menu does not work.

2 participants