mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 04:49:54 +00:00
gps_power_control() mixed the two GPIO conventions on the same pins. It configured lines with GPIO_OUTPUT_HIGH / GPIO_OUTPUT_LOW, which Zephyr defines as physical levels, then drove the same lines with gpio_pin_set_dt(), which is logical. On a line declared GPIO_ACTIVE_LOW those are opposite, so the first call asserted the reverse of every call after it. Two lines are actually broken by this. gps-enable, on the boards declaring it active-low. Most of them reach the block named for the T1000-E rather than the "simple boards" path, because HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP) and a bare gps-reset alias is enough to route a board there. That block drives the enable physical HIGH unconditionally on every power-on, and power-off lands on physical HIGH too, so the line is de-asserted in both states and the module is never powered at all. gps-reset, on the six boards declaring it active-low: heltec_t096, heltec_t1, heltec_t114, thinknode_m1, heltec_wifi_lora32_v4 and _v43. The documented power-on sequence is "assert reset, hold 10 ms, release", written as GPIO_OUTPUT_HIGH then gpio_pin_set_dt(0). On an active-low line both of those are physical HIGH, so the pulse never asserts and the module starts uncontrolled -- the condition the power-off comment says this code prevents. heltec_wifi_lora32_v4's overlay documented the no-op rather than fixing it; that comment is updated here, with _v43's copy. vrtc, sleep, rtcint and resetb are converted too. Every board declaring them is active-high, so those change nothing today; they are converted so the root cause does not survive in four more places. Converting is safe by construction. gpio_pin_configure() flips the init level only when both GPIO_OUTPUT_INIT_LOGICAL and GPIO_ACTIVE_LOW are set, then strips INIT_LOGICAL, so on an active-high pin GPIO_OUTPUT_ACTIVE produces a flags word byte-identical to GPIO_OUTPUT_HIGH. Behaviour changes only where a line is declared active-low, which is only where it is currently wrong. gps_power_off_for_shutdown() had the same inversion and it mattered most there: on the active-low boards the old physical LOW asserted the enable, powering the GPS on as the device entered System OFF rather than powering it down. Its header comment also described a narrower scope than the function has, naming only the power enable where it configures vrtc, reset, sleep, rtcint and resetb as well; that is corrected here. Comments that described a level this change now drives logically are reworded to asserted/de-asserted. Comments stating genuine electrical facts, such as a pin floating LOW without its pull-up, are left as they are, since rewording those would make them wrong. Verified on hardware on a Heltec V4-R8, whose gps-enable is active-low: with this change the boot log reports "GPS power GPIO configured, set ACTIVE" and the L76K streams NMEA (210 sentences in 35 s), where without it the log reports "set HIGH" and the module never powers on. Also built for heltec_wifi_lora32_v4 and _v43 (active-low enable and reset), thinknode_m1 and heltec_t114 (active-low reset) and t1000_e (active-high throughout). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2427 lines
91 KiB
C++
2427 lines
91 KiB
C++
/*
|
|
* SPDX-License-Identifier: MIT
|
|
* Zephyr GPS Manager - GNSS power management, fix acquisition, constellation config
|
|
*
|
|
* Event-driven state machine with no polling loops:
|
|
* - LoRa/BLE events trigger GPS enable/disable
|
|
* - k_work_delayable handles standby/timeout timers
|
|
* - GNSS callback fires on fix data from driver
|
|
*
|
|
* Power strategy:
|
|
* - Direct GPIO toggle via gps-enable alias (all boards)
|
|
* - T1000-E warm standby: VRTC stays powered during standby, preserving
|
|
* ephemeris/almanac/RTC in backup RAM for fast re-acquisition (3-8s vs 15-45s)
|
|
* - GNSS UARTE suspended (device PM) while GPS is off/standby — releases
|
|
* HFCLK on nRF52840 (~0.5-1 mA), resumed before every wake
|
|
* - Full power-off only on user-disable or System OFF
|
|
*/
|
|
|
|
#include "ZephyrGPSManager.h"
|
|
#include "../../helpers/pm_sleep_guard.h"
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/devicetree.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/drivers/gpio.h>
|
|
#include <zephyr/drivers/uart.h>
|
|
#include <zephyr/drivers/regulator.h>
|
|
#include <zephyr/drivers/i2c.h>
|
|
#include <zephyr/pm/device.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#if defined(CONFIG_SOC_NRF52840)
|
|
#include <nrfx.h>
|
|
#endif
|
|
|
|
LOG_MODULE_REGISTER(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL);
|
|
|
|
/* ========== GNSS Support ========== */
|
|
#if DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic) || \
|
|
DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8) || \
|
|
DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p) || \
|
|
DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g) || \
|
|
DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g) || \
|
|
DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z)
|
|
#define HAS_GNSS 1
|
|
#include <zephyr/drivers/gnss.h>
|
|
#else
|
|
#define HAS_GNSS 0
|
|
#endif
|
|
|
|
/* ========== GPS Feature Detection ==========
|
|
* Every HAS_GPS_* predicate is defined HERE, before first use. They are pure
|
|
* devicetree tests with no side effects, kept apart from the variables they
|
|
* gate so that ordering can never drift again.
|
|
*
|
|
* Why this block exists: HAS_GPS_UART used to be defined ~500 lines below its
|
|
* first `#if`, and an undefined identifier in `#if` is silently 0 — so the
|
|
* entire PMTK/UBX module-configuration path compiled to nothing on every
|
|
* board, and GPS ran at module defaults (GPS-only constellations, no AOP).
|
|
* If you add another HAS_GPS_* macro, define it in this block.
|
|
*/
|
|
|
|
/* GNSS module hangs off a UART we can write to (any compatible). */
|
|
#if HAS_GNSS && DT_NODE_HAS_STATUS(DT_NODELABEL(gnss), okay) && \
|
|
DT_NODE_HAS_STATUS(DT_BUS(DT_NODELABEL(gnss)), okay)
|
|
#define HAS_GPS_UART 1
|
|
#else
|
|
#define HAS_GPS_UART 0
|
|
#endif
|
|
|
|
/* Discrete GPS power-enable GPIO (gps-enable alias). */
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_enable))
|
|
#define HAS_GPS_POWER_CONTROL 1
|
|
#else
|
|
#define HAS_GPS_POWER_CONTROL 0
|
|
#endif
|
|
|
|
/* GPS powered from a PMU regulator rail (chosen zephcore,gps-power). */
|
|
#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_power))
|
|
#define HAS_GPS_POWER_REGULATOR 1
|
|
#else
|
|
#define HAS_GPS_POWER_REGULATOR 0
|
|
#endif
|
|
|
|
/* Runtime diagnostics toggle. Declared OUT here, not inside the HAS_GNSS
|
|
* block: gps_set_diag()/gps_get_diag()/gps_get_diag_report() are part of the
|
|
* unconditional public API and are compiled on boards with no GNSS at all,
|
|
* so a declaration hidden behind HAS_GNSS breaks every such board. */
|
|
static bool gps_diag_on = false;
|
|
|
|
/* ========== GPS Power Strategy ==========
|
|
* Module power is GPIO/regulator controlled — GNSS driver PM is not used
|
|
* for the module itself:
|
|
* - Wio Tracker L1 (L76K): P1.09 is the module's WAKEUP pin, not a supply
|
|
* switch. Per the L76K hardware design: WAKEUP is a digital input, active
|
|
* low with an internal pull-up, that "enters or exits Standby mode". In
|
|
* Standby the RF is powered off but the internal core and I/O power domain
|
|
* stay active, so VCC is never removed and ephemeris/almanac/RTC survive —
|
|
* every wake is a warm start, not a cold one. (Backup mode, the deeper
|
|
* state, requires cutting VCC while V_BCKP holds the RTC domain; this
|
|
* board has no VCC switch, so Standby is the floor available to us.)
|
|
* - T1000-E (AG3335): GPS_EN de-asserted + VRTC asserted = warm standby (ephemeris
|
|
* preserved via backup RAM, ~1-2µA VRTC current)
|
|
* - All boards: gps-enable alias → GPIO power control
|
|
*
|
|
* CONFIG_PM_DEVICE is on globally, but nothing suspends automatically —
|
|
* system-managed suspend is compiled only under CONFIG_PM (off everywhere).
|
|
* This manager makes exactly two kinds of PM calls, both main-thread only:
|
|
* - a one-time RESUME of the GNSS device at boot (gnss-nmea-generic inits
|
|
* suspended under CONFIG_PM_DEVICE and never opens its pipe otherwise);
|
|
* - suspend/resume of the GNSS UARTE around standby/off (an armed UARTE RX
|
|
* holds HFCLK ≈0.5-1 mA on nRF52840 even with the module powered off).
|
|
* The old "PM broke GPS" deadlock was modem_chat_run_script() being reached
|
|
* from the system workqueue via driver PM hooks — the air530z driver is
|
|
* PM-less now and every PM call here stays on the main thread. */
|
|
|
|
/* ========== GPS State - Power Management ========== */
|
|
#if HAS_GNSS
|
|
static struct gps_position current_pos;
|
|
static struct gnss_time current_utc;
|
|
static bool gps_enabled = false;
|
|
static bool gps_available = false;
|
|
static K_MUTEX_DEFINE(gps_mutex);
|
|
static gps_enable_callback_t gps_enable_cb = NULL;
|
|
static gps_fix_callback_t gps_fix_cb = NULL;
|
|
static gps_event_callback_t gps_event_cb = NULL;
|
|
|
|
/* Pending GPS actions — set by work handlers (system work queue),
|
|
* consumed by gps_process_event() (main thread).
|
|
* This avoids calling blocking GNSS APIs from the system work queue,
|
|
* which deadlocks because modem_chat_run_script() blocks on a semaphore
|
|
* that's signaled from the same work queue. */
|
|
#define GPS_ACTION_WAKE BIT(0) /* Wake from standby → start acquiring */
|
|
#define GPS_ACTION_TIMEOUT BIT(1) /* Acquisition timeout → go to standby */
|
|
#define GPS_ACTION_FIX_DONE BIT(2) /* Got enough good fixes → go to standby */
|
|
static atomic_t pending_gps_actions;
|
|
|
|
/* GPS Power Management State Machine */
|
|
enum gps_state {
|
|
GPS_STATE_OFF, /* GPS disabled by user */
|
|
GPS_STATE_STANDBY, /* GPS enabled but sleeping (5 min cycle) */
|
|
GPS_STATE_ACQUIRING, /* GPS awake, waiting for fixes */
|
|
};
|
|
|
|
static enum gps_state gps_current_state = GPS_STATE_OFF;
|
|
static uint8_t consecutive_good_fixes = 0;
|
|
static bool first_fix_acquired = false; /* True after first 3-good-fix cycle since enable. Cleared on gps_enable(false) and at boot. */
|
|
static bool first_acquire_used = false; /* True once the one-time long cold-start window has ended (fix or timeout). Cleared on gps_enable(false) and at boot. */
|
|
static bool gps_time_synced = false; /* True after GPS syncs RTC. Starts false at boot (RTC reset),
|
|
* set true after 3 good fixes, cleared when GPS disabled. */
|
|
static int64_t last_fix_uptime_ms = 0; /* k_uptime when last validated fix was acquired */
|
|
static int64_t standby_start_ms = 0; /* k_uptime when standby started (for next-wake calc) */
|
|
static uint64_t standby_interval_ms = 0; /* How long standby lasts (for next-wake calc) */
|
|
|
|
#define GPS_GOOD_FIX_COUNT 3 /* Need 3 consecutive good fixes */
|
|
#define GPS_MIN_SATELLITES 4 /* Minimum satellites for valid fix */
|
|
|
|
/* Runtime-configurable intervals, initialized from Kconfig defaults */
|
|
static uint32_t gps_acquire_timeout_ms = CONFIG_ZEPHCORE_GPS_FIX_TIMEOUT_SEC * 1000U;
|
|
static uint32_t gps_first_fix_timeout_ms = CONFIG_ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC * 1000U;
|
|
static uint32_t gps_wake_interval_ms = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC * 1000U;
|
|
|
|
/* Duty cycle vs always-on: a non-zero standby interval duty-cycles; interval 0
|
|
* keeps the GPS in continuous acquisition (never sleeps) so it streams fresh
|
|
* fixes for telemetry and can download a full almanac. */
|
|
static inline bool gps_duty_cycling(void)
|
|
{
|
|
return gps_wake_interval_ms != 0;
|
|
}
|
|
|
|
/* k_uptime of the last always-on fix-callback invocation — rate-limits the
|
|
* RTC sync / node-position update to gps_acquire_timeout_ms while streaming
|
|
* (see gnss_data_cb), so 1Hz fixes don't fire the callback continuously. */
|
|
static int64_t last_promote_ms = 0;
|
|
|
|
/* Repeater acquire window — GPS only for time sync. The standby interval is
|
|
* unified with companions via gps_wake_interval_ms (prefs.gps_interval). */
|
|
#define GPS_REPEATER_SYNC_TIMEOUT_MS (5 * 60 * 1000) /* 5 minutes */
|
|
|
|
static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */
|
|
static bool gnss_activity_seen_this_cycle = false; /* Runtime-only: set by GNSS callback while acquiring */
|
|
|
|
/* Forward declarations for work handlers and state functions */
|
|
static void gps_wake_work_fn(struct k_work *work);
|
|
static void gps_timeout_work_fn(struct k_work *work);
|
|
static void gps_go_to_standby(void);
|
|
static void gps_start_acquiring(void);
|
|
|
|
/* Delayable work for event-driven timers (no polling!) */
|
|
static K_WORK_DELAYABLE_DEFINE(gps_wake_work, gps_wake_work_fn);
|
|
static K_WORK_DELAYABLE_DEFINE(gps_timeout_work, gps_timeout_work_fn);
|
|
|
|
#else
|
|
static gps_enable_callback_t gps_enable_cb = NULL;
|
|
#endif
|
|
|
|
void gps_set_enable_callback(gps_enable_callback_t cb)
|
|
{
|
|
gps_enable_cb = cb;
|
|
}
|
|
|
|
void gps_set_fix_callback(gps_fix_callback_t cb)
|
|
{
|
|
#if HAS_GNSS
|
|
gps_fix_cb = cb;
|
|
#else
|
|
ARG_UNUSED(cb);
|
|
#endif
|
|
}
|
|
|
|
void gps_set_event_callback(gps_event_callback_t cb)
|
|
{
|
|
#if HAS_GNSS
|
|
gps_event_cb = cb;
|
|
#else
|
|
ARG_UNUSED(cb);
|
|
#endif
|
|
}
|
|
|
|
#if HAS_GNSS
|
|
|
|
/* GNSS callback - called when new fix data is available */
|
|
static void gnss_data_cb(const struct device *dev, const struct gnss_data *data)
|
|
{
|
|
ARG_UNUSED(dev);
|
|
|
|
if (!gps_enabled || gps_current_state == GPS_STATE_STANDBY) {
|
|
/* GPS disabled or in standby — ignore NMEA data.
|
|
* The GNSS driver fires callbacks as long as the UART has data,
|
|
* even after we de-assert GPS_EN (module drains its buffer).
|
|
* On boards without GPS power control (e.g. RAK3401 where 3V3_S
|
|
* rail is shared with LoRa FEM), the GPS module stays powered in
|
|
* standby and keeps streaming NMEA — suppress those callbacks to
|
|
* avoid log spam and wasted CPU for the entire standby period. */
|
|
return;
|
|
}
|
|
|
|
if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
/* Any callback means GNSS hardware/UART path is alive, even without a fix. */
|
|
gnss_activity_seen_this_cycle = true;
|
|
}
|
|
|
|
LOG_DBG("GNSS callback: fix=%d sats=%d state=%d",
|
|
data->info.fix_status, data->info.satellites_cnt, gps_current_state);
|
|
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
|
|
if (data->info.fix_status >= GNSS_FIX_STATUS_GNSS_FIX) {
|
|
/* Reject "Null Island" (0,0) fixes. Zephyr's NMEA parser splits
|
|
* data across callbacks: gnss_nmea0183_parse_gga fills altitude +
|
|
* fix_status but NOT lat/lon; gnss_nmea0183_parse_rmc fills
|
|
* lat/lon. A merged publish fires when both GGA and RMC share a
|
|
* UTC. If the chip emits GGA quality=1 while RMC is still 'V'
|
|
* (or reports null-island coords), parse_rmc's early-exit on 'V'
|
|
* leaves lat/lon at their previous value (zero at first boot, or
|
|
* stale) while altitude advances — the caller sees valid
|
|
* fix_status + altitude-only motion + (0,0) coords. Observed on
|
|
* AT6558R (RAK WisMesh Tag) during early acquisition; Air530Z
|
|
* (ThinkNode M1) doesn't desync GGA/RMC this way. (0,0) is never
|
|
* a real fix — skip so we don't poison current_pos, persist
|
|
* zeros to flash, or promote consecutive_good_fixes. */
|
|
if (data->nav_data.latitude == 0 && data->nav_data.longitude == 0) {
|
|
LOG_DBG("GPS: Ignoring (0,0) fix — GGA/RMC desync "
|
|
"(fix=%d sats=%d alt_mm=%d)",
|
|
data->info.fix_status,
|
|
data->info.satellites_cnt,
|
|
data->nav_data.altitude);
|
|
if (gps_current_state == GPS_STATE_ACQUIRING &&
|
|
consecutive_good_fixes > 0) {
|
|
consecutive_good_fixes = 0;
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
return;
|
|
}
|
|
|
|
current_pos.latitude_ndeg = data->nav_data.latitude;
|
|
current_pos.longitude_ndeg = data->nav_data.longitude;
|
|
current_pos.altitude_mm = data->nav_data.altitude;
|
|
current_pos.satellites = data->info.satellites_cnt;
|
|
current_pos.valid = true;
|
|
current_pos.timestamp_ms = k_uptime_get();
|
|
current_utc = data->utc;
|
|
|
|
/* Fix validation during acquisition */
|
|
if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
if (data->info.satellites_cnt >= GPS_MIN_SATELLITES) {
|
|
consecutive_good_fixes++;
|
|
LOG_INF("GPS: Good fix %d/%d (sats=%d) lat=%lld lon=%lld",
|
|
consecutive_good_fixes, GPS_GOOD_FIX_COUNT,
|
|
data->info.satellites_cnt,
|
|
current_pos.latitude_ndeg / 1000000,
|
|
current_pos.longitude_ndeg / 1000000);
|
|
|
|
if (consecutive_good_fixes >= GPS_GOOD_FIX_COUNT) {
|
|
bool duty = gps_duty_cycling();
|
|
bool first_ever = !first_fix_acquired;
|
|
|
|
LOG_INF("GPS: Got %d good fixes, updating location/time",
|
|
GPS_GOOD_FIX_COUNT);
|
|
|
|
/* Mark first fix acquired (enables timeout for future cycles) */
|
|
first_fix_acquired = true;
|
|
|
|
/* Mark time as synced from GPS - blocks phone time sync */
|
|
gps_time_synced = true;
|
|
last_fix_uptime_ms = k_uptime_get();
|
|
|
|
if (duty) {
|
|
/* Cancel timeout */
|
|
k_work_cancel_delayable(&gps_timeout_work);
|
|
|
|
/* Notify fix callback with validated position */
|
|
if (gps_fix_cb) {
|
|
double lat = (double)data->nav_data.latitude / 1000000000.0;
|
|
double lon = (double)data->nav_data.longitude / 1000000000.0;
|
|
k_mutex_unlock(&gps_mutex);
|
|
gps_fix_cb(lat, lon, gps_get_utc_time());
|
|
} else {
|
|
k_mutex_unlock(&gps_mutex);
|
|
}
|
|
|
|
/* Defer standby to main thread — we're on the system
|
|
* workqueue here (GNSS callback), can't call PM suspend
|
|
* (modem_chat_run_script deadlocks on same workqueue). */
|
|
atomic_or(&pending_gps_actions, GPS_ACTION_FIX_DONE);
|
|
if (gps_event_cb) {
|
|
gps_event_cb();
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Always-on: keep streaming (no standby). Throttle the fix
|
|
* callback (RTC sync + node-position update in main) to
|
|
* once per gps_acquire_timeout_ms — at 1Hz fixes it would
|
|
* otherwise fire constantly. Always fire on the first-ever
|
|
* fix so the clock syncs right away. current_pos (the
|
|
* telemetry source) is updated on every fix above. */
|
|
consecutive_good_fixes = 0;
|
|
bool promote = first_ever ||
|
|
(k_uptime_get() - last_promote_ms >=
|
|
(int64_t)gps_acquire_timeout_ms);
|
|
if (promote && gps_fix_cb) {
|
|
last_promote_ms = k_uptime_get();
|
|
double lat = (double)data->nav_data.latitude / 1000000000.0;
|
|
double lon = (double)data->nav_data.longitude / 1000000000.0;
|
|
k_mutex_unlock(&gps_mutex);
|
|
gps_fix_cb(lat, lon, gps_get_utc_time());
|
|
return;
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
return;
|
|
}
|
|
} else {
|
|
/* Reset counter on bad fix (< 4 satellites) */
|
|
if (consecutive_good_fixes > 0) {
|
|
LOG_DBG("GPS: Poor fix (sats=%d), resetting counter",
|
|
data->info.satellites_cnt);
|
|
}
|
|
consecutive_good_fixes = 0;
|
|
}
|
|
}
|
|
} else {
|
|
/* Don't clear current_pos — preserve last good fix for telemetry.
|
|
* Only reset the consecutive fix counter during acquisition. */
|
|
if (gps_current_state == GPS_STATE_ACQUIRING && consecutive_good_fixes > 0) {
|
|
LOG_DBG("GPS: No fix, resetting counter");
|
|
consecutive_good_fixes = 0;
|
|
}
|
|
|
|
/* Periodic status at INF level so user knows NMEA is flowing.
|
|
* Without this, GPS is completely silent until first fix (all
|
|
* NMEA parsing is at DBG level in the driver). */
|
|
if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
static int64_t last_status_ms;
|
|
int64_t now = k_uptime_get();
|
|
if (now - last_status_ms >= 10000) {
|
|
LOG_INF("GPS: Searching... sats=%d fix=%d",
|
|
data->info.satellites_cnt,
|
|
data->info.fix_status);
|
|
last_status_ms = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
k_mutex_unlock(&gps_mutex);
|
|
}
|
|
|
|
/* Register GNSS callback for all GNSS devices */
|
|
GNSS_DATA_CALLBACK_DEFINE(NULL, gnss_data_cb);
|
|
|
|
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
/* ========== Per-constellation satellite tally (diagnostic) ==========
|
|
* The Zephyr GSV parser fills gnss_satellite.system from the NMEA talker ID
|
|
* ($GPGSV/$GLGSV/$GAGSV/$GBGSV), so this is direct evidence of which
|
|
* constellations the module is actually tracking — the only way to confirm
|
|
* that the boot-time PMTK353 / UBX-CFG-GNSS configuration was accepted.
|
|
* A module still in its GPS-only default yields sats_gps only.
|
|
*
|
|
* Counts only tracked satellites (is_tracked), not merely visible ones. */
|
|
static uint8_t sat_count[5]; /* gps, glonass, galileo, beidou, other */
|
|
static int64_t sat_seen_ms[5]; /* uptime when each bucket was last reported */
|
|
|
|
/* A constellation absent for this long is reported as zero. Long enough to
|
|
* ride out a missed GSV cycle (they repeat at the fix rate), short enough
|
|
* that a constellation which genuinely drops out stops being claimed. */
|
|
#define SAT_TALLY_STALE_MS 30000
|
|
|
|
static void gnss_satellites_cb(const struct device *dev,
|
|
const struct gnss_satellite *satellites,
|
|
uint16_t size)
|
|
{
|
|
ARG_UNUSED(dev);
|
|
uint8_t tally[5] = { 0 };
|
|
bool seen[5] = { false };
|
|
|
|
for (uint16_t i = 0; i < size; i++) {
|
|
int idx;
|
|
|
|
switch (satellites[i].system) {
|
|
case GNSS_SYSTEM_GPS: idx = 0; break;
|
|
case GNSS_SYSTEM_GLONASS: idx = 1; break;
|
|
case GNSS_SYSTEM_GALILEO: idx = 2; break;
|
|
case GNSS_SYSTEM_BEIDOU: idx = 3; break;
|
|
default: idx = 4; break;
|
|
}
|
|
|
|
/* Mark the constellation as reported even when nothing in it is
|
|
* tracked — that is a real "zero", distinct from "not heard". */
|
|
seen[idx] = true;
|
|
if (satellites[i].is_tracked) {
|
|
tally[idx]++;
|
|
}
|
|
}
|
|
|
|
/* One GSV burst carries ONE constellation: the parser publishes each
|
|
* talker's group separately (satellites_length == number_of_svs for
|
|
* that group). So update only the buckets this burst reported —
|
|
* replacing all five wholesale wipes the constellations that arrived
|
|
* in the previous burst, which reads as G0 next to a healthy fix. */
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
for (int i = 0; i < 5; i++) {
|
|
if (seen[i]) {
|
|
sat_count[i] = tally[i];
|
|
sat_seen_ms[i] = k_uptime_get();
|
|
}
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
}
|
|
|
|
GNSS_SATELLITES_CALLBACK_DEFINE(NULL, gnss_satellites_cb);
|
|
#endif /* CONFIG_ZEPHCORE_GPS_SAT_DIAG */
|
|
|
|
/* Find and initialize GNSS device */
|
|
static const struct device *gnss_dev = NULL;
|
|
|
|
/* Multi-constellation configuration — runs ONCE at boot.
|
|
* modem_chat_run_script() blocks on a semaphore signaled from the system
|
|
* work queue. Calling it after a GPIO power cycle can deadlock because:
|
|
* 1. The L76K needs ~300ms to boot after power restore
|
|
* 2. Meanwhile the modem_chat may be processing stale UART data
|
|
* 3. The script completion callback competes with NMEA processing
|
|
*
|
|
* Safe to call at boot because the driver init already ran and the chip
|
|
* is powered and outputting NMEA. After power cycles, the L76K retains
|
|
* constellation + fix rate settings in internal flash (PCAS commands
|
|
* persist). So we only need to configure once. */
|
|
static bool gnss_configured = false;
|
|
|
|
/* ========== Configuration Diagnostics ==========
|
|
* Module configuration is sent blind — nothing in the protocol path tells us
|
|
* the module accepted it. This records what was attempted so the operator can
|
|
* read it back over the CLI on a release build. `gps_set_diag(true)` also
|
|
* clears gnss_configured, so the next GPS enable re-runs configuration and
|
|
* refreshes the record. RAM only, never persisted. */
|
|
enum gps_cfg_path {
|
|
GPS_CFG_NEVER = 0, /* configuration has not run yet */
|
|
GPS_CFG_API, /* driver implements the GNSS API (air530z, lc76g, ...) */
|
|
GPS_CFG_UART, /* passive NMEA listener — raw PMTK + UBX sent */
|
|
GPS_CFG_BLIND, /* no GNSS API and no writable UART — module defaults */
|
|
};
|
|
|
|
static struct {
|
|
uint8_t path; /* enum gps_cfg_path */
|
|
int8_t api_ret; /* gnss_set_enabled_systems() result */
|
|
int8_t rate_ret; /* gnss_set_fix_rate() result */
|
|
uint8_t cmds; /* config commands written to the UART */
|
|
uint16_t bytes; /* bytes written to the UART */
|
|
int64_t at_ms; /* uptime when configuration last ran */
|
|
} gps_cfg_diag;
|
|
|
|
|
|
/* Module identification string, captured by the GNSS driver from the reply to
|
|
* its version query (CASIC parts answer in-band as a $GPTXT sentence).
|
|
* Weak so that boards whose driver has no version query still link — an
|
|
* absent symbol and an empty string mean the same thing to the report.
|
|
*
|
|
* Its real value is not the version text: on a transport where every command
|
|
* is written blind, a captured reply is the only positive proof that the
|
|
* 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;
|
|
|
|
/* ========== Vendor-Specific Configuration Commands ==========
|
|
*
|
|
* The RAK WisBlock GPS slot accepts multiple modules (L76K, ZOE-M8Q, etc.)
|
|
* and we use gnss-nmea-generic which is a passive NMEA listener — it has no
|
|
* GNSS API for configuration.
|
|
*
|
|
* Strategy: send BOTH Quectel PMTK and u-blox UBX configuration commands.
|
|
* Each module ignores the protocol it doesn't understand.
|
|
*
|
|
* This runs once at boot. Both modules persist config to internal flash,
|
|
* so these are effectively no-ops on subsequent boots. */
|
|
|
|
#if HAS_GPS_UART
|
|
|
|
/* The UART the GNSS module is connected to. Works for any GNSS-on-UART node
|
|
* regardless of compatible string. */
|
|
static const struct device *gps_uart_dev = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss)));
|
|
|
|
/* Send raw bytes to the GPS UART using blocking poll_out.
|
|
* Safe to call even though modem_chat/modem_ubx owns the UART pipe:
|
|
* uart_poll_out writes one byte at a time through the TX register,
|
|
* and GNSS modules are receive-only (no TX contention).
|
|
*
|
|
* Gated on HAS_GPS_UART alone — writing to the module is safe on every
|
|
* UART-attached GNSS. The narrower power-control gate below applies to the
|
|
* software *sleep* commands, which are only a fallback for boards that
|
|
* cannot cut GPS power. */
|
|
static void gps_uart_send(const uint8_t *data, size_t len)
|
|
{
|
|
if (!device_is_ready(gps_uart_dev)) {
|
|
return;
|
|
}
|
|
for (size_t i = 0; i < len; i++) {
|
|
uart_poll_out(gps_uart_dev, data[i]);
|
|
}
|
|
/* Count only configuration traffic — the same helper carries the
|
|
* sleep/wake commands, which would otherwise inflate the tally. */
|
|
if (gps_cfg_counting) {
|
|
gps_cfg_diag.cmds++;
|
|
gps_cfg_diag.bytes += (uint16_t)len;
|
|
}
|
|
}
|
|
|
|
/* --- MediaTek-family (PMTK) configuration ---
|
|
*
|
|
* PMTK is MediaTek's protocol. It applies to genuine MTK parts (L76B and
|
|
* relatives). It does NOT apply to the Quectel L76K/L76KB or Air530Z, which
|
|
* are CASIC silicon and speak PCAS — their protocol specification contains no
|
|
* PMTK command at all, so these sentences are inert there. Those modules are
|
|
* driven by the air530z driver via the GNSS API instead and never reach this
|
|
* path. Kept because the RAK WisBlock GPS slot can hold an MTK part. */
|
|
|
|
/* PMTK353: Enable GPS + GLONASS + Galileo + BeiDou (no QZSS).
|
|
* Default is GPS-only. Multi-constellation dramatically improves TTFF
|
|
* and fix reliability, especially indoors or with limited sky view. */
|
|
static const char pmtk_constellations[] = "$PMTK353,1,1,1,1,0*2B\r\n";
|
|
|
|
/* PMTK869: Enable EASY (Embedded Assist System).
|
|
* Caches predicted satellite ephemeris in the GNSS module's internal flash.
|
|
* Reduces TTFF from 15-45s (cold) to 1-3s (warm) for up to 3 days after
|
|
* last fix. Setting persists in flash — resending is a harmless no-op. */
|
|
static const char pmtk_easy[] = "$PMTK869,1,1*35\r\n";
|
|
|
|
/* PMTK286: Enable AIC (Active Interference Cancellation).
|
|
* Filters out narrowband jammers (e.g. harmonics from nearby electronics,
|
|
* LoRa radio leakage). Improves sensitivity by ~2dB in noisy environments.
|
|
* Especially useful when GPS antenna is near the SX1262 + SKY66122 PA. */
|
|
static const char pmtk_aic[] = "$PMTK286,1*23\r\n";
|
|
|
|
/* --- CASIC (PCAS) configuration ---
|
|
*
|
|
* The Quectel L76K/L76KB (RAK12501) and Air530Z are CASIC silicon: they speak
|
|
* neither PMTK nor UBX, so without these sentences such a module in a
|
|
* WisBlock slot receives no configuration at all and sits on its factory
|
|
* defaults. Boards that always carry one use the air530z driver and the GNSS
|
|
* API instead; these are for the generic-NMEA boards whose GPS slot can hold
|
|
* any module.
|
|
*
|
|
* Inert on the other families, same as PMTK and UBX are here. */
|
|
|
|
/* PCAS03: NMEA sentence selection. Field order is
|
|
* GGA,GLL,GSA,GSV,RMC,VTG,ZDA,ANT,... — keep GGA + RMC (position, time) and
|
|
* add GSV only when the satellite tally needs it, to keep the 9600-baud link
|
|
* from spending its budget on sentences nobody parses. */
|
|
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
static const char pcas_sentences[] = "$PCAS03,1,0,0,1,1,0,0,0,0,0,0,0,0*1F\r\n";
|
|
#else
|
|
static const char pcas_sentences[] = "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0*1E\r\n";
|
|
#endif
|
|
|
|
/* PCAS04,7 = GPS + BeiDou + GLONASS, everything the part supports.
|
|
* (No Galileo on these modules — that is silicon, not configuration.) */
|
|
static const char pcas_constellations[] = "$PCAS04,7*1E\r\n";
|
|
|
|
/* PCAS11: navigation dynamic model. Stored IN THE MODULE and survives
|
|
* reflashing the host, so a slot module that previously lived in another
|
|
* device can arrive stuck in an automotive or airborne model that quietly
|
|
* degrades fixes on a fixed site. See CONFIG_ZEPHCORE_GPS_NAV_MODE. */
|
|
#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 0
|
|
static const char pcas_nav_mode[] = "$PCAS11,0*1D\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 1
|
|
static const char pcas_nav_mode[] = "$PCAS11,1*1C\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2
|
|
static const char pcas_nav_mode[] = "$PCAS11,2*1F\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3
|
|
static const char pcas_nav_mode[] = "$PCAS11,3*1E\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4
|
|
static const char pcas_nav_mode[] = "$PCAS11,4*19\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 5
|
|
static const char pcas_nav_mode[] = "$PCAS11,5*18\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 6
|
|
static const char pcas_nav_mode[] = "$PCAS11,6*1B\r\n";
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 7
|
|
static const char pcas_nav_mode[] = "$PCAS11,7*1A\r\n";
|
|
#endif
|
|
|
|
/* PCAS06,0: ask the module to identify itself. A CASIC part answers in-band
|
|
* with a $GPTXT sentence — the only readable reply available on this
|
|
* otherwise write-only path, and therefore the only positive proof that the
|
|
* MCU's TX line reaches the module at all. Captured by the GNSS driver and
|
|
* surfaced as `mod=` in "get gps diag". */
|
|
static const char pcas_version_query[] = "$PCAS06,0*1B\r\n";
|
|
|
|
/* u-blox equivalent of the query above. u-blox ignore $PCAS06 entirely, so
|
|
* without this a u-blox module always reports "no reply" and the TX-path
|
|
* proof — the whole point of asking — is unavailable on those boards. The
|
|
* $PUBX,04 reply carries time and clock status, not a version; what matters
|
|
* is that a reply arrives at all, which only happens if the module received
|
|
* the request. */
|
|
static const char pubx_version_query[] = "$PUBX,04*37\r\n";
|
|
|
|
/* --- u-blox NMEA output trim ($PUBX,40) ---
|
|
*
|
|
* THE LINK BUDGET IS THE CONSTRAINT, and it is easy to blow past it. At 9600
|
|
* baud only ~960 bytes/s fit. With multi-GNSS enabled a u-blox emits, per
|
|
* second: GGA + RMC + GLL + VTG, one GSA per constellation, and a GSV burst
|
|
* that grows with satellite count — roughly 1030 bytes/s at ~26 SVs. The
|
|
* stream then cannot fit in the second it is generated in, sentences are
|
|
* truncated or dropped, and the symptom is not "slow GPS" but a receiver
|
|
* that appears to have stopped: no parseable GGA, so no fix, so no position.
|
|
*
|
|
* Enabling constellations without trimming output is therefore actively
|
|
* harmful on a 9600-baud link. We already do exactly this trim for CASIC
|
|
* parts via $PCAS03; u-blox had no equivalent, which is the asymmetry this
|
|
* fixes. GLL, GSA and VTG are dropped outright — nothing in the driver parses
|
|
* them — and GSV is kept only when the satellite tally needs it.
|
|
*
|
|
* Dropping GLL/GSA/VTG takes ~1030 -> ~650 bytes/s; dropping GSV as well
|
|
* takes it to ~160. */
|
|
static const char pubx_off_gll[] = "$PUBX,40,GLL,0,0,0,0,0,0*5C\r\n";
|
|
static const char pubx_off_gsa[] = "$PUBX,40,GSA,0,0,0,0,0,0*4E\r\n";
|
|
static const char pubx_off_vtg[] = "$PUBX,40,VTG,0,0,0,0,0,0*5E\r\n";
|
|
#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
static const char pubx_off_gsv[] = "$PUBX,40,GSV,0,0,0,0,0,0*59\r\n";
|
|
#endif
|
|
|
|
/* --- u-blox ZOE-M8Q (UBX binary) configuration --- */
|
|
|
|
/* UBX-CFG-PRT: force UART1 to 9600 8N1 with BOTH UBX and NMEA enabled in and
|
|
* out. Sent first, before anything that depends on the module talking to us.
|
|
*
|
|
* This exists because the module's port configuration is persistent and not
|
|
* necessarily ours. RAK's own RAK12500 example — and any host using the
|
|
* SparkFun u-blox library — calls setUART1Output(COM_TYPE_UBX) followed by
|
|
* saveConfiguration(), which stores "UBX only, NMEA off" in the module's
|
|
* flash. A module that has ever been driven that way stays silent on an
|
|
* NMEA-only host forever after, through power cycles and reflashes, and
|
|
* presents as a completely dead receiver: no GGA, no fix, no reply to any
|
|
* query. Re-asserting the port configuration costs one frame and removes a
|
|
* failure mode that is otherwise almost impossible to diagnose from the host.
|
|
*
|
|
* Limitation: if the module was also saved at a different baud rate, it will
|
|
* not parse this frame either. Recovering from that needs a baud scan, which
|
|
* the devicetree's fixed current-speed does not currently allow. */
|
|
static const uint8_t ubx_cfg_prt_uart1[] = {
|
|
0xB5, 0x62, 0x06, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x08,
|
|
0x00, 0x00, 0x80, 0x25, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x8E, 0x95
|
|
};
|
|
|
|
/* UBX-CFG-GNSS: Enable GPS + Galileo + GLONASS (+ QZSS) on u-blox M8.
|
|
*
|
|
* Config block layout is gnssId, resTrkCh, maxTrkCh, reserved0, flags[4] —
|
|
* EIGHT bytes, and the payload length must be exactly 4 + 8*numConfigBlocks
|
|
* or the receiver rejects the whole message. The previous version of this
|
|
* frame omitted reserved0, giving 7-byte blocks and a 39-byte payload where
|
|
* numConfigBlocks=5 demanded 44, so no u-blox module ever accepted it.
|
|
*
|
|
* Only THREE major GNSS (GPS/Galileo/GLONASS/BeiDou) can run concurrently on
|
|
* M8, so BeiDou is explicitly disabled rather than left alone: if the module
|
|
* came up with BeiDou on, enabling three others would make four and the
|
|
* message would be refused.
|
|
*
|
|
* QZSS is enabled even though it is Japan-regional and costs ~3 channels —
|
|
* u-blox require GPS and QZSS to be both enabled or both disabled (they
|
|
* share L1 C/A), and a mismatch is grounds for rejection.
|
|
*
|
|
* SBAS stays off: it needs 30-60 s to download corrections, useless for our
|
|
* quick-fix-then-sleep pattern (companions 30 s, repeaters 5 min).
|
|
*
|
|
* numTrkChHw = 0 and numTrkChUse = 0xFF (read-only / "use max available").
|
|
*
|
|
* resTrkCh MUST be 0 on the disabled blocks. Reserving tracking channels for
|
|
* a system whose enable bit is clear is self-contradictory, and the receiver
|
|
* validates CFG-GNSS atomically — one bad block rejects all six. An earlier
|
|
* revision left SBAS at 1 and BeiDou at 8 and the whole frame was refused
|
|
* (observed on hardware: RAK12500/ZOE-M8Q stayed GPS-only, sys=G8/R0/E0/B0).
|
|
*
|
|
* NOTE — this is an M8 frame. u-blox 7 parts (MAX-7Q on the RAK1910) have no
|
|
* Galileo or BeiDou and use a different sigCfgMask, so they will refuse it
|
|
* and stay GPS-only. Sending an M7 frame as well is NOT safe blind: its
|
|
* sigCfgMask of 0 would be a signal-disabling value on M8. */
|
|
static const uint8_t ubx_cfg_gnss[] = {
|
|
0xB5, 0x62, 0x06, 0x3E, 0x34, 0x00, 0x00, 0x00, 0xFF, 0x06, 0x00, 0x08,
|
|
0x10, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00,
|
|
0x01, 0x01, 0x02, 0x04, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x03, 0x00,
|
|
0x10, 0x00, 0x00, 0x00, 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x01, 0x00,
|
|
0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x01, 0x00, 0x01, 0x01, 0xEE, 0x64
|
|
};
|
|
|
|
/* UBX-CFG-NMEA: switch the NMEA output to version 4.10.
|
|
*
|
|
* Without this, Galileo and BeiDou satellites cannot be reported at all: the
|
|
* $GAGSV and $GBGSV talker IDs only exist from NMEA 4.10, and M8 firmware
|
|
* defaults lower. So a fully successful CFG-GNSS would still show zero
|
|
* Galileo in "get gps diag" — a reporting limit masquerading as a config
|
|
* failure. Mirrors Meshtastic's "enable NMEA 4.10" step (src/gps/ubx.h).
|
|
*
|
|
* gsvTalkerId = 0 (use the GNSS-specific talker per constellation) is what
|
|
* makes the per-constellation tally work — do not set it to 1, which forces
|
|
* every GSV onto the main talker and would collapse the tally into GPS. */
|
|
static const uint8_t ubx_cfg_nmea_410[] = {
|
|
0xB5, 0x62, 0x06, 0x17, 0x14, 0x00, 0x00, 0x41, 0x00, 0x02, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x75, 0x57
|
|
};
|
|
|
|
/* UBX-CFG-NAV5: Set 5° minimum satellite elevation.
|
|
* Ignore satellites below 5° elevation — they have more atmospheric
|
|
* noise and multipath, degrading fix quality. The default 0° lets in
|
|
* everything including horizon-level junk.
|
|
* Dynamic model left at factory default (Portable) — works for fixed
|
|
* repeaters, walking companions, and vehicles alike.
|
|
* apply mask 0x0002 = minEl(bit1) only
|
|
*
|
|
* The trailing checksum was CK_B=0x37 (should be 0xE7) — a one-byte typo
|
|
* that made every receiver drop this frame silently, with no NAK. */
|
|
static const uint8_t ubx_cfg_nav5_minelev[] = {
|
|
0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0xE7
|
|
};
|
|
|
|
/* UBX-CFG-NAVX5: Enable AssistNow Autonomous (AOP).
|
|
* u-blox equivalent of Quectel EASY — the receiver autonomously predicts
|
|
* satellite orbits from previously downloaded ephemeris data. Predictions
|
|
* stay valid for 3-6 days, reducing TTFF from 26-30s (cold) to 2-5s.
|
|
* No server connection needed — runs entirely on-chip.
|
|
* mask1 bit 14 = aop, aopCfg bit 0 = enable. */
|
|
static const uint8_t ubx_cfg_navx5_aop[] = {
|
|
0xB5, 0x62, 0x06, 0x23, 0x28, 0x00, 0x04, 0x00, 0x00, 0x40, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x96, 0x66
|
|
};
|
|
|
|
/* UBX-CFG-CFG: Save all configuration to BBR + Flash + EEPROM.
|
|
* Persists constellation, nav model, SBAS settings across power cycles
|
|
* and backup mode. Without this, ZOE-M8Q reverts to factory defaults
|
|
* after a full power loss (though BBR survives backup mode). */
|
|
static const uint8_t ubx_cfg_save[] = {
|
|
0xB5, 0x62, 0x06, 0x09, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x31, 0xBF
|
|
};
|
|
|
|
/* Settle time after a command that makes the receiver restart its navigation
|
|
* engine — constellation changes on every family do this. A command issued
|
|
* into a restarting engine is simply lost, and because everything here is
|
|
* written blind the loss is silent. 20-50 ms was optimistic. */
|
|
#define GPS_CFG_SETTLE_MS 120
|
|
#define GPS_CFG_RESTART_MS 500
|
|
|
|
/* Send an NMEA command string (including \r\n). */
|
|
static void gps_send_nmea(const char *cmd, uint32_t settle_ms = GPS_CFG_SETTLE_MS)
|
|
{
|
|
gps_uart_send((const uint8_t *)cmd, strlen(cmd));
|
|
k_msleep(settle_ms);
|
|
}
|
|
|
|
/* Send a UBX binary frame. */
|
|
static void gps_send_ubx(const uint8_t *frame, size_t len,
|
|
uint32_t settle_ms = GPS_CFG_SETTLE_MS)
|
|
{
|
|
gps_uart_send(frame, len);
|
|
k_msleep(settle_ms);
|
|
}
|
|
|
|
/* u-blox dynamic model for CFG-NAV5, translated from the CASIC-numbered
|
|
* CONFIG_ZEPHCORE_GPS_NAV_MODE so one setting drives both families.
|
|
* u-blox: 0 portable, 2 stationary, 3 pedestrian, 4 automotive, 5 sea,
|
|
* 6-8 airborne. */
|
|
#if CONFIG_ZEPHCORE_GPS_NAV_MODE == 1
|
|
#define UBX_DYNMODEL 2
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 2
|
|
#define UBX_DYNMODEL 3
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 3
|
|
#define UBX_DYNMODEL 4
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 4
|
|
#define UBX_DYNMODEL 5
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE >= 5
|
|
#define UBX_DYNMODEL 6
|
|
#elif CONFIG_ZEPHCORE_GPS_NAV_MODE == 0
|
|
#define UBX_DYNMODEL 0
|
|
#endif
|
|
|
|
/* Send CFG-NAV5 with the dynamic model patched in and the checksum redone.
|
|
* A stationary model on a fixed node suppresses position wander and lets the
|
|
* receiver apply much tighter velocity assumptions; leaving a roof-mounted
|
|
* repeater on the factory Portable model throws that away. Patched at runtime
|
|
* rather than as eight hard-coded frames — one place to get the checksum
|
|
* right instead of eight. */
|
|
#ifdef UBX_DYNMODEL
|
|
static void gps_send_ubx_nav5(void)
|
|
{
|
|
uint8_t f[sizeof(ubx_cfg_nav5_minelev)];
|
|
|
|
memcpy(f, ubx_cfg_nav5_minelev, sizeof(f));
|
|
f[6] = 0x03; /* mask: dyn (bit0) + minEl (bit1) */
|
|
f[8] = UBX_DYNMODEL; /* dynModel */
|
|
|
|
uint8_t ck_a = 0, ck_b = 0;
|
|
for (size_t i = 2; i < sizeof(f) - 2; i++) {
|
|
ck_a = (uint8_t)(ck_a + f[i]);
|
|
ck_b = (uint8_t)(ck_b + ck_a);
|
|
}
|
|
f[sizeof(f) - 2] = ck_a;
|
|
f[sizeof(f) - 1] = ck_b;
|
|
|
|
gps_send_ubx(f, sizeof(f));
|
|
}
|
|
#endif
|
|
|
|
/* Configure the GPS module with optimal settings for a mesh repeater.
|
|
* Sends both PMTK (Quectel) and UBX (u-blox) commands — the module that
|
|
* isn't present ignores bytes it doesn't understand. */
|
|
static void gps_configure_via_uart(void)
|
|
{
|
|
LOG_INF("GPS: Configuring via UART (PMTK + UBX dual-protocol)");
|
|
|
|
gps_cfg_diag.cmds = 0;
|
|
gps_cfg_diag.bytes = 0;
|
|
gps_cfg_counting = true;
|
|
|
|
/* --- MediaTek-family (PMTK) --- */
|
|
gps_send_nmea(pmtk_constellations, GPS_CFG_RESTART_MS);
|
|
gps_send_nmea(pmtk_easy);
|
|
gps_send_nmea(pmtk_aic);
|
|
LOG_INF("GPS: PMTK config sent (constellations, EASY, AIC)");
|
|
|
|
/* --- CASIC (PCAS) ---
|
|
* Version query last, so its $GPTXT reply is not stepped on by a
|
|
* constellation restart. */
|
|
gps_send_nmea(pcas_sentences);
|
|
gps_send_nmea(pcas_constellations, GPS_CFG_RESTART_MS);
|
|
#if CONFIG_ZEPHCORE_GPS_NAV_MODE >= 0
|
|
gps_send_nmea(pcas_nav_mode);
|
|
#endif
|
|
gps_send_nmea(pcas_version_query);
|
|
LOG_INF("GPS: PCAS config sent (sentences, constellations, nav mode, version query)");
|
|
|
|
/* --- u-blox ZOE-M8Q (UBX) ---
|
|
* NMEA 4.10 goes first: it governs whether the constellations enabled
|
|
* by the next frame can be *reported* at all. CFG-GNSS restarts the
|
|
* navigation engine, so it gets the long settle before the frames that
|
|
* follow it — at 50 ms they were being issued into a restarting
|
|
* receiver. CFG-CFG stays last so it only persists whatever was
|
|
* actually accepted; note that makes a rejected configuration sticky
|
|
* too, which is why a frame bug here survives power cycles. */
|
|
/* Make sure NMEA output is even switched on before anything else — a
|
|
* module saved as UBX-only by a previous host is otherwise mute. */
|
|
gps_send_ubx(ubx_cfg_prt_uart1, sizeof(ubx_cfg_prt_uart1), GPS_CFG_RESTART_MS);
|
|
|
|
/* Trim the NMEA stream BEFORE enabling more constellations — the extra
|
|
* GSV traffic must have somewhere to fit. */
|
|
gps_send_nmea(pubx_off_gll);
|
|
gps_send_nmea(pubx_off_gsa);
|
|
gps_send_nmea(pubx_off_vtg);
|
|
#ifndef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
gps_send_nmea(pubx_off_gsv);
|
|
#endif
|
|
gps_send_ubx(ubx_cfg_nmea_410, sizeof(ubx_cfg_nmea_410));
|
|
gps_send_ubx(ubx_cfg_gnss, sizeof(ubx_cfg_gnss), GPS_CFG_RESTART_MS);
|
|
#ifdef UBX_DYNMODEL
|
|
gps_send_ubx_nav5();
|
|
#else
|
|
gps_send_ubx(ubx_cfg_nav5_minelev, sizeof(ubx_cfg_nav5_minelev));
|
|
#endif
|
|
gps_send_ubx(ubx_cfg_navx5_aop, sizeof(ubx_cfg_navx5_aop));
|
|
gps_send_ubx(ubx_cfg_save, sizeof(ubx_cfg_save));
|
|
/* Ask a u-blox to say something back — the TX-path proof for this
|
|
* family, sent last so the reply is not stepped on by a restart. */
|
|
gps_send_nmea(pubx_version_query);
|
|
LOG_INF("GPS: UBX config sent (NMEA 4.10, multi-GNSS, 5° min elev, AOP, saved)");
|
|
|
|
gps_cfg_counting = false;
|
|
}
|
|
#endif /* HAS_GPS_UART */
|
|
|
|
/* Re-run module configuration on a GPS enable, but only when diagnostics are
|
|
* armed — this is a deliberate, operator-triggered action, not a normal path.
|
|
*
|
|
* Only the raw-UART path is re-runnable. gnss_configure()'s API path goes
|
|
* through modem_chat_run_script(), which is safe at boot only: after a GPIO
|
|
* power restore the chip needs ~300 ms and calling it here deadlocks the main
|
|
* thread. So on API-driver boards this records nothing new and "get gps diag"
|
|
* keeps reporting the boot-time result, which is the honest answer. */
|
|
static void gps_diag_maybe_reconfigure(void)
|
|
{
|
|
if (!gps_diag_on || gnss_configured || gnss_dev == NULL) {
|
|
return;
|
|
}
|
|
#if HAS_GPS_UART
|
|
if (gps_cfg_diag.path == GPS_CFG_UART || gps_cfg_diag.path == GPS_CFG_NEVER) {
|
|
/* The module has just been powered; give it time to boot before
|
|
* clocking configuration at it (same ~300 ms the modem needs). */
|
|
k_msleep(300);
|
|
gps_cfg_diag.path = GPS_CFG_UART;
|
|
gps_cfg_diag.at_ms = k_uptime_get();
|
|
gps_configure_via_uart();
|
|
}
|
|
#endif
|
|
gnss_configured = true;
|
|
}
|
|
|
|
static void gnss_configure(void)
|
|
{
|
|
if (gnss_configured || gnss_dev == NULL) {
|
|
return;
|
|
}
|
|
|
|
/* Enable all available constellation systems for faster TTFF.
|
|
* Try GPS+GLONASS+Galileo+BeiDou first (AG3335 supports all).
|
|
* Fall back to GPS+GLONASS+BeiDou if Galileo not supported (L76KB). */
|
|
gnss_systems_t systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS |
|
|
GNSS_SYSTEM_GALILEO | GNSS_SYSTEM_BEIDOU;
|
|
int ret = gnss_set_enabled_systems(gnss_dev, systems);
|
|
if (ret == -EINVAL) {
|
|
/* Some systems not supported — try without Galileo */
|
|
systems = GNSS_SYSTEM_GPS | GNSS_SYSTEM_GLONASS | GNSS_SYSTEM_BEIDOU;
|
|
ret = gnss_set_enabled_systems(gnss_dev, systems);
|
|
}
|
|
gps_cfg_diag.api_ret = (int8_t)ret;
|
|
gps_cfg_diag.at_ms = k_uptime_get();
|
|
|
|
if (ret == 0) {
|
|
LOG_INF("GPS: Multi-constellation enabled via GNSS API");
|
|
gps_cfg_diag.path = GPS_CFG_API;
|
|
} else if (ret == -ENOSYS || ret == -ENOTSUP) {
|
|
#if HAS_GPS_UART
|
|
/* gnss-nmea-generic is a passive listener — no GNSS API.
|
|
* Configure everything via direct UART commands instead. */
|
|
gps_cfg_diag.path = GPS_CFG_UART;
|
|
gps_configure_via_uart();
|
|
#else
|
|
LOG_INF("GPS: No GNSS API and no UART access — using module defaults");
|
|
gps_cfg_diag.path = GPS_CFG_BLIND;
|
|
#endif
|
|
} else {
|
|
LOG_WRN("GPS: Failed to set constellations: %d", ret);
|
|
/* Will retry on next power-on cycle */
|
|
return;
|
|
}
|
|
|
|
/* Set 1Hz fix rate (explicit, don't rely on chip defaults) */
|
|
ret = gnss_set_fix_rate(gnss_dev, 1000);
|
|
gps_cfg_diag.rate_ret = (int8_t)ret;
|
|
if (ret == 0) {
|
|
LOG_INF("GPS: Fix rate set to 1Hz");
|
|
} else if (ret != -ENOSYS && ret != -ENOTSUP) {
|
|
LOG_WRN("GPS: Failed to set fix rate: %d", ret);
|
|
}
|
|
|
|
gnss_configured = true;
|
|
}
|
|
|
|
#endif /* HAS_GNSS - GPS power GPIO section is unconditional (needed for shutdown) */
|
|
|
|
/* ========== GPS Power GPIO Control ==========
|
|
* These are unconditional (not gated by HAS_GNSS) because
|
|
* gps_power_off_for_shutdown() must be available for System OFF
|
|
* even on boards without a GNSS driver.
|
|
*
|
|
* IMPORTANT: Do NOT touch GPIO during init! The GNSS driver needs the GPS
|
|
* to be powered and outputting NMEA for the modem pipe to work.
|
|
* We only configure GPIO lazily on first power-off request.
|
|
*
|
|
* Board-specific pins (defined in board overlays as gps-enable alias):
|
|
* - T1000-E: P1.11 (GPS_EN), P0.8 (GPS_VRTC_EN), P1.15 (GPS_RESET), P1.12 (GPS_SLEEP_INT)
|
|
* - Wio Tracker L1: P1.09 (GPS power, shared with luatos,air530z on-off-gpios)
|
|
*/
|
|
#if HAS_GPS_POWER_CONTROL
|
|
static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios);
|
|
#endif
|
|
|
|
/* GPS powered by a PMU regulator rail instead of a discrete enable GPIO (e.g.
|
|
* LilyGo T-Beam: GPS is on the AXP2101 ALDO3 rail). Selected via the chosen
|
|
* `zephcore,gps-power` node pointing at the regulator. Acts as a master power
|
|
* switch driven by enable/disable; the duty-cycle standby/wake uses software
|
|
* sleep/wake (UART) and leaves the rail up, so the regulator is only toggled on
|
|
* the (unguarded) enable/disable/boot paths — never per duty cycle. */
|
|
#if HAS_GPS_POWER_REGULATOR
|
|
static const struct device *const gps_power_reg =
|
|
DEVICE_DT_GET(DT_CHOSEN(zephcore_gps_power));
|
|
/* Tracks our intended rail state so enable/disable stay balanced (idempotent).
|
|
* Starts true: the rail is `regulator-boot-on`, so it is already up at boot. */
|
|
static bool gps_reg_enabled = true;
|
|
#endif
|
|
|
|
/* AXP2101 backup (button-battery) charger — feeds the GPS receiver's V_BCKP
|
|
* domain so ephemeris/RTC survive main-rail (ALDO3) power cuts, giving a
|
|
* warm/hot re-fix instead of a cold start each duty cycle. The Zephyr regulator
|
|
* driver doesn't expose VBACKUP, so enable it with raw I2C at boot (mirrors
|
|
* Arduino enablePowerOutput(XPOWERS_VBACKUP) + setPowerChannelVoltage 3.3V).
|
|
* Selected via chosen `zephcore,gps-backup-pmu` pointing at the AXP2101 node. */
|
|
#if DT_NODE_EXISTS(DT_CHOSEN(zephcore_gps_backup_pmu))
|
|
#define AXP2101_REG_CHG_GAUGE_WDT_CTRL 0x18U /* bit 2 = button-battery charge enable */
|
|
#define AXP2101_BTN_CHARGE_ENABLE BIT(2)
|
|
#define AXP2101_REG_BTN_BAT_CHG_VOL_SET 0x6AU /* low 3 bits: (mV - 2600) / 100 */
|
|
#define AXP2101_BTN_VOL_3V3 0x07U /* (3300 - 2600) / 100 */
|
|
static int gps_backup_charger_init(void)
|
|
{
|
|
static const struct i2c_dt_spec axp = I2C_DT_SPEC_GET(DT_CHOSEN(zephcore_gps_backup_pmu));
|
|
|
|
if (!device_is_ready(axp.bus)) {
|
|
LOG_WRN("GPS backup: AXP2101 I2C bus not ready");
|
|
return 0;
|
|
}
|
|
/* Set the backup-charge target to 3.3V (low 3 bits), then enable the
|
|
* charger. Read-modify-write so the fuel-gauge enable (bit 3 of 0x18) and
|
|
* the other 0x6A bits are preserved. */
|
|
i2c_reg_update_byte_dt(&axp, AXP2101_REG_BTN_BAT_CHG_VOL_SET, 0x07U, AXP2101_BTN_VOL_3V3);
|
|
i2c_reg_update_byte_dt(&axp, AXP2101_REG_CHG_GAUGE_WDT_CTRL,
|
|
AXP2101_BTN_CHARGE_ENABLE, AXP2101_BTN_CHARGE_ENABLE);
|
|
LOG_INF("GPS backup: AXP2101 VBACKUP charger enabled (3.3V)");
|
|
return 0;
|
|
}
|
|
/* After the MFD/I2C is up (POST_KERNEL ~86); APPLICATION is safely later. */
|
|
SYS_INIT(gps_backup_charger_init, APPLICATION, 50);
|
|
#endif
|
|
|
|
/* T1000-E specific GPS control pins */
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_vrtc_enable))
|
|
static const struct gpio_dt_spec gps_vrtc_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_vrtc_enable), gpios);
|
|
#define HAS_GPS_VRTC 1
|
|
#else
|
|
#define HAS_GPS_VRTC 0
|
|
#endif
|
|
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_reset))
|
|
static const struct gpio_dt_spec gps_reset_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_reset), gpios);
|
|
#define HAS_GPS_RESET 1
|
|
#else
|
|
#define HAS_GPS_RESET 0
|
|
#endif
|
|
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_sleep_int))
|
|
static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_sleep_int), gpios);
|
|
#define HAS_GPS_SLEEP 1
|
|
#else
|
|
#define HAS_GPS_SLEEP 0
|
|
#endif
|
|
|
|
/* GPS RTC interrupt pin — held de-asserted during normal operation */
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_rtc_int))
|
|
static const struct gpio_dt_spec gps_rtcint_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_rtc_int), gpios);
|
|
#define HAS_GPS_RTCINT 1
|
|
#else
|
|
#define HAS_GPS_RTCINT 0
|
|
#endif
|
|
|
|
/* GPS RESETB (active-LOW reset) — must be INPUT_PULLUP for normal operation.
|
|
* Without the pull-up, this pin floats LOW and holds the AG3335 in permanent
|
|
* reset, preventing any UART output. */
|
|
#if DT_NODE_EXISTS(DT_ALIAS(gps_resetb))
|
|
static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_resetb), gpios);
|
|
#define HAS_GPS_RESETB 1
|
|
#else
|
|
#define HAS_GPS_RESETB 0
|
|
#endif
|
|
|
|
/* T1000-E has extra GPS control pins that require a specific init sequence */
|
|
#define HAS_T1000_GPS_CONTROL (HAS_GPS_VRTC || HAS_GPS_RESET || HAS_GPS_SLEEP)
|
|
|
|
#if HAS_GPS_POWER_CONTROL
|
|
static bool gps_gpio_configured = false;
|
|
#endif
|
|
|
|
/* GPS power control with warm standby support.
|
|
* @param on true = power on, false = power off
|
|
* @param keep_vrtc When powering off: true = keep VRTC alive (warm standby,
|
|
* preserves ephemeris/almanac/RTC for fast re-acquisition),
|
|
* false = full power-off (cold start on next wake).
|
|
* Only relevant on T1000-E (HAS_GPS_VRTC); ignored on other boards. */
|
|
static void gps_power_control(bool on, bool keep_vrtc = false)
|
|
{
|
|
#if HAS_GPS_POWER_REGULATOR
|
|
/* Master power rail (PMU regulator). Idempotent enable/disable so the
|
|
* refcount stays balanced regardless of how often this is called. */
|
|
if (on != gps_reg_enabled && device_is_ready(gps_power_reg)) {
|
|
int ret = on ? regulator_enable(gps_power_reg)
|
|
: regulator_disable(gps_power_reg);
|
|
if (ret == 0) {
|
|
gps_reg_enabled = on;
|
|
LOG_INF("GPS power %s (regulator)", on ? "ON" : "OFF");
|
|
} else {
|
|
LOG_WRN("GPS regulator %s failed: %d", on ? "enable" : "disable", ret);
|
|
}
|
|
}
|
|
#endif
|
|
#if HAS_GPS_POWER_CONTROL
|
|
/* Direct GPIO power control — works on all boards.
|
|
* We toggle the GPS power pin ourselves rather than using driver PM
|
|
* (driver PM can hang on modem_pipe_close / modem_chat_run_script).
|
|
* The GNSS driver's modem pipe stays open.
|
|
*
|
|
* T1000-E (HAS_GPS_VRTC): Use warm standby (keep VRTC) for app toggle
|
|
* so UART/chip state is preserved. Matches Arduino sleep_gps().
|
|
* Simple boards (Wio etc.): Full power off/on via GPS_EN. */
|
|
if (on) {
|
|
#if HAS_T1000_GPS_CONTROL
|
|
/* T1000-E power-on sequence (from Arduino target.cpp start_gps())
|
|
* Must follow this exact order with delays. Levels are ASSERTED /
|
|
* DE-ASSERTED, not physical: Arduino states them as HIGH/LOW because
|
|
* its pins are all active-high, while this block is also reached by
|
|
* boards whose lines are active-low (see the gate below).
|
|
* 1. GPS_EN asserted, delay 10ms
|
|
* 2. GPS_VRTC_EN asserted, delay 10ms (critical - RTC power)
|
|
* 3. GPS_RESET asserted, delay 10ms, then released
|
|
* 4. GPS_SLEEP_INT asserted
|
|
*
|
|
* Despite the name this is not a T1000-E-only path:
|
|
* HAS_T1000_GPS_CONTROL is (HAS_GPS_VRTC || HAS_GPS_RESET ||
|
|
* HAS_GPS_SLEEP), so a bare gps-reset alias is enough to route a
|
|
* board here. heltec_wifi_lora32_v4, _v43 and thinknode_m9 all arrive
|
|
* this way, and all three declare gps-enable active-low.
|
|
*/
|
|
if (gpio_is_ready_dt(&gps_enable_gpio)) {
|
|
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE);
|
|
}
|
|
k_msleep(10);
|
|
|
|
#if HAS_GPS_VRTC
|
|
if (gpio_is_ready_dt(&gps_vrtc_gpio)) {
|
|
gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_ACTIVE);
|
|
}
|
|
k_msleep(10);
|
|
#endif
|
|
|
|
#if HAS_GPS_RESET
|
|
if (gpio_is_ready_dt(&gps_reset_gpio)) {
|
|
gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE);
|
|
k_msleep(10);
|
|
gpio_pin_set_dt(&gps_reset_gpio, 0); /* Release reset (logical) */
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_SLEEP
|
|
if (gpio_is_ready_dt(&gps_sleep_gpio)) {
|
|
gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_ACTIVE);
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_RTCINT
|
|
/* GPS_RTC_INT (P0.15) — held de-asserted during normal operation */
|
|
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
|
|
gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_RESETB
|
|
/* GPS_RESETB (P1.14) — active-LOW reset, must be pulled HIGH.
|
|
* INPUT_PULLUP de-asserts reset so the AG3335 can boot.
|
|
* Without this the pin floats LOW → chip stuck in reset → no UART. */
|
|
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
|
|
gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_INPUT | GPIO_PULL_UP);
|
|
}
|
|
#endif
|
|
gps_gpio_configured = true;
|
|
LOG_INF("GPS power ON (T1000-E sequence)");
|
|
#else
|
|
/* Simple boards - just GPS_EN */
|
|
if (!gps_gpio_configured) {
|
|
if (gpio_is_ready_dt(&gps_enable_gpio)) {
|
|
/* ACTIVE, not HIGH: gpio_pin_set_dt() below is
|
|
* logical, so a physical init flag here would assert
|
|
* the opposite level on an active-low gps-enable. */
|
|
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_ACTIVE);
|
|
gps_gpio_configured = true;
|
|
LOG_INF("GPS power GPIO configured, set ACTIVE");
|
|
} else {
|
|
LOG_WRN("GPS power GPIO not ready");
|
|
return;
|
|
}
|
|
} else {
|
|
gpio_pin_set_dt(&gps_enable_gpio, 1);
|
|
LOG_INF("GPS power ON");
|
|
}
|
|
#endif
|
|
} else {
|
|
/* Power off sequence */
|
|
#if HAS_GPS_RESET
|
|
/* Hold GPS in reset during power-off — matches Arduino sleep_gps()/stop_gps().
|
|
* Ensures chip sees RESET asserted when GPS_EN is asserted on next
|
|
* power-on, preventing uncontrolled startup before the reset pulse.
|
|
* Configure-on-first-use (mirrors the GPS_EN pin below): on a
|
|
* boot-with-GPS-off the power-on path never ran, so the pin isn't an
|
|
* output yet — gpio_pin_set_dt() alone would leave it floating instead
|
|
* of asserting reset. GPIO_OUTPUT_ACTIVE drives the active (asserted)
|
|
* level directly. */
|
|
if (gpio_is_ready_dt(&gps_reset_gpio)) {
|
|
if (!gps_gpio_configured) {
|
|
gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE);
|
|
} else {
|
|
gpio_pin_set_dt(&gps_reset_gpio, 1);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_VRTC
|
|
if (!keep_vrtc) {
|
|
/* Full power-off: VRTC off too (cold start on next wake) */
|
|
if (gpio_is_ready_dt(&gps_vrtc_gpio)) {
|
|
if (!gps_gpio_configured) {
|
|
gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE);
|
|
} else {
|
|
gpio_pin_set_dt(&gps_vrtc_gpio, 0);
|
|
}
|
|
}
|
|
}
|
|
/* else: warm standby — VRTC stays asserted, preserving
|
|
* ephemeris/almanac/RTC for fast re-acquisition (~1-2 µA) */
|
|
#endif
|
|
if (gpio_is_ready_dt(&gps_enable_gpio)) {
|
|
if (!gps_gpio_configured) {
|
|
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE);
|
|
gps_gpio_configured = true;
|
|
} else {
|
|
gpio_pin_set_dt(&gps_enable_gpio, 0);
|
|
}
|
|
}
|
|
|
|
#if HAS_GPS_RESETB
|
|
/* Assert RESETB when GPS is off (Arduino sleep_gps/stop_gps) */
|
|
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
|
|
gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_RTCINT
|
|
/* GPS_RTC_INT stays de-asserted during sleep/off (same as normal operation) */
|
|
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
|
|
gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
|
|
#if HAS_GPS_VRTC
|
|
LOG_INF("GPS power OFF (%s)", keep_vrtc ?
|
|
"standby — VRTC retained" : "full");
|
|
#else
|
|
LOG_INF("GPS power OFF");
|
|
#endif
|
|
}
|
|
#else
|
|
ARG_UNUSED(keep_vrtc);
|
|
#endif
|
|
}
|
|
|
|
/* Put every GPS control line this board declares into its de-asserted state
|
|
* for System OFF -- the power enable, and where present VRTC, reset, sleep,
|
|
* rtcint and resetb -- not the power enable alone.
|
|
* Uses gpio_pin_configure_dt() so pins are properly set even if
|
|
* gps_power_control() was never called (GPIO not yet configured). */
|
|
void gps_power_off_for_shutdown(void)
|
|
{
|
|
#if HAS_GPS_POWER_REGULATOR
|
|
if (gps_reg_enabled && device_is_ready(gps_power_reg)) {
|
|
regulator_disable(gps_power_reg);
|
|
gps_reg_enabled = false;
|
|
}
|
|
#endif
|
|
#if HAS_GPS_POWER_CONTROL
|
|
/* INACTIVE: physical LOW leaves an active-low enable asserted, i.e. the
|
|
* module still powered across System OFF. */
|
|
if (gpio_is_ready_dt(&gps_enable_gpio)) {
|
|
gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
#if HAS_GPS_VRTC
|
|
if (gpio_is_ready_dt(&gps_vrtc_gpio)) {
|
|
gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
#if HAS_GPS_RESET
|
|
if (gpio_is_ready_dt(&gps_reset_gpio)) {
|
|
gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
#if HAS_GPS_SLEEP
|
|
if (gpio_is_ready_dt(&gps_sleep_gpio)) {
|
|
gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
#if HAS_GPS_RTCINT
|
|
if (gpio_is_ready_dt(&gps_rtcint_gpio)) {
|
|
gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
#if HAS_GPS_RESETB
|
|
if (gpio_is_ready_dt(&gps_resetb_gpio)) {
|
|
gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_INACTIVE);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if HAS_GNSS /* Resume GNSS-specific code */
|
|
|
|
/* ========== Software Sleep/Wake (no GPIO required) ==========
|
|
*
|
|
* On boards without dedicated GPS power control (e.g. RAK3401 where the
|
|
* 3V3_S rail is shared with the LoRa FEM), we send vendor-specific UART
|
|
* commands to put the GPS module into low-power mode.
|
|
*
|
|
* Strategy: send BOTH MediaTek and u-blox sleep commands — the module that
|
|
* isn't present simply ignores the bytes it doesn't understand.
|
|
*
|
|
* - MediaTek (e.g. L76B): $PMTK161,0*28\r\n → standby (~1mA), wake on UART
|
|
* - u-blox ZOE-M8Q (RAK12500): UBX-RXM-PMREQ → backup (~7µA), wake on UART
|
|
*
|
|
* Neither reaches a CASIC part (L76K/L76KB/Air530Z): those ignore PMTK, UBX
|
|
* and PCAS12 sleep commands alike — verified on hardware, which is why the
|
|
* boards carrying them duty-cycle with a power GPIO instead. Note also that
|
|
* the RAK1910 is a u-blox MAX-7Q, not an L76K, despite older comments here.
|
|
*
|
|
* Wake: any byte on UART wakes both modules from their low-power modes.
|
|
* After wake, the module resumes outputting NMEA autonomously.
|
|
*/
|
|
|
|
/* HAS_GPS_UART, gps_uart_dev and gps_uart_send are defined near the top of
|
|
* this file (see "GPS Feature Detection") — they are needed by the boot-time
|
|
* module configuration, which runs long before this section. */
|
|
|
|
/* ========== GNSS UART Suspend/Resume (device PM) ==========
|
|
* nRF UARTE only. An armed UARTE RX holds HFCLK (~0.5-1 mA on nRF52840)
|
|
* even when the GPS module is powered off or silent, so standby/off
|
|
* suspends the UART device and every wake resumes it first.
|
|
*
|
|
* Verified symmetric in uart_nrfx_uarte.c under the still-open modem pipe:
|
|
* suspend saves the RX-interrupt state, STOPRXes, disables the peripheral
|
|
* and applies the sleep pinctrl; resume restores all of it. Other UART
|
|
* drivers (legacy nordic,nrf-uart on RAK4631, ESP32) are deliberately not
|
|
* gated in — their suspend/resume round-trip is unverified and the HFCLK
|
|
* cost is UARTE-specific.
|
|
*
|
|
* Every GPS UART node must carry a sleep pinctrl state (all boards do):
|
|
* without one, suspend fails *after* disabling RX while the PM state stays
|
|
* ACTIVE, so the next resume no-ops with -EALREADY — a dead GPS.
|
|
*
|
|
* REQUIRES GPS POWER CONTROL — do not relax this gate.
|
|
* uarte_pm_suspend() busy-waits for RXTO with no timeout after triggering
|
|
* STOPRX (uart_nrfx_uarte.c, the only unbounded wait in the path). In
|
|
* interrupt-driven mode RX runs on a 1-byte buffer with no ENDRX_STARTRX
|
|
* short, so the receiver stops after every byte until the ISR re-arms it —
|
|
* and suspend disables the ENDRX interrupt *before* STOPRX, removing the
|
|
* re-arm. Land in that window with bytes still arriving and STOPRX hits an
|
|
* already-stopped receiver, no RXTO is generated, and the caller spins
|
|
* forever. It runs on the main thread, so the whole mesh wedges (observed:
|
|
* RAK3401 1W repeater on 1.16.6, CLI answering only "-> busy").
|
|
*
|
|
* Boards with GPIO/regulator power control cut the module before we get
|
|
* here, so the line is genuinely quiet and STOPRX always yields RXTO.
|
|
* Boards without it fall back to gps_software_sleep(), whose PMTK/UBX
|
|
* commands the module may simply ignore (u-blox MAX-7Q on RAK3401 is
|
|
* protocol 14/15; the 16-byte UBX-RXM-PMREQ we send is protocol 23+) —
|
|
* NMEA keeps streaming straight into the suspend. Those boards give up the
|
|
* ~0.5-1 mA HFCLK saving; uptime wins. */
|
|
#if HAS_GPS_UART && defined(CONFIG_PM_DEVICE) && \
|
|
(HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR) && \
|
|
DT_NODE_HAS_COMPAT(DT_BUS(DT_NODELABEL(gnss)), nordic_nrf_uarte)
|
|
#define HAS_GPS_UART_PM 1
|
|
#else
|
|
#define HAS_GPS_UART_PM 0
|
|
#endif
|
|
|
|
/* Suspend/resume the GNSS UART. Main thread only (like all GPS power
|
|
* paths — pm_device_action_run() calls the driver synchronously).
|
|
* Ordering: resume BEFORE powering the module / sending the wake byte;
|
|
* suspend AFTER the module is off / sleep commands were sent. */
|
|
#if HAS_GPS_UART_PM
|
|
static void gps_uart_set_power(bool on)
|
|
{
|
|
if (!device_is_ready(gps_uart_dev)) {
|
|
return;
|
|
}
|
|
if (!on) {
|
|
/* Let the GNSS line go quiet before suspending. Every caller
|
|
* cuts module power (GPS_EN low / reset asserted / regulator
|
|
* off) immediately before this, but a byte can still be in
|
|
* flight. Settle so it finishes and the driver's RX ISR re-arms,
|
|
* leaving the receiver armed-and-idle when the suspend's STOPRX
|
|
* fires — that state yields RXTO, whereas a just-stopped,
|
|
* un-rearmed receiver can produce none and (pre-0010) hung the
|
|
* main thread. ~5 ms comfortably covers one character time at
|
|
* GNSS baud plus ISR latency; standby happens at most every few
|
|
* minutes, so the cost is negligible. Backstop: patch 0010
|
|
* bounds the driver's RXTO wait so a missed RXTO can never hang
|
|
* us even if a byte still lands in the race window. */
|
|
k_msleep(5);
|
|
}
|
|
int ret = pm_device_action_run(gps_uart_dev,
|
|
on ? PM_DEVICE_ACTION_RESUME
|
|
: PM_DEVICE_ACTION_SUSPEND);
|
|
if (ret == 0) {
|
|
LOG_INF("GPS UART %s", on ? "resumed" : "suspended");
|
|
} else if (ret != -EALREADY) {
|
|
LOG_WRN("GPS UART %s failed: %d", on ? "resume" : "suspend", ret);
|
|
}
|
|
}
|
|
#else
|
|
static inline void gps_uart_set_power(bool on) { ARG_UNUSED(on); }
|
|
#endif
|
|
|
|
#if HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR
|
|
/* gps_uart_send() lives near the top of the file — see "GPS Feature
|
|
* Detection". Only the sleep/wake commands below are gated on this board
|
|
* having no hardware GPS power control. */
|
|
|
|
/* MediaTek parts: $PMTK161,0*28\r\n → enter standby mode
|
|
* Module stops NMEA output and draws ~1mA. Wakes on any UART RX byte.
|
|
* Inert on CASIC parts (L76K and relatives) — they have no such command. */
|
|
static const uint8_t pmtk_standby[] = "$PMTK161,0*28\r\n";
|
|
|
|
/* u-blox ZOE-M8Q: UBX-RXM-PMREQ → enter backup mode
|
|
* UBX frame: B5 62 | 02 41 | 10 00 | payload(16) | CK_A CK_B
|
|
* Payload (protocol 23+, 16 bytes):
|
|
* version=0, reserved[3]=0,
|
|
* duration=0x00000000 (infinite),
|
|
* flags=0x00000006 (backup + force),
|
|
* 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 */
|
|
0x10, 0x00, /* Length: 16 bytes (little-endian) */
|
|
/* Payload */
|
|
0x00, /* version */
|
|
0x00, 0x00, 0x00, /* reserved1[3] */
|
|
0x00, 0x00, 0x00, 0x00, /* duration: 0 = infinite */
|
|
0x06, 0x00, 0x00, 0x00, /* flags: backup(0x02) | force(0x04) */
|
|
0x28, 0x00, 0x00, 0x00, /* wakeupSources: uartrx(bit3) | extint0(bit5) */
|
|
/* Checksum (Fletcher-8 over class..payload) */
|
|
0x81, 0xEB
|
|
};
|
|
|
|
/* Put GPS module into software sleep (for boards without GPIO power control).
|
|
* Sends both Quectel PMTK and u-blox UBX commands — the wrong one is
|
|
* harmlessly ignored by whichever module is actually connected. */
|
|
static void gps_software_sleep(void)
|
|
{
|
|
LOG_INF("GPS: Sending software sleep (PMTK + UBX)");
|
|
|
|
/* MediaTek standby */
|
|
gps_uart_send(pmtk_standby, sizeof(pmtk_standby) - 1); /* exclude null terminator */
|
|
|
|
/* Small delay between commands — let the first one drain */
|
|
k_msleep(50);
|
|
|
|
/* u-blox ZOE-M8Q backup */
|
|
gps_uart_send(ubx_pmreq_backup, sizeof(ubx_pmreq_backup));
|
|
|
|
LOG_DBG("GPS: Software sleep commands sent");
|
|
}
|
|
|
|
/* Wake GPS module from software sleep.
|
|
* A single 0xFF byte on UART triggers wake on both Quectel and u-blox.
|
|
* After wake, the module resumes NMEA output within ~100-500ms. */
|
|
static void gps_software_wake(void)
|
|
{
|
|
LOG_INF("GPS: Sending UART wake byte");
|
|
const uint8_t wake = 0xFF;
|
|
gps_uart_send(&wake, 1);
|
|
/* Give the module time to boot and start NMEA output */
|
|
k_msleep(200);
|
|
}
|
|
#endif /* HAS_GPS_UART && !HAS_GPS_POWER_CONTROL && !HAS_GPS_POWER_REGULATOR */
|
|
|
|
/* Acquire-window timeout (ms) for the current phase.
|
|
* - Repeater: fixed 5-min time-sync window.
|
|
* - First acquisition after enable (cold start, no fix yet): a generous but
|
|
* bounded window so almanac download has time, without pinning the module
|
|
* on forever when there's no sky. Spent once (first_acquire_used set on the
|
|
* first standby), after which the node uses the normal duty cycle regardless
|
|
* of whether a fix was obtained.
|
|
* - All later windows: the normal (warm) acquire timeout. */
|
|
static uint32_t gps_acquire_window_ms(void)
|
|
{
|
|
if (gps_repeater_mode) {
|
|
return GPS_REPEATER_SYNC_TIMEOUT_MS;
|
|
}
|
|
if (!first_fix_acquired && !first_acquire_used) {
|
|
return gps_first_fix_timeout_ms;
|
|
}
|
|
return gps_acquire_timeout_ms;
|
|
}
|
|
|
|
/* Go to standby and schedule next wake.
|
|
* GPIO power control only — keep VRTC for warm start on T1000-E,
|
|
* FORCE_ON de-asserted for L76K hardware standby. */
|
|
static void gps_go_to_standby(void)
|
|
{
|
|
/* Unified standby interval for both roles — set from prefs.gps_interval
|
|
* at boot (companion default 300s, repeater default 48h). Always-on
|
|
* (interval 0) never reaches here. */
|
|
uint64_t wake_interval = gps_wake_interval_ms;
|
|
|
|
LOG_INF("GPS: Going to standby for %llu s%s",
|
|
(unsigned long long)(wake_interval / 1000),
|
|
gps_repeater_mode ? " (repeater time sync)" : "");
|
|
gps_current_state = GPS_STATE_STANDBY;
|
|
consecutive_good_fixes = 0;
|
|
/* The one-time long cold-start window (if any) is now spent — later
|
|
* wakes use the normal (warm) acquire timeout via gps_acquire_window_ms(). */
|
|
first_acquire_used = true;
|
|
|
|
/* Record standby timing for UI (next-wake calculation) */
|
|
standby_start_ms = k_uptime_get();
|
|
standby_interval_ms = wake_interval;
|
|
|
|
/* Power down the GPS module.
|
|
* GPIO boards: hardware power-off (keep VRTC for warm start on T1000-E).
|
|
* Regulator boards: cut the main rail entirely (both roles). The AXP2101
|
|
* VBACKUP charger keeps the receiver's V_BCKP domain alive, so ephemeris/
|
|
* RTC survive the cut and re-acquisition is a warm/hot start, not cold.
|
|
* Other non-GPIO boards: software sleep via UART commands (PMTK + UBX). */
|
|
#if HAS_GPS_POWER_CONTROL
|
|
gps_power_control(false, true);
|
|
#elif HAS_GPS_POWER_REGULATOR
|
|
gps_power_control(false);
|
|
#elif HAS_GPS_UART
|
|
gps_software_sleep();
|
|
#endif
|
|
|
|
/* Module is off/asleep — release the UART until the next wake
|
|
* (nRF: drops the HFCLK request held by the armed RX). */
|
|
gps_uart_set_power(false);
|
|
|
|
/* NOTE: gnss_configured stays true — L76K retains PCAS settings in
|
|
* flash across power cycles. Re-running gnss_configure() after GPIO
|
|
* wake would call modem_chat_run_script() before the chip has booted,
|
|
* risking a deadlock (modem_chat blocks on system work queue). */
|
|
|
|
/* Schedule next wake (event-driven, no polling!) */
|
|
k_work_schedule(&gps_wake_work, K_MSEC(wake_interval));
|
|
}
|
|
|
|
/* Wake GPS and start acquiring.
|
|
* GPIO boards: hardware power-on.
|
|
* Non-GPIO boards: UART wake byte (wakes L76K from standby, ZOE-M8Q from backup).
|
|
* Does NOT call gnss_configure() — constellation/fix-rate settings persist
|
|
* in L76K flash across power cycles. Calling modem_chat_run_script() here
|
|
* would deadlock: the chip needs ~300ms to boot after GPIO power restore,
|
|
* but modem_chat blocks the calling thread waiting for the system work
|
|
* queue which may be processing stale UART data. */
|
|
static void gps_start_acquiring(void)
|
|
{
|
|
LOG_INF("GPS: Waking for %s", gps_repeater_mode ? "time sync" : "position fix");
|
|
gps_current_state = GPS_STATE_ACQUIRING;
|
|
consecutive_good_fixes = 0;
|
|
gnss_activity_seen_this_cycle = false;
|
|
|
|
/* Bring the UART back before the module powers up / the wake byte
|
|
* goes out, so the first NMEA sentences aren't lost. */
|
|
gps_uart_set_power(true);
|
|
|
|
#if HAS_GPS_POWER_CONTROL || HAS_GPS_POWER_REGULATOR
|
|
gps_power_control(true);
|
|
#elif HAS_GPS_UART
|
|
gps_software_wake();
|
|
#endif
|
|
|
|
/* Schedule the standby timeout — unless always-on (interval 0), where the
|
|
* GPS stays in continuous acquisition and never sleeps. Every duty window
|
|
* is bounded (the first cold-start window is just longer; see
|
|
* gps_acquire_window_ms). */
|
|
if (gps_duty_cycling()) {
|
|
uint32_t timeout_ms = gps_acquire_window_ms();
|
|
LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U);
|
|
k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms));
|
|
} else {
|
|
LOG_INF("GPS: Always-on (continuous, no standby)");
|
|
}
|
|
}
|
|
|
|
/* Work handler: wake GPS for fix.
|
|
* Runs on system work queue — MUST NOT call blocking GNSS APIs directly!
|
|
* modem_chat_run_script() blocks on a semaphore signaled from this same
|
|
* work queue, causing a deadlock. Instead, set a flag and signal the
|
|
* main thread to do the actual wake via gps_process_event(). */
|
|
static void gps_wake_work_fn(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
|
|
if (!gps_enabled || gps_current_state != GPS_STATE_STANDBY) {
|
|
return;
|
|
}
|
|
|
|
atomic_or(&pending_gps_actions, GPS_ACTION_WAKE);
|
|
if (gps_event_cb) {
|
|
gps_event_cb();
|
|
}
|
|
}
|
|
|
|
/* Work handler: timeout waiting for fix.
|
|
* Runs on system work queue — MUST NOT call blocking GNSS APIs directly!
|
|
* Same deadlock risk as gps_wake_work_fn. Defer to main thread. */
|
|
static void gps_timeout_work_fn(struct k_work *work)
|
|
{
|
|
ARG_UNUSED(work);
|
|
|
|
if (gps_current_state != GPS_STATE_ACQUIRING) {
|
|
return;
|
|
}
|
|
|
|
LOG_WRN("GPS: Timeout after %d/%d fixes, deferring standby to main thread",
|
|
consecutive_good_fixes, GPS_GOOD_FIX_COUNT);
|
|
|
|
if (gps_repeater_mode && !gnss_activity_seen_this_cycle) {
|
|
LOG_WRN("GPS: Repeater acquire window had no GNSS callbacks; retrying on next cycle");
|
|
}
|
|
|
|
atomic_or(&pending_gps_actions, GPS_ACTION_TIMEOUT);
|
|
if (gps_event_cb) {
|
|
gps_event_cb();
|
|
}
|
|
}
|
|
|
|
/* ========== GPS UART Diagnostics ========== */
|
|
|
|
/**
|
|
* Dump nRF52840 UARTE0 hardware register state.
|
|
* Reads PSEL (pin select), ENABLE, BAUDRATE, and ERRORSRC directly
|
|
* from the peripheral registers — no assumptions, just facts.
|
|
*/
|
|
static void gps_uart_dump_hw_state(void)
|
|
{
|
|
#if defined(CONFIG_SOC_NRF52840)
|
|
NRF_UARTE_Type *uart = NRF_UARTE0;
|
|
|
|
uint32_t psel_txd = uart->PSEL.TXD;
|
|
uint32_t psel_rxd = uart->PSEL.RXD;
|
|
uint32_t enable = uart->ENABLE;
|
|
uint32_t baudrate = uart->BAUDRATE;
|
|
uint32_t errorsrc = uart->ERRORSRC;
|
|
|
|
/* PSEL format: bit 31 = CONNECT (0=connected, 1=disconnected),
|
|
* bits 4:0 = pin, bit 5 = port */
|
|
bool txd_connected = !(psel_txd & (1U << 31));
|
|
bool rxd_connected = !(psel_rxd & (1U << 31));
|
|
uint8_t txd_port = (psel_txd >> 5) & 1;
|
|
uint8_t txd_pin = psel_txd & 0x1F;
|
|
uint8_t rxd_port = (psel_rxd >> 5) & 1;
|
|
uint8_t rxd_pin = psel_rxd & 0x1F;
|
|
|
|
LOG_INF("UART0 HW state:");
|
|
LOG_INF(" ENABLE=0x%02x (8=enabled)", enable);
|
|
LOG_INF(" PSEL.TXD=0x%08x → P%d.%02d %s",
|
|
psel_txd, txd_port, txd_pin,
|
|
txd_connected ? "CONNECTED" : "DISCONNECTED");
|
|
LOG_INF(" PSEL.RXD=0x%08x → P%d.%02d %s",
|
|
psel_rxd, rxd_port, rxd_pin,
|
|
rxd_connected ? "CONNECTED" : "DISCONNECTED");
|
|
LOG_INF(" BAUDRATE=0x%08x ERRORSRC=0x%x", baudrate, errorsrc);
|
|
|
|
/* Clear any error flags */
|
|
if (errorsrc) {
|
|
uart->ERRORSRC = errorsrc;
|
|
LOG_WRN(" UART errors cleared: overrun=%d parity=%d framing=%d break=%d",
|
|
(errorsrc >> 0) & 1, (errorsrc >> 1) & 1,
|
|
(errorsrc >> 2) & 1, (errorsrc >> 3) & 1);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if HAS_GPS_POWER_CONTROL
|
|
/**
|
|
* Log actual GPIO pin states after power-up sequence.
|
|
* Reads back each configured pin to verify the hardware accepted our config.
|
|
*/
|
|
static void gps_dump_gpio_states(void)
|
|
{
|
|
/* Port/pin come from the gpio_dt_spec so the board's real wiring is
|
|
* printed. These used to be hardcoded T1000-E pin numbers, which read
|
|
* as plausible nonsense on every other board. */
|
|
#define GPS_LOG_PIN(_label, _spec) if (gpio_is_ready_dt(&(_spec))) { LOG_INF(" %-14s %s.%02u: %d", _label, (_spec).port->name, (_spec).pin, gpio_pin_get_dt(&(_spec))); }
|
|
|
|
LOG_INF("GPS GPIO states after power-up:");
|
|
GPS_LOG_PIN("GPS_EN", gps_enable_gpio);
|
|
#if HAS_GPS_VRTC
|
|
GPS_LOG_PIN("GPS_VRTC_EN", gps_vrtc_gpio);
|
|
#endif
|
|
#if HAS_GPS_RESET
|
|
GPS_LOG_PIN("GPS_RESET", gps_reset_gpio);
|
|
#endif
|
|
#if HAS_GPS_SLEEP
|
|
GPS_LOG_PIN("GPS_SLEEP_INT", gps_sleep_gpio);
|
|
#endif
|
|
#if HAS_GPS_RTCINT
|
|
GPS_LOG_PIN("GPS_RTC_INT", gps_rtcint_gpio);
|
|
#endif
|
|
#if HAS_GPS_RESETB
|
|
GPS_LOG_PIN("GPS_RESETB", gps_resetb_gpio); /* INPUT_PULLUP, expect 1 */
|
|
#endif
|
|
|
|
#undef GPS_LOG_PIN
|
|
}
|
|
#endif /* HAS_GPS_POWER_CONTROL */
|
|
|
|
/* ========== GNSS Init ========== */
|
|
|
|
static int gnss_init(void)
|
|
{
|
|
/* Try to find a GNSS device - prefer chip-specific drivers
|
|
* (they support constellation config, fix rate, etc.) over
|
|
* the generic NMEA parser which is passive only.
|
|
* Power control is done lazily in gps_enable(). */
|
|
#if DT_HAS_COMPAT_STATUS_OKAY(quectel_lc76g)
|
|
gnss_dev = DEVICE_DT_GET_ANY(quectel_lc76g);
|
|
#elif DT_HAS_COMPAT_STATUS_OKAY(luatos_air530z)
|
|
gnss_dev = DEVICE_DT_GET_ANY(luatos_air530z);
|
|
#elif DT_HAS_COMPAT_STATUS_OKAY(quectel_lcx6g)
|
|
gnss_dev = DEVICE_DT_GET_ANY(quectel_lcx6g);
|
|
#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_m8)
|
|
gnss_dev = DEVICE_DT_GET_ANY(u_blox_m8);
|
|
#elif DT_HAS_COMPAT_STATUS_OKAY(u_blox_f9p)
|
|
gnss_dev = DEVICE_DT_GET_ANY(u_blox_f9p);
|
|
#elif DT_HAS_COMPAT_STATUS_OKAY(gnss_nmea_generic)
|
|
gnss_dev = DEVICE_DT_GET_ANY(gnss_nmea_generic);
|
|
#endif
|
|
|
|
if (gnss_dev == NULL) {
|
|
LOG_WRN("No GNSS device found in device tree");
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (!device_is_ready(gnss_dev)) {
|
|
/* Root cause: GPS transmits NMEA immediately at power-up before
|
|
* modem_chat opens its DMA pipe. UARTE accumulates overrun/framing
|
|
* errors, causing modem_pipe_open() to fail and device_init to return
|
|
* an error.
|
|
*
|
|
* Strategy: use UARTE ERRORSRC as a real signal. Wait until errors
|
|
* appear (GPS is transmitting), clear them, then call device_init.
|
|
* This avoids arbitrary delays — we act when the hardware tells us
|
|
* conditions are ready, not after a fixed sleep.
|
|
*
|
|
* IMPORTANT: Do NOT use uart_poll_in() — it corrupts nRF52840 UARTE
|
|
* DMA state and breaks modem_pipe async receive. */
|
|
LOG_INF("GNSS device not ready — waiting for GPS activity on UART");
|
|
gps_power_control(true);
|
|
|
|
#if HAS_GPS_POWER_CONTROL
|
|
gps_dump_gpio_states();
|
|
#endif
|
|
|
|
#if defined(CONFIG_SOC_NRF52840)
|
|
NRF_UARTE_Type *uart = NRF_UARTE0;
|
|
|
|
/* Wait up to 2s for UARTE errors — their presence means the GPS
|
|
* module is alive and transmitting (ERRORSRC gets set because no
|
|
* DMA buffer is configured yet). */
|
|
bool gps_active = false;
|
|
for (int t = 0; t < 200; t++) {
|
|
if (uart->ERRORSRC != 0) {
|
|
LOG_INF("GPS UART activity detected after ~%dms "
|
|
"(ERRORSRC=0x%x)", t * 10, uart->ERRORSRC);
|
|
gps_active = true;
|
|
break;
|
|
}
|
|
k_msleep(10);
|
|
}
|
|
if (!gps_active) {
|
|
LOG_WRN("No GPS UART activity within 2s — module may not be "
|
|
"powered or transmitting");
|
|
}
|
|
#else
|
|
/* Non-nRF52840: no direct UARTE register access, fall back to
|
|
* a brief fixed wait for the GPS to start transmitting. */
|
|
k_msleep(500);
|
|
#endif
|
|
|
|
bool init_ok = false;
|
|
for (int attempt = 0; attempt < 3 && !init_ok; attempt++) {
|
|
/* Clear accumulated UART errors before opening the modem pipe */
|
|
gps_uart_dump_hw_state();
|
|
|
|
int ret = device_init(gnss_dev);
|
|
if (ret != 0 && ret != -EALREADY) {
|
|
LOG_WRN("GNSS device_init attempt %d failed: %d",
|
|
attempt + 1, ret);
|
|
/* Small wait for UART to settle, then retry */
|
|
k_msleep(100);
|
|
continue;
|
|
}
|
|
|
|
/* Poll for readiness — modem_chat needs a brief moment to
|
|
* complete pipe setup after device_init returns. */
|
|
for (int t = 0; t < 50; t++) {
|
|
if (device_is_ready(gnss_dev)) {
|
|
LOG_INF("GNSS ready after ~%dms (attempt %d)",
|
|
t * 10, attempt + 1);
|
|
init_ok = true;
|
|
break;
|
|
}
|
|
k_msleep(10);
|
|
}
|
|
|
|
if (!init_ok) {
|
|
LOG_WRN("GNSS not ready after attempt %d", attempt + 1);
|
|
}
|
|
}
|
|
|
|
if (!init_ok) {
|
|
LOG_ERR("GNSS device failed to initialize");
|
|
gps_uart_dump_hw_state();
|
|
return -ENODEV;
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_PM_DEVICE
|
|
/* Some upstream GNSS drivers (gnss-nmea-generic) start suspended under
|
|
* CONFIG_PM_DEVICE and never open their modem pipe until resumed — no
|
|
* NMEA would ever flow (the old "PM broke GPS" trap). Resume once
|
|
* here: main thread at boot, the one safe context for the modem_chat
|
|
* scripts a resume may run. PM-less drivers (luatos,air530z) return
|
|
* -ENOSYS. Retried like device_init above — opening the pipe while
|
|
* the module is mid-sentence can fail transiently. */
|
|
int pm_ret = pm_device_action_run(gnss_dev, PM_DEVICE_ACTION_RESUME);
|
|
for (int attempt = 1; pm_ret != 0 && pm_ret != -EALREADY &&
|
|
pm_ret != -ENOSYS && attempt < 3; attempt++) {
|
|
LOG_WRN("GNSS PM resume failed (%d), retrying", pm_ret);
|
|
k_msleep(100);
|
|
pm_ret = pm_device_action_run(gnss_dev, PM_DEVICE_ACTION_RESUME);
|
|
}
|
|
if (pm_ret != 0 && pm_ret != -EALREADY && pm_ret != -ENOSYS) {
|
|
LOG_ERR("GNSS PM resume failed: %d", pm_ret);
|
|
return -ENODEV;
|
|
}
|
|
#endif
|
|
|
|
LOG_INF("GNSS device %s initialized", gnss_dev->name);
|
|
gps_available = true;
|
|
return 0;
|
|
}
|
|
#endif /* HAS_GNSS */
|
|
|
|
/* ========== Public API ========== */
|
|
|
|
int gps_manager_init(void)
|
|
{
|
|
#if HAS_GNSS
|
|
gnss_init();
|
|
|
|
/* Configure constellations + fix rate NOW while chip is powered
|
|
* and the modem pipe is open (driver init already ran).
|
|
* This is the ONLY safe place to call modem_chat_run_script() —
|
|
* after power cycles the chip needs ~300ms boot time and calling
|
|
* modem_chat from the main thread risks deadlock. L76K retains
|
|
* PCAS settings in flash, so one-time config at boot is enough. */
|
|
gnss_configure();
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
bool gps_is_available(void)
|
|
{
|
|
#if HAS_GNSS
|
|
return gps_available;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool gps_is_enabled(void)
|
|
{
|
|
#if HAS_GNSS
|
|
return gps_enabled;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void gps_ensure_power_state(bool should_be_enabled)
|
|
{
|
|
#if HAS_GNSS
|
|
if (!gps_available) {
|
|
return;
|
|
}
|
|
|
|
/* At boot, GPS hardware is powered (bootloader/pull-up).
|
|
* If it should be disabled, explicitly power it off now. */
|
|
if (!should_be_enabled) {
|
|
LOG_INF("GPS: Powering off at boot (disabled in prefs)");
|
|
gps_power_control(false);
|
|
/* GPS stays off — release the UART too. Without this, the RX
|
|
* armed at driver init would hold HFCLK for the entire uptime
|
|
* of every GPS-disabled node. */
|
|
gps_uart_set_power(false);
|
|
gps_current_state = GPS_STATE_OFF;
|
|
}
|
|
#else
|
|
ARG_UNUSED(should_be_enabled);
|
|
#endif
|
|
}
|
|
|
|
void gps_set_repeater_mode(bool repeater)
|
|
{
|
|
#if HAS_GNSS
|
|
if (!gps_available) {
|
|
return;
|
|
}
|
|
|
|
gps_repeater_mode = repeater;
|
|
|
|
if (repeater) {
|
|
LOG_INF("GPS: Repeater mode - starting initial time sync, then every 48h");
|
|
|
|
gps_enabled = true; /* Logically enabled */
|
|
|
|
/* Start acquiring immediately for initial time sync at boot.
|
|
* GPS hardware is already powered from bootloader, so we just
|
|
* start the acquisition state machine. */
|
|
gps_start_acquiring();
|
|
} else {
|
|
LOG_INF("GPS: Companion mode");
|
|
}
|
|
#else
|
|
ARG_UNUSED(repeater);
|
|
#endif
|
|
}
|
|
|
|
void gps_enable(bool enable)
|
|
{
|
|
#if HAS_GNSS
|
|
if (!gps_available) {
|
|
LOG_WRN("GPS not available");
|
|
return;
|
|
}
|
|
|
|
if (enable == gps_enabled) {
|
|
return;
|
|
}
|
|
|
|
gps_enabled = enable;
|
|
|
|
if (enable) {
|
|
LOG_INF("GPS enabled - starting acquisition");
|
|
|
|
/* Block SoC light sleep for as long as the module is powered.
|
|
* The GNSS UART is not a wake source, so a sleeping SoC drops
|
|
* inbound NMEA outright — sentences would be lost mid-stream and
|
|
* a fix would never converge. Balanced by the put in the disable
|
|
* branch; the early return above keeps the pair 1:1, and under a
|
|
* GPS duty cycle the lock is only held during the awake phase.
|
|
* Compiles to nothing without CONFIG_PM. */
|
|
zc_pm_block_sleep();
|
|
|
|
/* Start acquiring immediately (no delay for first wake) */
|
|
gps_current_state = GPS_STATE_ACQUIRING;
|
|
consecutive_good_fixes = 0;
|
|
|
|
/* Resume the GNSS UART first so no NMEA is lost at power-on
|
|
* (it may be suspended from a boot-with-GPS-off or a prior
|
|
* disable). */
|
|
gps_uart_set_power(true);
|
|
|
|
/* Power on GPS - uses lazy GPIO init */
|
|
gps_power_control(true);
|
|
|
|
/* gnss_configure() runs once at boot (see gps_manager_init path).
|
|
* L76K retains PCAS settings in flash across power cycles.
|
|
* Do NOT call modem_chat_run_script() here — the chip needs
|
|
* ~300ms to boot after GPIO power restore and calling it
|
|
* immediately deadlocks the main thread. */
|
|
gps_diag_maybe_reconfigure();
|
|
|
|
/* Bounded first-acquisition window, then the normal duty cycle —
|
|
* unless always-on (interval 0), where GPS never sleeps. */
|
|
if (gps_duty_cycling()) {
|
|
uint32_t timeout_ms = gps_acquire_window_ms();
|
|
LOG_INF("GPS: Acquire window %u s", timeout_ms / 1000U);
|
|
k_work_schedule(&gps_timeout_work, K_MSEC(timeout_ms));
|
|
} else {
|
|
LOG_INF("GPS: Always-on (continuous, no standby)");
|
|
}
|
|
} else {
|
|
LOG_INF("GPS disabled - canceling timers and powering off");
|
|
|
|
/* Matches the block taken in the enable branch. */
|
|
zc_pm_unblock_sleep();
|
|
|
|
/* Cancel any pending work */
|
|
k_work_cancel_delayable(&gps_wake_work);
|
|
k_work_cancel_delayable(&gps_timeout_work);
|
|
|
|
/* Power off GPS — warm standby if VRTC available (Arduino sleep_gps),
|
|
* full power off otherwise. Warm standby preserves ephemeris/RTC
|
|
* in AG3335 backup RAM for fast re-acquisition (1-8s vs 15-45s). */
|
|
#if HAS_GPS_VRTC
|
|
gps_power_control(false, true); /* Warm standby — keep VRTC */
|
|
#else
|
|
gps_power_control(false); /* No VRTC — full power off */
|
|
#endif
|
|
|
|
/* GPS is off until re-enabled — release the UART. */
|
|
gps_uart_set_power(false);
|
|
|
|
gps_current_state = GPS_STATE_OFF;
|
|
consecutive_good_fixes = 0;
|
|
|
|
/* Zero the stale satellite count so a re-enable doesn't briefly
|
|
* report a live fix (e.g. joystick UI showing "3D FIX") off old
|
|
* data before any new NMEA sentence arrives. lat/lon/valid are
|
|
* deliberately left alone — telemetry/UI "last known position"
|
|
* reads (gps_get_position) intentionally survive an on/off
|
|
* toggle; only the live fix-quality indicator resets. */
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
current_pos.satellites = 0;
|
|
k_mutex_unlock(&gps_mutex);
|
|
|
|
/* Clear first-fix flags so the next enable gets a fresh long
|
|
* first-acquisition window again — the user explicitly toggled GPS
|
|
* expecting it to try hard for a fix. */
|
|
first_fix_acquired = false;
|
|
first_acquire_used = false;
|
|
|
|
/* Clear time sync flag - time will drift, allow phone sync again */
|
|
gps_time_synced = false;
|
|
}
|
|
|
|
/* Notify callback (for persistence in main.cpp) */
|
|
if (gps_enable_cb) {
|
|
gps_enable_cb(enable);
|
|
}
|
|
#else
|
|
ARG_UNUSED(enable);
|
|
#endif
|
|
}
|
|
|
|
void gps_get_position(struct gps_position *pos)
|
|
{
|
|
#if HAS_GNSS
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
*pos = current_pos;
|
|
k_mutex_unlock(&gps_mutex);
|
|
#else
|
|
memset(pos, 0, sizeof(*pos));
|
|
#endif
|
|
}
|
|
|
|
uint32_t gps_get_poll_interval_sec(void)
|
|
{
|
|
#if HAS_GNSS
|
|
return gps_wake_interval_ms / 1000U;
|
|
#else
|
|
return CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC;
|
|
#endif
|
|
}
|
|
|
|
void gps_set_poll_interval_sec(uint32_t interval)
|
|
{
|
|
#if HAS_GNSS
|
|
/* 0 = always-on (no standby); otherwise floor 10s. Cap at 1 week — sane
|
|
* for time-sync and safely below the interval*1000 uint32 overflow (~49d). */
|
|
if (interval != 0 && interval < 10) interval = 10;
|
|
if (interval > 604800) interval = 604800;
|
|
gps_wake_interval_ms = interval * 1000U;
|
|
LOG_INF("GPS poll interval set to %u seconds%s", interval,
|
|
interval == 0 ? " (always on)" : "");
|
|
|
|
/* Live re-arm so a runtime change takes effect without a reboot. */
|
|
if (!gps_enabled) {
|
|
return;
|
|
}
|
|
if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
if (interval == 0) {
|
|
/* Switch to always-on: drop the standby timeout so it won't sleep. */
|
|
k_work_cancel_delayable(&gps_timeout_work);
|
|
} else if (!k_work_delayable_is_pending(&gps_timeout_work)) {
|
|
/* Was always-on: arm a timeout so it starts duty cycling. */
|
|
k_work_reschedule(&gps_timeout_work, K_MSEC(gps_acquire_window_ms()));
|
|
}
|
|
} else if (gps_current_state == GPS_STATE_STANDBY) {
|
|
k_work_cancel_delayable(&gps_wake_work);
|
|
if (interval == 0) {
|
|
/* Wake now and stay on. */
|
|
atomic_or(&pending_gps_actions, GPS_ACTION_WAKE);
|
|
if (gps_event_cb) {
|
|
gps_event_cb();
|
|
}
|
|
} else {
|
|
standby_interval_ms = gps_wake_interval_ms;
|
|
k_work_reschedule(&gps_wake_work, K_MSEC(gps_wake_interval_ms));
|
|
}
|
|
}
|
|
#else
|
|
ARG_UNUSED(interval);
|
|
#endif
|
|
}
|
|
|
|
int64_t gps_get_utc_time(void)
|
|
{
|
|
#if HAS_GNSS
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
if (!current_pos.valid) {
|
|
k_mutex_unlock(&gps_mutex);
|
|
return 0;
|
|
}
|
|
|
|
struct gnss_time t = current_utc;
|
|
k_mutex_unlock(&gps_mutex);
|
|
|
|
/* Defensive: the date math below indexes month_days[m] for m < t.month.
|
|
* t.month is a uint8_t straight from the GNSS driver — bound it (and the
|
|
* day) so a driver that doesn't range-check (the NMEA parser does; binary
|
|
* UBX/chip drivers are not all verified) can't drive an OOB read of
|
|
* month_days[13] or a garbage RTC set. */
|
|
if (t.month < 1 || t.month > 12 || t.month_day < 1 || t.month_day > 31) {
|
|
return 0;
|
|
}
|
|
|
|
int year = 2000 + t.century_year;
|
|
int days = 0;
|
|
|
|
for (int y = 1970; y < year; y++) {
|
|
days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365;
|
|
}
|
|
|
|
static const int month_days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
|
for (int m = 1; m < t.month; m++) {
|
|
days += month_days[m];
|
|
if (m == 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) {
|
|
days++;
|
|
}
|
|
}
|
|
|
|
days += t.month_day - 1;
|
|
|
|
int64_t timestamp = (int64_t)days * 86400;
|
|
timestamp += t.hour * 3600;
|
|
timestamp += t.minute * 60;
|
|
timestamp += t.millisecond / 1000;
|
|
|
|
return timestamp;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
bool gps_has_time_sync(void)
|
|
{
|
|
#if HAS_GNSS
|
|
/* Returns true if GPS has recently synced the RTC.
|
|
* Expires after 2 hours without a fix so the phone can re-sync
|
|
* (e.g. node moved indoors, GPS lost sky, RTC drifting). */
|
|
if (!gps_time_synced) {
|
|
return false;
|
|
}
|
|
int64_t age_ms = k_uptime_get() - last_fix_uptime_ms;
|
|
|
|
if (age_ms > (2 * 60 * 60 * 1000LL)) {
|
|
gps_time_synced = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool gps_get_last_known_position(struct gps_position *pos)
|
|
{
|
|
#if HAS_GNSS
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
if (current_pos.valid) {
|
|
*pos = current_pos;
|
|
k_mutex_unlock(&gps_mutex);
|
|
return true;
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
#endif
|
|
memset(pos, 0, sizeof(*pos));
|
|
return false;
|
|
}
|
|
|
|
void gps_request_fresh_fix(void)
|
|
{
|
|
#if HAS_GNSS
|
|
if (!gps_available || !gps_enabled) {
|
|
return;
|
|
}
|
|
|
|
if (gps_current_state == GPS_STATE_STANDBY) {
|
|
LOG_INF("GPS: Fresh fix requested, waking early");
|
|
/* Cancel scheduled wake and wake immediately */
|
|
k_work_cancel_delayable(&gps_wake_work);
|
|
gps_start_acquiring();
|
|
} else if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
/* Already acquiring — reschedule the timeout so the caller's
|
|
* fresh-fix request gets a full window from now. Otherwise, a
|
|
* telemetry request that arrives 25s into a 30s acquire window
|
|
* only has 5s left, which in marginal signal usually means the
|
|
* chip goes to standby before producing a fix the requester
|
|
* could use. Each duty phase has a bounded window
|
|
* (gps_acquire_window_ms); in always-on there's no timeout to extend
|
|
* (GPS is continuously acquiring and current_pos is always fresh). */
|
|
if (gps_duty_cycling()) {
|
|
uint32_t timeout_ms = gps_acquire_window_ms();
|
|
LOG_INF("GPS: Fresh fix requested, extending acquire timeout to %u s",
|
|
timeout_ms / 1000U);
|
|
k_work_reschedule(&gps_timeout_work, K_MSEC(timeout_ms));
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void gps_set_diag(bool on)
|
|
{
|
|
gps_diag_on = on;
|
|
#if HAS_GNSS
|
|
if (on) {
|
|
/* Re-arm configuration so the next GPS enable ("gps off" then
|
|
* "gps on") actually re-runs it and refreshes the record. */
|
|
gnss_configured = false;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool gps_get_diag(void)
|
|
{
|
|
return gps_diag_on;
|
|
}
|
|
|
|
void gps_get_diag_report(char *buf, size_t len)
|
|
{
|
|
#if HAS_GNSS
|
|
static const char *const path_str[] = { "never", "api", "uart", "blind" };
|
|
uint8_t path = gps_cfg_diag.path;
|
|
const char *pname = (path < ARRAY_SIZE(path_str)) ? path_str[path] : "?";
|
|
|
|
if (path == GPS_CFG_NEVER) {
|
|
snprintf(buf, len, "diag=%s cfg=never-run (enable, then 'gps off'/'gps on')",
|
|
gps_diag_on ? "on" : "off");
|
|
return;
|
|
}
|
|
|
|
uint32_t age_s = (uint32_t)((k_uptime_get() - gps_cfg_diag.at_ms) / 1000);
|
|
size_t n = (size_t)snprintf(buf, len, "diag=%s cfg=%s age=%us",
|
|
gps_diag_on ? "on" : "off", pname, age_s);
|
|
if (n >= 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. */
|
|
if (zephcore_gnss_version_get != NULL) {
|
|
char ver[40];
|
|
if (zephcore_gnss_version_get(ver, sizeof(ver)) > 0) {
|
|
n += (size_t)snprintf(buf + n, len - n, " mod=%s", ver);
|
|
} else {
|
|
n += (size_t)snprintf(buf + n, len - n, " mod=no-reply");
|
|
}
|
|
if (n >= len) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (path == GPS_CFG_UART) {
|
|
/* Bytes actually clocked out to the module. Note these are sent
|
|
* blind — this proves transmission, not acceptance. Constellation
|
|
* tallies below are the acceptance evidence. */
|
|
n += (size_t)snprintf(buf + n, len - n, " sent=%u/%uB",
|
|
gps_cfg_diag.cmds, gps_cfg_diag.bytes);
|
|
} else if (path == GPS_CFG_API) {
|
|
n += (size_t)snprintf(buf + n, len - n, " sys_ret=%d rate_ret=%d",
|
|
gps_cfg_diag.api_ret, gps_cfg_diag.rate_ret);
|
|
}
|
|
if (n >= len) {
|
|
return;
|
|
}
|
|
|
|
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
/* Age out constellations that have stopped reporting, so a stale count
|
|
* is never presented as current. */
|
|
int64_t now_ms = k_uptime_get();
|
|
uint8_t shown[5];
|
|
for (int i = 0; i < 5; i++) {
|
|
shown[i] = ((now_ms - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i];
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
snprintf(buf + n, len - n, " sys=G%u/R%u/E%u/B%u/?%u",
|
|
shown[0], shown[1], shown[2], shown[3], shown[4]);
|
|
#else
|
|
snprintf(buf + n, len - n, " (build without GPS_SAT_DIAG: no per-constellation proof)");
|
|
#endif
|
|
#else
|
|
ARG_UNUSED(gps_diag_on);
|
|
snprintf(buf, len, "no GNSS on this board");
|
|
#endif
|
|
}
|
|
|
|
void gps_get_state_info(struct gps_state_info *info)
|
|
{
|
|
memset(info, 0, sizeof(*info));
|
|
#if HAS_GNSS
|
|
info->state = (uint8_t)gps_current_state;
|
|
info->satellites = current_pos.satellites;
|
|
|
|
if (last_fix_uptime_ms > 0) {
|
|
/* Seconds since last validated fix */
|
|
info->last_fix_age_s = (uint32_t)((k_uptime_get() - last_fix_uptime_ms) / 1000);
|
|
} else {
|
|
info->last_fix_age_s = UINT32_MAX; /* No fix yet */
|
|
}
|
|
|
|
if (gps_current_state == GPS_STATE_STANDBY && standby_interval_ms > 0) {
|
|
int64_t wake_at = standby_start_ms + (int64_t)standby_interval_ms;
|
|
int64_t remaining = wake_at - k_uptime_get();
|
|
|
|
info->next_search_s = (remaining > 0) ? (uint32_t)(remaining / 1000) : 0;
|
|
} else if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
info->next_search_s = 0; /* Searching right now */
|
|
}
|
|
|
|
#ifdef CONFIG_ZEPHCORE_GPS_SAT_DIAG
|
|
/* Same staleness rule as the diag report — never present a count for a
|
|
* constellation that has stopped reporting. */
|
|
k_mutex_lock(&gps_mutex, K_FOREVER);
|
|
int64_t sat_now = k_uptime_get();
|
|
uint8_t *dst[5] = { &info->sats_gps, &info->sats_glonass, &info->sats_galileo,
|
|
&info->sats_beidou, &info->sats_other };
|
|
for (int i = 0; i < 5; i++) {
|
|
*dst[i] = ((sat_now - sat_seen_ms[i]) > SAT_TALLY_STALE_MS) ? 0 : sat_count[i];
|
|
}
|
|
k_mutex_unlock(&gps_mutex);
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
/* Process pending GPS state transitions — called from main thread.
|
|
* Work handlers on the system work queue set flags + signal the main
|
|
* thread via gps_event_cb(). The main thread then calls this function,
|
|
* which safely executes blocking GNSS configuration (modem_chat_run_script
|
|
* blocks on a semaphore signaled from the system work queue — calling it
|
|
* FROM the work queue deadlocks). */
|
|
void gps_process_event(void)
|
|
{
|
|
#if HAS_GNSS
|
|
uint32_t actions = (uint32_t)atomic_clear(&pending_gps_actions);
|
|
|
|
if (actions == 0) {
|
|
return;
|
|
}
|
|
|
|
/* Wake takes priority — if both wake and timeout/fix-done are pending
|
|
* (shouldn't happen, but be safe), wake wins. */
|
|
if (actions & GPS_ACTION_WAKE) {
|
|
if (gps_enabled && gps_current_state == GPS_STATE_STANDBY) {
|
|
gps_start_acquiring();
|
|
}
|
|
} else if (actions & (GPS_ACTION_TIMEOUT | GPS_ACTION_FIX_DONE)) {
|
|
if (gps_current_state == GPS_STATE_ACQUIRING) {
|
|
gps_go_to_standby();
|
|
}
|
|
}
|
|
#endif
|
|
}
|