Skip to content

feat(automation): Panadapter and Waterfall bridge surface — antennas, CWX, floors, pan mgmt (#3646)#3832

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:feat/3646-bridge-tx-testing
Jun 26, 2026
Merged

feat(automation): Panadapter and Waterfall bridge surface — antennas, CWX, floors, pan mgmt (#3646)#3832
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:feat/3646-bridge-tx-testing

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Expands the agent automation bridge with the verbs and read-fields a TX/spectrum behavioural test actually needs. Every one was exercised live against a FLEX radio while reproducing and verifying the #3804 FFT-floor work (companion fix PR #3831). No behaviour changes outside AETHER_AUTOMATION.

New verbs

Verb Purpose TX-gated
slice txant <ANT> / slice rxant <ANT> Set TX/RX antenna deterministically — the GUI controls are blocking QMenu::exec() popups invoke() can't drive. Routes through SliceModel::setTx/RxAntenna. no
floors Lightweight per-pan FFT floor read (displayFloorDbm/noiseFloorDbm/panIndex), ~15 ms vs dumpTree's ~250 ms — fast enough to sample a sub-second post-TX transient. no
cwx send <text> / cwx speed <wpm> / cwx stop Drive the CWX keyer (CW's rapid TX→RX edges are the easy floor-recovery repro). send only
txwaterfall on|off Toggle the radio's show_tx_in_waterfall so a test can confirm keyed TX paints the waterfall. no
pan create / pan remove <id> / pan center <mhz> Add/remove a panadapter and recenter it (band-change lever — plain tune uses autopan=0 and clamps to the pan's range). Multi-band coverage. no

New read-fields / properties

  • txAntenna on the slice snapshot (get slice) — previously unreadable, so the live TX antenna couldn't be asserted before keying. This is what makes a dummy-load TX-safety gate enforceable.
  • showTxInWaterfall on the transmit snapshot (get transmit).
  • noiseFloorDbm / displayFloorDbm / panIndex Q_PROPERTYs on SpectrumWidget, surfaced generically via QObject::property() in describeWidget/floors — no core→gui coupling. displayFloorDbm reflects the smoothed trace (m_smoothed); noiseFloorDbm tracks the raw frame.

Safety

cwx send rides the existing AETHER_AUTOMATION_ALLOW_TX rail and arms the force-unkey watchdog, same as txtest/key. Antenna/floor/pan verbs don't key.

Known follow-ups (not in this PR)

  • RadioModel::removePanadapter sends display pan close, which doesn't close a panafall-created pan — the second pan had to be torn down via slice removal.
  • grab returns the first SpectrumWidget in a multi-pan layout — a grab pan <index> selector (keyed on the new panIndex) would let a driver capture a specific pan.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

… mgmt (aethersdr#3646)

Expands the agent automation bridge with the verbs and read-fields a
TX/spectrum behavioural test actually needs. All exercised live against
a FLEX radio while reproducing/verifying the aethersdr#3804 FFT-floor work.

New verbs:
- slice txant <ANT> / rxant <ANT> — set TX/RX antenna deterministically.
  The GUI controls are blocking QMenu::exec() popups invoke() can't drive;
  these route through SliceModel::setTx/RxAntenna. Reading txAntenna back
  is what makes the dummy-load TX-safety gate enforceable at all.
- floors — lightweight per-pan FFT noise-floor read (displayFloorDbm /
  noiseFloorDbm / panIndex). dumpTree serialises ~2600 widgets (~250 ms),
  far too slow to sample a sub-second post-TX transient; this touches only
  widgets exposing the floor Q_PROPERTY (~15 ms, pollable at 20+ Hz).
- cwx send <text> | speed <wpm> | stop — drive the CWX keyer. CW's rapid
  TX→RX edges are the easy reproduction vehicle for post-TX floor recovery.
  send is gated by AETHER_AUTOMATION_ALLOW_TX and arms the force-unkey
  watchdog; speed/stop do not key.
- txwaterfall on|off — toggle the radio's show_tx_in_waterfall flag so a
  test can confirm keyed-up TX paints the waterfall, not just the trace.
- pan create | remove <id> | center <mhz> — add/remove a panadapter and
  recenter it (the band-change lever; plain tune uses autopan=0 so it
  clamps to the pan's RF range). Enables multi-band test coverage.

New read-fields / properties:
- txAntenna on the slice snapshot (get slice) — previously unreadable, so
  the live TX antenna couldn't be asserted before keying.
- showTxInWaterfall on the transmit snapshot (get transmit).
- noiseFloorDbm / displayFloorDbm / panIndex Q_PROPERTYs on SpectrumWidget,
  surfaced generically via QObject::property() in describeWidget (dumpTree)
  and the floors verb — no core->gui coupling. displayFloorDbm reflects the
  smoothed trace (m_smoothed); noiseFloorDbm tracks the raw frame.

Known follow-ups (not in this PR): RadioModel::removePanadapter sends
`display pan close` which doesn't close a panafall-created pan; and grab
can't target a specific pan's SpectrumWidget in a multi-pan layout
(returns the first) — a `grab pan <index>` selector would close that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensenpat jensenpat changed the title feat(automation): TX-test bridge surface — antennas, CWX, floors, pan mgmt (#3646) feat(automation): Panadapter and Waterfall bridge surface — antennas, CWX, floors, pan mgmt (#3646) Jun 26, 2026
@jensenpat jensenpat marked this pull request as ready for review June 26, 2026 06:29
@jensenpat jensenpat requested a review from a team as a code owner June 26, 2026 06:29

@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.

Reviewed the diff against the codebase — all referenced APIs resolve cleanly (SliceModel::txAntenna()/setTxAntenna()/setRxAntenna()/txAntennaList()/rxAntennaList(), TransmitModel::showTxInWaterfall(), RadioModel::createPanadapter()/removePanadapter()/setPanCenter()/maxPanadapters()/cwxModel(), CwxModel::send()/setSpeed()/clearBuffer(), and SpectrumWidget::panIndex()/m_smoothed/estimateNoiseFloorDbm). The generic QObject::property() read keeps core decoupled from the GUI class — nice approach. CI is all green and everything is correctly behind AETHER_AUTOMATION. Two notes, one worth addressing before merge.

1. The TX-safety watchdog can't actually stop a CWX transmission (worth a look).

doCwx("send", …) arms the watchdog by setting m_txKeyedSinceMs, matching txtest/key. But when the watchdog fires, forceUnkey() only does:

tx.stopTune();
tx.setMox(false);

CWX keys the transmitter through the CW keyer buffer, not MOX or TUNE — so neither call cuts a CWX transmission in flight. A long cwx send string at low WPM can exceed m_txMaxKeyMs (default 20 s); the watchdog would log a force-unkey while the radio keeps keying until the buffer drains on its own. The PR body states cwx send "arms the force-unkey watchdog, same as txtest/key" — it arms it, but the backstop is a no-op for this keying path, which undercuts the safety claim.

Suggest making forceUnkey() also abort the keyer when a radio model is present:

void AutomationServer::forceUnkey(const char* reason)
{
    if (!m_radioModel)
        return;
    auto& tx = m_radioModel->transmitModel();
    tx.stopTune();
    tx.setMox(false);
    m_radioModel->cwxModel().clearBuffer();   // also cut a CWX transmission
    m_txKeyedSinceMs = 0;
    ...
}

(cwx stop already does clearBuffer(), so this is the same lever the manual abort uses.)

2. Minor: the doFloors() comment says the payload is "keyed by panIndex," but it builds a flat QJsonArray with panIndex as a field on each entry. Cosmetic — just align the comment with the shape.

Everything else looks good: null-guards on m_radioModel/slice resolution are in place, the antenna-list validation with an empty-list fallback is sensible, and the pan create/remove guards (limit check, refuse-last-pan) are the right shape. The two known follow-ups you documented (panafall-created pan teardown, grab pan <index>) are reasonable to defer.

Thanks for the thorough write-up and the honest bench-vs-live caveat on the companion #3831 — that kind of transparency about what the test does and doesn't prove is appreciated. Just the watchdog/CWX gap is worth closing before this lands.


🤖 aethersdr-agent · cost: $7.2988 · model: claude-opus-4-8

…unaway cwx send

The TX-safety watchdog arms on `cwx send` (m_txKeyedSinceMs) and calls
forceUnkey() past the limit, but forceUnkey only did stopTune() + setMox(false).
CWX keying is driven by its own buffer, largely independent of MOX, so setMox(0)
alone does not abort an in-flight `cwx send` — the watchdog would re-fire every
tick with no effect until the buffer drained, defeating the 'can never leave a
live transmitter on' guarantee.

Add cwxModel().clearBuffer() (the same abort `cwx stop` uses) to forceUnkey so
the all-stop actually covers CWX keying.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876

ten9876 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Pushed a small TX-safety fix on top (f51f4b0a) — flagged during review.

The gap: cwx send arms the watchdog (m_txKeyedSinceMs), and the watchdog calls forceUnkey() past the limit — but forceUnkey() only did stopTune() + setMox(false). CWX keying is driven by its own buffer, largely independent of MOX, so setMox(false) alone won't abort an in-flight cwx send: the watchdog would re-fire every 500 ms tick with no effect until the buffer drained, defeating the "can never leave a live transmitter on" guarantee for the new verb.

The fix: forceUnkey() now also calls cwxModel().clearBuffer() — the same abort cwx stop uses — so the all-stop actually covers CWX keying. +5 lines, builds clean.

Everything else in the PR is solid (non-keying verbs correctly ungated, txAntenna read makes the dummy-load gate enforceable, floor Q_PROPERTYs surfaced generically). Worth a quick on-hardware confirm that a long cwx send now force-unkeys at the watchdog limit. Thanks @jensenpat.

@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.

Reviewed in depth earlier — the bridge expansion is solid: non-keying verbs (slice txant/rxant, floors, txwaterfall, pan create/remove/center) correctly ungated; txAntenna/showTxInWaterfall and the noiseFloorDbm/displayFloorDbm/panIndex Q_PROPERTYs surfaced generically via QObject::property() (no core→gui coupling); cwx send properly ALLOW_TX-gated and arming the watchdog.

Transparency note: the one TX-safety gap I found in review — forceUnkey() (the watchdog all-stop) only did stopTune()+setMox(false), which can't abort an in-flight cwx send since CWX is buffer-driven independent of MOX — I closed by pushing f51f4b0, adding cwxModel().clearBuffer() to forceUnkey. So I co-authored that one commit; the rest is @jensenpat's work. CI is green on the fixed head.

Caveat for the record: the CWX-abort is logic/build-verified but not yet on-hardware-confirmed — worth a real-radio check that a long cwx send now force-unkeys at the watchdog limit. Not a blocker. Thanks @jensenpat.

@ten9876 ten9876 merged commit 1ed91d1 into aethersdr:main Jun 26, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jun 28, 2026
## Release prep for v26.6.5

Docs + version-string updates for the **50 merges** since v26.6.4. No
code change — safe to merge without a rebuild.

### Changes
- **CHANGELOG.md** — new `[v26.6.5] — 2026-06-28` section, headlined by
**KiwiSDR receive sync** (#3872), **SmartMTR TX meters** (#3776), the
**PROF profile-switcher applet** (#3829), and a large **agent automation
bridge** expansion (#3851/#3856/#3883/#3842/#3832/#3819); categorized
Added/Changed/Fixed/Performance/Removed compiled from the full merge
list. Folds in the prior `[Unreleased]` entries and resets
`[Unreleased]` to empty.
- **CMakeLists.txt / README.md / AGENTS.md** — `26.6.4` → `26.6.5`.
- **ROADMAP.md** — current cycle → `post-v26.6.5`; four v26.6.5
highlights added atop *Recently shipped*.

### Not included
- Tagging `v26.6.5` is a separate, explicit step (not done here).
- The untracked `docs/aetherd-headless-engine-design.md` RFC and
`prototypes/` are intentionally left out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <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.

2 participants