Skip to content

fix(cat): stop set_split_vfo poll from ping-ponging TX between slices#2931

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
pepefrog1234:fix/cat-split-vfo-tx-pingpong
May 23, 2026
Merged

fix(cat): stop set_split_vfo poll from ping-ponging TX between slices#2931
ten9876 merged 2 commits into
aethersdr:mainfrom
pepefrog1234:fix/cat-split-vfo-tx-pingpong

Conversation

@pepefrog1234

Copy link
Copy Markdown
Contributor

Summary

With two or more slices, TX would not stay on the slice the user picked — it kept jumping back to another slice every couple of seconds, and the user couldn't keep TX where they wanted it. Root cause is cmdSetSplitVfo reclaiming TX on every set_split_vfo 0 poll rather than on a real split-state change.

Root Cause

Each rigctld / PTY CAT channel is bound to a fixed slice (m_sliceIndex). cmdSetSplitVfo moved TX onto that slice whenever a client sent set_split_vfo 0 (split disabled). #2568 added an !isTxSlice() guard to stop the redundant command spam, but that only suppressed the already-TX case — it didn't stop TX from being seized.

Hamlib loggers (VARA, VARAC, N1MM, fldigi) poll set_split_vfo 0 VFOA every few seconds. With a CAT channel bound to a non-TX slice the sequence was:

user clicks slice A TX badge    → slice set 0 tx=1   (A is TX)
~1.7 s later logger polls split → channel bound to slice B
cmdSetSplitVfo sees B != TX      → slice set 1 tx=1
TX jumps back to B

The !isTxSlice() guard actually guaranteed the re-seize: the user moving TX away is exactly the moment the bound slice becomes non-TX, which re-satisfies the guard on the very next poll.

Captured in the connection log (slice 0 = A, slice 1 = B, single client, no split):

08:11:59.663  TX: C162|slice set 0 tx=1     ← user clicks A's TX badge
08:11:59.785  RX: slice 0 ... tx=1          ← A is TX (user succeeded)
08:12:01.379  TX: C167|slice set 1 tx=1     ← logger split poll, 1.7 s later
08:12:01.379  TX: C168|slice set 1 tx=1
08:12:01.379  TX: C169|slice set 1 tx=1
08:12:01.379  TX: C170|slice set 1 tx=1
08:12:01.498  RX: slice 1 ... tx=1          ← TX yanked back to B

Fix

Track the last split state per RigctlProtocol instance (tri-state: unknown / disabled / enabled) and reclaim TX only on a genuine split→non-split transition (1→0 edge) — never on a steady-state poll or the first report after connect.

const bool wasEnabled  = (m_lastSplitEnable == 1);
const bool firstReport = (m_lastSplitEnable < 0);
m_lastSplitEnable = enable ? 1 : 0;

if (!enable && wasEnabled && !firstReport) {
    if (auto* s = currentSlice(); s && !s->isTxSlice())
        s->setTxSlice(true);
}

A logger that sits in non-split and polls repeatedly no longer fights the user's TX choice, while a real "turn split off" still moves TX back to the client's slice.

Test plan

  • macOS, FLEX-6600 fw 4.2.18, VARAC on rigctld
  • Slice A = TX, add slice B → select TX back to A → TX stays on A (previously yanked to B within ~2 s)
  • Genuine split toggle (split on → split off) still moves TX to the channel's slice
  • Reviewer: confirm contest-logger split workflows (N1MM true split on/off) still behave on Windows/Linux

Each rigctld/PTY CAT channel is bound to a fixed slice. cmdSetSplitVfo
reclaimed TX onto that slice whenever a client sent `set_split_vfo 0`
(split disabled). PR aethersdr#2568 added an `!isTxSlice()` guard to stop the
command spam, but that only suppressed the redundant case — it did not
stop TX from being seized.

