diff --git a/zephcore/adapters/ble/ZephyrBLE.cpp b/zephcore/adapters/ble/ZephyrBLE.cpp index a1c670b..0666a3d 100644 --- a/zephcore/adapters/ble/ZephyrBLE.cpp +++ b/zephcore/adapters/ble/ZephyrBLE.cpp @@ -108,9 +108,13 @@ static void kick_tx_drain(void); /* ========== GATT Service ========== */ /* - * Secure NUS service with MITM-authenticated permissions. - * Unlike the default NUS service, this requires PIN pairing before - * any NUS communication is allowed (matches Arduino's SECMODE_ENC_WITH_MITM). + * NUS service — secured with AUTHEN permissions. + * Matches Arduino's SECMODE_ENC_WITH_MITM on bleuart. + * + * When the phone tries to subscribe (CCC write) or send data (RX write), + * Zephyr returns ATT_ERR_AUTHENTICATION. The phone's BLE stack should + * then initiate pairing (PIN dialog). After pairing succeeds, + * security_changed() fires at L3+ and the phone retries the operation. */ BT_GATT_SERVICE_DEFINE(secure_nus_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE), @@ -161,7 +165,7 @@ static void ble_tx_complete_cb(struct bt_conn *conn, void *user_data) static int secure_nus_send(struct bt_conn *conn, const void *data, uint16_t len) { struct bt_gatt_notify_params params = { - .attr = &secure_nus_svc.attrs[1], /* TX characteristic */ + .attr = &secure_nus_svc.attrs[2], /* TX characteristic value (not declaration) */ .data = data, .len = len, .func = ble_tx_complete_cb, @@ -224,7 +228,7 @@ static void connected(struct bt_conn *conn, uint8_t err) LOG_WRN("connection failed: %s err 0x%02x", addr, err); return; } - LOG_DBG("%s", addr); + LOG_INF("connected: %s", addr); current_conn = bt_conn_ref(conn); /* Cancel slow advertising work - we're connected now */ @@ -249,25 +253,27 @@ static void connected(struct bt_conn *conn, uint8_t err) } #endif - /* Proactively request authenticated encryption (MITM). - * - Bonded device: re-encrypts with stored keys (instant, no user interaction) - * - New device: triggers passkey pairing dialog on the central + /* Do NOT proactively request security here. * - * L3 = authenticated pairing with MITM protection. iOS/Android will - * negotiate Secure Connections (LESC) automatically when supported. - * L4 (SC-only) is NOT used because Windows never shows the passkey - * dialog for SC pairing — it only works with Legacy passkey entry. + * Arduino reference: the SoftDevice never sends SMP Security Request + * on connection. Pairing is triggered naturally when the phone tries + * to access a GATT characteristic with AUTHEN permissions — the stack + * returns "Insufficient Authentication" and the phone's BLE stack + * initiates pairing (PIN dialog). * - * This is needed because some BLE stacks (Windows) don't automatically - * initiate pairing when they get "Insufficient Authentication" from a - * protected characteristic. By requesting security here, all platforms - * (iOS, Android, Windows) get the pairing prompt immediately. + * Our NUS service has BT_GATT_PERM_*_AUTHEN on CCC and RX, so + * pairing triggers automatically when the MeshCore app accesses them. * - * security_changed() callback handles the rest once encryption is up. */ - int sec_err = bt_conn_set_security(conn, BT_SECURITY_L3); - if (sec_err && sec_err != -EALREADY) { - LOG_WRN("Failed to request security: %d", sec_err); - } + * The old bt_conn_set_security(L3) call sent a proactive SMP Security + * Request that the MeshCore app doesn't handle — phone would connect + * but never show a PIN dialog, causing a "freeze." + * + * For bonded reconnects, Zephyr auto-encrypts with stored keys when + * CONFIG_BT_SMP and CONFIG_BT_BONDABLE are enabled. + * + * NOTE: If Windows support is needed later, Windows may not initiate + * pairing from "Insufficient Authentication" — add a platform-specific + * Security Request path for Windows clients only. */ /* Notify main of BLE connection */ if (ble_cbs && ble_cbs->on_connected) { @@ -279,7 +285,7 @@ static void disconnected(struct bt_conn *conn, uint8_t reason) { char addr[BT_ADDR_LE_STR_LEN]; bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr)); - LOG_DBG("%s reason 0x%02x", addr, reason); + LOG_INF("disconnected: %s reason 0x%02x", addr, reason); if (conn == current_conn) { bt_conn_unref(current_conn); @@ -332,19 +338,27 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_ } LOG_INF("%s level %u", addr, level); - /* For bonded reconnects, pairing_complete is NOT called - only security_changed. - * Enable TX when we have sufficient security (level 2+ = encrypted). - * This handles both fresh pairing (pairing_complete also sets it) and - * reconnects with existing bond. + /* Enable TX when we have sufficient security (level 2+ = encrypted). + * This is the ONLY place that sets ble_tx_ready — security_changed is + * the authority. CCC subscription (secure_nus_ccc_changed) only kicks + * the TX drain; it never sets ble_tx_ready. */ if (level >= BT_SECURITY_L2 && !ble_tx_ready) { LOG_INF("security established, enabling TX"); ble_tx_ready = true; active_iface = ZEPHCORE_IFACE_BLE; - /* Request our preferred connection parameters. - * This is the single place for conn param requests - - * fires for both fresh pairing and bonded reconnects. */ + /* If CCC was already subscribed (bonded reconnect — phone writes + * CCC before security_changed fires), kick TX now. On fresh + * pairing CCC hasn't been written yet, so this is a no-op and + * TX starts when CCC fires later. */ + if (nus_notif_enabled) { + kick_tx_drain(); + } + } + + if (level >= BT_SECURITY_L2) { + /* Request our preferred connection parameters. */ struct bt_le_conn_param conn_param = { .interval_min = BLE_DEFAULT_MIN_INTERVAL, .interval_max = BLE_DEFAULT_MAX_INTERVAL, @@ -360,10 +374,6 @@ static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_ BLE_DEFAULT_MAX_INTERVAL * 5 / 4, BLE_DEFAULT_LATENCY); } - - /* Don't kick TX here — MTU exchange may not be done yet. - * TX starts when client subscribes to CCC (secure_nus_ccc_changed), - * which is the last step and implies MTU is negotiated. */ } } @@ -430,7 +440,7 @@ static struct bt_conn_auth_cb auth_cb = { static void pairing_complete(struct bt_conn *conn, bool bonded) { ARG_UNUSED(conn); - LOG_DBG("bonded=%d", bonded); + LOG_INF("pairing complete: bonded=%d", bonded); /* Switch to BLE interface (fresh pairing only). * Conn params and TX enable are handled in security_changed(), @@ -572,9 +582,13 @@ static void secure_nus_ccc_changed(const struct bt_gatt_attr *attr, uint16_t val { ARG_UNUSED(attr); bool enabled = (value == BT_GATT_CCC_NOTIFY); - LOG_DBG("enabled=%d", enabled); + LOG_INF("CCCD notif %s (value=0x%04x)", enabled ? "enabled" : "disabled", value); nus_notif_enabled = enabled; if (enabled) { + /* Kick TX drain — if security_changed already set ble_tx_ready, + * data starts flowing. If security hasn't fired yet (bonded + * reconnect race), kick_tx_drain bails harmlessly and + * security_changed will kick again once ble_tx_ready is set. */ kick_tx_drain(); } } @@ -594,7 +608,7 @@ static ssize_t secure_nus_rx_write(struct bt_conn *conn, const struct bt_gatt_at const uint8_t *data = (const uint8_t *)buf; uint8_t cmd = data[0]; - LOG_DBG("len=%u cmd=0x%02x", len, cmd); + LOG_INF("NUS RX: len=%u cmd=0x%02x", len, cmd); /* Notify main via callback */ if (ble_cbs && ble_cbs->on_rx_frame) { diff --git a/zephcore/adapters/gps/ZephyrGPSManager.cpp b/zephcore/adapters/gps/ZephyrGPSManager.cpp index dce2d57..686cac9 100644 --- a/zephcore/adapters/gps/ZephyrGPSManager.cpp +++ b/zephcore/adapters/gps/ZephyrGPSManager.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include LOG_MODULE_REGISTER(zephcore_gps, CONFIG_ZEPHCORE_GPS_LOG_LEVEL); @@ -223,6 +225,20 @@ static void gnss_data_cb(const struct device *dev, const struct gnss_data *data) 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); @@ -267,7 +283,20 @@ static void gnss_configure(void) if (ret == 0) { LOG_INF("GPS: Multi-constellation enabled"); } else if (ret == -ENOSYS || ret == -ENOTSUP) { - LOG_INF("GPS: Constellation config not supported by driver"); + /* 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 */ @@ -329,6 +358,24 @@ static const struct gpio_dt_spec gps_sleep_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps_ #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) @@ -348,8 +395,11 @@ static void gps_power_control(bool on, bool keep_vrtc = false) /* 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; NMEA data simply stops - * when power is cut and resumes when power is restored. */ + * 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()) @@ -384,6 +434,22 @@ static void gps_power_control(bool on, bool keep_vrtc = false) 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 @@ -404,6 +470,15 @@ static void gps_power_control(bool on, bool keep_vrtc = false) #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) */ @@ -426,6 +501,21 @@ static void gps_power_control(bool on, bool keep_vrtc = false) 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"); @@ -463,6 +553,16 @@ void gps_power_off_for_shutdown(void) 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 */ @@ -564,6 +664,95 @@ static void gps_timeout_work_fn(struct k_work *work) } } +/* ========== 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 @@ -590,8 +779,38 @@ static int gnss_init(void) } if (!device_is_ready(gnss_dev)) { - LOG_ERR("GNSS device %s not ready", gnss_dev->name); - return -ENODEV; + /* 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); @@ -724,8 +943,14 @@ void gps_enable(bool enable) k_work_cancel_delayable(&gps_wake_work); k_work_cancel_delayable(&gps_timeout_work); - /* Power off GPS */ - gps_power_control(false); + /* 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; diff --git a/zephcore/adapters/radio/LoRaRadioBase.cpp b/zephcore/adapters/radio/LoRaRadioBase.cpp index c43ca03..4e0fc5e 100644 --- a/zephcore/adapters/radio/LoRaRadioBase.cpp +++ b/zephcore/adapters/radio/LoRaRadioBase.cpp @@ -477,11 +477,15 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) return; } - /* RX duty cycle: radio alternates HW RX/sleep autonomously. - * During sleep windows hwGetCurrentRSSI() returns garbage (0 or very - * high) — the sampling filter rejects those, and the count>=32 - * threshold ensures we only update when enough valid samples exist. */ + /* Skip when duty cycle is active — the radio alternates between + * short RX windows and sleep. GetRssiInst sent during the sleep + * phase hangs the SPI bus (BUSY stuck high for the full 3 s timeout) + * because the chip cannot process commands while asleep. */ + if (_rx_duty_cycle_enabled) { + return; + } + int64_t start = k_uptime_get(); int sum = 0; int count = 0; for (int i = 0; i < NUM_NOISE_FLOOR_SAMPLES; i++) { @@ -494,13 +498,15 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold) count++; } } + int64_t elapsed = k_uptime_get() - start; if (count >= NUM_NOISE_FLOOR_SAMPLES / 2) { _noise_floor = sum / count; if (_noise_floor < -120) _noise_floor = -120; if (_noise_floor > -50) _noise_floor = -50; - LOG_DBG("noise floor: %d dBm (%d samples)", _noise_floor, count); } + LOG_INF("noise_floor_cal: %d samples/%d total, floor=%d, took %lld ms", + count, NUM_NOISE_FLOOR_SAMPLES, _noise_floor, elapsed); } void LoRaRadioBase::resetAGC() diff --git a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c index 1e02782..36fbbb7 100644 --- a/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c +++ b/zephcore/adapters/radio/lr11xx/lr11xx_hal_zephyr.c @@ -37,6 +37,10 @@ static void dio1_work_handler(struct k_work *work) } } +/* Track the last SPI opcode for debugging BUSY stuck */ +static uint16_t last_opcode; +static int64_t last_cmd_time; + /** * @brief Wait until BUSY pin goes low or timeout */ @@ -47,8 +51,10 @@ static lr11xx_hal_status_t wait_on_busy(struct lr11xx_hal_context *ctx) while (gpio_pin_get_dt(&ctx->busy)) { if ((k_uptime_get() - start) > LR11XX_BUSY_TIMEOUT_MS) { - LOG_ERR("BUSY timeout after %d loops (%dms)!", - loops, LR11XX_BUSY_TIMEOUT_MS); + LOG_ERR("BUSY timeout! last_op=0x%04x sent_at=%lld (%lld ms ago) DIO1=%d", + last_opcode, last_cmd_time, + k_uptime_get() - last_cmd_time, + gpio_pin_get_dt(&ctx->dio1)); return LR11XX_HAL_STATUS_ERROR; } k_busy_wait(100); /* 100us */ @@ -178,7 +184,14 @@ lr11xx_hal_status_t lr11xx_hal_write(const void *context, const uint8_t *command struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; int ret; + /* Track opcode for BUSY timeout diagnostics */ + if (command_length >= 2) { + last_opcode = ((uint16_t)command[0] << 8) | command[1]; + } + last_cmd_time = k_uptime_get(); + if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + LOG_ERR("hal_write: device not ready, op=0x%04x", last_opcode); return LR11XX_HAL_STATUS_ERROR; } @@ -222,12 +235,19 @@ lr11xx_hal_status_t lr11xx_hal_read(const void *context, const uint8_t *command, struct lr11xx_hal_context *ctx = (struct lr11xx_hal_context *)context; int ret; + /* Track opcode for BUSY timeout diagnostics */ + if (command_length >= 2) { + last_opcode = ((uint16_t)command[0] << 8) | command[1]; + } + last_cmd_time = k_uptime_get(); + /* Special case: crypto restore command needs delay */ if (command_length >= 2 && command[0] == 0x05 && command[1] == 0x0B) { k_busy_wait(1000); } if (check_device_ready(ctx) != LR11XX_HAL_STATUS_OK) { + LOG_ERR("hal_read: device not ready, op=0x%04x", last_opcode); return LR11XX_HAL_STATUS_ERROR; } diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index c188ed2..fe0bea5 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -88,6 +88,16 @@ CONFIG_BT_BUF_ACL_RX_SIZE=251 CONFIG_BT_BUF_ACL_TX_SIZE=251 CONFIG_BT_L2CAP_TX_MTU=247 +# BLE TX buffers — default 3 is too low, causes system workqueue deadlock +# when BLE activity bursts (DLE + param update + ATT) exhaust all buffers +# and bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq forever. +CONFIG_BT_BUF_ACL_TX_COUNT=7 +CONFIG_BT_L2CAP_TX_BUF_COUNT=7 +CONFIG_BT_CONN_TX_MAX=7 + +# BLE RX thread stack — default 1200 too small for ATT handlers + logging + asserts +CONFIG_BT_RX_STACK_SIZE=2048 + # Data Length Extension update support (host side) CONFIG_BT_USER_DATA_LEN_UPDATE=y @@ -116,6 +126,13 @@ CONFIG_BT_DIS_SW_REV=y CONFIG_BT_DIS_SW_REV_STR="Zephyr" CONFIG_BT_DIS_PNP=n +# Disable GATT Caching (Robust Caching / Database Hash) +# With caching enabled (Kconfig default), Zephyr silently drops ATT requests +# from "change-unaware" clients after the GATT database changes. This breaks +# service discovery on phones that enable Robust Caching (modern iOS/Android). +# Arduino SoftDevice has no GATT caching — disabling matches that behavior. +CONFIG_BT_GATT_CACHING=n + # Persistent bonds (NVS) CONFIG_BT_SETTINGS=y CONFIG_SETTINGS_RUNTIME=y @@ -143,6 +160,16 @@ CONFIG_INPUT_EVENT_DUMP=n # instead of a dedicated 1KB input thread. Saves a thread stack. CONFIG_INPUT_MODE_SYNCHRONOUS=y +# ========== Logging ========== +# RTT log backend: DROP mode (not BLOCK) — when no RTT host is connected the +# buffer is always full; BLOCK mode sleeps 20ms per message with irq_lock held, +# disrupting BLE timing and cascading into buffer exhaustion deadlocks. +CONFIG_LOG_BACKEND_RTT_MODE_DROP=y + +# Silence USB CH9 control-transfer spam (dozens of INF lines during enumeration). +# WRN keeps real USB errors visible. +CONFIG_USBD_LOG_LEVEL_WRN=y + # ========== Power Management ========== CONFIG_POWEROFF=y diff --git a/zephcore/boards/example_board/board.conf b/zephcore/boards/example_board/board.conf index 0e6c6db..45fd8b9 100644 --- a/zephcore/boards/example_board/board.conf +++ b/zephcore/boards/example_board/board.conf @@ -64,11 +64,41 @@ CONFIG_NORDIC_QSPI_NOR=n # 1. Add the display node to your board.overlay (see examples there) # 2. Set zephyr,display = &your_display; in the "chosen" block # 3. That's it — driver, CFB, and UI all auto-enable +# +# To DISABLE display on a board WITHOUT a screen (enables LED heartbeat): +# CONFIG_ZEPHCORE_UI_DISPLAY=n +# The heartbeat LED (led0 alias) only blinks when display is disabled +# (HAS_HEARTBEAT_LED requires !CONFIG_ZEPHCORE_UI_DISPLAY). +# CONFIG_ZEPHCORE_UI_DISPLAY=n # ========== OPTIONAL: Buzzer ========== # Uncomment if your board has a piezo buzzer on a PWM pin. # CONFIG_PWM=y +# ========== OPTIONAL: UART Async API (nRF52840 GPS boards) ========== +# Enable this if your board has a GPS module on UART with deferred-init +# (i.e., GPS power is controlled by external GPIOs, not the GNSS driver). +# +# WHY: The nRF52840 UARTE is DMA-based. The default ISR modem backend +# emulates byte-by-byte interrupts on top of DMA, which breaks when GPS +# data arrives before the ISR handler is enabled (GPS transmits during +# the boot delay → overrun → modem_chat sees nothing). +# +# The async backend uses native UARTE DMA, which is more robust for GPS +# modules that start transmitting before the software is ready to listen. +# +# WHEN TO ENABLE: +# - Your board uses quectel,lc76g with zephyr,deferred-init (e.g., T1000-E) +# - GPS power is managed by external GPIOs in ZephyrGPSManager +# - GPS module starts transmitting before modem_pipe_open() is called +# +# WHEN NOT NEEDED: +# - luatos,air530z driver (manages power internally via on-off-gpios) +# - GPS at 9600 baud (slow enough for ISR backend) +# - Boards where the GNSS driver controls power (no deferred-init) +# +# CONFIG_UART_ASYNC_API=y + # ========== OPTIONAL: Platform-Specific Overrides ========== # # --- nRF52840 --- diff --git a/zephcore/boards/example_board/board.overlay b/zephcore/boards/example_board/board.overlay index c378112..4e5967a 100644 --- a/zephcore/boards/example_board/board.overlay +++ b/zephcore/boards/example_board/board.overlay @@ -679,6 +679,14 @@ * * Supported: "luatos,air530z", "quectel,lc76g", "gnss-nmea-generic" * + * Two patterns depending on who controls GPS power: + * + * PATTERN A: Driver-managed power (simple, GPS at 9600 baud) + * Driver uses on-off-gpios to control power internally. + * UART ISR backend works fine (driver enables power after ISR is ready). + * No deferred-init needed, no CONFIG_UART_ASYNC_API needed. + * Example: Wio Tracker L1 (L76K via air530z driver) + * * &uart0 { * current-speed = <9600>; * gnss: gnss { @@ -686,6 +694,64 @@ * on-off-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>; * }; * }; + * + * + * PATTERN B: External GPIO power with deferred init (GPS at 115200 baud) + * GPS power is managed by ZephyrGPSManager via multiple GPIOs. + * Must use zephyr,deferred-init so driver init waits for GPS power-up. + * Must use CONFIG_UART_ASYNC_API=y in board.conf (see notes there). + * Example: T1000-E (AG3335 via lc76g driver, PAIR protocol) + * + * WHY DEFERRED INIT: The GNSS driver's init() immediately opens the + * modem pipe and sends commands. Without deferred-init, the driver + * tries to talk to the GPS before it's powered → guaranteed timeout. + * With deferred-init, ZephyrGPSManager powers up GPIO first, then + * calls device_init() to start the driver conversation. + * + * &uart0 { + * compatible = "nordic,nrf-uarte"; + * status = "okay"; + * current-speed = <115200>; + * pinctrl-0 = <&uart0_default>; + * pinctrl-1 = <&uart0_sleep>; + * pinctrl-names = "default", "sleep"; + * + * gnss: gnss { + * compatible = "quectel,lc76g"; + * pps-mode = "GNSS_PPS_MODE_DISABLED"; + * zephyr,deferred-init; + * }; + * }; + * + * + * GPS POWER GPIO ALIASES (add to aliases block above): + * For Pattern A: only gps-enable is needed (shared with on-off-gpios) + * For Pattern B: define all GPIOs your GPS module requires + * + * aliases { + * gps-enable = &gps_en_pin; Required: GPS main power + * gps-vrtc-enable = &gps_vrtc_pin; Optional: VRTC backup power (warm standby) + * gps-reset = &gps_reset_pin; Optional: GPS hardware reset + * gps-sleep-int = &gps_sleep_pin; Optional: GPS sleep/wake interrupt + * gps-rtc-int = &gps_rtcint_pin; Optional: GPS RTC interrupt (held LOW) + * gps-resetb = &gps_resetb_pin; Optional: Active-LOW reset (INPUT_PULLUP!) + * }; + * + * GPS GPIO nodes (add to root / { } block): + * + * gps_power: gps-power { + * compatible = "gpio-leds"; + * gps_en_pin: gps_en { + * gpios = <&gpio1 11 GPIO_ACTIVE_HIGH>; + * label = "GPS Enable"; + * }; + * }; + * + * IMPORTANT for AG3335-based boards (T1000-E, etc.): + * gps-resetb (active-LOW reset) MUST be configured as INPUT_PULLUP. + * Without pull-up, the pin floats LOW → AG3335 stays in permanent + * reset → zero UART output. ZephyrGPSManager handles this for + * power-on (INPUT_PULLUP) and power-off (OUTPUT_LOW). */ /* --- OLED Display on I2C (any platform with I2C) --- diff --git a/zephcore/boards/nrf52840/t1000_e/board.conf b/zephcore/boards/nrf52840/t1000_e/board.conf index f4e9361..7dd0a81 100644 --- a/zephcore/boards/nrf52840/t1000_e/board.conf +++ b/zephcore/boards/nrf52840/t1000_e/board.conf @@ -21,3 +21,11 @@ CONFIG_SPI=y # PWM for buzzer CONFIG_PWM=y + +# T1000-E has no display — disable to enable LED heartbeat +# (HAS_HEARTBEAT_LED requires !CONFIG_ZEPHCORE_UI_DISPLAY) +CONFIG_ZEPHCORE_UI_DISPLAY=n + +# Debug logging removed — GPS and BLE confirmed working at protocol level. +# Re-enable selectively with: -DCONFIG_BT_CONN_LOG_LEVEL_DBG=y etc. +# Heavy debug logging contributed to BLE freeze (stack pressure + RTT blocking). diff --git a/zephcore/boards/nrf52840/t1000_e/board.overlay b/zephcore/boards/nrf52840/t1000_e/board.overlay index 1c70a03..7ab4552 100644 --- a/zephcore/boards/nrf52840/t1000_e/board.overlay +++ b/zephcore/boards/nrf52840/t1000_e/board.overlay @@ -46,10 +46,33 @@ }; }; + /* GPS RTC interrupt - P0.15 (held LOW during normal operation) */ + gps_rtcint: gps-rtc-int { + compatible = "gpio-leds"; + gps_rtcint_pin: gps_rtc_int { + gpios = <&gpio0 15 GPIO_ACTIVE_HIGH>; + label = "GPS RTC Int"; + }; + }; + + /* GPS RESETB (active-LOW reset) - P1.14 + * Must be pulled HIGH (INPUT_PULLUP) for normal operation. + * Without pull-up, this pin floats LOW and holds the AG3335 + * in permanent reset — zero UART output! */ + gps_resetb: gps-resetb { + compatible = "gpio-leds"; + gps_resetb_pin: gps_resetb { + gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; + label = "GPS RESETB"; + }; + }; + aliases { gps-enable = &gps_enable_pin; gps-vrtc-enable = &gps_vrtc_pin; gps-reset = &gps_reset_pin; gps-sleep-int = &gps_sleep_pin; + gps-rtc-int = &gps_rtcint_pin; + gps-resetb = &gps_resetb_pin; }; }; diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts index b1ea22f..b6e97ca 100644 --- a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts @@ -202,9 +202,14 @@ pinctrl-names = "default", "sleep"; }; -/* Airoha AG3335 GNSS on UART0 - 115200 baud (PAIR protocol) - * Uses Zephyr quectel,lc76g driver (LC76G is built on AG3335, - * same PAIR command set for constellation config, fix rate, etc.) */ +/* Airoha AG3335 GNSS on UART0 - 115200 baud + * + * Using gnss-nmea-generic (passive NMEA listener) instead of quectel,lc76g: + * - Arduino reference code just listens for NMEA, never sends PAIR commands + * - AG3335 outputs NMEA autonomously at factory defaults + * - Eliminates 10s+ blocking resume_script timeout on PAIR command + * - Trade-off: no gnss_set_enabled_systems() / gnss_set_fix_rate() API + * (AG3335 defaults are fine: GPS+GLONASS+Galileo+BeiDou, 1Hz) */ &uart0 { compatible = "nordic,nrf-uarte"; status = "okay"; @@ -214,8 +219,11 @@ pinctrl-names = "default", "sleep"; gnss: gnss { - compatible = "quectel,lc76g"; - pps-mode = "GNSS_PPS_MODE_DISABLED"; + compatible = "gnss-nmea-generic"; + /* Defer driver init — AG3335 needs GPIO power-up + * (GPS_EN, GPS_VRTC_EN, GPS_RESET) before NMEA output. + * ZephyrGPSManager handles power-up then calls device_init(). */ + zephyr,deferred-init; }; }; diff --git a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c index 64c8db4..d3520ae 100644 --- a/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c +++ b/zephcore/patches/zephyr-new/drivers/lora/lr11xx/lr11xx_lora.c @@ -294,12 +294,15 @@ static void lr11xx_start_rx(struct lr11xx_data *data, { void *ctx = &data->hal_ctx; + LOG_INF("start_rx: t=%lld duty=%d", k_uptime_get(), + data->rx_duty_cycle_enabled); + /* Standby first — wake from any sleep state */ data->hal_ctx.radio_is_sleeping = true; lr11xx_status_t rc = lr11xx_system_set_standby(ctx, LR11XX_SYSTEM_STANDBY_CFG_RC); if (rc != LR11XX_STATUS_OK) { - LOG_ERR("standby failed — triggering HW reset"); + LOG_ERR("standby failed (rc=%d) — triggering HW reset", rc); lr11xx_hardware_reset(data, cfg); } @@ -340,7 +343,8 @@ static void lr11xx_dio1_work_handler(struct k_work *work) lr11xx_system_irq_mask_t irq; lr11xx_system_get_and_clear_irq_status(ctx, &irq); - LOG_DBG("DIO1 IRQ: 0x%08x tx=%d", irq, data->tx_active); + LOG_INF("DIO1 IRQ: 0x%08x tx=%d t=%lld", irq, data->tx_active, + k_uptime_get()); /* ── RX done ── */ if (irq & LR11XX_SYSTEM_IRQ_RX_DONE) { @@ -392,7 +396,8 @@ static void lr11xx_dio1_work_handler(struct k_work *work) /* ── Timeout ── */ if (irq & LR11XX_SYSTEM_IRQ_TIMEOUT) { - LOG_DBG("Timeout IRQ"); + LOG_INF("Timeout IRQ — restarting RX (duty_cycle=%d)", + data->rx_duty_cycle_enabled); if (!data->tx_active) { lr11xx_start_rx(data, cfg); } @@ -664,21 +669,19 @@ bool lr11xx_is_receiving(const struct device *dev) void lr11xx_set_rx_duty_cycle(const struct device *dev, bool enable) { struct lr11xx_data *data = dev->data; + const struct lr11xx_config *cfg = dev->config; data->rx_duty_cycle_enabled = enable; LOG_INF("RX duty cycle %s", enable ? "enabled" : "disabled"); + /* If currently in RX, restart with proper Standby transition. + * LR1110 requires Standby before SetRx or SetRxDutyCycle — + * issuing these while already in RX puts the radio in an + * undefined state where RSSI reads work but packet detection + * is broken (zero DIO1 IRQs). */ if (data->in_rx_mode) { k_mutex_lock(&data->spi_mutex, K_FOREVER); - if (enable) { - lr11xx_apply_rx_duty_cycle(data); - } else { - lr11xx_radio_set_rx(&data->hal_ctx, 0xFFFFFF); - if (data->rx_boost_enabled) { - lr11xx_radio_cfg_rx_boosted(&data->hal_ctx, - true); - } - } + lr11xx_start_rx(data, cfg); k_mutex_unlock(&data->spi_mutex); } } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 258d984..317ad88 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -489,6 +489,7 @@ int main(void) companion_mesh.prefs.rx_delay_base = 0.0f; /* Disabled for companion */ companion_mesh.prefs.airtime_factor = 10.0f; /* 10% duty cycle (EU 868 default) */ companion_mesh.prefs.rx_duty_cycle = 1; /* Companions: duty cycle ON by default (power save) */ + companion_mesh.prefs.rx_boost = 1; /* Default: boosted RX (+3dB sensitivity, +2mA) */ /* Generate default node name from hardware device ID */ uint8_t dev_id[8];