gps "fiddling" v2 and some debug log fixes

This commit is contained in:
liquidraver
2026-08-05 11:48:33 +02:00
parent f83ce0bed4
commit d1093bfc5e
9 changed files with 396 additions and 54 deletions
@@ -30,6 +30,10 @@
> as a result. Reconnecting your terminal normally resets the node and gives you another 10 minutes;
> if it doesn't, power-cycle. Details in the light-sleep section below.
>
> **If your node's GPS went quiet after its first fix, this release fixes it.** A wrong bit in the
> sleep command put u-blox modules into permanent backup mode on boards without hardware GPS power
> control. Recovery needed a physical power cycle — a reboot was not enough. See the GPS section.
>
> **One CLI rename:** `set/get cad.probe.interval` is now `set/get probe.interval` — the same setting,
> renamed because one measurement now feeds both the noise floor and the CAD probe. Your stored value
> carries over; only the command name changed. `set/get agc.reset.interval` is gone (see below).
@@ -134,6 +138,64 @@ replaced by **`sp:<mean-spread>/<zero-spread %>(<count>)`**. A **non-zero mean p
independent**, however high the zero-spread share climbs — only mean `0.0` with a high share indicts the
sampler. Measured on air at BW 62.5: `0.6/90%` quiet, `0.9/84%` with the floor at 103 dBm.
### GPS: module configuration was never actually running — multi-constellation now works
The whole boot-time GNSS configuration path (constellations, AssistNow/EASY, elevation mask, fix rate)
was **dead code**. A feature macro was used ~500 lines before it was defined, and an undefined
identifier in `#if` is silently zero, so every board on the generic-NMEA driver ran on whatever its
module happened to default to. Fixing the ordering exposed several further problems, all of which had
been masked by the code never executing:
- **u-blox `UBX-CFG-GNSS` was malformed.** Config blocks were written 7 bytes long instead of 8 (the
`reserved0` field was missing from every block), so the receiver rejected the whole message. Rebuilt
correctly, and now limited to three concurrent major constellations, which is the M8 hardware limit.
- **`UBX-CFG-NAV5` had a one-byte checksum error**, so the elevation mask was silently discarded.
- **Galileo could not be reported at all** until the NMEA version is raised to 4.10 — the `$GAGSV`
talker does not exist below it. Now sent before the constellation config.
- **CASIC modules (Quectel L76K/L76KB, Air530Z) received no configuration whatsoever.** The generic
path spoke only PMTK and UBX, and CASIC parts understand neither. It now speaks PCAS as well, so a
L76K in a WisBlock slot is finally configured instead of sitting on its GPS+BeiDou factory default.
**If your Wio Tracker L1 has been tracking noticeably fewer satellites than it should, this is why.**
- **The NMEA output stream was being over-run.** At 9600 baud a multi-GNSS receiver emits more than the
link can carry once GSA and GSV multiply per constellation, and the failure mode is not "slow GPS"
but a receiver that appears dead. The output is now trimmed to what the firmware actually parses.
- **BeiDou satellites were being discarded by the parser** — the pre-4.10 `$BDGSV` talker was not
recognised, so BeiDou always read as zero even while tracking normally.
- **The navigation dynamic model is now set explicitly.** It is stored *in the module* and survives
reflashing the host, so a second-hand module can arrive stuck in an airborne or automotive model.
Repeaters and room servers get stationary; everything else automotive.
### Fixed: GPS sleep put u-blox modules to sleep permanently
`UBX-RXM-PMREQ` was sent with the wrong wake-source bit — `extint0` instead of `uartrx` — combined with
an infinite duration. On boards with no hardware GPS power control the module was told to enter backup
mode forever, wakeable only by a pin that is not routed on the RAK12500. **The wake byte we sent on the
UART could never wake it, and backup also powers down the I2C interface, so the module went completely
silent on every interface.** A reboot did not help either, because the supply rail is `regulator-boot-on`
and never toggles — only physically removing power recovered it.
In practice: any affected node got one fix after boot, then lost its GPS until someone power-cycled the
hardware. Fixed, and confirmed on hardware — the module now enters backup and wakes on the next duty
cycle with a ~3 second hot start.
### New: `set gps diag` / `get gps diag`
GNSS configuration is written blind — nothing in the protocol path confirms the module accepted it, and
a module with no sky view, one that is silent, and one stranded at the wrong baud rate all look
identical to the firmware. `set gps diag 1` re-runs configuration on the next `gps off` / `gps on`, and
`get gps diag` reports what happened:
```
> diag=on cfg=uart age=216s rx=559 mod=u-blox sent=17/464B sys=G9/R8/E6/B0/?0
```
`rx=` is parsed NMEA sentences (non-zero means the module is genuinely talking), `mod=` is the module
identity if it answered a version query, and `sys=` is tracked satellites per constellation, which is
the only real proof the constellation config took. Not persisted — it clears on reboot.
`CONFIG_ZEPHCORE_GPS_NMEA_DUMP` (see `boards/common/gps_debug.conf`) additionally logs every NMEA
sentence to the console for field diagnosis.
### Fixed: SX126x RSSI/AGC calibration used the wrong band on sub-GHz builds
Datasheet rev 2.2 (§6.1.6, new in Dec 2024) documents that the SX126x RSSI/AGC calibration registers are
+13
View File
@@ -764,6 +764,19 @@ config ZEPHCORE_REPEATER_GPS_INTERVAL_SEC
overrides it with "set gps duty <sec>" (persisted to flash).
Companions default to ZEPHCORE_GPS_POLL_INTERVAL_SEC (300s) instead.
config ZEPHCORE_GPS_NMEA_DUMP
bool "Log every NMEA sentence received from the GNSS module"
help
Print each sentence the GNSS driver parses, plus everything else the
module emits, to the console. For diagnosing a receiver that reports
no satellites: it distinguishes a module that is talking normally
while it searches the sky from one that is silent, at the wrong baud,
or has had its NMEA output disabled — cases that are otherwise
indistinguishable from the application.
Verbose: at 1 Hz with multi-GNSS this is a continuous stream, and on
a repeater it shares the console with the CLI. Diagnostic builds only.
config ZEPHCORE_GPS_NAV_MODE
int "GNSS navigation dynamic model (CASIC $PCAS11)"
range -1 7
+4 -3
View File
@@ -147,17 +147,18 @@ get gps diag # read it back
Sample reply:
```
> diag=on cfg=uart age=910s mod=URANUS5 sent=12/336B sys=G3/R4/E0/B3/?0
> diag=on cfg=uart age=910s rx=120 mod=URANUS5 sent=12/336B sys=G3/R4/E0/B3/?0
```
- `rx=` NMEA sentences the driver has parsed. **Check this first** — it is the only field that cannot be misread. Non-zero means the module is alive, at the right baud, and talking, so anything still wrong is signal or antenna. Zero means nothing is arriving at all, and no antenna work will help
- `cfg=` which path ran — `uart` (raw PMTK+PCAS+UBX), `api` (driver GNSS API), `blind` (neither available), or `never-run`
- `mod=` module identification captured from the `$GPTXT` reply to a version query, or `no-reply`
- `mod=` module identification, from a CASIC `$GPTXT` version reply or a u-blox `$PUBX` poll response, or `no-reply`. Only an explicit software-version token is accepted as an identity — TXT sentences also carry warnings, and a warning reported as an identity is worse than no answer
- `sent=` commands/bytes written to the module (UART path), or `sys_ret=`/`rate_ret=` return codes (API path)
- `sys=` tracked satellites per constellation from GSV talker IDs: **G**PS / GLONASS (**R**) / Galileo (**E**) / **B**eiDou / other. A constellation that stops reporting for 30 s decays to zero rather than showing a stale count
`sys=` totalling more than `sats=` in `get gps` is expected, not a discrepancy: GSV counts satellites **tracked**, GGA counts satellites **used in the fix solution**.
**`mod=` is the TX-path proof.** Everything else on this transport is written blind, so a module that hears nothing looks exactly like one that hears everything and ignores it. A version string means the module received a command and answered. `mod=no-reply` alongside a healthy `sats=` in `get gps` means the receive direction works but the module is not hearing us — wiring or pin assignment, not configuration.
**`rx=` first, then `mod=`.** `rx=` is the only field that cannot be misread: non-zero means the module is alive, at the right baud and talking, so anything still wrong is signal or antenna; zero means nothing is arriving at all. `mod=` then tells you whether the module *heard* us — everything on this transport is written blind, so a module that hears nothing looks exactly like one that hears everything and ignores it. `mod=no-reply` with `rx=` climbing means the receive direction works but our transmit does not reach it: wiring or pin assignment, not configuration.
**`sent=` proves transmission, not acceptance.** Only `sys=` shows what the module actually did. A module still running its factory or previously saved configuration reports `G` non-zero with the rest at `0`. Note `B0` is expected on u-blox M8 (BeiDou is deliberately disabled — only three major constellations can run concurrently), and `?0` is normal outside Japan (QZSS is regional).
+37 -4
View File
@@ -494,6 +494,14 @@ static bool gps_diag_on = false;
* MCU's TX line reaches the module at all. */
extern "C" int zephcore_gnss_version_get(char *buf, size_t len) __attribute__((weak));
/* Count of NMEA sentences the GNSS driver has parsed. This is the one signal
* in the whole diagnostic that cannot be misread: no satellites, a module
* silenced by a disabled output protocol, and a module stranded at the wrong
* baud all look identical otherwise. Non-zero means the module is alive and
* talking at our baud, so anything still wrong is signal; zero means nothing
* is arriving and no antenna work will change that. */
extern "C" uint32_t zephcore_gnss_rx_count(void) __attribute__((weak));
/* True only while gps_configure_via_uart() is running (see gps_uart_send). */
static bool gps_cfg_counting = false;
@@ -1429,8 +1437,23 @@ static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n";
* version=0, reserved[3]=0,
* duration=0x00000000 (infinite),
* flags=0x00000006 (backup + force),
* wakeupSources=0x00000020 (UART RX)
* Module stops all output and draws ~7µA. Wakes on any UART RX byte. */
* wakeupSources=0x00000028 (uartrx bit3 | extint0 bit5)
*
* THE WAKE SOURCE BIT IS LOAD-BEARING. wakeupSources bit 3 is uartrx; bit 5
* is extint0. This frame previously sent 0x20 — extint0 only — with a comment
* claiming that was "UART RX (bit 5)". It is not. EXTINT is not routed to the
* WisBlock connector on the RAK12500 (RAK's datasheet: only UART/I2C, 1PPS,
* RESET, VDD and GND are connected), so the module was told to sleep forever
* with a wake source that can never be asserted. duration=0 means infinite,
* so it never came back: no NMEA at any baud, and no I2C either, because
* backup mode powers down the DDC interface too. Only a physical power cycle
* recovered it — a reboot does not, since the 3V3_S rail stays up.
* Confirmed on hardware: reseat -> module answers -> one fix -> first standby
* -> gone again.
*
* 0x28 sets both, so a board that does wire EXTINT keeps that path as well.
* Module stops all output and draws ~20µA (ZOE-M8Q at 3V, datasheet Table 13;
* the 15µA hardware-backup figure needs VCC removed entirely). */
static const uint8_t ubx_pmreq_backup[] = {
0xB5, 0x62, /* UBX sync chars */
0x02, 0x41, /* Class: RXM, ID: PMREQ */
@@ -1440,9 +1463,9 @@ static const uint8_t ubx_pmreq_backup[] = {
0x00, 0x00, 0x00, /* reserved1[3] */
0x00, 0x00, 0x00, 0x00, /* duration: 0 = infinite */
0x06, 0x00, 0x00, 0x00, /* flags: backup(0x02) | force(0x04) */
0x20, 0x00, 0x00, 0x00, /* wakeupSources: UART RX (bit 5) */
0x28, 0x00, 0x00, 0x00, /* wakeupSources: uartrx(bit3) | extint0(bit5) */
/* Checksum (Fletcher-8 over class..payload) */
0x79, 0xCB
0x81, 0xEB
};
/* Put GPS module into software sleep (for boards without GPIO power control).
@@ -2259,6 +2282,16 @@ void gps_get_diag_report(char *buf, size_t len)
return;
}
/* Sentences parsed from the module. Print this before anything else
* that could be misinterpreted — rx=0 makes every other field moot. */
if (zephcore_gnss_rx_count != NULL) {
n += (size_t)snprintf(buf + n, len - n, " rx=%u",
(unsigned)zephcore_gnss_rx_count());
if (n >= len) {
return;
}
}
/* Module identity, when the driver could obtain it. Present means the
* module answered us, i.e. the TX path is good; absent on a driver that
* asks is a strong hint the module never hears our configuration. */
+24 -2
View File
@@ -589,7 +589,20 @@ void LoRaRadioBase::startReceive()
atomic_set(&_in_recv_mode, 1);
return;
}
if (ret != -ENOSYS) {
if (ret == -EBUSY) {
/* A concurrent TX owns the chip. Not the
* CAD-busy case: when the LBT branch of
* send_async restores RX in-driver it
* leaves the chip in RX, and the driver's
* idempotent fast-path (patch 0003)
* re-arms duty cycle from there AGC
* reset included rather than refusing.
* So -EBUSY here means the radio is
* genuinely mid-transmit; the fall-through
* to lora_recv_async will fail the same
* way and report it. */
LOG_DBG("rxduty: busy (TX in progress) — continuous RX");
} else if (ret != -ENOSYS) {
LOG_ERR("lora_recv_duty_cycle failed: %d", ret);
}
/* Fall through to continuous RX */
@@ -684,7 +697,16 @@ bool LoRaRadioBase::startSendRaw(const uint8_t *bytes, int len)
int ret = hwSendAsync(_tx_buf, (uint32_t)len, &_tx_signal);
if (ret < 0) {
LOG_ERR("hwSendAsync failed: %d", ret);
if (ret == -EBUSY) {
/* LBT refused the transmit because the channel is busy
* the designed outcome, not a fault. The dispatcher
* re-queues and retries. On a busy site this fires
* constantly, and at ERR it buries real faults and
* makes a healthy repeater look broken. */
LOG_DBG("hwSendAsync: channel busy (LBT), re-queuing");
} else {
LOG_ERR("hwSendAsync failed: %d", ret);
}
_board->onAfterTransmit();
atomic_set(&_tx_active, 0);
/* startReceive() is safe to call here regardless of failure
+36
View File
@@ -0,0 +1,36 @@
# GPS diagnostic overlay — dumps the raw NMEA stream to the USB console.
#
# For a receiver that reports no satellites. From the application's point of
# view a module with no sky view, a module silenced by a disabled output
# protocol, and a module stranded at a baud rate saved by a previous host all
# look identical: zero satellites, no fix. Seeing the actual bytes separates
# them in seconds.
#
# west build -b rak3401_1watt zephcore --pristine -- \
# -DEXTRA_CONF_FILE="boards/common/repeater.conf;boards/common/gps_debug.conf"
#
# Then watch the USB serial port. Expect one burst per second:
# NMEA: $GNRMC,... NMEA: $GNGGA,... NMEA: $GPGSV,...
#
# Sentences flowing -> module is alive at 9600; any remaining
# problem is signal or antenna.
# Nothing at all -> module is silent, at another baud, or not
# connected. Check "get gps diag" (rx=0 confirms).
# Garbled/partial lines -> wrong baud rate.
#
# Deliberately NOT boards/common/debug.conf: that routes logging to SEGGER RTT
# only, which needs a debug probe. This sends it to the console UART, which on
# these boards is the USB CDC port already used by the CLI.
# Logging to the console rather than RTT
CONFIG_LOG=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_BACKEND_RTT=n
CONFIG_USE_SEGGER_RTT=n
# The GNSS driver logs the sentence dump and the module version at INF
CONFIG_GNSS_LOG_LEVEL_INF=y
CONFIG_ZEPHCORE_GPS_LOG_LEVEL_INF=y
# Dump every NMEA sentence received
CONFIG_ZEPHCORE_GPS_NMEA_DUMP=y
@@ -1052,7 +1052,7 @@ index 30243ba5dc7..f0794661515 100644
if (ret < 0) {
sx126x_set_sleep(dev);
k_mutex_unlock(&data->lock);
@@ -1083,14 +1718,671 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
@@ -1083,14 +1718,733 @@ static int sx126x_lora_test_cw(const struct device *dev, uint32_t frequency,
return 0;
}
@@ -1576,6 +1576,91 @@ index 30243ba5dc7..f0794661515 100644
+ return -EINVAL;
+ }
+
+ /* Explicit timing only. The window sizing (detection budget,
+ * datasheet Tpreamble+Theader <= 2*rxPeriod+sleepPeriod rule, TCXO
+ * transition accounting) is owned by the caller -- the adapter layer
+ * knows the mesh preamble policy, the driver just programs periods.
+ * Validated before we claim the state so a malformed request cannot
+ * leave the chip half-configured. */
+ if (K_TIMEOUT_EQ(rx_period, K_FOREVER) ||
+ K_TIMEOUT_EQ(sleep_period, K_FOREVER)) {
+ LOG_ERR("recv_duty_cycle: explicit rx/sleep periods required");
+ k_mutex_unlock(&data->lock);
+ return -EINVAL;
+ }
+
+ /* Convert us to 15.625us steps */
+ uint32_t rx_time = (k_ticks_to_us_ceil32(rx_period.ticks) * 64) / 1000;
+ uint32_t sleep_time = (k_ticks_to_us_ceil32(sleep_period.ticks) * 64) / 1000;
+
+ if (rx_time < 64) rx_time = 64;
+ if (sleep_time < 64) sleep_time = 64;
+
+ /* Idempotent fast-path: the chip is already in RX.
+ *
+ * The usual way here is the LBT branch of sx126x_lora_send_async: on
+ * CAD-busy it calls sx126x_restart_rx() and returns -EBUSY, leaving
+ * state == RX. Failing the CAS below would then report -EBUSY to a
+ * caller whose only honest reading of it is "a concurrent TX owns the
+ * chip". Worse, it is a one-way door: an entry that fell back to
+ * continuous RX (any error path below that returns before
+ * rx_duty_cycle_enabled is set) could never re-arm, because every
+ * later attempt hits the same failed CAS -- the node quietly listens
+ * at full RX current until it is rebooted.
+ *
+ * sx126x_restart_rx() runs the identical entry sequence to the one
+ * below (Calibrate(ALL) AGC reset, CalibrateImage, DIO2 RF switch,
+ * StopTimerOnPreamble, RX-gain retention, SetRxDutyCycle), so re-arming
+ * through it gives up nothing. When the chip is already duty cycling
+ * on these exact periods there is nothing to re-arm and only the
+ * callback is refreshed: no SPI traffic, no ~5 ms calibration, no
+ * dropped RX window. */
+ if (atomic_get(&data->state) == SX126X_STATE_RX) {
+ const bool rearm = !data->rx_duty_cycle_enabled ||
+ data->dc_rx_time != rx_time ||
+ data->dc_sleep_time != sleep_time;
+
+ data->rx_cb = cb;
+ data->rx_cb_user_data = user_data;
+ atomic_inc(&data->rx_cb_gen);
+
+ if (!rearm) {
+ k_mutex_unlock(&data->lock);
+ return 0;
+ }
+
+ data->dc_rx_time = rx_time;
+ data->dc_sleep_time = sleep_time;
+ data->rx_duty_cycle_enabled = true;
+
+ /* Mid-cycle the chip may be in its duty-cycle sleep phase with
+ * BUSY asserted; talking to it then stalls the HAL in
+ * wait_busy(). Poke CS first, same as the cancel path above. */
+ if (sx126x_hal_is_busy(dev)) {
+ sx126x_hal_wakeup(dev);
+ }
+
+ ret = sx126x_restart_rx(dev, data);
+ if (ret < 0) {
+ /* Hand the chip back un-owned so the caller's
+ * continuous-RX fallback performs a full clean entry
+ * rather than fast-pathing onto a radio we have just
+ * failed to program. */
+ data->rx_duty_cycle_enabled = false;
+ data->rx_cb = NULL;
+ data->rx_cb_user_data = NULL;
+ atomic_inc(&data->rx_cb_gen);
+ atomic_set(&data->state, SX126X_REST_STATE);
+ k_mutex_unlock(&data->lock);
+ return ret;
+ }
+
+ k_mutex_unlock(&data->lock);
+ LOG_DBG("recv_duty_cycle: re-armed in place rx=%uus sleep=%uus",
+ (rx_time * 1000) / 64, (sleep_time * 1000) / 64);
+ return 0;
+ }
+
+ if (!atomic_cas(&data->state, SX126X_REST_STATE, SX126X_STATE_RX)) {
+ k_mutex_unlock(&data->lock);
+ return -EBUSY;
@@ -1649,31 +1734,8 @@ index 30243ba5dc7..f0794661515 100644
+
+ sx126x_set_rf_path(dev, true, false);
+
+ uint32_t rx_time, sleep_time;
+
+ /* Explicit timing only. The window sizing (detection budget,
+ * datasheet Tpreamble+Theader <= 2*rxPeriod+sleepPeriod rule, TCXO
+ * transition accounting) is owned by the caller -- the adapter layer
+ * knows the mesh preamble policy, the driver just programs periods. */
+ if (K_TIMEOUT_EQ(rx_period, K_FOREVER) ||
+ K_TIMEOUT_EQ(sleep_period, K_FOREVER)) {
+ LOG_ERR("recv_duty_cycle: explicit rx/sleep periods required");
+ data->rx_cb = NULL;
+ atomic_inc(&data->rx_cb_gen);
+ sx126x_set_sleep(dev);
+ k_mutex_unlock(&data->lock);
+ atomic_set(&data->state, SX126X_REST_STATE);
+ return -EINVAL;
+ }
+
+ /* Convert us to 15.625us steps */
+ rx_time = (k_ticks_to_us_ceil32(rx_period.ticks) * 64) / 1000;
+ sleep_time = (k_ticks_to_us_ceil32(sleep_period.ticks) * 64) / 1000;
+
+ if (rx_time < 64) rx_time = 64;
+ if (sleep_time < 64) sleep_time = 64;
+
+ /* Store for restart_rx to re-use */
+ /* Store for restart_rx to re-use (periods validated and converted at
+ * function entry, before the state was claimed). */
+ data->dc_rx_time = rx_time;
+ data->dc_sleep_time = sleep_time;
+ data->rx_duty_cycle_enabled = true;
@@ -43,7 +43,7 @@ index c5d09261da5..94ff2848b88 100644
+
endif
diff --git a/drivers/gnss/gnss_luatos_air530z.c b/drivers/gnss/gnss_luatos_air530z.c
index 74708edbd62..7fc5e4768f1 100644
index 74708edbd62..82320abc125 100644
--- a/drivers/gnss/gnss_luatos_air530z.c
+++ b/drivers/gnss/gnss_luatos_air530z.c
@@ -9,7 +9,6 @@
@@ -132,7 +132,7 @@ index 74708edbd62..7fc5e4768f1 100644
MODEM_CHAT_SCRIPT_NO_ABORT_DEFINE(init_script, init_script_cmds, NULL, 5);
struct gnss_luatos_air530z_config {
@@ -73,12 +131,57 @@ struct gnss_luatos_air530z_data {
@@ -73,12 +131,81 @@ struct gnss_luatos_air530z_data {
struct k_sem lock;
};
@@ -177,10 +177,36 @@ index 74708edbd62..7fc5e4768f1 100644
+ return (int)strlen(buf);
+}
+#endif /* CONFIG_GNSS_LUATOS_AIR530Z_VERSION_QUERY */
+
+/* Raw sentence counter — see the equivalent note in gnss_nmea_generic.c.
+ * Non-zero means the module is alive, at the right baud, and emitting
+ * parseable NMEA; zero means nothing is arriving at all. */
+static uint32_t air530z_rx_sentences;
+
+static void air530z_gga_cb(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ air530z_rx_sentences++;
+ gnss_nmea0183_match_gga_callback(chat, argv, argc, user_data);
+}
+
+static void air530z_rmc_cb(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ air530z_rx_sentences++;
+ gnss_nmea0183_match_rmc_callback(chat, argv, argc, user_data);
+}
+
+__weak uint32_t zephcore_gnss_rx_count(void)
+{
+ return air530z_rx_sentences;
+}
+
MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
- MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
- MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
+ MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", air530z_gga_cb),
+ MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", air530z_rmc_cb),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
@@ -190,7 +216,7 @@ index 74708edbd62..7fc5e4768f1 100644
);
static void luatos_air530z_lock(const struct device *dev)
@@ -217,54 +320,11 @@ static int gnss_luatos_air530z_init(const struct device *dev)
@@ -217,54 +344,11 @@ static int gnss_luatos_air530z_init(const struct device *dev)
return 0;
}
@@ -250,7 +276,7 @@ index 74708edbd62..7fc5e4768f1 100644
static int luatos_air530z_set_fix_rate(const struct device *dev, uint32_t fix_interval_ms)
{
@@ -356,10 +416,8 @@ static DEVICE_API(gnss, gnss_api) = {
@@ -356,10 +440,8 @@ static DEVICE_API(gnss, gnss_api) = {
.dynamic_separators_buf = {',', '*'}, \
}; \
\
@@ -282,10 +308,10 @@ index d4e7b502e39..c8e8f2da2d2 100644
*sv_system = GNSS_SYSTEM_GPS;
break;
diff --git a/drivers/gnss/gnss_nmea_generic.c b/drivers/gnss/gnss_nmea_generic.c
index bef7989828d..5e5a90716fb 100644
index bef7989828d..2042c85a2ee 100644
--- a/drivers/gnss/gnss_nmea_generic.c
+++ b/drivers/gnss/gnss_nmea_generic.c
@@ -50,12 +50,83 @@ struct gnss_nmea_generic_data {
@@ -50,11 +50,163 @@ struct gnss_nmea_generic_data {
uint8_t *chat_argv[CHAT_ARGV_SZ];
};
@@ -307,8 +333,7 @@ index bef7989828d..5e5a90716fb 100644
+ ARG_UNUSED(chat);
+ ARG_UNUSED(user_data);
+
+ /* Prefer an explicit software-version token; otherwise keep the last
+ * text field, which is where u-blox put their boot banner. */
+ /* Only an explicit software-version token counts as an identity. */
+ for (uint16_t i = 1; i < argc; i++) {
+ if (strncmp(argv[i], "SW=", 3) == 0) {
+ strncpy(nmea_generic_version, argv[i] + 3,
@@ -319,10 +344,12 @@ index bef7989828d..5e5a90716fb 100644
+ }
+ }
+
+ if (argc > 4 && nmea_generic_version[0] == '\0') {
+ strncpy(nmea_generic_version, argv[4], sizeof(nmea_generic_version) - 1);
+ nmea_generic_version[sizeof(nmea_generic_version) - 1] = '\0';
+ }
+ /* Deliberately NO generic fallback to "keep the last text field".
+ * TXT sentences also carry warnings and errors — a module complaining
+ * about a sentence it did not understand was being captured and then
+ * reported as though it were the module's identity. A diagnostic that
+ * invents an answer is worse than one admitting it has none, so an
+ * unrecognised TXT is ignored and the field reads as no-reply. */
+}
+
+/* u-blox reply to a $PUBX poll. u-blox ignore the CASIC $PCAS06 query, so
@@ -357,15 +384,96 @@ index bef7989828d..5e5a90716fb 100644
+ buf[len - 1] = '\0';
+ return (int)strlen(buf);
+}
+
+/* Raw sentence dump for field diagnosis (CONFIG_ZEPHCORE_GPS_NMEA_DUMP).
+ *
+ * Reconstructs the sentence from the parsed argv and logs it. Enabled only
+ * for deliberate debugging: at 1 Hz with multi-GNSS this is a lot of console
+ * traffic, and on a repeater the console is shared with the CLI.
+ *
+ * Matching is first-match-wins, so the ordinary GGA/RMC/GSV matches never
+ * reach a catch-all placed after them — hence the logging lives in the
+ * wrappers as well as in the catch-all, which then covers everything else the
+ * module emits (TXT banners, PUBX replies, sentences we do not parse).
+ */
+#ifdef CONFIG_ZEPHCORE_GPS_NMEA_DUMP
+static void nmea_generic_dump(char **argv, uint16_t argc)
+{
+ char line[128];
+ size_t n = 0;
+
+ for (uint16_t i = 0; i < argc && n < sizeof(line) - 2; i++) {
+ n += (size_t)snprintk(&line[n], sizeof(line) - n, "%s%s",
+ (i == 0) ? "" : ",", argv[i]);
+ }
+ LOG_INF("NMEA: %s", line);
+}
+
+static void nmea_generic_catchall_cb(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ ARG_UNUSED(chat);
+ ARG_UNUSED(user_data);
+ nmea_generic_dump(argv, argc);
+}
+#else
+#define nmea_generic_dump(argv, argc) do { } while (0)
+#endif
+
+/* Raw sentence counter — the only unambiguous "is the module talking to us"
+ * signal available.
+ *
+ * Everything else can be misread. No satellites, a module mute because its
+ * output protocol was disabled, and a module stranded at the wrong baud all
+ * present identically to the application: zero satellites and no fix. Even
+ * the position callback cannot tell them apart, because the NMEA matcher only
+ * publishes once GGA and RMC agree on a valid UTC — so a receiver that is
+ * healthily emitting NMEA while it searches the sky publishes nothing at all.
+ *
+ * Counting parsed sentences separates the cases outright: non-zero means the
+ * module is alive, at the right baud, and emitting NMEA we can parse, so any
+ * remaining problem is signal. Zero means nothing is arriving and no amount
+ * of antenna work will help.
+ */
+static uint32_t nmea_rx_sentences;
+
+static void nmea_generic_gga_cb(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ nmea_rx_sentences++;
+ nmea_generic_dump(argv, argc);
+ gnss_nmea0183_match_gga_callback(chat, argv, argc, user_data);
+}
+
+static void nmea_generic_rmc_cb(struct modem_chat *chat, char **argv, uint16_t argc,
+ void *user_data)
+{
+ nmea_rx_sentences++;
+ nmea_generic_dump(argv, argc);
+ gnss_nmea0183_match_rmc_callback(chat, argv, argc, user_data);
+}
+
+/* Exported for application-level diagnostics. Weak so boards building another
+ * GNSS driver that also exports it still link. */
+__weak uint32_t zephcore_gnss_rx_count(void)
+{
+ return nmea_rx_sentences;
+}
+
MODEM_CHAT_MATCHES_DEFINE(unsol_matches,
MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
- MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", gnss_nmea0183_match_gga_callback),
- MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", gnss_nmea0183_match_rmc_callback),
+ MODEM_CHAT_MATCH_WILDCARD("$??GGA,", ",*", nmea_generic_gga_cb),
+ MODEM_CHAT_MATCH_WILDCARD("$??RMC,", ",*", nmea_generic_rmc_cb),
#if CONFIG_GNSS_SATELLITES
MODEM_CHAT_MATCH_WILDCARD("$??GSV,", ",*", gnss_nmea0183_match_gsv_callback),
#endif
+#endif
+ MODEM_CHAT_MATCH_WILDCARD("$??TXT,", ",*", nmea_generic_txt_callback),
+ MODEM_CHAT_MATCH("$PUBX", ",*", nmea_generic_pubx_callback),
+#ifdef CONFIG_ZEPHCORE_GPS_NMEA_DUMP
+ /* Must stay LAST: a zero-length match matches every line, so anything
+ * after it would be unreachable. */
+ MODEM_CHAT_MATCH("", ",*", nmea_generic_catchall_cb),
#endif
);
static int gnss_nmea_generic_resume(const struct device *dev)
+6 -1
View File
@@ -511,7 +511,12 @@ void Dispatcher::checkSend()
bool success = _radio->startSendRaw(raw, len);
if (!success) {
uint32_t retry = getCADFailRetryDelay();
LOG_ERR("checkSend: startSendRaw failed! re-queuing delay=%u", retry);
/* Almost always LBT refusing a busy channel, which
* is the designed outcome the packet is
* re-queued below and retried. At ERR this fires
* continuously on a busy site and buries real
* faults. */
LOG_DBG("checkSend: startSendRaw refused, re-queuing delay=%u", retry);
logTxFail(outbound, outbound->getRawLength());
_mgr->queueOutbound(outbound, outbound_priority, futureMillis((int)retry));
outbound = nullptr;