Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 288 additions & 9 deletions variants/lilygo_tbeam_1w/TBeam1WBoard.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#include "TBeam1WBoard.h"

#include <errno.h>
#include <limits.h>
#include <math.h>
#include <stdlib.h>

void TBeam1WBoard::begin() {
ESP32Board::begin();

Expand All @@ -15,31 +20,61 @@ void TBeam1WBoard::begin() {
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);

// Initialize fan control (on by default - 1W PA can overheat)
// NTC ADC (PA-adjacent thermistor)
pinMode(NTC_PIN, INPUT);
analogSetPinAttenuation(NTC_PIN, ADC_11db);
analogReadResolution(12);

// Fan: auto/onoff. Thermal on at 36C / off below 30C; TX still forces a cooldown.
pinMode(FAN_CTRL_PIN, OUTPUT);
digitalWrite(FAN_CTRL_PIN, HIGH);
_fan_on = true;
_temp_c = readNtcTempC();
startFanTask();
}

void TBeam1WBoard::startFanTask() {
if (_fan_task) return;
xTaskCreate(fanTaskThunk, "tbeam1w_fan", 4096, this, 1, &_fan_task);
}

void TBeam1WBoard::fanTaskThunk(void* arg) {
auto* self = static_cast<TBeam1WBoard*>(arg);
for (;;) {
self->updateFan();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}

void TBeam1WBoard::onBeforeTransmit() {
// RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins()
digitalWrite(LED_PIN, HIGH); // TX LED on
portENTER_CRITICAL(&_fan_mux);
_tx_active = true;
if (_mode == FAN_AUTO && !_stopped) {
setFanOutputLocked(true);
}
portEXIT_CRITICAL(&_fan_mux);
}

void TBeam1WBoard::onAfterTransmit() {
digitalWrite(LED_PIN, LOW); // TX LED off
portENTER_CRITICAL(&_fan_mux);
if (!_stopped) {
_tx_until_ms = millis() + FAN_TX_COOLDOWN_MS;
_tx_cooldown_active = true;
}
_tx_active = false;
portEXIT_CRITICAL(&_fan_mux);
}

uint16_t TBeam1WBoard::getBattMilliVolts() {
// T-Beam 1W uses 7.4V battery with voltage divider
// ADC reads through divider - adjust multiplier based on actual divider ratio
analogReadResolution(12);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(BATTERY_PIN);
}
raw = raw / 8;
// Assuming voltage divider ratio from ADC_MULTIPLIER
// 3.3V reference, 12-bit ADC (4095 max)
return static_cast<uint16_t>((raw * 3300 * ADC_MULTIPLIER) / 4095);
}

Expand All @@ -48,24 +83,268 @@ const char* TBeam1WBoard::getManufacturerName() const {
}

void TBeam1WBoard::powerOff() {
portENTER_CRITICAL(&_fan_mux);
_stopped = true;
_tx_active = false;
_tx_cooldown_active = false;
setFanOutputLocked(false);
portEXIT_CRITICAL(&_fan_mux);

// Turn off radio LNA (CTRL pin must be LOW when not receiving)
digitalWrite(SX126X_RXEN, LOW);

// Turn off radio power
digitalWrite(SX126X_POWER_EN, LOW);
radio_powered = false;

// Turn off LED and fan
digitalWrite(LED_PIN, LOW);
digitalWrite(FAN_CTRL_PIN, LOW);

ESP32Board::powerOff();
}

void TBeam1WBoard::setFanEnabled(bool enabled) {
digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW);
portENTER_CRITICAL(&_fan_mux);
setFanOutputLocked(enabled && !_stopped);
portEXIT_CRITICAL(&_fan_mux);
}

bool TBeam1WBoard::isFanEnabled() const {
return digitalRead(FAN_CTRL_PIN) == HIGH;
portENTER_CRITICAL(&_fan_mux);
bool enabled = _fan_on;
portEXIT_CRITICAL(&_fan_mux);
return enabled;
}

