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
Binary file added usermods/KOBA_EGPU/egpu-board.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
334 changes: 334 additions & 0 deletions usermods/KOBA_EGPU/koba_egpu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
#include "wled.h"

#if not defined(CONFIG_IDF_TARGET_ESP32C3)
//#error Adapted only for ESP32C3
#endif

#if not defined(THERMISTOR_GPU_PIN) or THERMISTOR_GPU_PIN not_eq 0
#error THERMISTOR_GPU_PIN NOT defined or not equals GPIO0
#endif

#if not defined(THERMISTOR_FAN_PIN) or THERMISTOR_FAN_PIN not_eq 1
#error THERMISTOR_FAN_PIN NOT defined or not equals GPIO1
#endif

#if not defined(FAN_TACHOMETER_PIN) or FAN_TACHOMETER_PIN not_eq 6
#error FAN_TACHOMETER_PIN NOT defined
#endif

#if not defined(FAN_PWM_PIN) or FAN_PWM_PIN not_eq 10
#error FAN_PWM_PIN NOT defined
#endif

#include <esp_adc_cal.h>

namespace {
float float_map(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
Comment on lines +26 to +28
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential division by zero if in_min == in_max.

While current usage validates thresholds, this helper could cause undefined behavior if called with equal min/max values.

🛡️ Defensive fix
     float float_map(float x, float in_min, float in_max, float out_min, float out_max) {
+        if (in_max == in_min) return out_min;
         return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
     }
🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` around lines 26 - 28, float_map can divide
by zero when in_min == in_max; fix by checking for that case (use a
floating-point tolerant check like fabs(in_max - in_min) < FLT_EPSILON) at the
start of float_map and return a sensible fallback (e.g., the midpoint of the
output range (out_min+out_max)/2 or out_min) instead of performing the division;
keep the rest of the mapping logic unchanged so float_map returns the mapped
value when the denominator is non-zero.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

proposed fix:

    if (in_min == in_max) return out_min;
    else return ....

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softhack007, perfect! That fix looks good and will properly handle the edge case where in_min == in_max. Returning out_min in that scenario is a sensible fallback behavior. 👍


🧠 Learnings used
Learnt from: DedeHai
Repo: wled/WLED PR: 4456
File: usermods/deep_sleep/deep_sleep.cpp:224-230
Timestamp: 2025-11-30T15:29:00.726Z
Learning: In WLED, `touchThreshold` is a global variable defined in wled00/wled.h and is accessible to all usermods. It stores the touch sensitivity threshold for touch-capable GPIO pins on ESP32 variants and should not be flagged as undefined when used in usermods.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.


class NTCThermistor {
private:
float r0;
float inv_t0;
float beta;
public:
explicit NTCThermistor(float r0 = 10000.0f, float beta = 3950.0f, float t0 = 25.0f)
: r0(r0), inv_t0(1.0f / (t0 + 273.15f)), beta(beta) {}

float calculateTemperatureFromResistance(float resistance) const {
if (resistance <= 0.0f) return NAN;
float inv_t = logf(resistance / r0) / beta + inv_t0;
return 1.0f / inv_t - 273.15f;
}
};

struct KobaEGPUUserModCoreConfig {
float fan_max_speed_threshold_temperature = 70.0f;
float fan_min_speed_threshold_temperature = 45.0f;
};

class KobaEGPUUserModCore {
private:
bool _configured = false;
uint32_t _delta_update_time_ms = 100;
uint32_t _pulses_count = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition on _pulses_count between ISR and main thread.

_pulses_count is modified by the ISR (line 120) and read/reset by _update_tachometer_speed (lines 106-108) without synchronization. This can cause lost pulse counts or corrupted reads.

🔒 Proposed fix using atomic operations
+#include <atomic>
+
 namespace {
     // ...
     class KobaEGPUUserModCore {
     private:
         bool _configured = false;
         uint32_t _delta_update_time_ms = 100;
-        uint32_t _pulses_count = 0;
+        std::atomic<uint32_t> _pulses_count{0};
         // ...

         void _update_tachometer_speed(uint32_t delta_ms) {
-            float new_rpm = float(_pulses_count) * 60.0f * 1000.0f / float(delta_ms);
+            uint32_t pulses = _pulses_count.exchange(0);
+            float new_rpm = float(pulses) * 60.0f * 1000.0f / float(delta_ms);
             _last_check_tachometer_speed = millis();
-            _pulses_count = 0;
             if (not isnan(new_rpm) and new_rpm not_eq _current_rpm) {
                 _current_rpm = new_rpm;
             }
         }

         IRAM_ATTR static void tachometer_isr(void *ctx) {
-            reinterpret_cast<KobaEGPUUserModCore *>(ctx)->_pulses_count++;
+            reinterpret_cast<KobaEGPUUserModCore *>(ctx)->_pulses_count.fetch_add(1, std::memory_order_relaxed);
         }

Also applies to: 105-112, 119-121

🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` at line 55, _pulses_count is subject to a
race between the ISR and _update_tachometer_speed; change its type to
std::atomic<uint32_t> (include <atomic>) and replace non-atomic reads/resets
with atomic operations (e.g. use fetch_exchange(0) or exchange(0) in
_update_tachometer_speed to atomically capture-and-clear the count) while
keeping the ISR increment using fetch_add(1) (or ++ on the atomic). Update all
references to _pulses_count (ISR increment site and _update_tachometer_speed
read/reset) to use the atomic API so reads/writes are safe and ordered.

float _current_rpm = NAN;
uint64_t _last_check_tachometer_speed = 0;
ledc_channel_t _fan_pwm_channel = LEDC_CHANNEL_5;
esp_adc_cal_characteristics_t *_adc_chars = nullptr;
float _last_thr_gpu_temp = NAN;
float _last_thr_fan_temp = NAN;
NTCThermistor _thermistor = NTCThermistor(10000.0f, 3950.0f, 25.0f);
KobaEGPUUserModCoreConfig _cfg;

float _calc_temp(int adc_value) {
if (adc_value > 4000 or adc_value < 100) return NAN;
float voltage = (static_cast<float>(adc_value) / 4095.0f) * 3.0f;
float rt = 10000.0f * (voltage / (3.3f - voltage));
return _thermistor.calculateTemperatureFromResistance(rt);
}
Comment on lines +65 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent reference voltage (3.0V vs 3.3V) causes incorrect temperature calculation.

Line 67 calculates voltage using 3.0V reference, but line 68 uses 3.3V for the voltage divider formula. This inconsistency will produce incorrect resistance and temperature values.

🐛 Proposed fix
         float _calc_temp(int adc_value) {
             if (adc_value > 4000 or adc_value < 100) return NAN;
-            float voltage = (static_cast<float>(adc_value) / 4095.0f) * 3.0f;
-            float rt = 10000.0f * (voltage / (3.3f - voltage));
+            constexpr float vref = 3.3f;
+            float voltage = (static_cast<float>(adc_value) / 4095.0f) * vref;
+            float rt = 10000.0f * (voltage / (vref - voltage));
             return _thermistor.calculateTemperatureFromResistance(rt);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
float _calc_temp(int adc_value) {
if (adc_value > 4000 or adc_value < 100) return NAN;
float voltage = (static_cast<float>(adc_value) / 4095.0f) * 3.0f;
float rt = 10000.0f * (voltage / (3.3f - voltage));
return _thermistor.calculateTemperatureFromResistance(rt);
}
float _calc_temp(int adc_value) {
if (adc_value > 4000 or adc_value < 100) return NAN;
constexpr float vref = 3.3f;
float voltage = (static_cast<float>(adc_value) / 4095.0f) * vref;
float rt = 10000.0f * (voltage / (vref - voltage));
return _thermistor.calculateTemperatureFromResistance(rt);
}
🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` around lines 65 - 70, The
voltage-to-resistance math in _calc_temp is using inconsistent reference
voltages (3.0f for voltage calculation and 3.3f in the divider), causing wrong
resistance/temperature; update the voltage computation to use the same ADC
reference as the divider (replace 3.0f with 3.3f or better, centralize into a
REF_VOLTAGE constant) so voltage = (float(adc_value) / 4095.0f) * REF_VOLTAGE
and then compute rt = 10000.0f * (voltage / (REF_VOLTAGE - voltage)); keep the
bounds check and return via _thermistor.calculateTemperatureFromResistance(rt).


void _update_fan_speed() {
if (isnan(_last_thr_gpu_temp)) {
return;
}

uint8_t new_pwm = _last_thr_gpu_temp > _cfg.fan_max_speed_threshold_temperature ? 255 : 0;
if (_last_thr_gpu_temp > _cfg.fan_min_speed_threshold_temperature and
_last_thr_gpu_temp < _cfg.fan_max_speed_threshold_temperature) {
new_pwm = uint8_t(float_map(
_last_thr_gpu_temp,
_cfg.fan_min_speed_threshold_temperature, _cfg.fan_max_speed_threshold_temperature,
0, 255
));
}
_set_fan_speed(new_pwm);
}

void _update_temperatures() {
auto gpu_temp = _calc_temp(adc1_get_raw(adc1_channel_t(THERMISTOR_GPU_PIN)));
auto fan_temp = _calc_temp(adc1_get_raw(adc1_channel_t(THERMISTOR_FAN_PIN)));

bool need_update_fan_speed = false;
if (not isnan(fan_temp) and fan_temp not_eq _last_thr_fan_temp) {
_last_thr_fan_temp = fan_temp;
need_update_fan_speed = true;
}
if (not isnan(gpu_temp) and gpu_temp not_eq _last_thr_gpu_temp) {
_last_thr_gpu_temp = gpu_temp;
need_update_fan_speed = true;
}
if (need_update_fan_speed) _update_fan_speed();
}

void _update_tachometer_speed(uint32_t delta_ms) {
float new_rpm = float(_pulses_count) * 60.0f * 1000.0f / float(delta_ms);
_last_check_tachometer_speed = millis();
_pulses_count = 0;
if (not isnan(new_rpm) and new_rpm not_eq _current_rpm) {
_current_rpm = new_rpm;
}
}

void _set_fan_speed(uint8_t pwm_value) {
ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, _fan_pwm_channel, pwm_value));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, _fan_pwm_channel));
}

IRAM_ATTR static void tachometer_isr(void *ctx) {
reinterpret_cast<KobaEGPUUserModCore *>(ctx)->_pulses_count++;
}

public:
KobaEGPUUserModCore() = default;

~KobaEGPUUserModCore() {
// todo destroy adc
delete _adc_chars;
gpio_intr_disable(GPIO_NUM_8);
}
Comment on lines +126 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Destructor disables wrong GPIO pin.

gpio_intr_disable(GPIO_NUM_8) is hardcoded, but the tachometer ISR was installed on FAN_TACHOMETER_PIN (GPIO 6). The ISR on the correct pin remains active after destruction.

🐛 Proposed fix
         ~KobaEGPUUserModCore() {
             //  todo destroy adc
             delete _adc_chars;
-            gpio_intr_disable(GPIO_NUM_8);
+            gpio_isr_handler_remove(gpio_num_t(FAN_TACHOMETER_PIN));
+            gpio_intr_disable(gpio_num_t(FAN_TACHOMETER_PIN));
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
~KobaEGPUUserModCore() {
// todo destroy adc
delete _adc_chars;
gpio_intr_disable(GPIO_NUM_8);
}
~KobaEGPUUserModCore() {
// todo destroy adc
delete _adc_chars;
gpio_isr_handler_remove(gpio_num_t(FAN_TACHOMETER_PIN));
gpio_intr_disable(gpio_num_t(FAN_TACHOMETER_PIN));
}
🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` around lines 126 - 130, The destructor
~KobaEGPUUserModCore currently disables the wrong GPIO (GPIO_NUM_8) while the
tachometer ISR was installed on FAN_TACHOMETER_PIN; update the destructor to
call gpio_isr_handler_remove(FAN_TACHOMETER_PIN) and then
gpio_intr_disable(FAN_TACHOMETER_PIN) (instead of GPIO_NUM_8) to properly remove
the ISR and disable interrupts, keeping the existing delete _adc_chars handling
intact.


KobaEGPUUserModCoreConfig get_config() {
return _cfg;
}

float get_fan_rpm() const {
return _current_rpm;
}

float get_gpu_temp() const {
return _last_thr_gpu_temp;
}

float get_fan_temp() const {
return _last_thr_fan_temp;
}

void set_cfg(KobaEGPUUserModCoreConfig cfg) {
if (cfg.fan_min_speed_threshold_temperature >= cfg.fan_max_speed_threshold_temperature or
cfg.fan_min_speed_threshold_temperature < 0.0f or cfg.fan_max_speed_threshold_temperature < 10.0f) {
return;
}
_cfg = cfg;
}

void setup() {
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = LEDC_TIMER_0,
.freq_hz = 20000,
.clk_cfg = LEDC_AUTO_CLK
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
ledc_channel_config_t ledc_channel = {
.gpio_num = FAN_PWM_PIN,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = _fan_pwm_channel,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_0,
.duty = 0, // Set duty to 0%
.hpoint = 0
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
_adc_chars = new esp_adc_cal_characteristics_t;
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, _adc_chars);
ESP_ERROR_CHECK(adc1_config_width(ADC_WIDTH_BIT_12));
ESP_ERROR_CHECK(adc1_config_channel_atten(adc1_channel_t(THERMISTOR_GPU_PIN), ADC_ATTEN_DB_11));
ESP_ERROR_CHECK(adc1_config_channel_atten(adc1_channel_t(THERMISTOR_FAN_PIN), ADC_ATTEN_DB_11));
ESP_ERROR_CHECK(gpio_set_direction(gpio_num_t(FAN_TACHOMETER_PIN), GPIO_MODE_INPUT));
ESP_ERROR_CHECK(gpio_set_intr_type(gpio_num_t(FAN_TACHOMETER_PIN), GPIO_INTR_NEGEDGE));
ESP_ERROR_CHECK(gpio_install_isr_service(0));
ESP_ERROR_CHECK(gpio_isr_handler_add(gpio_num_t(FAN_TACHOMETER_PIN), tachometer_isr, this));
Comment on lines +182 to +183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

gpio_install_isr_service may abort if already installed by another usermod.

If another usermod has already installed the ISR service, this call returns ESP_ERR_INVALID_STATE and ESP_ERROR_CHECK will abort. Handle this case gracefully.

🛡️ Proposed fix
-            ESP_ERROR_CHECK(gpio_install_isr_service(0));
+            esp_err_t err = gpio_install_isr_service(0);
+            if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
+                ESP_ERROR_CHECK(err);  // Will abort on unexpected errors
+            }
             ESP_ERROR_CHECK(gpio_isr_handler_add(gpio_num_t(FAN_TACHOMETER_PIN), tachometer_isr, this));
🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` around lines 182 - 183, The call to
gpio_install_isr_service(0) can return ESP_ERR_INVALID_STATE if another usermod
already installed the ISR service; replace the unconditional
ESP_ERROR_CHECK(gpio_install_isr_service(0)) with a guarded check: call
gpio_install_isr_service(0) into an esp_err_t variable, and only treat it as an
error if it's neither ESP_OK nor ESP_ERR_INVALID_STATE (i.e., ignore
ESP_ERR_INVALID_STATE), then proceed to call
gpio_isr_handler_add(gpio_num_t(FAN_TACHOMETER_PIN), tachometer_isr, this) as
before; keep references to gpio_install_isr_service, ESP_ERROR_CHECK,
gpio_isr_handler_add, FAN_TACHOMETER_PIN, tachometer_isr, and this so the change
is localized and clear.

_last_check_tachometer_speed = millis();
_configured = true;
}

void task_iter() {
if (not _configured) return;
static uint64_t last_check_temp = 0;
if (millis() - last_check_temp > _delta_update_time_ms) {
_update_temperatures();
last_check_temp = millis();
}
uint32_t delta = millis() - _last_check_tachometer_speed;
if (delta > _delta_update_time_ms) {
_update_tachometer_speed(delta);
}
}
};
}

class KobaEGPUUsermod : public Usermod {
private:

bool _init_done = false;
bool _is_enabled = true;
KobaEGPUUserModCore _core;

static const char _mod_name_key[];
static const char _mod_enabled_string[];
static const char _unavailable_string[];
static const char _celsius_prefix_string[];
static const char _rpm_prefix_string[];
static const char _fan_speed_string[];
static const char _gpu_temp_string[];
static const char _fan_temp_string[];
static const char _fan_min_speed_threshold_temperature_key[];
static const char _fan_max_speed_threshold_temperature_key[];


public:

void setup() override {
_core.setup();
_init_done = true;
}


void loop() override {
if (not _is_enabled or strip.isUpdating()) return;
_core.task_iter();
}

void addToJsonInfo(JsonObject &root) override {
JsonObject user = root["u"];
if (user.isNull()) user = root.createNestedObject("u");
{
JsonArray infoArr = user.createNestedArray(FPSTR(_mod_name_key));
String uiDomString = F("<button class=\"btn btn-xs\" onclick=\"requestJson({'");
uiDomString += FPSTR(_mod_name_key);
uiDomString += F("':{'");
uiDomString += FPSTR(_mod_enabled_string);
uiDomString += F("':");
uiDomString += _is_enabled ? "false" : "true";
uiDomString += F("}});\"><i class=\"icons ");
uiDomString += _is_enabled ? "on" : "off";
uiDomString += F("\">&#xe08f;</i></button>");
infoArr.add(uiDomString);
}
if (_is_enabled) {
{
JsonArray data = user.createNestedArray(_fan_speed_string);
if (not isnan(_core.get_fan_rpm())) {
data.add(uint32_t(_core.get_fan_rpm()));
data.add(_rpm_prefix_string);
} else {
data.add(_unavailable_string);
}
}
{
JsonArray data = user.createNestedArray(_gpu_temp_string);
if (not isnan(_core.get_gpu_temp())) {
data.add(_core.get_gpu_temp());
data.add(_celsius_prefix_string);
} else {
data.add(_unavailable_string);
}
}
{
JsonArray data = user.createNestedArray(_fan_temp_string);
if (not isnan(_core.get_fan_temp())) {
data.add(_core.get_fan_temp());
data.add(_celsius_prefix_string);
} else {
data.add(_unavailable_string);
}
}
}
}


void readFromJsonState(JsonObject &root) override {
// from info section
if (not _init_done) return;
JsonObject usermod = root[FPSTR(_mod_name_key)];

if (usermod.isNull()) return;
if (usermod[FPSTR(_mod_enabled_string)].is<bool>()) {
_is_enabled = usermod[FPSTR(_mod_enabled_string)].as<bool>();
}
}

void addToConfig(JsonObject &root) override {
JsonObject top = root.createNestedObject(FPSTR(_mod_name_key));
auto cfg = _core.get_config();
top[FPSTR(_mod_enabled_string)] = _is_enabled;
top[FPSTR(_fan_min_speed_threshold_temperature_key)] = cfg.fan_min_speed_threshold_temperature;
top[FPSTR(_fan_max_speed_threshold_temperature_key)] = cfg.fan_max_speed_threshold_temperature;
}

bool readFromConfig(JsonObject &root) override {
// from usermod settings
JsonObject top = root[FPSTR(_mod_name_key)];
if (top.isNull()) {
return false;
}
_is_enabled = top[FPSTR(_mod_enabled_string)] | _is_enabled;
KobaEGPUUserModCoreConfig cfg;
cfg.fan_min_speed_threshold_temperature =
top[FPSTR(_fan_min_speed_threshold_temperature_key)] | cfg.fan_min_speed_threshold_temperature;
cfg.fan_max_speed_threshold_temperature =
top[FPSTR(_fan_max_speed_threshold_temperature_key)] | cfg.fan_max_speed_threshold_temperature;
_core.set_cfg(cfg);
return not top[FPSTR(_mod_enabled_string)].isNull();
}

uint16_t getId() override { return USERMOD_ID_KOBA_EGPU; }
};


const char KobaEGPUUsermod::_mod_name_key[] PROGMEM = "KOBA eGPU";
const char KobaEGPUUsermod::_mod_enabled_string[] PROGMEM = "Mod enabled";
const char KobaEGPUUsermod::_unavailable_string[] PROGMEM = "n/d";
const char KobaEGPUUsermod::_celsius_prefix_string[] PROGMEM = " (C)";
const char KobaEGPUUsermod::_rpm_prefix_string[] PROGMEM = " RPM";
const char KobaEGPUUsermod::_fan_speed_string[] PROGMEM = "FAN Speed";
const char KobaEGPUUsermod::_gpu_temp_string[] = "GPU Temp";
const char KobaEGPUUsermod::_fan_temp_string[] = "FAN Temp";
const char KobaEGPUUsermod::_fan_min_speed_threshold_temperature_key[] = "Fan min speed threshold temperature (C)";
const char KobaEGPUUsermod::_fan_max_speed_threshold_temperature_key[] = "Fan max speed threshold temperature (C)";
Comment on lines +328 to +331
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing PROGMEM on string constants wastes RAM.

These strings are missing PROGMEM unlike the others above, causing them to be stored in RAM instead of flash.

♻️ Add PROGMEM for consistency
-const char KobaEGPUUsermod::_gpu_temp_string[] = "GPU Temp";
-const char KobaEGPUUsermod::_fan_temp_string[] = "FAN Temp";
-const char KobaEGPUUsermod::_fan_min_speed_threshold_temperature_key[] = "Fan min speed threshold temperature (C)";
-const char KobaEGPUUsermod::_fan_max_speed_threshold_temperature_key[] = "Fan max speed threshold temperature (C)";
+const char KobaEGPUUsermod::_gpu_temp_string[] PROGMEM = "GPU Temp";
+const char KobaEGPUUsermod::_fan_temp_string[] PROGMEM = "FAN Temp";
+const char KobaEGPUUsermod::_fan_min_speed_threshold_temperature_key[] PROGMEM = "Fan min speed threshold temperature (C)";
+const char KobaEGPUUsermod::_fan_max_speed_threshold_temperature_key[] PROGMEM = "Fan max speed threshold temperature (C)";
🤖 Prompt for AI Agents
In `@usermods/KOBA_EGPU/koba_egpu.cpp` around lines 328 - 331, The string
constants KobaEGPUUsermod::_gpu_temp_string, KobaEGPUUsermod::_fan_temp_string,
KobaEGPUUsermod::_fan_min_speed_threshold_temperature_key, and
KobaEGPUUsermod::_fan_max_speed_threshold_temperature_key are missing the
PROGMEM attribute; update their declarations to store them in flash (add PROGMEM
to each const char array declaration to match the other strings in this class)
so they are defined like the other PROGMEM strings in KobaEGPUUsermod.


static KobaEGPUUsermod koba_egpu;
REGISTER_USERMOD(koba_egpu);
6 changes: 6 additions & 0 deletions usermods/KOBA_EGPU/library.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "KOBA_EGPU",
"build": {
"libArchive": false
}
}
53 changes: 53 additions & 0 deletions usermods/KOBA_EGPU/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Koba EGPU

The mod does not make significant changes to the WLED system, but only supplements it with an additional, optional mod. If we describe the mod most compactly, then it is intended for a self-made eGPU station. The system controls the backlight of the case cooling cooler (ARGB) and the backlight of the power supply (RGB). The main task of the system is to control the rotation speed of the cooling cooler, based on an NTC thermistor mounted on the radiator of the video card. The second NTC resistor is designed to assess the temperature of the air next to the cooling cooler before it leaves the box.

## Description

At the end of 2024, I upgraded my laptop, which lacked a high-performance graphics card, but it was compact and convenient. Sometimes my tasks require a video accelerator and parallel computing, and sometimes I also want to play. Therefore, I decided to study eGPU adapters. I purchased a TH3P4, a power supply with an LLC converter (no squeak during operation and high efficiency), as well as an NVIDIA RTX 3070 graphics card. I checked the bundle, everything was fine, but for beauty I didn't have enough housing for the system. I decided to make a case with my father out of plexiglass and aluminum, as he had done it many times in his life. We had a great time together, we made it, but in order for everything to work properly, we needed a microcontroller (to control the rotation speed of the cooler cooling box). And since I'm adding a microcontroller, it would be nice to make my design a little more beautiful by adding lights and the ability to control it through a smart HomeAssistant. The Wled firmware was the best option for my task. I explored the possibilities of using mods, looked at examples, and made my own implementation. I have seen other implementations of mods with cooler rotation speed control, however, their code and implementations, in my opinion, were quite simple. That's why I wrote my own mod, which in my opinion is better made and tested. The only bug that exists now is an incorrect determination of the speed of rotation of the cooler by the tachometer. I don't know why, but while I was testing the board, everything worked well, but when I installed it in the case, added some of the changes, it stopped showing the correct values. But to be honest, I no longer needed the rotation speed value after debugging. I think the problem is related to the features of the ESP32-C3, since it doesn't work out to calculate revolutions beautifully. The microcontroller does not have an impulse counter module. Maybe this mod will be useful to someone, so I tried to document and describe it better, as well as show my board.



### The appearance of the connection layout board on both sides.

The right fragment is reflected horizontally.
![egpu-board](./egpu-board.webp)
The resistors next to the thermistors must have the same nominal value. The resistors in the transistor circuits are probably 1k, I don't remember exactly. The transistors themselves and the corresponding resistors may have been chosen suboptimally by me, but I did it in a hurry and from what I had at hand. Your implementation may be different.

### Platformio configuration for using the mod.
You can add this fragment to platformio_override.ini and restart platformio in the development environment so that the configuration becomes available for use.

```ini
[env:esp32c3_ar_egpu]
extends = env:esp32c3dev
board = esp32-c3-mini
upload_speed = 1000000
monitor_filters = esp32_exception_decoder
build_flags = ${env:esp32c3dev.build_flags} ${esp32.AR_build_flags}
-D USERMOD_AUDIOREACTIVE
-D WLED_DISABLE_ALEXA
-D WLED_DISABLE_ADALIGHT
-D WLED_DISABLE_LOXONE
-D WLED_DISABLE_HUESYNC
-D WLED_DISABLE_INFRARED
-D WLED_NTP_ENABLED=true
-DARDUINO_USB_CDC_ON_BOOT=1
-DARDUINO_USB_MODE=1
-D DATA_PINS=7,3,2
-D LED_TYPES=TYPE_ANALOG_3CH
-D PIXEL_COUNTS=10
-D BTNPIN=-1,-1
-D BTNTYPE=BTN_TYPE_PUSH,BTN_TYPE_NONE
-D ABL_MILLIAMPS_DEFAULT=0
-D MDNS_NAME='"WLED-Koba-eGPU"'
-D FAN_PWM_PIN=10
-D FAN_TACHOMETER_PIN=6
-D THERMISTOR_GPU_PIN=0
-D THERMISTOR_FAN_PIN=1
-D FAN_LED_RED_PIN=7
-D FAN_LED_GREEN_PIN=3
-D FAN_LED_BLUE_PIN=2
custom_usermods = audioreactive Internal_Temperature_v2 KOBA_EGPU
lib_deps = ${env:esp32c3dev.lib_deps}
${esp32.AR_lib_deps}
```
Loading