Fix peripheral power shutdown and auto-detect Station G3 radio slots
This commit is contained in:
1 parent
c6e43c5530
commit
8eced48d34
103 files changed
+4900
-194
No files matched your search
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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, ðernet_legacy_task_handle) != pdPASS) {
|
||||
ethernet_legacy_worker_active.store(false);
|
||||
ethernet_legacy_task_handle = nullptr;
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user