├── .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 | ![Build](https://github.com/KlausMu/esp32-fan-controller/actions/workflows/build-platformio.yml/badge.svg) 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 | ![Wiring diagram fan and BME280](https://github.com/KlausMu/esp32-fan-controller/wiki/images/fritzingESP32_BME280_fan.png) 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 | ![TempControllerModeArduiTouch](https://github.com/KlausMu/esp32-fan-controller/wiki/images/tempControllerModeArduiTouch.jpg) 83 | ### Images of ESP32 fan controller used in a 3D printer housing 84 | 85 | 86 | ![3DPrinter](https://github.com/KlausMu/esp32-fan-controller/wiki/images/3Dprinter.jpg) 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 --------------------------------------------------------------------------------