Skip to content

Add RC-28 rotation sensitivity divider and auto-snap to 1 kHz#3875

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
wa2n-code:fix-issue-3841
Jun 28, 2026
Merged

Add RC-28 rotation sensitivity divider and auto-snap to 1 kHz#3875
ten9876 merged 2 commits into
aethersdr:mainfrom
wa2n-code:fix-issue-3841

Conversation

@wa2n-code

@wa2n-code wa2n-code commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #3841

Summary

  • Pulses per step slider (1–10) in the RC-28 Mapping dialog Encoder section. Accumulates raw encoder pulses before firing one frequency step, so large step sizes (500 Hz / 1 kHz) require more physical rotation to advance. Direction reversal clears the accumulator. Defaults to 1 (existing behaviour unchanged).

  • Auto-snap to nearest 1 kHz checkbox in the same section. A 600 ms single-shot timer restarts on every encoder event; when the knob stops, it snaps to the nearest 1 kHz using TuneIntent::IncrementalTune — the spectrum display does not recenter. Off by default.

Both settings are stored in the existing RC28Mapping JSON blob (Principle V) and take effect immediately when changed in the dialog without requiring a restart.

Test plan

  • Open Settings → Icom RC-28 Remote Encoder — Encoder section shows "Pulses per step" slider and "Auto-snap" checkbox
  • Slider at 1: knob behaviour identical to before
  • Slider at 3–5: noticeably more rotation required per step at 500 Hz step size
  • Direction reversal mid-spin doesn't cause spurious steps in the old direction
  • Auto-snap off: no snap after rotation stops
  • Auto-snap on: frequency snaps to nearest 1 kHz ~600 ms after knob stops, spectrum does not recenter
  • Settings persist across restart
  • Non-RC-28 HID devices (TMate 2, StreamDeck+) unaffected

🤖 Generated with Claude Code

@wa2n-code wa2n-code requested review from a team as code owners June 28, 2026 05:50
@wa2n-code

Copy link
Copy Markdown
Contributor Author

Tested on Windows 11 with MSVC 2022. Not yet tested on macOS or Linux.

@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 for this, @wa2n-code — clean, well-scoped implementation that closely follows the existing RC-28 conventions. The settings go through the nested RC28Mapping JSON blob (Principle V ✅), the accumulator/direction-reversal logic reads clearly, the QTimer is parented to this (no leak), units are handled correctly (frequency() is MHz, so round(f*1000)/1000 snaps to the nearest kHz), and the "take effect immediately" wiring in MainWindow_Menus.cpp is a nice touch. Defaults preserve existing behaviour.

One substantive issue plus a couple of smaller notes:

1. Auto-snap timer bypasses the slice lock (should fix)

Every other tune path guards against a locked slice before tuning — applyFlexControlWheelAction (MainWindow_Controllers.cpp:1333) and the keyboard nudge (MainWindow_Shortcuts.cpp:550) both check s->isLocked() and call notifyTuneBlockedByLock(). The new snap timer calls applyTuneRequest directly with no such guard:

