fix(hid): correct IcomRC28Parser byte offsets and report size (#1896)#2870
fix(hid): correct IcomRC28Parser byte offsets and report size (#1896)#2870NF0T wants to merge 1 commit into
Conversation
…sdr#1896) The RC-28 parser had completely wrong byte assumptions: it treated the constant report-ID byte as an encoder counter, and the sequence counter as a button bitmask, causing the knob to do nothing while spuriously toggling ToggleMox on every tick. Rewrite parse() to match the actual 32-byte RC-28 report layout: - Strip report ID (0x01) when present (Windows/Linux hidapi behaviour) - Rotation from direction byte at data[2] (0x01=CW, 0x02=CCW) - Emit only on seq==0x01 to produce one step per detent (device sends 2 reports per click) - Button state from absolute enum at data[4]: F1=0x05, F2=0x03, TX=0x06, idle=0x07 - Expand from 2 to 3 buttons (F1, F2, TX bar) - Fix reportSize() 64→32 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @NF0T — solid bit of detective work. The byte-layout table in the description matches the diff exactly and the failure modes you describe (buf[0]==0x01 constant → no tuning, buf[1] alternating → spurious ToggleMox) drop out cleanly from the old code, so the root-cause story is convincing. Scope is clean: only the two files claimed in the title.
One substantive concern, plus a couple of smaller ones.
The buf[0] == 0x01 heuristic collides with macOS seq=0x01 reports
const uint8_t* data = (buf[0] == 0x01) ? buf + 1 : buf;On Windows/Linux, buf[0] is the report ID (always 0x01) — strip is correct.
On macOS (report ID stripped by hidapi), buf[0] is the seq counter, which is 0x01 on the first report of every detent. So on macOS:
- seq=0x01 report: parser sees
buf[0]==0x01, incorrectly strips a byte.data[0]becomes the unused byte (0x00),data[2]becomes the byte after direction (0x00). Theseq == 0x01rotation guard fails, no rotate event fires. - seq=0x02 report: parser correctly doesn't strip.
data[0]==0x02, but the guard requiresseq == 0x01, so no rotate event fires.
Net effect: rotation never fires on macOS with this fix — the exact platform the PR description calls out as broken. Buttons would only work half the time (on seq=0x02 reports), since the button state byte position shifts on the falsely-stripped seq=0x01 reports.
Robust disambiguation can use buf[1]: on Windows/Linux buf[1] is the seq counter (0x01 or 0x02); on macOS buf[1] is the unused byte (0x00). So:
const bool hasReportId = (buf[0] == 0x01) && (buf[1] == 0x01 || buf[1] == 0x02);
const uint8_t* data = hasReportId ? buf + 1 : buf;That cleanly distinguishes "report ID 0x01 followed by seq 0x01/0x02" from "seq 0x01 followed by unused 0x00" and works without platform #ifdefs. Worth confirming against a raw macOS capture from @bobbyrmanson before merging.
Minor — reportSize() = 32 truncates the report ID byte on Windows/Linux
HidEncoderManager calls hid_read(device, buf, parser->reportSize()). On Windows/Linux the wire report is 33 bytes (1 report ID + 32 data), so capping at 32 means we lose the last data byte. The parser only reads bytes 0–4 so this doesn't matter functionally today, but if anyone ever adds a check on a later byte it will silently fail on Windows/Linux but not macOS. Consider reportSize() = 33 — it lets you use len to detect the platform path unambiguously (33 = report-ID prefixed, 32 = stripped) and dodges the buf[0]/buf[1] heuristic entirely.
Small — m_prevButtonState{0x07} first-report behaviour
Initialising to 0x07 (idle) is correct for the common case. If AetherSDR starts with a button physically held, the first non-0x07 report will fire a press event — which is the right behaviour, just worth noting in case anyone wonders why a stuck button generates an event on startup.
Looks good
- Removing the encoder-wrap math and the
m_firstReportgate is correct given the new format isn't a counter. len < 6minimum coversdata[4]access in both strip / no-strip cases — defensive and right.qCDebug/qCWarningaren't relevant here since the parser is silent by design; the manager logs disconnect, which is the right boundary.
Happy to re-review once the macOS path is sorted. The Windows/Linux side of this fix looks right and the description's analysis is convincing — please get @bobbyrmanson to confirm CW/CCW rotation works on macOS before merging.
Summary
The Icom RC-28 parser has never worked correctly. The knob did not tune
the radio, and turning it caused spurious transmit toggles. This fixes
the root cause: completely wrong byte-offset assumptions in
IcomRC28Parser::parse().Root cause
The parser comment claimed "64-byte reports, byte 0 = encoder counter,
byte 1 = button bitmask." The RC-28 actually sends 32-byte reports
with this layout (0-indexed, after the hidapi report ID byte):
On Windows/Linux, hidapi prepends the report ID (0x01) as
buf[0],so the old parser read:
buf[0]= 0x01 (constant) → encoder position never changed → no tuningbuf[1]= sequence counter alternating 0x01/0x02 per knob tick →interpreted as button bitmask bit-0 toggling → ToggleMox fired on
every knob movement
On macOS, hidapi strips the report ID, producing different but
equally broken behaviour. The user confirmed failure on both platforms.
Fix
buf[0] == 0x01(Windows/Linux) beforeparsing; otherwise use
bufdirectly (macOS).data[2]; emitHidEvent::Rotateonly onseq == 0x01(first report of each pair) — the RC-28 sends tworeports per detent, so without this guard each click tunes two steps.
data[4]as an absolute enum, not a bitmask.TX bar (button 3).
reportSize(): 64 → 32.Raw HID captures and platform confirmation provided by @bobbyrmanson in
the issue thread. Root cause analysis from AetherClaude (2026-05-01)
validated against source and independently confirmed here.
Testing
@bobbyrmanson has offered to test a patched build on macOS and Windows 11.
Fixes #1896