fix: sd card shares the radio SPI object

Este commit está contenido en:
Jared Dohrman
2026-04-21 16:01:19 +10:00
padre eb1a934849
commit 746396aee9
Se han modificado 13 ficheros con 273 adiciones y 13 borrados
+25
Ver fichero
@@ -89,6 +89,31 @@ Legacy dotted aliases are also accepted:
- `get battery.reporting`: shows whether board battery reporting is enabled. Support is board-dependent. - `get battery.reporting`: shows whether board battery reporting is enabled. Support is board-dependent.
- `set battery.reporting on|off`: enables or disables battery voltage reporting on supported boards. This is currently useful for Heltec V3 boards where USB-only power can produce misleading battery readings. If your board needs this too, open an issue and support can be added board-by-board. - `set battery.reporting on|off`: enables or disables battery voltage reporting on supported boards. This is currently useful for Heltec V3 boards where USB-only power can produce misleading battery readings. If your board needs this too, open an issue and support can be added board-by-board.
### T-Beam 1W Fan Control
These commands are only available on `LilyGo_TBeam_1W_*` repeater builds.
- `get fan`: shows the current fan mode, current fan state, and the last NTC-based board temperature when available.
- `set fan auto`: returns the fan to automatic control and persists that mode across reboot.
- `set fan on`: forces the fan on and persists that mode across reboot.
- `set fan off`: forces the fan off and persists that mode across reboot.
- `set fan timeout <Ns>`: changes the automatic post-TX hold window in seconds and persists it across reboot, for example `set fan timeout 45s`.
Auto mode behavior:
- forces the fan on during TX and keeps it on for the configured timeout afterward
- otherwise turns the fan on at `48C`
- turns it back off at `42C`
- keeps the fan on if the NTC reading is unavailable
Notes:
- default repeater fan mode is `auto`
- default post-TX timeout is `30s`
- fan mode and timeout are stored in repeater prefs and survive reboot
- only `LilyGo_TBeam_1W_*` repeater builds use these persisted fan settings
- accepted range is `0s` to `600s`
## Web Panel CLI Access ## Web Panel CLI Access
When the repeater web panel is enabled and you are authenticated, the browser CLI panel can run the same CLI commands accepted by the repeater. When the repeater web panel is enabled and you are authenticated, the browser CLI panel can run the same CLI commands accepted by the repeater.
+4
Ver fichero
@@ -227,6 +227,10 @@ void setup() {
} }
void loop() { void loop() {
#if defined(TBEAM_1W)
board.updateFanControl();
#endif
the_mesh.loop(); the_mesh.loop();
sensors.loop(); sensors.loop();
#ifdef DISPLAY_CLASS #ifdef DISPLAY_CLASS
+61
Ver fichero
@@ -1,5 +1,10 @@
#include "MyMesh.h" #include "MyMesh.h"
#include <algorithm> #include <algorithm>
#include <cstdlib>
#if defined(TBEAM_1W)
#include <TBeam1WBoard.h>
#endif
#if defined(ESP32) && WITH_WEB_PANEL #if defined(ESP32) && WITH_WEB_PANEL
#include <WiFi.h> #include <WiFi.h>
@@ -1043,6 +1048,8 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
_prefs.battery_reporting_enabled = 1; _prefs.battery_reporting_enabled = 1;
_prefs.fan_mode = 0; // auto
_prefs.fan_timeout_secs = 30;
#if defined(USE_SX1262) || defined(USE_SX1268) #if defined(USE_SX1262) || defined(USE_SX1268)
#ifdef SX126X_RX_BOOSTED_GAIN #ifdef SX126X_RX_BOOSTED_GAIN
@@ -1148,6 +1155,11 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
board.setAdcMultiplier(_prefs.adc_multiplier); board.setAdcMultiplier(_prefs.adc_multiplier);
board.setBatteryReporting(_prefs.battery_reporting_enabled); board.setBatteryReporting(_prefs.battery_reporting_enabled);
#if defined(TBEAM_1W)
auto& tbeam1w_board = static_cast<TBeam1WBoard&>(board);
tbeam1w_board.setFanPostTxHoldMs(static_cast<uint32_t>(_prefs.fan_timeout_secs) * 1000UL);
tbeam1w_board.setFanMode(static_cast<TBeam1WBoard::FanMode>(_prefs.fan_mode));
#endif
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
applyGpsPrefs(); applyGpsPrefs();
@@ -1835,6 +1847,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
static_cast<unsigned>(_stats_history.getEventCapacity()), static_cast<unsigned>(_stats_history.getEventCapacity()),
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable"); (_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable");
#endif #endif
#if defined(TBEAM_1W)
} else if (strcmp(command, "get fan") == 0) {
auto& tbeam1w_board = static_cast<TBeam1WBoard&>(board);
const float temp_c = tbeam1w_board.getLastBoardTemperatureC();
if (isnan(temp_c)) {
snprintf(reply, 160, "> mode:%s state:%s timeout:%lus temp:unavailable",
tbeam1w_board.getFanModeName(),
tbeam1w_board.isFanEnabled() ? "on" : "off",
static_cast<unsigned long>(tbeam1w_board.getFanPostTxHoldMs() / 1000UL));
} else {
snprintf(reply, 160, "> mode:%s state:%s timeout:%lus temp:%.2fC",
tbeam1w_board.getFanModeName(),
tbeam1w_board.isFanEnabled() ? "on" : "off",
static_cast<unsigned long>(tbeam1w_board.getFanPostTxHoldMs() / 1000UL),
temp_c);
}
} else if (memcmp(command, "set fan ", 8) == 0) {
auto& tbeam1w_board = static_cast<TBeam1WBoard&>(board);
const char* mode = &command[8];
if (memcmp(mode, "auto", 4) == 0) {
_prefs.fan_mode = static_cast<uint8_t>(TBeam1WBoard::FanMode::Auto);
tbeam1w_board.setFanMode(TBeam1WBoard::FanMode::Auto);
savePrefs();
strcpy(reply, "OK - fan auto");
} else if (memcmp(mode, "on", 2) == 0) {
_prefs.fan_mode = static_cast<uint8_t>(TBeam1WBoard::FanMode::On);
tbeam1w_board.setFanMode(TBeam1WBoard::FanMode::On);
savePrefs();
strcpy(reply, "OK - fan on");
} else if (memcmp(mode, "off", 3) == 0) {
_prefs.fan_mode = static_cast<uint8_t>(TBeam1WBoard::FanMode::Off);
tbeam1w_board.setFanMode(TBeam1WBoard::FanMode::Off);
savePrefs();
strcpy(reply, "OK - fan off");
} else if (memcmp(mode, "timeout ", 8) == 0) {
char* end = nullptr;
const unsigned long timeout_s = strtoul(&mode[8], &end, 10);
while (end != nullptr && *end == ' ') end++;
if (end != nullptr && *end == 's' && *(end + 1) == 0 && tbeam1w_board.setFanPostTxHoldMs(static_cast<uint32_t>(timeout_s * 1000UL))) {
_prefs.fan_timeout_secs = static_cast<uint16_t>(timeout_s);
savePrefs();
snprintf(reply, 160, "OK - fan timeout %lus", timeout_s);
} else {
strcpy(reply, "Err - use 0s..600s");
}
} else {
strcpy(reply, "Err - use auto|on|off|timeout <Ns>");
}
#endif
#if defined(ESP_PLATFORM) #if defined(ESP_PLATFORM)
} else if (memcmp(command, "get wifi.status", 15) == 0) { } else if (memcmp(command, "get wifi.status", 15) == 0) {
network.formatWifiStatusReply(reply, 160); network.formatWifiStatusReply(reply, 160);
+4
Ver fichero
@@ -109,6 +109,10 @@ void setup() {
} }
void loop() { void loop() {
#if defined(TBEAM_1W)
board.updateFanControl();
#endif
int len = strlen(command); int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) { while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read(); char c = Serial.read();
+4
Ver fichero
@@ -83,6 +83,10 @@ void setup() {
} }
void loop() { void loop() {
#if defined(TBEAM_1W)
board.updateFanControl();
#endif
int len = strlen(command); int len = strlen(command);
while (Serial.available() && len < sizeof(command)-1) { while (Serial.available() && len < sizeof(command)-1) {
char c = Serial.read(); char c = Serial.read();
+16 -6
Ver fichero
@@ -35,12 +35,16 @@ const char* cardTypeName(uint8_t type) {
} // namespace } // namespace
#if defined(ESP32)
SPIClass* getBoardSharedArchiveSPI() __attribute__((weak));
#endif
ArchiveStorage::ArchiveStorage() ArchiveStorage::ArchiveStorage()
: _attempted(false), _mounted(false), _mount_failed(false), _supported(false), _card_type(0), _spi_bus(HSPI), : _attempted(false), _mounted(false), _mount_failed(false), _supported(false), _card_type(0), _spi_bus(HSPI),
_cs_pin(0xFF), _sck_pin(0xFF), _cs_pin(0xFF), _sck_pin(0xFF),
_miso_pin(0xFF), _mosi_pin(0xFF), _card_size_bytes(0), _total_bytes(0), _used_bytes(0) _miso_pin(0xFF), _mosi_pin(0xFF), _card_size_bytes(0), _total_bytes(0), _used_bytes(0)
#if defined(ESP32) #if defined(ESP32)
, _spi(nullptr) , _spi(nullptr), _owns_spi(false)
#endif #endif
{ {
} }
@@ -88,11 +92,15 @@ void ArchiveStorage::begin() {
static_cast<unsigned>(_miso_pin), static_cast<unsigned>(_miso_pin),
static_cast<unsigned>(_mosi_pin)); static_cast<unsigned>(_mosi_pin));
_spi = new SPIClass(_spi_bus); _spi = getBoardSharedArchiveSPI();
_owns_spi = (_spi == nullptr);
if (_spi == nullptr) { if (_spi == nullptr) {
_mount_failed = true; _spi = new SPIClass(_spi_bus);
ARCHIVE_LOG("mount failed: SPI alloc failed"); if (_spi == nullptr) {
return; _mount_failed = true;
ARCHIVE_LOG("mount failed: SPI alloc failed");
return;
}
} }
if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) { if (!mountArchiveSd(_spi, _spi_bus, _cs_pin, _sck_pin, _miso_pin, _mosi_pin)) {
@@ -130,7 +138,9 @@ bool ArchiveStorage::recover() {
ARCHIVE_LOG("recover begin bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin)); ARCHIVE_LOG("recover begin bus=%u cs=%u", static_cast<unsigned>(_spi_bus), static_cast<unsigned>(_cs_pin));
SD.end(); SD.end();
_spi->end(); if (_owns_spi) {
_spi->end();
}
_mounted = false; _mounted = false;
_mount_failed = false; _mount_failed = false;
_card_size_bytes = 0; _card_size_bytes = 0;
+1
Ver fichero
@@ -55,5 +55,6 @@ private:
#if defined(ESP32) #if defined(ESP32)
SPIClass* _spi; SPIClass* _spi;
bool _owns_spi;
#endif #endif
}; };
+12 -2
Ver fichero
@@ -93,7 +93,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) { if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) {
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291 file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291
} }
// next: 292 if (file.available() >= (int)sizeof(_prefs->fan_mode)) {
file.read((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292
}
if (file.available() >= (int)sizeof(_prefs->fan_timeout_secs)) {
file.read((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293
}
// next: 295
// sanitise bad pref values // sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -124,6 +130,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// sanitise settings // sanitise settings
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->fan_mode = constrain(_prefs->fan_mode, 0, 2);
_prefs->fan_timeout_secs = constrain(_prefs->fan_timeout_secs, 0, 600);
file.close(); file.close();
} }
@@ -186,7 +194,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
file.write((uint8_t *)&_prefs->battery_reporting_enabled, sizeof(_prefs->battery_reporting_enabled)); // 290 file.write((uint8_t *)&_prefs->battery_reporting_enabled, sizeof(_prefs->battery_reporting_enabled)); // 290
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291 file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 291
// next: 292 file.write((uint8_t *)&_prefs->fan_mode, sizeof(_prefs->fan_mode)); // 292
file.write((uint8_t *)&_prefs->fan_timeout_secs, sizeof(_prefs->fan_timeout_secs)); // 293
// next: 295
file.close(); file.close();
} }
+2
Ver fichero
@@ -62,6 +62,8 @@ struct NodePrefs { // persisted to file
uint8_t rx_boosted_gain; // power settings uint8_t rx_boosted_gain; // power settings
uint8_t path_hash_mode; // which path mode to use when sending uint8_t path_hash_mode; // which path mode to use when sending
uint8_t loop_detect; uint8_t loop_detect;
uint8_t fan_mode;
uint16_t fan_timeout_secs;
}; };
class CommonCLICallbacks { class CommonCLICallbacks {
+117 -3
Ver fichero
@@ -1,5 +1,19 @@
#include "TBeam1WBoard.h" #include "TBeam1WBoard.h"
namespace {
constexpr float kFanTempOnC = 48.0f;
constexpr float kFanTempOffC = 42.0f;
constexpr uint32_t kFanControlCheckMs = 5000;
constexpr uint32_t kFanPostTxHoldMs = 30000;
constexpr float kNtcSeriesResistorOhms = 10000.0f;
constexpr float kNtcBeta = 3950.0f;
constexpr float kNtcRoomTempKelvin = 298.15f;
constexpr float kNtcRoomResistanceOhms = 10000.0f;
constexpr float kNtcVrefVolts = 3.3f;
constexpr uint32_t kMinFanPostTxHoldMs = 0;
constexpr uint32_t kMaxFanPostTxHoldMs = 600000;
}
void TBeam1WBoard::begin() { void TBeam1WBoard::begin() {
ESP32Board::begin(); ESP32Board::begin();
@@ -17,16 +31,25 @@ void TBeam1WBoard::begin() {
// Initialize fan control (on by default - 1W PA can overheat) // Initialize fan control (on by default - 1W PA can overheat)
pinMode(FAN_CTRL_PIN, OUTPUT); pinMode(FAN_CTRL_PIN, OUTPUT);
digitalWrite(FAN_CTRL_PIN, HIGH); setFanEnabled(true);
fan_force_on_until = millis() + fan_post_tx_hold_ms;
next_fan_control_check_at = millis() + kFanControlCheckMs;
} }
void TBeam1WBoard::onBeforeTransmit() { void TBeam1WBoard::onBeforeTransmit() {
// RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
digitalWrite(LED_PIN, HIGH); // TX LED on digitalWrite(LED_PIN, HIGH); // TX LED on
if (fan_mode == FanMode::Auto) {
fan_force_on_until = millis() + fan_post_tx_hold_ms;
setFanEnabled(true);
}
} }
void TBeam1WBoard::onAfterTransmit() { void TBeam1WBoard::onAfterTransmit() {
digitalWrite(LED_PIN, LOW); // TX LED off digitalWrite(LED_PIN, LOW); // TX LED off
if (fan_mode == FanMode::Auto) {
fan_force_on_until = millis() + fan_post_tx_hold_ms;
}
} }
uint16_t TBeam1WBoard::getBattMilliVolts() { uint16_t TBeam1WBoard::getBattMilliVolts() {
@@ -57,15 +80,106 @@ void TBeam1WBoard::powerOff() {
// Turn off LED and fan // Turn off LED and fan
digitalWrite(LED_PIN, LOW); digitalWrite(LED_PIN, LOW);
digitalWrite(FAN_CTRL_PIN, LOW); setFanEnabled(false);
ESP32Board::powerOff(); ESP32Board::powerOff();
} }
void TBeam1WBoard::setFanEnabled(bool enabled) { void TBeam1WBoard::setFanEnabled(bool enabled) {
fan_enabled = enabled;
digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW);
} }
bool TBeam1WBoard::isFanEnabled() const { bool TBeam1WBoard::isFanEnabled() const {
return digitalRead(FAN_CTRL_PIN) == HIGH; return fan_enabled;
}
void TBeam1WBoard::setFanMode(FanMode mode) {
fan_mode = mode;
next_fan_control_check_at = 0;
updateFanControl(true);
}
TBeam1WBoard::FanMode TBeam1WBoard::getFanMode() const {
return fan_mode;
}
const char* TBeam1WBoard::getFanModeName() const {
switch (fan_mode) {
case FanMode::On:
return "on";
case FanMode::Off:
return "off";
case FanMode::Auto:
default:
return "auto";
}
}
float TBeam1WBoard::getLastBoardTemperatureC() const {
return last_board_temperature_c;
}
bool TBeam1WBoard::setFanPostTxHoldMs(uint32_t hold_ms) {
if (hold_ms < kMinFanPostTxHoldMs || hold_ms > kMaxFanPostTxHoldMs) {
return false;
}
fan_post_tx_hold_ms = hold_ms;
return true;
}
uint32_t TBeam1WBoard::getFanPostTxHoldMs() const {
return fan_post_tx_hold_ms;
}
float TBeam1WBoard::readBoardTemperatureC() const {
const float millivolts = analogReadMilliVolts(NTC_PIN);
if (millivolts <= 0.0f || millivolts >= (kNtcVrefVolts * 1000.0f)) {
return NAN;
}
const float voltage = millivolts / 1000.0f;
const float resistance = kNtcSeriesResistorOhms * ((kNtcVrefVolts / voltage) - 1.0f);
if (!(resistance > 0.0f)) {
return NAN;
}
const float kelvin = 1.0f / ((log(resistance / kNtcRoomResistanceOhms) / kNtcBeta) + (1.0f / kNtcRoomTempKelvin));
return kelvin - 273.15f;
}
void TBeam1WBoard::updateFanControl(bool force) {
const uint32_t now = millis();
if (!force && now < next_fan_control_check_at) {
return;
}
next_fan_control_check_at = now + kFanControlCheckMs;
if (fan_mode == FanMode::On) {
setFanEnabled(true);
return;
}
if (fan_mode == FanMode::Off) {
setFanEnabled(false);
return;
}
if (now < fan_force_on_until) {
setFanEnabled(true);
return;
}
const float temperature = readBoardTemperatureC();
last_board_temperature_c = temperature;
if (isnan(temperature)) {
// Keep the fan on if temperature sensing is unavailable.
setFanEnabled(true);
return;
}
if (temperature >= kFanTempOnC) {
setFanEnabled(true);
} else if (temperature <= kFanTempOffC) {
setFanEnabled(false);
}
} }
+21
Ver fichero
@@ -28,8 +28,22 @@
// - Battery must support 2A+ discharge for high-power TX // - Battery must support 2A+ discharge for high-power TX
class TBeam1WBoard : public ESP32Board { class TBeam1WBoard : public ESP32Board {
public:
enum class FanMode : uint8_t {
Auto = 0,
On,
Off
};
private: private:
bool radio_powered = false; bool radio_powered = false;
bool fan_enabled = true;
uint32_t fan_force_on_until = 0;
uint32_t next_fan_control_check_at = 0;
uint32_t fan_post_tx_hold_ms = 30000;
FanMode fan_mode = FanMode::Auto;
float last_board_temperature_c = NAN;
float readBoardTemperatureC() const;
public: public:
void begin(); void begin();
@@ -42,4 +56,11 @@ public:
// Fan control methods // Fan control methods
void setFanEnabled(bool enabled); void setFanEnabled(bool enabled);
bool isFanEnabled() const; bool isFanEnabled() const;
void setFanMode(FanMode mode);
FanMode getFanMode() const;
const char* getFanModeName() const;
float getLastBoardTemperatureC() const;
bool setFanPostTxHoldMs(uint32_t hold_ms);
uint32_t getFanPostTxHoldMs() const;
void updateFanControl(bool force = false);
}; };
+4 -1
Ver fichero
@@ -10,6 +10,10 @@ TBeam1WBoard board;
static SPIClass spi; static SPIClass spi;
SPIClass* getBoardSharedArchiveSPI() {
return &spi;
}
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
WRAPPER_CLASS radio_driver(radio, board); WRAPPER_CLASS radio_driver(radio, board);
@@ -62,4 +66,3 @@ mesh::LocalIdentity radio_new_identity() {
RadioNoiseListener rng(radio); RadioNoiseListener rng(radio);
return mesh::LocalIdentity(&rng); return mesh::LocalIdentity(&rng);
} }
+2 -1
Ver fichero
@@ -20,9 +20,10 @@ extern WRAPPER_CLASS radio_driver;
extern AutoDiscoverRTCClock rtc_clock; extern AutoDiscoverRTCClock rtc_clock;
extern EnvironmentSensorManager sensors; extern EnvironmentSensorManager sensors;
SPIClass* getBoardSharedArchiveSPI();
bool radio_init(); bool radio_init();
uint32_t radio_get_rng_seed(); uint32_t radio_get_rng_seed();
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
void radio_set_tx_power(int8_t dbm); void radio_set_tx_power(int8_t dbm);
mesh::LocalIdentity radio_new_identity(); mesh::LocalIdentity radio_new_identity();