float TBeam1WBoard::readNtcTempC() {
analogReadMilliVolts(NTC_PIN); // settle
uint32_t sum = 0;
for (int i = 0; i < 8; i++) {
uint32_t sample_mv = analogReadMilliVolts(NTC_PIN);
// GPIO14 is ADC2 on ESP32-S3. Wi-Fi/ESP-NOW arbitration failures are
// reported by Arduino as 0 mV; never average a failed sample into a
// plausible-but-low temperature.
if (sample_mv == 0 || sample_mv >= NTC_VCC_MV) return NAN;
sum += sample_mv;
}
float mv = sum / 8.0f;

// R_ntc = R_fixed * (Vcc - V) / V for 3.3V-NTC-ADC-10k-GND
float r_ntc = NTC_R_FIXED * (NTC_VCC_MV - mv) / mv;
if (r_ntc <= 0.0f) return NAN;

float temp_k = 1.0f / (1.0f / 298.15f + (1.0f / NTC_B) * logf(r_ntc / NTC_R25));
return temp_k - 273.15f;
}

bool TBeam1WBoard::ntcImplausible(float temp_c) const {
return isnan(temp_c) || temp_c < -20.0f || temp_c > 120.0f;
}

bool TBeam1WBoard::isTxCoolingLocked(uint32_t now) {
if (_tx_active) return true;
if (!_tx_cooldown_active) return false;
if ((int32_t)(now - _tx_until_ms) >= 0) {
_tx_cooldown_active = false;
return false;
}
return true;
}

int TBeam1WBoard::cooldownSecsLocked() {
if (_tx_active) return (FAN_TX_COOLDOWN_MS + 999) / 1000;
if (!_tx_cooldown_active) return 0;
int32_t remain_ms = (int32_t)(_tx_until_ms - millis());
if (remain_ms <= 0) {
_tx_cooldown_active = false;
return 0;
}
return (remain_ms + 999) / 1000;
}

void TBeam1WBoard::setFanOutputLocked(bool enabled) {
_fan_on = enabled;
digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW);
}

void TBeam1WBoard::updateFan() {
float t = readNtcTempC();
uint32_t now = millis();

portENTER_CRITICAL(&_fan_mux);
if (_stopped) {
portEXIT_CRITICAL(&_fan_mux);
return;
}

_temp_c = t;
bool tx_cooling = isTxCoolingLocked(now);

bool enabled;
if (_mode == FAN_ON) {
enabled = true;
} else if (_mode == FAN_OFF) {
enabled = false;
} else if (ntcImplausible(t)) {
enabled = true; // fail-safe: treat bad NTC as hot
} else {
// TX cooldown must not latch _thermal_on, or a TX at 30C keeps the fan
// running until the temperature dips under lo.
if (t >= (float)_hi_c) _thermal_on = true;
else if (t < (float)_lo_c) _thermal_on = false;
enabled = _thermal_on || tx_cooling;
}

setFanOutputLocked(enabled);
portEXIT_CRITICAL(&_fan_mux);
}

bool TBeam1WBoard::persistKey(const char* key, const char* value) {
return _prefs && _prefs->setByKey(key, value);
}

bool TBeam1WBoard::parseIntArg(const char* text, int& value) {
if (!text || !*text) return false;
errno = 0;
char* end = nullptr;
long parsed = strtol(text, &end, 10);
if (errno == ERANGE || end == text || *end != '\0' || parsed < INT_MIN || parsed > INT_MAX) {
return false;
}
value = (int)parsed;
return true;
}

void TBeam1WBoard::loadFanPrefs() {
if (!_prefs) return;

FanMode mode = FAN_AUTO;
char buf[12];
buf[0] = 0;
if (_prefs->getByKey("fan", buf, 11)) {
if (strcmp(buf, "off") == 0) mode = FAN_OFF;
else if (strcmp(buf, "on") == 0) mode = FAN_ON;
}

int lo = FAN_DEFAULT_LO_C;
int hi = FAN_DEFAULT_HI_C;
buf[0] = 0;
if (_prefs->getByKey("fan_lo", buf, 11)) lo = atoi(buf);
buf[0] = 0;
if (_prefs->getByKey("fan_hi", buf, 11)) hi = atoi(buf);

portENTER_CRITICAL(&_fan_mux);
_mode = mode;
if (lo >= 0 && hi <= 120 && lo < hi) {
_lo_c = lo;
_hi_c = hi;
}
portEXIT_CRITICAL(&_fan_mux);
}