Hamlib loggers (VARA, VARAC, N1MM, fldigi) poll `set_split_vfo 0 VFOA`
every few seconds as part of their state-sync loop. With multiple slices
and a CAT channel bound to a non-TX slice, the sequence was:

  1. user clicks slice A's TX badge       → `slice set 0 tx=1`, A is TX
  2. ~1.7 s later the logger polls split   → channel bound to slice B
  3. cmdSetSplitVfo sees B is no longer TX → `slice set 1 tx=1`
  4. TX jumps back to B

So the `!isTxSlice()` guard actually *guaranteed* the re-seize: the user
moving TX away is exactly the moment the bound slice becomes non-TX, which
re-satisfies the guard on the next poll. TX ping-pongs back within seconds
and the user cannot keep it on the slice they chose. Confirmed in the
connection log: a manual `slice set 0 tx=1` is followed ~1.7 s later by
four `slice set 1 tx=1` commands and the radio echoes slice 1 tx=1.

Fix: track the last split state per protocol instance (tri-state) and only
reclaim TX on a genuine split→non-split *transition* (1→0 edge), never on a
steady-state poll or the first report after connect. A logger that sits in
non-split and polls it repeatedly no longer fights the user's TX choice,
while a real "turn split off" still moves TX back to the client's slice.

Tested on macOS with FLEX-6600 fw 4.2.18 + VARAC: with slice A as TX and a
second slice B added, TX now stays on A after the user selects it instead
of being pulled back to B every couple of seconds.

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

@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 @pepefrog1234 — clean, well-reasoned fix. The write-up makes the bug obvious in hindsight: the !isTxSlice() guard from #2568 was exactly what armed the re-seize on the next poll.

Logic review:

  • m_lastSplitEnable is per RigctlProtocol instance, which matches the "each CAT channel is bound to a fixed slice" framing — only the client that toggled split will reclaim, no cross-client interference.
  • Tri-state walk:
    • First poll (enable=0): wasEnabled=false, firstReport=true → no action, state recorded. ✓
    • Steady-state enable=0 polls: wasEnabled=false → no action. ✓
    • Client raises split (1), then drops it (0): wasEnabled=true, firstReport=false → genuine reclaim. ✓
    • First poll is enable=1 (logger started mid-split): falls through the !enable branch, state records as 1, next 0 triggers reclaim. ✓
  • currentSlice() null-guard preserved.
  • State is only updated inside cmdSetSplitVfo; cmdSetSplitFreq and friends don't touch it, which is fine — split state in this protocol is solely what the client has told us.

Scope is exactly two files matching the stated fix. No conventions / RAII / system-boundary concerns. LGTM as a community contribution — pending the reviewer note on N1MM-style true-split workflows on Windows/Linux that you already called out in the test plan.

…dr#2995

Reconciles three concurrent improvements to cmdSetSplitVfo:
- pepefrog1234's edge-detection (m_lastSplitEnable tri-state) so the
  Hamlib-logger poll doesn't ping-pong TX away from the user's choice
- K5PTB's deferred-promotion state (m_pendingSplitEnable / m_pendingTxSlice /
  m_pendingSplitFreqMHz / m_pendingSplitMode) from aethersdr#2975
- The m_pendingTxSliceChange same-pass race flag from aethersdr#2975

The combined logic: reclaim TX on the RX slice only when we observe a real
1→0 split transition OR the same-pass race (split-1 then split-0 in one
onClientData() pass).  Steady-state poll-zeros and the first-report-after-
connect leave the user's TX badge alone.  The race path stays distinct so
the queued setTxSlice() race from aethersdr#2975 is still covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit 4f64d31 into aethersdr:main May 23, 2026
4 checks passed
@ten9876

ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Merged as 4f64d31. Thanks @pepefrog1234 — clean diagnosis: catching that #2568's !isTxSlice() guard was the trap arming the next re-seize (rather than just suppressing redundant commands) is exactly the kind of root-cause read this PR earns its keep on.

