├── .github
└── workflows
│ └── build-platformio.yml
├── .gitignore
├── .travis.yml
├── README.md
├── include
└── README
├── platformio.ini
└── src
├── OTA.cpp
├── OTA.h
├── config.h
├── config_override_example.h
├── fanPWM.cpp
├── fanPWM.h
├── fanTacho.cpp
├── fanTacho.h
├── log.cpp
├── log.h
├── main.cpp
├── mqtt.cpp
├── mqtt.h
├── sensorBME280.cpp
├── sensorBME280.h
├── temperatureController.cpp
├── temperatureController.h
├── tft.cpp
├── tft.h
├── tftTouch.cpp
├── tftTouch.h
├── wifiCommunication.cpp
└── wifiCommunication.h
/.github/workflows/build-platformio.yml:
--------------------------------------------------------------------------------
1 | name: PlatformIO build
2 |
3 | on: [push,workflow_dispatch]
4 |
5 | jobs:
6 | build:
7 | strategy:
8 | fail-fast: false
9 | matrix:
10 | os: [ubuntu-latest, windows-latest]
11 | runs-on: ${{ matrix.os }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: actions/cache@v4
15 | with:
16 | path: |
17 | ~/.cache/pip
18 | ~/.platformio/.cache
19 | key: ${{ runner.os }}-pio
20 | - uses: actions/setup-python@v5
21 | with:
22 | python-version: "3.12"
23 |
24 | - name: Install PlatformIO Core
25 | run: pip install --upgrade platformio
26 |
27 | - name: Build PlatformIO env:ESP32
28 | run: pio run
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pio
2 | .vscode/.browse.c_cpp.db*
3 | .vscode/c_cpp_properties.json
4 | .vscode/extensions.json
5 | .vscode/launch.json
6 | .vscode/ipch
7 | src/config_override.h
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Continuous Integration (CI) is the practice, in software
2 | # engineering, of merging all developer working copies with a shared mainline
3 | # several times a day < https://docs.platformio.org/page/ci/index.html >
4 | #
5 | # Documentation:
6 | #
7 | # * Travis CI Embedded Builds with PlatformIO
8 | # < https://docs.travis-ci.com/user/integration/platformio/ >
9 | #
10 | # * PlatformIO integration with Travis CI
11 | # < https://docs.platformio.org/page/ci/travis.html >
12 | #
13 | # * User Guide for `platformio ci` command
14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html >
15 | #
16 | #
17 | # Please choose one of the following templates (proposed below) and uncomment
18 | # it (remove "# " before each line) or use own configuration according to the
19 | # Travis CI documentation (see above).
20 | #
21 |
22 |
23 | #
24 | # Template #1: General project. Test it using existing `platformio.ini`.
25 | #
26 |
27 | # language: python
28 | # python:
29 | # - "2.7"
30 | #
31 | # sudo: false
32 | # cache:
33 | # directories:
34 | # - "~/.platformio"
35 | #
36 | # install:
37 | # - pip install -U platformio
38 | # - platformio update
39 | #
40 | # script:
41 | # - platformio run
42 |
43 |
44 | #
45 | # Template #2: The project is intended to be used as a library with examples.
46 | #
47 |
48 | # language: python
49 | # python:
50 | # - "2.7"
51 | #
52 | # sudo: false
53 | # cache:
54 | # directories:
55 | # - "~/.platformio"
56 | #
57 | # env:
58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c
59 | # - PLATFORMIO_CI_SRC=examples/file.ino
60 | # - PLATFORMIO_CI_SRC=path/to/test/directory
61 | #
62 | # install:
63 | # - pip install -U platformio
64 | # - platformio update
65 | #
66 | # script:
67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N
68 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # ESP32 fan controller with MQTT support
4 | This project describes how to use an ESP32 microcontroller for controlling a 4 pin fan (pwm controlled fan). Main features are:
5 | * mode 1 (fan mode or pwm mode): directly setting fan speed via pwm signal
6 | * mode 2 (climate mode or temperature controller mode): fan speed automatically increases if temperature is getting close to or higher than target temperature. Of course temperature can never get lower than air temperature of room.
7 | * measurement of fan speed via tacho signal
8 | * measurement of ambient values via BME280: temperature, humidity, pressure
9 | * support of MQTT
10 | * support of OTA (over the air updates of firmware). Please see Wiki: 07 OTA Over the air updates
11 |
12 | * TFT display for showing status information, different resolutions supported (tested with 320x240 and 160x128)
13 | * TFT touch display for setting pwm or target temperature
14 | * optional: integration into home automation software Home Assistant (with MQTT discovery) or openHAB.
15 |
16 | Even if you don't want to use all of these features, the project can hopefully easily be simplified or extended. With some minor modifications an ESP8266 / D1 mini should be usable.
17 |
18 | I did this project for having an automatic temperature controller for my 3D printer housing. But of course at least the ideas used here could be used for many other purposes.
19 |
20 | For more information please see the Wiki
21 |
22 | ## Integration in Home Assistant
23 | With mqtt discovery, you can integrate the fan controller with almost no effort in Home Assistant.
24 |
25 |
26 | Please see Wiki: 05 Home Assistant
27 |
28 | ## Operation modes
29 | You can operate the ESP32 fan controller mainly in two different modes, depending on your needs:
30 | mode | description | how to set PWM | how to set actual temperature | how to set target temperature
31 | ------------ | ------------- | ------------- | ------------- | -------------
32 | fan mode | fan speed directly set via PWM signal | MQTT, touch or both | BME280 (optional, only used for information) |
33 | climate mode | automatic temperature control
fan speed is automatically set depending on difference between target temperature and actual temperature | | MQTT or BME280 | MQTT, touch or both
34 |
35 | In both modes, a TFT panel can optionally be used for showing status information from the fan, ambient (BME280: temperature, humidity, pressure) and the chosen target temperature. Different resolutions of the TFT panel are supported, layout will automatically be adapted (tested with 320x240 and 160x128).
36 |
37 | If you use a TFT touch panel, you can set the PWM value or target temperature via the touch panel (otherwise you have to use MQTT).
38 |
39 | For more information please see the Wiki: 03 Examples - operation modes and breadboards
40 |
41 | ## Wiring diagram for fan and BME280
42 | 
43 |
44 | For more information please see the Wiki: 01 Wiring diagram
45 |
46 | ## Part list
47 | Function | Parts | Remarks | approx. price
48 | ------------ | ------------- | ------------- | -------------
49 | mandatory
50 | microcontroller | ESP32 | e.g. from AZ-Delivery | 8 EUR
51 | fan | 4 pin fan (4 pin means pwm controlled), 5V or 12V | tested with a standard CPU fan and a Noctua NF-F12 PWM
for a list of premium fans see https://noctua.at/en/products/fan | 20 EUR for Noctua
52 | measuring tacho signal of fan | - pullup resistor 10 kΩ
- RC circuit: resistor 3.3 kΩ; ceramic capacitor 100 pF
53 | power supply | - 5V for ESP32, 5V or 12V for fan (depending on fan)
or
-12V when using AZ-touch (see below) | e.g. with 5.5×2.5 mm coaxial power connector | 12 EUR
54 | optional
55 | temperature sensor | - BME280
- 2 pullup resistors 3.3 kΩ (for I2C) | e.g. from AZ-Delivery | 6.50 EUR
56 | optional
57 | TFT display (non touch) | 1.8 inch 160x128, ST7735 | e.g. from AZ-Delivery | 6.80 EUR
58 | TFT touch display with ESP32 housing | AZ-touch from AZ delivery
including voltage regulator and TFT touch display (2.8 inch 320x240, ILI9341, XPT2046) | e.g. from AZ-Delivery
(you can also use the older 2.4 inch ArduiTouch)| 30 EUR
59 | connectors for detaching parts from AZ-touch | - e.g. 5.5×2.5 mm coaxial power connector male
- JST-XH 2.54 mm for BME280
- included extra cables and connectors in case of Noctua fan
60 |
61 | Other TFTs can most likely easily be used, as long as there is a library from Adafruit for it. If resolution is smaller than 160x128 it might be necessary to change the code in file tft.cpp. Anything bigger should automatically be rearranged. If you want to use touch, your TFT should have the XPT2046 chip to use it without any code change.
62 |
63 | ## Software installation
64 | If you're only used to the Arduino IDE, I highly recommend having a look at PlatformIO IDE.
65 |
66 | While the Arduino IDE is sufficient for flashing, it is not very comfortable for software development. There is no syntax highlighting and no autocompletion. All the needed libraries have to be installed manually, and you will sooner or later run into trouble with different versions of the same library.
67 |
68 | This cannot happen with PlatformIO. All libraries will automatically be installed into the project folder and cannot influence other projects.
69 |
70 | If you absolutely want to use the Arduino IDE, please have look at the file "platformio.ini" for the libraries needed.
71 |
72 | For installing PlatformIO IDE, follow this guide. It is as simple as:
73 | * install VSCode (Visual Studio Code)
74 | * install PlatformIO as an VSCode extension
75 | * clone this repository or download it
76 | * use "open folder" in VSCode to open this repository
77 | * check settings in "config.h"
78 | * upload to ESP32
79 |
80 | ## Images
81 | ### ArduiTouch running in "climate mode"
82 | 
83 | ### Images of ESP32 fan controller used in a 3D printer housing
84 |
85 |
86 | 
87 |
88 | For more information please see the Wiki: 04 AZ‐touch / ArduiTouch
89 |
--------------------------------------------------------------------------------
/include/README:
--------------------------------------------------------------------------------
1 |
2 | This directory is intended for project header files.
3 |
4 | A header file is a file containing C declarations and macro definitions
5 | to be shared between several project source files. You request the use of a
6 | header file in your project source file (C, C++, etc) located in `src` folder
7 | by including it, with the C preprocessing directive `#include'.
8 |
9 | ```src/main.c
10 |
11 | #include "header.h"
12 |
13 | int main (void)
14 | {
15 | ...
16 | }
17 | ```
18 |
19 | Including a header file produces the same results as copying the header file
20 | into each source file that needs it. Such copying would be time-consuming
21 | and error-prone. With a header file, the related declarations appear
22 | in only one place. If they need to be changed, they can be changed in one
23 | place, and programs that include the header file will automatically use the
24 | new version when next recompiled. The header file eliminates the labor of
25 | finding and changing all the copies as well as the risk that a failure to
26 | find one copy will result in inconsistencies within a program.
27 |
28 | In C, the usual convention is to give header files names that end with `.h'.
29 | It is most portable to use only letters, digits, dashes, and underscores in
30 | header file names, and at most one dot.
31 |
32 | Read more about using header files in official GCC documentation:
33 |
34 | * Include Syntax
35 | * Include Operation
36 | * Once-Only Headers
37 | * Computed Includes
38 |
39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
40 |
--------------------------------------------------------------------------------
/platformio.ini:
--------------------------------------------------------------------------------
1 | ; PlatformIO Project Configuration File
2 | ;
3 | ; Build options: build flags, source filter
4 | ; Upload options: custom upload port, speed and extra flags
5 | ; Library options: dependencies, extra library storages
6 | ; Advanced options: extra scripting
7 | ;
8 | ; Please visit documentation for the other options and examples
9 | ; https://docs.platformio.org/page/projectconf.html
10 |
11 | [env:esp32dev]
12 | platform = espressif32
13 | board = esp32dev
14 | framework = arduino
15 | monitor_speed = 115200
16 | ; begin OTA
17 | ;upload_protocol = espota
18 | ;upload_port =
19 | ; end OTA
20 | lib_deps =
21 | knolleary/PubSubClient@^2.8
22 | adafruit/Adafruit BME280 Library@^2.2.2
23 | adafruit/Adafruit BusIO@^1.13.2
24 | adafruit/Adafruit ILI9341@^1.5.12
25 | paulstoffregen/XPT2046_Touchscreen@0.0.0-alpha+sha.26b691b2c8
26 | gerlech/TouchEvent@^1.3
27 | adafruit/Adafruit ST7735 and ST7789 Library@^1.9.3
28 | jandrassy/TelnetStream@^1.2.2
--------------------------------------------------------------------------------
/src/OTA.cpp:
--------------------------------------------------------------------------------
1 | #include "config.h"
2 |
3 | #ifdef ESP32
4 | #include
5 | #include
6 | #else
7 | #include
8 | #include
9 | #endif
10 |
11 | #include
12 | #include
13 |
14 | #if defined(useOTA_RTOS)
15 | void ota_handle( void * parameter ) {
16 | for (;;) {
17 | ArduinoOTA.handle();
18 | delay(3500);
19 | }
20 | }
21 | #endif
22 |
23 | void OTA_setup(const char* nameprefix) { // }, const char* ssid, const char* password) {
24 | // Configure the hostname
25 | // Bugfix 8 statt 7 für \0
26 | // https://github.com/SensorsIot/ESP32-OTA/pull/16
27 | // uint16_t maxlen = must be longer one byte; +1 adds space for trailing \0, otherwise the final symbol in suffix is missed
28 | uint16_t maxlen = strlen(nameprefix) + 8;
29 | char *fullhostname = new char[maxlen];
30 | uint8_t mac[6];
31 | WiFi.macAddress(mac);
32 | snprintf(fullhostname, maxlen, "%s-%02x%02x%02x", nameprefix, mac[3], mac[4], mac[5]);
33 | ArduinoOTA.setHostname(fullhostname);
34 | delete[] fullhostname;
35 |
36 | // Not neccessary. Recognises if WiFi is available or not. Even survives disconnect() and connect()
37 | /*
38 | // Configure and start the WiFi station
39 | WiFi.mode(WIFI_STA);
40 | WiFi.begin(ssid, password);
41 |
42 | // Wait for connection
43 | while (WiFi.waitForConnectResult() != WL_CONNECTED) {
44 | Serial.println("Connection Failed! Rebooting...");
45 | delay(5000);
46 | ESP.restart();
47 | }
48 | */
49 |
50 | // Port defaults to 3232
51 | // ArduinoOTA.setPort(3232); // Use 8266 port if you are working in Sloeber IDE, it is fixed there and not adjustable
52 |
53 | // No authentication by default
54 | // ArduinoOTA.setPassword("admin");
55 |
56 | // Password can be set with it's md5 value as well
57 | // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
58 | // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
59 |
60 | ArduinoOTA.onStart([]() {
61 | //NOTE: make .detach() here for all functions called by Ticker.h library - not to interrupt transfer process in any way.
62 | String type;
63 | if (ArduinoOTA.getCommand() == U_FLASH)
64 | type = "sketch";
65 | else // U_SPIFFS
66 | type = "filesystem";
67 |
68 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
69 | Serial.println("Start updating " + type);
70 | });
71 |
72 | ArduinoOTA.onEnd([]() {
73 | Serial.println("\nEnd");
74 | });
75 |
76 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
77 | Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
78 | });
79 |
80 | ArduinoOTA.onError([](ota_error_t error) {
81 | Serial.printf("Error[%u]: ", error);
82 | if (error == OTA_AUTH_ERROR) Serial.println("\nAuth Failed");
83 | else if (error == OTA_BEGIN_ERROR) Serial.println("\nBegin Failed");
84 | else if (error == OTA_CONNECT_ERROR) Serial.println("\nConnect Failed");
85 | else if (error == OTA_RECEIVE_ERROR) Serial.println("\nReceive Failed");
86 | else if (error == OTA_END_ERROR) Serial.println("\nEnd Failed");
87 | });
88 |
89 | // Do not start OTA. Save heap space and start it via MQTT only when needed.
90 | // ArduinoOTA.begin();
91 |
92 | Serial.println("OTA Initialized");
93 | Serial.print("IP address: ");
94 | Serial.println(WiFi.localIP());
95 |
96 | #if defined(useOTA_RTOS)
97 | xTaskCreate(
98 | ota_handle, /* Task function. */
99 | "OTA_HANDLE", /* String with name of task. */
100 | 10000, /* Stack size in bytes. */
101 | NULL, /* Parameter passed as input of the task */
102 | 1, /* Priority of the task. */
103 | NULL); /* Task handle. */
104 | #endif
105 | }
--------------------------------------------------------------------------------
/src/OTA.h:
--------------------------------------------------------------------------------
1 | void OTA_setup(const char* nameprefix);
--------------------------------------------------------------------------------
/src/config.h:
--------------------------------------------------------------------------------
1 | /*
2 | Before changing anything in this file, consider to copy file "config_override_example.h" to file "config_override.h" and to do your changes there.
3 | Doing so, you will
4 | - keep your credentials secret
5 | - most likely never have conflicts with new versions of this file
6 | Any define in CAPITALS can be moved to "config_override.h".
7 | All defines having BOTH lowercase and uppercase MUST stay in "config.h". They define the mode the "esp32 fan controller" is running in.
8 | */
9 |
10 | #ifndef __CONFIG_H__
11 | #define __CONFIG_H__
12 |
13 | #include
14 | #include
15 |
16 | // --- Begin: choose operation mode -------------------------------------------------------------------------------------------------------------------------------
17 | // ----------------------------------------------------------------------------------------------------------------------------------------------------------------
18 | // You have two ways to choose the operation mode:
19 | // 1. either use one of the presets
20 | // 2. or define every low level option manually
21 | // Recommendation is to start with one of the presets.
22 | // In both cases, after choosing the operation mode, go further down in this file to set additional settings needed for the chosen mode (unused options should be greyed out).
23 |
24 | #define usePresets
25 |
26 | #if defined(usePresets)
27 | // --- Way 1 to choose the operation mode: choose one of the presets. All further options are automatically set -----------------------------
28 | /* These are the presets:
29 | Fan mode: speed of the fan will be directly set, either via mqtt, via a touch display or both
30 | Climate mode: speed of the fan will be controlled by temperature. If actual temperature is higher than target temperature, fan runs at high speed.
31 | Of course, actual temperature can never be lower than the air temperature which is transported by the fan from "outside" to "inside".
32 | Actual temperature: the actual temperature can be measured by an BME280 or can be provided via mqtt
33 | Target temperature: the target temperature will be tried to reach. The target temmperate can be provided via mqtt, via a touch display or both.
34 | */
35 | // --- Begin: list of presets. Choose exactly one. ---
36 | #define fan_controlledByMQTT
37 | //#define fan_controlledByTouch
38 | //#define fan_controlledByMQTTandTouch
39 | //#define climate_controlledByBME_targetByMQTT
40 | //#define climate_controlledByBME_targetByTouch
41 | //#define climate_controlledByBME_targetByMQTTandTouch
42 | //#define climate_controlledByMQTT_targetByMQTT
43 | //#define climate_controlledByMQTT_targetByMQTTandTouch
44 | // --- End: list of presets --------------------------
45 |
46 | // --- based on the preset, automatically define other options --------------
47 | // --- normally you shouldn't change the next lines -------------------------
48 | #if defined(climate_controlledByBME_targetByMQTT) || defined(climate_controlledByBME_targetByTouch) || defined(climate_controlledByBME_targetByMQTTandTouch) || defined(climate_controlledByMQTT_targetByMQTT) || defined(climate_controlledByMQTT_targetByMQTTandTouch)
49 | #define useAutomaticTemperatureControl
50 | #if defined(climate_controlledByBME_targetByMQTT) || defined(climate_controlledByBME_targetByTouch) || defined(climate_controlledByBME_targetByMQTTandTouch)
51 | #define setActualTemperatureViaBME280
52 | #define useTemperatureSensorBME280
53 | #endif
54 | #if defined(climate_controlledByMQTT_targetByMQTT) || defined(climate_controlledByMQTT_targetByMQTTandTouch)
55 | #define setActualTemperatureViaMQTT
56 | #endif
57 | #endif
58 | #if defined(fan_controlledByMQTT) || defined(fan_controlledByMQTTandTouch) || defined(climate_controlledByBME_targetByMQTT) || defined(climate_controlledByBME_targetByMQTTandTouch) || defined(climate_controlledByMQTT_targetByMQTT) || defined(climate_controlledByMQTT_targetByMQTTandTouch)
59 | #define useWIFI
60 | #define useMQTT
61 | #define useOTAUpdate
62 | // #define useOTA_RTOS // not recommended because of additional 10K of heap space needed
63 | #endif
64 | #if defined(fan_controlledByTouch) || defined(fan_controlledByMQTTandTouch) || defined(climate_controlledByBME_targetByTouch) || defined(climate_controlledByBME_targetByMQTTandTouch) || defined(climate_controlledByMQTT_targetByMQTTandTouch)
65 | #define useTFT
66 | #define DRIVER_ILI9341 // e.g. 2.8 inch touch panel, 320x240, used in AZ-Touch
67 | #define useTouch
68 | #endif
69 |
70 | #else
71 | // --- Way 2 to choose the operation mode: manually define every low level option -----------------------------------------------------------
72 | /*
73 | Mode 1: pwm mode
74 | directly setting fan speed via pwm signal
75 | -> comment "useAutomaticTemperatureControl"
76 |
77 | Mode 2: temperature controller mode
78 | fan speed automatically increases if temperature is getting close to or higher than target temperature. Of course temperature can never get lower than air temperature of room
79 | -> uncomment "useAutomaticTemperatureControl"
80 | -> choose "setActualTemperatureViaBME280"
81 | -> uncomment "useTemperatureSensorBME280"
82 | XOR "setActualTemperatureViaMQTT"
83 |
84 | In both modes you have to have at least one of "useMQTT" and "useTouch", otherwise you cannot control the fan.
85 |
86 | Everything else is optional.
87 |
88 | First set mode, then go further down in this file to set other options needed for the chosen mode (unused options should be greyed out).
89 |
90 | */
91 | // --- setting mode -------------------------------------------------------------------------------------------------------------------------
92 | // #define useAutomaticTemperatureControl
93 | #ifdef useAutomaticTemperatureControl
94 | // --- choose how to set target temperature. Activate only one. --------------------------------------
95 | #define setActualTemperatureViaBME280
96 | // #define setActualTemperatureViaMQTT
97 | #endif
98 | // #define useTemperatureSensorBME280
99 | #define useWIFI
100 | #define useMQTT
101 | #define useOTAUpdate
102 | // #define useOTA_RTOS // not recommended because of additional 10K of heap space needed
103 | // #define useTFT
104 | #ifdef useTFT
105 | // --- choose which display to use. Activate only one. -----------------------------------------------
106 | // #define DRIVER_ILI9341 // 2.8 inch touch panel, 320x240, used in AZ-Touch
107 | #define DRIVER_ST7735 // 1.8 inch panel, 160x128
108 | #endif
109 | // #define useTouch
110 | #endif
111 | // ----------------------------------------------------------------------------------------------------------------------------------------------------------------
112 | // --- End: choose operation mode ---------------------------------------------------------------------------------------------------------------------------------
113 |
114 |
115 |
116 | // --- Begin: additional settings ---------------------------------------------------------------------------------------------------------------------------------
117 | // ----------------------------------------------------------------------------------------------------------------------------------------------------------------
118 | // Now, as the basic operation mode is chosen, you can define additional settings
119 |
120 | // --- Home Assistant MQTT discovery --------------------------------------------------------------------------------------------------------
121 | /* If you are using Home Assistant, you can activate auto discovery of the climate/fan and sensors.
122 | Please also see https://github.com/KlausMu/esp32-fan-controller/wiki/06-Home-Assistant
123 | If needed, e.g. if you are using more than one esp32 fan controller, please adjust mqtt settings further down in this file */
124 | #if defined(useMQTT)
125 | #define useHomeassistantMQTTDiscovery
126 | #endif
127 | #if defined(useHomeassistantMQTTDiscovery) && !defined(useMQTT)
128 | static_assert(false, "You have to use \"#define useMQTT\" when having \"#define useHomeassistantMQTTDiscovery\"");
129 | #endif
130 |
131 | /* --- If you have a touch display, you can show a standbyButton or shutdownButton
132 | There are two kind of shutdown buttons:
133 | 1. set the fan controller to standby
134 | - pwm is set to 0. Note: it is not guaranteed that fan stops if pwm is set to 0
135 | - display is turned off
136 | - you can get your fan controller back to normal mode via an mqtt message or if you touch the display
137 | 2. Call an external http REST endpoint, which can trigger any action you want.
138 | The display of the fan controller simply shows a counter from 30 to 0 and expects something to happen (e.g. power should be turned off by external means).
139 | If nothing happens and counter reaches 0, the fan controller goes back to normal operation mode.
140 | For example you could define in Home Assistant an input button. In an automation you could:
141 | - shutdown a Raspberry Pi
142 | - turn a smart plug off, so that the Raspberry Pi, a 3D printer and the fan controller get powered off
143 | */
144 |
145 | // #define useShutdownButton
146 | #ifdef useTouch
147 | #define useStandbyButton
148 | #endif
149 | #if defined(useStandbyButton) && defined(useShutdownButton)
150 | static_assert(false, "You cannot have both \"#define useStandbyButton\" and \"#define useShutdownButton\"");
151 | #endif
152 |
153 | // --- fan specs ----------------------------------------------------------------------------------------------------------------------------
154 | // fanPWM
155 | #define PWMPIN GPIO_NUM_17
156 | #define PWMFREQ 25000
157 | #define PWMCHANNEL 0
158 | #define PWMRESOLUTION 8
159 | #define FANMAXRPM 1500 // only used for showing at how many percent fan is running
160 |
161 | // fanTacho
162 | #define TACHOPIN GPIO_NUM_16
163 | #define TACHOUPDATECYCLE 1000 // how often tacho speed shall be determined, in milliseconds
164 | #define NUMBEROFINTERRUPSINONESINGLEROTATION 2 // Number of interrupts ESP32 sees on tacho signal on a single fan rotation. All the fans I've seen trigger two interrups.
165 |
166 | // --- automatic temperature control --------------------------------------------------------------------------------------------------------
167 |
168 | // ifdef: adaptive fan speed depending on actual temperature and target temperature
169 | // target temperature can be set via tft touch or via mqtt
170 | // needs "useTemperatureSensorBME280 defined"
171 | // ifndef: fan speed (pwm) is directly set, no adaptive temperature control
172 | // you can set fan speed either via tft touch or via mqtt
173 |
174 | #ifdef useAutomaticTemperatureControl
175 | // initial target temperature on startup
176 | #define INITIALTARGETTEMPERATURE 27.0
177 | // Lowest pwm value the temperature controller should use to set fan speed. If you want the fan not to turn off, set a value so that fan always runs.
178 | #define PWMMINIMUMVALUE 120
179 | #else
180 | // delta used when manually increasing or decreasing pwm
181 | #define PWMSTEP 10
182 | #endif
183 |
184 | // initial pwm fan speed on startup (0 <= value <= 255)
185 | #define INITIALPWMVALUE 120
186 |
187 | // sanity check
188 | #if !defined(setActualTemperatureViaBME280) && !defined(setActualTemperatureViaMQTT) && defined(useAutomaticTemperatureControl)
189 | static_assert(false, "You have to use \"#define setActualTemperatureViaBME280\" or \"#define setActualTemperatureViaMQTT\" when having \"#define useAutomaticTemperatureControl\"");
190 | #endif
191 | #if defined(setActualTemperatureViaBME280) && !defined(useTemperatureSensorBME280)
192 | static_assert(false, "You have to use \"#define useTemperatureSensorBME280\" when having \"#define setActualTemperatureViaBME280\"");
193 | #endif
194 | #if defined(setActualTemperatureViaBME280) && defined(setActualTemperatureViaMQTT)
195 | static_assert(false, "You cannot have both \"#define setActualTemperatureViaBME280\" and \"#define setActualTemperatureViaMQTT\"");
196 | #endif
197 |
198 | // --- temperature sensor BME280 ------------------------------------------------------------------------------------------------------------
199 |
200 | #ifdef useTemperatureSensorBME280
201 | // I2C pins used for BME280
202 | #define I2C_SCL GPIO_NUM_32 // GPIO_NUM_22 // GPIO_NUM_17
203 | #define I2C_SDA GPIO_NUM_33 // GPIO_NUM_21 // GPIO_NUM_16
204 | #define I2C_FREQ 100000 // 400000
205 | #define BME280_ADDR 0x76
206 | // in order to calibrate BME280 at startup, provide here the height over sea level in meter at your location
207 | #define HEIGHTOVERSEALEVELATYOURLOCATION 112.0
208 | #endif
209 |
210 | // --- wifi ---------------------------------------------------------------------------------------------------------------------------------
211 |
212 | #ifdef useWIFI
213 | #define WIFI_SSID "YourWifiSSID" // override it in file "config_override.h"
214 | #define WIFI_PASSWORD "YourWifiPassword" // override it in file "config_override.h"
215 | //#define WIFI_KNOWN_APS_COUNT 2
216 | //#define WIFI_KNOWN_APS \
217 | // { "00:11:22:33:44:55", "Your AP 2,4 GHz"}, \
218 | // { "66:77:88:99:AA:BB", "Your AP 5 GHz"}
219 | #endif
220 |
221 | // --- OTA Update ---------------------------------------------------------------------------------------------------------------------------
222 |
223 | #if !defined(useWIFI) && defined(useOTAUpdate)
224 | static_assert(false, "\"#define useOTAUpdate\" is only possible with \"#define useWIFI\"");
225 | #endif
226 | #if !defined(ESP32) && defined(useOTA_RTOS)
227 | static_assert(false, "\"#define useOTA_RTOS\" is only possible with ESP32");
228 | #endif
229 | #if defined(useOTA_RTOS) && !defined(useOTAUpdate)
230 | static_assert(false, "You cannot use \"#define useOTA_RTOS\" without \"#define useOTAUpdate\"");
231 | #endif
232 |
233 | #define useSerial
234 | #define useTelnetStream
235 |
236 | // --- mqtt ---------------------------------------------------------------------------------------------------------------------------------
237 | /*
238 | ----- IMPORTANT -----
239 | ----- MORE THAN ONE INSTANCE OF THE ESP32 FAN CONTROLLER -----
240 | If you want to have more than one instance of the esp32 fan controller in your network, every instance has to have it's own unique mqtt topcics (and IDs and name in HA, if you are using HA)
241 | For this the define UNIQUE_DEVICE_FRIENDLYNAME and UNIQUE_DEVICE_NAME is used. You can keep it unchanged if you have only one instance in your network.
242 | Otherwise you can change it to e.g. "Fan Controller 2" and "esp32_fan_controller_2"
243 | */
244 | #ifdef useMQTT
245 | #define UNIQUE_DEVICE_FRIENDLYNAME "Fan Controller" // override it in file "config_override.h"
246 | #define UNIQUE_DEVICE_NAME "esp32_fan_controller" // override it in file "config_override.h"
247 |
248 | #define MQTT_SERVER "IPAddressOfYourBroker" // override it in file "config_override.h"
249 | #define MQTT_SERVER_PORT 1883 // override it in file "config_override.h"
250 | #define MQTT_USER "" // override it in file "config_override.h"
251 | #define MQTT_PASS "" // override it in file "config_override.h"
252 | #define MQTT_CLIENTNAME UNIQUE_DEVICE_NAME
253 |
254 | /*
255 | For understanding when "cmnd", "stat" and "tele" is used, have a look at how Tasmota is doing it.
256 | https://tasmota.github.io/docs/MQTT
257 | https://tasmota.github.io/docs/openHAB/
258 | https://www.openhab.org/addons/bindings/mqtt.generic/
259 | https://www.openhab.org/addons/bindings/mqtt/
260 | https://community.openhab.org/t/itead-sonoff-switches-and-sockets-cheap-esp8266-wifi-mqtt-hardware/15024
261 | for debugging:
262 | mosquitto_sub -h localhost -t "esp32_fan_controller/#" -v
263 | mosquitto_sub -h localhost -t "homeassistant/climate/esp32_fan_controller/#" -v
264 | mosquitto_sub -h localhost -t "homeassistant/fan/esp32_fan_controller/#" -v
265 | mosquitto_sub -h localhost -t "homeassistant/sensor/esp32_fan_controller/#" -v
266 | */
267 |
268 | #define MQTTCMNDTARGETTEMP UNIQUE_DEVICE_NAME "/cmnd/TARGETTEMP"
269 | #define MQTTSTATTARGETTEMP UNIQUE_DEVICE_NAME "/stat/TARGETTEMP"
270 | #define MQTTCMNDACTUALTEMP UNIQUE_DEVICE_NAME "/cmnd/ACTUALTEMP"
271 | #define MQTTSTATACTUALTEMP UNIQUE_DEVICE_NAME "/stat/ACTUALTEMP"
272 | #define MQTTCMNDFANPWM UNIQUE_DEVICE_NAME "/cmnd/FANPWM"
273 | #define MQTTSTATFANPWM UNIQUE_DEVICE_NAME "/stat/FANPWM"
274 | // https://www.home-assistant.io/integrations/climate.mqtt/#mode_command_topic
275 | // https://www.home-assistant.io/integrations/climate.mqtt/#mode_state_topic
276 | // note: it is not guaranteed that fan stops if pwm is set to 0
277 | #define MQTTCMNDFANMODE UNIQUE_DEVICE_NAME "/cmnd/MODE" // can be "off" and "fan_only"
278 | #define MQTTSTATFANMODE UNIQUE_DEVICE_NAME "/stat/MODE"
279 | #define MQTTFANMODEOFFPAYLOAD "off"
280 | #define MQTTFANMODEFANONLYPAYLOAD "fan_only"
281 |
282 | #if defined(useOTAUpdate)
283 | #define MQTTCMNDOTA UNIQUE_DEVICE_NAME "/cmnd/OTA"
284 | #endif
285 |
286 | #ifdef useTemperatureSensorBME280
287 | #define MQTTTELESTATE1 UNIQUE_DEVICE_NAME "/tele/STATE1"
288 | #endif
289 | #define MQTTTELESTATE2 UNIQUE_DEVICE_NAME "/tele/STATE2"
290 | #define MQTTTELESTATE3 UNIQUE_DEVICE_NAME "/tele/STATE3"
291 | #define MQTTTELESTATE4 UNIQUE_DEVICE_NAME "/tele/STATE4"
292 |
293 | #if defined(useHomeassistantMQTTDiscovery)
294 | /* see
295 | https://www.home-assistant.io/integrations/mqtt
296 | https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
297 | https://www.home-assistant.io/integrations/mqtt/#discovery-messages
298 | https://www.home-assistant.io/integrations/mqtt/#birth-and-last-will-messages
299 | */
300 | #define HASSSTATUSTOPIC "homeassistant/status" // can be "online" and "offline"
301 | #define HASSSTATUSONLINEPAYLOAD "online"
302 | #define HASSSTATUSOFFLINEPAYLOAD "offline"
303 | /*
304 | When HA sends status online, we have to resent the discovery. But we have to wait some seconds, otherwise HA will not recognize the mqtt messages.
305 | If you have HA running on a weak mini computer, you may have to increase the waiting time. Value is in ms.
306 | Remark: the whole discovery process will be done in the following order:
307 | discovery, delay(1000), status=online, delay(1000), all inital values
308 | */
309 | #define WAITAFTERHAISONLINEUNTILDISCOVERYWILLBESENT 1000
310 | #define HASSFANSTATUSTOPIC UNIQUE_DEVICE_NAME "/stat/STATUS" // can be "online" and "offline"
311 |
312 | // The define HOMEASSISTANTDEVICE will be reused in all discovery payloads for the climate/fan and the sensors. Everything should be contained in the same device.
313 | #define HOMEASSISTANTDEVICE "\"dev\":{\"name\":\"" UNIQUE_DEVICE_FRIENDLYNAME "\", \"model\":\"" UNIQUE_DEVICE_NAME "\", \"identifiers\":[\"" UNIQUE_DEVICE_NAME "\"], \"manufacturer\":\"KlausMu\"}"
314 |
315 | // climate
316 | // see https://www.home-assistant.io/integrations/climate.mqtt/
317 | #ifdef useAutomaticTemperatureControl
318 | #define HASSCLIMATEDISCOVERYTOPIC "homeassistant/climate/" UNIQUE_DEVICE_NAME "/config"
319 | #ifdef useTemperatureSensorBME280
320 | #define CURRENTHUMIDITYINCLIMATE "\"current_humidity_topic\":\"~/tele/STATE1\", \"current_humidity_template\":\"{{value_json.hum | round(0)}}\", "
321 | #else
322 | #define CURRENTHUMIDITYINCLIMATE
323 | #endif
324 | #define HASSCLIMATEDISCOVERYPAYLOAD "{\"name\":null, \"unique_id\":\"" UNIQUE_DEVICE_NAME "\", \"object_id\":\"" UNIQUE_DEVICE_NAME "\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"icon\":\"mdi:fan\", \"min_temp\":10, \"max_temp\":50, \"temp_step\":1, \"precision\":0.1, " CURRENTHUMIDITYINCLIMATE "\"current_temperature_topic\":\"~/stat/ACTUALTEMP\", \"temperature_command_topic\":\"~/cmnd/TARGETTEMP\", \"temperature_state_topic\":\"~/stat/TARGETTEMP\", \"modes\":[\"off\",\"fan_only\"], \"mode_command_topic\":\"~/cmnd/MODE\", \"mode_state_topic\":\"~/stat/MODE\", \"availability_topic\":\"~/stat/STATUS\", " HOMEASSISTANTDEVICE "}"
325 | #endif
326 |
327 | // fan
328 | // see https://www.home-assistant.io/integrations/fan.mqtt/
329 | #ifndef useAutomaticTemperatureControl
330 | #define HASSFANDISCOVERYTOPIC "homeassistant/fan/" UNIQUE_DEVICE_NAME "/config"
331 | #define HASSFANDISCOVERYPAYLOAD "{\"name\":null, \"unique_id\":\"" UNIQUE_DEVICE_NAME "\", \"object_id\":\"" UNIQUE_DEVICE_NAME "\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"icon\":\"mdi:fan\", \"command_topic\":\"~/cmnd/MODE\", \"state_topic\":\"~/stat/MODE\", \"payload_on\": \"fan_only\", \"payload_off\": \"off\", \"percentage_state_topic\": \"~/stat/FANPWM\", \"percentage_command_topic\": \"~/cmnd/FANPWM\", \"speed_range_min\": 1, \"speed_range_max\": 255,\"availability_topic\":\"~/stat/STATUS\", " HOMEASSISTANTDEVICE "}"
332 | #endif
333 |
334 | // sensors
335 | // see https://www.home-assistant.io/integrations/sensor.mqtt/
336 | #ifdef useTemperatureSensorBME280
337 | #define HASSHUMIDITYSENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/humidity/config"
338 | #define HASSHUMIDITYSENSORDISCOVERYPAYLOAD "{\"name\":\"Humidity\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_humidity\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_humidity\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE1\", \"value_template\":\"{{ value_json.hum | round(0) }}\", \"device_class\":\"humidity\", \"unit_of_measurement\":\"%\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
339 | #define HASSTEMPERATURESENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/temperature/config"
340 | #define HASSTEMPERATURESENSORDISCOVERYPAYLOAD "{\"name\":\"Temperature\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_temperature\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_temperature\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE1\", \"value_template\":\"{{ value_json.ActTemp | round(1) }}\", \"device_class\":\"temperature\", \"unit_of_measurement\":\"°C\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
341 | #define HASSPRESSURESENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/pressure/config"
342 | #define HASSPRESSURESENSORDISCOVERYPAYLOAD "{\"name\":\"Pressure\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_pressure\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_pressure\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE1\", \"value_template\":\"{{ value_json.pres | round(0) }}\", \"device_class\":\"atmospheric_pressure\", \"unit_of_measurement\":\"hPa\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
343 | #define HASSALTITUDESENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/altitude/config"
344 | #define HASSALTITUDESENSORDISCOVERYPAYLOAD "{\"name\":\"Altitude\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_altitude\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_altitude\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE1\", \"value_template\":\"{{ value_json.alt | round(1) }}\", \"device_class\":\"distance\", \"unit_of_measurement\":\"m\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
345 | #endif
346 | #define HASSPWMSENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/pwm/config"
347 | #define HASSPWMSENSORDISCOVERYPAYLOAD "{\"name\":\"PWM\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_PWM\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_PWM\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE2\", \"value_template\":\"{{ value_json.pwm }}\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
348 | #define HASSRPMSENSORDISCOVERYTOPIC "homeassistant/sensor/" UNIQUE_DEVICE_NAME "/rpm/config"
349 | #define HASSRPMSENSORDISCOVERYPAYLOAD "{\"name\":\"RPM\", \"unique_id\":\"" UNIQUE_DEVICE_NAME "_RPM\", \"object_id\":\"" UNIQUE_DEVICE_NAME "_RPM\", \"~\":\"" UNIQUE_DEVICE_NAME "\", \"state_topic\":\"~/tele/STATE2\", \"value_template\":\"{{ value_json.rpm }}\", \"state_class\":\"measurement\", \"expire_after\": \"30\", " HOMEASSISTANTDEVICE "}"
350 |
351 | // see https://www.home-assistant.io/integrations/climate.mqtt/#availability_topic
352 | #endif
353 |
354 | #endif
355 |
356 | // sanity check
357 | #if defined(useMQTT) && !defined(useWIFI)
358 | static_assert(false, "You have to use \"#define useWIFI\" when having \"#define useMQTT\"");
359 | #endif
360 | #if defined(setActualTemperatureViaMQTT) && !defined(useMQTT)
361 | static_assert(false, "You have to use \"#define useMQTT\" when having \"#define setActualTemperatureViaMQTT\"");
362 | #endif
363 |
364 | // --- tft ----------------------------------------------------------------------------------------------------------------------------------
365 |
366 | #ifdef useTFT
367 | #define TFT_CS GPIO_NUM_5 //diplay chip select
368 | #define TFT_DC GPIO_NUM_4 //display d/c
369 | #define TFT_RST GPIO_NUM_22 //display reset
370 | #define TFT_MOSI GPIO_NUM_23 //diplay MOSI
371 | #define TFT_CLK GPIO_NUM_18 //display clock
372 |
373 |
374 | #ifdef DRIVER_ILI9341
375 | #define TFT_LED GPIO_NUM_15 //display background LED
376 | #define TFT_MISO GPIO_NUM_19 //display MISO
377 | #define TFT_ROTATION 3 // use 1 (landscape) or 3 (landscape upside down), nothing else. 0 and 2 (portrait) will not give a nice result.
378 | #endif
379 | #ifdef DRIVER_ST7735
380 | #define TFT_ROTATION 1 // use 1 (landscape) or 3 (landscape upside down), nothing else. 0 and 2 (portrait) will not give a nice result.
381 | #endif
382 |
383 | #endif
384 |
385 | // --- touch --------------------------------------------------------------------------------------------------------------------------------
386 |
387 | // Only AZ-Touch: here you have to set the pin for TOUCH_IRQ. The older "ArduiTouch" and the newer "AZ-Touch" use different pins. And you have to set the LED-PIN to different values to light up the TFT.
388 | // 1. "ArduiTouch" 2.4 inch (older version)
389 | // https://www.az-delivery.de/en/products/az-touch-wandgehauseset-mit-touchscreen-fur-esp8266-und-esp32
390 | #ifdef useTFT
391 | // #define LED_ON LOW // override it in file "config_override.h"
392 | #endif
393 | #ifdef useTouch
394 | // #define TOUCH_CS GPIO_NUM_14 // override it in file "config_override.h"
395 | // #define TOUCH_IRQ GPIO_NUM_2 // override it in file "config_override.h"
396 | // #define TOUCH_INVERT_COORDINATES // override it in file "config_override.h
397 | #endif
398 | // 2. "AZ-Touch" 2.8 inch, since November 2020
399 | // https://www.az-delivery.de/en/products/az-touch-wandgehauseset-mit-2-8-zoll-touchscreen-fur-esp8266-und-esp32
400 | // https://www.az-delivery.de/en/blogs/azdelivery-blog-fur-arduino-und-raspberry-pi/az-touch-mod
401 | #ifdef useTFT
402 | #define LED_ON HIGH // override it in file "config_override.h"
403 | #endif
404 | #ifdef useTouch
405 | #define TOUCH_CS GPIO_NUM_14 // override it in file "config_override.h"
406 | #define TOUCH_IRQ GPIO_NUM_27 // override it in file "config_override.h"
407 | // #define TOUCH_INVERT_COORDINATES // override it in file "config_override.h
408 | #endif
409 |
410 | // sanity check
411 | #if defined(useTouch) && !defined(useTFT)
412 | static_assert(false, "You have to use \"#define useTFT\" when having \"#define useTouch\"");
413 | #endif
414 | #if defined(DRIVER_ST7735) && defined(useTouch)
415 | static_assert(false, "TFT ST7735 doesn't support touch. Please disable it.");
416 | #endif
417 | #if !defined(useTouch) && !defined(useMQTT)
418 | static_assert(false, "You cannot disable both MQTT and touch, otherwise you cannot control the fan");
419 | #endif
420 |
421 | // --- shutdown Raspberry Pi and power off --------------------------------------------------------------------------------------------------
422 | /* Shutdown Raspberry Pi and turn off wifi power socket.
423 | In my setting I have a Raspberry Pi running Octoprint, a 3D printer, and both is powered by a wifi power socket.
424 | I wanted to shutdown the Pi and turn off power by means of the ESP32.
425 | This is very special to my setting. You can completely disable it.
426 | If this option is enabled, then a power off button is shown on the TFT screen.
427 | When you hit the button, a http request is send to OpenHab which starts a script (script has to be defined in OpenHab) with the following actions:
428 | - shutdown Raspberry Pi
429 | - wait 30 seconds
430 | - turn off wifi power socket (switch off 3D printer and Raspberry Pi)
431 | Since the OpenHab script (in my case) waits 30 seconds before turning off power, there is a simple countdown with same duration on the TFT display.
432 | The ESP32 does not need to turn off exactly when 0 is shown on the display. This depends on when the OpenHab script turns off the wifi power socket.
433 | ESP32 can actually power off when countdown is e.g. at 5 or even less than 0 ...
434 | */
435 |
436 | #ifdef useShutdownButton
437 | #define MQTTCMNDSHUTDOWNTOPIC UNIQUE_DEVICE_NAME "/cmnd/shutdown" // override it in file "config_override.h"
438 | #define MQTTCMNDSHUTDOWNPAYLOAD "shutdown" // override it in file "config_override.h"
439 | #define SHUTDOWNCOUNTDOWN 30 // in seconds
440 | #endif
441 |
442 | // sanity check
443 | #if defined(useShutdownButton) && !defined(useMQTT)
444 | static_assert(false, "You have to use \"#define useMQTT\" when having \"#define useShutdownButton\"");
445 | #endif
446 | #if (defined(useStandbyButton) || defined(useShutdownButton)) && !defined(useTouch)
447 | static_assert(false, "You have to use \"#define useTouch\" when having \"#define useStandbyButton\" or \"#define useShutdownButton\"");
448 | #endif
449 |
450 | // --- not used -----------------------------------------------------------------------------------------------------------------------------
451 | #ifdef DRIVER_ILI9341
452 | // Occupied by AZ-touch. This software doesn't use this pin
453 | #define BUZZER GPIO_NUM_21
454 | // #define A0 GPIO_NUM_36
455 | #endif
456 |
457 | // ----------------------------------------------------------------------------------------------------------------------------------------------------------------
458 | // --- End: additional settings -----------------------------------------------------------------------------------------------------------------------------------
459 |
460 |
461 | // --- include override settings from seperate file ---------------------------------------------------------------------------------------------------------------
462 | #if __has_include("config_override.h")
463 | #include "config_override.h"
464 | #endif
465 |
466 | // --- sanity check: only one preset must be choosen --------------------------------------------------------------------------------------------------------------
467 | #if (defined(fan_controlledByMQTT) && defined(fan_controlledByTouch)) || \
468 | (defined(fan_controlledByMQTT) && defined(fan_controlledByMQTTandTouch)) || \
469 | (defined(fan_controlledByMQTT) && defined(climate_controlledByBME_targetByMQTT)) || \
470 | (defined(fan_controlledByMQTT) && defined(climate_controlledByBME_targetByTouch)) || \
471 | (defined(fan_controlledByMQTT) && defined(climate_controlledByBME_targetByMQTTandTouch)) || \
472 | (defined(fan_controlledByMQTT) && defined(climate_controlledByMQTT_targetByMQTT)) || \
473 | (defined(fan_controlledByMQTT) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
474 | \
475 | (defined(fan_controlledByTouch) && defined(fan_controlledByMQTTandTouch)) || \
476 | (defined(fan_controlledByTouch) && defined(climate_controlledByBME_targetByMQTT)) || \
477 | (defined(fan_controlledByTouch) && defined(climate_controlledByBME_targetByTouch)) || \
478 | (defined(fan_controlledByTouch) && defined(climate_controlledByBME_targetByMQTTandTouch)) || \
479 | (defined(fan_controlledByTouch) && defined(climate_controlledByMQTT_targetByMQTT)) || \
480 | (defined(fan_controlledByTouch) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
481 | \
482 | (defined(fan_controlledByMQTTandTouch) && defined(climate_controlledByBME_targetByMQTT)) || \
483 | (defined(fan_controlledByMQTTandTouch) && defined(climate_controlledByBME_targetByTouch)) || \
484 | (defined(fan_controlledByMQTTandTouch) && defined(climate_controlledByBME_targetByMQTTandTouch)) || \
485 | (defined(fan_controlledByMQTTandTouch) && defined(climate_controlledByMQTT_targetByMQTT)) || \
486 | (defined(fan_controlledByMQTTandTouch) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
487 | \
488 | (defined(climate_controlledByBME_targetByMQTT) && defined(climate_controlledByBME_targetByTouch)) || \
489 | (defined(climate_controlledByBME_targetByMQTT) && defined(climate_controlledByBME_targetByMQTTandTouch)) || \
490 | (defined(climate_controlledByBME_targetByMQTT) && defined(climate_controlledByMQTT_targetByMQTT)) || \
491 | (defined(climate_controlledByBME_targetByMQTT) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
492 | \
493 | (defined(climate_controlledByBME_targetByTouch) && defined(climate_controlledByBME_targetByMQTTandTouch)) || \
494 | (defined(climate_controlledByBME_targetByTouch) && defined(climate_controlledByMQTT_targetByMQTT)) || \
495 | (defined(climate_controlledByBME_targetByTouch) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
496 | \
497 | (defined(climate_controlledByBME_targetByMQTTandTouch) && defined(climate_controlledByMQTT_targetByMQTT)) || \
498 | (defined(climate_controlledByBME_targetByMQTTandTouch) && defined(climate_controlledByMQTT_targetByMQTTandTouch)) || \
499 | \
500 | (defined(climate_controlledByMQTT_targetByMQTT) && defined(climate_controlledByMQTT_targetByMQTTandTouch))
501 | static_assert(false, "You cannot choose more than one preset at the same time");
502 | #endif
503 |
504 | #endif /*__CONFIG_H__*/
505 |
--------------------------------------------------------------------------------
/src/config_override_example.h:
--------------------------------------------------------------------------------
1 | /*
2 | Copy this file to "config_override.h"
3 | Any defines from "config.h" in CAPITALS can be overridden in "config_override.h".
4 | All defines having BOTH lowercase and uppercase MUST stay in "config.h". They define the mode the "esp32 fan controller" is running in.
5 | If you add additional overrides here, you have to
6 | 1. first add #undef
7 | 2. add new #define
8 | */
9 | #undef WIFI_SSID
10 | #undef WIFI_PASSWORD
11 | #undef MQTT_SERVER
12 | #undef MQTT_SERVER_PORT
13 | #undef MQTT_USER
14 | #undef MQTT_PASS
15 | #undef UNIQUE_DEVICE_FRIENDLYNAME
16 | #undef UNIQUE_DEVICE_NAME
17 | #undef MQTTCMNDSHUTDOWNTOPIC
18 | #undef MQTTCMNDSHUTDOWNPAYLOAD
19 | #undef TOUCH_CS
20 | #undef TOUCH_IRQ
21 | #undef TFT_ROTATION
22 | #undef LED_ON
23 | #undef TOUCH_INVERT_COORDINATES
24 |
25 | #ifdef useWIFI
26 | #define WIFI_SSID "YourWifiSSID" // override here
27 | #define WIFI_PASSWORD "YourWifiPassword" // override here
28 | #endif
29 |
30 | #ifdef useMQTT
31 | #define MQTT_SERVER "IPAddressOfYourBroker" // override here
32 | #define MQTT_SERVER_PORT 1883 // override here
33 | #define MQTT_USER "myUser or empty" // override here
34 | #define MQTT_PASS "myPassword or empty" // override here
35 | #define UNIQUE_DEVICE_FRIENDLYNAME "Fan Controller" // override here
36 | #define UNIQUE_DEVICE_NAME "esp32_fan_controller" // override here
37 | #endif
38 |
39 | #ifdef useShutdownButton
40 | #define MQTTCMNDSHUTDOWNTOPIC UNIQUE_DEVICE_NAME "/cmnd/shutdown" // override here
41 | #define MQTTCMNDSHUTDOWNPAYLOAD "shutdown" // override here
42 | #endif
43 |
44 | #ifdef useTFT
45 | #ifdef DRIVER_ILI9341
46 | #define TFT_ROTATION 3 // use 1 (landscape) or 3 (landscape upside down), nothing else. 0 and 2 (portrait) will not give a nice result.
47 | #endif
48 | #ifdef DRIVER_ST7735
49 | #define TFT_ROTATION 1 // use 1 (landscape) or 3 (landscape upside down), nothing else. 0 and 2 (portrait) will not give a nice result.
50 | #endif
51 | #define LED_ON HIGH // override here
52 | #endif
53 | #ifdef useTouch
54 | #define TOUCH_CS GPIO_NUM_14 // override here
55 | #define TOUCH_IRQ GPIO_NUM_27 // override here
56 | //#define TOUCH_INVERT_COORDINATES // override here
57 | #endif
58 |
--------------------------------------------------------------------------------
/src/fanPWM.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "config.h"
5 | #include "log.h"
6 | #include "mqtt.h"
7 | #include "tft.h"
8 |
9 | int pwmValue = 0;
10 | bool modeIsOff = false;
11 | void updateMQTT_Screen_withNewPWMvalue(int aPWMvalue, bool force);
12 | void updateMQTT_Screen_withNewMode(bool aModeIsOff, bool force);
13 |
14 | // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/
15 | void initPWMfan(void){
16 | // configure LED PWM functionalitites
17 | ledcSetup(PWMCHANNEL, PWMFREQ, PWMRESOLUTION);
18 | // attach the channel to the GPIO to be controlled
19 | ledcAttachPin(PWMPIN, PWMCHANNEL);
20 |
21 | pwmValue = INITIALPWMVALUE;
22 | updateMQTT_Screen_withNewPWMvalue(pwmValue, true);
23 | updateMQTT_Screen_withNewMode(false, true);
24 |
25 | Log.printf(" Fan PWM sucessfully initialized.\r\n");
26 | }
27 |
28 | void updateFanSpeed(void){
29 | ledcWrite(PWMCHANNEL, pwmValue);
30 | }
31 |
32 | void updateMQTT_Screen_withNewPWMvalue(int aPWMvalue, bool force) {
33 | // note: it is not guaranteed that fan stops if pwm is set to 0
34 | if (modeIsOff) {aPWMvalue = 0;}
35 | if ((pwmValue != aPWMvalue) || force) {
36 | pwmValue = aPWMvalue;
37 | if (pwmValue < 0) {pwmValue = 0;};
38 | if (pwmValue > 255) {pwmValue = 255;};
39 | updateFanSpeed();
40 | #ifdef useMQTT
41 | mqtt_publish_stat_fanPWM();
42 | mqtt_publish_tele();
43 | #endif
44 | draw_screen();
45 | }
46 | }
47 |
48 | void updateMQTT_Screen_withNewMode(bool aModeIsOff, bool force) {
49 | if ((modeIsOff != aModeIsOff) || force) {
50 | modeIsOff = aModeIsOff;
51 | #ifdef useMQTT
52 | mqtt_publish_stat_mode();
53 | #endif
54 | switchOff_screen(modeIsOff);
55 | }
56 | if (modeIsOff) {
57 | updateMQTT_Screen_withNewPWMvalue(0, true);
58 | } else {
59 | updateMQTT_Screen_withNewPWMvalue(INITIALPWMVALUE, true);
60 | }
61 | }
62 |
63 | #ifndef useAutomaticTemperatureControl
64 | void incFanSpeed(void){
65 | int newPWMValue = min(pwmValue+PWMSTEP, 255);
66 | updateMQTT_Screen_withNewPWMvalue(newPWMValue, false);
67 | }
68 | void decFanSpeed(void){
69 | int newPWMValue = max(pwmValue-PWMSTEP, 0);
70 | updateMQTT_Screen_withNewPWMvalue(newPWMValue, false);
71 | }
72 | #endif
73 |
74 | int getPWMvalue(){
75 | return pwmValue;
76 | }
77 |
78 | bool getModeIsOff(void) {
79 | return modeIsOff;
80 | }
81 |
--------------------------------------------------------------------------------
/src/fanPWM.h:
--------------------------------------------------------------------------------
1 | void initPWMfan(void);
2 | void updateMQTT_Screen_withNewPWMvalue(int aPWMvalue, bool force);
3 | void updateMQTT_Screen_withNewMode(bool aModeIsOff, bool force);
4 | #ifndef useAutomaticTemperatureControl
5 | void incFanSpeed(void);
6 | void decFanSpeed(void);
7 | #endif
8 | int getPWMvalue();
9 | bool getModeIsOff(void);
10 |
--------------------------------------------------------------------------------
/src/fanTacho.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "config.h"
5 | #include "log.h"
6 |
7 | static volatile int counter_rpm = 0;
8 | int last_rpm = 0;
9 | unsigned long millisecondsLastTachoMeasurement = 0;
10 |
11 | // Interrupt counting every rotation of the fan
12 | // https://desire.giesecke.tk/index.php/2018/01/30/change-global-variables-from-isr/
13 | void IRAM_ATTR rpm_fan() {
14 | counter_rpm++;
15 | }
16 |
17 | void initTacho(void) {
18 | pinMode(TACHOPIN, INPUT);
19 | digitalWrite(TACHOPIN, HIGH);
20 | attachInterrupt(digitalPinToInterrupt(TACHOPIN), rpm_fan, FALLING);
21 | Log.printf(" Fan tacho detection sucessfully initialized.\r\n");
22 | }
23 |
24 | void updateTacho(void) {
25 | // start of tacho measurement
26 | if ((unsigned long)(millis() - millisecondsLastTachoMeasurement) >= TACHOUPDATECYCLE)
27 | {
28 | // detach interrupt while calculating rpm
29 | detachInterrupt(digitalPinToInterrupt(TACHOPIN));
30 | // calculate rpm
31 | last_rpm = counter_rpm * ((float)60 / (float)NUMBEROFINTERRUPSINONESINGLEROTATION) * ((float)1000 / (float)TACHOUPDATECYCLE);
32 | // Log.printf("fan rpm = %d\r\n", last_rpm);
33 |
34 | // reset counter
35 | counter_rpm = 0;
36 | // store milliseconds when tacho was measured the last time
37 | millisecondsLastTachoMeasurement = millis();
38 |
39 | // attach interrupt again
40 | attachInterrupt(digitalPinToInterrupt(TACHOPIN), rpm_fan, FALLING);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/fanTacho.h:
--------------------------------------------------------------------------------
1 | extern int last_rpm;
2 |
3 | void initTacho(void);
4 | void updateTacho(void);
--------------------------------------------------------------------------------
/src/log.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "config.h"
4 | #include "log.h"
5 | #include "sensorBME280.h"
6 | #include "fanPWM.h"
7 | #include "fanTacho.h"
8 | #include "temperatureController.h"
9 |
10 | #if defined(useTelnetStream)
11 | #include "TelnetStream.h"
12 | #endif
13 |
14 | // https://www.learncpp.com/cpp-tutorial/class-code-and-header-files/
15 |
16 | LogStreamClass::LogStreamClass(void) {
17 | }
18 |
19 | int LogStreamClass::read() {
20 | // return Serial.read();
21 | return -1;
22 | }
23 |
24 | int LogStreamClass::available() {
25 | // return Serial.available();
26 | return -1;
27 | }
28 |
29 | int LogStreamClass::peek() {
30 | // return Serial.peek();
31 | return -1;
32 | }
33 |
34 | #ifdef ESP32
35 | void LogStreamClass::flush() {
36 | return;
37 | }
38 | #endif
39 |
40 | size_t LogStreamClass::write(uint8_t val) {
41 | // return Serial.write(val);
42 | return -1;
43 | }
44 |
45 | size_t LogStreamClass::write(const uint8_t *buf, size_t size) {
46 | // return Serial.write(buf, size);
47 | return -1;
48 | }
49 |
50 | size_t LogStreamClass::printf(const char * format, ...) {
51 | // https://stackoverflow.com/questions/3530771/passing-variable-arguments-to-another-function-that-accepts-a-variable-argument
52 | // https://stackoverflow.com/questions/1056411/how-to-pass-variable-number-of-arguments-to-printf-sprintf
53 |
54 | size_t res;
55 | va_list args;
56 | va_start(args, format);
57 |
58 | // maximum number of characters in log message
59 | char buf[1000];
60 | vsnprintf(buf, sizeof(buf), format, args);
61 |
62 | // print out #1: Serial
63 | #if defined(useSerial)
64 | res = Serial.printf(MY_LOG_FORMAT("%s"), buf);
65 | #endif
66 |
67 | // print out #2: TelnetStream
68 | #if defined(useTelnetStream)
69 | res = TelnetStream.printf(MY_LOG_FORMAT("%s"), buf);
70 | #endif
71 |
72 | va_end(args);
73 | return res;
74 | };
75 |
76 | LogStreamClass Log;
77 |
78 | void doLog(void){
79 | #ifdef useTemperatureSensorBME280
80 | Log.printf("actual temperature = %.2f *C, pressure = %.2f hPa, approx. altitude = %.2f m, humidity = %.2f %%\r\n", lastTempSensorValues[0], lastTempSensorValues[1], lastTempSensorValues[2], lastTempSensorValues[3]);
81 | #endif
82 | #ifdef useAutomaticTemperatureControl
83 | Log.printf("target temperature = %.2f *C\r\n", getTargetTemperature());
84 | #endif
85 | Log.printf("fan rpm = %d, fan pwm = %d\r\n", last_rpm, getPWMvalue());
86 | }
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/log.h:
--------------------------------------------------------------------------------
1 | // #define MY_LOG_FORMAT(format) "%lu ms: " format "\r\n", millis()
2 | #define MY_LOG_FORMAT(format) "%lu ms: " format, millis()
3 |
4 | #include "Arduino.h"
5 |
6 | #ifndef _LOGSTREAMCLASS_H_
7 | #define _LOGSTREAMCLASS_H_
8 |
9 | class LogStreamClass : public Stream {
10 | public:
11 | LogStreamClass(void);
12 |
13 | // Stream implementation
14 | int read();
15 | int available();
16 | int peek();
17 | #ifdef ESP32
18 | void flush();
19 | #endif
20 |
21 | // Print implementation
22 | virtual size_t write(uint8_t val);
23 | virtual size_t write(const uint8_t *buf, size_t size);
24 | using Print::write; // pull in write(str) and write(buf, size) from Print
25 |
26 | size_t printf(const char * format, ...) __attribute__ ((format (printf, 2, 3)));
27 | };
28 |
29 | extern LogStreamClass Log;
30 |
31 | void doLog(void);
32 | #endif
33 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "config.h"
4 | #include "log.h"
5 | #include "wifiCommunication.h"
6 | #include "mqtt.h"
7 | #include "sensorBME280.h"
8 | #include "fanPWM.h"
9 | #include "fanTacho.h"
10 | #include "temperatureController.h"
11 | #include "tft.h"
12 | #include "tftTouch.h"
13 |
14 | #if defined(useOTAUpdate)
15 | // https://github.com/SensorsIot/ESP32-OTA
16 | #include "OTA.h"
17 | #if !defined(useOTA_RTOS)
18 | #include
19 | #endif
20 | #endif
21 | #if defined(useTelnetStream)
22 | #include "TelnetStream.h"
23 | #endif
24 |
25 | unsigned long previousMillis1000Cycle = 0;
26 | unsigned long interval1000Cycle = 1000;
27 | unsigned long previousMillis10000Cycle = 0;
28 | unsigned long interval10000Cycle = 10000;
29 |
30 | void setup(){
31 | Serial.begin(115200);
32 | Serial.println("");
33 | Log.printf("Setting things up ...\r\n");
34 |
35 | #ifdef useWIFI
36 | wifi_setup();
37 | wifi_enable();
38 | #endif
39 | #if defined(useOTAUpdate)
40 | OTA_setup("ESP32fancontroller");
41 | // Do not start OTA. Save heap space and start it via MQTT only when needed.
42 | // ArduinoOTA.begin();
43 | #endif
44 | #if defined(useTelnetStream)
45 | TelnetStream.begin();
46 | #endif
47 | #ifdef useTFT
48 | initTFT();
49 | #endif
50 | #ifdef useTouch
51 | initTFTtouch();
52 | #endif
53 | initPWMfan();
54 | initTacho();
55 | #ifdef useTemperatureSensorBME280
56 | initBME280();
57 | #endif
58 | #ifdef useAutomaticTemperatureControl
59 | initTemperatureController();
60 | #endif
61 | #ifdef useMQTT
62 | mqtt_setup();
63 | #endif
64 |
65 | Log.printf("Settings done. Have fun.\r\n");
66 | }
67 |
68 | void loop(){
69 | // functions that shall be called as often as possible
70 | // these functions should take care on their own that they don't nee too much time
71 | updateTacho();
72 | #ifdef useTouch
73 | processUserInput();
74 | #endif
75 | #if defined(useOTAUpdate) && !defined(useOTA_RTOS)
76 | // If you do not use FreeRTOS, you have to regulary call the handle method
77 | ArduinoOTA.handle();
78 | #endif
79 | // mqtt_loop() is doing mqtt keepAlive, processes incoming messages and hence triggers callback
80 | #ifdef useMQTT
81 | mqtt_loop();
82 | #endif
83 |
84 | unsigned long currentMillis = millis();
85 |
86 | // functions that shall be called every 1000 ms
87 | if ((currentMillis - previousMillis1000Cycle) >= interval1000Cycle) {
88 | previousMillis1000Cycle = currentMillis;
89 |
90 | #ifdef useTemperatureSensorBME280
91 | updateBME280();
92 | #endif
93 | #ifdef useAutomaticTemperatureControl
94 | setFanPWMbasedOnTemperature();
95 | #endif
96 | #ifdef useTFT
97 | draw_screen();
98 | #endif
99 | #ifdef useHomeassistantMQTTDiscovery
100 | if (((currentMillis - timerStartForHAdiscovery) >= WAITAFTERHAISONLINEUNTILDISCOVERYWILLBESENT) && (timerStartForHAdiscovery != 0)) {
101 | mqtt_publish_hass_discovery();
102 | }
103 | #endif
104 | }
105 |
106 | // functions that shall be called every 10000 ms
107 | if ((currentMillis - previousMillis10000Cycle) >= interval10000Cycle) {
108 | previousMillis10000Cycle = currentMillis;
109 |
110 | #ifdef useMQTT
111 | mqtt_publish_tele();
112 | #endif
113 | doLog();
114 | }
115 | }
--------------------------------------------------------------------------------
/src/mqtt.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #if defined(ESP32)
4 | #include
5 | #endif
6 | #if defined(ESP8266)
7 | #include
8 | #endif
9 | #include
10 | #include
11 |
12 | #include "config.h"
13 | #include "log.h"
14 | #include "wifiCommunication.h"
15 | #include "mqtt.h"
16 | #include "fanPWM.h"
17 | #include "fanTacho.h"
18 | #include "sensorBME280.h"
19 | #include "temperatureController.h"
20 | #include "tft.h"
21 |
22 | #ifdef useMQTT
23 | // https://randomnerdtutorials.com/esp32-mqtt-publish-subscribe-arduino-ide/
24 | // https://github.com/knolleary/pubsubclient
25 | // https://gist.github.com/igrr/7f7e7973366fc01d6393
26 |
27 | unsigned long reconnectInterval = 5000;
28 | // in order to do reconnect immediately ...
29 | unsigned long lastReconnectAttempt = millis() - reconnectInterval - 1;
30 | #ifdef useHomeassistantMQTTDiscovery
31 | unsigned long timerStartForHAdiscovery = 1;
32 | #endif
33 |
34 | void callback(char* topic, byte* payload, unsigned int length);
35 |
36 | WiFiClient wifiClient;
37 |
38 | PubSubClient mqttClient(MQTT_SERVER, MQTT_SERVER_PORT, callback, wifiClient);
39 |
40 | bool checkMQTTconnection();
41 |
42 | void mqtt_setup() {
43 | #ifdef useHomeassistantMQTTDiscovery
44 | // Set buffer size to allow hass discovery payload
45 | mqttClient.setBufferSize(1280);
46 | #endif
47 | }
48 |
49 | void mqtt_loop(){
50 | if (!mqttClient.connected()) {
51 | unsigned long currentMillis = millis();
52 | if ((currentMillis - lastReconnectAttempt) > reconnectInterval) {
53 | lastReconnectAttempt = currentMillis;
54 | // Attempt to reconnect
55 | checkMQTTconnection();
56 | }
57 | }
58 |
59 | if (mqttClient.connected()) {
60 | mqttClient.loop();
61 | }
62 | }
63 |
64 | bool checkMQTTconnection() {
65 | if (wifiIsDisabled) return false;
66 |
67 | if (WiFi.isConnected()) {
68 | if (mqttClient.connected()) {
69 | return true;
70 | } else {
71 | // try to connect to mqtt server
72 | #if !defined(useHomeassistantMQTTDiscovery)
73 | if (mqttClient.connect(MQTT_CLIENTNAME, MQTT_USER, MQTT_PASS)) {
74 | #else
75 | // In case of Home Assistant, connect with last will to the broker to set the device offline when the esp32 fan controller is swtiched off
76 | if (mqttClient.connect(MQTT_CLIENTNAME, MQTT_USER, MQTT_PASS,
77 | HASSFANSTATUSTOPIC, 0, 1, HASSSTATUSOFFLINEPAYLOAD)) {
78 | #endif
79 | Log.printf(" Successfully connected to MQTT broker\r\n");
80 |
81 | // subscribes to messages with given topic.
82 | // Callback function will be called 1. in client.loop() 2. when sending a message
83 | mqttClient.subscribe(MQTTCMNDTARGETTEMP);
84 | mqttClient.subscribe(MQTTCMNDACTUALTEMP);
85 | mqttClient.subscribe(MQTTCMNDFANPWM);
86 | mqttClient.subscribe(MQTTCMNDFANMODE);
87 | #if defined(useOTAUpdate)
88 | mqttClient.subscribe(MQTTCMNDOTA);
89 | #endif
90 | #if defined(useHomeassistantMQTTDiscovery)
91 | mqttClient.subscribe(HASSSTATUSTOPIC);
92 | // if we successfully connected or reconnected to the mqtt server, send HA discovery
93 | timerStartForHAdiscovery = millis();
94 | #endif
95 | } else {
96 | Log.printf(" MQTT connection failed (but WiFi is available). Will try later ...\r\n");
97 | }
98 | return mqttClient.connected();
99 | }
100 | } else {
101 | Log.printf(" No connection to MQTT server, because WiFi ist not connected.\r\n");
102 | return false;
103 | }
104 | }
105 |
106 | bool publishMQTTMessage(const char *topic, const char *payload, boolean retained){
107 | if (wifiIsDisabled) return false;
108 |
109 | if (checkMQTTconnection()) {
110 | // Log.printf("Sending mqtt payload to topic \"%s\": %s\r\n", topic, payload);
111 |
112 | if (mqttClient.publish(topic, payload, retained)) {
113 | // Log.printf("Publish ok\r\n");
114 | return true;
115 | }
116 | else {
117 | Log.printf("Publish failed\r\n");
118 | }
119 | } else {
120 | Log.printf(" Cannot publish mqtt message, because checkMQTTconnection failed (WiFi or mqtt is not connected)\r\n");
121 | }
122 | return false;
123 | }
124 |
125 | bool publishMQTTMessage(const char *topic, const char *payload){
126 | return publishMQTTMessage(topic, payload, false);
127 | }
128 |
129 | bool mqtt_publish_stat_targetTemp() {
130 | return publishMQTTMessage(MQTTSTATTARGETTEMP, ((String)getTargetTemperature()).c_str());
131 | };
132 | bool mqtt_publish_stat_actualTemp() {
133 | return publishMQTTMessage(MQTTSTATACTUALTEMP, ((String)getActualTemperature()).c_str());
134 | };
135 | bool mqtt_publish_stat_fanPWM() {
136 | return publishMQTTMessage(MQTTSTATFANPWM, ((String)getPWMvalue()).c_str());
137 | };
138 | bool mqtt_publish_stat_mode() {
139 | return publishMQTTMessage(MQTTSTATFANMODE, getModeIsOff() ? MQTTFANMODEOFFPAYLOAD : MQTTFANMODEFANONLYPAYLOAD);
140 | };
141 | #ifdef useShutdownButton
142 | bool mqtt_publish_shutdown() {
143 | return publishMQTTMessage(MQTTCMNDSHUTDOWNTOPIC, MQTTCMNDSHUTDOWNPAYLOAD);
144 | };
145 | #endif
146 |
147 | #ifdef useHomeassistantMQTTDiscovery
148 | bool mqtt_publish_hass_discovery() {
149 | Log.printf("Will send HA discovery now.\r\n");
150 | bool error = false;
151 | #ifdef useAutomaticTemperatureControl
152 | error = !publishMQTTMessage(HASSCLIMATEDISCOVERYTOPIC, HASSCLIMATEDISCOVERYPAYLOAD);
153 | #else
154 | error = !publishMQTTMessage(HASSFANDISCOVERYTOPIC, HASSFANDISCOVERYPAYLOAD);
155 | #endif
156 | #ifdef useTemperatureSensorBME280
157 | error = error || !publishMQTTMessage(HASSHUMIDITYSENSORDISCOVERYTOPIC, HASSHUMIDITYSENSORDISCOVERYPAYLOAD);
158 | error = error || !publishMQTTMessage(HASSTEMPERATURESENSORDISCOVERYTOPIC, HASSTEMPERATURESENSORDISCOVERYPAYLOAD);
159 | error = error || !publishMQTTMessage(HASSPRESSURESENSORDISCOVERYTOPIC, HASSPRESSURESENSORDISCOVERYPAYLOAD);
160 | error = error || !publishMQTTMessage(HASSALTITUDESENSORDISCOVERYTOPIC, HASSALTITUDESENSORDISCOVERYPAYLOAD);
161 | #endif
162 | error = error || !publishMQTTMessage(HASSPWMSENSORDISCOVERYTOPIC, HASSPWMSENSORDISCOVERYPAYLOAD);
163 | error = error || !publishMQTTMessage(HASSRPMSENSORDISCOVERYTOPIC, HASSRPMSENSORDISCOVERYPAYLOAD);
164 |
165 | if (!error) {delay(1000);}
166 | // publish that we are online. Remark: offline is sent via last will retained message
167 | error = error || !publishMQTTMessage(HASSFANSTATUSTOPIC, "", true);
168 | error = error || !publishMQTTMessage(HASSFANSTATUSTOPIC, HASSSTATUSONLINEPAYLOAD);
169 |
170 | if (!error) {delay(1000);}
171 | // MODE????
172 |
173 | // that's not really part of the discovery message, but this enables the climate slider in HA and immediately provides all values
174 | error = error || !mqtt_publish_stat_targetTemp();
175 | error = error || !mqtt_publish_stat_actualTemp();
176 | error = error || !mqtt_publish_stat_fanPWM();
177 | error = error || !mqtt_publish_stat_mode();
178 | error = error || !mqtt_publish_tele();
179 | if (!error) {
180 | // will not resend discovery as long as timerStartForHAdiscovery == 0
181 | Log.printf("Will set timer to 0 now, this means I will not send discovery again.\r\n");
182 | timerStartForHAdiscovery = 0;
183 | } else {
184 | Log.printf("Some error occured while sending discovery. Will try again.\r\n");
185 | }
186 | return !error;
187 | }
188 | #endif
189 |
190 | bool mqtt_publish_tele() {
191 | bool error = false;
192 | // maximum message length 128 Byte
193 | String payload = "";
194 | // BME280
195 | #ifdef useTemperatureSensorBME280
196 | payload += "{\"ActTemp\":";
197 | payload += lastTempSensorValues[0];
198 | payload += ",\"pres\":";
199 | payload += lastTempSensorValues[1];
200 | payload += ",\"alt\":";
201 | payload += lastTempSensorValues[2];
202 | payload += ",\"hum\":";
203 | payload += lastTempSensorValues[3];
204 | payload += ",\"TargTemp\":";
205 | payload += getTargetTemperature();
206 | payload += "}";
207 | error = !publishMQTTMessage(MQTTTELESTATE1, payload.c_str());
208 | #endif
209 |
210 | // Fan
211 | payload = "";
212 | payload += "{\"rpm\":";
213 | payload += last_rpm;
214 | payload += ",\"pwm\":";
215 | payload += getPWMvalue();
216 | payload += "}";
217 | error = error || !publishMQTTMessage(MQTTTELESTATE2, payload.c_str());
218 |
219 | // WiFi
220 | payload = "";
221 | payload += "{\"wifiRSSI\":";
222 | payload += WiFi.RSSI();
223 | payload += ",\"wifiChan\":";
224 | payload += WiFi.channel();
225 | payload += ",\"wifiSSID\":";
226 | payload += WiFi.SSID();
227 | payload += ",\"wifiBSSID\":";
228 | payload += WiFi.BSSIDstr();
229 | #if defined(WIFI_KNOWN_APS)
230 | payload += ",\"wifiAP\":";
231 | payload += accessPointName;
232 | #endif
233 | payload += ",\"IP\":";
234 | payload += WiFi.localIP().toString();
235 | payload += "}";
236 | error = error || !publishMQTTMessage(MQTTTELESTATE3, payload.c_str());
237 |
238 | // ESP32 stats
239 | payload = "";
240 | payload += "{\"up\":";
241 | payload += String(millis());
242 | payload += ",\"heapSize\":";
243 | payload += String(ESP.getHeapSize());
244 | payload += ",\"heapFree\":";
245 | payload += String(ESP.getFreeHeap());
246 | payload += ",\"heapMin\":";
247 | payload += String(ESP.getMinFreeHeap());
248 | payload += ",\"heapMax\":";
249 | payload += String(ESP.getMaxAllocHeap());
250 | payload += "}";
251 | error = error || !publishMQTTMessage(MQTTTELESTATE4, payload.c_str());
252 |
253 | return !error;
254 | }
255 |
256 | void callback(char* topic, byte* payload, unsigned int length) {
257 | // handle message arrived
258 | std::string strPayload(reinterpret_cast(payload), length);
259 |
260 | Log.printf("MQTT message arrived [%s] %s\r\n", topic, strPayload.c_str());
261 |
262 | String topicReceived(topic);
263 |
264 | String topicCmndTargetTemp(MQTTCMNDTARGETTEMP);
265 | String topicCmndActualTemp(MQTTCMNDACTUALTEMP);
266 | String topicCmndFanPWM(MQTTCMNDFANPWM);
267 | String topicCmndFanMode(MQTTCMNDFANMODE);
268 | #if defined(useOTAUpdate)
269 | String topicCmndOTA(MQTTCMNDOTA);
270 | #endif
271 | #if defined(useHomeassistantMQTTDiscovery)
272 | String topicHaStatus(HASSSTATUSTOPIC);
273 | #endif
274 | if (topicReceived == topicCmndTargetTemp) {
275 | #ifdef useAutomaticTemperatureControl
276 | Log.printf("Setting targetTemp via mqtt\r\n");
277 | float num_float = ::atof(strPayload.c_str());
278 | Log.printf("new targetTemp: %.2f\r\n", num_float);
279 | updatePWM_MQTT_Screen_withNewTargetTemperature(num_float, true);
280 | #else
281 | Log.printf("\"#define useAutomaticTemperatureControl\" is NOT used in config.h Cannot set target temperature. Please set fan pwm.\r\n");
282 | updatePWM_MQTT_Screen_withNewTargetTemperature(getTargetTemperature(), true);
283 | #endif
284 | } else if (topicReceived == topicCmndActualTemp) {
285 | #if defined(useAutomaticTemperatureControl) && defined(setActualTemperatureViaMQTT)
286 | Log.printf("Setting actualTemp via mqtt\r\n");
287 | float num_float = ::atoi(strPayload.c_str());
288 | Log.printf("new actualTemp: %.2f\r\n", num_float);
289 | updatePWM_MQTT_Screen_withNewActualTemperature(num_float, true);
290 | #else
291 | Log.printf("\"#define setActualTemperatureViaMQTT\" is NOT used in config.h Cannot set actual temperature. Please use BME280.\r\n");
292 | updatePWM_MQTT_Screen_withNewActualTemperature(getActualTemperature(), true);
293 | #endif
294 | } else if (topicReceived == topicCmndFanPWM) {
295 | #ifndef useAutomaticTemperatureControl
296 | Log.printf("Setting fan pwm via mqtt\r\n");
297 | int num_int = ::atoi(strPayload.c_str());
298 | Log.printf("new fan pwm: %d\r\n", num_int);
299 | updateMQTT_Screen_withNewPWMvalue(num_int, true);
300 | #else
301 | Log.printf("\"#define useAutomaticTemperatureControl\" is used in config.h Cannot set fan pwm. Please set target temperature.\r\n");
302 | updateMQTT_Screen_withNewPWMvalue(getPWMvalue(), true);
303 | #endif
304 | } else if (topicReceived == topicCmndFanMode) {
305 | Log.printf("Setting HVAC mode from HA received via mqtt\r\n");
306 | if (strPayload == MQTTFANMODEFANONLYPAYLOAD) {
307 | Log.printf(" Will turn fan into \"fan_only\" mode\r\n");
308 | updateMQTT_Screen_withNewMode(false, true);
309 | } else if (strPayload == MQTTFANMODEOFFPAYLOAD) {
310 | Log.printf(" Will switch fan off\r\n");
311 | updateMQTT_Screen_withNewMode(true, true);
312 | } else {
313 | Log.printf("Payload %s not supported\r\n", strPayload.c_str());
314 | }
315 | #if defined(useOTAUpdate)
316 | } else if (topicReceived == topicCmndOTA) {
317 | if (strPayload == "ON") {
318 | Log.printf("MQTT command TURN ON OTA received\r\n");
319 | ArduinoOTA.begin();
320 | } else if (strPayload == "OFF") {
321 | Log.printf("MQTT command TURN OFF OTA received\r\n");
322 | ArduinoOTA.end();
323 | } else {
324 | Log.printf("Payload %s not supported\r\n", strPayload.c_str());
325 | }
326 | #endif
327 | #if defined(useHomeassistantMQTTDiscovery)
328 | } else if (topicReceived == topicHaStatus) {
329 | if (strPayload == HASSSTATUSONLINEPAYLOAD) {
330 | Log.printf("HA status online received. This means HA has restarted. Will send discovery again in some seconds as defined in config.h\r\n");
331 | // set timer so that discovery will be resent after some seconds (as defined in config.h)
332 | timerStartForHAdiscovery = millis();
333 | // Very unlikely. Can only happen if millis() overflowed max unsigned long every approx. 50 days
334 | if (timerStartForHAdiscovery == 0) {timerStartForHAdiscovery = 1;}
335 | } else if (strPayload == HASSSTATUSOFFLINEPAYLOAD) {
336 | Log.printf("HA status offline received. Nice to know. Currently we don't react to this.\r\n");
337 | } else {
338 | Log.printf("Payload %s not supported\r\n", strPayload.c_str());
339 | }
340 | #endif
341 | }
342 | }
343 | #endif
344 |
--------------------------------------------------------------------------------
/src/mqtt.h:
--------------------------------------------------------------------------------
1 | #ifdef useMQTT
2 | void mqtt_setup(void);
3 | void mqtt_loop(void);
4 | bool mqtt_publish_tele(void);
5 | bool mqtt_publish_stat_targetTemp();
6 | bool mqtt_publish_stat_actualTemp();
7 | bool mqtt_publish_stat_fanPWM();
8 | bool mqtt_publish_stat_mode();
9 | #ifdef useShutdownButton
10 | bool mqtt_publish_shutdown();
11 | #endif
12 | #ifdef useHomeassistantMQTTDiscovery
13 | /* Sets the start of the timer until HA discovery is sent.
14 | It will be waited WAITAFTERHAISONLINEUNTILDISCOVERYWILLBESENT ms before the discovery is sent.
15 | 0: discovery will not be sent
16 | >0: discovery will be sent as soon as "WAITAFTERHAISONLINEUNTILDISCOVERYWILLBESENT" ms are over
17 | */
18 | extern unsigned long timerStartForHAdiscovery;
19 | bool mqtt_publish_hass_discovery();
20 | #endif
21 | #endif
--------------------------------------------------------------------------------
/src/sensorBME280.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "config.h"
4 | #include "log.h"
5 | #include "temperatureController.h"
6 |
7 | #ifdef useTemperatureSensorBME280
8 | // Standard pressure at sea level. Will be calibrated in initBME280
9 | float calibratedPressureAtSeaLevel = 1013.25;
10 |
11 | float lastTempSensorValues[4];
12 |
13 | Adafruit_BME280 bme;
14 |
15 | TwoWire I2Cone = TwoWire(0);
16 | bool status_BME280 = 0;
17 | #endif
18 |
19 | void initBME280(void){
20 | #ifdef useTemperatureSensorBME280
21 | lastTempSensorValues[0] = NAN;
22 | lastTempSensorValues[1] = NAN;
23 | lastTempSensorValues[2] = NAN;
24 | lastTempSensorValues[3] = NAN;
25 |
26 | I2Cone.begin(I2C_SDA, I2C_SCL, I2C_FREQ);
27 | status_BME280 = bme.begin(BME280_ADDR, &I2Cone);
28 |
29 | if (!status_BME280) {
30 | Log.printf(" Could not find a valid BME280 sensor, check wiring!\r\n");
31 | } else {
32 | Log.printf(" BME280 sucessfully initialized.\r\n");
33 | // Calibrate BME280 with actual pressure and given height. Will be used until restart of ESP32
34 | calibratedPressureAtSeaLevel = (bme.seaLevelForAltitude(HEIGHTOVERSEALEVELATYOURLOCATION, bme.readPressure() / 100.0F));
35 | Log.printf(" BME280 was calibrated to %.1f m\r\n", HEIGHTOVERSEALEVELATYOURLOCATION);
36 | }
37 | #else
38 | Log.printf(" BME280 is disabled in config.h\r\n");
39 | #endif
40 | }
41 |
42 | void updateBME280(void){
43 | #ifdef useTemperatureSensorBME280
44 | if (!status_BME280){
45 | Log.printf("BME280 sensor not initialized, trying again ...\r\n");
46 | initBME280();
47 | if (status_BME280){
48 | Log.printf("success!\r\n");
49 | } else {
50 | lastTempSensorValues[0] = NAN;
51 | #ifndef setActualTemperatureViaMQTT
52 | setActualTemperatureAndPublishMQTT(lastTempSensorValues[0]);
53 | #endif
54 | lastTempSensorValues[1] = NAN;
55 | lastTempSensorValues[2] = NAN;
56 | lastTempSensorValues[3] = NAN;
57 | return;
58 | }
59 | }
60 | lastTempSensorValues[0] = bme.readTemperature();
61 | #ifndef setActualTemperatureViaMQTT
62 | setActualTemperatureAndPublishMQTT(lastTempSensorValues[0]);
63 | #endif
64 | lastTempSensorValues[1] = bme.readPressure() / 100.0F;
65 | lastTempSensorValues[2] = bme.readAltitude(calibratedPressureAtSeaLevel);
66 | lastTempSensorValues[3] = bme.readHumidity();
67 | #endif
68 | }
--------------------------------------------------------------------------------
/src/sensorBME280.h:
--------------------------------------------------------------------------------
1 | #ifdef useTemperatureSensorBME280
2 | extern float lastTempSensorValues[4];
3 | #endif
4 | void initBME280(void);
5 | void updateBME280(void);
--------------------------------------------------------------------------------
/src/temperatureController.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "config.h"
5 | #include "fanPWM.h"
6 | #include "log.h"
7 | #include "mqtt.h"
8 | #include "sensorBME280.h"
9 | #include "tft.h"
10 |
11 | float targetTemperature;
12 | float actualTemperature;
13 | void setActualTemperatureAndPublishMQTT(float aActualTemperature) {
14 | if (actualTemperature != aActualTemperature) {
15 | actualTemperature = aActualTemperature;
16 | #ifdef useMQTT
17 | mqtt_publish_stat_actualTemp();
18 | #endif
19 | }
20 | }
21 |
22 | void updatePWM_MQTT_Screen_withNewTargetTemperature(float aTargetTemperature, bool force);
23 | void updatePWM_MQTT_Screen_withNewActualTemperature(float aActualTemperature, bool force);
24 |
25 | void initTemperatureController(void) {
26 | #ifdef useAutomaticTemperatureControl
27 | targetTemperature = INITIALTARGETTEMPERATURE;
28 | updatePWM_MQTT_Screen_withNewTargetTemperature(targetTemperature, true);
29 | #ifdef setActualTemperatureViaMQTT
30 | setActualTemperatureAndPublishMQTT(NAN);
31 | updatePWM_MQTT_Screen_withNewActualTemperature(actualTemperature, true);
32 | #endif
33 |
34 | #else
35 | Log.printf(" Temperature control is disabled in config.h\r\n");
36 | #endif
37 | }
38 |
39 | float getTargetTemperature(void) {
40 | return targetTemperature;
41 | }
42 | float getActualTemperature(void) {
43 | return actualTemperature;
44 | }
45 |
46 | void setFanPWMbasedOnTemperature(void) {
47 | #ifdef useAutomaticTemperatureControl
48 | float difftemp = getActualTemperature() - targetTemperature;
49 | int newPWMvalue = 255;
50 |
51 | if ((getActualTemperature() == NAN) || (getActualTemperature() <= 0.0)){
52 | Log.printf("WARNING: no temperature value available. Cannot do temperature control. Will set PWM fan to 255.\r\n");
53 | newPWMvalue = 255;
54 | } else if (difftemp <= 0.0) {
55 | // Temperature is below target temperature. Run fan at minimum speed.
56 | newPWMvalue = PWMMINIMUMVALUE;
57 | } else if (difftemp <= 0.5) {
58 | newPWMvalue = 140;
59 | } else if (difftemp <= 1.0) {
60 | newPWMvalue = 160;
61 | } else if (difftemp <= 1.5) {
62 | newPWMvalue = 180;
63 | } else if (difftemp <= 2.0) {
64 | newPWMvalue = 200;
65 | } else if (difftemp <= 2.5) {
66 | newPWMvalue = 220;
67 | } else if (difftemp <= 3.0) {
68 | newPWMvalue = 240;
69 | } else {
70 | // Temperature much too high. Run fan at full speed.
71 | newPWMvalue = 255;
72 | }
73 |
74 | // Log.printf("difftemp = %.2\r\n", difftemp);
75 | // Log.printf("newPWMvalue = %d\r\n", newPWMvalue);
76 |
77 | updateMQTT_Screen_withNewPWMvalue(newPWMvalue, false);
78 | #endif
79 | }
80 |
81 | void updatePWM_MQTT_Screen_withNewTargetTemperature(float aTargetTemperature, bool force) {
82 | if ((targetTemperature != aTargetTemperature) || force) {
83 | targetTemperature = aTargetTemperature;
84 | setFanPWMbasedOnTemperature();
85 | #ifdef useMQTT
86 | mqtt_publish_stat_targetTemp();
87 | #endif
88 | draw_screen();
89 | }
90 | }
91 |
92 | void updatePWM_MQTT_Screen_withNewActualTemperature(float aActualTemperature, bool force) {
93 | if ((actualTemperature != aActualTemperature) || force) {
94 | actualTemperature = aActualTemperature;
95 | setFanPWMbasedOnTemperature();
96 | #ifdef useMQTT
97 | mqtt_publish_stat_actualTemp();
98 | #endif
99 | draw_screen();
100 | }
101 | }
--------------------------------------------------------------------------------
/src/temperatureController.h:
--------------------------------------------------------------------------------
1 | void initTemperatureController(void);
2 | void setFanPWMbasedOnTemperature(void);
3 | float getTargetTemperature(void);
4 | float getActualTemperature(void);
5 | void setActualTemperatureAndPublishMQTT(float aActualTemperature);
6 | void updatePWM_MQTT_Screen_withNewTargetTemperature(float aTargetTemperature, bool force);
7 | void updatePWM_MQTT_Screen_withNewActualTemperature(float aActualTemperature, bool force);
8 |
--------------------------------------------------------------------------------
/src/tft.cpp:
--------------------------------------------------------------------------------
1 | #include "config.h"
2 | #include "fanPWM.h"
3 | #include "fanTacho.h"
4 | #include "log.h"
5 | #include "sensorBME280.h"
6 | #include "temperatureController.h"
7 | #include "tft.h"
8 |
9 | #ifdef DRIVER_ILI9341
10 | #include
11 | #include
12 | #endif
13 | #ifdef DRIVER_ST7735
14 | #include
15 | #endif
16 |
17 | //prepare driver for display
18 | #ifdef DRIVER_ILI9341
19 | Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
20 | // Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);
21 | #endif
22 | #ifdef DRIVER_ST7735
23 | Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
24 | // Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST);
25 | #endif
26 |
27 | #ifdef useTFT
28 | const GFXfont *myFont;
29 | int textSizeOffset;
30 |
31 | // number of screen to display
32 | int screen = SCREEN_NORMALMODE;
33 |
34 | unsigned long startCountdown = 0;
35 |
36 | void calcDimensionsOfElements(void);
37 | void draw_screen(void);
38 | #endif
39 |
40 | void initTFT(void) {
41 | #ifdef useTFT
42 | // start driver
43 | #ifdef DRIVER_ILI9341
44 | // switch display on
45 | pinMode(TFT_LED, OUTPUT);
46 | digitalWrite(TFT_LED, LED_ON);
47 | tft.begin();
48 | myFont = &FreeSans9pt7b;
49 | textSizeOffset = 0;
50 | #endif
51 | #ifdef DRIVER_ST7735
52 | tft.initR(INITR_BLACKTAB);
53 | myFont = NULL;
54 | textSizeOffset = 0;
55 | #endif
56 |
57 | tft.setFont(myFont);
58 | tft.setRotation(TFT_ROTATION);
59 |
60 | calcDimensionsOfElements();
61 |
62 | // clear screen
63 | tft.fillScreen(TFT_BLACK);
64 | draw_screen();
65 |
66 | // show the displays resolution
67 | Log.printf(" TFT sucessfully initialized.\r\n");
68 | Log.printf(" tftx = %d, tfty = %d\r\n", tft.width(), tft.height());
69 |
70 | #else
71 | Log.printf(" TFT is disabled in config.h\r\n");
72 | #endif
73 | }
74 |
75 | #ifdef useTFT
76 | int16_t getRelativeX(int16_t xBasedOnTFTwithScreenWidth320px) {
77 | return (float)(xBasedOnTFTwithScreenWidth320px) /(float)(320) * tft_getWidth();;
78 | }
79 |
80 | int16_t getRelativeY(int16_t yBasedOnTFTwithScreenHeight240px) {
81 | return (float)(yBasedOnTFTwithScreenHeight240px)/(float)(240) * tft_getHeight();;
82 | }
83 |
84 | // rect: x, y, width, heigth
85 | int valueUpRect[4];
86 | int valueDownRect[4];
87 | #if defined (useStandbyButton) || defined(useShutdownButton)
88 | int shutdownRect[4];
89 | int confirmShutdownYesRect[4];
90 | int confirmShutdownNoRect[4];
91 | #endif
92 |
93 | int plusMinusHorizontalLineMarginLeft;
94 | int plusMinusHorizontalLineMarginTop;
95 | int plusMinusHorizontalLineLength;
96 | int plusMinusVerticalLineMarginTop;
97 | int plusMinusVerticalLineLength;
98 | int plusMinusVerticalLineMarginLeft;
99 |
100 | int tempAreaLeft; int tempAreaTop; int tempAreaWidth;
101 | int fanAreaLeft; int fanAreaTop; int fanAreaWidth;
102 | int ambientAreaLeft; int ambientAreaTop; int ambientAreaWidth;
103 |
104 | #if defined (useStandbyButton) || defined(useShutdownButton)
105 | int shutdownWidthAbsolute;
106 | int shutdownHeightAbsolute;
107 | #endif
108 |
109 | void calcDimensionsOfElements(void) {
110 | // upper left corner is 0,0
111 | // width and heigth are only valid for landscape (rotation=1) or landscape upside down (rotation=3)
112 | // ILI9341 ST7735
113 | // AZ-Touch
114 | // tft.width 0 <= x < 320 160
115 | // tft.height 0 <= y < 240 128
116 |
117 | // ALL VALUES ARE BASED ON A 320x240 DISPLAY and automatically resized to the actual display size via getRelativeX() and getRelativeYI()
118 | int marginTopAbsolute = 12;
119 | int marginLeftAbsolute = 14;
120 | // int areaHeightAbsolute = 64; // make sure: 4*marginTopAbsolute + 3*areaHeightAbsolute = 240
121 | int areaHeightAbsolute = (240 - 4*marginTopAbsolute) / 3;
122 |
123 | int valueUpDownWidthAbsolute = 80;
124 | int valueUpDownHeightAbsolute = 55;
125 | #if defined (useStandbyButton) || defined(useShutdownButton)
126 | shutdownWidthAbsolute = 40;
127 | shutdownHeightAbsolute = 40;
128 | #endif
129 | int valueUpRectTop;
130 | int valueDownRectTop;
131 | #if defined (useStandbyButton) || defined(useShutdownButton)
132 | int shutdownRectTop;
133 | #endif
134 |
135 | tempAreaLeft = getRelativeX(marginLeftAbsolute);
136 | fanAreaLeft = getRelativeX(marginLeftAbsolute);
137 | ambientAreaLeft = getRelativeX(marginLeftAbsolute);
138 | #ifdef useAutomaticTemperatureControl
139 | tempAreaTop = getRelativeY(marginTopAbsolute);
140 | fanAreaTop = getRelativeY(marginTopAbsolute+areaHeightAbsolute+marginTopAbsolute);
141 | valueUpRectTop = fanAreaTop;
142 | ambientAreaTop = getRelativeY(marginTopAbsolute+areaHeightAbsolute+marginTopAbsolute+areaHeightAbsolute+marginTopAbsolute );
143 | valueDownRectTop = ambientAreaTop;
144 | #if defined (useStandbyButton) || defined(useShutdownButton)
145 | tempAreaWidth = getRelativeX(320-marginLeftAbsolute - shutdownWidthAbsolute-marginLeftAbsolute); // screen - marginleft - [Area] - 40 shutdown - marginright
146 | #else
147 | tempAreaWidth = getRelativeX(320-marginLeftAbsolute - 0); // screen - marginleft - [Area] - marginright
148 | #endif
149 | #ifdef useTouch
150 | fanAreaWidth = getRelativeX(320-marginLeftAbsolute - valueUpDownWidthAbsolute-marginLeftAbsolute); // screen - marginleft - [Area] - 80 up/down - marginright
151 | ambientAreaWidth = getRelativeX(320-marginLeftAbsolute - valueUpDownWidthAbsolute-marginLeftAbsolute);
152 | #else
153 | fanAreaWidth = getRelativeX(320-marginLeftAbsolute - 0); // screen - marginleft - [Area] - marginright
154 | ambientAreaWidth = getRelativeX(320-marginLeftAbsolute - 0);
155 | #endif
156 | #if defined (useStandbyButton) || defined(useShutdownButton)
157 | shutdownRectTop = getRelativeY(marginTopAbsolute);
158 | #endif
159 | #else
160 | fanAreaTop = getRelativeY(marginTopAbsolute);
161 | valueUpRectTop = fanAreaTop;
162 | ambientAreaTop = getRelativeY(marginTopAbsolute+areaHeightAbsolute+marginTopAbsolute);
163 | valueDownRectTop = ambientAreaTop;
164 | #ifdef useTouch
165 | fanAreaWidth = getRelativeX(320-marginLeftAbsolute - valueUpDownWidthAbsolute-marginLeftAbsolute); // screen - marginleft - [Area] - 80 up/down - marginright
166 | ambientAreaWidth = getRelativeX(320-marginLeftAbsolute - valueUpDownWidthAbsolute-marginLeftAbsolute);
167 | #else
168 | fanAreaWidth = getRelativeX(320-marginLeftAbsolute - 0); // screen - marginleft - [Area] - marginright
169 | ambientAreaWidth = getRelativeX(320-marginLeftAbsolute - 0);
170 | #endif
171 | #if defined (useStandbyButton) || defined(useShutdownButton)
172 | shutdownRectTop = getRelativeY(240-shutdownHeightAbsolute-marginTopAbsolute);
173 | #endif
174 | #endif
175 |
176 | valueUpRect[0] = getRelativeX(320-valueUpDownWidthAbsolute-marginLeftAbsolute);
177 | valueUpRect[1] = valueUpRectTop;
178 | valueUpRect[2] = getRelativeX(valueUpDownWidthAbsolute);
179 | valueUpRect[3] = getRelativeY(valueUpDownHeightAbsolute);
180 |
181 | valueDownRect[0] = getRelativeX(320-valueUpDownWidthAbsolute-marginLeftAbsolute);
182 | valueDownRect[1] = valueDownRectTop;
183 | valueDownRect[2] = getRelativeX(valueUpDownWidthAbsolute);
184 | valueDownRect[3] = getRelativeY(valueUpDownHeightAbsolute);
185 |
186 | plusMinusHorizontalLineLength = (valueUpRect[2] / 2) - 4; // 36
187 | plusMinusHorizontalLineMarginLeft = (valueUpRect[2] - plusMinusHorizontalLineLength) / 2; // 22
188 | plusMinusHorizontalLineMarginTop = valueUpRect[3] / 2; // 27
189 |
190 | plusMinusVerticalLineLength = plusMinusHorizontalLineLength; // 36
191 | plusMinusVerticalLineMarginTop = (valueUpRect[3] - plusMinusVerticalLineLength) / 2; // 9
192 | plusMinusVerticalLineMarginLeft = valueUpRect[2] / 2; // 40
193 |
194 | #if defined (useStandbyButton) || defined(useShutdownButton)
195 | shutdownRect[0] = getRelativeX(320-shutdownWidthAbsolute-marginLeftAbsolute);
196 | shutdownRect[1] = shutdownRectTop;
197 | shutdownRect[2] = getRelativeX(shutdownWidthAbsolute);
198 | shutdownRect[3] = getRelativeY(shutdownHeightAbsolute);
199 |
200 | confirmShutdownYesRect[0] = getRelativeX(40);
201 | confirmShutdownYesRect[1] = getRelativeY(90);
202 | confirmShutdownYesRect[2] = getRelativeX(60);
203 | confirmShutdownYesRect[3] = getRelativeY(60);
204 | confirmShutdownNoRect[0] = getRelativeX(200);
205 | confirmShutdownNoRect[1] = getRelativeY(90);
206 | confirmShutdownNoRect[2] = getRelativeX(60);
207 | confirmShutdownNoRect[3] = getRelativeY(60);
208 | #endif
209 | }
210 |
211 | int16_t tft_getWidth(void) {
212 | return tft.width();
213 | }
214 |
215 | int16_t tft_getHeight(void) {
216 | return tft.height();
217 | }
218 |
219 | void tft_fillScreen(void) {
220 | tft.fillScreen(TFT_BLACK);
221 | };
222 |
223 | /*
224 | https://learn.adafruit.com/adafruit-gfx-graphics-library/using-fonts
225 | https://www.heise.de/ratgeber/Adafruit-GFX-Library-Einfache-Grafiken-mit-ESP32-und-Display-erzeugen-7546653.html?seite=all
226 | https://www.heise.de/select/make/2023/2/2304608284785808657
227 | AZ-Touch
228 | ST7735 ILI9341
229 | TextSize Standard FreeSans9pt7b FreeSerif9pt7b FreeMono9pt7b FreeSerifBoldItalic24pt7b
230 | y1/h
231 | 1 pwm hum 0/8 -12/17 -11/16 -9/13 -30/40
232 | 2 temp 0/16 -24/34 -22/32 -18/26 -60/80
233 | 3 0/24 -36/51 -33/48 -27/39 -90/120
234 | 4 countdown 0/32 -48/68 -44/64 -36/52 -120/160
235 | 8 0/64 -96/136 -88/128 -72/104 -240/320
236 | 15 0/120 -180/255 -165240 -135/195 -450/600
237 | */
238 | void printText(int areaX, int areaY, int areaWidth, int lineNr, const char *str, uint8_t textSize, const GFXfont *f, bool wipe) {
239 | // get text bounds
240 | GFXcanvas1 testCanvas(tft_getWidth(), tft_getHeight());
241 | int16_t x1; int16_t y1; uint16_t w; uint16_t h;
242 | testCanvas.setFont(f);
243 | testCanvas.setTextSize(textSize);
244 | testCanvas.setTextWrap(false);
245 | testCanvas.getTextBounds("0WIYgy,", 0, 0, &x1, &y1, &w, &h);
246 | // Log.printf(" x1 = %d, y1 = %d, w=%d, h=%d\r\n", x1, y1, w, h);
247 | int textHeight = h;
248 | int textAreaHeight = textHeight +2; // additional 2 px as vertical spacing between lines
249 | // y1=0 only for standardfont, with every other font this value gets negative!
250 | // This means that when using standarfont at (0,0), it really gets printed at 0,0.
251 | // With every other font, printing at (0,0) means that text starts at (0, y1) with y1 being negative!
252 | int textAreaOffset = -y1;
253 | // Don't know why, but a little additional correction has to be done for every font other than standard font. Doesn't work perfectly, sometimes position is wrong by 1 pixel
254 | // if (textAreaOffset != 0) {
255 | // textAreaOffset = textAreaOffset + textSize;
256 | // }
257 |
258 | // Version 1: flickering, but faster
259 | #ifdef useTouch
260 | tft.setFont(f);
261 | tft.setTextSize(textSize);
262 | tft.setTextWrap(false);
263 | if (wipe) {
264 | tft.fillRect (areaX, areaY + lineNr*textAreaHeight, areaWidth, textAreaHeight, TFT_BLACK);
265 | }
266 | tft.setCursor(areaX, areaY + textAreaOffset + lineNr*textAreaHeight);
267 | tft.printf(str);
268 | #endif
269 |
270 | // Version 2: flicker-free, but slower. Touch becomes unusable.
271 | #ifndef useTouch
272 | GFXcanvas1 canvas(areaWidth, textAreaHeight);
273 | canvas.setFont(f);
274 | canvas.setTextSize(textSize);
275 | canvas.setTextWrap(false);
276 | canvas.setCursor(0, textAreaOffset);
277 | canvas.println(str);
278 | tft.drawBitmap(areaX, areaY + lineNr*textAreaHeight, canvas.getBuffer(), areaWidth, textAreaHeight, TFT_WHITE, TFT_BLACK);
279 | #endif
280 | }
281 | #endif
282 |
283 | void switchOff_screen(boolean switchOff) {
284 | #ifdef useTFT
285 | if (switchOff) {
286 | Log.printf(" Will switch TFT off.\r\n");
287 | digitalWrite(TFT_LED, !LED_ON);
288 | // if digitalWrite does not work for your screen, then try: tft_fillScreen();
289 | } else {
290 | Log.printf(" Will switch TFT on.\r\n");
291 | digitalWrite(TFT_LED, LED_ON);
292 | // if digitalWrite does not work for your screen, then try: draw_screen();
293 | }
294 | #endif
295 | }
296 |
297 | void draw_screen(void) {
298 | if (getModeIsOff()) {
299 | return;
300 | }
301 | #ifdef useTFT
302 | char buffer[100];
303 | // don't understand why I have to do this
304 | #ifdef useTouch
305 | String percentEscaped = "%%";
306 | #else
307 | String percentEscaped = "%";
308 | #endif
309 |
310 | if (screen == SCREEN_NORMALMODE) {
311 | tft.setTextColor(TFT_WHITE, TFT_BLACK);
312 |
313 | // actual temperature and target temperature
314 | #ifdef useAutomaticTemperatureControl
315 | sprintf(buffer, "%.1f (actual)", getActualTemperature());
316 | printText(tempAreaLeft, tempAreaTop, tempAreaWidth, 0, buffer, textSizeOffset + 2, myFont, true);
317 | sprintf(buffer, "%.1f (target)", getTargetTemperature());
318 | printText(tempAreaLeft, tempAreaTop, tempAreaWidth, 1, buffer, textSizeOffset + 2, myFont, true);
319 | #endif
320 |
321 | // fan
322 | printText(fanAreaLeft, fanAreaTop, fanAreaWidth, 0, "Fan:", textSizeOffset + 1, myFont, false);
323 | sprintf(buffer, "%d rpm (%d%s)", last_rpm, (100*last_rpm)/FANMAXRPM, percentEscaped.c_str());
324 | printText(fanAreaLeft, fanAreaTop, fanAreaWidth, 1, buffer, textSizeOffset + 1, myFont, true);
325 | sprintf(buffer, "%d/255 pwm (%d%s)", getPWMvalue(), (100*getPWMvalue())/255, percentEscaped.c_str());
326 | printText(fanAreaLeft, fanAreaTop, fanAreaWidth, 2, buffer, textSizeOffset + 1, myFont, true);
327 |
328 | // relative humidity, barometric pressure and ambient temperature
329 | #ifdef useTemperatureSensorBME280
330 | int ambientLine = 0;
331 | printText(ambientAreaLeft, ambientAreaTop, ambientAreaWidth, ambientLine++, "Ambient:", textSizeOffset + 1, myFont, false);
332 | #if ( (!defined(useAutomaticTemperatureControl) && defined(useTemperatureSensorBME280)) || ( defined(useAutomaticTemperatureControl) && defined(setActualTemperatureViaMQTT)) )
333 | sprintf(buffer, "%.1f temperature", lastTempSensorValues[0]);
334 | printText(ambientAreaLeft, ambientAreaTop, ambientAreaWidth, ambientLine++, buffer, textSizeOffset + 1, myFont, true);
335 | #endif
336 | sprintf(buffer, "%.2f hPa (%.2f m)", lastTempSensorValues[1], lastTempSensorValues[2]);
337 | printText(ambientAreaLeft, ambientAreaTop, ambientAreaWidth, ambientLine++, buffer, textSizeOffset + 1, myFont, true);
338 | sprintf(buffer, "%.2f%s humidity", lastTempSensorValues[3], percentEscaped.c_str());
339 | printText(ambientAreaLeft, ambientAreaTop, ambientAreaWidth, ambientLine++, buffer, textSizeOffset + 1, myFont, true);
340 | #endif
341 |
342 | #ifdef useTouch
343 | // increase temperature or pwm
344 | tft.fillRoundRect(valueUpRect[0], valueUpRect[1], valueUpRect[2], valueUpRect[3], 4, TFT_GREEN);
345 | // plus sign
346 | // horizontal line
347 | tft.drawLine(valueUpRect[0]+plusMinusHorizontalLineMarginLeft, valueUpRect[1]+plusMinusHorizontalLineMarginTop, valueUpRect[0]+plusMinusHorizontalLineMarginLeft + plusMinusHorizontalLineLength, valueUpRect[1]+plusMinusHorizontalLineMarginTop, TFT_BLACK);
348 | tft.drawLine(valueUpRect[0]+plusMinusHorizontalLineMarginLeft, valueUpRect[1]+plusMinusHorizontalLineMarginTop+1, valueUpRect[0]+plusMinusHorizontalLineMarginLeft + plusMinusHorizontalLineLength, valueUpRect[1]+plusMinusHorizontalLineMarginTop+1, TFT_BLACK);
349 | // vertical line
350 | tft.drawLine(valueUpRect[0]+plusMinusVerticalLineMarginLeft, valueUpRect[1]+plusMinusVerticalLineMarginTop, valueUpRect[0]+plusMinusVerticalLineMarginLeft, valueUpRect[1]+plusMinusVerticalLineMarginTop + plusMinusVerticalLineLength, TFT_BLACK);
351 | tft.drawLine(valueUpRect[0]+plusMinusVerticalLineMarginLeft+1, valueUpRect[1]+plusMinusVerticalLineMarginTop, valueUpRect[0]+plusMinusVerticalLineMarginLeft+1, valueUpRect[1]+plusMinusVerticalLineMarginTop + plusMinusVerticalLineLength, TFT_BLACK);
352 | // decrease temperature or pwm
353 | tft.fillRoundRect(valueDownRect[0], valueDownRect[1], valueDownRect[2], valueDownRect[3], 4, TFT_GREEN);
354 | // minus sign
355 | // horizontal line
356 | tft.drawLine(valueDownRect[0]+plusMinusHorizontalLineMarginLeft, valueDownRect[1]+plusMinusHorizontalLineMarginTop, valueDownRect[0]+plusMinusHorizontalLineMarginLeft + plusMinusHorizontalLineLength, valueDownRect[1]+plusMinusHorizontalLineMarginTop, TFT_BLACK);
357 | tft.drawLine(valueDownRect[0]+plusMinusHorizontalLineMarginLeft, valueDownRect[1]+plusMinusHorizontalLineMarginTop+1, valueDownRect[0]+plusMinusHorizontalLineMarginLeft + plusMinusHorizontalLineLength, valueDownRect[1]+plusMinusHorizontalLineMarginTop+1, TFT_BLACK);
358 | #endif
359 |
360 | #if defined (useStandbyButton) || defined(useShutdownButton)
361 | // shutdown button
362 | // square, without round corners
363 | // tft.fillRect(shutdownRect[0], shutdownRect[1], shutdownRect[2], shutdownRect[3], TFT_RED);
364 | // round corners, inner part
365 | tft.fillRoundRect(shutdownRect[0], shutdownRect[1], shutdownRect[2], shutdownRect[3], getRelativeX(4), TFT_RED);
366 | // round corners, outer line
367 | // tft.drawRoundRect(shutdownRect[0], shutdownRect[1], shutdownRect[2], shutdownRect[3], 4, TFT_WHITE);
368 | tft.drawCircle(shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2), shutdownRect[1]+getRelativeY(shutdownHeightAbsolute/2), getRelativeX(shutdownWidthAbsolute*0.375), TFT_WHITE);
369 | tft.drawCircle(shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2), shutdownRect[1]+getRelativeY(shutdownHeightAbsolute/2), getRelativeX(shutdownWidthAbsolute*0.375)-1, TFT_WHITE);
370 | tft.drawLine( shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2), shutdownRect[1]+getRelativeY(shutdownHeightAbsolute/4), shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2), shutdownRect[1]+getRelativeY(shutdownHeightAbsolute*3/4), TFT_WHITE);
371 | tft.drawLine( shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2)+1, shutdownRect[1]+getRelativeY(shutdownHeightAbsolute/4), shutdownRect[0]+getRelativeX(shutdownWidthAbsolute/2)+1, shutdownRect[1]+getRelativeY(shutdownHeightAbsolute*3/4), TFT_WHITE);
372 | #endif
373 | #ifdef useShutdownButton
374 | } else if (screen == SCREEN_CONFIRMSHUTDOWN) {
375 | printText(getRelativeX(44), getRelativeY(50), getRelativeX(200), 0, "Please confirm shutdown", textSizeOffset + 1, myFont, false);
376 |
377 | // confirm: yes
378 | tft.fillRoundRect(confirmShutdownYesRect[0], confirmShutdownYesRect[1], confirmShutdownYesRect[2], confirmShutdownYesRect[3], 4, TFT_RED);
379 | printText(confirmShutdownYesRect[0]+getRelativeX(12), confirmShutdownYesRect[1] + getRelativeY(22), getRelativeX(200), 0, "Yes", textSizeOffset + 1, myFont, false);
380 | // confirm: no
381 | tft.fillRoundRect(confirmShutdownNoRect[0], confirmShutdownNoRect[1], confirmShutdownNoRect[2], confirmShutdownNoRect[3], 4, TFT_GREEN);
382 | printText(confirmShutdownNoRect[0]+ getRelativeX(18), confirmShutdownNoRect[1] + getRelativeY(22), getRelativeX(200), 0, "No", textSizeOffset + 1, myFont, false);
383 | } else if (screen == SCREEN_COUNTDOWN) {
384 | float floatSecondsSinceShutdown = (unsigned long)(millis()-startCountdown) / 1000;
385 | // rounding
386 | floatSecondsSinceShutdown = floatSecondsSinceShutdown + 0.5 - (floatSecondsSinceShutdown<0);
387 | // convert float to int
388 | int intSecondsSinceShutdown = (int)floatSecondsSinceShutdown;
389 |
390 | // clear screen
391 | tft.fillScreen(TFT_BLACK);
392 | sprintf(buffer, "%d", SHUTDOWNCOUNTDOWN - intSecondsSinceShutdown);
393 | printText(getRelativeX(115), getRelativeY(80), getRelativeX(200), 0, buffer, textSizeOffset + 4, myFont, false);
394 |
395 | if ((unsigned long)(millis() - startCountdown) > SHUTDOWNCOUNTDOWN*1000 + 15000){
396 | // if EPS32 is still alive, which means power is still available, then stop countdown and go back to normal mode
397 | Log.printf("hm, still alive? Better show mainscreen again\r\n");
398 | screen = SCREEN_NORMALMODE;
399 | // clear screen
400 | tft.fillRect(0, 0, 320, 240, TFT_BLACK);
401 | draw_screen();
402 | }
403 | #endif
404 | }
405 | #endif
406 | }
407 |
--------------------------------------------------------------------------------
/src/tft.h:
--------------------------------------------------------------------------------
1 | void initTFT(void);
2 | void draw_screen(void);
3 | void switchOff_screen(boolean switchOff);
4 | #ifdef useTFT
5 | extern int screen;
6 | const int SCREEN_NORMALMODE = 1;
7 | const int SCREEN_CONFIRMSHUTDOWN = 2;
8 | const int SCREEN_COUNTDOWN = 3;
9 |
10 | extern unsigned long startCountdown;
11 | extern int valueUpRect[4];
12 | extern int valueDownRect[4];
13 | extern int shutdownRect[4];
14 | extern int confirmShutdownYesRect[4];
15 | extern int confirmShutdownNoRect[4];
16 |
17 | int16_t tft_getWidth(void);
18 | int16_t tft_getHeight(void);
19 | void tft_fillScreen(void);
20 |
21 | #define TFT_BLACK 0x0000 /* 0, 0, 0 */
22 | #define TFT_NAVY 0x000F /* 0, 0, 128 */
23 | #define TFT_DARKGREEN 0x03E0 /* 0, 128, 0 */
24 | #define TFT_DARKCYAN 0x03EF /* 0, 128, 128 */
25 | #define TFT_MAROON 0x7800 /* 128, 0, 0 */
26 | #define TFT_PURPLE 0x780F /* 128, 0, 128 */
27 | #define TFT_OLIVE 0x7BE0 /* 128, 128, 0 */
28 | #define TFT_LIGHTGREY 0xD69A /* 211, 211, 211 */
29 | #define TFT_DARKGREY 0x7BEF /* 128, 128, 128 */
30 | #define TFT_BLUE 0x001F /* 0, 0, 255 */
31 | #define TFT_GREEN 0x07E0 /* 0, 255, 0 */
32 | #define TFT_CYAN 0x07FF /* 0, 255, 255 */
33 | #define TFT_RED 0xF800 /* 255, 0, 0 */
34 | #define TFT_MAGENTA 0xF81F /* 255, 0, 255 */
35 | #define TFT_YELLOW 0xFFE0 /* 255, 255, 0 */
36 | #define TFT_WHITE 0xFFFF /* 255, 255, 255 */
37 | #define TFT_ORANGE 0xFDA0 /* 255, 180, 0 */
38 | #define TFT_GREENYELLOW 0xB7E0 /* 180, 255, 0 */
39 | #define TFT_PINK 0xFC9F /* 255, 192, 203 */
40 | #define TFT_BROWN 0x9A60 /* 150, 75, 0 */
41 | #define TFT_GOLD 0xFEA0 /* 255, 215, 0 */
42 | #define TFT_SILVER 0xC618 /* 192, 192, 192 */
43 | #define TFT_SKYBLUE 0x867D /* 135, 206, 235 */
44 | #define TFT_VIOLET 0x915C /* 180, 46, 226 */
45 | #endif
--------------------------------------------------------------------------------
/src/tftTouch.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "config.h"
5 | #include "fanPWM.h"
6 | #include "log.h"
7 | #include "temperatureController.h"
8 | #include "tft.h"
9 | #include "mqtt.h"
10 |
11 | #ifdef useTouch
12 | //prepare driver for touch screen
13 | XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);
14 | // init TouchEvent with pointer to the touch screen driver
15 | TouchEvent tevent(touch);
16 |
17 | // point on touchscreen that got hit
18 | TS_Point p;
19 | // current position
20 | int tsx, tsy, tsxraw, tsyraw;
21 |
22 | // checks if point x/y is inside rect[]
23 | bool pointInRect(const int rect[], int x, int y) {
24 | static bool invertTouchCoordinates = (TFT_ROTATION == 3);
25 | #if defined(TOUCH_INVERT_COORDINATES)
26 | invertTouchCoordinates = !invertTouchCoordinates;
27 | #endif
28 |
29 | if (!invertTouchCoordinates) {
30 | return
31 | (x >= rect[0]) &&
32 | (x <= rect[0] + rect[2]) &&
33 | (y >= rect[1]) &&
34 | (y <= rect[1] + rect[3]);
35 | } else {
36 | return
37 | (tft_getWidth() - x >= rect[0]) &&
38 | (tft_getWidth() - x <= rect[0] + rect[2]) &&
39 | (tft_getHeight() - y >= rect[1]) &&
40 | (tft_getHeight() - y <= rect[1] + rect[3]);
41 | }
42 | }
43 |
44 | void onClick(TS_Point p) {
45 | if (getModeIsOff()) {
46 | // when screen is off, don't react to event, only turn on screen
47 | updateMQTT_Screen_withNewMode(false, true);
48 | return;
49 | }
50 | // store x and y as raw data
51 | tsxraw = p.x;
52 | tsyraw = p.y;
53 |
54 | tsx = 320 - tsxraw;
55 | tsy = 240 - tsyraw;
56 | Log.printf("click %d %d\r\n", tsx, tsy);
57 |
58 | if (screen == SCREEN_NORMALMODE) {
59 | if (pointInRect(valueUpRect, tsx, tsy)) {
60 | Log.printf("up button hit\r\n");
61 | #ifdef useAutomaticTemperatureControl
62 | updatePWM_MQTT_Screen_withNewTargetTemperature(getTargetTemperature() + 1, true);
63 | #else
64 | incFanSpeed();
65 | #endif
66 | } else if (pointInRect(valueDownRect, tsx, tsy)) {
67 | Log.printf("down button hit\r\n");
68 | #ifdef useAutomaticTemperatureControl
69 | updatePWM_MQTT_Screen_withNewTargetTemperature(getTargetTemperature() -1, true);
70 | #else
71 | decFanSpeed();
72 | #endif
73 | }
74 | #ifdef useShutdownButton
75 | else if (pointInRect(shutdownRect, tsx, tsy)) {
76 | Log.printf("shutdown button hit\r\n");
77 | screen = SCREEN_CONFIRMSHUTDOWN;
78 | // clear screen
79 | tft_fillScreen();
80 | draw_screen();
81 | }
82 | #endif
83 | #ifdef useStandbyButton
84 | else if (pointInRect(shutdownRect, tsx, tsy)) {
85 | Log.printf("standby button hit\r\n");
86 | updateMQTT_Screen_withNewMode(true, true);
87 | }
88 | #endif
89 | }
90 | #ifdef useShutdownButton
91 | else if (screen == SCREEN_CONFIRMSHUTDOWN) {
92 | if (pointInRect(confirmShutdownYesRect, tsx, tsy)) {
93 | Log.printf("confirm shutdown yes hit\r\n");
94 | if (mqtt_publish_shutdown()){
95 | screen = SCREEN_COUNTDOWN;
96 | startCountdown = millis();
97 | } else {
98 | screen =SCREEN_NORMALMODE;
99 | }
100 | // clear screen
101 | tft_fillScreen();
102 | draw_screen();
103 | } else if (pointInRect(confirmShutdownNoRect, tsx, tsy)) {
104 | Log.printf("confirm shutdown no hit\r\n");
105 | screen = SCREEN_NORMALMODE;
106 | // clear screen
107 | tft_fillScreen();
108 | draw_screen();
109 | }
110 | }
111 | #endif
112 | }
113 | #endif
114 | void initTFTtouch(void) {
115 | #ifdef useTouch
116 | //start driver
117 | touch.begin();
118 |
119 | //init TouchEvent instance
120 | tevent.setResolution(tft_getWidth(),tft_getHeight());
121 | tevent.setDblClick(010);
122 | // tevent.registerOnTouchSwipe(onSwipe);
123 | tevent.registerOnTouchClick(onClick);
124 | // tevent.registerOnTouchDblClick(onDblClick);
125 | // tevent.registerOnTouchLong(onLongClick);
126 | // tevent.registerOnTouchDraw(onDraw);
127 | // tevent.registerOnTouchDown(onTouch);
128 | // tevent.registerOnTouchUp(onUntouch);
129 |
130 | Log.printf(" TFTtouch sucessfully initialized.\r\n");
131 | #else
132 | Log.printf(" Touch is disabled in config.h\r\n");
133 | #endif
134 | }
135 |
136 | void processUserInput(void) {
137 | #ifdef useTouch
138 | tevent.pollTouchScreen();
139 | #endif
140 | }
--------------------------------------------------------------------------------
/src/tftTouch.h:
--------------------------------------------------------------------------------
1 | void initTFTtouch(void);
2 | void processUserInput(void);
3 |
--------------------------------------------------------------------------------
/src/wifiCommunication.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #if defined(ESP32)
3 | #include
4 | #endif
5 | #if defined(ESP8266)
6 | #include
7 | #endif
8 |
9 | #include "config.h"
10 | #include "log.h"
11 | #include "wifiCommunication.h"
12 |
13 | #ifdef useWIFI
14 |
15 | boolean connected = false;
16 | bool wifiIsDisabled = true;
17 |
18 | #if defined(WIFI_KNOWN_APS)
19 | String accessPointName;
20 | const std::string wifiAccessPoints[WIFI_KNOWN_APS_COUNT][2] = {WIFI_KNOWN_APS};
21 | #endif
22 |
23 | #if defined(WIFI_KNOWN_APS)
24 | void setAccessPointName() {
25 | String BSSID = String(WiFi.BSSIDstr());
26 | for (unsigned int i = 0; i < WIFI_KNOWN_APS_COUNT; i++) {
27 | if (wifiAccessPoints[i][0].compare(BSSID.c_str()) == 0) {
28 | accessPointName = wifiAccessPoints[i][1].c_str();
29 | return;
30 | }
31 | }
32 | accessPointName = "unknown";
33 | return;
34 | }
35 | #endif
36 |
37 | #if defined(ESP32)
38 | void printWiFiStatus(void){
39 | if (wifiIsDisabled) return;
40 |
41 | if (WiFi.isConnected()) {
42 | Serial.printf(MY_LOG_FORMAT(" WiFi.status() == connected\r\n"));
43 | } else {
44 | Serial.printf(MY_LOG_FORMAT(" WiFi.status() == DIS-connected\r\n"));
45 | }
46 | // Serial.println(WiFi.localIP());
47 | Serial.printf(MY_LOG_FORMAT(" IP address: %s\r\n"), WiFi.localIP().toString().c_str());
48 |
49 | if (WiFi.isConnected()) { // && WiFi.localIP().isSet()) {
50 | Serial.printf(MY_LOG_FORMAT(" WiFi connected and IP is set :-)\r\n"));
51 | } else {
52 | Serial.printf(MY_LOG_FORMAT(" WiFi not completely available :-(\r\n"));
53 | }
54 | }
55 | void WiFiStationConnected(WiFiEvent_t event, WiFiEventInfo_t info){
56 | Serial.printf(MY_LOG_FORMAT(" Callback \"StationConnected\"\r\n"));
57 |
58 | printWiFiStatus();
59 | }
60 | void WiFiStationDisconnected(WiFiEvent_t event, WiFiEventInfo_t info){
61 | Serial.printf(MY_LOG_FORMAT(" Callback \"StationDisconnected\"\r\n"));
62 | connected = false;
63 |
64 | printWiFiStatus();
65 |
66 | // shouldn't even be here when wifiIsDisabled, but still happens ...
67 | if (!wifiIsDisabled) {
68 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
69 | }
70 | }
71 | void WiFiGotIP(WiFiEvent_t event, WiFiEventInfo_t info){
72 | Serial.printf(MY_LOG_FORMAT(" Callback \"GotIP\"\r\n"));
73 | connected = true;
74 | #if defined(WIFI_KNOWN_APS)
75 | setAccessPointName();
76 | #endif
77 |
78 | printWiFiStatus();
79 | }
80 | #endif
81 | #if defined(ESP8266)
82 | void printWiFiStatus(void){
83 | if (wifiIsDisabled) return;
84 |
85 | if (WiFi.isConnected()) {
86 | Serial.printf(MY_LOG_FORMAT(" WiFi.status() == connected\r\n"));
87 | } else {
88 | Serial.printf(MY_LOG_FORMAT(" WiFi.status() == DIS-connected\r\n"));
89 | }
90 | // Serial.println(WiFi.localIP());
91 | Serial.printf(MY_LOG_FORMAT(" IP address: %s\r\n"), WiFi.localIP().toString().c_str());
92 |
93 | if (WiFi.isConnected()) { // && WiFi.localIP().isSet()) {
94 | Serial.printf(MY_LOG_FORMAT(" WiFi connected and IP is set :-)\r\n"));
95 | } else {
96 | Serial.printf(MY_LOG_FORMAT(" WiFi not completely available :-(\r\n"));
97 | }
98 | }
99 | //callback on WiFi connected
100 | void onSTAConnected (WiFiEventStationModeConnected event_info) {
101 | Serial.printf(MY_LOG_FORMAT(" Callback \"onStationModeConnected\"\r\n"));
102 | Serial.printf(MY_LOG_FORMAT(" Connected to %s\r\n"), event_info.ssid.c_str ());
103 |
104 | printWiFiStatus();
105 | }
106 | // Manage network disconnection
107 | void onSTADisconnected (WiFiEventStationModeDisconnected event_info) {
108 | Serial.printf(MY_LOG_FORMAT(" Callback \"onStationModeDisconnected\"\r\n"));
109 | Serial.printf(MY_LOG_FORMAT(" Disconnected from SSID: %s\r\n"), event_info.ssid.c_str ());
110 | Serial.printf(MY_LOG_FORMAT(" Reason: %d\r\n"), event_info.reason);
111 | connected = false;
112 |
113 | printWiFiStatus();
114 |
115 | // shouldn't even be here when wifiIsDisabled, but still happens ...
116 | if (!wifiIsDisabled) {
117 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
118 | }
119 | }
120 | //callback on got IP address
121 | // Start NTP only after IP network is connected
122 | void onSTAGotIP (WiFiEventStationModeGotIP event_info) {
123 | Serial.printf(MY_LOG_FORMAT(" Callback \"onStationModeGotIP\"\r\n"));
124 | Serial.printf(MY_LOG_FORMAT(" Got IP: %s\r\n"), event_info.ip.toString ().c_str ());
125 | Serial.printf(MY_LOG_FORMAT(" Connected: %s\r\n"), WiFi.isConnected() ? "yes" : "no");
126 | connected = true;
127 | setAccessPointName();
128 |
129 | printWiFiStatus();
130 | }
131 | #endif
132 |
133 | void wifi_enable(void) {
134 | wifiIsDisabled = false;
135 |
136 | #if defined(ESP32)
137 | #if defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2)
138 | WiFi.onEvent(WiFiStationDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
139 | #else
140 | WiFi.onEvent(WiFiStationDisconnected, SYSTEM_EVENT_STA_DISCONNECTED);
141 | #endif
142 | #endif
143 | #if defined(ESP8266)
144 | static WiFiEventHandler e2;
145 | e2 = WiFi.onStationModeDisconnected(onSTADisconnected);
146 | #endif
147 | WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
148 | }
149 | void wifi_disable(void){
150 | wifiIsDisabled = true;
151 |
152 | #if defined(ESP32)
153 | #if defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2)
154 | WiFi.removeEvent(ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
155 | #else
156 | WiFi.removeEvent(SYSTEM_EVENT_STA_DISCONNECTED);
157 | #endif
158 | #endif
159 | #if defined(ESP8266)
160 | // not tested
161 | WiFi.onStationModeDisconnected(NULL);
162 | #endif
163 | WiFi.disconnect();
164 | }
165 |
166 | void wifi_setup(){
167 | /*
168 | WiFi will be startetd here. Won't wait until WiFi has connected.
169 | Event connected: Will only be logged, nothing else happens
170 | Event GotIP: From here on WiFi can be used. Only from here on IP address is available
171 | Event Disconnected: Will automatically try to reconnect here. If reconnection happens, first event connected will be fired, after this event gotIP fires
172 | */
173 | #if defined(ESP32)
174 | #if defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2)
175 | WiFi.onEvent(WiFiStationConnected, ARDUINO_EVENT_WIFI_STA_CONNECTED);
176 | WiFi.onEvent(WiFiGotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP);
177 | WiFi.onEvent(WiFiStationDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
178 | #else
179 | WiFi.onEvent(WiFiStationConnected, SYSTEM_EVENT_STA_CONNECTED);
180 | WiFi.onEvent(WiFiGotIP, SYSTEM_EVENT_STA_GOT_IP);
181 | WiFi.onEvent(WiFiStationDisconnected, SYSTEM_EVENT_STA_DISCONNECTED);
182 | #endif
183 | #endif
184 | #if defined(ESP8266)
185 | static WiFiEventHandler e1, e2, e3;
186 | e1 = WiFi.onStationModeConnected(onSTAConnected);
187 | e2 = WiFi.onStationModeDisconnected(onSTADisconnected);
188 | e3 = WiFi.onStationModeGotIP(onSTAGotIP);// As soon WiFi is connected, start NTP Client
189 | #endif
190 | WiFi.mode(WIFI_STA);
191 |
192 | wifi_disable();
193 | }
194 | #endif
--------------------------------------------------------------------------------
/src/wifiCommunication.h:
--------------------------------------------------------------------------------
1 | #ifdef useWIFI
2 | extern bool wifiIsDisabled;
3 | #if defined(WIFI_KNOWN_APS)
4 | extern String accessPointName;
5 | #endif
6 |
7 | void wifi_setup(void);
8 | void wifi_enable(void);
9 | void wifi_disable(void);
10 | #endif
--------------------------------------------------------------------------------