Skip to content

gps: find a u-blox by listening, not by guessing - #11999

Open
MrScothh wants to merge 1 commit into
iNavFlight:maintenance-10.xfrom
MrScothh:feature/gps-autobaud-listen
Open

MrScothh wants to merge 1 commit into
iNavFlight:maintenance-10.xfrom
MrScothh:feature/gps-autobaud-listen

Conversation

@MrScothh

@MrScothh MrScothh commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

What this fixes

A u-blox receiver that is not at the configured baud rate is often never found, and when it
is found it takes tens of seconds.

Autobaud walks the baud rate list and sends $PUBX,41 at every rate. At each rate that is
not the receiver's, that sentence reaches it as noise, and a u-blox then ignores its input
for about a second. In a blind sweep that deaf second falls on the one rate where the
command would have been understood, so the receiver is reached only when its rate happens
to be the first one tried.

Measured on a NEO-F10N whose flash had been left at 230400 by another flight stack, with
INAV configured for 115200, on an H743 board:

Sequence Receiver moved to the configured rate
As it is today 3 of 10
Only the step at the receiver's own rate 10 of 10
Same, but the rates are stepped through without sending anything 10 of 10
The command repeated for a second at the receiver's rate 10 of 10
A single command, two seconds after the last noise 10 of 10

In normal operation the same board went 20 to 70 seconds before the receiver was
identified, and sometimes never, reporting one timeout after another while the receiver was
transmitting the whole time.

What I changed

Listen before speaking. Each candidate rate gets a window, and the receiver is recognised by
what it sends: a UBX frame that passed its checksum, or two sentences shaped like NMEA,
which is all a module still in its factory configuration sends. Only then does the baud
rate command go out, once, at the rate the receiver is actually using.

  • The configured rate is listened to first, since a receiver INAV has already set up is the
    common case, and that case now costs 250 ms.
  • A second, slower pass gives each rate 1.2 s, enough for a module sending NMEA at 1 Hz.
  • A receiver that says nothing at any rate still gets the old blind sweep, run as the last
    pass of the same loop, since nothing else would reach it.

NMEA sentences are followed in the UBX parser's idle state, where the bytes are passing
anyway, so nothing is added to the receive path. The checksum of a sentence is not worked
out: two whole sentences are asked for, which noise at the wrong rate does not produce.

Testing

On the bench, with the same NEO-F10N and an H743:

  • ten baud rate changes out of ten succeeded, in 2 to 4 s each, with no timeouts, against
    none out of five in one direction and three slow ones out of five in the other before;
  • with the receiver already at the configured rate, nothing changes in what is sent, and the
    extra cost is the 250 ms listening window;
  • gps_ublox_unittest passes.

Built for F405, F722 and H743.

Size

The change costs 416 bytes of flash. The build job fails on ZEEZF7V3, which on
maintenance-10.x itself is already 179 bytes over its flash region (the same job fails on
other open PRs by that amount), and this change takes it to 595 over. Measured locally with
the same toolchain as CI.

@qodo-code-review

Copy link
Copy Markdown
Contributor

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Detect u-blox baud rates by listening before autobaud commands

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Detects active u-blox baud rates from checksum-valid UBX or repeated NMEA traffic.
• Prioritizes configured baud, then performs fast and slow listening passes.
• Sends one rate-change command after detection, retaining blind-sweep fallback for silent
 receivers.
Diagram

graph TD
    A["Autobaud start"] --> B["Configured first"] --> C["Listen two passes"] --> D{"Valid traffic?"}
    D -->|Yes| E{"Rate differs?"}
    E -->|Yes| F["Send baud command"] --> H["Configured baud"]
    E -->|No| H
    D -->|No| G["Blind sweep"] --> H
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Passive-only detection
  • ➕ Never sends disruptive noise at incorrect baud rates
  • ➕ Removes the legacy sweep and its additional delays
  • ➖ Cannot discover receivers that remain silent until queried
  • ➖ Would regress compatibility with receive-silent module configurations
2. Delayed active probing
  • ➕ Retains support for silent receivers
  • ➕ Requires less parser-side traffic classification
  • ➖ Still injects commands as noise at incorrect rates
  • ➖ Needs long quiet intervals and produces slower worst-case discovery
  • ➖ May remain sensitive to receiver-specific input suppression

Recommendation: Keep the hybrid listen-first strategy. Checksum-validated passive detection avoids disrupting transmitting receivers, while the legacy blind sweep remains an appropriate compatibility fallback for silent hardware. Active probing alone does not address the demonstrated u-blox deaf-period failure.

Files changed (2) +124 / -18

Bug fix (2) +124 / -18
gps_private.hAdd autobaud listening durations and persistent scan state +6/-0

Add autobaud listening durations and persistent scan state

• Defines fast and slow listening windows for configured UBX and factory NMEA output rates. Adds persistent pass and candidate-rate fields required across protothread delays.

src/main/io/gps_private.h

gps_ublox.cReplace blind-first autobaud with traffic-guided receiver detection +118/-18

Replace blind-first autobaud with traffic-guided receiver detection

• Tracks checksum-valid UBX frames and NMEA sentences as evidence of receiver traffic. Autobaud now scans the configured rate first using two listening windows, sends one baud-change command only after detection, and falls back to the prior blind sweep when no traffic is heard.

src/main/io/gps_ublox.c

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. U-blox firmware builds do not compile ✓ Resolved 🐞 Bug ≡ Correctness
Description
gpsNewFrameUBLOX() opens switch (nmeaStep) but reaches the existing outer parser cases without
closing that switch and its surrounding else block. Whenever GPS and the u-blox protocol are
enabled, the compiler encounters a duplicate case 1 in the nested switch and rejects the
translation unit.
Code

