Feature/rx audio level strip - #61
Merged
Merged
Conversation
…ta-mode option
Two independent bugs in the Band Plan → Direct Serial tune path.
## Bug 1: FT-450D frequency changes were silently rejected
Root cause: Yaesu's `FA` (set-frequency) CAT command uses a
model-dependent digit width. FT-450/450D wants 8 digits, no leading
zero ("FA14230000;"); FT-991A and the other "modern CAT" rigs
(FT-891, FT-710, FTDX10, FTDX101, FT-950) want 9, zero-padded
("FA014230000;"). YaesuRig.set_freq() hardcoded the 9-digit format,
so every FA command sent to an FT-450D was malformed and the radio
rejected it outright with "?;".
That rejection was completely invisible, for two compounding
reasons:
- Yaesu/Kenwood *set* commands are fire-and-forget by design (the
radio sends no response at all to a legitimate set, so
_write_command() never reads one back) — there was no way to
observe a "?;" reply to the set itself.
- _RigPollWorker.tune() wrapped set_freq()/get_mode()/set_mode() in
one `except Exception: _log.warning(...)` with nothing reaching
the GUI, and the transient "Tuning to X (mode)..." status message
wasn't corrected on failure — so a rejected tune looked exactly
like a successful one.
Meanwhile MD (mode), a single-digit command, isn't sensitive to a
digit-width mismatch, so mode changes kept working — which is
exactly the asymmetry reported: freq never moved, mode did.
Fix:
- serial_rig.py: YaesuRig now detects the FA digit width from a live
get_freq() response (already lenient about either length) and
caches it per connection instead of hardcoding 9. set_freq() probes
once via get_freq() if the width isn't known yet, so a tune right
after Connect can't race the 1 Hz poll loop for this detection.
- main_window.py: _RigPollWorker.tune() now reads back get_freq()
after every set_freq() and raises/reports a mismatch (readback of 0
is treated as "this backend doesn't report frequency", not a
mismatch — e.g. SerialPttRig). A new tune_failed signal carries the
reason to the GUI thread, which now replaces the status-bar message
instead of only logging it — so any future rejection (this radio or
another) is visible instead of silent, whatever the root cause.
## Bug 2: Band Plan always forced plain USB/LSB, never a data mode
SSTV_BAND_PLAN hardcodes rig_mode="USB"/"LSB"/"FM", and that literal
was passed straight to Rig.set_mode() whenever tune() decided a mode
change was needed. Every backend's own mode map only knows how to
turn that literal into *plain* USB/LSB — even YaesuRig, which already
supports "DATA-U"/"DATA-L" if only asked for them. There was no
setting anywhere for which CAT mode Band Plan tuning should actually
request.
Fix: a WSJT-X-style "SSTV mode" policy (Settings -> Radio -> Direct
Serial): None / Voice (default, today's behavior) / Data-Pkt. "Data"
resolves through a small per-protocol table in band_plan.py
(resolve_tune_mode / DATA_MODE_BY_PROTOCOL) — currently populated for
Yaesu CAT only (DATA-U/DATA-L), since Icom's data mode is a separate
CI-V sub-command (0x1A 0x06) and Kenwood/Elecraft's is model-specific
(e.g. K3's DT command) — neither verified against real hardware, so
both intentionally fall back to Voice with a logged warning rather
than guess a wrong CAT string.
Docs: README's Direct Serial / Band Plan sections and
docs/hamlib-integration-notes.md (the Hamlib-direct research doc's
"known gotchas" list) updated to describe both the digit-width quirk
and the new SSTV-mode setting. CHANGELOG and version intentionally
left untouched.
Tests: TestRigPollWorkerTune (main_window), TestYaesuFreqDigitWidth
(serial_rig), TestResolveTuneMode (band_plan) — 348 tests green
across tests/radio, tests/config, tests/ui/test_main_window.py, and
tests/ui/test_settings_dialog.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e poisoning, missing log Follow-up to the FT-450D / Band Plan fix (0c4e7b9), addressing bucknova's review on bucknova#47: bucknova#47 (comment) ## 1. Readback false-fails on async-cached backends (TCI, FlexRadio) — must fix _RigPollWorker.tune() is the Band Plan path for every backend (the Band Plan button is gated only on "connected", not connection mode). The set_freq() -> get_freq() -> raise-on-mismatch sequence added in the original fix assumed get_freq() is a synchronous radio query. It isn't, on two of our four backends: - TciRig.get_freq() returns a locally-cached _last_freq that only updates when the server's async push arrives on the receive thread; set_freq() is a fire-and-forget send(). The readback ran essentially immediately after, so it read the pre-tune value almost every time. - FlexRig.get_freq() reads self._freq_hz, updated from an async slice- status push; set_freq()'s _command() blocks for the reply, but not for that separate push. Worse, the mismatch raise sat before the `if mode:` block, so a false frequency failure also skipped the mode change — a functional regression on a previously-working path. Fix: set_freq(), the mode-change block, and frequency verification each catch their own exception independently now and collect into an error list, so a frequency false-positive (or a real failure) can never block the mode change. Frequency verification is a small settle-and-retry loop (_verify_freq_settled): on mismatch, sleep ~150 ms and re-read, up to 2 extra attempts (~300 ms total budget, nothing added on the success path), with a 10 Hz tolerance instead of exact equality to tolerate step- quantizing rigs. Chosen over a synchronous-backend capability flag (the reviewer's other suggested option) because it needs no per-backend classification a future backend could forget to set correctly. ## 2. _freq_digits cache could be poisoned by a garbled response — small fix YaesuRig.get_freq() cached the detected FA digit width before int() confirmed the response actually parsed. A noise-garbled but "FA"- prefixed, correctly-terminated response could poison the cached width for the rest of the connection. Fixed: the width is cached only after a successful parse. ## 3. resolve_tune_mode() silently fell back without the promised log — small fix The original PR description said an unsupported protocol under the "data" policy falls back to Voice "with a logged warning" — the code didn't actually log anything, so a user picking Data/Pkt on e.g. Icom got plain USB with zero indication why. Added a _log.warning in the fallback branch (only for USB/LSB families — FM has no data-mode concept at all, so passing it through isn't a missing mapping and doesn't warn). Tests: TestRigPollWorkerTune gets a new async-catch-up case (get_freq() side_effect returning stale values before the real one) and the existing mismatch test now asserts the mode change still ran; TestYaesuFreqDigitWidth gets two garbled-response cases; TestResolveTuneMode gets caplog-based assertions that the fallback warns (and that FM, known mappings, and voice/none policies stay silent). 348 tests green across tests/radio, tests/config, tests/ui/test_main_window.py, and tests/ui/test_settings_dialog.py. Two points from the review are explicitly out of scope for this PR per the reviewer's own note: extending the mode policy to rigctld users, and using topic branches instead of the fork's main for future PRs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a slim fixed-width column to the right of the RX panel holding three vertical controls: - TX gain slider — mirrors AppConfig.audio_output_gain (same value as Settings -> Audio -> Software Gain), pushed live to TxWorker on every tick and persisted to disk (debounced) when the slider settles. - RX gain slider — mirrors AppConfig.audio_input_gain, same behaviour. - Colour-zoned dBFS input-level meter fed from the post-input-gain RX audio (RxWorker.waterfall_chunk), with green/yellow/red zones and a falling peak-hold marker, so the operator can set RX gain by eye. The sliders stay two-way in sync with the Settings dialog: a Settings save flows back into the strip via _apply_config, and overdrive expands the TX ceiling to 200% in the strip too. A pending debounced gain write is flushed in closeEvent. New widgets: ui/level_meter.py (LevelMeter), ui/audio_level_strip.py (AudioLevelStrip). Tests: test_level_meter.py, test_audio_level_strip.py, plus TestAudioLevelStrip in test_main_window.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Title: feat(rx): always-on audio strip with TX/RX gain sliders and input level meter
SUMMARY
Adds a slim, always-visible strip down the right edge of the Receive panel
with three vertical controls, so software gain can be set - and the incoming
level watched - without opening the Settings dialog:
TX gain slider - same value as Settings > Audio > Software Gain > TX output
gain (AppConfig.audio_output_gain). Pushed live to TxWorker on every tick;
written to the config file (debounced ~400 ms) once the slider settles.
RX gain slider - same value as Settings > Audio > RX input gain
(AppConfig.audio_input_gain), same live-push + debounced-persist behaviour.
Input level meter - colour-zoned dBFS bar (green <= -6, yellow -6..-1,
red >= -1) with a slow falling peak-hold marker, fed from
RxWorker.waterfall_chunk (post-input-gain, i.e. what the decoder sees).
Moves only during capture; drops to silence on stop.
BEHAVIOUR DETAILS
strip via _apply_config; enabling overdrive expands the strip's TX ceiling
to 200% too.
a pending write is flushed in closeEvent.
sibling - not a resizable splitter pane.
waterfall_chunk signal already delivered to the GUI thread.
NEW FILES
attack / ~24 dB per second release, theme-aware).
TESTS
config, live worker push, debounced persist + close-flush, meter fed by RX
chunk, reset on stop, Settings<->strip sync, overdrive ceiling); fixed the
splitter-child assertion for the new RX wrapper.
selection I,F,UP,W) clean; mypy clean on the new widgets.
DOCS