Просмотр исходного кода

fix: sd card shares the radio SPI object

Jared Dohrman 3 месяцев назад
Родитель
Сommit
746396aee9

+ 25 - 0
docs/custom-cli.md

@@ -89,6 +89,31 @@ Legacy dotted aliases are also accepted:
 - `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.
 
+### 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
 
 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 - 0
examples/companion_radio/main.cpp

@@ -227,6 +227,10 @@ void setup() {
 }
 
 void loop() {
+#if defined(TBEAM_1W)
+  board.updateFanControl();
+#endif
+
   the_mesh.loop();
   sensors.loop();
 #ifdef DISPLAY_CLASS

+ 61 - 0
examples/simple_repeater/MyMesh.cpp

@@ -1,5 +1,10 @@
 #include "MyMesh.h"
 #include <algorithm>
+#include <cstdlib>
+
+#if defined(TBEAM_1W)
+  #include <TBeam1WBoard.h>
+#endif
 
 #if defined(ESP32) && WITH_WEB_PANEL
   #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.battery_reporting_enabled = 1;
+  _prefs.fan_mode = 0; // auto
+  _prefs.fan_timeout_secs = 30;
 
 #if defined(USE_SX1262) || defined(USE_SX1268)
 #ifdef SX126X_RX_BOOSTED_GAIN
@@ -1148,6 +1155,11 @@ void MyMesh::begin(FILESYSTEM *fs, ArchiveStorage* archive) {
 
   board.setAdcMultiplier(_prefs.adc_multiplier);
   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
   applyGpsPrefs();
@@ -1835,6 +1847,55 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
              static_cast<unsigned>(_stats_history.getEventCapacity()),
              (_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable");
 #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)
   } else if (memcmp(command, "get wifi.status", 15) == 0) {
     network.formatWifiStatusReply(reply, 160);

+ 4 - 0
examples/simple_repeater/main.cpp

@@ -109,6 +109,10 @@ void setup() {
 }
 
 void loop() {
+#if defined(TBEAM_1W)
+  board.updateFanControl();
+#endif
+
   int len = strlen(command);
   while (Serial.available() && len < sizeof(command)-1) {
     char c = Serial.read();

+ 4 - 0
examples/simple_room_server/main.cpp

@@ -83,6 +83,10 @@ void setup() {
 }
 
 void loop() {
+#if defined(TBEAM_1W)
+  board.updateFanControl();
+#endif
+
   int len = strlen(command);
   while (Serial.available() && len < sizeof(command)-1) {
     char c = Serial.read();

+ 16 - 6
src/helpers/ArchiveStorage.cpp

@@ -35,12 +35,16 @@ const char* cardTypeName(uint8_t type) {
 
 }  // namespace
 
+#if defined(ESP32)
+SPIClass* getBoardSharedArchiveSPI() __attribute__((weak));
+#endif
+
 ArchiveStorage::ArchiveStorage()
     : _attempted(false), _mounted(false), _mount_failed(false), _supported(false), _card_type(0), _spi_bus(HSPI),
       _cs_pin(0xFF), _sck_pin(0xFF),
       _miso_pin(0xFF), _mosi_pin(0xFF), _card_size_bytes(0), _total_bytes(0), _used_bytes(0)
 #if defined(ESP32)
-      , _spi(nullptr)
+      , _spi(nullptr), _owns_spi(false)
 #endif
 {
 }
@@ -88,11 +92,15 @@ void ArchiveStorage::begin() {
               static_cast<unsigned>(_miso_pin),
               static_cast<unsigned>(_mosi_pin));
 
-  _spi = new SPIClass(_spi_bus);
+  _spi = getBoardSharedArchiveSPI();
+  _owns_spi = (_spi == nullptr);
   if (_spi == nullptr) {
-    _mount_failed = true;
-    ARCHIVE_LOG("mount failed: SPI alloc failed");
-    return;
+    _spi = new SPIClass(_spi_bus);
+    if (_spi == nullptr) {
+      _mount_failed = true;
+      ARCHIVE_LOG("mount failed: SPI alloc failed");
+      return;
+    }
   }
 
   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));
   SD.end();
-  _spi->end();
+  if (_owns_spi) {
+    _spi->end();
+  }
   _mounted = false;
   _mount_failed = false;
   _card_size_bytes = 0;

+ 1 - 0
src/helpers/ArchiveStorage.h

@@ -55,5 +55,6 @@ private:
 
 #if defined(ESP32)
   SPIClass* _spi;
+  bool _owns_spi;
 #endif
 };

+ 12 - 2
src/helpers/CommonCLI.cpp

@@ -93,7 +93,13 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
     if (file.available() >= (int)sizeof(_prefs->rx_boosted_gain)) {
       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
     _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
     _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();
   }
@@ -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->battery_reporting_enabled, sizeof(_prefs->battery_reporting_enabled)); // 290
     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();
   }

+ 2 - 0
src/helpers/CommonCLI.h

@@ -62,6 +62,8 @@ struct NodePrefs { // persisted to file
   uint8_t rx_boosted_gain; // power settings
   uint8_t path_hash_mode;   // which path mode to use when sending
   uint8_t loop_detect;
+  uint8_t fan_mode;
+  uint16_t fan_timeout_secs;
 };
 
 class CommonCLICallbacks {

+ 117 - 3
variants/lilygo_tbeam_1w/TBeam1WBoard.cpp

@@ -1,5 +1,19 @@
 #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() {
   ESP32Board::begin();
 
@@ -17,16 +31,25 @@ void TBeam1WBoard::begin() {
 
   // Initialize fan control (on by default - 1W PA can overheat)
   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() {
   // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
   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() {
   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() {
@@ -57,15 +80,106 @@ void TBeam1WBoard::powerOff() {
 
   // Turn off LED and fan
   digitalWrite(LED_PIN, LOW);
-  digitalWrite(FAN_CTRL_PIN, LOW);
+  setFanEnabled(false);
 
   ESP32Board::powerOff();
 }
 
 void TBeam1WBoard::setFanEnabled(bool enabled) {
+  fan_enabled = enabled;
   digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW);
 }
 
 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 - 0
variants/lilygo_tbeam_1w/TBeam1WBoard.h

@@ -28,8 +28,22 @@
 //   - Battery must support 2A+ discharge for high-power TX
 
 class TBeam1WBoard : public ESP32Board {
+public:
+  enum class FanMode : uint8_t {
+    Auto = 0,
+    On,
+    Off
+  };
+
 private:
   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:
   void begin();
@@ -42,4 +56,11 @@ public:
   // Fan control methods
   void setFanEnabled(bool enabled);
   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
variants/lilygo_tbeam_1w/target.cpp

@@ -10,6 +10,10 @@ TBeam1WBoard board;
 
 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);
 
 WRAPPER_CLASS radio_driver(radio, board);
@@ -62,4 +66,3 @@ mesh::LocalIdentity radio_new_identity() {
   RadioNoiseListener rng(radio);
   return mesh::LocalIdentity(&rng);
 }
-

+ 2 - 1
variants/lilygo_tbeam_1w/target.h

@@ -20,9 +20,10 @@ extern WRAPPER_CLASS radio_driver;
 extern AutoDiscoverRTCClock rtc_clock;
 extern EnvironmentSensorManager sensors;
 
+SPIClass* getBoardSharedArchiveSPI();
+
 bool radio_init();
 uint32_t radio_get_rng_seed();
 void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
 void radio_set_tx_power(int8_t dbm);
 mesh::LocalIdentity radio_new_identity();
-