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>
ESP32-C3 Fan Controller
An automatic server fan controller built on the ESP32-C3. Drives a 5 V fan over PWM through an IRLZ44N MOSFET, auto-selects fan speed from a temperature table with hysteresis, shows live status on an OLED, and publishes sensor/fan state to Home Assistant over MQTT.
Features
- Temperature-driven PWM fan control — 5 discrete speed levels, selected automatically from the current temperature
- Hysteresis — 1 °C drop-down margin prevents level flapping
- DHT22 sensor — reads temperature and humidity every 3 s
- SSD1306 OLED — displays live temp, humidity, fan state, and level
- MQTT publishing — streams sensor and fan data for Home Assistant
- WiFi — connects on boot, auto-reconnects WiFi and MQTT if they drop
Hardware
| Component | Details |
|---|---|
| Microcontroller | ESP32-C3 |
| Sensor | DHT22 (temperature + humidity) |
| Driver | IRLZ44N N-channel MOSFET (logic-level) |
| Fan | 2-wire 5 V fan |
| Display | SSD1306 128×64 OLED (I²C) |
Wiring
| ESP32-C3 Pin | Connected To |
|---|---|
GPIO2 |
DHT22 DATA (+ 4.7 kΩ pullup to 3V3) |
GPIO3 |
MOSFET gate (via ~150 Ω) + 100 kΩ gate→GND pulldown |
GPIO5 (SDA) |
OLED SDA |
GPIO6 (SCL) |
OLED SCL |
3V3 |
DHT22 VCC, OLED VCC |
GND |
DHT22 GND, OLED GND, MOSFET source, fan − |
Fan wiring:
- Fan + → USB 5 V
- Fan − → MOSFET drain
- MOSFET source → GND
- MOSFET gate →
GPIO3
Configuration
Copy main/secrets.h.example to main/secrets.h and fill in your values:
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASS "your_wifi_password"
#define MQTT_HOST "192.168.1.X"
#define MQTT_PORT 1883
#define MQTT_USER "mqtt_user"
#define MQTT_PASS "mqtt_password"
#define MQTT_CLIENT "esp32c3_fan"
secrets.h is gitignored.
Fan Level Table
Fan speed is selected from a table of (temperature threshold → PWM duty) in main/main.ino:
| Level | Temp ≥ | PWM duty (0–255) |
|---|---|---|
| 0 (off) | — | 0 |
| 1 | 28 °C | 230 |
| 2 | 30 °C | 236 |
| 3 | 32 °C | 242 |
| 4 | 34 °C | 249 |
| 5 | 36 °C | 255 |
TEMP_HYST (default 1.0 °C) controls the drop-down margin: the fan only drops a level once temperature falls this much below that level's threshold. Below level 1's threshold minus hysteresis, the fan turns off.
Tune LEVELS[] and TEMP_HYST in main/main.ino to match your thermal setup.
MQTT Topics
All topics are publish-only (the firmware does not accept remote commands).
| Topic | Payload | Notes |
|---|---|---|
esp32c3_fan/temperature |
23.5 |
°C, every loop |
esp32c3_fan/humidity |
41 |
%, every loop |
esp32c3_fan/fan/state |
ON / OFF |
retained |
esp32c3_fan/fan/speed |
0..5 |
current level, retained |
Home Assistant Setup
Add to configuration.yaml:
mqtt:
sensor:
- name: "Room Temperature"
unique_id: "esp32c3_temperature"
state_topic: "esp32c3_fan/temperature"
unit_of_measurement: "°C"
device_class: temperature
- name: "Room Humidity"
unique_id: "esp32c3_humidity"
state_topic: "esp32c3_fan/humidity"
unit_of_measurement: "%"
device_class: humidity
- name: "Server Fan Level"
unique_id: "esp32c3_fan_level"
state_topic: "esp32c3_fan/fan/speed"
binary_sensor:
- name: "Server Fan"
unique_id: "esp32c3_fan"
state_topic: "esp32c3_fan/fan/state"
payload_on: "ON"
payload_off: "OFF"
device_class: running
Restart HA after saving. Requires the Mosquitto broker add-on installed and running.
Dependencies
Install via Arduino IDE Library Manager:
- U8g2 — OLED display
- DHT sensor library — DHT22
- PubSubClient — MQTT client
- WiFi (built-in with ESP32 board package)
Project Structure
fan4life/
├── main/
│ ├── main.ino # Main sketch
│ ├── secrets.h # WiFi & MQTT credentials (gitignored)
│ └── secrets.h.example # Credentials template
├── cad/ # Mechanical / enclosure files
├── .gitignore
└── README.md
License
MIT