Previous kick-start only fired when duty < 200, but all level duties are ≥230, so L1 starts from rest at PWM 230 and the rotor can't break static friction. Now we briefly drive 100% on any off→on transition before dropping to the target duty. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
228 lines
5.8 KiB
C++
228 lines
5.8 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() {
|
|
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);
|
|
}
|