/* * SPDX-License-Identifier: Apache-2.0 * 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) * - Full power-off only on user-disable or System OFF */ #include "ZephyrGPSManager.h" #include #include #include #include #include #include #if defined(CONFIG_SOC_NRF52840) #include #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 #else #define HAS_GNSS 0 #endif /* ========== GPS Module Capability Flags ========== * GPS power management uses GPIO only (no PM_DEVICE): * - Wio Tracker L1 (L76K): FORCE_ON pin LOW = hardware standby (~360µA, * Vcc stays on, ephemeris/almanac/RTC preserved, hot-start 1-2s) * - T1000-E (AG3335): GPS_EN LOW + VRTC HIGH = warm standby (ephemeris * preserved via backup RAM, ~1-2µA VRTC current) * - All boards: gps-enable alias → GPIO power control * * PM_DEVICE is intentionally NOT used — the system-managed PM subsystem * auto-suspends devices during idle, calling modem_chat_run_script() from * an unexpected context which can deadlock the system. */ /* ========== 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 successful fix since enable */ 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_wake_interval_ms = CONFIG_ZEPHCORE_GPS_POLL_INTERVAL_SEC * 1000U; /* Repeater mode intervals - GPS only for time sync */ #define GPS_REPEATER_SYNC_INTERVAL_MS (48ULL * 60 * 60 * 1000) /* 48 hours */ #define GPS_REPEATER_SYNC_TIMEOUT_MS (5 * 60 * 1000) /* 5 minutes */ static bool gps_repeater_mode = false; /* True = repeater (time sync only), False = companion */ /* 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 disabled — ignore stale NMEA from hardware power-down. * The GNSS driver fires callbacks as long as the UART has data, * even after we drive GPS_EN LOW (module drains its buffer). */ return; } 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) { 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) { 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(); /* 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; } } 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 { current_pos.valid = false; /* Reset counter on no fix */ 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); /* 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; 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); } if (ret == 0) { LOG_INF("GPS: Multi-constellation enabled"); } else if (ret == -ENOSYS || ret == -ENOTSUP) { /* gnss-nmea-generic is a passive listener — no GNSS API. * Send PMTK353 directly via UART for constellation config. * uart_poll_out is safe: gnss-nmea-generic has no TX activity. */ const struct device *gnss_uart = DEVICE_DT_GET(DT_BUS(DT_NODELABEL(gnss))); if (device_is_ready(gnss_uart)) { /* GPS + GLONASS + Galileo + BeiDou (no QZSS) */ static const char pmtk[] = "$PMTK353,1,1,1,1,0*2B\r\n"; for (size_t i = 0; pmtk[i]; i++) { uart_poll_out(gnss_uart, pmtk[i]); } LOG_INF("GPS: Sent PMTK353 (GPS+GLONASS+Galileo+BeiDou)"); } else { LOG_INF("GPS: Constellation config not supported (UART not ready)"); } } 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); 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 DT_NODE_EXISTS(DT_ALIAS(gps_enable)) static const struct gpio_dt_spec gps_enable_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_enable), gpios); #define HAS_GPS_POWER_CONTROL 1 #else #define HAS_GPS_POWER_CONTROL 0 #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 LOW 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_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: * 1. GPS_EN HIGH, delay 10ms * 2. GPS_VRTC_EN HIGH, delay 10ms (critical - RTC power) * 3. GPS_RESET HIGH, delay 10ms, then LOW * 4. GPS_SLEEP_INT HIGH */ if (gpio_is_ready_dt(&gps_enable_gpio)) { gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_HIGH); } k_msleep(10); #if HAS_GPS_VRTC if (gpio_is_ready_dt(&gps_vrtc_gpio)) { gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_HIGH); } 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_HIGH); k_msleep(10); gpio_pin_set_dt(&gps_reset_gpio, 0); /* Release reset */ } #endif #if HAS_GPS_SLEEP if (gpio_is_ready_dt(&gps_sleep_gpio)) { gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_HIGH); } #endif #if HAS_GPS_RTCINT /* GPS_RTC_INT (P0.15) — held LOW during normal operation */ if (gpio_is_ready_dt(&gps_rtcint_gpio)) { gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_LOW); } #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)) { gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_HIGH); gps_gpio_configured = true; LOG_INF("GPS power GPIO configured, set HIGH"); } 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 goes HIGH on next * power-on, preventing uncontrolled startup before the reset pulse. */ if (gpio_is_ready_dt(&gps_reset_gpio)) { 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_LOW); } else { gpio_pin_set_dt(&gps_vrtc_gpio, 0); } } } /* else: warm standby — VRTC stays HIGH, 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_LOW); gps_gpio_configured = true; } else { gpio_pin_set_dt(&gps_enable_gpio, 0); } } #if HAS_GPS_RESETB /* Drive RESETB LOW 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_LOW); } #endif #if HAS_GPS_RTCINT /* GPS_RTC_INT stays LOW during sleep/off (same as normal operation) */ if (gpio_is_ready_dt(&gps_rtcint_gpio)) { gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_LOW); } #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 } /* Drive all GPS power-enable GPIOs LOW for System OFF. * 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_CONTROL if (gpio_is_ready_dt(&gps_enable_gpio)) { gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_LOW); } #endif #if HAS_GPS_VRTC if (gpio_is_ready_dt(&gps_vrtc_gpio)) { gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_LOW); } #endif #if HAS_GPS_RESET if (gpio_is_ready_dt(&gps_reset_gpio)) { gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_LOW); } #endif #if HAS_GPS_SLEEP if (gpio_is_ready_dt(&gps_sleep_gpio)) { gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_LOW); } #endif #if HAS_GPS_RTCINT if (gpio_is_ready_dt(&gps_rtcint_gpio)) { gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_LOW); } #endif #if HAS_GPS_RESETB if (gpio_is_ready_dt(&gps_resetb_gpio)) { gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_LOW); } #endif } #if HAS_GNSS /* Resume GNSS-specific code */ /* Go to standby and schedule next wake. * GPIO power control only — keep VRTC for warm start on T1000-E, * FORCE_ON pin LOW for L76K hardware standby. */ static void gps_go_to_standby(void) { uint64_t wake_interval = gps_repeater_mode ? GPS_REPEATER_SYNC_INTERVAL_MS : gps_wake_interval_ms; if (gps_repeater_mode) { LOG_INF("GPS: Going to standby for 48 hours (repeater mode)"); } else { LOG_INF("GPS: Going to standby for %d minutes", (int)(wake_interval / 60000)); } gps_current_state = GPS_STATE_STANDBY; consecutive_good_fixes = 0; /* Record standby timing for UI (next-wake calculation) */ standby_start_ms = k_uptime_get(); standby_interval_ms = wake_interval; /* GPIO power-off — keep VRTC for warm start on T1000-E */ gps_power_control(false, true); /* 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 power-on. * 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; gps_power_control(true); /* Start timeout timer */ if (gps_repeater_mode) { /* Repeater mode: always use 5 min timeout for time sync */ k_work_schedule(&gps_timeout_work, K_MSEC(GPS_REPEATER_SYNC_TIMEOUT_MS)); } else if (first_fix_acquired) { /* Companion mode: use short timeout after first fix acquired */ k_work_schedule(&gps_timeout_work, K_MSEC(gps_acquire_timeout_ms)); } else { LOG_INF("GPS: First fix mode - no timeout"); } } /* 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); 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) { LOG_INF("GPS GPIO states after power-up:"); if (gpio_is_ready_dt(&gps_enable_gpio)) { LOG_INF(" GPS_EN (P1.11): %d", gpio_pin_get_dt(&gps_enable_gpio)); } #if HAS_GPS_VRTC if (gpio_is_ready_dt(&gps_vrtc_gpio)) { LOG_INF(" GPS_VRTC_EN (P0.08): %d", gpio_pin_get_dt(&gps_vrtc_gpio)); } #endif #if HAS_GPS_RESET if (gpio_is_ready_dt(&gps_reset_gpio)) { LOG_INF(" GPS_RESET (P1.15): %d", gpio_pin_get_dt(&gps_reset_gpio)); } #endif #if HAS_GPS_SLEEP if (gpio_is_ready_dt(&gps_sleep_gpio)) { LOG_INF(" GPS_SLEEP_INT (P1.12): %d", gpio_pin_get_dt(&gps_sleep_gpio)); } #endif #if HAS_GPS_RTCINT if (gpio_is_ready_dt(&gps_rtcint_gpio)) { LOG_INF(" GPS_RTC_INT (P0.15): %d", gpio_pin_get_dt(&gps_rtcint_gpio)); } #endif #if HAS_GPS_RESETB if (gpio_is_ready_dt(&gps_resetb_gpio)) { LOG_INF(" GPS_RESETB (P1.14): %d (INPUT_PULLUP, expect 1)", gpio_pin_get_dt(&gps_resetb_gpio)); } #endif } #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)) { /* Device not ready — deferred init. Power up GPS then init driver. * IMPORTANT: Do NOT use uart_poll_in() here — it corrupts the * nRF52840 UARTE DMA state and breaks modem_pipe async receive. * Previous diagnostic proved GPS IS transmitting (415 bytes/2s). */ LOG_INF("GNSS device not ready — powering up for deferred init"); gps_power_control(true); #if HAS_GPS_POWER_CONTROL /* Verify GPIO states immediately after power-up */ gps_dump_gpio_states(); #endif /* Dump UART0 hardware register state (read-only, non-destructive) */ gps_uart_dump_hw_state(); /* Wait for AG3335 firmware boot before driver init. * GPS transmits boot messages + NMEA at 115200 baud during this * delay — the Zephyr UART driver handles any accumulated errors * internally when modem_pipe_open() enables the ISR. */ k_msleep(500); int ret = device_init(gnss_dev); if (ret != 0 && ret != -EALREADY) { LOG_ERR("GNSS device_init failed: %d", ret); /* Dump UART state after failure for debugging */ gps_uart_dump_hw_state(); return -ENODEV; } if (!device_is_ready(gnss_dev)) { LOG_ERR("GNSS device still not ready after deferred init"); return -ENODEV; } } 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_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"); /* Start acquiring immediately (no delay for first wake) */ gps_current_state = GPS_STATE_ACQUIRING; consecutive_good_fixes = 0; /* 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. */ /* No timeout for first fix after enable - wait as long as needed */ if (first_fix_acquired) { k_work_schedule(&gps_timeout_work, K_MSEC(gps_acquire_timeout_ms)); } else { LOG_INF("GPS: First fix mode - no timeout until first successful fix"); } } else { LOG_INF("GPS disabled - canceling timers and powering off"); /* 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_current_state = GPS_STATE_OFF; consecutive_good_fixes = 0; /* 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 if (interval < 10) interval = 10; if (interval > 86400) interval = 86400; gps_wake_interval_ms = interval * 1000U; LOG_INF("GPS poll interval set to %u seconds", interval); #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); 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) { LOG_DBG("GPS: Fresh fix requested but already acquiring"); } #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 */ } #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 }