├── LICENSE
├── README.md
├── esp8266-fan-control-diagram.drawio
├── esp8266-fan-control-diagram.png
├── fan-control-and-influxdb
└── fan-control-and-influxdb.ino
├── fan-control-only
└── fan-control-only.ino
└── temperature-fan-graph.png
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2021, Stefan Thoss
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # esp8266-fan-control
2 |
3 | ESP8266-based PWM fan control with a BME280 temperature sensor and an optional InfluxDB 2.0 integration.
4 |
5 | ## Hardware
6 |
7 | The following hardware is used:
8 | * [Adafruit Feather HUZZAH with ESP8266](https://www.adafruit.com/products/2821)
9 | * [Noctua NF-A12x25 5V PWM](https://noctua.at/en/nf-a12x25-5v-pwm)
10 | * BME280 sensor
11 | * 2kΩ resistor (anything above 1kΩ should work, do your own research if you don't want to risk frying your ESP8266)
12 |
13 | ## Setup
14 |
15 | The ESP8266 needs to be powered via USB.
16 |
17 | 
18 |
19 | The BME280 sensor gets powered via the 3.3V and GND ports and data communication is connected via I2C using the SCL and SDA pins.
20 |
21 | The PWM fan is powered (yellow wire) via the VBUS pin which provides 5V directly from the USB power. Connect GND of the fan (black wire) to the same GND as the sensor. For the PWM signal (blue wire) use GPIO12 (pin 6). The RPM speed signal (green wire) needs a 5V GPIO signal but the ESP8266's GPIO pins run on 3.3V. This can be solved by using a pull-up resistor (I use a 2kΩ resistor, larger resistors should work as well). Connect the RPM speed signal to GPIO13 (pin 7) and to the 3.3V pin through the pull-up resistor. For more details on the Noctua PWM fan specifications, check out the [Noctua PWM specifications white paper](https://noctua.at/pub/media/wysiwyg/Noctua_PWM_specifications_white_paper.pdf)
22 |
23 | ## Software
24 |
25 | This software uses Adafruit's ESP8266 Feather board with the Arduino IDE as described [here](https://learn.adafruit.com/adafruit-feather-huzzah-esp8266/using-arduino-ide).
26 |
27 | There are two version of the software:
28 |
29 | * `fan-control-only` controls the fan with the behavior described below.
30 | * `fan-control-and-influxdb` controls the fan and also transmits temperature and fan data via Wi-Fi to an InfluxDB server.
31 |
32 | ## Behavior
33 |
34 | Adapt the constants at the beginning of the Arduino sketch to change the values.
35 |
36 | 
37 |
38 | The fan does not spin below `minTemp`. As soon as the temperature rises above `minTemp`, the fan starts spinning at `minFanSpeedPercent`. This is implemented because a lot of fans have a minimum rotational speed (e.g. the Noctua fan has a minimum speed of 450 RPM and a maximum speed of 1900 RPM, thus the 24% minimum). The fan starts spinning faster linearly until the temperature hits `maxTemp` at which the fan spins at 100%.
39 |
40 | ## InfluxDB
41 |
42 | When using the InfluxDB version, first configure the Wi-Fi and InfluxDB connection at the top of the script. It is meant to be used with an InfluxDB 2.0 instance that is configured via URL, authentication token, organization, and bucket name. You can check the connection information via the serial console (baud rate 9600). It will connect to Wi-Fi, print the IP, synchronize the time via NTP, and connect to the InfluxDB server. Finally, it will start printing temperature and fan information in an infinite loop:
43 |
44 | ```
45 | Connecting to Wi-Fi.....
46 | Wi-Fi connected. IP address: 192.168.1.11
47 |
48 | Syncing time.
49 | Synchronized time: Sun May 23 17:57:09 2021
50 |
51 | Connected to InfluxDB: https://192.168.1.10:8086
52 |
53 | Temperature is 29.12 deg C
54 | Setting fan speed to 44 %
55 | Fan speed is 384 RPM
56 | ```
57 |
58 | The software will write a measurement called `fan_control` every 10 seconds in the InfluxDB bucket that you configured. That measurement has 3 fields:
59 |
60 | * `temperature` (unit: °C): The measured temperature.
61 | * `fan_speed_percent` (unit: %): The speed that the fan is supposed to spin at based on the configuration.
62 | * `actual_fan_speed_rpm` (unit: RPM): The speed that the fan is actually spinning at as measured by the fan's RPM speed signal.
63 |
64 | ## Grafana Dashboard
65 |
66 | t.b.d.
67 |
--------------------------------------------------------------------------------
/esp8266-fan-control-diagram.drawio:
--------------------------------------------------------------------------------
1 | 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
--------------------------------------------------------------------------------
/esp8266-fan-control-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanthoss/esp8266-fan-control/58f7d462a0dbed7cecfd0e1a701d433f90cd5924/esp8266-fan-control-diagram.png
--------------------------------------------------------------------------------
/fan-control-and-influxdb/fan-control-and-influxdb.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #define WIFI_SSID "SSID"
9 | #define WIFI_PASSWORD "PASSWORD"
10 |
11 | #define INFLUXDB_URL "influxdb-url"
12 | #define INFLUXDB_TOKEN "token"
13 | #define INFLUXDB_ORG "org"
14 | #define INFLUXDB_BUCKET "bucket"
15 |
16 | #define TZ_INFO "PST8PDT"
17 |
18 | #define FAN_PIN 12
19 | #define SIGNAL_PIN 13
20 |
21 | #define DELAY_TIME 10000 // time between measurements [ms]
22 | #define MIN_FAN_SPEED_PERCENT 24 // minimum fan speed [%]
23 | #define MIN_TEMP 25 // turn fan off below [deg C]
24 | #define MAX_TEMP 40 // turn fan to full speed above [deg C]
25 |
26 | Adafruit_BME280 bme; // I2C
27 |
28 | ESP8266WiFiMulti wifiMulti;
29 | InfluxDBClient client(INFLUXDB_URL, INFLUXDB_ORG, INFLUXDB_BUCKET, INFLUXDB_TOKEN);
30 | Point sensor("fan_control");
31 |
32 | void setup() {
33 | Serial.begin(9600);
34 |
35 | pinMode(FAN_PIN, OUTPUT);
36 | pinMode(SIGNAL_PIN, INPUT);
37 |
38 | bool status = bme.begin(0x76);
39 |
40 | if (!status) {
41 | Serial.println("Could not find a valid BME280 sensor.");
42 | while (1);
43 | }
44 |
45 | // Setup Wi-Fi
46 | WiFi.mode(WIFI_STA);
47 | wifiMulti.addAP(WIFI_SSID, WIFI_PASSWORD);
48 |
49 | Serial.print("Connecting to Wi-Fi");
50 | while (wifiMulti.run() != WL_CONNECTED) {
51 | Serial.print(".");
52 | delay(500);
53 | }
54 | Serial.println();
55 | Serial.print("Wi-Fi connected. IP address: ");
56 | Serial.println(WiFi.localIP());
57 | Serial.println();
58 |
59 | timeSync(TZ_INFO, "pool.ntp.org", "time.nis.gov");
60 |
61 | // Check server connection
62 | client.setInsecure();
63 | if (client.validateConnection()) {
64 | Serial.print("Connected to InfluxDB: ");
65 | Serial.println(client.getServerUrl());
66 | } else {
67 | Serial.print("InfluxDB connection failed: ");
68 | Serial.println(client.getLastErrorMessage());
69 | }
70 | Serial.println();
71 | }
72 |
73 | int getFanSpeedRpm() {
74 | int highTime = pulseIn(SIGNAL_PIN, HIGH);
75 | int lowTime = pulseIn(SIGNAL_PIN, LOW);
76 | int period = highTime + lowTime;
77 | if (period == 0) {
78 | return 0;
79 | }
80 | float freq = 1000000.0 / (float)period;
81 | return (freq * 60.0) / 2.0; // two cycles per revolution
82 | }
83 |
84 | void setFanSpeedPercent(int p) {
85 | int value = (p / 100.0) * 255;
86 | analogWrite(FAN_PIN, value);
87 | }
88 |
89 | void loop() {
90 | sensor.clearFields();
91 |
92 | float temp = bme.readTemperature();
93 | sensor.addField("temperature", temp);
94 | Serial.print("Temperature is ");
95 | Serial.print(temp);
96 | Serial.println(" deg C");
97 |
98 | int fanSpeedPercent, actualFanSpeedRpm;
99 |
100 | if (temp < MIN_TEMP) {
101 | fanSpeedPercent = 0;
102 | } else if (temp > MAX_TEMP) {
103 | fanSpeedPercent = 100;
104 | } else {
105 | fanSpeedPercent = (100 - MIN_FAN_SPEED_PERCENT) * (temp - MIN_TEMP) / (MAX_TEMP - MIN_TEMP) + MIN_FAN_SPEED_PERCENT;
106 | }
107 |
108 | sensor.addField("fan_speed_percent", fanSpeedPercent);
109 | Serial.print("Setting fan speed to ");
110 | Serial.print(fanSpeedPercent);
111 | Serial.println(" %");
112 | setFanSpeedPercent(fanSpeedPercent);
113 |
114 | actualFanSpeedRpm = getFanSpeedRpm();
115 | sensor.addField("actual_fan_speed_rpm", actualFanSpeedRpm);
116 | Serial.print("Fan speed is ");
117 | Serial.print(actualFanSpeedRpm);
118 | Serial.println(" RPM");
119 |
120 | Serial.println();
121 | if (!client.writePoint(sensor)) {
122 | Serial.print("InfluxDB write failed: ");
123 | Serial.println(client.getLastErrorMessage());
124 | Serial.println();
125 | }
126 | delay(DELAY_TIME);
127 | }
128 |
--------------------------------------------------------------------------------
/fan-control-only/fan-control-only.ino:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #define FAN_PIN 12
6 | #define SIGNAL_PIN 13
7 |
8 | #define DELAY_TIME 10000 // time between measurements [ms]
9 | #define MIN_FAN_SPEED_PERCENT 24 // minimum fan speed [%]
10 | #define MIN_TEMP 25 // turn fan off below [deg C]
11 | #define MAX_TEMP 40 // turn fan to full speed above [deg C]
12 |
13 | Adafruit_BME280 bme; // I2C
14 |
15 | void setup() {
16 | Serial.begin(9600);
17 |
18 | pinMode(FAN_PIN, OUTPUT);
19 | pinMode(SIGNAL_PIN, INPUT);
20 |
21 | bool status = bme.begin(0x76);
22 |
23 | if (!status) {
24 | Serial.println("Could not find a valid BME280 sensor.");
25 | while (1);
26 | }
27 | }
28 |
29 | int getFanSpeedRpm() {
30 | int highTime = pulseIn(SIGNAL_PIN, HIGH);
31 | int lowTime = pulseIn(SIGNAL_PIN, LOW);
32 | int period = highTime + lowTime;
33 | if (period == 0) {
34 | return 0;
35 | }
36 | float freq = 1000000.0 / (float)period;
37 | return (freq * 60.0) / 2.0; // two cycles per revolution
38 | }
39 |
40 | void setFanSpeedPercent(int p) {
41 | int value = (p / 100.0) * 255;
42 | analogWrite(FAN_PIN, value);
43 | }
44 |
45 | void loop() {
46 | float temp = bme.readTemperature();
47 | Serial.print("Temperature is ");
48 | Serial.print(temp);
49 | Serial.println(" deg C");
50 |
51 | int fanSpeedPercent, actualFanSpeedRpm;
52 |
53 | if (temp < MIN_TEMP) {
54 | fanSpeedPercent = 0;
55 | } else if (temp > MAX_TEMP) {
56 | fanSpeedPercent = 100;
57 | } else {
58 | fanSpeedPercent = (100 - MIN_FAN_SPEED_PERCENT) * (temp - MIN_TEMP) / (MAX_TEMP - MIN_TEMP) + MIN_FAN_SPEED_PERCENT;
59 | }
60 |
61 | Serial.print("Setting fan speed to ");
62 | Serial.print(fanSpeedPercent);
63 | Serial.println(" %");
64 | setFanSpeedPercent(fanSpeedPercent);
65 |
66 | actualFanSpeedRpm = getFanSpeedRpm();
67 | Serial.print("Fan speed is ");
68 | Serial.print(actualFanSpeedRpm);
69 | Serial.println(" RPM");
70 |
71 | Serial.println();
72 | delay(DELAY_TIME);
73 | }
74 |
--------------------------------------------------------------------------------
/temperature-fan-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanthoss/esp8266-fan-control/58f7d462a0dbed7cecfd0e1a701d433f90cd5924/temperature-fan-graph.png
--------------------------------------------------------------------------------