├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── main.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── data └── img │ ├── arduinobootstrapper.png │ └── jetbrains.png ├── examples ├── ChangeName.cpp └── ChangeName.h ├── library.json ├── library.properties ├── platformio.ini ├── secrets.ini └── src ├── BootstrapManager.cpp ├── BootstrapManager.h ├── Configuration.cpp ├── Configuration.h ├── EthManager.cpp ├── EthManager.h ├── Helpers.cpp ├── Helpers.h ├── PingESP.cpp ├── PingESP.h ├── QueueManager.cpp ├── QueueManager.h ├── Secrets.h ├── WifiManager.cpp └── WifiManager.h /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [sblantipodi] 2 | custom: ["https://www.paypal.com/donate?hosted_button_id=ZEJM8ZLQW5E4A"] 3 | 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: '3.11' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install platformio 20 | python --version 21 | - name: Install submodules 22 | run: | 23 | pwd 24 | git submodule update --init 25 | ls 26 | - name: Setup template config files 27 | run: | 28 | mkdir /home/runner/work/arduino_bootstrapper/arduino_bootstrapper/include 29 | cp /home/runner/work/arduino_bootstrapper/arduino_bootstrapper/examples/ChangeName.cpp /home/runner/work/arduino_bootstrapper/arduino_bootstrapper/src/ChangeName.cpp 30 | cp /home/runner/work/arduino_bootstrapper/arduino_bootstrapper/examples/ChangeName.h /home/runner/work/arduino_bootstrapper/arduino_bootstrapper/include/ChangeName.h 31 | # - name: Setup tmate session 32 | # uses: mxschmitt/action-tmate@v3 33 | - name: Static code analysis 34 | run: platformio check --verbose --severity=high --skip-packages 35 | - name: Run PlatformIO 36 | run: platformio run -e bootstrapper 37 | - name: Creating artifact from BIN file 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: firmware_build_artifact_arduino_bootstrapper.bin 41 | path: .pio/build/bootstrapper/firmware.bin -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | build: 11 | env: 12 | commitmsg: ${{ github.event.head_commit.message }} 13 | 14 | name: Create Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | - name: get commit message 20 | run: | 21 | echo $commitmsg 22 | - name: Create Release 23 | id: create_release 24 | uses: softprops/action-gh-release@v1 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | tag_name: ${{ github.ref }} 28 | name: Release ${{ github.ref_name }} 29 | body: | 30 | Changes in this Release 31 | ${{ env.commitmsg }} 32 | draft: false 33 | prerelease: false -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@main 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' 14 | days-before-stale: 30 15 | days-before-close: 5 16 | labels-to-add-when-unstale: 'keep' 17 | exempt-issue-labels: 'blocked,must,should,keep' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .pio 4 | src/ChangeName.cpp 5 | include/ChangeName.h 6 | cmake-build-* 7 | CMakeLists.txt 8 | CMakeListsPrivate.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Davide Perini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arduino Bootstrapper 2 | 3 | Utility classes for bootstrapping Arduino projects. 4 | In few minutes you will be able to read and write to storage (in JSON format), read and send messages to an MQTT queue, connect to WiFi without hardcoding passwords and more... 5 | `Arduino Bootstrapper` wants to be a starting point for **creating good projects without code duplications**. 6 | 7 | Espressif **`ESP8266/ESP32`** are the default platforms but you can easily add support for other platforms and boards. 8 | _Written for Arduino IDE and PlatformIO._ 9 | 10 | [![GitHub Actions CI](https://github.com/sblantipodi/arduino_bootstrapper/workflows/GitHub%20Actions%20CI/badge.svg)](https://github.com/sblantipodi/arduino_bootstrapper/actions) 11 | [![arduino-library-badge](https://www.ardu-badge.com/badge/Bootstrapper.svg?)](https://www.ardu-badge.com/Bootstrapper) 12 | [![GitHub version](https://img.shields.io/github/v/release/sblantipodi/arduino_bootstrapper.svg)](https://github.com/sblantipodi/arduino_bootstrapper/releases) 13 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 14 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-purple.svg)](https://GitHub.com/sblantipodi/arduino_bootstrapper/graphs/commit-activity) 15 | [![DPsoftware](https://img.shields.io/static/v1?label=DP&message=Software&color=orange)](https://www.dpsoftware.org) 16 | 17 | If you like **Arduino Bootstrapper**, give it a star, or fork it and contribute! 18 | 19 | [![GitHub stars](https://img.shields.io/github/stars/sblantipodi/arduino_bootstrapper.svg?style=social&label=Star)](https://github.com/sblantipodi/arduino_bootstrapper/stargazers) 20 | [![GitHub forks](https://img.shields.io/github/forks/sblantipodi/arduino_bootstrapper.svg?style=social&label=Fork)](https://github.com/sblantipodi/arduino_bootstrapper/network) 21 | 22 | ## Credits 23 | - Davide Perini 24 | 25 | ## How to get help if you are stuck? 26 | Open an issue here on Github, your questions could be useful to other users. 27 | 28 | ## How To 29 | There is two way to bootstrap your software using this utilities. 30 | 1) Bootstrap a project from scratch 31 | 2) Import those utilities to your existing project 32 | 33 | #### There are other projects that uses this utility, you can explore their sources here: 34 | [Smart Thermostat](https://github.com/sblantipodi/smart_thermostat), [Solar Station](https://github.com/sblantipodi/solar_station), [Glow Worm Luciferin](https://github.com/sblantipodi/glow_worm_luciferin), [Smart Watch Winder](https://github.com/sblantipodi/smart_watch_winder) 35 | 36 | ## 1) Bootstrap a project from scratch 37 | Clone the bootstrapper 38 | ``` 39 | git clone git@github.com:sblantipodi/arduino_bootstrapper.git 40 | ``` 41 | Simply edit those file as per description, _few minutes required_ 42 | 43 | . 44 | ├── ... 45 | ├── examples # Project src folder 46 | │ ├── ChangeName.cpp # Main file with loop() and setup() function, rename and copy it into your src folder 47 | │ ├── ChangeName.h # Main header file, rename and copy it into your include folder 48 | │ └── ... 49 | ├── src # Folder for core files, edit those files and contribute! 50 | │ ├── BootstrapManager.h # Core header file with utility classes for bootstrapping 51 | │ ├── QueueManager.h # Core header file with utility classes for Queue and MQTT management 52 | │ ├── WifiManager.h # Core header file with utility classes for Wifi and OTA upload management 53 | │ ├── Helpers.h # Core header file with helper classes 54 | │ └── ... 55 | ├── platformio.ini # Configure all the required info (ex: Wifi device name, DNS gateway, ecc.) 56 | ├── secrets.ini.template # Configure password and rename the file in secrets.ini 57 | └── ... 58 | 59 | ***NOTE:*** 60 | You should implement those functions that are passed by _*pointer_ to the main bootstrap functions: 61 | ```c++ 62 | class BootstrapManager { 63 | ... 64 | public: 65 | void bootstrapSetup( 66 | void (*manageDisconnectionFunction)(), 67 | void (*manageHardwareButton)(), 68 | void (*callback)(char*, byte*, unsigned int) 69 | ); 70 | void bootstrapLoop( 71 | void (*manageDisconnectionFunction)(), 72 | void (*manageQueueSubscription)(), 73 | void (*manageHardwareButton)() 74 | ); 75 | ... 76 | }; 77 | ``` 78 | 79 | - `manageDisconnections()` # _OPTIONAL_ put the logic you need in case your microcontroller is disconnected from the network 80 | - `manageHardwareButton()` # _OPTIONAL_ put special instruction for hardware button management during network disconnections 81 | - `manageQueueSubscription()` # subscribe to the desired mqtt topics 82 | - `callback()` # callback function called when a message arrives from the queue 83 | 84 | ## 2) Import those utilities to your existing project 85 | You can import `Arduino Bootstrapper` into your existing projects in two way: 86 | 1) Import via public registries (easyest way to import) 87 | ``` 88 | // For PlatformIO 89 | Add `lib_deps` to your `platformio.ini` 90 | lib_deps = ArduinoBootstrapper 91 | ``` 92 | ``` 93 | // For ArduinoIDE 94 | Simply import the Bootstrapper library from the library manager 95 | ``` 96 | 2) Import via git submodules (faster updates to latest releases) 97 | ``` 98 | git submodule add https://github.com/sblantipodi/arduino_bootstrapper.git arduino_bootstrapper 99 | ``` 100 | 101 | For both importing method you should then add extra dirs to your `platformio.ini` 102 | ```ini 103 | lib_extra_dirs = arduino_bootstrapper 104 | ``` 105 | Copy and configure `~/arduino_bootstrapper/secrets.ini.template` into `secrets.ini.template` 106 | 107 | Please include BootrapManager.h into your main header file: 108 | ```c++ 109 | #include "BootstrapManager.h" 110 | ``` 111 | and initialize the BootstrapManager class: 112 | ``` 113 | BootstrapManager bootstrapManager; 114 | ``` 115 | 116 | In your `setup()` function add the Wifi, MQTT and OTA bootstrapper 117 | ``` 118 | bootstrapManager.bootstrapSetup(manageDisconnections, manageHardwareButton, callback); 119 | ``` 120 | 121 | In your `loop()` function add the bootstrap manager function 122 | ``` 123 | bootstrapManager.bootstrapLoop(manageDisconnections, manageQueueSubscription, manageHardwareButton); 124 | ``` 125 | 126 | Please follow the `Bootstrap a project from scratch` instructions without the initial git clone part. 127 | 128 | #### Enable symlinks in GIT for Windows 129 | This project uses symlinks, Windows does not enable symlinks by default, to enable it, run this cmd from an admin console: 130 | ```bash 131 | export MSYS=winsymlinks:nativestrict 132 | ``` 133 | 134 | ## Continuous Integrations 135 | This project supports CI via GitHub Actions. 136 | In the `.github/workflows` folder there are two workflows 137 | - one for automatic release that is triggered when a git tag is pushed 138 | - one for building the project and creating the artifact (binary firmware) 139 | 140 | If you use this syntax: 141 | ``` 142 | git tag -a v1.0.0 -m "your commit msg"; 143 | git push origin --tags; 144 | ``` 145 | text in the commit message will be the description of your release. 146 | 147 | ## Access Point frontend 148 | Arduino Bootstrapper will search for a `secrets.ini`, if you don't configure it, access point is started. 149 | You can connect to the AP with your mobile and go to http://192.168.4.1 to access the gui 150 | that will let you enter all the passwords without the needs of hardcoding them. 151 | 152 |

153 | 154 |

