Fix peripheral power shutdown and auto-detect Station G3 radio slots

This commit is contained in:
mikecarper committed 2026-10-15 22:30:37 -07:00
1 parent c6e43c5530
commit 8eced48d34
103 files changed
+4900 -194

No files matched your search

+12 -8
View File
@@ -1,4 +1,5 @@
#ifdef ESP_PLATFORM
#include <helpers/ESP32GpioHold.h>
#include "ESP32Board.h"
#include "WirelessControl.h"
@@ -1285,20 +1286,22 @@ void ESP32Board::shutdownPeripherals() {
// Power off LoRa
radio_driver.powerOff();
#ifdef P_LORA_NSS
// Keep LoRa inactive during deepsleep
digitalWrite(P_LORA_NSS, HIGH);
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
gpio_hold_en((gpio_num_t)P_LORA_NSS);
#else
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
#endif
#if defined(STATION_G3_ESP32)
mesh::esp32::holdOutputForDeepSleep(
(gpio_num_t)station_g3::activePins().nss, HIGH);
#elif defined(P_LORA_NSS)
// Keep LoRa deselected even when NSS is not an RTC pad (classic GPIO18,
// Station G3 slot2 GPIO43). Configure OUTPUT even after partial radio init.
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_NSS, HIGH);
#endif
// Power off GPS if any
if (sensors.getLocationProvider() != NULL) {
sensors.getLocationProvider()->stop();
}
// A stopped receiver may already have lost its rail. Avoid latching an
// idle-high UART output into it when digital deep-sleep auto-hold is armed.
sensors.shutdownGpsTransportForDeepSleep();
// Native USB must not wait for a host which has stopped reading.
// Keep the original flush behavior for UART targets.
@@ -1326,6 +1329,7 @@ void ESP32Board::enterDeepSleep(uint32_t secs) {
}
// Finally set ESP32 into deepsleep
mesh::esp32::commitTerminalDeepSleepHolds();
esp_deep_sleep_start(); // CPU halts here and never returns!
}
#endif
+14
View File
@@ -16,8 +16,12 @@
#include "esp_system.h"
#include <driver/rtc_io.h>
#include "ESP32TrueRandom.h"
#include "ESP32GpioHold.h"
#include "UsbLogging.h"
#include "UsbHostSleepPolicy.h"
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
#ifndef MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS
#define MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS 120000UL
@@ -53,6 +57,16 @@ protected:
public:
void begin() {
// Digital deep-sleep auto-hold survives wake. Clear it before runtime,
// restoring the dedicated radio select before releasing its per-pin hold.
mesh::esp32::resetTerminalDeepSleepHolds();
#if defined(STATION_G3_ESP32)
mesh::esp32::releaseOutputAfterDeepSleep(
(gpio_num_t)station_g3::activePins().nss, HIGH);
#elif defined(P_LORA_NSS)
mesh::esp32::releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_NSS, HIGH);
#endif
// Arduino's early init hook normally captured this before initVariant().
// Keep this idempotent fallback before this class touches ADC peripherals.
mesh::initializeESP32TrueRandom();
+105
View File
@@ -0,0 +1,105 @@
#pragma once
#include <driver/gpio.h>
#include <driver/rtc_io.h>
namespace mesh {
namespace esp32 {
// The RTC pad map differs by chip and board pin choice. NSS18 on classic
// ESP32 and NSS43/LNA42 on S3, for example, are ordinary digital GPIOs.
// These RTC-only wrappers are also used outside terminal shutdown. Do not
// replace them with digital holds: that would freeze live peripheral pins.
inline bool supportsRtcGpio(gpio_num_t pin) {
#if defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) && SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
return static_cast<int>(pin) >= 0 && rtc_gpio_is_valid_gpio(pin);
#else
(void)pin;
return false;
#endif
}
inline esp_err_t holdRtcGpioIfSupported(gpio_num_t pin) {
#if defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) && SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
if (supportsRtcGpio(pin)) return rtc_gpio_hold_en(pin);
#else
(void)pin;
#endif
return ESP_OK;
}
inline esp_err_t releaseRtcGpioIfSupported(gpio_num_t pin) {
#if defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) && SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
if (supportsRtcGpio(pin)) return rtc_gpio_hold_dis(pin);
#else
(void)pin;
#endif
return ESP_OK;
}
inline esp_err_t deinitRtcGpioIfSupported(gpio_num_t pin) {
#if defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) && SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
if (supportsRtcGpio(pin)) return rtc_gpio_deinit(pin);
#else
(void)pin;
#endif
return ESP_OK;
}
// An inline function's local static is shared across translation units. A
// namespace-static flag here would silently give each board/FEM its own copy.
inline bool& terminalDigitalHoldPending() {
static bool pending = false;
return pending;
}
inline esp_err_t configureHeldOutput(gpio_num_t pin, uint32_t level) {
if (static_cast<int>(pin) < 0 || !GPIO_IS_VALID_OUTPUT_GPIO(pin)) {
return ESP_ERR_INVALID_ARG;
}
// Preload the latch before enabling the output. gpio_config also selects
// GPIO rather than a boot-time/peripheral IOMUX function after partial init.
esp_err_t result = gpio_set_level(pin, level ? 1 : 0);
if (result != ESP_OK) return result;
gpio_config_t config = {};
config.pin_bit_mask = 1ULL << static_cast<unsigned>(pin);
config.mode = GPIO_MODE_OUTPUT;
config.pull_up_en = GPIO_PULLUP_DISABLE;
config.pull_down_en = GPIO_PULLDOWN_DISABLE;
config.intr_type = GPIO_INTR_DISABLE;
return gpio_config(&config);
}
// Terminal shutdown only: explicitly select a known output before latching
// it. The IDF GPIO API selects the correct RTC/digital hold implementation.
inline esp_err_t holdOutputForDeepSleep(gpio_num_t pin, uint32_t level) {
esp_err_t result = configureHeldOutput(pin, level);
if (result != ESP_OK) return result;
result = gpio_hold_en(pin);
if (result == ESP_OK && !supportsRtcGpio(pin)) {
terminalDigitalHoldPending() = true;
}
return result;
}
inline esp_err_t releaseOutputAfterDeepSleep(gpio_num_t pin, uint32_t level) {
// IDF requires mode/value to be restored BEFORE hold_dis to avoid a low
// glitch on wake. This also covers software resets while a hold was active.
esp_err_t result = configureHeldOutput(pin, level);
return result == ESP_OK ? gpio_hold_dis(pin) : result;
}
inline void resetTerminalDeepSleepHolds() {
gpio_deep_sleep_hold_dis();
terminalDigitalHoldPending() = false;
}
inline void commitTerminalDeepSleepHolds() {
// IDF4.4's digital auto-hold affects all digital pads DURING deep sleep.
// Arm it only after every virtual shutdown hook and transport quiescence,
// immediately before terminal deep sleep; never in runtime or light sleep.
if (terminalDigitalHoldPending()) gpio_deep_sleep_hold_en();
}
} // namespace esp32
} // namespace mesh
+6
View File
@@ -162,9 +162,15 @@ public:
return false;
}
virtual LocationProvider* getLocationProvider() { return NULL; }
// Terminal deep sleep only. Providers may cut power without ending their
// UART; a concrete manager can release its own transport before pad hold.
virtual void shutdownGpsTransportForDeepSleep() { }
// WisBlock slot power is a boot-time choice, separate from logical GPS on/off.
virtual bool supportsSharedPeripheralMode() const { return false; }
virtual void configureSharedPeripheralMode(bool shared) { (void)shared; }
// Terminal rail removal must not interrupt external storage or its retained
// recovery state. Unknown managers/discovery fail closed; no probing here.
virtual bool canRemoveSharedStoragePowerForDeepSleep() const { return false; }
virtual bool isGpsPowered() const {
LocationProvider* location = const_cast<SensorManager*>(this)->getLocationProvider();
return location != NULL && location->isEnabled();
+9 -1
View File
@@ -4,6 +4,9 @@
#include <stdint.h>
#include "sensors/EnvironmentI2CConfig.h"
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
// Simulator targets do not include RadioLib, but board pin macros can still
// use its sentinel value. Keep the fallback local to this header.
@@ -14,10 +17,15 @@
// This header is intentionally included only after target.h. The target's
// build-time pin definitions are the authoritative list of GPIOs claimed by
// the firmware in that particular image.
// the firmware, except boot-selectable G3 slots which reserve both pin sets.
namespace UserGpioPinPolicy {
inline bool isFirmwareReserved(uint8_t pin) {
#if defined(STATION_G3_ESP32)
// A module may be present in the inactive slot too. Never offer its pins
// for user GPIO, even when the current boot selected the other slot.
if (station_g3::isRadioPin(pin)) return true;
#endif
// RadioLib 7 represents its disconnected-pin sentinel as unsigned
// 0xFFFFFFFF. Use a signed type wide enough for that value as well as the
// board definitions that use -1, then reject both forms with the 0..63
+3 -2
View File
@@ -1,4 +1,5 @@
#if defined(TBEAM_SUPREME_SX1262) || defined(TBEAM_SX1262) || defined(TBEAM_SX1276)
#include <helpers/ESP32GpioHold.h>
#include <Arduino.h>
#include "TBeamBoard.h"
@@ -50,8 +51,8 @@ void TBeamBoard::begin() {
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
mesh::esp32::releaseRtcGpioIfSupported((gpio_num_t)P_LORA_NSS);
mesh::esp32::deinitRtcGpioIfSupported((gpio_num_t)P_LORA_DIO_1);
}
}
@@ -1,5 +1,7 @@
#include "RAK13800EthernetInterface.h"
#include "../../nrf52/EthernetMac.h"
#include "../../nrf52/TerminalPeripheralShutdown.h"
#include "../../nrf52/EthernetCliTransmit.h"
#include <SPI.h>
#include <EthernetUdp.h>
@@ -14,11 +16,10 @@ SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI
#define PIN_ETHERNET_SS 26
bool RAK13800EthernetInterface::begin() {
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset — the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
if (_terminalStopping) return false;
if (!mesh::nrf52::registerTerminalEthernetShutdown(terminalShutdown, this)) return false;
// Preserve existing startup/link timing. PoE supplies separate 5V through
// the base's VCC_IN; WB_IO2 only supplies the W5100S's switched 3V3_S.
#ifdef PIN_ETHERNET_RESET
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
@@ -36,6 +37,7 @@ bool RAK13800EthernetInterface::begin() {
mac[4],
mac[5]);
ETHERNET_SPI_PORT.begin();
_spiStarted = true;
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
// Use static IP if build flags are defined, otherwise DHCP
@@ -71,15 +73,39 @@ bool RAK13800EthernetInterface::begin() {
return true;
}
bool RAK13800EthernetInterface::terminalShutdown(void* context, uint32_t timeout_ms) {
return static_cast<RAK13800EthernetInterface*>(context)->shutdownForPowerOff(timeout_ms);
}
bool RAK13800EthernetInterface::shutdownForPowerOff(uint32_t timeout_ms) {
(void)timeout_ms; // All operations below are finite register transfers.
_terminalStopping = true;
// disable() cancels framed/session producers, but disconnectClient() must
// not enter the vendor socket-stop/ACK wait during terminal shutdown.
SerialEthernetInterface::disable();
if (!_spiStarted) return true;
for (uint8_t socket = 0; socket < 4; ++socket) {
(void)mesh::nrf52::EthernetCliTransmit::close(socket);
}
client = EthernetClient();
ETHERNET_SPI_PORT.end();
_spiStarted = false;
mesh::nrf52::releaseRakEthernetBusPads();
return true;
}
int RAK13800EthernetInterface::available() {
if (!_spiStarted || _terminalStopping) return 0;
return client.available();
}
int RAK13800EthernetInterface::read() {
if (!_spiStarted || _terminalStopping) return -1;
return client.read();
}
size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) {
if (!_spiStarted || _terminalStopping) return 0;
return client.write(buf, size);
}
@@ -89,11 +115,11 @@ bool RAK13800EthernetInterface::isConnected() const {
void RAK13800EthernetInterface::disconnectClient() {
_isConnected = false;
client.stop();
if (_spiStarted && !_terminalStopping) client.stop();
}
void RAK13800EthernetInterface::loop() {
if (!_spiStarted || _terminalStopping) return;
Ethernet.maintain();
if (!isEnabled()) return;
if (_isConnected && !client.connected()) {
@@ -7,11 +7,15 @@
class RAK13800EthernetInterface : public SerialEthernetInterface {
bool _isConnected;
bool _spiStarted = false;
bool _terminalStopping = false;
EthernetServer server;
EthernetClient client;
protected:
void disconnectClient() override;
bool shutdownForPowerOff(uint32_t timeout_ms);
static bool terminalShutdown(void* context, uint32_t timeout_ms);
public:
RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) {
+64 -18
View File
@@ -7,9 +7,8 @@
#include <RAK13800_W5100S.h>
#include <helpers/nrf52/EthernetMac.h>
#include <helpers/UsbLogging.h>
#if defined(RAK4631_COMBINED_ETHERNET)
#include <atomic>
#endif
#include "TerminalPeripheralShutdown.h"
#include <atomic>
// Ethernet startup runs on a FreeRTOS task, and client acceptance runs in the
// mesh loop. Never let either path wait on a CDC host which stopped reading.
@@ -44,11 +43,7 @@ static EthernetServer ethernet_server(ETHERNET_TCP_PORT);
#if !defined(RAK4631_COMBINED_ETHERNET)
static EthernetClient ethernet_client;
#endif
#if defined(RAK4631_COMBINED_ETHERNET)
static std::atomic<bool> ethernet_running{false};
#else
static volatile bool ethernet_running = false;
#endif
#if defined(RAK4631_COMBINED_ETHERNET)
static std::atomic<bool> ethernet_session_reset{false};
static bool ethernet_line_discarding = false; // Main-thread framing state.
@@ -72,19 +67,58 @@ static void ethernet_check_client();
#include "EthernetCliRuntime.h"
#else
static std::atomic<bool> ethernet_legacy_stop{false};
static std::atomic<bool> ethernet_legacy_worker_active{false};
static bool ethernet_legacy_spi_started = false; // Published by worker completion.
static TaskHandle_t ethernet_legacy_task_handle = nullptr;
static void ethernet_legacy_finish_task() {
// No more hardware accesses after publication. This task deletes itself;
// terminal shutdown must never force-delete it inside a vendor SPI call.
taskENTER_CRITICAL();
ethernet_legacy_worker_active.store(false);
ethernet_legacy_task_handle = nullptr;
taskEXIT_CRITICAL();
vTaskDelete(nullptr);
}
static bool ethernet_terminal_shutdown(void*, uint32_t timeout_ms) {
ethernet_legacy_stop.store(true);
ethernet_running = false;
taskENTER_CRITICAL();
if (ethernet_legacy_task_handle) xTaskNotifyGive(ethernet_legacy_task_handle);
taskEXIT_CRITICAL();
const uint32_t started = millis();
while (ethernet_legacy_worker_active.load()) {
if (static_cast<uint32_t>(millis() - started) >= timeout_ms) return false;
vTaskDelay(pdMS_TO_TICKS(1));
}
ethernet_running = false;
// Main-loop client/maintain calls are now gated and the worker has no more
// hardware work. Do not call client.stop(): vendor ACK waits are unbounded.
ethernet_client = EthernetClient();
ethernet_session_reset = true;
if (ethernet_legacy_spi_started) {
ETHERNET_SPI_PORT.end();
ethernet_legacy_spi_started = false;
mesh::nrf52::releaseRakEthernetBusPads();
}
return true;
}
// FreeRTOS task: handles hw init, DHCP, and retries in the background
static void ethernet_task(void* param) {
(void)param;
if (ethernet_legacy_stop.load()) { ethernet_legacy_finish_task(); return; }
ETHERNET_CLI_LOG("Initializing hardware");
// WB_IO2 (power enable) is already driven HIGH by early constructor
// in RAK4631Board.cpp to support POE boot.
// Skip hardware reset - the W5100S comes out of power-on reset cleanly,
// and toggling reset kills the PHY link which breaks POE power.
// WB_IO2 supplies the Ethernet controller, not PoE's separate 5V input.
// Retain the existing no-reset startup so the PHY link is not disturbed.
pinMode(PIN_ETHERNET_RESET, OUTPUT);
digitalWrite(PIN_ETHERNET_RESET, HIGH);
ETHERNET_SPI_PORT.begin();
ethernet_legacy_spi_started = true;
Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS);
uint8_t mac[6];
@@ -93,12 +127,14 @@ static void ethernet_task(void* param) {
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Retry loop: keep trying until we get an IP
while (!ethernet_running) {
while (!ethernet_running && !ethernet_legacy_stop.load()) {
ETHERNET_CLI_LOG("Attempting DHCP...");
if (Ethernet.begin(mac, 10000, 2000) == 0) {
const bool connected = Ethernet.begin(mac, 10000, 2000) != 0;
if (ethernet_legacy_stop.load()) break;
if (!connected) {
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
ETHERNET_CLI_LOG("Hardware not found, giving up");
vTaskDelete(NULL);
ethernet_legacy_finish_task();
return;
}
if (Ethernet.linkStatus() == LinkOFF) {
@@ -106,7 +142,7 @@ static void ethernet_task(void* param) {
} else {
ETHERNET_CLI_LOG("DHCP failed, will retry");
}
vTaskDelay(pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS));
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(ETHERNET_RETRY_INTERVAL_MS));
continue;
}
@@ -114,15 +150,25 @@ static void ethernet_task(void* param) {
ETHERNET_CLI_LOG("IP: %u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
ETHERNET_CLI_LOG("Listening on TCP port %d", ETHERNET_TCP_PORT);
ethernet_server.begin();
ethernet_running = true;
taskENTER_CRITICAL();
if (!ethernet_legacy_stop.load()) ethernet_running = true;
taskEXIT_CRITICAL();
}
// DHCP succeeded, task is done
vTaskDelete(NULL);
ethernet_legacy_finish_task();
}
static void ethernet_start_task() {
xTaskCreate(ethernet_task, "eth_init", 1024, NULL, 1, NULL);
if (ethernet_legacy_worker_active.load() || ethernet_running || ethernet_legacy_stop.load()) return;
if (!mesh::nrf52::registerTerminalEthernetShutdown(ethernet_terminal_shutdown, nullptr)) return;
taskENTER_CRITICAL();
ethernet_legacy_worker_active.store(true);
if (xTaskCreate(ethernet_task, "eth_init", 1024, nullptr, 1, &ethernet_legacy_task_handle) != pdPASS) {
ethernet_legacy_worker_active.store(false);
ethernet_legacy_task_handle = nullptr;
}
taskEXIT_CRITICAL();
}
#endif
+34
View File
@@ -32,6 +32,7 @@ static TaskHandle_t ethernet_task_handle = nullptr;
static bool ethernet_bus_reserved = false;
static bool ethernet_spi_started = false;
static bool ethernet_hardware_found = false;
static bool ethernet_terminal_pads_released = false; // Main-thread, per bus acquisition.
static EthernetClient ethernet_raw_client;
static std::atomic<bool> ethernet_client_attached{false};
static std::atomic<uint32_t> ethernet_session_generation{0};
@@ -194,9 +195,11 @@ public:
using Print::write;
};
static EthernetCliClient ethernet_client;
static bool ethernet_terminal_shutdown(void*, uint32_t timeout_ms);
static bool ethernet_configure(const EthernetCliHooks& hooks, bool saved_enabled = false) {
if (ethernet_bus_reserved || ethernet_worker_active.load()) return false;
if (!mesh::nrf52::registerTerminalEthernetShutdown(ethernet_terminal_shutdown, nullptr)) return false;
ethernet_hooks = hooks;
ethernet_enabled.store(saved_enabled);
ethernet_state.store(EthernetCliState::Off);
@@ -253,6 +256,36 @@ static bool ethernet_complete_release() {
return true;
}
static bool ethernet_terminal_shutdown(void*, uint32_t timeout_ms) {
// This is a terminal lifecycle request, not a saved eth.off preference.
// A blocked DHCP/vendor call keeps its live bus and supply on until it
// cooperatively finishes; never delete that task or remap its pads early.
const bool had_bus = ethernet_bus_reserved || ethernet_spi_started || ethernet_worker_active.load();
ethernet_enabled.store(false);
ethernet_running.store(false);
ethernet_state.store(EthernetCliState::Stopping);
ethernet_notify_worker();
const uint32_t started = millis();
for (;;) {
if (!ethernet_worker_active.load() || ethernet_worker_stopped.load()) {
if (had_bus && !ethernet_terminal_pads_released) {
// The stopped worker has ended its SPI controller. Release Ethernet
// pads before the OTA hook can reacquire those nets for storage.
mesh::nrf52::releaseRakEthernetBusPads();
ethernet_terminal_pads_released = true;
}
// Includes synchronous task reclamation and the OTA-owner release hook
// (which can restore the radio). It must precede board common shutdown.
if (ethernet_complete_release()) {
ethernet_state.store(EthernetCliState::Off);
return true;
}
}
if (static_cast<uint32_t>(millis() - started) >= timeout_ms) return false;
vTaskDelay(pdMS_TO_TICKS(1));
}
}
static void ethernet_worker_take_access() {
while (ethernet_spi_access.load() != EthernetSpiAccess::Worker) {
EthernetSpiAccess expected = EthernetSpiAccess::Idle;
@@ -462,6 +495,7 @@ static bool ethernet_request_start(char* reply, bool persist) {
return false;
}
ethernet_bus_reserved = true;
ethernet_terminal_pads_released = false;
ethernet_tx_queue = static_cast<uint8_t*>(malloc(ETHERNET_CLI_TX_BUFFER_BYTES));
if (!ethernet_tx_queue) {
(void)ethernet_complete_release();
@@ -0,0 +1,53 @@
#pragma once
#include <Arduino.h>
namespace mesh { namespace nrf52 {
// Registration and shutdown run on the application thread, never from an ISR.
// An inline function-local static is one shared object across translation
// units; a header-level static would silently separate the board and owner.
struct TerminalEthernetOwner {
bool (*stop)(void*, uint32_t) = nullptr;
void* context = nullptr;
};
inline TerminalEthernetOwner& terminalEthernetOwner() {
static TerminalEthernetOwner owner;
return owner;
}
inline bool registerTerminalEthernetShutdown(bool (*stop)(void*, uint32_t), void* context) {
#if defined(RAK_3401)
// RAK13302 and RAK13800 occupy the same WisBlock IO slot and share SPI
// SCK/MISO/MOSI/CS; Ethernet RESET also aliases the radio FEM enable IO3.
// No runtime arbitration exists for this electrically conflicting pair.
(void)stop;
(void)context;
return false;
#else
if (!stop) return false;
auto& owner = terminalEthernetOwner();
if (owner.stop && (owner.stop != stop || owner.context != context)) return false;
owner.stop = stop;
owner.context = context;
return true;
#endif
}
inline bool terminalEthernetShutdown(uint32_t timeout_ms = 500) {
const auto& owner = terminalEthernetOwner();
return !owner.stop || owner.stop(owner.context, timeout_ms);
}
// Only after the actual SPI owner has stopped/ended its controller. These
// WisBlock nets otherwise retain high CS/reset/MOSI latches into 3V3_S loads.
inline void releaseRakEthernetBusPads() {
pinMode(3, INPUT); // SCK
pinMode(29, INPUT); // MISO
pinMode(30, INPUT); // MOSI
pinMode(26, INPUT); // CS
pinMode(21, INPUT); // W5100S reset (IO3)
}
} } // namespace mesh::nrf52
+18
View File
@@ -9,6 +9,12 @@
#define LR2021_TX_BUSY_TIMEOUT_MS 1000UL
#endif
// SIMO needs a fitted external inductor. Opt in only for verified board
// layouts; LR2021 also supports LDO-only designs without that circuitry.
#ifndef LR2021_USE_DCDC
#define LR2021_USE_DCDC 0
#endif
class CustomLR2021 : public LR2021 {
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
@@ -64,6 +70,11 @@ class CustomLR2021 : public LR2021 {
const uint8_t gain = mesh::lr2021::isHighBand(frequency_mhz) ? gainModeHf : gainModeLf;
int16_t status = LR2021::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
// Native modem initialization resets the regulator to LDO. Verified
// SIMO boards must restore DC/DC before admitting RX after any reset.
#if LR2021_USE_DCDC
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
#endif
if (status == RADIOLIB_ERR_NONE) status = restoreRfSwitchTable();
if (status == RADIOLIB_ERR_NONE) status = LR2021::setRxBoostedGainMode(gain);
if (status == RADIOLIB_ERR_NONE) {
@@ -115,6 +126,13 @@ class CustomLR2021 : public LR2021 {
return false; // fail
}
#if LR2021_USE_DCDC
// Also reached by radioDeepInit() when restoring LoRa after Morse RX.
// Check this before later setters can overwrite a regulator error.
status = setRegulatorDCDC();
if (status != RADIOLIB_ERR_NONE) return false;
#endif
status = setCRC(2);
if (status != RADIOLIB_ERR_NONE) return false;
status = explicitHeader();
+11 -1
View File
@@ -6,6 +6,9 @@
#include "RXPowerSaving.h"
#include "SX1262ProfileSwitchState.h"
#include "SX126xMorseRssi.h"
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
#ifndef MC_SX1262_FAST_PROFILE_SWITCH
// CustomSX1262 targets use direct radio I/O unless a board opts out. Keep
@@ -372,7 +375,14 @@ class CustomSX1262 : public SX1262 {
MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC");
#if defined(P_LORA_SCLK)
#if defined(STATION_G3_ESP32)
// Reinitialization/watchdog recovery must keep the boot-selected slot;
// the build's historical slot macros are not the live SPI routing.
if (spi) {
const auto& pins = station_g3::activePins();
spi->begin(pins.sck, pins.miso, pins.mosi);
}
#elif defined(P_LORA_SCLK)
#ifdef NRF52_PLATFORM
if (spi) { spi->setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); spi->begin(); }
#elif defined(RP2040_PLATFORM)
+9
View File
@@ -9,6 +9,9 @@
#include "RadioAirtime.h"
#include "RXPowerSaving.h"
#include "HilTxFrequencyGuard.h"
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
// Fallback RX powersaving timings, only used until setRxPowerSaving() is called
// (begin() always applies the persisted values). The authoritative defaults live
@@ -201,6 +204,11 @@ protected:
return estimator.samplesRemaining() * (cycle_us / 1000000.0f);
}
#if defined(STATION_G3_ESP32)
static bool hasDirectRadioResetPin() {
return station_g3::activePins().reset != static_cast<uint8_t>(RADIOLIB_NC);
}
#else
static constexpr bool hasDirectRadioResetPin() {
#if defined(P_LORA_RESET)
return P_LORA_RESET != RADIOLIB_NC;
@@ -208,6 +216,7 @@ protected:
return false;
#endif
}
#endif
bool prepareRadioHardReset() {
if (_board->supportsRadioHardReset()) return _board->resetRadio();
// A direct NRST is pulsed by the RadioLib begin() call that follows.
@@ -1222,6 +1222,12 @@ void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
void EnvironmentSensorManager::finishBasicGpsDiscovery(bool found) {
gps_discovery_pending = false;
gps_detected = found;
#if defined(ESP32_PLATFORM)
// Relinquish the failed probe's UART while ownership is still known. Leaving
// its idle-high TX installed after the receiver rail is cut can back-power
// the receiver, especially when terminal deep sleep retains digital pads.
if (!found) shutdownGpsTransportForDeepSleep();
#endif
gps_serial_transport = found;
if (found) {
MESH_DEBUG_PRINTLN("GPS detected");
@@ -1388,6 +1394,19 @@ void EnvironmentSensorManager::configureSharedPeripheralMode(bool shared) {
#endif
}
bool EnvironmentSensorManager::canRemoveSharedStoragePowerForDeepSleep() const {
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
// Only the boot-frozen, read-only ownership result is relevant. Do not
// re-probe NOR, cancel a stage, reset a store, or assume unknown WIP ended.
// Radio and Ethernet are separately quiesced by the terminal board hook.
const uint8_t stopped_owners = mesh::gps::RadioPower | mesh::gps::EthernetPower;
return _wisblock_power.isConfigured() && !_wisblock_power.discoveryActive()
&& (_wisblock_power.holds() & static_cast<uint8_t>(~stopped_owners)) == 0;
#else
return false;
#endif
}
bool EnvironmentSensorManager::isGpsPowered() const {
#ifdef RAK_WISBLOCK_GPS
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
@@ -1669,6 +1688,22 @@ bool EnvironmentSensorManager::gpsUsesSerialUart(uint8_t uart) const {
#endif
}
void EnvironmentSensorManager::shutdownGpsTransportForDeepSleep() {
#if defined(ESP32_PLATFORM) && ENV_INCLUDE_GPS \
&& defined(PIN_GPS_TX) && PIN_GPS_TX >= 0 \
&& defined(PIN_GPS_RX) && PIN_GPS_RX >= 0
// gpsUsesSerialUart stays true while a bridge temporarily owns the UART.
// Do not end that owner's transport or touch an I2C/shared peripheral bus.
if (gps_serial_transport && !gps_serial_transport_blocked) {
if (Serial1) Serial1.end();
// The Arduino pin names are from the receiver's perspective on several
// boards. Release BOTH pads rather than assuming which one drives TX.
pinMode(PIN_GPS_TX, INPUT);
pinMode(PIN_GPS_RX, INPUT);
}
#endif
}
bool EnvironmentSensorManager::gpsSerialTransportMayConflict(
uint8_t uart) const {
#if ENV_INCLUDE_GPS
@@ -102,9 +102,11 @@ public:
void setPowerSavingEnabled(bool enabled) override;
bool supportsSharedPeripheralMode() const override;
void configureSharedPeripheralMode(bool shared) override;
bool canRemoveSharedStoragePowerForDeepSleep() const override;
bool isGpsPowered() const override;
bool gpsUsesSerialUart(uint8_t uart) const override;
bool gpsSerialTransportMayConflict(uint8_t uart) const override;
bool gpsSerialTransportCanYield(uint8_t uart) const override;
bool setGpsSerialTransportBlocked(uint8_t uart, bool blocked) override;
void shutdownGpsTransportForDeepSleep() override;
};
@@ -17,9 +17,10 @@ enum WisBlockRailHold : uint8_t {
ExternalStorage = 1U << 2,
};
// These are hardware dependencies, not whether the corresponding service is
// enabled at runtime. In particular, removing Ethernet power can brown out a
// PoE-powered core even when its network service is disabled.
// Conservative live-service/bus ownership holds, not proof of physical module
// presence. Ethernet builds may still have an active worker or retained bus;
// keep their normal runtime policy until an explicit owner-aware handoff.
// RAK13800 PoE's separate 5V input does not depend on this switched 3V3_S rail.
constexpr uint8_t wisBlockCompileRailHolds() {
return 0
#if defined(RAK_3401)
+40 -4
View File
@@ -3,13 +3,26 @@
#include "DRV2605Vibration.h"
#include <Wire.h>
namespace {
// TI DRV2605L datasheet 8.4.1.4: software standby retains configuration and
// I2C access. Idle INTTRIG/RTP mode alone does not enter this low-power state.
constexpr uint8_t Standby = 0x40;
constexpr uint32_t EffectPollMillis = 20;
}
void DRV2605Vibration::begin() {
_ready = false;
_effect_active = _rtp_active = false;
_last_trigger = 0;
#ifdef PIN_DRV_EN
pinMode(PIN_DRV_EN, OUTPUT);
digitalWrite(PIN_DRV_EN, HIGH); // power up the haptic driver
delay(10);
#endif
if (!drv.begin(&Wire)) {
#ifdef PIN_DRV_EN
digitalWrite(PIN_DRV_EN, LOW);
#endif
return; // no haptic driver found, stay silent
}
#ifdef DRV2605_USE_LRA
@@ -17,7 +30,7 @@ void DRV2605Vibration::begin() {
drv.writeRegister8(DRV2605_REG_FEEDBACK, 0xB6);
#endif
drv.selectLibrary(1);
drv.setMode(DRV2605_MODE_INTTRIG);
drv.setMode(Standby);
_ready = true;
}
@@ -32,23 +45,46 @@ void DRV2605Vibration::trigger(bool force) {
drv.setWaveform(2, DRV2605_EFFECT);
drv.setWaveform(3, 0); // end of sequence
drv.go();
_effect_active = true;
_rtp_active = false;
_last_effect_check = now;
}
void DRV2605Vibration::loop() {
if (!_ready || !_effect_active) return;
const uint32_t now = millis();
if (now - _last_effect_check < EffectPollMillis) return;
_last_effect_check = now;
// GO self-clears only after the complete waveform sequence. Do not infer
// completion from a fixed duration or poll GO in RTP mode (datasheet 8.4.3).
if ((drv.readRegister8(DRV2605_REG_GO) & 1) == 0) {
drv.setMode(Standby);
_effect_active = false;
}
}
bool DRV2605Vibration::isVibrating() {
return false; // effects are short, treat as instantaneous
return _effect_active || _rtp_active;
}
void DRV2605Vibration::stop() {
if (_ready) drv.stop();
if (!_ready) return;
drv.stop();
drv.setRealtimeValue(0);
drv.setMode(Standby);
_effect_active = _rtp_active = false;
}
void DRV2605Vibration::pulse(bool on) {
if (!_ready) return;
if (!on) {
stop();
return;
}
drv.setMode(DRV2605_MODE_REALTIME);
drv.setRealtimeValue(on ? 100 : 0);
drv.setRealtimeValue(100);
_effect_active = false;
_rtp_active = true;
}
#endif // ifdef HAS_DRV2605
+4 -1
View File
@@ -23,7 +23,7 @@ class DRV2605Vibration {
public:
void begin(); // power up and init the DRV2605
void trigger(bool force = false); // trigger vibration; force skips the cooldown
void loop(); // no-op, the DRV2605 plays effects autonomously
void loop(); // return to standby after autonomous effects complete
bool isVibrating();
void stop(); // stop vibration immediately
void pulse(bool on); // continuous RTP amplitude for scheduled ms pulses
@@ -34,6 +34,9 @@ private:
Adafruit_DRV2605 drv;
bool _ready = false;
bool _quiet = false;
bool _effect_active = false;
bool _rtp_active = false;
uint32_t _last_effect_check = 0;
unsigned long _last_trigger = 0;
};