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 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 01:16:12 +02:00
parent 2e4cfde547
commit cdf9b081bc
2 changed files with 184 additions and 0 deletions

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ obj/
# Dependencies and libraries
node_modules/
dist/
main/secrets.h

183
main/main.ino Normal file
View File

@@ -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 <U8g2lib.h>
#include <DHT.h>
#include <WiFi.h>
#include <PubSubClient.h>
#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);
}