155 | 156 | ## License 157 | This program is licensed under MIT License 158 | -------------------------------------------------------------------------------- /data/img/arduinobootstrapper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sblantipodi/arduino_bootstrapper/d1e16f3735be8de256d9829a7ad5bee4f7b76c13/data/img/arduinobootstrapper.png -------------------------------------------------------------------------------- /data/img/jetbrains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sblantipodi/arduino_bootstrapper/d1e16f3735be8de256d9829a7ad5bee4f7b76c13/data/img/jetbrains.png -------------------------------------------------------------------------------- /examples/ChangeName.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | ChangeName.cpp - Main Arduino File - Utility classes for bootstrapping arduino projects with Wifi management, 3 | OTA upload management, memory management, MQTT and queue management. 4 | 5 | Copyright © 2020 - 2024 Davide Perini 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | You should have received a copy of the MIT License along with this program. 18 | If not, see . 19 | */ 20 | 21 | #include "ChangeName.h" 22 | 23 | void setup() { 24 | 25 | Serial.begin(SERIAL_RATE); 26 | // Optional - setup MQTT last will & testament 27 | bootstrapManager.setMQTTWill("path/for/last/will/message","last will payload",lastWillQOS,lastWillRetain,cleanSession); 28 | bootstrapManager.bootstrapSetup(manageDisconnections, manageHardwareButton, callback); 29 | 30 | // ENTER YOUR CODE HERE 31 | 32 | } 33 | 34 | /********************************** MANAGE WIFI AND MQTT DISCONNECTION *****************************************/ 35 | void manageDisconnections() { 36 | 37 | } 38 | 39 | /********************************** MQTT SUBSCRIPTIONS *****************************************/ 40 | void manageQueueSubscription() { 41 | 42 | // example to topic subscription 43 | bootstrapManager.subscribe(CHANGE_ME_TOPIC); 44 | bootstrapManager.subscribe(CHANGE_ME_JSON_TOPIC); 45 | 46 | } 47 | 48 | /********************************** MANAGE HARDWARE BUTTON *****************************************/ 49 | void manageHardwareButton() { 50 | 51 | } 52 | 53 | /********************************** START CALLBACK *****************************************/ 54 | void callback(char* topic, byte* payload, unsigned int length) { 55 | 56 | // Transform all messages in a JSON format 57 | JsonDocument json = bootstrapManager.parseQueueMsg(topic, payload, length); 58 | 59 | if(strcmp(topic, CHANGE_ME_TOPIC) == 0) { 60 | String simpleMsg = json[VALUE]; 61 | // Serial.println(simpleMsg); 62 | } else if(strcmp(topic, CHANGE_ME_JSON_TOPIC) == 0) { 63 | String simpleMsg = bootstrapManager.jsonDoc[F("ROOT_EXAMPLE")]; 64 | // Serial.println(simpleMsg); 65 | } 66 | 67 | } 68 | 69 | /********************************** START MAIN LOOP *****************************************/ 70 | void loop() { 71 | 72 | if (isConfigFileOk) { 73 | 74 | // Bootsrap loop() with Wifi, MQTT and OTA functions 75 | bootstrapManager.bootstrapLoop(manageDisconnections, manageQueueSubscription, manageHardwareButton); 76 | 77 | // ENTER YOUR CODE HERE 78 | 79 | Serial.print("Hello World"); 80 | delay(1000); 81 | 82 | // Send MSG to the MQTT queue with no retention 83 | bootstrapManager.publish(CHANGE_ME_TOPIC, "SEND SIMPLE MSG TO THE QUEUE", false); 84 | 85 | // Send JSON MSG to the MQTT queue with no retention 86 | JsonObject root = bootstrapManager.getJsonObject(); 87 | root["ROOT_EXAMPLE"] = "SEND JSON MSG TO THE QUEUE"; 88 | bootstrapManager.publish(CHANGE_ME_JSON_TOPIC, root, false); 89 | 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /examples/ChangeName.h: -------------------------------------------------------------------------------- 1 | /* 2 | ChangeName.h - Main header 3 | 4 | Copyright © 2020 - 2024 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include 21 | #include 22 | #include "BootstrapManager.h" 23 | 24 | 25 | /****************** BOOTSTRAP MANAGER ******************/ 26 | BootstrapManager bootstrapManager; 27 | 28 | 29 | /**************************** PIN DEFINITIONS **************************************************/ 30 | // Define your pin here 31 | 32 | 33 | /************* MQTT TOPICS **************************/ 34 | const char* CHANGE_ME_TOPIC = "tele/changeme/CHANGEME"; 35 | const char* CHANGE_ME_JSON_TOPIC = "tele/changeme/CHANGEME_JSON"; 36 | static int lastWillQOS = 1; 37 | boolean lastWillRetain = false; 38 | boolean cleanSession = false; 39 | 40 | 41 | /********************************** FUNCTION DECLARATION (NEEDED BY PLATFORMIO WHILE COMPILING CPP FILES) *****************************************/ 42 | void callback(char* topic, byte* payload, unsigned int length); 43 | void manageDisconnections(); 44 | void manageQueueSubscription(); 45 | void manageHardwareButton(); 46 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ArduinoBootstrapper", 3 | "keywords": "bootstrapper, mqtt, wifi, iot", 4 | "description": "Utility classes for bootstrapping arduino projects with Wifi management, OTA upload management, memory management, MQTT and queue management. (ESP8266/ESP32 ready)", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sblantipodi/arduino_bootstrapper.git" 8 | }, 9 | "version": "1.18.3", 10 | "examples": "examples/*.cpp", 11 | "exclude": "tests", 12 | "frameworks": "arduino", 13 | "platforms": [ 14 | "espressif8266", 15 | "espressif32" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=Bootstrapper 2 | version=1.18.3 3 | author=Davide Perini 4 | maintainer=Davide Perini 5 | sentence=A client library for MQTT messaging. 6 | paragraph=Utility classes for bootstrapping arduino projects with Wifi management, OTA upload management, memory management, MQTT and queue management. (ESP8266/ESP32 ready) 7 | category=Other 8 | url=https://github.com/sblantipodi/arduino_bootstrapper 9 | architectures=* 10 | depends=ArduinoJson, PubSubClient, Adafruit SSD1306 -------------------------------------------------------------------------------- /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 | [platformio] 12 | extra_configs = secrets.ini 13 | 14 | [env:bootstrapper] 15 | platform = espressif8266@4.2.1 ; for ESP32 use: espressif32 16 | ;platform_packages = platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#0e5d358c3c15cff4b12fd89d9e605ff9fa0709a6 17 | board = d1_mini ; for ESP32 Lolin D32 board use: lolin_d32 18 | framework = arduino 19 | 20 | build_flags = 21 | '-D AUTHOR="DPsoftware"' 22 | '-D SERIAL_RATE=115200' 23 | '-D DEBUG_QUEUE_MSG=true' 24 | '-D DISPLAY_ENABLED=false' 25 | '-D WIFI_DEVICE_NAME="ArduinoBootstrapper"' 26 | '-D MICROCONTROLLER_OTA_PORT=8199' 27 | '-D WIFI_SIGNAL_STRENGTH=0' 28 | '-D GATEWAY_IP="192.168.1.1"' 29 | '-D SUBNET_IP="192.168.1.1"' 30 | '-D MICROCONTROLLER_IP="192.168.1.99"' 31 | '-D MQTT_SERVER_IP="192.168.1.3"' 32 | '-D MQTT_SERVER_PORT="1883"' 33 | '-D MAX_RECONNECT=500' 34 | '-D MAX_JSON_OBJECT_SIZE=50' 35 | '-D MQTT_MAX_PACKET_SIZE=1024' 36 | '-D WIFI_SSID="${secrets.wifi_ssid}"' 37 | '-D WIFI_PWD="${secrets.wifi_password}"' 38 | '-D MQTT_USER="${secrets.mqtt_username}"' 39 | '-D MQTT_PWD="${secrets.mqtt_password}"' 40 | '-D OTA_PWD="${secrets.ota_password}"' 41 | '-D ADDITIONAL_PARAM_TEXT="ADDITIONAL PARAM TEXT"' 42 | 43 | monitor_port = COM4 44 | monitor_speed = 115200 45 | monitor_filters = esp8266_exception_decoder ; for ESP32 use: esp32_exception_decoder 46 | board_build.f_cpu = 80000000L ; for ESP32 use: 240000000L, ESP8266 can run @160000000L if you need it 47 | ; upload_protocol = espota 48 | ; upload_port = 192.168.1.99 49 | ; upload_flags = 50 | ; --port=8268 51 | ; --auth=${secrets.ota_password} ; configure secrets.ini for OTA Upload, first upload using COM port is needed 52 | upload_port = COM4 53 | lib_deps = 54 | bblanchon/ArduinoJson 55 | knolleary/PubSubClient 56 | -------------------------------------------------------------------------------- /secrets.ini: -------------------------------------------------------------------------------- 1 | [secrets] 2 | wifi_ssid=XXX 3 | wifi_password=XXX 4 | mqtt_username=XXX 5 | mqtt_password=XXX 6 | ota_password=XXX -------------------------------------------------------------------------------- /src/BootstrapManager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | BoostrapManager.cpp - Main file for bootstrapping arduino projects 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include "BootstrapManager.h" 21 | 22 | void BootstrapManager::littleFsInit() { 23 | #if defined(ESP8266) 24 | if (!LittleFS.begin()) { 25 | #elif defined(ARDUINO_ARCH_ESP32) 26 | if (!LittleFS.begin(true)) { 27 | #endif 28 | Serial.println("LittleFS mount failed"); 29 | return; 30 | } 31 | } 32 | 33 | /********************************** BOOTSTRAP FUNCTIONS FOR SETUP() *****************************************/ 34 | void BootstrapManager::bootstrapSetup(void (*manageDisconnections)(), void (*manageHardwareButton)(), 35 | void (*callback)(char *, byte *, unsigned int)) { 36 | littleFsInit(); 37 | if (isWifiConfigured() && !forceWebServer) { 38 | isConfigFileOk = true; 39 | // Initialize Wifi manager 40 | wifiManager.setupWiFi(manageDisconnections, manageHardwareButton); 41 | // Initialize Queue Manager 42 | if (mqttIP.length() > 0) { 43 | QueueManager::setupMQTTQueue(callback); 44 | } else { 45 | Serial.println(F("Skip MQTT connection.")); 46 | } 47 | // Initialize OTA manager 48 | WifiManager::setupOTAUpload(); 49 | } else { 50 | isConfigFileOk = false; 51 | launchWebServerForOTAConfig(); 52 | } 53 | #if defined(ARDUINO_ARCH_ESP32) 54 | esp_task_wdt_init(3000, true); //enable panic so ESP32 restarts 55 | esp_task_wdt_add(NULL); //add current thread to WDT watch 56 | #endif 57 | #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 58 | Serial.setTxTimeoutMs(0); 59 | #endif 60 | } 61 | 62 | #if defined(ARDUINO_ARCH_ESP32) 63 | void eth_event(WiFiEvent_t event) { 64 | switch (event) { 65 | case ARDUINO_EVENT_ETH_START: 66 | ethConnected = true; 67 | break; 68 | case ARDUINO_EVENT_ETH_CONNECTED: 69 | MAC = WiFi.macAddress(); 70 | ethConnected = true; 71 | break; 72 | case ARDUINO_EVENT_ETH_GOT_IP: 73 | MAC = WiFi.macAddress(); 74 | microcontrollerIP = ETH.localIP().toString(); 75 | ethConnected = true; 76 | break; 77 | case ARDUINO_EVENT_ETH_DISCONNECTED: 78 | ethConnected = false; 79 | break; 80 | case ARDUINO_EVENT_ETH_STOP: 81 | ethConnected = false; 82 | break; 83 | default: 84 | break; 85 | } 86 | } 87 | #endif 88 | 89 | /********************************** BOOTSTRAP FUNCTIONS FOR SETUP() *****************************************/ 90 | void BootstrapManager::bootstrapSetup(void (*manageDisconnections)(), void (*manageHardwareButton)(), 91 | void (*callback)(char *, byte *, unsigned int), bool waitImprov, 92 | void (*listener)()) { 93 | littleFsInit(); 94 | if (isWifiConfigured() && !forceWebServer && (ethd == 0 || ethd == -1)) { 95 | isConfigFileOk = true; 96 | // Initialize Wifi manager 97 | wifiManager.setupWiFi(manageDisconnections, manageHardwareButton); 98 | initMqttOta(callback); 99 | } else if ((ethd == 0 || ethd == -1)){ 100 | isConfigFileOk = false; 101 | launchWebServerCustom(waitImprov, listener); 102 | } else { 103 | #if defined(ARDUINO_ARCH_ESP32) 104 | isConfigFileOk = true; 105 | ETH.setHostname(Helpers::string2char(deviceName)); 106 | WiFi.onEvent(eth_event); 107 | EthManager::connectToEthernet(ethd); 108 | Serial.println(F("Ethernet connected.")); 109 | initMqttOta(callback); 110 | #endif 111 | } 112 | #if defined(ARDUINO_ARCH_ESP32) 113 | esp_task_wdt_init(3000, true); //enable panic so ESP32 restarts 114 | esp_task_wdt_add(NULL); //add current thread to WDT watch 115 | #endif 116 | #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 117 | Serial.setTxTimeoutMs(0); 118 | #endif 119 | } 120 | 121 | void BootstrapManager::initMqttOta(void (*callback)(char *, byte *, unsigned int)) { 122 | // Initialize Queue Manager 123 | if (mqttIP.length() > 0) { 124 | QueueManager::setupMQTTQueue(callback); 125 | } else { 126 | Serial.println(F("Skip MQTT connection.")); 127 | } 128 | // Initialize OTA manager 129 | WifiManager::setupOTAUpload(); 130 | } 131 | 132 | /********************************** BOOTSTRAP FUNCTIONS FOR LOOP() *****************************************/ 133 | bool rcpResponseSent = false; 134 | void BootstrapManager::bootstrapLoop(void (*manageDisconnections)(), void (*manageQueueSubscription)(), void (*manageHardwareButton)()) { 135 | #if defined(ARDUINO_ARCH_ESP32) 136 | if (millis() - lastMillisForWdt >= 3000) { 137 | lastMillisForWdt = millis(); 138 | esp_task_wdt_reset(); 139 | } 140 | #endif 141 | #if (IMPROV_ENABLED > 0) 142 | if ((ethd == 0 || ethd == -1)) { 143 | if (!rcpResponseSent && WifiManager::isConnected()) { 144 | rcpResponseSent = true; 145 | wifiManager.sendImprovRPCResponse(0x01, true); 146 | } 147 | if (!temporaryDisableImprove) { 148 | wifiManager.handleImprovPacket(); 149 | } 150 | } 151 | #endif 152 | if (ethd == 0 || ethd == -1) { 153 | wifiManager.reconnectToWiFi(manageDisconnections, manageHardwareButton); 154 | } 155 | ArduinoOTA.handle(); 156 | if (mqttIP.length() > 0) { 157 | queueManager.queueLoop(manageDisconnections, manageQueueSubscription, manageHardwareButton); 158 | } 159 | } 160 | 161 | /********************************** SET LAST WILL PARAMETERS IN THE Q MANAGER **********************************/ 162 | void BootstrapManager::setMQTTWill(const char *topic, const char *payload, const int qos, boolean retain, 163 | boolean cleanSession) { 164 | QueueManager::setMQTTWill(topic, payload, qos, retain, cleanSession); 165 | } 166 | 167 | /********************************** SEND A SIMPLE MESSAGE ON THE QUEUE **********************************/ 168 | void BootstrapManager::publish(const char *topic, const char *payload, boolean retained) { 169 | if (DEBUG_QUEUE_MSG) { 170 | Serial.print(F("QUEUE MSG SENT [")); 171 | Serial.print(topic); 172 | Serial.println(F("] ")); 173 | Serial.println(payload); 174 | } 175 | QueueManager::publish(topic, payload, retained); 176 | } 177 | 178 | /********************************** SEND A JSON MESSAGE ON THE QUEUE **********************************/ 179 | void BootstrapManager::publish(const char *topic, JsonObject objectToSend, boolean retained) { 180 | char buffer[measureJson(objectToSend) + 1]; 181 | serializeJson(objectToSend, buffer, sizeof(buffer)); 182 | QueueManager::publish(topic, buffer, retained); 183 | if (DEBUG_QUEUE_MSG) { 184 | Serial.print(F("QUEUE MSG SENT [")); 185 | Serial.print(topic); 186 | Serial.println(F("] ")); 187 | serializeJsonPretty(objectToSend, Serial); 188 | Serial.println(); 189 | } 190 | } 191 | 192 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 193 | void BootstrapManager::unsubscribe(const char *topic) { 194 | QueueManager::unsubscribe(topic); 195 | if (DEBUG_QUEUE_MSG) { 196 | Serial.print(F("TOPIC SUBSCRIBED [")); 197 | Serial.print(topic); 198 | Serial.println(F("] ")); 199 | } 200 | } 201 | 202 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 203 | void BootstrapManager::subscribe(const char *topic) { 204 | QueueManager::subscribe(topic); 205 | if (DEBUG_QUEUE_MSG) { 206 | Serial.print(F("TOPIC SUBSCRIBED [")); 207 | Serial.print(topic); 208 | Serial.println(F("] ")); 209 | } 210 | } 211 | 212 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 213 | void BootstrapManager::subscribe(const char *topic, uint8_t qos) { 214 | QueueManager::subscribe(topic, qos); 215 | if (DEBUG_QUEUE_MSG) { 216 | Serial.print(F("TOPIC SUBSCRIBED [")); 217 | Serial.print(topic); 218 | Serial.println(F("] ")); 219 | } 220 | } 221 | 222 | /********************************** PRINT THE MESSAGE ARRIVING FROM THE QUEUE **********************************/ 223 | JsonDocument BootstrapManager::parseQueueMsg(char *topic, byte *payload, unsigned int length) { 224 | if (DEBUG_QUEUE_MSG) { 225 | Serial.print(F("QUEUE MSG ARRIVED [")); 226 | Serial.print(topic); 227 | Serial.println(F("] ")); 228 | } 229 | char message[length + 1]; 230 | for (unsigned int i = 0; i < length; i++) { 231 | message[i] = (char) payload[i]; 232 | } 233 | message[length] = '\0'; 234 | DeserializationError error = deserializeJson(jsonDoc, (const byte *) payload, length); 235 | // non json msg 236 | if (error) { 237 | JsonObject root = jsonDoc.to(); 238 | root[VALUE] = message; 239 | if (DEBUG_QUEUE_MSG) { 240 | String msg = root[VALUE]; 241 | Serial.println(msg); 242 | } 243 | return jsonDoc; 244 | } else { // return json doc 245 | if (DEBUG_QUEUE_MSG) { 246 | serializeJsonPretty(jsonDoc, Serial); 247 | Serial.println(); 248 | } 249 | return jsonDoc; 250 | } 251 | } 252 | 253 | /********************************** PRINT THE MESSAGE ARRIVING FROM HTTP **********************************/ 254 | JsonDocument BootstrapManager::parseHttpMsg(String payload, unsigned int length) { 255 | char message[length + 1]; 256 | for (unsigned int i = 0; i < length; i++) { 257 | message[i] = (char) payload[i]; 258 | } 259 | message[length] = '\0'; 260 | DeserializationError error = deserializeJson(jsonDoc, payload.c_str(), length); 261 | // non json msg 262 | if (error) { 263 | JsonObject root = jsonDoc.to(); 264 | root[VALUE] = message; 265 | if (DEBUG_QUEUE_MSG) { 266 | String msg = root[VALUE]; 267 | Serial.println(msg); 268 | } 269 | return jsonDoc; 270 | } else { // return json doc 271 | if (DEBUG_QUEUE_MSG) { 272 | serializeJsonPretty(jsonDoc, Serial); 273 | Serial.println(); 274 | } 275 | return jsonDoc; 276 | } 277 | } 278 | 279 | // return a new json object instance 280 | JsonObject BootstrapManager::getJsonObject() { 281 | return jsonDoc.to(); 282 | } 283 | 284 | // Blink LED_BUILTIN without bloking delay 285 | [[maybe_unused]] void BootstrapManager::nonBlokingBlink() { 286 | unsigned long currentMillis = millis(); 287 | if (currentMillis - previousMillis >= interval && ledTriggered) { 288 | // save the last time you blinked the LED 289 | previousMillis = currentMillis; 290 | // blink led 291 | #if defined(LED_BUILTIN) 292 | digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); 293 | #endif 294 | blinkCounter++; 295 | if (blinkCounter >= blinkTimes) { 296 | blinkCounter = 0; 297 | ledTriggered = false; 298 | #if defined(LED_BUILTIN) 299 | digitalWrite(LED_BUILTIN, HIGH); 300 | #endif 301 | } 302 | } 303 | } 304 | 305 | // Print or display microcontroller infos 306 | void BootstrapManager::getMicrocontrollerInfo() { 307 | Helpers::smartPrint(F("Wifi: ")); 308 | Helpers::smartPrint(WifiManager::getQuality()); 309 | Helpers::smartPrintln(F("%")); 310 | #if defined(ESP8266) 311 | Helpers::smartPrint(F("Heap: ")); Helpers::smartPrint(EspClass::getFreeHeap()/1024); Helpers::smartPrintln(F(" KB")); 312 | Helpers::smartPrint(F("Free Flash: ")); Helpers::smartPrint(EspClass::getFreeSketchSpace()/1024); Helpers::smartPrintln(F(" KB")); 313 | Helpers::smartPrint(F("Frequency: ")); Helpers::smartPrint(EspClass::getCpuFreqMHz()); Helpers::smartPrintln(F("MHz")); 314 | Helpers::smartPrint(F("Flash: ")); Helpers::smartPrint(EspClass::getFlashChipSize()/1024); Helpers::smartPrintln(F(" KB")); 315 | Helpers::smartPrint(F("Sketch: ")); Helpers::smartPrint(EspClass::getSketchSize()/1024); Helpers::smartPrintln(F(" KB")); 316 | Helpers::smartPrint(F("SDK: ")); Helpers::smartPrintln(EspClass::getSdkVersion()); 317 | #elif defined(ARDUINO_ARCH_ESP32) 318 | Helpers::smartPrint(F("Heap: ")); 319 | Helpers::smartPrint(ESP.getFreeHeap() / 1024); 320 | Helpers::smartPrintln(F(" KB")); 321 | Helpers::smartPrint(F("Free Flash: ")); 322 | Helpers::smartPrint(ESP.getFreeSketchSpace() / 1024); 323 | Helpers::smartPrintln(F(" KB")); 324 | Helpers::smartPrint(F("Frequency: ")); 325 | Helpers::smartPrint(ESP.getCpuFreqMHz()); 326 | Helpers::smartPrintln(F("MHz")); 327 | Helpers::smartPrint(F("Flash: ")); 328 | Helpers::smartPrint(ESP.getFlashChipSize() / 1024); 329 | Helpers::smartPrintln(F(" KB")); 330 | Helpers::smartPrint(F("Sketch: ")); 331 | Helpers::smartPrint(ESP.getSketchSize() / 1024); 332 | Helpers::smartPrintln(F(" KB")); 333 | Helpers::smartPrint(F("SDK: ")); 334 | Helpers::smartPrintln(ESP.getSdkVersion()); 335 | #endif 336 | Helpers::smartPrintln(F("MAC: ")); 337 | if ((ethd == 0 || ethd == -1)) { 338 | Helpers::smartPrintln(WiFi.macAddress()); 339 | } else { 340 | #if defined(ARDUINO_ARCH_ESP32) 341 | Helpers::smartPrintln(ETH.macAddress()); 342 | #endif 343 | } 344 | Helpers::smartPrint(F("IP: ")); 345 | Helpers::smartPrintln(microcontrollerIP); 346 | // helper.smartPrint(F("Arduino Core: ")); helper.smartPrintln(ESP.getCoreVersion()); 347 | Helpers::smartPrintln(F("Last Boot: ")); 348 | Helpers::smartPrintln(lastBoot); 349 | Helpers::smartPrintln(F("Last WiFi connection:")); 350 | Helpers::smartPrintln(lastWIFiConnection); 351 | Helpers::smartPrintln(F("Last MQTT connection:")); 352 | Helpers::smartPrintln(lastMQTTConnection); 353 | } 354 | 355 | // Draw screensaver useful for OLED displays 356 | [[maybe_unused]] void BootstrapManager::drawScreenSaver(const String &txt) { 357 | #if (DISPLAY_ENABLED) 358 | if (screenSaverTriggered) { 359 | display.clearDisplay(); 360 | for (int i = 0; i < 50; i++) { 361 | display.clearDisplay(); 362 | display.setTextSize(2); 363 | display.setCursor(5,17); 364 | display.fillRect(0, 0, display.width(), display.height(), i%2 != 0 ? WHITE : BLACK); 365 | display.setTextColor(i%2 == 0 ? WHITE : BLACK); 366 | display.drawRoundRect(0, 0, display.width()-1, display.height()-1, display.height()/4, i%2 == 0 ? WHITE : BLACK); 367 | display.println(txt); 368 | display.display(); 369 | } 370 | display.setTextColor(WHITE); 371 | screenSaverTriggered = false; 372 | } 373 | #endif 374 | } 375 | 376 | // draw some infos about your controller 377 | [[maybe_unused]] void BootstrapManager::drawInfoPage(const String &softwareVersion, const String &author) { 378 | #if (DISPLAY_ENABLED) 379 | yoffset -= 1; 380 | // add/remove 8 pixel for every line yoffset <= -209, if you want to add a line yoffset <= -217 381 | if (yoffset <= -209) { 382 | yoffset = SCREEN_HEIGHT + 6; 383 | lastPageScrollTriggered = true; 384 | } 385 | int effectiveOffset = (yoffset >= 0 && !lastPageScrollTriggered) ? 0 : yoffset; 386 | 387 | if (haVersion[0] != '\0') { 388 | display.drawBitmap((display.width()-HABIGLOGOW)-1, effectiveOffset+5, HABIGLOGO, HABIGLOGOW, HABIGLOGOH, 1); 389 | } 390 | display.setCursor(0, effectiveOffset); 391 | display.setTextSize(1); 392 | display.print(deviceName); display.print(F(" ")); 393 | display.println(softwareVersion); 394 | display.println("by " + author); 395 | display.println(F("")); 396 | 397 | if (haVersion[0] != '\0') { 398 | display.print(F("HA: ")); display.print(F("(")); display.print(haVersion); display.println(F(")")); 399 | } 400 | 401 | getMicrocontrollerInfo(); 402 | 403 | // add/remove 8 pixel for every line effectiveOffset+175, if you want to add a line effectiveOffset+183 404 | display.drawBitmap((((display.width()/2)-(ARDUINOLOGOW/2))), effectiveOffset+175, ARDUINOLOGO, ARDUINOLOGOW, ARDUINOLOGOH, 1); 405 | #endif 406 | } 407 | 408 | // send the state of your controller to the mqtt queue 409 | [[maybe_unused]] void BootstrapManager::sendState(const char *topic, JsonObject objectToSend, const String &version) { 410 | objectToSend["Whoami"] = deviceName; 411 | objectToSend["IP"] = microcontrollerIP; 412 | objectToSend["MAC"] = MAC; 413 | objectToSend["ver"] = version; 414 | objectToSend["time"] = timedate; 415 | objectToSend["wifi"] = WifiManager::getQuality(); 416 | 417 | // publish state only if it has received time from HA 418 | if (timedate != OFF_CMD) { 419 | // This topic should be retained, we don't want unknown values on battery voltage or wifi signal 420 | publish(topic, objectToSend, true); 421 | } 422 | } 423 | 424 | // write json file to storage 425 | void BootstrapManager::writeToLittleFS(const JsonDocument &jDoc, const String &filenameToUse) { 426 | File jsonFile = LittleFS.open("/" + filenameToUse, FILE_WRITE); 427 | if (!jsonFile) { 428 | Helpers::smartPrintln("Failed to open [" + filenameToUse + "] file for writing"); 429 | } else { 430 | serializeJsonPretty(jDoc, Serial); 431 | serializeJson(jDoc, jsonFile); 432 | jsonFile.close(); 433 | Helpers::smartPrintln("[" + filenameToUse + "] written correctly"); 434 | } 435 | } 436 | 437 | // read json file from storage 438 | JsonDocument BootstrapManager::readLittleFS(const String &filenameToUse) { 439 | // Helpers classes 440 | Helpers helper; 441 | #if (DISPLAY_ENABLED) 442 | display.clearDisplay(); 443 | display.setCursor(0, 0); 444 | display.setTextSize(1); 445 | #endif 446 | Helpers::smartPrintln(F("Mounting LittleFS...")); 447 | helper.smartDisplay(); 448 | File jsonFile = LittleFS.open("/" + filenameToUse, FILE_READ); 449 | if (!jsonFile) { 450 | Helpers::smartPrintln("Failed to open [" + filenameToUse + "] file"); 451 | helper.smartDisplay(); 452 | } 453 | size_t size = jsonFile.size(); 454 | // Allocate a buffer to store contents of the file. 455 | std::unique_ptr buf(new char[size]); 456 | // We don't use String here because ArduinoJson library requires the input 457 | // buffer to be mutable. If you don't use ArduinoJson, you may as well 458 | // use configFile.readString instead. 459 | jsonFile.readBytes(buf.get(), size); 460 | JsonDocument jsonDoc; 461 | auto error = deserializeJson(jsonDoc, buf.get()); 462 | if (filenameToUse != "setup.json") serializeJsonPretty(jsonDoc, Serial); 463 | jsonFile.close(); 464 | if (error) { 465 | Helpers::smartPrintln("Failed to parse [" + filenameToUse + "] file"); 466 | helper.smartDisplay(DELAY_2000); 467 | } else { 468 | Helpers::smartPrintln("[" + filenameToUse + "]\nJSON parsed"); 469 | helper.smartDisplay(DELAY_2000); 470 | return jsonDoc; 471 | } 472 | helper.smartDisplay(DELAY_2000); 473 | return jsonDoc; 474 | } 475 | 476 | String BootstrapManager::readValueFromFile(const String &filenameToUse, const String ¶mName) { 477 | String returnStr = ""; 478 | if (!LittleFS.begin()) { 479 | Serial.println("LittleFS mount failed"); 480 | } 481 | File jsonFile = LittleFS.open("/" + filenameToUse, FILE_READ); 482 | if (!jsonFile) { 483 | Helpers::smartPrintln("Failed to open [" + filenameToUse + "] file"); 484 | helper.smartDisplay(); 485 | } 486 | size_t size = jsonFile.size(); 487 | std::unique_ptr buf(new char[size]); 488 | jsonFile.readBytes(buf.get(), size); 489 | JsonDocument jDoc; 490 | auto error = deserializeJson(jDoc, buf.get()); 491 | serializeJsonPretty(jDoc, Serial); 492 | JsonVariant answer = jDoc[paramName]; 493 | if (answer.is()) { 494 | returnStr = answer.as(); 495 | } else { 496 | auto returnVal = answer.as(); 497 | returnStr = String(returnVal); 498 | } 499 | jsonFile.close(); 500 | if (error) { 501 | returnStr = ""; 502 | } 503 | return returnStr; 504 | } 505 | 506 | // check if wifi is correctly configured 507 | bool BootstrapManager::isWifiConfigured() { 508 | if (WifiManager::isWifiConfigured()) { 509 | deviceName = DEVICE_NAME; 510 | microcontrollerIP = IP_MICROCONTROLLER; 511 | qsid = SSID; 512 | qpass = PASSWORD; 513 | OTApass = OTAPASSWORD; 514 | mqttIP = MQTT_SERVER; 515 | mqttPort = MQTT_PORT; 516 | mqttuser = MQTT_USERNAME; 517 | mqttpass = MQTT_PASSWORD; 518 | additionalParam = PARAM_ADDITIONAL; 519 | return true; 520 | } else { 521 | JsonDocument mydoc = readLittleFS(F("setup.json")); 522 | if (mydoc[F("qsid")].is()) { 523 | Serial.println(F("Storage OK, restoring WiFi and MQTT config.")); 524 | microcontrollerIP = Helpers::getValue(mydoc["microcontrollerIP"]); 525 | qsid = Helpers::getValue(mydoc[F("qsid")]); 526 | qpass = Helpers::getValue(mydoc[F("qpass")]); 527 | OTApass = Helpers::getValue(mydoc[F("OTApass")]); 528 | if (OTApass.isEmpty()) { 529 | OTApass = OTAPASSWORD; 530 | } 531 | mqttIP = Helpers::getValue(mydoc[F("mqttIP")]); 532 | mqttPort = Helpers::getValue(mydoc[F("mqttPort")]); 533 | mqttuser = Helpers::getValue(mydoc[F("mqttuser")]); 534 | mqttpass = Helpers::getValue(mydoc[F("mqttpass")]); 535 | additionalParam = Helpers::getValue(mydoc[F("additionalParam")]); 536 | deviceName = Helpers::getValue(mydoc[F("deviceName")]); 537 | ethd = mydoc[F("ethd")].as(); 538 | #if defined(ESP8266) 539 | ethd = -1; 540 | #endif 541 | return true; 542 | } else { 543 | Serial.println(F("No setup file")); 544 | } 545 | } 546 | return false; 547 | } 548 | 549 | // if no ssid available, launch web server to get config params via browser 550 | void BootstrapManager::launchWebServerForOTAConfig() { 551 | #if (IMPROV_ENABLED > 0) 552 | manageImprov(); 553 | #endif 554 | return WifiManager::launchWebServerForOTAConfig(); 555 | } 556 | 557 | void BootstrapManager::manageImprov() { 558 | unsigned long timeNowStatus = 0; 559 | bool switchToWebServer = false; 560 | // If WiFi is not configured, handle improv packet for 15 seconds, then switch to settinigs managed by web server 561 | WiFi.disconnect(); 562 | while (((WiFi.localIP()[0] == 0 && WiFi.status() != WL_CONNECTED) && !switchToWebServer) || improvePacketReceived) { 563 | if (millis() > timeNowStatus + IMPROV_ENABLED) { 564 | timeNowStatus = millis(); 565 | switchToWebServer = true; 566 | } 567 | wifiManager.manageImprovWifi(); 568 | } 569 | } 570 | 571 | void BootstrapManager::launchWebServerCustom(bool waitImprov, void (*listener)()) { 572 | #if (IMPROV_ENABLED > 0) 573 | if (waitImprov) { 574 | manageImprov(); 575 | } 576 | #endif 577 | return WifiManager::launchWebServerCustom(listener); 578 | } 579 | 580 | // get the wifi quality 581 | int BootstrapManager::getWifiQuality() { 582 | return WifiManager::getQuality(); 583 | } -------------------------------------------------------------------------------- /src/BootstrapManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | BoostrapManager.cpp - Main header for bootstrapping arduino projects 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_BOOTSTRAP_MANAGER_H 21 | #define _DPSOFTWARE_BOOTSTRAP_MANAGER_H 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "Configuration.h" 30 | #include "Helpers.h" 31 | #include "WifiManager.h" 32 | #include "QueueManager.h" 33 | #if defined(ARDUINO_ARCH_ESP32) 34 | #include "EthManager.h" 35 | #include 36 | #endif 37 | 38 | class BootstrapManager { 39 | 40 | private: 41 | WifiManager wifiManager; // WifiManager classes for Wifi management 42 | QueueManager queueManager; // QueueManager classes for MQTT queue management 43 | Helpers helper; 44 | #if defined(ARDUINO_ARCH_ESP32) 45 | unsigned long lastMillisForWdt = millis(); 46 | #endif 47 | 48 | public: 49 | JsonDocument jsonDoc; 50 | JsonDocument jsonDocBigSize; 51 | // using JsonDocument = StaticJsonDocument; 52 | JsonDocument parseQueueMsg(char* topic, byte* payload, unsigned int length); // print the message arriving from the queue 53 | JsonDocument parseHttpMsg(String payload, unsigned int length); // print the message arriving from HTTP 54 | void littleFsInit(); 55 | void bootstrapSetup(void (*manageDisconnectionFunction)(), void (*manageHardwareButton)(), void (*callback)(char*, byte*, unsigned int)); // bootstrap setup() 56 | void bootstrapSetup(void (*manageDisconnectionFunction)(), void (*manageHardwareButton)(), void (*callback)(char*, byte*, unsigned int), bool waitImprov, void (*listener)()); // bootstrap setup() 57 | void bootstrapLoop(void (*manageDisconnectionFunction)(), void (*manageQueueSubscription)(), void (*manageHardwareButton)()); // bootstrap loop() 58 | static void setMQTTWill(const char *topic, const char *payload, int qos, boolean retain, boolean cleanSession); // set the last will parameters for mqtt 59 | static void publish(const char *topic, const char *payload, boolean retained); // send a message on the queue 60 | static void publish(const char *topic, JsonObject objectToSend, boolean retained); // send a message on the queue 61 | static void unsubscribe(const char *topic); // unsubscribe to a queue topic 62 | static void subscribe(const char *topic); // subscribe to a queue topic 63 | static void subscribe(const char *topic, uint8_t qos); // subscribe to a queue topic with qos 0 or 1 64 | JsonObject getJsonObject(); // return a new json object instance 65 | [[maybe_unused]] static void nonBlokingBlink(); // blink default LED when sending data to the queue 66 | [[maybe_unused]] static void getMicrocontrollerInfo(); // print or display microcontroller's info 67 | [[maybe_unused]] void drawInfoPage(const String& softwareVersion, const String& author); // draw a page with all the microcontroller's info 68 | [[maybe_unused]] void drawScreenSaver(const String& txt); // useful for OLED displays 69 | [[maybe_unused]] static void sendState(const char *topic, JsonObject objectToSend, const String& version); // send microcontroller's info on the queue 70 | [[maybe_unused]] static void writeToLittleFS(const JsonDocument& jDoc, const String& filenameToUse); // write json file to storage 71 | [[maybe_unused]] static JsonDocument readLittleFS(const String& filenameToUse); // read json file from storage 72 | [[maybe_unused]] String readValueFromFile(const String& filenameToUse, const String& paramName); // read a param from a json file 73 | static bool isWifiConfigured(); // check if wifi is correctly configured 74 | void launchWebServerForOTAConfig(); // if no ssid available, launch web server to get config params via browser 75 | void launchWebServerCustom(bool waitImprov, void (*listener)()); // if no ssid available, launch web server to get config params via browser 76 | static int getWifiQuality(); // get the wifi quality 77 | void manageImprov(); 78 | static void initMqttOta(void (*callback)(char *, byte *, unsigned int)) ; 79 | }; 80 | 81 | #endif -------------------------------------------------------------------------------- /src/Configuration.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Configuration.cpp - Configuration impl 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #if (DISPLAY_ENABLED) 21 | #include "Configuration.h" 22 | // Initialize the display 23 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 24 | #endif -------------------------------------------------------------------------------- /src/Configuration.h: -------------------------------------------------------------------------------- 1 | /* 2 | Configuration.h - Config header 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_CONFIG_H 21 | #define _DPSOFTWARE_CONFIG_H 22 | 23 | #if defined(ESP8266) 24 | #include 25 | #include 26 | #include 27 | #include 28 | #elif defined(ARDUINO_ARCH_ESP32) 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #endif 35 | 36 | #ifndef AUTHOR 37 | #define AUTHOR "DPsoftware" 38 | #endif 39 | 40 | // Serial rate for debug 41 | #ifndef SERIAL_RATE 42 | #define SERIAL_RATE 115200 43 | #endif 44 | 45 | #ifndef DEBUG_QUEUE_MSG 46 | #define DEBUG_QUEUE_MSG false 47 | #endif 48 | 49 | // Specify if you want to use a display or only Serial 50 | #ifndef DISPLAY_ENABLED 51 | #define DISPLAY_ENABLED false 52 | #endif 53 | 54 | #if (DISPLAY_ENABLED) 55 | #include 56 | // Display specs 57 | #define OLED_RESET LED_BUILTIN // Pin used for integrated D1 Mini blue LED 58 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 59 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 60 | extern Adafruit_SSD1306 display; 61 | #endif 62 | 63 | // Values greater then 0 enables Improv for that milliseconds period 64 | #ifndef IMPROV_ENABLED 65 | #define IMPROV_ENABLED 0 66 | #endif 67 | 68 | // SENSORNAME will be used as device network name 69 | #ifndef WIFI_DEVICE_NAME 70 | #define WIFI_DEVICE_NAME "ArduinoBootstrapper" 71 | #endif 72 | const char *const DEVICE_NAME = WIFI_DEVICE_NAME; 73 | 74 | // Port for the OTA firmware uplaod 75 | #ifndef MICROCONTROLLER_OTA_PORT 76 | #define MICROCONTROLLER_OTA_PORT 8199 77 | #endif 78 | const int OTA_PORT = MICROCONTROLLER_OTA_PORT; 79 | 80 | // Set wifi power in WIFI_POWER range 0/0.25, set to 0 to reduce PIR false positive due to wifi power, 0 low, 20.5 max. 81 | #ifndef WIFI_SIGNAL_STRENGTH 82 | #define WIFI_SIGNAL_STRENGTH 0 83 | #endif 84 | const double WIFI_POWER = WIFI_SIGNAL_STRENGTH; 85 | 86 | // GATEWAY IP 87 | #ifndef GATEWAY_IP 88 | #define GATEWAY_IP "192.168.1.1" // enter your gateway IP address 89 | #endif 90 | const char *const IP_GATEWAY = GATEWAY_IP; 91 | 92 | // SUBNET IP 93 | #ifndef SUBNET_IP 94 | #define SUBNET_IP "255.255.255.0" 95 | #endif 96 | const char *const IP_SUBNET = SUBNET_IP; 97 | 98 | // DNS IP 99 | #ifndef DNS_IP 100 | #define DNS_IP "8.8.8.8" // enter a DNS server IP if you need domain name access and static IP is selected 101 | // example: 8.8.8.8 for Google DNS server or use the DNS server provided by your ISP (see your gateway configuration) 102 | #endif 103 | const char *const IP_DNS = DNS_IP; 104 | 105 | // STATIC IP FOR THE MICROCONTROLLER 106 | #ifndef MICROCONTROLLER_IP 107 | #define MICROCONTROLLER_IP "192.168.1.99" // enter a valid IP address for static IP or "DHCP" for dynamic IP 108 | #endif 109 | const char *const IP_MICROCONTROLLER = MICROCONTROLLER_IP; 110 | 111 | // MQTT server IP 112 | #ifndef MQTT_SERVER_IP 113 | #define MQTT_SERVER_IP "192.168.1.3" 114 | #endif 115 | const char *const MQTT_SERVER = MQTT_SERVER_IP; 116 | 117 | // MQTT server port 118 | #ifndef MQTT_SERVER_PORT 119 | #define MQTT_SERVER_PORT "1883" 120 | #endif 121 | const char *const MQTT_PORT = MQTT_SERVER_PORT; 122 | 123 | // Maximum number of reconnection (WiFi/MQTT) attemp before powering off peripherals 124 | #ifndef MAX_RECONNECT 125 | #define MAX_RECONNECT 500 126 | #endif 127 | 128 | // Maximum JSON Object Size 129 | #ifndef MAX_JSON_OBJECT_SIZE 130 | #define MAX_JSON_OBJECT_SIZE 50 131 | #endif 132 | 133 | // Maximum JSON Object Size 134 | #ifndef SMALL_JSON_OBJECT_SIZE 135 | #define SMALL_JSON_OBJECT_SIZE 50 136 | #endif 137 | 138 | // Maximum JSON Object Size 139 | #ifndef MQTT_MAX_PACKET_SIZE 140 | #define MQTT_MAX_PACKET_SIZE 1024 141 | #endif 142 | 143 | // MQTT Keep Alive 144 | #ifndef MQTT_KEEP_ALIVE 145 | #define MQTT_KEEP_ALIVE 60 146 | #endif 147 | 148 | // Additional param that can be used for general purpose use 149 | #ifndef ADDITIONAL_PARAM_TEXT 150 | #define ADDITIONAL_PARAM_TEXT "ADDITIONAL PARAM" 151 | #endif 152 | 153 | // Additional param that can be used for general purpose use 154 | #ifndef ADDITIONAL_PARAM 155 | #define ADDITIONAL_PARAM "none" 156 | #endif 157 | const char *const PARAM_ADDITIONAL = ADDITIONAL_PARAM; 158 | 159 | #endif -------------------------------------------------------------------------------- /src/EthManager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | EthManager.cpp - Managing WiFi and OTA 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | #include "EthManager.h" 20 | 21 | 22 | #if defined(ARDUINO_ARCH_ESP32) 23 | /** 24 | * Supported ethernet devices 25 | */ 26 | const ethernet_config ethernetDevices[] = { 27 | // No Ethernet 28 | { 29 | }, 30 | // QuinLed-ESP32-Ethernet 31 | { 32 | 33 | 0, 34 | 5, 35 | 23, 36 | 18, 37 | ETH_PHY_LAN8720, 38 | ETH_CLOCK_GPIO17_OUT 39 | }, 40 | // QuinLed-Dig-Octa Brainboard-32-8L and LilyGO-T-ETH-POE 41 | { 42 | 0, 43 | -1, 44 | 23, 45 | 18, 46 | ETH_PHY_LAN8720, 47 | ETH_CLOCK_GPIO17_OUT 48 | }, 49 | // WT32-EHT01 50 | // These pins works well: IO2, IO4, IO12, IO14, IO15 51 | // These not: IO35, IO36, IO39 52 | { 53 | 1, 54 | 16, 55 | 23, 56 | 18, 57 | ETH_PHY_LAN8720, 58 | ETH_CLOCK_GPIO0_IN 59 | }, 60 | // ESP32-ETHERNET-KIT-VE 61 | { 62 | 0, 63 | 5, 64 | 23, 65 | 18, 66 | ETH_PHY_IP101, 67 | ETH_CLOCK_GPIO0_IN 68 | }, 69 | // ESP32-POE 70 | { 71 | 0, 72 | 12, 73 | 23, 74 | 18, 75 | ETH_PHY_LAN8720, 76 | ETH_CLOCK_GPIO17_OUT 77 | }, 78 | // WESP32 79 | { 80 | 0, 81 | -1, 82 | 16, 83 | 17, 84 | ETH_PHY_LAN8720, 85 | ETH_CLOCK_GPIO0_IN 86 | }, 87 | // LilyGO-T-POE-Pro 88 | { 89 | 0, 90 | 5, 91 | 23, 92 | 18, 93 | ETH_PHY_LAN8720, 94 | ETH_CLOCK_GPIO0_OUT 95 | }, 96 | // ESP32-POE-WROVER 97 | { 98 | 0, 99 | 12, 100 | 23, 101 | 18, 102 | ETH_PHY_LAN8720, 103 | ETH_CLOCK_GPIO0_OUT 104 | } 105 | }; 106 | 107 | /** 108 | * Connect to ethernet 109 | * @param deviceNumber to use 110 | */ 111 | void EthManager::connectToEthernet(int8_t deviceNumber) { 112 | ETH.begin( 113 | ethernetDevices[deviceNumber].address, 114 | ethernetDevices[deviceNumber].power, 115 | ethernetDevices[deviceNumber].mdc, 116 | ethernetDevices[deviceNumber].mdio, 117 | ethernetDevices[deviceNumber].type, 118 | ethernetDevices[deviceNumber].clk_mode 119 | ); 120 | } 121 | 122 | /** 123 | * Deallocate ethernet pins 124 | * @param deviceNumber to deallocate 125 | */ 126 | void EthManager::deallocateEthernetPins(int8_t deviceNumber) { 127 | const uint32_t MATRIX_DETACH_OUT_SIG = 0x100; 128 | gpio_matrix_out(ethernetDevices[deviceNumber].address, MATRIX_DETACH_OUT_SIG, false, false); 129 | gpio_matrix_out(ethernetDevices[deviceNumber].power, MATRIX_DETACH_OUT_SIG, false, false); 130 | gpio_matrix_out(ethernetDevices[deviceNumber].mdc, MATRIX_DETACH_OUT_SIG, false, false); 131 | gpio_matrix_out(ethernetDevices[deviceNumber].mdio, MATRIX_DETACH_OUT_SIG, false, false); 132 | gpio_matrix_out(ethernetDevices[deviceNumber].clk_mode, MATRIX_DETACH_OUT_SIG, false, false); 133 | pinMode(ethernetDevices[deviceNumber].address, INPUT); 134 | pinMode(ethernetDevices[deviceNumber].power, INPUT); 135 | pinMode(ethernetDevices[deviceNumber].mdc, INPUT); 136 | pinMode(ethernetDevices[deviceNumber].mdio, INPUT); 137 | pinMode(ethernetDevices[deviceNumber].clk_mode, INPUT); 138 | } 139 | 140 | #endif 141 | -------------------------------------------------------------------------------- /src/EthManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | EthManager.h - Managing Wifi and OTA 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include 21 | 22 | #if defined(ARDUINO_ARCH_ESP32) 23 | 24 | #include 25 | 26 | #endif 27 | 28 | #ifndef _DPSOFTWARE_ETH_MANAGER_H 29 | #define _DPSOFTWARE_ETH_MANAGER_H 30 | 31 | #if defined(ARDUINO_ARCH_ESP32) 32 | typedef struct EthConfig { 33 | uint8_t address; 34 | int power; 35 | int mdc; 36 | int mdio; 37 | eth_phy_type_t type; 38 | eth_clock_mode_t clk_mode; 39 | } ethernet_config; 40 | 41 | extern const ethernet_config ethernetDevices[]; 42 | 43 | class EthManager { 44 | 45 | public: 46 | static void connectToEthernet(int8_t deviceNumber); 47 | static void deallocateEthernetPins(int8_t deviceNumber); 48 | }; 49 | 50 | #endif 51 | #endif -------------------------------------------------------------------------------- /src/Helpers.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | Helpers.cpp - Helper classes 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include "Helpers.h" 21 | 22 | bool isConfigFileOk = false; 23 | String lastMQTTConnection = "OFF"; 24 | String lastWIFiConnection = "OFF"; 25 | String lastBoot = " "; 26 | 27 | bool screenSaverTriggered = false; 28 | 29 | bool lastPageScrollTriggered = false; 30 | int yoffset = 150; 31 | String haVersion = ""; 32 | String firmwareVersion = ""; 33 | String IP = ""; 34 | String MAC = ""; 35 | String deviceName = "XXX"; 36 | String microcontrollerIP = "XXX"; 37 | bool dhcpInUse = true; 38 | #if defined(ARDUINO_ARCH_ESP32) 39 | int8_t ethd = 0; 40 | #else 41 | int8_t ethd = -1; 42 | #endif 43 | String qsid = "XXX"; 44 | String qpass = "XXX"; 45 | String OTApass = "XXX"; 46 | String mqttIP = "XXX"; 47 | String mqttPort = "XXX"; 48 | String mqttuser = "XXX"; 49 | String mqttpass = "XXX"; 50 | String mqttWillTopic = "0"; 51 | String mqttWillPayload = "0"; 52 | int mqttWillQOS = 1; 53 | bool mqttWillRetain = false; 54 | bool mqttCleanSession = true; 55 | bool blockingMqtt = true; 56 | bool mqttConnected = false; 57 | String additionalParam = "XXX"; 58 | bool ethConnected = false; 59 | 60 | unsigned long previousMillis = 0; 61 | const unsigned long interval = 200; 62 | bool ledTriggered = false; 63 | int blinkCounter = 0; 64 | const int blinkTimes = 6; 65 | 66 | String timedate = "OFF"; 67 | [[maybe_unused]] String date = "OFF"; 68 | [[maybe_unused]] String currentime = "OFF"; 69 | String ERROR = "ERROR"; 70 | 71 | int wifiReconnectAttemp = 0; 72 | int mqttReconnectAttemp = 0; 73 | bool fastDisconnectionManagement = false; 74 | bool forceWebServer = false; 75 | bool temporaryDisableImprove = false; 76 | bool improvePacketReceived = false; 77 | 78 | void Helpers::smartPrint(String msg) { 79 | #if (DISPLAY_ENABLED) 80 | display.print(msg); 81 | #else 82 | Serial.print(msg); 83 | #endif 84 | } 85 | 86 | void Helpers::smartPrintln(String msg) { 87 | #if (DISPLAY_ENABLED) 88 | display.println(msg); 89 | #else 90 | Serial.println(msg); 91 | #endif 92 | } 93 | 94 | void Helpers::smartPrint(int msg) { 95 | #if (DISPLAY_ENABLED) 96 | display.print(msg); 97 | #else 98 | Serial.print(msg); 99 | #endif 100 | } 101 | 102 | void Helpers::smartPrintln(int msg) { 103 | #if (DISPLAY_ENABLED) 104 | display.println(msg); 105 | #else 106 | Serial.println(msg); 107 | #endif 108 | } 109 | 110 | void Helpers::smartDisplay() { 111 | #if (DISPLAY_ENABLED) 112 | display.display(); 113 | #endif 114 | } 115 | 116 | void Helpers::smartDisplay(int delayTime) { 117 | #if (DISPLAY_ENABLED) 118 | display.display(); 119 | delay(delayTime); 120 | #endif 121 | } 122 | 123 | /* 124 | This function returns a single string separated by a predefined character at a given index. For example: 125 | String split = "hi this is a split test"; 126 | String word3 = getValue(split, ' ', 2); 127 | Serial.println(word3); 128 | Should print 'is'. You also can try with index 0 returning 'hi' or safely trying index 5 returning 'test'. 129 | */ 130 | String Helpers::getValue(String data, char separator, int index) { 131 | int found = 0; 132 | int strIndex[] = {0, -1}; 133 | int maxIndex = data.length() - 1; 134 | 135 | for (int i = 0; i <= maxIndex && found <= index; i++) { 136 | if (data.charAt(i) == separator || i == maxIndex) { 137 | found++; 138 | strIndex[0] = strIndex[1] + 1; 139 | strIndex[1] = (i == maxIndex) ? i + 1 : i; 140 | } 141 | } 142 | return found > index ? data.substring(strIndex[0], strIndex[1]) : ""; 143 | } 144 | 145 | // Set date and time based on a timestamp 146 | void Helpers::setDateTime(String timeConst) { 147 | timedate = timeConst; 148 | date = timedate.substring(8, 10) + "/" + timedate.substring(5, 7) + "/" + timedate.substring(0, 4); 149 | currentime = timedate.substring(11, 16); 150 | } 151 | 152 | // Return ON OFF value 153 | String Helpers::isOnOff(JsonDocument json) { 154 | String str = json[VALUE]; 155 | return ((str == ON_CMD) || (str == on_CMD)) ? ON_CMD : OFF_CMD; 156 | } 157 | 158 | // Useful for Arduino Json lib to disambiguate types 159 | String Helpers::getValue(String string) { 160 | return string; 161 | } 162 | 163 | // String to char* 164 | char *Helpers::string2char(const String command) { 165 | char *p = const_cast(command.c_str()); 166 | return p; 167 | } 168 | 169 | // From version number to Number 170 | long Helpers::versionNumberToNumber(const String &latestReleaseStr) { 171 | long longVersion = 0; 172 | longVersion += Helpers::getValue(latestReleaseStr, '.', 0).toInt() + 1000000; 173 | longVersion += Helpers::getValue(latestReleaseStr, '.', 1).toInt() + 1000; 174 | longVersion += Helpers::getValue(latestReleaseStr, '.', 2).toInt(); 175 | return longVersion; 176 | } 177 | -------------------------------------------------------------------------------- /src/Helpers.h: -------------------------------------------------------------------------------- 1 | /* 2 | Helpers.h - Helper classes 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_HELPERS_UTILS_H 21 | #define _DPSOFTWARE_HELPERS_UTILS_H 22 | 23 | #include 24 | #include "Configuration.h" 25 | 26 | 27 | extern bool isConfigFileOk; 28 | extern String lastMQTTConnection; 29 | extern String lastWIFiConnection; 30 | extern String lastBoot; 31 | 32 | extern bool screenSaverTriggered; 33 | 34 | extern bool lastPageScrollTriggered; 35 | extern int yoffset; 36 | 37 | extern String haVersion; 38 | extern String firmwareVersion; 39 | extern String MAC; 40 | extern String deviceName; 41 | extern String microcontrollerIP; 42 | extern bool dhcpInUse; 43 | extern int8_t ethd; 44 | extern String qsid; 45 | extern String qpass; 46 | extern String OTApass; 47 | extern String mqttIP; 48 | extern String mqttPort; 49 | extern String mqttuser; 50 | extern String mqttpass; 51 | extern String mqttWillTopic; 52 | extern String mqttWillPayload; 53 | extern int mqttWillQOS; 54 | extern bool mqttWillRetain; 55 | extern bool mqttCleanSession; 56 | extern bool mqttConnected; 57 | extern bool blockingMqtt; 58 | extern String additionalParam; 59 | extern bool ethConnected; 60 | 61 | // Blink LED vars 62 | extern unsigned long previousMillis; // will store last time LED was updated 63 | extern const unsigned long interval; // interval at which to blink (milliseconds) 64 | extern bool ledTriggered; 65 | extern int blinkCounter; 66 | extern const int blinkTimes; // 6 equals to 3 blink on and 3 off 67 | 68 | extern String timedate; 69 | [[maybe_unused]] extern String date; 70 | [[maybe_unused]] extern String currentime; 71 | extern String ERROR; 72 | 73 | extern int wifiReconnectAttemp; 74 | extern int mqttReconnectAttemp; 75 | extern bool fastDisconnectionManagement; 76 | extern bool forceWebServer; // if set to true, forces the use of launchWebServerForOTAConfig - added by Pronoe on 02/03/2022 77 | 78 | [[maybe_unused]] const int DELAY_10 = 10; 79 | [[maybe_unused]] const int DELAY_50 = 50; 80 | [[maybe_unused]] const int DELAY_200 = 200; 81 | [[maybe_unused]] const int DELAY_500 = 500; 82 | [[maybe_unused]] const int DELAY_1000 = 1000; 83 | [[maybe_unused]] const int DELAY_1500 = 1500; 84 | [[maybe_unused]] const int DELAY_3000 = 3000; 85 | [[maybe_unused]] const int DELAY_2000 = 2000; 86 | [[maybe_unused]] const int DELAY_4000 = 4000; 87 | [[maybe_unused]] const int DELAY_5000 = 5000; 88 | 89 | [[maybe_unused]] const String ON_CMD = "ON"; 90 | [[maybe_unused]] const String OFF_CMD = "OFF"; 91 | [[maybe_unused]] const String on_CMD = "on"; 92 | [[maybe_unused]] const String off_CMD = "off"; 93 | [[maybe_unused]] const String VALUE = "value"; 94 | [[maybe_unused]] const String EMPTY_STR = ""; 95 | #if defined(ESP8266) 96 | #define FILE_READ "r" 97 | #define FILE_WRITE "w" 98 | #endif 99 | 100 | extern bool temporaryDisableImprove; 101 | extern bool improvePacketReceived; 102 | 103 | // 'arduino', 45x31px 104 | const unsigned char ARDUINOLOGO[] PROGMEM = { 105 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x01, 0xe0, 0x00, 0x01, 0xff, 0x80, 0x0f, 106 | 0xfc, 0x40, 0x07, 0xff, 0xe0, 0x3f, 0xff, 0x00, 0x0f, 0xff, 0xf0, 0x7f, 0xff, 0x80, 0x1f, 0x01, 107 | 0xf8, 0xfc, 0x07, 0xc0, 0x1e, 0x00, 0x7d, 0xf0, 0x03, 0xc0, 0x3c, 0x00, 0x3d, 0xe0, 0x01, 0xe0, 108 | 0x38, 0x00, 0x1f, 0xc0, 0xc0, 0xe0, 0x78, 0x00, 0x1f, 0xc0, 0xc0, 0xf0, 0x78, 0x7e, 0x0f, 0x83, 109 | 0xf0, 0xf0, 0x78, 0x7e, 0x0f, 0x83, 0xf0, 0xf0, 0x78, 0x00, 0x0f, 0x80, 0xc0, 0xf0, 0x38, 0x00, 110 | 0x1f, 0xc0, 0xc0, 0xe0, 0x3c, 0x00, 0x3d, 0xe0, 0x01, 0xe0, 0x3e, 0x00, 0x7d, 0xf0, 0x03, 0xe0, 111 | 0x1f, 0x00, 0xf8, 0xf8, 0x07, 0xc0, 0x0f, 0xe7, 0xf0, 0x7f, 0x3f, 0x80, 0x07, 0xff, 0xe0, 0x3f, 112 | 0xff, 0x00, 0x03, 0xff, 0xc0, 0x1f, 0xfe, 0x00, 0x00, 0xff, 0x00, 0x07, 0xf8, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x7b, 0xec, 0x9e, 0xc9, 0xe0, 114 | 0x1c, 0x4b, 0x2c, 0x8c, 0xeb, 0x20, 0x16, 0x5b, 0x3c, 0x8c, 0xea, 0x30, 0x16, 0x73, 0x3c, 0x8c, 115 | 0xfa, 0x30, 0x1e, 0x5b, 0x2c, 0x8c, 0xdb, 0x30, 0x22, 0x4b, 0x6c, 0x8c, 0xd9, 0x60, 0x23, 0x4f, 116 | 0x83, 0x1e, 0xc8, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 117 | }; 118 | #define ARDUINOLOGOW 45 119 | #define ARDUINOLOGOH 31 120 | 121 | // 'home-assistant_big', 44x44px 122 | static const unsigned char HABIGLOGO[] PROGMEM = { 123 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 124 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 125 | 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x00, 0x00, 126 | 0x00, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf8, 127 | 0x00, 0x00, 0x00, 0x03, 0xf0, 0xfd, 0xe0, 0x00, 0x00, 0x07, 0xe0, 0x7f, 0xe0, 0x00, 0x00, 0x0f, 128 | 0xcf, 0x3f, 0xe0, 0x00, 0x00, 0x1f, 0xcf, 0x3f, 0xe0, 0x00, 0x00, 0x3f, 0xe6, 0x7f, 0xe0, 0x00, 129 | 0x00, 0x7f, 0xe0, 0x7f, 0xe0, 0x00, 0x00, 0xff, 0xf0, 0xff, 0xf0, 0x00, 0x01, 0xff, 0xf0, 0xff, 130 | 0xf8, 0x00, 0x03, 0xff, 0xf0, 0xff, 0xfc, 0x00, 0x07, 0xf0, 0xf0, 0xf0, 0xfe, 0x00, 0x0f, 0xe0, 131 | 0x70, 0xe0, 0x7f, 0x00, 0x1f, 0xce, 0x70, 0xe7, 0x3f, 0x80, 0x1f, 0xcf, 0x70, 0xef, 0x3f, 0x80, 132 | 0x01, 0xce, 0x70, 0xe7, 0x38, 0x00, 0x01, 0xe0, 0x30, 0xc0, 0x78, 0x00, 0x01, 0xf0, 0x10, 0x80, 133 | 0xf8, 0x00, 0x01, 0xff, 0x00, 0x0f, 0xf8, 0x00, 0x01, 0xff, 0x80, 0x0f, 0xf8, 0x00, 0x01, 0xff, 134 | 0xc0, 0x1f, 0xf8, 0x00, 0x01, 0xff, 0xe0, 0x3f, 0xf8, 0x00, 0x01, 0xff, 0xf0, 0x7f, 0xf8, 0x00, 135 | 0x01, 0xff, 0xf0, 0xff, 0xf8, 0x00, 0x01, 0xff, 0xf0, 0xff, 0xf8, 0x00, 0x01, 0xff, 0xf0, 0xff, 136 | 0xf8, 0x00, 0x01, 0xff, 0xf0, 0xff, 0xf8, 0x00, 0x01, 0xff, 0xf0, 0xff, 0xf8, 0x00, 0x01, 0xff, 137 | 0xf0, 0xff, 0xf8, 0x00, 0x01, 0xff, 0xf0, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 138 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 139 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 140 | }; 141 | #define HABIGLOGOW 44 142 | #define HABIGLOGOH 44 143 | 144 | class Helpers { 145 | 146 | public: 147 | static void smartPrint(String msg); 148 | 149 | static void smartPrintln(String msg); 150 | 151 | static void smartPrint(int msg); 152 | 153 | static void smartPrintln(int msg); 154 | 155 | void smartDisplay(); 156 | 157 | void smartDisplay(int delayTime); 158 | 159 | static String getValue(String data, char separator, int index); 160 | 161 | static String getValue(String string); 162 | 163 | [[maybe_unused]] static long versionNumberToNumber(const String &latestReleaseStr); 164 | 165 | [[maybe_unused]] static char *string2char(String command); 166 | 167 | [[maybe_unused]] static void setDateTime(String timeConst); 168 | 169 | [[maybe_unused]] static String isOnOff(JsonDocument json); 170 | 171 | }; 172 | 173 | #endif 174 | 175 | -------------------------------------------------------------------------------- /src/PingESP.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | PingESP.cpp - Utility class for pining the default gateway from ESP8266, 3 | some routers isn't able to detect connected devices if there is no traffic over the internet or direct to the gateway. 4 | This is helpful as a stay alive class, useful for not triggering security features on routers. 5 | 6 | Copyright © 2020 - 2025 Davide Perini 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | You should have received a copy of the MIT License along with this program. 19 | If not, see . 20 | */ 21 | 22 | #if defined(ESP8266) 23 | 24 | #include "PingESP.h" 25 | 26 | extern "C" 27 | extern "C" void esp_yield(); 28 | extern "C" void __esp_suspend(); 29 | 30 | /** 31 | * Newer Arduino SDK than 3.0.2 does esp_schedule() inside esp_yield(), this break ping functions here. 32 | * Direct alternative would be to use new esp_suspend(). 33 | * Continue to use esp_yield() implementation when there is no esp_suspend() function (<= 3.0.2), 34 | * use new esp_suspend() instead (> 3.0.2). 35 | */ 36 | extern "C" void suspend_or_yield() { 37 | esp_yield(); 38 | } 39 | void __esp_suspend(void) __attribute__((weak, alias("suspend_or_yield"))); 40 | 41 | PingESP::PingESP() {} 42 | 43 | byte PingESP::pingError = 0; 44 | byte PingESP::pingSuccess = 0; 45 | 46 | /** 47 | * Ping another device 48 | * @param dest gateway to ping 49 | * @return 50 | */ 51 | bool PingESP::ping(IPAddress dest) { 52 | pingError = 0; 53 | pingSuccess = 0; 54 | memset(&pingOptions, 0, sizeof(struct ping_option)); 55 | pingOptions.count = 1; // repeat only 1 time 56 | pingOptions.coarse_time = 1; 57 | pingOptions.ip = dest; 58 | pingOptions.recv_function = reinterpret_cast(&PingESP::receivePingCallback); 59 | pingOptions.sent_function = nullptr; 60 | if (ping_start(&pingOptions)) { 61 | // Sleep until the ping is finished 62 | __esp_suspend(); 63 | } 64 | return (pingSuccess > 0); 65 | } 66 | 67 | /** 68 | * Ping gateway to keep the connection alive 69 | * @param dest gateway to ping 70 | * @return 71 | */ 72 | bool PingESP::ping() { 73 | pingError = 0; 74 | pingSuccess = 0; 75 | memset(&pingOptions, 0, sizeof(struct ping_option)); 76 | pingOptions.count = 1; // repeat only 1 time 77 | pingOptions.coarse_time = 1; 78 | pingOptions.ip = WiFi.gatewayIP(); 79 | pingOptions.recv_function = reinterpret_cast(&PingESP::receivePingCallback); 80 | pingOptions.sent_function = nullptr; 81 | if (ping_start(&pingOptions)) { 82 | // Sleep until the ping is finished 83 | __esp_suspend(); 84 | } 85 | return (pingSuccess > 0); 86 | } 87 | 88 | /** 89 | * Callback from the ESP core ping_start 90 | * @param opt options 91 | * @param resp response 92 | */ 93 | void PingESP::receivePingCallback(void *opt, void *resp) { 94 | // ping_resp from ESP core 95 | ping_resp *ping_resp = reinterpret_cast(resp); 96 | // Check for errors 97 | if (ping_resp->ping_err == -1) { 98 | pingError++; 99 | Serial.println(F("PING KO")); 100 | } else { 101 | pingSuccess++; 102 | } 103 | if (pingSuccess + pingError == 1) { 104 | // ping sent, return 105 | esp_schedule(); 106 | } 107 | } 108 | #endif -------------------------------------------------------------------------------- /src/PingESP.h: -------------------------------------------------------------------------------- 1 | /* 2 | PingESP.h - Utility class for pining the default gateway from ESP8266, 3 | some routers aren't able to detect connected devices if there is no traffic over the internet or direct to the gateway. 4 | This is helpful as a stay alive class, useful for not triggering security features on routers. 5 | Usage: 6 | - add the following declaration in your application header file: 7 | #include "PingESP.h" 8 | PingESP pingESP; 9 | - use `pingESP.ping(WiFi.gatewayIP());` in your application code 10 | 11 | Copyright © 2020 - 2025 Davide Perini 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of 14 | this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in 21 | all copies or substantial portions of the Software. 22 | 23 | You should have received a copy of the MIT License along with this program. 24 | If not, see . 25 | */ 26 | 27 | #if defined(ESP8266) 28 | 29 | #ifndef PingESP_H 30 | #define PingESP_H 31 | 32 | #include 33 | #include 34 | 35 | extern "C" { 36 | #include 37 | } 38 | 39 | class PingESP { 40 | public: 41 | PingESP(); 42 | 43 | bool ping(IPAddress dest); 44 | 45 | bool ping(); 46 | 47 | protected: 48 | static void receivePingCallback(void *opt, void *pdata); 49 | 50 | ping_option pingOptions; 51 | static byte pingCount, pingError, pingSuccess; 52 | }; 53 | 54 | #endif // PingESP_H 55 | 56 | #endif -------------------------------------------------------------------------------- /src/QueueManager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | QueueManager.cpp - Managing MQTT queue 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include "QueueManager.h" 21 | 22 | 23 | PubSubClient mqttClient(espClient); 24 | 25 | /********************************** SETUP MQTT QUEUE **********************************/ 26 | void QueueManager::setupMQTTQueue(void (*callback)(char *, byte *, unsigned int)) { 27 | mqttClient.setServer(IPAddress(Helpers::getValue(mqttIP, '.', 0).toInt(), 28 | Helpers::getValue(mqttIP, '.', 1).toInt(), 29 | Helpers::getValue(mqttIP, '.', 2).toInt(), 30 | Helpers::getValue(mqttIP, '.', 3).toInt()), mqttPort.toInt()); 31 | mqttClient.setCallback(callback); 32 | mqttClient.setBufferSize(MQTT_MAX_PACKET_SIZE); 33 | mqttClient.setKeepAlive(MQTT_KEEP_ALIVE); 34 | } 35 | 36 | /********************************** SET LAST WILL PARAMETERS **********************************/ 37 | void 38 | QueueManager::setMQTTWill(const char *topic, const char *payload, const int qos, boolean retain, boolean cleanSession) { 39 | mqttWillTopic = topic; 40 | mqttWillPayload = payload; 41 | mqttWillQOS = qos; 42 | mqttWillRetain = retain; 43 | mqttCleanSession = cleanSession; 44 | } 45 | 46 | /********************************** MQTT RECONNECT **********************************/ 47 | void QueueManager::mqttReconnect(void (*manageDisconnections)(), void (*manageQueueSubscription)(), 48 | void (*manageHardwareButton)()) { 49 | // how many attemps to MQTT connection 50 | mqttReconnectAttemp = 0; 51 | // Loop until we're reconnected 52 | while (!mqttClient.connected() && (WiFi.status() == WL_CONNECTED || ethd >= 0)) { 53 | #if (DISPLAY_ENABLED) 54 | display.clearDisplay(); 55 | display.setTextSize(1); 56 | display.setCursor(0,0); 57 | #endif 58 | if (mqttReconnectAttemp <= 20) { 59 | Helpers::smartPrintln(F("Connecting to")); 60 | Helpers::smartPrintln(F("MQTT Broker...")); 61 | } 62 | helper.smartDisplay(); 63 | // Manage hardware button if any 64 | manageHardwareButton(); 65 | // Attempt to connect to MQTT server with QoS = 1 (pubsubclient supports QoS 1 for subscribe only, published msg have QoS 0 this is why I implemented a custom solution) 66 | boolean mqttSuccess; 67 | Serial.println("MQTT Last Will Params: "); 68 | Serial.print("willTopic: "); 69 | Serial.println(mqttWillTopic); 70 | Serial.print("willPayload: "); 71 | Serial.println(mqttWillPayload); 72 | Serial.print("qos: "); 73 | Serial.println(mqttWillQOS); 74 | Serial.print("retain: "); 75 | Serial.println(mqttWillRetain); 76 | Serial.print("clean session: "); 77 | Serial.println(mqttCleanSession); 78 | if (mqttuser.isEmpty() || mqttpass.isEmpty()) { 79 | mqttSuccess = mqttClient.connect(Helpers::string2char(deviceName), Helpers::string2char(mqttWillTopic), 80 | mqttWillQOS, mqttWillRetain, Helpers::string2char(mqttWillPayload)); 81 | } else { 82 | mqttSuccess = mqttClient.connect(Helpers::string2char(deviceName), Helpers::string2char(mqttuser), 83 | Helpers::string2char(mqttpass), Helpers::string2char(mqttWillTopic), mqttWillQOS, 84 | mqttWillRetain, Helpers::string2char(mqttWillPayload), mqttCleanSession); 85 | } 86 | if (mqttSuccess) { 87 | Helpers::smartPrintln(F("")); 88 | Helpers::smartPrintln(F("MQTT CONNECTED")); 89 | Helpers::smartPrintln(F("")); 90 | Helpers::smartPrintln(F("Reading data from")); 91 | Helpers::smartPrintln(F("the network...")); 92 | helper.smartDisplay(); 93 | // Subscribe to MQTT topics 94 | manageQueueSubscription(); 95 | delay(DELAY_2000); 96 | mqttReconnectAttemp = 0; 97 | // reset the lastMQTTConnection to off, will be initialized by next time update 98 | lastMQTTConnection = OFF_CMD; 99 | } else { 100 | Helpers::smartPrintln(F("MQTT attempts=")); 101 | Helpers::smartPrintln(mqttReconnectAttemp); 102 | helper.smartDisplay(); 103 | // after MAX_RECONNECT attemps all peripherals are shut down 104 | if (mqttReconnectAttemp >= MAX_RECONNECT || fastDisconnectionManagement) { 105 | Helpers::smartPrintln(F("Max retry reached, powering off peripherals.")); 106 | helper.smartDisplay(); 107 | // Manage disconnections, powering off peripherals 108 | manageDisconnections(); 109 | } else if (mqttReconnectAttemp > 10000) { 110 | mqttReconnectAttemp = 0; 111 | } 112 | mqttReconnectAttemp++; 113 | // Wait 500 millis before retrying 114 | delay(DELAY_500); 115 | } 116 | if (!blockingMqtt) { 117 | break; 118 | } 119 | } 120 | } 121 | 122 | void QueueManager::queueLoop(void (*manageDisconnections)(), void (*manageQueueSubscription)(), 123 | void (*manageHardwareButton)()) { 124 | if (!mqttClient.connected()) { 125 | mqttConnected = false; 126 | mqttReconnect(manageDisconnections, manageQueueSubscription, manageHardwareButton); 127 | } else { 128 | mqttConnected = true; 129 | } 130 | mqttClient.loop(); 131 | } 132 | 133 | /********************************** SEND A MESSAGE ON THE QUEUE **********************************/ 134 | void QueueManager::publish(const char *topic, const char *payload, boolean retained) { 135 | mqttClient.publish(topic, payload, retained); 136 | } 137 | 138 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 139 | void QueueManager::unsubscribe(const char *topic) { 140 | mqttClient.unsubscribe(topic); 141 | } 142 | 143 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 144 | void QueueManager::subscribe(const char *topic) { 145 | mqttClient.subscribe(topic); 146 | } 147 | 148 | /********************************** SUBSCRIBE TO A QUEUE TOPIC **********************************/ 149 | void QueueManager::subscribe(const char *topic, uint8_t qos) { 150 | mqttClient.subscribe(topic, qos); 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/QueueManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | QueueManager.h - Managing MQTT queue 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_QUEUE_MANAGER_H 21 | #define _DPSOFTWARE_QUEUE_MANAGER_H 22 | 23 | #include 24 | #include "WifiManager.h" 25 | 26 | 27 | class QueueManager { 28 | 29 | private: 30 | Helpers helper; 31 | 32 | public: 33 | static void setupMQTTQueue(void (*callback)(char *, byte *, unsigned int)); // setup the queue 34 | static void setMQTTWill(const char *topic, const char *payload, int qos, boolean retain, boolean cleanSession); // set the last will parameters for mqtt 35 | void mqttReconnect(void (*manageDisconnections)(), void (*manageQueueSubscription)(), void (*manageHardwareButton)()); // manage reconnection on the queue 36 | void queueLoop(void (*manageDisconnections)(), void (*manageQueueSubscription)(), void (*manageHardwareButton)()); // manage queue loop 37 | static void publish(const char *topic, const char *payload, boolean retained); // send a message on the queue 38 | static void unsubscribe(const char *topic); // unsubscribe to a queue topic 39 | static void subscribe(const char *topic); // subscribe to a queue topic 40 | static void subscribe(const char *topic, uint8_t qos); // subscribe to a queue topic with qos 0 or 1 41 | 42 | }; 43 | 44 | #endif -------------------------------------------------------------------------------- /src/Secrets.h: -------------------------------------------------------------------------------- 1 | /* 2 | Secrets.h - Store your secrets here 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_SECRETS_H 21 | #define _DPSOFTWARE_SECRETS_H 22 | 23 | #ifndef WIFI_SSID 24 | #define WIFI_SSID "XXX" 25 | #endif 26 | const char *const SSID = WIFI_SSID; 27 | 28 | #ifndef WIFI_PWD 29 | #define WIFI_PWD "XXX" 30 | #endif 31 | const char *const PASSWORD = WIFI_PWD; 32 | 33 | #ifndef MQTT_USER 34 | #define MQTT_USER "XXX" 35 | #endif 36 | const char *const MQTT_USERNAME = MQTT_USER; 37 | 38 | #ifndef MQTT_PWD 39 | #define MQTT_PWD "XXX" 40 | #endif 41 | const char *const MQTT_PASSWORD = MQTT_PWD; 42 | 43 | #ifndef OTA_PWD 44 | #define OTA_PWD "XXX" 45 | #endif 46 | const char *const OTAPASSWORD = OTA_PWD; //the password you will need to enter for OTA upload 47 | 48 | #endif -------------------------------------------------------------------------------- /src/WifiManager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | WifiManager.cpp - Managing WiFi and OTA 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #include "WifiManager.h" 21 | 22 | //Establishing Local server at port 80 whenever required 23 | #if defined(ESP8266) 24 | ESP8266WebServer server(80); 25 | #elif defined(ARDUINO_ARCH_ESP32) 26 | WebServer server(80); 27 | #endif 28 | // WiFiClient 29 | WiFiClient espClient; 30 | // WebServer content 31 | String content; 32 | // WebServer status code 33 | int statusCode; 34 | // WebServer HTML frontend 35 | String htmlString; 36 | byte improvActive; //0: no improv packet received, 1: improv active, 2: provisioning 37 | byte improvError; 38 | char serverDescription[33] = "Luciferin"; 39 | char cmDNS[33] = "x"; 40 | char clientSSID[33]; 41 | char clientPass[65]; 42 | unsigned long previousMillisEsp32Reconnect = 0; 43 | unsigned long intervalEsp32Reconnect = 15000; 44 | 45 | /********************************** SETUP WIFI *****************************************/ 46 | void WifiManager::setupWiFi(void (*manageDisconnections)(), void (*manageHardwareButton)()) { 47 | wifiReconnectAttemp = 0; 48 | // DPsoftware domotics 49 | #if (DISPLAY_ENABLED) 50 | display.clearDisplay(); 51 | display.setTextSize(2); 52 | display.setCursor(5,17); 53 | display.drawRoundRect(0, 0, display.width()-1, display.height()-1, display.height()/4, WHITE); 54 | #endif 55 | Helpers::smartPrintln(F("DPsoftware domotics")); 56 | helper.smartDisplay(DELAY_3000); 57 | #if (DISPLAY_ENABLED) 58 | display.clearDisplay(); 59 | display.setTextSize(1); 60 | display.setCursor(0,0); 61 | #endif 62 | Helpers::smartPrintln(F("Connecting to: ")); 63 | Helpers::smartPrint(qsid); 64 | Helpers::smartPrintln(F("...")); 65 | helper.smartDisplay(DELAY_2000); 66 | WiFi.persistent(true); // Solve possible wifi init errors (re-add at 6.2.1.16 #4044, #4083) 67 | WiFi.disconnect(true); // Delete SDK wifi config 68 | delay(DELAY_200); 69 | WiFi.mode(WIFI_STA); // Disable AP mode 70 | #if defined(ESP8266) 71 | WiFi.setSleepMode(WIFI_NONE_SLEEP); 72 | #endif 73 | WiFi.setAutoConnect(true); 74 | WiFi.setAutoReconnect(true); 75 | Serial.println(microcontrollerIP); 76 | if (!microcontrollerIP.equals("DHCP")) { 77 | WiFi.config(IPAddress(Helpers::getValue(microcontrollerIP, '.', 0).toInt(), 78 | Helpers::getValue(microcontrollerIP, '.', 1).toInt(), 79 | Helpers::getValue(microcontrollerIP, '.', 2).toInt(), 80 | Helpers::getValue(microcontrollerIP, '.', 3).toInt()), 81 | IPAddress(Helpers::getValue(IP_GATEWAY, '.', 0).toInt(), 82 | Helpers::getValue(IP_GATEWAY, '.', 1).toInt(), 83 | Helpers::getValue(IP_GATEWAY, '.', 2).toInt(), 84 | Helpers::getValue(IP_GATEWAY, '.', 3).toInt()), 85 | IPAddress(Helpers::getValue(IP_SUBNET, '.', 0).toInt(), 86 | Helpers::getValue(IP_SUBNET, '.', 1).toInt(), 87 | Helpers::getValue(IP_SUBNET, '.', 2).toInt(), 88 | Helpers::getValue(IP_SUBNET, '.', 3).toInt()), 89 | IPAddress(Helpers::getValue(IP_DNS, '.', 0).toInt(), 90 | Helpers::getValue(IP_DNS, '.', 1).toInt(), 91 | Helpers::getValue(IP_DNS, '.', 2).toInt(), 92 | Helpers::getValue(IP_DNS, '.', 3).toInt())); 93 | Serial.println(F("Using static IP address")); 94 | dhcpInUse = false; 95 | } else { 96 | Serial.println(F("Using DHCP")); 97 | dhcpInUse = true; 98 | } 99 | #if defined(ESP8266) 100 | WiFi.hostname(Helpers::string2char(deviceName)); 101 | // Set wifi power in dbm range 0/0.25, set to 0 to reduce PIR false positive due to wifi power, 0 low, 20.5 max. 102 | WiFi.setOutputPower(WIFI_POWER); 103 | if (microcontrollerIP.equals("DHCP")) { 104 | WiFi.config(0U, 0U, 0U); 105 | } 106 | WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected &event) { 107 | Serial.println(F("WiFi connection is lost.")); 108 | WiFi.reconnect(); 109 | }); 110 | #elif defined(ARDUINO_ARCH_ESP32) 111 | WiFi.setHostname(helper.string2char(deviceName)); 112 | btStop(); 113 | #endif 114 | // Start wifi connection 115 | WiFi.begin(qsid.c_str(), qpass.c_str()); 116 | #if defined(ARDUINO_ARCH_ESP32) 117 | setTxPower(); 118 | WiFi.setSleep(false); 119 | #endif 120 | reconnectToWiFi(manageDisconnections, manageHardwareButton); 121 | MAC = WiFi.macAddress(); 122 | delay(DELAY_1500); 123 | // reset the lastWIFiConnection to off, will be initialized by next time update 124 | lastWIFiConnection = OFF_CMD; 125 | } 126 | 127 | /** 128 | * Set the TX power based on dBm values. 129 | */ 130 | #if defined(ARDUINO_ARCH_ESP32) 131 | void WifiManager::setTxPower() const { 132 | wifi_power_t val = WIFI_POWER_MINUS_1dBm; 133 | if (WIFI_POWER < 0) { 134 | val = WIFI_POWER_MINUS_1dBm; 135 | } else if (WIFI_POWER < 3.5f) { 136 | val = WIFI_POWER_2dBm; 137 | } else if (WIFI_POWER < 6) { 138 | val = WIFI_POWER_5dBm; 139 | } else if (WIFI_POWER < 8) { 140 | val = WIFI_POWER_7dBm; 141 | } else if (WIFI_POWER < 10) { 142 | val = WIFI_POWER_8_5dBm; 143 | } else if (WIFI_POWER < 12) { 144 | val = WIFI_POWER_11dBm; 145 | } else if (WIFI_POWER < 14) { 146 | val = WIFI_POWER_13dBm; 147 | } else if (WIFI_POWER < 16) { 148 | val = WIFI_POWER_15dBm; 149 | } else if (WIFI_POWER < 17.75f) { 150 | val = WIFI_POWER_17dBm; 151 | } else if (WIFI_POWER < 18.75f) { 152 | val = WIFI_POWER_18_5dBm; 153 | } else if (WIFI_POWER < 19.25f) { 154 | val = WIFI_POWER_19dBm; 155 | } else { 156 | val = WIFI_POWER_19_5dBm; 157 | } 158 | WiFi.setTxPower(val); 159 | } 160 | #endif 161 | 162 | void WifiManager::reconnectToWiFi(void (*manageDisconnections)(), void (*manageHardwareButton)()) { 163 | wifiReconnectAttemp = 0; 164 | // loop here until connection 165 | while (WiFi.status() != WL_CONNECTED) { 166 | manageHardwareButton(); 167 | delay(DELAY_500); 168 | Serial.print(F(".")); 169 | wifiReconnectAttemp++; 170 | if (wifiReconnectAttemp > 10) { 171 | // if fastDisconnectionManagement we need to execute the callback immediately, 172 | // example: power off a watering system can't wait MAX_RECONNECT attemps 173 | if (fastDisconnectionManagement) { 174 | manageDisconnections(); 175 | } 176 | #if (DISPLAY_ENABLED) 177 | display.setCursor(0,0); 178 | display.clearDisplay(); 179 | display.setTextSize(1); 180 | #endif 181 | Helpers::smartPrint(F("Wifi attemps= ")); 182 | Helpers::smartPrintln(wifiReconnectAttemp); 183 | #if defined(ARDUINO_ARCH_ESP32) 184 | // Arduino 2.x for ESP32 seems to not support callback, polling to reconnect. 185 | unsigned long currentMillisEsp32Reconnect = millis(); 186 | if (currentMillisEsp32Reconnect - previousMillisEsp32Reconnect >= intervalEsp32Reconnect) { 187 | WiFi.disconnect(); 188 | WiFi.begin(qsid.c_str(), qpass.c_str()); 189 | setTxPower(); 190 | WiFi.setSleep(false); 191 | previousMillisEsp32Reconnect = currentMillisEsp32Reconnect; 192 | } 193 | #endif 194 | if (wifiReconnectAttemp >= MAX_RECONNECT) { 195 | Helpers::smartPrintln(F("Max retry reached, powering off peripherals.")); 196 | manageDisconnections(); 197 | } 198 | helper.smartDisplay(); 199 | } else if (wifiReconnectAttemp > 10000) { 200 | wifiReconnectAttemp = 0; 201 | } 202 | } 203 | if (wifiReconnectAttemp > 0) { 204 | Helpers::smartPrint(F("\nWIFI CONNECTED\nIP Address: ")); 205 | microcontrollerIP = WiFi.localIP().toString(); 206 | Helpers::smartPrintln(microcontrollerIP); 207 | Helpers::smartPrint(F("nb of attempts: ")); 208 | Helpers::smartPrintln(wifiReconnectAttemp); 209 | } 210 | } 211 | 212 | /********************************** SETUP OTA *****************************************/ 213 | void WifiManager::setupOTAUpload() { 214 | //OTA SETUP 215 | ArduinoOTA.setPort(OTA_PORT); 216 | // Hostname defaults to esp8266-[ChipID] 217 | ArduinoOTA.setHostname(Helpers::string2char(deviceName)); 218 | // No authentication by default 219 | ArduinoOTA.setPassword((const char *) Helpers::string2char(OTApass)); 220 | ArduinoOTA.onStart([]() { 221 | Serial.println(F("Starting")); 222 | }); 223 | ArduinoOTA.onEnd([]() { 224 | Serial.println(F("End")); 225 | }); 226 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 227 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 228 | }); 229 | ArduinoOTA.onError([](ota_error_t error) { 230 | Serial.printf("Error[%u]: ", error); 231 | if (error == OTA_AUTH_ERROR) Serial.println(F("Auth Failed")); 232 | else if (error == OTA_BEGIN_ERROR) Serial.println(F("Begin Failed")); 233 | else if (error == OTA_CONNECT_ERROR) Serial.println(F("Connect Failed")); 234 | else if (error == OTA_RECEIVE_ERROR) Serial.println(F("Receive Failed")); 235 | else if (error == OTA_END_ERROR) Serial.println(F("End Failed")); 236 | }); 237 | ArduinoOTA.begin(); 238 | } 239 | 240 | /* 241 | Return the quality (Received Signal Strength Indicator) of the WiFi network. 242 | Returns a number between 0 and 100 if WiFi is connected. 243 | Returns -1 if WiFi is disconnected. 244 | */ 245 | int WifiManager::getQuality() { 246 | if (WiFi.status() != WL_CONNECTED) { 247 | return -1; 248 | } 249 | int dBm = WiFi.RSSI(); 250 | if (dBm <= -100) { 251 | return 0; 252 | } 253 | if (dBm >= -50) { 254 | return 100; 255 | } 256 | return 2 * (dBm + 100); 257 | } 258 | 259 | // check if wifi is correctly configured 260 | bool WifiManager::isWifiConfigured() { 261 | if (strcmp(SSID, "XXX") != 0) { 262 | return true; 263 | } 264 | return false; 265 | } 266 | 267 | // if no ssid available, launch web server to get config params via browser 268 | void WifiManager::launchWebServerForOTAConfig() { 269 | WiFi.disconnect(); 270 | Serial.println(F("Turning HotSpot On")); 271 | setupAP(); 272 | launchWeb(); 273 | while (WiFi.status() != WL_CONNECTED) { 274 | delay(10); 275 | server.handleClient(); 276 | } 277 | } 278 | 279 | void WifiManager::launchWebServerCustom(void (*listener)()) { 280 | WiFi.disconnect(); 281 | Serial.println(F("Turning HotSpot On")); 282 | WiFi.mode(WIFI_STA); 283 | WiFi.disconnect(); 284 | delay(DELAY_200); 285 | WiFi.softAP(WIFI_DEVICE_NAME, ""); 286 | listener(); 287 | server.begin(); 288 | while (WiFi.status() != WL_CONNECTED) { 289 | delay(10); 290 | server.handleClient(); 291 | } 292 | } 293 | 294 | // Manage improv wifi 295 | void WifiManager::manageImprovWifi() { 296 | handleImprovPacket(); 297 | } 298 | 299 | void WifiManager::launchWeb() { 300 | Serial.println(""); 301 | if (WiFi.status() == WL_CONNECTED) { 302 | Serial.println("WiFi connected"); 303 | } 304 | Serial.print("Local IP: "); 305 | Serial.println(WiFi.localIP()); 306 | Serial.print("SoftAP IP: "); 307 | Serial.println(WiFi.softAPIP()); 308 | createWebServer(); 309 | server.begin(); 310 | Serial.println("Server started"); 311 | } 312 | 313 | void WifiManager::setupAP(void) { 314 | WiFi.mode(WIFI_STA); 315 | WiFi.disconnect(); 316 | delay(DELAY_200); 317 | int n = WiFi.scanNetworks(); 318 | Serial.println("scan done"); 319 | if (n == 0) { 320 | Serial.println("no networks found"); 321 | } else { 322 | Serial.print(n); 323 | Serial.println(" networks found"); 324 | for (int i = 0; i < n; ++i) { 325 | // Print SSID and RSSI for each network found 326 | Serial.print(i + 1); 327 | Serial.print(": "); 328 | Serial.print(WiFi.SSID(i)); 329 | Serial.print(" ("); 330 | Serial.print(WiFi.RSSI(i)); 331 | Serial.print(")"); 332 | #if defined(ESP8266) 333 | Serial.println((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? " " : "*"); 334 | #elif defined(ARDUINO_ARCH_ESP32) 335 | Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " " : "*"); 336 | #endif 337 | delay(10); 338 | } 339 | } 340 | Serial.println(""); 341 | htmlString = ""; 342 | for (int i = 0; i < n; ++i) { 343 | htmlString += ""; 344 | htmlString += ""; 347 | htmlString += ""; 350 | htmlString += ""; 357 | htmlString += ""; 358 | } 359 | htmlString += "
SSIDRSSIEnctipted
"; 345 | htmlString += WiFi.SSID(i); 346 | htmlString += ""; 348 | htmlString += WiFi.RSSI(i); 349 | htmlString += ""; 351 | #if defined(ESP8266) 352 | htmlString += ((WiFi.encryptionType(i) == ENC_TYPE_NONE) ? "PUBLIC" : "ENCRYPTED"); 353 | #elif defined(ARDUINO_ARCH_ESP32) 354 | htmlString += ((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "PUBLIC" : "ENCRYPTED"); 355 | #endif 356 | htmlString += "
"; 360 | delay(100); 361 | WiFi.softAP(WIFI_DEVICE_NAME, ""); 362 | launchWeb(); 363 | } 364 | 365 | void WifiManager::createWebServer() { 366 | { 367 | server.on("/", []() { 368 | IPAddress ip = WiFi.softAPIP(); 369 | String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]); 370 | content = "\n" 371 | "
\n" 372 | " \n" 376 | " \n" 444 | "
\n" 445 | "
\n"; 446 | content += htmlString; 447 | content += "
\n" 448 | " \n" 449 | " \n" 450 | " " 451 | " \n" 452 | " \n" 453 | " " 454 | "
\n" 455 | " \n" 456 | " \n" 457 | "
\n"; 458 | content += " \n" 461 | " \n" 462 | "
\n" 463 | "
\n" 464 | "
\n" 465 | " \n" 466 | " GitHub\n" 467 | "
" 468 | ""; 488 | server.send(200, "text/html", content); 489 | }); 490 | 491 | server.on("/setting", []() { 492 | String deviceName = server.arg("deviceName"); 493 | String microcontrollerIP = server.arg("microcontrollerIP"); 494 | String qsid = server.arg("ssid"); 495 | String qpass = server.arg("pass"); 496 | String mqttCheckbox = server.arg("mqttCheckbox"); 497 | String mqttIP = server.arg("mqttIP"); 498 | String mqttPort = server.arg("mqttPort"); 499 | String OTApass = server.arg("OTApass"); 500 | String mqttuser = server.arg("mqttuser"); 501 | String mqttpass = server.arg("mqttpass"); 502 | String additionalParam = server.arg("additionalParam"); 503 | JsonDocument doc; 504 | 505 | if (deviceName.length() > 0 && qsid.length() > 0 && qpass.length() > 0 && OTApass.length() > 0 506 | && ((mqttCheckbox.length() == 0) || (mqttIP.length() > 0 && mqttPort.length() > 0))) { 507 | 508 | Serial.println("deviceName"); 509 | Serial.println(deviceName); 510 | Serial.println("microcontrollerIP"); 511 | if (microcontrollerIP.length() == 0) { 512 | microcontrollerIP = "DHCP"; 513 | } 514 | Serial.println(microcontrollerIP); 515 | Serial.println("qsid"); 516 | Serial.println(qsid); 517 | Serial.println("qpass"); 518 | Serial.println(qpass); 519 | Serial.println("OTApass"); 520 | Serial.println(OTApass); 521 | Serial.println("mqttIP"); 522 | Serial.println(mqttIP); 523 | Serial.println("mqttPort"); 524 | Serial.println(mqttPort); 525 | Serial.println("mqttuser"); 526 | Serial.println(mqttuser); 527 | Serial.println("mqttpass"); 528 | Serial.println(mqttpass); 529 | Serial.println("additionalParam"); 530 | Serial.println(additionalParam); 531 | 532 | doc["deviceName"] = deviceName; 533 | doc["microcontrollerIP"] = microcontrollerIP; 534 | doc["qsid"] = qsid; 535 | doc["qpass"] = qpass; 536 | doc["OTApass"] = OTApass; 537 | if (mqttCheckbox.equals("on")) { 538 | doc["mqttIP"] = mqttIP; 539 | doc["mqttPort"] = mqttPort; 540 | doc["mqttuser"] = mqttuser; 541 | doc["mqttpass"] = mqttpass; 542 | } else { 543 | doc["mqttIP"] = ""; 544 | doc["mqttPort"] = ""; 545 | doc["mqttuser"] = ""; 546 | doc["mqttpass"] = ""; 547 | } 548 | doc["additionalParam"] = additionalParam; 549 | content = "Success: rebooting the microcontroller using your credentials."; 550 | statusCode = 200; 551 | } else { 552 | content = "Error: missing required fields."; 553 | statusCode = 404; 554 | Serial.println("Sending 404"); 555 | } 556 | delay(DELAY_500); 557 | server.sendHeader("Access-Control-Allow-Origin", "*"); 558 | server.send(statusCode, "text/plain", content); 559 | delay(DELAY_500); 560 | 561 | // Write to LittleFS 562 | Serial.println(F("Saving setup.json")); 563 | File jsonFile = LittleFS.open("/setup.json", FILE_WRITE); 564 | if (!jsonFile) { 565 | Serial.println("Failed to open [setup.json] file for writing"); 566 | } else { 567 | serializeJsonPretty(doc, Serial); 568 | serializeJson(doc, jsonFile); 569 | jsonFile.close(); 570 | Serial.println("[setup.json] written correctly"); 571 | } 572 | delay(DELAY_200); 573 | #if defined(ARDUINO_ARCH_ESP32) 574 | ESP.restart(); 575 | #elif defined(ESP8266) 576 | EspClass::restart(); 577 | #endif 578 | }); 579 | } 580 | } 581 | 582 | bool WifiManager::isConnected() { 583 | return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED); 584 | } 585 | 586 | void WifiManager::sendImprovStateResponse(uint8_t state, bool error) { 587 | if (!error && improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); 588 | if (error) improvError = state; 589 | char out[11] = {'I', 'M', 'P', 'R', 'O', 'V'}; 590 | out[6] = IMPROV_VERSION; 591 | out[7] = error ? ImprovPacketType::Error_State : ImprovPacketType::Current_State; 592 | out[8] = 1; 593 | out[9] = state; 594 | uint8_t checksum = 0; 595 | for (uint8_t i = 0; i < 10; i++) checksum += out[i]; 596 | out[10] = checksum; 597 | Serial.write((uint8_t *) out, 11); 598 | Serial.write('\n'); 599 | Serial.flush(); 600 | } 601 | 602 | void WifiManager::sendImprovRPCResponse(byte commandId) { 603 | sendImprovRPCResponse(commandId, false); 604 | } 605 | 606 | void WifiManager::sendImprovRPCResponse(byte commandId, bool forceConnection) { 607 | if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); 608 | uint8_t packetLen = 12; 609 | char out[64] = {'I', 'M', 'P', 'R', 'O', 'V'}; 610 | out[6] = IMPROV_VERSION; 611 | out[7] = ImprovPacketType::RPC_Response; 612 | out[8] = 2; //Length (set below) 613 | out[9] = commandId; 614 | out[10] = 0; //Data len (set below) 615 | out[11] = '\0'; //URL len (set below) 616 | if (isConnected() || forceConnection) { 617 | IPAddress localIP = WiFi.localIP(); 618 | uint8_t len = sprintf(out + 12, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); 619 | if (len > 24) return; //sprintf fail 620 | out[11] = len; 621 | out[10] = 1 + len; 622 | out[8] = 3 + len; //RPC command type + data len + url len + url 623 | packetLen = 13 + len; 624 | } 625 | uint8_t checksum = 0; 626 | for (uint8_t i = 0; i < packetLen - 1; i++) checksum += out[i]; 627 | out[packetLen - 1] = checksum; 628 | Serial.write((uint8_t *) out, packetLen); 629 | Serial.write('\n'); 630 | Serial.flush(); 631 | improvActive = 1; //no longer provisioning 632 | } 633 | 634 | void WifiManager::sendImprovInfoResponse() { 635 | if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); 636 | uint8_t packetLen = 12; 637 | char out[128] = {'I', 'M', 'P', 'R', 'O', 'V'}; 638 | out[6] = IMPROV_VERSION; 639 | out[7] = ImprovPacketType::RPC_Response; 640 | //out[8] = 2; //Length (set below) 641 | out[9] = ImprovRPCType::Request_Info; 642 | //out[10] = 0; //Data len (set below) 643 | out[11] = 4; //Firmware len ("FIRM") 644 | out[12] = 'F'; 645 | out[13] = 'I'; 646 | out[14] = 'R'; 647 | out[15] = 'M'; 648 | uint8_t lengthSum = 17; 649 | uint8_t vlen = sprintf_P(out + lengthSum, firmwareVersion.c_str()); 650 | out[16] = vlen; 651 | lengthSum += vlen; 652 | uint8_t hlen = 7; 653 | #ifdef ESP8266 654 | strcpy(out + lengthSum + 1, "esp8266"); 655 | #else 656 | hlen = 5; 657 | strcpy(out + lengthSum + 1, "esp32"); 658 | #endif 659 | out[lengthSum] = hlen; 660 | lengthSum += hlen + 1; 661 | //Use serverDescription if it has been changed from the default "FIRM", else mDNS name 662 | bool useMdnsName = (strcmp(serverDescription, "FIRM") == 0 && strlen(cmDNS) > 0); 663 | strcpy(out + lengthSum + 1, useMdnsName ? cmDNS : serverDescription); 664 | uint8_t nlen = strlen(useMdnsName ? cmDNS : serverDescription); 665 | out[lengthSum] = nlen; 666 | lengthSum += nlen + 1; 667 | packetLen = lengthSum + 1; 668 | out[8] = lengthSum - 9; 669 | out[10] = lengthSum - 11; 670 | uint8_t checksum = 0; 671 | for (uint8_t i = 0; i < packetLen - 1; i++) checksum += out[i]; 672 | out[packetLen - 1] = checksum; 673 | Serial.write((uint8_t *) out, packetLen); 674 | Serial.write('\n'); 675 | Serial.flush(); 676 | DIMPROV_PRINT("Info checksum"); 677 | DIMPROV_PRINTLN(checksum); 678 | } 679 | 680 | void WifiManager::parseWiFiCommand(char *rpcData) { 681 | uint8_t len = rpcData[0]; 682 | if (!len || len > 126) return; 683 | uint8_t ssidLen = rpcData[1]; 684 | if (ssidLen > len - 1 || ssidLen > 32) return; 685 | memset(clientSSID, 0, 32); 686 | memcpy(clientSSID, rpcData + 2, ssidLen); 687 | memset(clientPass, 0, 64); 688 | if (len > ssidLen + 1) { 689 | uint8_t passLen = rpcData[2 + ssidLen]; 690 | memset(clientPass, 0, 64); 691 | memcpy(clientPass, rpcData + 3 + ssidLen, passLen); 692 | } 693 | sendImprovStateResponse(0x03, false); //provisioning 694 | improvActive = 2; 695 | JsonDocument doc; 696 | String devName = String(random(0, 90000)); 697 | doc["deviceName"] = String(DEVICE_NAME) + "_" + devName; 698 | doc["microcontrollerIP"] = "DHCP"; 699 | doc["qsid"] = clientSSID; 700 | doc["qpass"] = clientPass; 701 | doc["OTApass"] = ""; 702 | doc["mqttIP"] = ""; 703 | doc["mqttPort"] = ""; 704 | doc["mqttuser"] = ""; 705 | doc["mqttpass"] = ""; 706 | additionalParam = "2"; 707 | Serial.println(F("Saving setup.json")); 708 | File jsonFile = LittleFS.open("/setup.json", FILE_WRITE); 709 | if (!jsonFile) { 710 | Serial.println("Failed to open [setup.json] file for writing"); 711 | } else { 712 | serializeJsonPretty(doc, Serial); 713 | serializeJson(doc, jsonFile); 714 | jsonFile.close(); 715 | } 716 | delay(DELAY_200); 717 | sendImprovRPCResponse(ImprovRPCType::Request_State); 718 | delay(DELAY_200); 719 | sendImprovStateResponse(0x04, false); 720 | delay(DELAY_200); 721 | #if defined(ARDUINO_ARCH_ESP32) 722 | ESP.restart(); 723 | #elif defined(ESP8266) 724 | EspClass::restart(); 725 | #endif 726 | } 727 | 728 | //blocking function to parse an Improv Serial packet 729 | void WifiManager::handleImprovPacket() { 730 | uint8_t header[6] = {'I', 'M', 'P', 'R', 'O', 'V'}; 731 | bool timeout = false; 732 | uint16_t packetByte = 0; 733 | uint8_t packetLen = 9; 734 | uint8_t checksum = 0; 735 | uint8_t waitTime = 255; 736 | uint8_t rpcCommandType = 0; 737 | char rpcData[128]; 738 | rpcData[0] = 0; 739 | while (!timeout) { 740 | if (Serial.available() < 1) { 741 | delay(1); 742 | waitTime--; 743 | if (!waitTime) timeout = true; 744 | continue; 745 | } 746 | byte next = Serial.read(); 747 | DIMPROV_PRINT("Received improv byte: "); DIMPROV_PRINTF("%x\r\n", next); 748 | switch (packetByte) { 749 | case ImprovPacketByte::Version: { 750 | if (next != IMPROV_VERSION) { 751 | DIMPROV_PRINTLN(F("Invalid version")); 752 | return; 753 | } 754 | break; 755 | } 756 | case ImprovPacketByte::PacketType: { 757 | if (next != ImprovPacketType::RPC_Command) { 758 | DIMPROV_PRINTF("Non RPC-command improv packet type %i\n", next); 759 | return; 760 | } 761 | if (!improvActive) { 762 | improvActive = 1; 763 | improvePacketReceived = true; 764 | } 765 | break; 766 | } 767 | case ImprovPacketByte::Length: 768 | packetLen = 9 + next; 769 | break; 770 | case ImprovPacketByte::RPC_CommandType: 771 | rpcCommandType = next; 772 | break; 773 | default: { 774 | if (packetByte >= packetLen) { //end of packet, check checksum match 775 | if (checksum != next) { 776 | DIMPROV_PRINTF("Got RPC checksum %i, expected %i", next, checksum); 777 | sendImprovStateResponse(0x01, true); 778 | return; 779 | } 780 | switch (rpcCommandType) { 781 | case ImprovRPCType::Command_Wifi: 782 | parseWiFiCommand(rpcData); 783 | break; 784 | case ImprovRPCType::Request_State: { 785 | uint8_t improvState = 0x02; //authorized 786 | if (isWifiConfigured()) improvState = 0x03; //provisioning 787 | if (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) improvState = 0x04; //provisioned 788 | sendImprovStateResponse(improvState, false); 789 | if (improvState == 0x04) sendImprovRPCResponse(ImprovRPCType::Request_State); 790 | break; 791 | } 792 | case ImprovRPCType::Request_Info: 793 | sendImprovInfoResponse(); 794 | break; 795 | default: { 796 | DIMPROV_PRINTF("Unknown RPC command %i\n", next); 797 | sendImprovStateResponse(0x02, true); 798 | } 799 | } 800 | return; 801 | } 802 | if (packetByte < 6) { //check header 803 | if (next != header[packetByte]) { 804 | DIMPROV_PRINTLN(F("Invalid improv header")); 805 | return; 806 | } 807 | } else if (packetByte > 9) { //RPC data 808 | rpcData[packetByte - 10] = next; 809 | if (packetByte > 137) return; //prevent buffer overflow 810 | } 811 | } 812 | } 813 | checksum += next; 814 | packetByte++; 815 | } 816 | } 817 | 818 | 819 | -------------------------------------------------------------------------------- /src/WifiManager.h: -------------------------------------------------------------------------------- 1 | /* 2 | WifiManager.h - Managing Wifi and OTA 3 | 4 | Copyright © 2020 - 2025 Davide Perini 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | You should have received a copy of the MIT License along with this program. 17 | If not, see . 18 | */ 19 | 20 | #ifndef _DPSOFTWARE_WIFI_MANAGER_H 21 | #define _DPSOFTWARE_WIFI_MANAGER_H 22 | 23 | #if defined(ESP8266) 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #elif defined(ARDUINO_ARCH_ESP32) 30 | 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #endif 38 | 39 | #include 40 | #include 41 | #include 42 | #include "Helpers.h" 43 | #include "Secrets.h" 44 | #include "Configuration.h" 45 | 46 | //Establishing Local server at port 80 whenever required 47 | #if defined(ESP8266) 48 | extern ESP8266WebServer server; 49 | #elif defined(ARDUINO_ARCH_ESP32) 50 | extern WebServer server; 51 | #endif 52 | // WiFi Client 53 | extern WiFiClient espClient; 54 | // WebServer content 55 | extern String content; 56 | // WebServer status code 57 | extern int statusCode; 58 | // WebServer HTML frontend 59 | extern String htmlString; 60 | 61 | #ifdef WLED_DEBUG_IMPROV 62 | #define DIMPROV_PRINT(x) Serial.print(x) 63 | #define DIMPROV_PRINTLN(x) Serial.println(x) 64 | #define DIMPROV_PRINTF(x...) Serial.printf(x) 65 | #else 66 | #define DIMPROV_PRINT(x) 67 | #define DIMPROV_PRINTLN(x) 68 | #define DIMPROV_PRINTF(x...) 69 | #endif 70 | #define IMPROV_VERSION 1 71 | 72 | [[maybe_unused]] void parseWiFiCommand(char *rpcData); 73 | 74 | enum ImprovPacketType { 75 | Current_State = 0x01, 76 | Error_State = 0x02, 77 | RPC_Command = 0x03, 78 | RPC_Response = 0x04 79 | }; 80 | 81 | enum ImprovPacketByte { 82 | Version = 6, 83 | PacketType = 7, 84 | Length = 8, 85 | RPC_CommandType = 9 86 | }; 87 | 88 | enum ImprovRPCType { 89 | Command_Wifi = 0x01, 90 | Request_State = 0x02, 91 | Request_Info = 0x03 92 | }; 93 | 94 | extern byte improvActive; //0: no improv packet received, 1: improv active, 2: provisioning 95 | extern byte improvError; 96 | extern char serverDescription[33]; 97 | extern char cmDNS[33]; 98 | extern char clientSSID[33]; 99 | extern char clientPass[65]; 100 | 101 | class WifiManager { 102 | 103 | private: 104 | Helpers helper; 105 | 106 | static void createWebServer(); 107 | 108 | static void setupAP(); 109 | 110 | static void launchWeb(); 111 | 112 | public: 113 | void setupWiFi(void (*manageDisconnections)(), void (*manageHardwareButton)()); 114 | 115 | void reconnectToWiFi(void (*manageDisconnections)(), void (*manageHardwareButton)()); 116 | 117 | static void setupOTAUpload(); 118 | 119 | static int getQuality(); 120 | 121 | static bool isWifiConfigured(); // check if wifi is correctly configured 122 | static void launchWebServerForOTAConfig(); // if no ssid available, launch web server to get config params via browser 123 | static void launchWebServerCustom(void (*listener)()); // if no ssid available, launch web server to get config params via browser 124 | void manageImprovWifi(); // if no ssid available, launch web server to get config params via browser 125 | void handleImprovPacket(); 126 | 127 | void sendImprovInfoResponse(); 128 | 129 | void parseWiFiCommand(char *rpcData); 130 | 131 | void sendImprovRPCResponse(byte commandId); 132 | 133 | void sendImprovRPCResponse(byte commandId, bool forceConnection); 134 | 135 | void sendImprovStateResponse(uint8_t state, bool error); 136 | 137 | static bool isConnected(); // return true if wifi is connected 138 | 139 | void setTxPower() const; 140 | }; 141 | 142 | #endif --------------------------------------------------------------------------------