// 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() { static uint8_t lastDuty = 0; if (fanLevel == 0) { ledcWrite(FAN_PIN, 0); lastDuty = 0; return; } uint8_t duty = LEVELS[fanLevel - 1].duty; // Kick-start whenever spinning up from stopped — full duty briefly so the // rotor breaks static friction, then drop to the target. if (lastDuty == 0) { ledcWrite(FAN_PIN, 255); delay(400); } ledcWrite(FAN_PIN, duty); lastDuty = 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\n", WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.setTxPower(WIFI_POWER_8_5dBm); WiFi.disconnect(true, true); delay(200); WiFi.setAutoReconnect(true); WiFi.persistent(false); WiFi.begin(WIFI_SSID, WIFI_PASS); uint8_t result = WiFi.waitForConnectResult(15000); if (result == WL_CONNECTED) { Serial.printf("WiFi connected: %s\n", WiFi.localIP().toString().c_str()); } else { Serial.printf("WiFi failed, result=%u, status=%d\n", result, WiFi.status()); } } 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) { (void)humidity; char tempStr[12]; snprintf(tempStr, sizeof(tempStr), "%.1f", tempC); u8g2.clearBuffer(); // Large temperature reading + small °C suffix, centered as a unit. u8g2.setFont(u8g2_font_logisoso22_tn); int tempW = u8g2.getStrWidth(tempStr); const int suffixW = 10; // approx width reserved for "°C" const int gap = 2; int totalW = tempW + gap + suffixW; int tempX = (128 - totalW) / 2; int tempY = 46; u8g2.drawStr(tempX, tempY, tempStr); u8g2.setFont(u8g2_font_6x10_tr); int suffixX = tempX + tempW + gap; u8g2.drawStr(suffixX, tempY - 14, "o"); u8g2.drawStr(suffixX, tempY - 2, "C"); // Fan level indicator: 5 stacked bars at the bottom. // Filled bars = current level, empty bars = remaining. const int barCount = NUM_LEVELS; const int barW = 18; const int barH = 6; const int barGap = 2; const int barsW = barCount * barW + (barCount - 1) * barGap; const int barY = 56; int barX0 = (128 - barsW) / 2; for (uint8_t i = 0; i < barCount; i++) { int x = barX0 + i * (barW + barGap); if (i < fanLevel) { u8g2.drawBox(x, barY, barW, barH); } else { u8g2.drawFrame(x, barY, barW, barH); } } 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); }