void TBeam1WBoard::attachDynamicPrefs(KeyValueStore* prefs) {
_prefs = prefs;
loadFanPrefs();
updateFan();
}

const char* TBeam1WBoard::modeNameLocked() const {
if (_mode == FAN_AUTO) return "auto";
if (_mode == FAN_OFF) return "off";
return "on";
}

bool TBeam1WBoard::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) {
(void)sender_timestamp;

if (strcmp(command, "get fan") == 0) {
portENTER_CRITICAL(&_fan_mux);
int cd = cooldownSecsLocked();
float temp_c = _temp_c;
bool enabled = _fan_on;
const char* mode = modeNameLocked();
portEXIT_CRITICAL(&_fan_mux);
if (ntcImplausible(temp_c)) {
sprintf(reply, "> %s n/a fan=%s cd=%ds", mode, enabled ? "on" : "off", cd);
} else {
sprintf(reply, "> %s %.1fC fan=%s cd=%ds",
mode, (double)temp_c, enabled ? "on" : "off", cd);
}
return true;
}

if (strncmp(command, "set fan.lo ", 11) == 0) {
int lo;
portENTER_CRITICAL(&_fan_mux);
int hi_limit = _hi_c;
portEXIT_CRITICAL(&_fan_mux);
if (!parseIntArg(&command[11], lo) || lo < 0 || lo >= hi_limit || lo > 100) {
strcpy(reply, "Error: fan.lo must be 0..100 and < fan.hi");
} else if (!persistKey("fan_lo", &command[11])) {
strcpy(reply, "Error: failed to save fan.lo");
} else {
portENTER_CRITICAL(&_fan_mux);
_lo_c = lo;
portEXIT_CRITICAL(&_fan_mux);
sprintf(reply, "OK - fan.lo %d", lo);
}
return true;
}

if (strncmp(command, "set fan.hi ", 11) == 0) {
int hi;
portENTER_CRITICAL(&_fan_mux);
int lo_limit = _lo_c;
portEXIT_CRITICAL(&_fan_mux);
if (!parseIntArg(&command[11], hi) || hi <= lo_limit || hi > 120) {
strcpy(reply, "Error: fan.hi must be > fan.lo and <= 120");
} else if (!persistKey("fan_hi", &command[11])) {
strcpy(reply, "Error: failed to save fan.hi");
} else {
portENTER_CRITICAL(&_fan_mux);
_hi_c = hi;
portEXIT_CRITICAL(&_fan_mux);
sprintf(reply, "OK - fan.hi %d", hi);
}
return true;
}

if (strncmp(command, "set fan ", 8) == 0) {
const char* arg = &command[8];
if (strcmp(arg, "on") == 0) {
if (!persistKey("fan", "on")) {
strcpy(reply, "Error: failed to save fan mode");
} else {
portENTER_CRITICAL(&_fan_mux);
_mode = FAN_ON;
if (!_stopped) setFanOutputLocked(true);
portEXIT_CRITICAL(&_fan_mux);
strcpy(reply, "OK - fan on");
}
} else if (strcmp(arg, "off") == 0) {
if (!persistKey("fan", "off")) {
strcpy(reply, "Error: failed to save fan mode");
} else {
portENTER_CRITICAL(&_fan_mux);
_mode = FAN_OFF;
setFanOutputLocked(false);
portEXIT_CRITICAL(&_fan_mux);
strcpy(reply, "OK - fan off");
}
} else if (strcmp(arg, "auto") == 0) {
if (!persistKey("fan", "auto")) {
strcpy(reply, "Error: failed to save fan mode");
} else {
portENTER_CRITICAL(&_fan_mux);
_mode = FAN_AUTO;
portEXIT_CRITICAL(&_fan_mux);
updateFan();
strcpy(reply, "OK - fan auto");
}
} else {
strcpy(reply, "Error: fan must be on, off, or auto");
}
return true;
}

return false;
}
Loading