connect(m_hidSnapTimer, &QTimer::timeout, this, [this] {
    if (auto* s = activeSlice()) {
        if (s->isLocked()) return;   // <-- add this
        const double snapped = std::round(s->frequency() * 1000.0) / 1000.0;
        ...

Because the timer is started in the tuneSteps handler before applyFlexControlWheelAction runs, turning the knob on a locked slice with auto-snap enabled is blocked from tuning (correct) but still schedules a snap — which then moves the locked slice ~600 ms later. Adding the isLocked() early-return closes that.

2. Snap timer fires regardless of encoder 0's mapped action (minor)

The sensitivity divider and snap-timer start are gated on isRC28Compatible() && encoderIndex == 0, but not on the configured action being frequency tuning. On the RC-28 the main knob is effectively always frequency, so this is fine in practice — but if encoder 0 is ever remapped (e.g. to WheelVolume/WheelRit), the divider would throttle those steps and the snap timer would still snap the frequency. Worth a brief comment noting the assumption, or gating the snap-start on the resolved actionId == "WheelFrequency" if you want to be defensive.

3. Test plan is unchecked — the checkboxes in the PR body are all empty. If you've run through them (especially direction-reversal and the "non-RC-28 devices unaffected" cases), it'd help to note that.

None of these block the concept — item 1 is the only one I'd consider important before merge. Nice work matching the surrounding style.


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

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

Thanks @wa2n-code — this is a clean, well-scoped feature that follows the RC-28 conventions nicely: settings persist in the nested RC28Mapping JSON (Principle V), the accumulator/direction-reversal logic reads clearly, the QTimer is parented to this (no leak), the MHz→kHz snap math is correct, and defaults preserve existing behaviour. The "take effect immediately" wiring is a nice touch.

One real bug to fix before merge, plus two small notes.

🔴 Auto-snap timer tunes a locked slice (must fix)

Every other tune path guards on s->isLocked() and calls notifyTuneBlockedByLock() — e.g. applyFlexControlWheelAction (MainWindow_Controllers.cpp:1333) and the keyboard nudges (MainWindow_Shortcuts.cpp:550,645). The new snap-timer callback calls applyTuneRequest(...) directly with no such guard.

Because the timer is start()ed in the tuneSteps handler before the live tune runs, turning the knob on a locked slice with auto-snap enabled is correctly blocked from the immediate step — but the snap still fires ~600 ms later and moves the locked slice. A lock should mean "don't move this," so this needs the same guard:

connect(m_hidSnapTimer, &QTimer::timeout, this, [this] {
    if (auto* s = activeSlice()) {
        if (s->isLocked()) return;        // ← add: respect the slice lock
        const double snapped = std::round(s->frequency() * 1000.0) / 1000.0;
        if (std::abs(snapped - s->frequency()) > 1e-9)
            applyTuneRequest(s, snapped, TuneIntent::IncrementalTune, "rc28-autosnap");
    }
});

🟡 Snap fires regardless of encoder 0's mapped action (minor)

The divider + snap-start are gated on isRC28Compatible() && encoderIndex == 0, but not on the configured action being frequency tuning. On the RC-28 the main knob is effectively always frequency, so this is fine in practice — but if encoder 0 is ever remapped (e.g. WheelVolume/WheelRit), the divider would throttle those steps and the snap timer would still snap the frequency. Worth a brief comment noting the assumption, or gating the snap-start on the resolved actionId == "WheelFrequency" if you'd like to be defensive.

🟡 Test plan

The checkboxes in the PR body are all unchecked — if you've run through them (especially direction-reversal mid-spin and "non-RC-28 devices unaffected"), a note confirming would help.

Only the locked-slice guard blocks merge — the rest is solid. Nice work matching the surrounding style; ping me when the guard's in and I'll re-review.

wa2n-code added a commit to wa2n-code/AetherSDR that referenced this pull request Jun 28, 2026
…quency action

- Add isLocked() early-return in the auto-snap timer callback so a locked
  slice cannot be moved by the 600 ms deferred snap (addresses must-fix
  from PR aethersdr#3875 review).
- Move actionId lookup before the RC-28 sensitivity/snap block and gate
  both the divider and snap-timer start on actionId == "WheelFrequency",
  so remapping encoder 0 to WheelVolume/WheelRit etc. neither throttles
  those steps nor triggers a stale frequency snap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wa2n-code

Copy link
Copy Markdown
Contributor Author

Both review items addressed in 9c20592:

🔴 Auto-snap timer tunes a locked slice — Added if (s->isLocked()) return; as the first check inside the snap-timer callback, matching the same guard pattern used in applyFlexControlWheelAction and the keyboard nudge paths. A locked slice will no longer be moved by the deferred snap.

🟡 Snap fires regardless of encoder 0's mapped action — Moved the actionId lookup (and isTMate2 bool) to before the RC-28 sensitivity/snap block, then added && actionId == QLatin1String("WheelFrequency") to the block's condition. If encoder 0 is ever remapped to WheelVolume, WheelRit, etc., neither the pulse divider nor the snap timer will engage.

Test plan checkboxes are all checked — tested direction-reversal mid-spin, non-RC-28 devices unaffected, and auto-snap on/off behaviour confirmed.

wa2n-code and others added 2 commits June 28, 2026 12:59
…dr#3841)

Adds two tuning aids to the RC-28 Mapping dialog's Encoder section:

- Pulses per step slider (1–10): accumulates raw encoder pulses before
  firing one frequency step, reducing twitchiness with large step sizes
  (e.g. 500 Hz / 1 kHz). Direction reversal clears the accumulator.
  Stored as rc28MappingField("sensitivity"); defaults to 1 (no change).

- Auto-snap checkbox: a 600 ms single-shot timer restarts on every
  RC-28 encoder event. When the knob stops, it snaps to the nearest
  1 kHz via TuneIntent::IncrementalTune so the spectrum display does
  not recenter. Stored as rc28MappingField("autoSnap"); off by default.

Both settings are read at startup and live-updated via mappingFieldChanged
without requiring a restart. Principle V.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…quency action

- Add isLocked() early-return in the auto-snap timer callback so a locked
  slice cannot be moved by the 600 ms deferred snap (addresses must-fix
  from PR aethersdr#3875 review).
- Move actionId lookup before the RC-28 sensitivity/snap block and gate
  both the divider and snap-timer start on actionId == "WheelFrequency",
  so remapping encoder 0 to WheelVolume/WheelRit etc. neither throttles
  those steps nor triggers a stale frequency snap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wa2n-code wa2n-code requested a review from ten9876 June 28, 2026 17:52

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

Re-reviewed b4bccfd — both points addressed, thanks @wa2n-code:

  • ✅ The snap timer now respects the slice lock (if (s->isLocked()) return;), so a locked slice is no longer tuned after the knob stops — the blocker from my last review.
  • ✅ The divider + snap-start are gated on actionId == "WheelFrequency" (and actionId is resolved before the block), so a remapped encoder 0 won't be throttled or snap the frequency.

Everything else still holds (Principle V JSON, accumulator/direction-reversal, no-leak QTimer, correct MHz→kHz units, defaults preserve behavior). Not a keying path. CI green on all 6. Nice responsiveness to review on a first contribution — LGTM.

@ten9876 ten9876 merged commit be2cd87 into aethersdr:main Jun 28, 2026
6 checks passed
@wa2n-code wa2n-code deleted the fix-issue-3841 branch June 29, 2026 13:53
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.

Icom RC-28 tuning knob rotation sensitivity makes it difficult to land on a precise frequency, better snap to 1 kHz options

2 participants