Files
fan4life/main/main.ino
2026-04-26 10:09:22 +02:00

223 lines
5.6 KiB
C++

// 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\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);
}