Skip to content

fix(tx): setAmCarrierLevel caches m_amCarrierLevel optimistically (#3711). Principle II.#3712

Merged
aethersdr-agent[bot] merged 1 commit into
mainfrom
aetherclaude/issue-3711
Jun 21, 2026
Merged

fix(tx): setAmCarrierLevel caches m_amCarrierLevel optimistically (#3711). Principle II.#3712
aethersdr-agent[bot] merged 1 commit into
mainfrom
aetherclaude/issue-3711

Conversation

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Summary

Fixes #3711

What was changed

fix(tx): setAmCarrierLevel caches m_amCarrierLevel optimistically (#3711). Principle II.

Files modified

  • src/models/TransmitModel.cpp
 src/models/TransmitModel.cpp | 4 ++++
 1 file changed, 4 insertions(+)

Generated by AetherClaude (automated agent for AetherSDR)


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

). Principle II.

setAmCarrierLevel() sent the radio command but never wrote m_amCarrierLevel,
so amCarrierLevel() read stale until the radio's async am_carrier_level= echo
arrived. It was the only TX setter missing the optimistic local update its
siblings (setVoxLevel, setVoxDelay, setMicBoost, setMicBias, setDexpLevel)
all perform. Mirror the sibling pattern: write the member behind a change
guard and emit phoneStateChanged() (AM carrier is a phone/VOX-group property,
parsed into phoneChanged in applyTransmitStatus). Keyword am_carrier is
correct (FlexLib Radio.cs AMCarrierLevel) and unchanged.

Principle II (radio authoritative on live state): the command has no
synchronous echo, so the client may update optimistically; the radio's
subsequent am_carrier_level= status remains the truth and supersedes.

No feedback loop: PhoneApplet slider valueChanged guards on
!m_updatingFromModel; syncFromModel (driven by phoneStateChanged) sets that
flag and wraps setValue in a QSignalBlocker, so one user change yields one
transmit set am_carrier command.

Blast radius: risk_score=0.082, 4 high-risk affected (top: MainWindow::MainWindow,
MainWindow::wireExternalControllers, MainWindow::registerMidiParams). All are
construction/wiring or the MIDI param binding; they invoke the setter
unchanged and only gain the optimistic read-back. Sole direct caller is
PhoneApplet, verified loop-free.

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

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed — this looks good. ✅

The change correctly implements the optimistic-cache pattern (Principle II) already established by the sibling setters in this file:

  • Matches the shape of setVoxLevel / setVoxEnable / setMicBoost — cache locally, emit phoneStateChanged(), then commandReady.
  • The am_carrier_level status-echo handler at TransmitModel.cpp:143-145 re-applies the radio's value when it arrives, so the radio stays authoritative — the "echo supersedes" comment is accurate.
  • No feedback loop: PhoneApplet::syncFromModel (the phoneStateChanged slot) wraps the slider update in a QSignalBlocker, so the optimistic emit can't re-enter setAmCarrierLevel.
  • The if (m_amCarrierLevel != level) guard is a nice touch — slightly better than the unconditional siblings, since it avoids redundant signal emission when the value is unchanged.

No bugs, null risks, or resource concerns; scope is tight (one file, one setter). Thanks for the clean, well-commented fix! 🙏


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

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looks good — clean, correct, and faithful to Principle II. ✅

What it does: setAmCarrierLevel now caches m_amCarrierLevel optimistically and emits phoneStateChanged() before sending the command, so the AM carrier slider reflects user intent immediately instead of waiting on the radio.

Verified against the surrounding code:

  • Right member, right signal. m_amCarrierLevel (TransmitModel.h:316) is the field read by the getter (amCarrierLevel()) and the PhoneApplet slider (PhoneApplet.cpp:399-400), and phoneStateChanged() is the signal that applet listens on. Consistent with the existing optimistic setters right above it (setVoxLevel, setMicBoost, setMicBias).
  • Echo supersedes correctly (Principle II). The status path at TransmitModel.cpp:143-145 overwrites m_amCarrierLevel from am_carrier_level and flags phoneChanged, so the radio's authoritative value still wins on the next status update. The optimistic write is genuinely just a UI-latency bridge.
  • Nice touch: the if (m_amCarrierLevel != level) guard avoids redundant phoneStateChanged() emissions — slightly better than the unconditional emit in the neighboring setters.

No null/leak/boundary concerns (qBound clamps the input), scope is the single intended file, and nothing here touches a system boundary that needs added error handling. Thanks for the tidy fix!


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

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

Independent human review — @NF0T

Code verified against the current codebase. Approving.

Bug confirmed: Pre-PR setAmCarrierLevel sent the command but never wrote m_amCarrierLevel, leaving amCarrierLevel() stale until the radio's async am_carrier_level= echo arrived. Every adjacent phone-group setter (setVoxLevel, setVoxDelay, setMicBoost, setMicBias) cached locally before emitting — this was the sole omission.

Fix is correct:

  • The optimistic write + phoneStateChanged() emission are properly guarded inside if (m_amCarrierLevel != level), which is a minor improvement over the unconditional sibling pattern (avoids redundant signal emission on no-change).
  • commandReady fires unconditionally outside the guard — correct; the command goes to the radio regardless.
  • No feedback loop: PhoneApplet::syncFromModel wraps setValue in QSignalBlocker(m_amCarrierSlider) and the !m_updatingFromModel guard at line 124 provides belt-and-suspenders protection. One user interaction → one command.
  • The radio's am_carrier_level= echo path at applyTransmitStatus:143-145 remains the truth and supersedes the optimistic value, satisfying Principle II as cited.
  • Command keyword am_carrier is unchanged and correct per FlexLib Radio.cs AMCarrierLevel.

Scope is one file, one function, four lines. No regressions observed.

@aethersdr-agent aethersdr-agent Bot merged commit 71d0e7e into main Jun 21, 2026
6 checks passed
@aethersdr-agent aethersdr-agent Bot deleted the aetherclaude/issue-3711 branch June 21, 2026 15:22
aethersdr-agent Bot added a commit that referenced this pull request Jun 22, 2026
…ciple II.

TransmitModel::setSbMonitor sent "transmit set mon=" but never updated its
local m_sbMonitor, so transmitModel().sbMonitor() read stale until the radio
echoed sb_monitor (measured non-convergent over a 410 ms window on a
FLEX-8400M). Same defect class as #3712 (setAmCarrierLevel).

Mirror the sibling pattern: write m_sbMonitor optimistically on change and
emit micStateChanged() — the signal the MON button's model->widget sync
(PhoneCwApplet::syncPhoneFromModel, connected to micStateChanged) actually
binds to, matching setMonGainSb. NOT phoneStateChanged() (that drives
syncCwFromModel, which never touches the MON button). No feedback loop: the
sync wraps its body in m_updatingFromModel and the toggled handler early-
returns on that flag, so the optimistic setChecked cannot re-emit the command.

Principle II: the command path stays client->radio; the radio's sb_monitor
status echo (parsed at TransmitModel.cpp:104-106) supersedes the optimistic
value, with no feedback loop. Command/status split per Principle I (FlexLib:
command "mon", status key "sb_monitor", Radio.cs:10615).

Blast radius: risk_score=0.411, 23 high-risk affected (top:
MainWindow::MainWindow, MainWindow::wireRadioModel, RadioModel::
troubleshootingSnapshot). All structural noise from class-level granularity;
the change only adds an existing signal emit already wired and feedback-
guarded, no signature/symbol/dependency change. Test-target link surface
unchanged (no new external symbol or include). Note: cmake unavailable in this
environment, so a full build was not run; the edit uses only pre-declared
members/signals isomorphic to verified sibling setters in the same TU.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aethersdr-agent Bot added a commit that referenced this pull request Jun 22, 2026
…inciple II.

setCwSidetone() sent the `cw sidetone` command but never updated its local
member m_cwSidetone, so cwSidetone() returned a stale value until the radio's
status echo arrived. Apply the canonical guarded-optimistic pattern (identical
to #3712 setAmCarrierLevel and the adjacent setCwBreakIn/setCwDelay/setCwIambic
setters): update m_cwSidetone and emit phoneStateChanged() when the value
changes, before sending the command.

No feedback loop: PhoneCwApplet's button->model edge is guarded with
!m_updatingFromModel and its phoneStateChanged sync wraps widget updates in
m_updatingFromModel=true/false plus QSignalBlocker, so the optimistic emit
cannot re-send the command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aethersdr-agent Bot added a commit that referenced this pull request Jun 22, 2026
TransmitModel::setDax sent the radio command but never wrote its local
m_daxOn member, so a read in the same tick reflected the change only
after the radio's dax= status echo (~80ms live). This mirrors the
sibling mic setters (setSpeechProcessorEnable, setMicLevel, setMonGainSb)
which update optimistically and emit micStateChanged(); setDax was the
odd one out. Latent today because the radio echoes dax= promptly, but a
slow or dropped echo would surface the same stale read as #3712.

Emits micStateChanged() (not phoneStateChanged() from the issue body):
the echo path sets m_daxOn under the micChanged flag and emits
micStateChanged() (TransmitModel.cpp:232), and PhoneCwApplet syncs the
DAX button in syncPhoneFromModel() (PhoneCwApplet.cpp:740), wired to
micStateChanged (:667-668). The re-emit is safe: syncPhoneFromModel()
sets m_updatingFromModel and wraps the button in QSignalBlocker, and the
toggled handler is gated by !m_updatingFromModel (:313-315).

commandReady stays unconditional, so callers that depend on the command
always reaching the radio (Ax25HfPacketDecodeDialog save/restore,
MainWindow shortcut/controller bindings) are unaffected; the optimistic
read actually improves the daxOn()-then-set call sites.

Blast radius: risk_score=0.128, 6 high-risk affected (top: MainWindow::MainWindow, Ax25HfPacketDecodeDialog::Ax25HfPacketDecodeDialog, MainWindow::registerShortcutActions). All real call sites verified non-regressing; high-risk entries are wiring/construction bridges that only instantiate TransmitModel.

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.

AM carrier level reads stale: setAmCarrierLevel() omits the optimistic local update its sibling TX setters have

1 participant