src/main/io/gps_ublox.c[R919-920]

+                    default:
+                        break;
Evidence
The outer _step switch begins at the u-blox parser, while the added switch (nmeaStep) declares
its own case 1 and is not closed after its default branch; the following UBX case 1 is therefore
parsed as a duplicate inner case. The file is compiled whenever both GPS and the u-blox protocol are
enabled.

src/main/io/gps_ublox.c[868-921]
src/main/io/gps_ublox.c[26-30]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The added NMEA parser does not close its nested switch and surrounding else block before the existing UBX parser cases, causing a duplicate case value and compilation failure.
## Fix Focus Areas
- src/main/io/gps_ublox.c[874-921]
## Recommended Fix
Close `switch (nmeaStep)` and the surrounding `else` block before the outer UBX parser's `case 1`, preserving the intended outer switch structure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Autobaud restarts before later rates ✓ Resolved 🐞 Bug ☼ Reliability
Description
gpsProtocolStateThread() installs the longer listening timeout through gpsSetProtocolTimeout(),
but gpsUpdate() decides communication loss using baseTimeoutMs instead of the resulting
timeoutMs. Once probing wrong rates takes more than one second, the GPS state changes to lost
communication and restarts initialization before the remaining first-pass rates or any slow-pass
rate can be heard.
Code

src/main/io/gps_ublox.c[R1286-1287]

+        gpsSetProtocolTimeout(MAX(GPS_TIMEOUT,
+                                  (GPS_BAUD_LISTEN_MS + GPS_BAUD_LISTEN_SLOW_MS + 50) * GPS_BAUDRATE_COUNT));
Evidence
The new sweep requests enough time for eight 250 ms windows plus eight 1.2 s windows, and every
ptDelayMs() returns control to gpsUpdate() while waiting. Although gpsSetProtocolTimeout()
stores that duration in gpsState.timeoutMs, the running-state check compares elapsed time to the
one-second baseTimeoutMs, then transitions through lost communication back to initialization.

src/main/io/gps_ublox.c[1282-1306]
src/main/io/gps_ublox.c[1323-1338]
src/main/io/gps.c[226-231]
src/main/io/gps.c[594-619]
src/main/scheduler/protothreads.h[151-163]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new multi-second autobaud listener sets `gpsState.timeoutMs`, but the running-state timeout check still uses the fixed base timeout and interrupts the sweep after one second.
## Fix Focus Areas
- src/main/io/gps_ublox.c[1286-1306]
- src/main/io/gps.c[226-231]
- src/main/io/gps.c[594-613]
## Recommended Fix
Change the running-state communication-loss check to use the active `gpsState.timeoutMs` set by `gpsSetProtocolTimeout()`, while retaining the existing calls that restore the base timeout after setup.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/main/io/gps_ublox.c Outdated
Comment thread src/main/io/gps_ublox.c Outdated
@MrScothh
MrScothh force-pushed the feature/gps-autobaud-listen branch from 3a463a1 to 968c682 Compare September 21, 2026 19:48
@MrScothh

Copy link
Copy Markdown
Contributor Author

Both findings were real, thank you.

The first was a botched transplant of the NMEA check onto this branch: the nested switch was left open, so any build with the u-blox protocol enabled failed to compile. Fixed, and built for SITL, F405, F722 and H743.

The second one I would not have caught on the bench, because the board I measure on also carries #11977, which is what makes the running state honour the timeout the protocol asks for. Rather than depend on that PR, the listening is now done in windows of 250 ms, each announced through gpsSetProtocolTimeout(), and the slow pass listens to six of them in a row instead of one long wait. Nothing the thread waits for is longer than the timeout that declares the receiver lost.

Measured again on the bench after the change: ten baud rate changes out of ten, 2.6 to 3.9 s each, no timeouts.

Autobaud sent the baud rate command at every rate in turn. At each rate that
is not the receiver's, the sentence arrives as noise, and a u-blox then
ignores its input for about a second. In a sweep that silence falls on the one
rate where the command would have been understood, so the receiver is reached
only when its rate happens to be the first one tried.

Measured on a NEO-F10N left at 230400 by another flight stack, with INAV
configured for 115200: the sweep moved it 3 times out of 10, and a board that
came up next to it went 20 to 70 seconds without a fix, sometimes never,
reporting one timeout after another while the receiver was talking the whole
time.

So listen first. Each rate is given a window, and the receiver is recognised
by what it sends: a UBX frame that passed its checksum, or two sentences
shaped like NMEA, which is all a module still in its factory configuration
sends. Only then does the command go out, once, at the rate the receiver is
actually using. The configured rate is tried first, since a receiver INAV has
already set up is the common case, and a second pass listens to several
windows in a row, which covers a module sending NMEA once per second. A
receiver that says nothing at any rate still gets the old sweep, run as the
last pass of the same loop, since nothing else would reach it.

The windows are shorter than the timeout that declares the receiver lost, and
each one is announced through gpsSetProtocolTimeout(), so that listening long
enough never restarts the search before it has heard anything.

With this, on the same bench, ten baud changes out of ten succeeded, each in
2 to 4 seconds, with no timeouts. The change costs 416 bytes of flash.
@MrScothh
MrScothh force-pushed the feature/gps-autobaud-listen branch from 968c682 to fd31c5d Compare September 21, 2026 21:42
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.

1 participant