Two things stood out in the implementation:

  • Tri-state m_lastSplitEnable (-1 unknown / 0 / 1) is the right shape for poll-driven CAT state. -1 protects the first-report-after-connect case so a logger that joins in non-split doesn't trigger a phantom reclaim. Worth keeping in mind for any future CAT-poll handler that needs to distinguish 'first observation' from 'no change.'
  • Merge resolution preserved K5PTB's m_pendingTxSliceChange race fix from feat(rigctld): full Hamlib NET rigctl implementation with 149-test suite #2975 — the same-pass set_split_vfo 1 then set_split_vfo 0 window your fix would otherwise have hidden behind the steady-state-poll guard. The combined logic now reclaims TX on either (a) a real 1→0 edge or (b) the same-pass race, with the steady-state polls leaving the user's TX badge alone.

Test plan note: VARAC's checked, N1MM true-split workflows on Windows/Linux haven't been verified — please file a follow-up if you observe any regression there.

73,
Jeremy KK7GWY & Claude (AI dev partner)

G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…aethersdr#2931)

## Summary

With two or more slices, TX would not stay on the slice the user picked
— it kept jumping back to another slice every couple of seconds, and the
user couldn't keep TX where they wanted it. Root cause is
`cmdSetSplitVfo` reclaiming TX on every `set_split_vfo 0` **poll**
rather than on a real split-state change.

## Root Cause

Each rigctld / PTY CAT channel is bound to a fixed slice
(`m_sliceIndex`). `cmdSetSplitVfo` moved TX onto that slice whenever a
client sent `set_split_vfo 0` (split disabled). aethersdr#2568 added an
`!isTxSlice()` guard to stop the redundant **command spam**, but that
only suppressed the already-TX case — it didn't stop TX from being
**seized**.

Hamlib loggers (VARA, VARAC, N1MM, fldigi) poll `set_split_vfo 0 VFOA`
every few seconds. With a CAT channel bound to a non-TX slice the
sequence was:

```
user clicks slice A TX badge    → slice set 0 tx=1   (A is TX)
~1.7 s later logger polls split → channel bound to slice B
cmdSetSplitVfo sees B != TX      → slice set 1 tx=1
TX jumps back to B
```

The `!isTxSlice()` guard actually *guaranteed* the re-seize: the user
moving TX away is exactly the moment the bound slice becomes non-TX,
which re-satisfies the guard on the very next poll.

Captured in the connection log (slice 0 = A, slice 1 = B, single client,
no split):

```
08:11:59.663  TX: C162|slice set 0 tx=1     ← user clicks A's TX badge
08:11:59.785  RX: slice 0 ... tx=1          ← A is TX (user succeeded)
08:12:01.379  TX: C167|slice set 1 tx=1     ← logger split poll, 1.7 s later
08:12:01.379  TX: C168|slice set 1 tx=1
08:12:01.379  TX: C169|slice set 1 tx=1
08:12:01.379  TX: C170|slice set 1 tx=1
08:12:01.498  RX: slice 1 ... tx=1          ← TX yanked back to B
```

## Fix

Track the last split state per `RigctlProtocol` instance (tri-state:
unknown / disabled / enabled) and reclaim TX only on a genuine
split→non-split **transition** (1→0 edge) — never on a steady-state poll
or the first report after connect.

```cpp
const bool wasEnabled  = (m_lastSplitEnable == 1);
const bool firstReport = (m_lastSplitEnable < 0);
m_lastSplitEnable = enable ? 1 : 0;

if (!enable && wasEnabled && !firstReport) {
    if (auto* s = currentSlice(); s && !s->isTxSlice())
        s->setTxSlice(true);
}
```

A logger that sits in non-split and polls repeatedly no longer fights
the user's TX choice, while a real "turn split off" still moves TX back
to the client's slice.

## Test plan

- [x] macOS, FLEX-6600 fw 4.2.18, VARAC on rigctld
- [x] Slice A = TX, add slice B → select TX back to A → TX **stays** on
A (previously yanked to B within ~2 s)
- [x] Genuine split toggle (split on → split off) still moves TX to the
channel's slice
- [ ] Reviewer: confirm contest-logger split workflows (N1MM true split
on/off) still behave on Windows/Linux

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.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