From cdf9b081bc8eac428a4175266acf4979bc193b34 Mon Sep 17 00:00:00 2001 From: dtoro Date: Thu, 9 Apr 2026 01:16:12 +0200 Subject: [PATCH] feat: integrate DHT22, OLED, WiFi/MQTT with 5-level temp-driven PWM fan Fan speed is now auto-selected from a temperature table (levels 1-5 at 28/30/32/34/36C mapping to PWM duties 230/236/242/249/255) with 1C hysteresis. State and level are published to MQTT for Home Assistant. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + main/main.ino | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 main/main.ino diff --git a/.gitignore b/.gitignore index a101992..0e3688a 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ obj/ # Dependencies and libraries node_modules/ dist/ +main/secrets.h diff --git a/main/main.ino b/main/main.ino new file mode 100644 index 0000000..2796986 --- /dev/null +++ b/main/main.ino @@ -0,0 +1,183 @@ +// fan4life main +// ESP32-C3 | Arduino core 3.x +// 2-pin 5V fan via IRLZ44N MOSFET (PWM) +// DHT22 temp/humidity, SSD1306 OLED, WiFi + MQTT (state publish only) + +#include +#include +#include +#include +#include "secrets.h" + +// --- MQTT topics (publish only) --- +#define TOPIC_TEMP "esp32c3_fan/temperature" +#define TOPIC_HUMIDITY "esp32c3_fan/humidity" +#define TOPIC_FAN_STATE "esp32c3_fan/fan/state" +#define TOPIC_FAN_SPEED "esp32c3_fan/fan/speed" + +// --- Pins --- +#define DHTPIN 2 +#define DHTTYPE DHT22 +#define FAN_PIN 3 +#define OLED_SDA 5 +#define OLED_SCL 6 + +// --- PWM --- +#define PWM_FREQ 20000 +#define PWM_RES 8 + +// --- Temperature -> fan level table --- +// Level 0 = off. Higher temp -> higher level. Hysteresis: drop a level +// only after temp falls TEMP_HYST below the level's threshold. +#define TEMP_HYST 1.0 +struct FanLevel { const char* label; float tempOn; uint8_t duty; }; +const FanLevel LEVELS[] = { + { "1", 28.0, 230 }, + { "2", 30.0, 236 }, + { "3", 32.0, 242 }, + { "4", 34.0, 249 }, + { "5", 36.0, 255 }, +}; +const uint8_t NUM_LEVELS = sizeof(LEVELS) / sizeof(LEVELS[0]); +#define TEMP_OFF (LEVELS[0].tempOn - TEMP_HYST) + +// --- OLED --- +#define OLED_RESET U8X8_PIN_NONE +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, OLED_RESET, OLED_SCL, OLED_SDA); + +DHT dht(DHTPIN, DHTTYPE); +WiFiClient wifiClient; +PubSubClient mqtt(wifiClient); + +int xOffset = 30; +int yOffset = 30; + +uint8_t fanLevel = 0; // 0 = off, 1..NUM_LEVELS + +void applyFan() { + if (fanLevel == 0) { + ledcWrite(FAN_PIN, 0); + return; + } + uint8_t duty = LEVELS[fanLevel - 1].duty; + if (duty > 0 && duty < 200) { + ledcWrite(FAN_PIN, 255); // kick-start + delay(300); + } + ledcWrite(FAN_PIN, duty); +} + +void publishFan() { + mqtt.publish(TOPIC_FAN_STATE, fanLevel > 0 ? "ON" : "OFF", true); + const char* label = fanLevel > 0 ? LEVELS[fanLevel - 1].label : "0"; + mqtt.publish(TOPIC_FAN_SPEED, label, true); +} + +void setFanLevel(uint8_t level) { + if (level == fanLevel) return; + fanLevel = level; + applyFan(); + publishFan(); +} + +// Pick the level for a given temperature, with hysteresis from current level. +uint8_t levelForTemp(float tempC) { + uint8_t target = 0; + for (uint8_t i = 0; i < NUM_LEVELS; i++) { + if (tempC >= LEVELS[i].tempOn) target = i + 1; + } + // Hysteresis: only drop if temp is clearly below the current level's on-point. + if (target < fanLevel) { + float dropBelow = LEVELS[fanLevel - 1].tempOn - TEMP_HYST; + if (tempC > dropBelow) target = fanLevel; + } + return target; +} + +void connectWiFi() { + Serial.printf("Connecting to %s", WIFI_SSID); + WiFi.begin(WIFI_SSID, WIFI_PASS); + while (WiFi.status() != WL_CONNECTED) { + delay(500); Serial.print("."); + } + Serial.printf("\nWiFi connected: %s\n", WiFi.localIP().toString().c_str()); +} + +void connectMQTT() { + while (!mqtt.connected()) { + Serial.print("Connecting MQTT..."); + if (mqtt.connect(MQTT_CLIENT, MQTT_USER, MQTT_PASS)) { + Serial.println("connected!"); + publishFan(); + } else { + Serial.printf("failed rc=%d, retry in 5s\n", mqtt.state()); + delay(5000); + } + } +} + +void handle_oled(float tempC, float humidity) { + char line1[20], line2[20], line3[20]; + snprintf(line1, sizeof(line1), "T: %.1f C", tempC); + snprintf(line2, sizeof(line2), "H: %.0f %%", humidity); + const char* lvl = fanLevel > 0 ? LEVELS[fanLevel - 1].label : "0"; + snprintf(line3, sizeof(line3), "Fan: %s L%s", + fanLevel > 0 ? "ON" : "OFF", lvl); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_4x6_tr); + u8g2.drawStr(xOffset, yOffset, line1); + u8g2.drawStr(xOffset, yOffset + 10, line2); + u8g2.drawStr(xOffset, yOffset + 20, line3); + u8g2.sendBuffer(); +} + +void setup() { + Serial.begin(115200); + delay(500); + + ledcAttach(FAN_PIN, PWM_FREQ, PWM_RES); + ledcWrite(FAN_PIN, 0); + + dht.begin(); + + u8g2.begin(); + u8g2.setContrast(255); + u8g2.setBusClock(400000); + u8g2.clearDisplay(); + + connectWiFi(); + mqtt.setServer(MQTT_HOST, MQTT_PORT); + connectMQTT(); + + Serial.println("Ready."); +} + +void loop() { + if (WiFi.status() != WL_CONNECTED) connectWiFi(); + if (!mqtt.connected()) connectMQTT(); + mqtt.loop(); + + float tempC = dht.readTemperature(); + float humidity = dht.readHumidity(); + + if (isnan(tempC) || isnan(humidity)) { + Serial.println("DHT22 read failed"); + delay(2000); + return; + } + + // Auto fan control: temperature -> level (with hysteresis) + setFanLevel(levelForTemp(tempC)); + + // Publish telemetry + char buf[10]; + dtostrf(tempC, 4, 1, buf); mqtt.publish(TOPIC_TEMP, buf); + dtostrf(humidity, 4, 0, buf); mqtt.publish(TOPIC_HUMIDITY, buf); + + Serial.printf("T:%.1fC H:%.0f%% Fan:%s L%u\n", + tempC, humidity, fanLevel > 0 ? "ON" : "OFF", fanLevel); + handle_oled(tempC, humidity); + + delay(3000); +}