Skip to content

Fix GuoHe Q900 frame sync: require four consecutive 0xA5 bytes - #624

Merged
patrickrb merged 2 commits into
devfrom
optio/task-3feed5fb-3891-4ca7-9049-3dae2e249565
Jul 22, 2026
Merged

Fix GuoHe Q900 frame sync: require four consecutive 0xA5 bytes#624
patrickrb merged 2 commits into
devfrom
optio/task-3feed5fb-3891-4ca7-9049-3dae2e249565

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

GuoHeQ900Rig.checkHead() misidentifies the frame sync header, which intermittently corrupts frequency tracking on the GuoHe Q900 (and its Xiegu-family siblings that share this framing).

Every GuoHe frame begins with four consecutive 0xA5 sync bytes (see GuoHeRigConstantPTT_ON, USB_MODE, READ_FREQ, etc. all start A5 A5 A5 A5 <len> …). The old checkHead counted 0xA5 bytes anywhere in the read buffer and returned the moment the 4th was seen:

if (data[i] == (byte) 0xa5) {
    count++;
    if (count == 4) return i + 1;   // never resets on a non-0xA5 byte
}

Root cause

A status frame (cmd 0x0b) carries two big-endian VFO frequencies at buffer[5..12], and those bytes are frequently 0xA5 (common HF frequencies land on it). USB-serial reads don't respect frame boundaries, so a read routinely delivers a prior frame's 0xA5-bearing tail followed by the next frame's sync run. The old counter reached 4 partway through those scattered/adjacent 0xA5 bytes and returned an index pointing into the sync run.

onReceiveData then read a 0xA5 as the length byte:

int len = data[startIndex] + 1;   // (byte)0xA5 + 1 == -90
buffer = new byte[len];           // new byte[-90] -> NegativeArraySizeException

The exception is swallowed by onReceiveData's try/catch, so it is not an app crash — but it aborts framing before clearBuffer(), leaving stale partial-frame state and silently dropping the frequency update. Symptom: the app's displayed frequency intermittently fails to track the GuoHe rig's VFO.

Fix

Reset the run counter on every non-0xA5 byte and return the first byte after a run of ≥4 consecutive 0xA5 — the true length byte:

if (data[i] == (byte) 0xa5) {
    count++;
} else if (count >= 4) {
    return i;      // first non-sync byte = length byte
} else {
    count = 0;     // sync must be consecutive
}

This also:

  • correctly skips a stray leading 0xA5 (e.g. a previous frame's CRC low byte) that would otherwise make five in a row and, under the old code, still land on a 0xA5 length byte;
  • returns -1 ("no complete header yet") instead of an out-of-range index when a sync run lands at the very end of a read.

checkHead is now package-private static (it referenced no instance state), so the framing logic is unit-tested directly without standing up the rig's Timer/connector.

Testing

New GuoHeCheckHeadTest (pure logic, no Robolectric needed) — 6 cases:

  • well-formed frame → length-byte index
  • non-consecutive 0xA5 payload bytes are not mistaken for sync
  • realistic status-frame tail (0xA5 freq bytes) + next sync → finds the length byte, never a 0xA5
  • 4 consecutive 0xA5 skips the whole sync run

  • no header → -1
  • sync run at buffer end → -1 (no overrun)

Four of these return the wrong index against the old implementation, so the suite fails-before / passes-after.

./gradlew :app:testDebugUnitTest --tests 'com.k1af.ft8af.rigs.*'
BUILD SUCCESSFUL   # GuoHeCheckHeadTest 6/6, all rigs tests green

Risk assessment

Very low. The change is confined to a single pure byte[] → int helper; no protocol/DSP/native code touched, no behavior change for correctly-framed input (the well-formed case returns the identical index as before). Strictly more robust for mis-framed input. No performance impact (same single linear pass).

Affected platforms

Android only — GuoHe Q900 CAT/frequency read path.

GuoHeQ900Rig.checkHead() counted 0xA5 bytes anywhere in the read buffer
and returned as soon as the 4th was seen. But every GuoHe frame begins
with FOUR CONSECUTIVE 0xA5 sync bytes (GuoHeRigConstant), and a status
frame's payload carries two big-endian VFO frequencies whose bytes are
frequently 0xA5. When a serial read splices a prior frame's 0xA5-bearing
tail onto the next frame's sync run, the counter reached 4 partway
through and returned an index *into* the sync run.

onReceiveData then read a 0xA5 as the length byte:
  (byte)0xA5 + 1 == -90  ->  new byte[-90]  ->  NegativeArraySizeException

The exception is swallowed by onReceiveData's try/catch (so it's not an
app crash) but it aborts framing before clearBuffer(), leaving stale
partial-frame state and silently dropping the frequency update — an
intermittent rig frequency-tracking failure on the GuoHe Q900.

Fix: reset the run counter on any non-0xA5 byte and return the first
byte after a run of >=4 consecutive 0xA5 (the true length byte). This
also correctly skips a stray leading 0xA5 that would otherwise make five
in a row, and reports "no header yet" (-1) instead of overrunning when a
sync run lands at the very end of a read.

checkHead is now package-private static (it uses no instance state) so
the framing logic is covered directly by GuoHeCheckHeadTest without
standing up the rig's Timer/connector.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes GuoHe Q900 (and related Xiegu-family) frame sync detection by updating GuoHeQ900Rig.checkHead() to only accept runs of 4+ consecutive 0xA5 bytes as the sync header, preventing mis-framing when 0xA5 bytes appear in payload (e.g., status-frame frequency fields).

Changes:

  • Update checkHead(byte[]) to reset the counter on non-0xA5 bytes and return the index of the first non-0xA5 byte after a >=4 sync run (the length byte).
  • Make checkHead static (package-private) so it can be unit-tested directly without instantiating the rig/timer.
  • Add a dedicated unit test suite covering well-formed frames and common mis-framing splice scenarios.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ft8af/app/src/main/java/com/k1af/ft8af/rigs/GuoHeQ900Rig.java Fixes header detection to require consecutive sync bytes and documents the framing rationale.
ft8af/app/src/test/java/com/k1af/ft8af/rigs/GuoHeCheckHeadTest.java Adds unit tests validating correct header detection across realistic stream/splice cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ft8af/app/src/main/java/com/k1af/ft8af/rigs/GuoHeQ900Rig.java Outdated
Comment thread ft8af/app/src/test/java/com/k1af/ft8af/rigs/GuoHeCheckHeadTest.java Outdated
- Javadoc: -1 also covers "sync run present but the length byte hasn't
  arrived yet" (a read ending inside the run), not just "no sync".
- Test comment: the scattered 0xA5s are sync bytes; the old bug was
  returning an index into the sync run so the caller read a sync byte as
  the length byte.

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

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 36.62%. Comparing base (1ab0385) to head (6e29e92).
⚠️ Report is 5 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##                dev     #624   +/-   ##
=========================================
  Coverage     36.62%   36.62%           
  Complexity      197      197           
=========================================
  Files           216      216           
  Lines         26885    26885           
  Branches       3294     3294           
=========================================
  Hits           9847     9847           
  Misses        16811    16811           
  Partials        227      227           
Flag Coverage Δ
android 15.03% <ø> (ø)
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@patrickrb
patrickrb merged commit 11776a8 into dev Jul 22, 2026
17 checks passed
@patrickrb
patrickrb deleted the optio/task-3feed5fb-3891-4ca7-9049-3dae2e249565 branch July 22, 2026 22:23
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