├── LICENSE ├── README.md ├── WiFiSettings.cpp ├── WiFiSettings.h ├── WiFiSettings_strings.h ├── examples ├── Advanced │ └── Advanced.ino ├── ArduinoOTA │ └── ArduinoOTA.ino └── Basic │ └── Basic.ino ├── keywords.txt ├── library.json ├── library.properties └── screenshots ├── advanced-example.png ├── basic-example.png ├── full.png ├── pwuts-climatenode.png └── snuffelding.png /LICENSE: -------------------------------------------------------------------------------- 1 | Pick your favourite OSI approved license :) 2 | 3 | http://www.opensource.org/licenses/alphabetical 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WiFi configuration manager for the ESP32 and ESP8266 platforms in the Arduino framework 2 | 3 |

4 | Screenshot of basic example 5 | Screenshot of advanced example 6 | Screenshot of everything 7 |

8 | 9 | 10 | * [WiFi configuration manager for the ESP32 and ESP8266 platforms in the Arduino framework](#wifi-configuration-manager-for-the-esp32-and-esp8266-platforms-in-the-arduino-framework) 11 | * [Description](#description) 12 | * [Examples](#examples) 13 | * [Minimal usage](#minimal-usage) 14 | * [Callbacks and custom variables](#callbacks-and-custom-variables) 15 | * [Other examples](#other-examples) 16 | * [Note for ESP8266 users](#note-for-esp8266-users) 17 | * [Installing](#installing) 18 | * [Reference](#reference) 19 | * [Functions](#functions) 20 | * [WiFiSettings.connect([...])](#wifisettingsconnect) 21 | * [WiFiSettings.portal()](#wifisettingsportal) 22 | * [WiFiSettings.integer(...)](#wifisettingsinteger) 23 | * [WiFiSettings.string(...)](#wifisettingsstring) 24 | * [WiFiSettings.checkbox(...)](#wifisettingscheckbox) 25 | * [WiFiSettings.html(...)](#wifisettingshtml) 26 | * [WiFiSettings.heading(...)](#wifisettingsheading) 27 | * [WiFiSettings.warning(...)](#wifisettingswarning) 28 | * [WiFiSettings.info(...)](#wifisettingsinfo) 29 | * [Variables](#variables) 30 | * [WiFiSettings.hostname](#wifisettingshostname) 31 | * [WiFiSettings.password](#wifisettingspassword) 32 | * [WiFiSettings.ssid](#wifisettingsssid) 33 | * [WiFiSettings.secure](#wifisettingssecure) 34 | * [WiFiSettings.language](#wifisettingslanguage) 35 | * [WiFiSettings.on*](#wifisettingson) 36 | * [History](#history) 37 | * [A note about Hyrum's Law](#a-note-about-hyrums-law) 38 | 39 | 40 | 41 | 42 | 43 | ## Description 44 | 45 | This is a very simple, and somewhat naive, WiFi configuration manager for 46 | ESP32 and ESP8266 programs written in the Arduino framework. It will allow you 47 | to configure your WiFi network name (SSID) and password via a captive portal: 48 | the ESP becomes an access point with a web based configuration page. 49 | 50 | It was written for ease of use, not for extended functionality. For example, 51 | restarting the microcontroller is the only way to leave the configuration 52 | portal. A button to restart is provided in the web interface. 53 | 54 | The library generates a random password to protect the portal with, but 55 | it's only secured if you choose to do so by checking a checkbox. Of course, 56 | the user can configure pick their own password. 57 | 58 | The configuration is stored in files in the flash filesystem of the ESP. The 59 | files are dumped in the root directory of the filesystem. Debug output 60 | (including the password to the configuration portal) is written to `Serial`. 61 | 62 | Only automatic IP address assignment (DHCP) is supported. 63 | 64 | ## Examples 65 | 66 | ### Minimal usage 67 | 68 | ```C++ 69 | #include 70 | #include 71 | 72 | void setup() { 73 | Serial.begin(115200); 74 | SPIFFS.begin(true); // On first run, will format after failing to mount 75 | 76 | WiFiSettings.connect(); 77 | } 78 | 79 | void loop() { 80 | ... 81 | } 82 | ``` 83 | 84 | ### Callbacks and custom variables 85 | 86 | ```C++ 87 | void setup() { 88 | Serial.begin(115200); 89 | SPIFFS.begin(true); // On first run, will format after failing to mount 90 | 91 | // Note that these examples call functions that you probably don't have. 92 | WiFiSettings.onSuccess = []() { green(); }; 93 | WiFiSettings.onFailure = []() { red(); }; 94 | WiFiSettings.onWaitLoop = []() { blue(); return 30; }; // delay 30 ms 95 | WiFiSettings.onPortalWaitLoop = []() { blink(); }; 96 | 97 | String host = WiFiSettings.string( "server_host", "default.example.org"); 98 | int port = WiFiSettings.integer("server_port", 0, 65535, 443); 99 | 100 | WiFiSettings.connect(true, 30); 101 | } 102 | ``` 103 | 104 | ### Other examples 105 | 106 | * The [ArduinoOTA example](examples/ArduinoOTA/ArduinoOTA.ino) shows how to 107 | enable over-the-air uploads in the WiFiSettings configuration portal. If you 108 | use the password from WiFiSettings as your OTA password, you no longer have 109 | to hard code it! 110 | 111 | ### Note for ESP8266 users 112 | 113 | The examples are written for ESP32. To use them with the older ESP8266 chip, 114 | note that in the ESP8266 world, SPIFFS is deprecated and replaced by LittleFS. 115 | 116 | WifiSettings uses SPIFFS on ESP32, and LittleFS on ESP8266. 117 | 118 | Simply change both occurrences of `SPIFFS` to `LittleFS`, and remove `true` in 119 | the call to `LittleFS.begin();`. LittleFS will format the filesystem by 120 | default. 121 | 122 | ## Installing 123 | 124 | Automated installation: 125 | * [Instructions for Arduino Library Manager](https://www.arduino.cc/en/guide/libraries) 126 | * [Instructions for PlatformIO Library Manager](https://platformio.org/lib/show/7251/esp32-WiFiSettings/installation) 127 | 128 | Getting the source for manual installation: 129 | * `git clone https://github.com/Juerd/ESP-WiFiSettings` 130 | * [.zip and .tar.gz files](https://github.com/Juerd/ESP-WiFiSettings/releases) 131 | 132 | ## Reference 133 | 134 | This library uses a singleton instance (object), `WiFiSettings`, and is not 135 | designed to be inherited from (subclassed), or to have multiple instances. 136 | 137 | ### Functions 138 | 139 | #### WiFiSettings.connect([...]) 140 | 141 | ```C++ 142 | bool connect(bool portal = true, int wait_seconds = 30); 143 | ``` 144 | 145 | If no WiFi network is configured yet, starts the configuration portal. 146 | In other cases, it will attempt to connect to the network in station (WiFi 147 | client) mode, and wait until either a connection is established, or 148 | `wait_seconds` has elapsed. Returns `true` if connection succeeded. 149 | 150 | By default, a failed connection (no connection established within the timeout) 151 | will cause the configuration portal to be started. Given `portal = false`, it 152 | will instead return `false`. 153 | 154 | To wait forever until WiFi is connected, use `wait_seconds = -1`. In this case, 155 | the value of `portal` is ignored. 156 | 157 | Calls the following callbacks: 158 | 159 | * WiFiSettings.onConnect 160 | * WiFiSettings.onWaitLoop -> int (milliseconds to wait) 161 | * WiFiSettings.onSuccess 162 | * WiFiSettings.onFailure 163 | 164 | #### WiFiSettings.portal() 165 | 166 | ```C++ 167 | void portal(); 168 | ``` 169 | 170 | Disconnects any active WiFi and turns the ESP into a captive portal with a 171 | DNS server that works on every hostname. 172 | 173 | Normally, this function is called by `.connect()`. To allow reconfiguration 174 | after the initial configuration, you could call `.portal()` manually, for 175 | example when a button is pressed during startup. 176 | 177 | This function never ends. A restart is required to resume normal operation. 178 | 179 | Calls the following callbacks: 180 | 181 | * WiFiSettings.onPortal 182 | * WiFiSettings.onPortalWaitLoop 183 | * WiFiSettings.onPortalView 184 | * WiFiSettings.onUserAgent(String& ua) 185 | * WiFiSettings.onConfigSaved 186 | * WiFiSettings.onRestart 187 | 188 | #### WiFiSettings.integer(...) 189 | #### WiFiSettings.string(...) 190 | #### WiFiSettings.checkbox(...) 191 | 192 | ```C++ 193 | int integer(String name, [long min, long max,] int init = 0, String label = name); 194 | String string(String name, [[unsigned int min_length,] unsigned int max_length,] String init = "", String label = name); 195 | bool checkbox(String name, bool init = false, String label = name); 196 | ``` 197 | 198 | Configures a custom configurable option and returns the current value. When no 199 | value (or an empty string) is configured, the value given as `init` is returned. 200 | 201 | These functions should be called *before* calling `.connect()` or `.portal()`. 202 | 203 | The `name` is used as the filename in the SPIFFS, and as an HTML form element 204 | name, and must be valid in both of those contexts. Any given `name` should only 205 | be used once! 206 | 207 | It is strongly suggested to include the name of a project in the `name` of the 208 | configuration option, if it is specific to that project. For example, an MQTT 209 | topic is probably specific to the application, while the server hostname 210 | is likely to be shared among several projects. This helps when the ESP is 211 | later reused for different applications. 212 | 213 | Optionally, `label` can be specified as a descriptive text to use on the 214 | configuration portal. 215 | 216 | Some restrictions for the values can be given. Note that these limitations are 217 | implemented on the client side, and may not be respected by browsers. For 218 | integers, a range can be specified by supplying both `min` and `max`. For 219 | strings, a maximum length can be specified as `max_length`. A minimum string 220 | length can be set with `min_length`, effectively making the field mandatory: 221 | it can no longer be left empty to get the `init` value. 222 | 223 | #### WiFiSettings.html(...) 224 | #### WiFiSettings.heading(...) 225 | #### WiFiSettings.warning(...) 226 | #### WiFiSettings.info(...) 227 | 228 | ```C++ 229 | void html(String tag, String contents, bool escape = true); 230 | void heading(String contents, bool escape = true); 231 | void warning(String contents, bool escape = true); 232 | void info(String contents, bool escape = true); 233 | ``` 234 | 235 | Mix in custom text or HTML fragments, such as headings, warning texts, or info 236 | texts. 237 | 238 | Custom HTML can be specified with the `html` function, which takes a tag (e.g. 239 | `"p"`) or a tag with attributes (e.g. `"p align=right"`) as the first argument. 240 | Only tags that take a closing tag should be used. The other functions are 241 | provided for convenience. 242 | 243 | The contents are safely escaped by default, but raw HTML can be added by 244 | providing `false` as the last argument, in which case the contents are added to 245 | the page without any verification or modification. Consider the security 246 | implications of using unescaped data from external sources. 247 | 248 | ### Variables 249 | 250 | Note: because of the way this library is designed, any assignment to the 251 | member variables should be done *before* calling any of the functions. 252 | 253 | #### WiFiSettings.hostname 254 | 255 | ```C++ 256 | String 257 | ``` 258 | 259 | Name to use as the hostname and SSID for the access point. By default, this is 260 | set to "esp32-" or "esp8266-", depending on the platform. 261 | 262 | If it ends in a `-` character, a unique 6 digit device identifier 263 | (specifically, the hexadecimal representation of the device interface specific 264 | part of the ESP's MAC address, in reverse byte order) is added automatically. 265 | This is highly recommended. 266 | 267 | Use only ASCII digits and letters. ASCII hyphens (`-`) can only be used in 268 | between other characters (i.e. not two in a row, and not as the first 269 | character). Most characters, including underscores (`_`) and spaces, are not 270 | valid in hostnames. 271 | 272 | This variable may be read any time after any other function is called to obtain 273 | for example a unique device id for MQTT's client-id. 274 | 275 | #### WiFiSettings.password 276 | 277 | ```C++ 278 | String 279 | ``` 280 | 281 | This variable is used to protect the configuration portal's softAP. When no 282 | password is explicitly assigned before the first custom configuration parameter 283 | is defined (with `.string`, `.integer`, or `.checkbox`), a password will be 284 | automatically generated and can be configured by the user. 285 | 286 | It's strongly recommended to leave this variable untouched, and use the 287 | built-in password generation feature, and letting the user configure their own 288 | password, instead of "hard coding" a password. 289 | 290 | The password has no effect unless the portal is secured; see `.secure`. 291 | 292 | #### WiFiSettings.ssid 293 | 294 | ```C++ 295 | String 296 | ``` 297 | 298 | This variable is used to expose the SSID. This variable can be used 299 | to, for example, have a callback that rescans and restarts the device if the 300 | prefered WiFi appears. 301 | 302 | #### WiFiSettings.secure 303 | 304 | ```C++ 305 | bool 306 | ``` 307 | 308 | By setting this to `true`, before any custom configuration parameter is defined 309 | with `.string`, `.integer`, or `.checkbox`, secure mode will be forced, instead 310 | of the default behavior, which is to initially use an insecure softAP and to 311 | let the user decide whether to secure it. 312 | 313 | When `.secure` is left in the default state, `false`, the user setting will be 314 | used. 315 | 316 | When forcing secure mode, it is still recommended to leave `.password` unset so 317 | that a password is automatically generated, if you have a way to communicate it 318 | to the user, for example with an LCD display, or 319 | `Serial.println(WiFiSettings.password);`. Having hard coded password literals 320 | in source code is generally considered a bad idea, because that makes it harder 321 | to share the code with others. 322 | 323 | #### WiFiSettings.language 324 | 325 | ```C++ 326 | String 327 | ``` 328 | 329 | The language to be used in the WiFiSettings portal. Currently supported are 330 | `en` and `nl`. Once the user has picked a language in the portal, the user 331 | setting overrides any value previously assigned. This variable is updated to 332 | reflect the currently selected language. 333 | 334 | By default, all available languages are available. To conserve flash storage 335 | space, it is possible to select only specific languages, by specifying build 336 | flags such as `LANGUAGE_EN`. If only a single language is defined, this 337 | language will be used regardless of any configuration, and no language 338 | drop-down is presented to the user. Note: build flags are not available in the 339 | Arduino IDE, but can be specified in Arduino board files. In PlatformIO, build 340 | flags can be specified in the `[env]` section, e.g. `build_flags = 341 | -DLANGUAGE_EN`. 342 | 343 | *If you wish to contribute a translation, please refer to 344 | `WiFiSettings_strings.h`. (Note: due to storage constraints on microcontroller 345 | flash filesystems, only widely used natural languages will be included.)* 346 | 347 | #### WiFiSettings.on* 348 | 349 | The callback functions are mentioned in the documentation for the respective 350 | functions that call them. 351 | 352 | ## History 353 | 354 | Note that this library was briefly named WiFiConfig, but was renamed to 355 | WiFiSettings because there was already another library called 356 | [WiFiConfig](https://github.com/snakeye/WifiConfig). 357 | 358 | From version 3.0.0, based on a contribution by Reinier van der Leer, this 359 | library also supports the older ESP8266, and the repository was renamed from 360 | esp32-WiFiSettings to ESP-WiFiSettings. 361 | 362 | ## A note about Hyrum's Law 363 | 364 | It is said that *all observable behaviors of your system will be depended on by 365 | somebody*, and you are of course free to explore the source and use any 366 | unintended feature you may find to your advantage. Bear in mind, however, that 367 | depending on any behavior that is not documented here, is more likely to cause 368 | breakage when you install a newer version of this library. The author feels no 369 | obligation to keep backwards compatibility with undocumented features :) 370 | -------------------------------------------------------------------------------- /WiFiSettings.cpp: -------------------------------------------------------------------------------- 1 | #include "WiFiSettings.h" 2 | #ifdef ESP32 3 | #define ESPFS SPIFFS 4 | #define ESPMAC (Sprintf("%06" PRIx64, ESP.getEfuseMac() >> 24)) 5 | #include 6 | #include 7 | #include 8 | #include 9 | #elif ESP8266 10 | #define ESPFS LittleFS 11 | #define ESPMAC (Sprintf("%06" PRIx32, ESP.getChipId())) 12 | #include 13 | #include 14 | #include 15 | #define WebServer ESP8266WebServer 16 | #define esp_task_wdt_reset wdt_reset 17 | #define wifi_auth_mode_t uint8_t // wl_enc_type 18 | #define WIFI_AUTH_OPEN ENC_TYPE_NONE 19 | constexpr auto WIFI_AUTH_WPA2_ENTERPRISE = -1337; // not available on ESP8266 20 | #define setHostname hostname 21 | #define INADDR_NONE IPAddress(0,0,0,0) 22 | #else 23 | #error "This library only supports ESP32 and ESP8266" 24 | #endif 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | WiFiSettingsLanguage::Texts _WSL_T; 31 | 32 | #define Sprintf(f, ...) ({ char* s; asprintf(&s, f, __VA_ARGS__); String r = s; free(s); r; }) 33 | 34 | namespace { // Helpers 35 | String slurp(const String& fn) { 36 | File f = ESPFS.open(fn, "r"); 37 | String r = f.readString(); 38 | f.close(); 39 | return r; 40 | } 41 | 42 | bool spurt(const String& fn, const String& content) { 43 | File f = ESPFS.open(fn, "w"); 44 | if (!f) return false; 45 | auto w = f.print(content); 46 | f.close(); 47 | return w == content.length(); 48 | } 49 | 50 | String pwgen() { 51 | const char* passchars = "ABCEFGHJKLMNPRSTUXYZabcdefhkmnorstvxz23456789-#@?!"; 52 | String password = ""; 53 | for (int i = 0; i < 16; i++) { 54 | // Note: no seed needed for ESP8266 and ESP32 hardware RNG 55 | password.concat( passchars[random(strlen(passchars))] ); 56 | } 57 | return password; 58 | } 59 | 60 | String html_entities(const String& raw) { 61 | String r; 62 | for (unsigned int i = 0; i < raw.length(); i++) { 63 | char c = raw.charAt(i); 64 | if (c < '!' || c == '"' || c == '&' || c == '\'' || c == '<' || c == '>' || c == 0x7f) { 65 | // ascii control characters, html syntax characters, and space 66 | r += Sprintf("&#%d;", c); 67 | } else { 68 | r += c; 69 | } 70 | } 71 | return r; 72 | } 73 | 74 | struct WiFiSettingsParameter { 75 | String name; 76 | String label; 77 | String value; 78 | String init; 79 | long min = LONG_MIN; 80 | long max = LONG_MAX; 81 | 82 | String filename() { String fn = "/"; fn += name; return fn; } 83 | bool store() { return (name && name.length())? spurt(filename(), value): true; } 84 | void fill() { if (name && name.length()) value = slurp(filename()); } 85 | virtual void set(const String&) = 0; 86 | virtual String html() = 0; 87 | }; 88 | 89 | struct WiFiSettingsString : WiFiSettingsParameter { 90 | virtual void set(const String& v) { value = v; } 91 | String html() { 92 | String h = F("

"); 93 | h.replace("{name}", html_entities(name)); 94 | h.replace("{value}", html_entities(value)); 95 | h.replace("{init}", html_entities(init)); 96 | h.replace("{label}", html_entities(label)); 97 | h.replace("{min}", String(min)); 98 | h.replace("{max}", String(max)); 99 | return h; 100 | } 101 | }; 102 | 103 | struct WiFiSettingsInt : WiFiSettingsParameter { 104 | virtual void set(const String& v) { value = v; } 105 | String html() { 106 | String h = F("

"); 107 | h.replace("{name}", html_entities(name)); 108 | h.replace("{value}", html_entities(value)); 109 | h.replace("{init}", html_entities(init)); 110 | h.replace("{label}", html_entities(label)); 111 | h.replace("{min}", String(min)); 112 | h.replace("{max}", String(max)); 113 | return h; 114 | } 115 | }; 116 | 117 | struct WiFiSettingsBool : WiFiSettingsParameter { 118 | virtual void set(const String& v) { value = v.length() ? "1" : "0"; } 119 | String html() { 120 | String h = F("

"); 121 | h.replace("{name}", html_entities(name)); 122 | h.replace("{default}", _WSL_T.init); 123 | h.replace("{checked}", value.toInt() ? " checked" : ""); 124 | h.replace("{init}", init.toInt() ? "☑" : "☐"); 125 | h.replace("{label}", html_entities(label)); 126 | return h; 127 | } 128 | }; 129 | 130 | struct WiFiSettingsHTML : WiFiSettingsParameter { 131 | // Raw HTML, not an actual parameter. The reason for the "if (name)" 132 | // in store and fill. Abuses several member variables for completely 133 | // different functionality. 134 | 135 | virtual void set(const String& v) { (void)v; } 136 | String html() { 137 | int space = value.indexOf(" "); 138 | 139 | String h = 140 | (value ? "<" + value + ">" : "") 141 | + 142 | (min ? html_entities(label) : label) 143 | + 144 | (value ? "= 0 ? value.substring(0, space) : value) + ">" : ""); 145 | return h; 146 | } 147 | }; 148 | 149 | struct std::vector params; 150 | } 151 | 152 | String WiFiSettingsClass::string(const String& name, const String& init, const String& label) { 153 | begin(); 154 | struct WiFiSettingsString* x = new WiFiSettingsString(); 155 | x->name = name; 156 | x->label = label.length() ? label : name; 157 | x->init = init; 158 | x->fill(); 159 | 160 | params.push_back(x); 161 | return x->value.length() ? x->value : x->init; 162 | } 163 | 164 | String WiFiSettingsClass::string(const String& name, unsigned int max_length, const String& init, const String& label) { 165 | String rv = string(name, init, label); 166 | params.back()->max = max_length; 167 | return rv; 168 | } 169 | 170 | String WiFiSettingsClass::string(const String& name, unsigned int min_length, unsigned int max_length, const String& init, const String& label) { 171 | String rv = string(name, init, label); 172 | params.back()->min = min_length; 173 | params.back()->max = max_length; 174 | return rv; 175 | } 176 | 177 | long WiFiSettingsClass::integer(const String& name, long init, const String& label) { 178 | begin(); 179 | struct WiFiSettingsInt* x = new WiFiSettingsInt(); 180 | x->name = name; 181 | x->label = label.length() ? label : name; 182 | x->init = init; 183 | x->fill(); 184 | 185 | params.push_back(x); 186 | return (x->value.length() ? x->value : x->init).toInt(); 187 | } 188 | 189 | long WiFiSettingsClass::integer(const String& name, long min, long max, long init, const String& label) { 190 | long rv = integer(name, init, label); 191 | params.back()->min = min; 192 | params.back()->max = max; 193 | return rv; 194 | } 195 | 196 | bool WiFiSettingsClass::checkbox(const String& name, bool init, const String& label) { 197 | begin(); 198 | struct WiFiSettingsBool* x = new WiFiSettingsBool(); 199 | x->name = name; 200 | x->label = label.length() ? label : name; 201 | x->init = String((int) init); 202 | x->fill(); 203 | 204 | // Apply default immediately because a checkbox has no placeholder to 205 | // show the default, and other UI elements aren't sufficiently pretty. 206 | if (! x->value.length()) x->value = x->init; 207 | 208 | params.push_back(x); 209 | return x->value.toInt(); 210 | } 211 | 212 | void WiFiSettingsClass::html(const String& tag, const String& contents, bool escape) { 213 | begin(); 214 | struct WiFiSettingsHTML* x = new WiFiSettingsHTML(); 215 | x->value = tag; 216 | x->label = contents; 217 | x->min = escape; 218 | 219 | params.push_back(x); 220 | } 221 | 222 | void WiFiSettingsClass::info(const String& contents, bool escape) { 223 | html(F("p class=i"), contents, escape); 224 | } 225 | 226 | void WiFiSettingsClass::warning(const String& contents, bool escape) { 227 | html(F("p class=w"), contents, escape); 228 | } 229 | 230 | void WiFiSettingsClass::heading(const String& contents, bool escape) { 231 | html("h2", contents, escape); 232 | } 233 | 234 | 235 | void WiFiSettingsClass::portal() { 236 | WebServer http(80); 237 | DNSServer dns; 238 | int num_networks = -1; 239 | begin(); 240 | 241 | #ifdef ESP32 242 | WiFi.disconnect(true, true); // reset state so .scanNetworks() works 243 | #else 244 | WiFi.disconnect(true); 245 | #endif 246 | 247 | Serial.println(F("Starting access point for configuration portal.")); 248 | if (secure && password.length()) { 249 | Serial.printf("SSID: '%s', Password: '%s'\n", hostname.c_str(), password.c_str()); 250 | WiFi.softAP(hostname.c_str(), password.c_str()); 251 | } else { 252 | Serial.printf("SSID: '%s'\n", hostname.c_str()); 253 | WiFi.softAP(hostname.c_str()); 254 | } 255 | delay(500); 256 | dns.setTTL(0); 257 | dns.start(53, "*", WiFi.softAPIP()); 258 | 259 | if (onPortal) onPortal(); 260 | String ip = WiFi.softAPIP().toString(); 261 | Serial.println(ip); 262 | 263 | auto redirect = [&http, &ip]() { 264 | // iPhone doesn't deal well with redirects to http://hostname/ and 265 | // will wait 40 to 60 seconds before succesful retry. Works flawlessly 266 | // with http://ip/ though. 267 | if (http.hostHeader() == ip) return false; 268 | 269 | http.sendHeader("Location", "http://" + ip + "/"); 270 | // Anecdotally, some devices require a non-empty response body 271 | http.send(302, "text/plain", ip); 272 | return true; 273 | }; 274 | 275 | const char* headers[] = {"User-Agent"}; 276 | http.collectHeaders(headers, sizeof(headers) / sizeof(char*)); 277 | 278 | http.on("/", HTTP_GET, [this, &http, &num_networks, &redirect]() { 279 | if (redirect()) return; 280 | 281 | String ua = http.header("User-Agent"); 282 | bool interactive = !ua.startsWith(F("CaptiveNetworkSupport")); 283 | 284 | if (interactive && onPortalView) onPortalView(); 285 | if (onUserAgent) onUserAgent(ua); 286 | 287 | http.setContentLength(CONTENT_LENGTH_UNKNOWN); 288 | http.send(200, "text/html"); 289 | http.sendContent(F("\n")); 290 | http.sendContent(html_entities(hostname)); 291 | http.sendContent(F("" 292 | "" 293 | "" 310 | "

" 311 | )); 312 | http.sendContent(F("

")); 315 | http.sendContent(_WSL_T.title); 316 | http.sendContent(F("

"); 358 | http.sendContent(_WSL_T.rescan); 359 | http.sendContent(F("


")); 365 | 366 | if (WiFiSettingsLanguage::multiple()) { 367 | http.sendContent(F("")); 379 | } 380 | 381 | for (auto& p : params) { 382 | http.sendContent(p->html()); 383 | } 384 | 385 | http.sendContent(F( 386 | "

" 387 | "

")); 391 | }); 392 | 393 | http.on("/", HTTP_POST, [this, &http]() { 394 | bool ok = true; 395 | if (! spurt("/wifi-ssid", http.arg("ssid"))) ok = false; 396 | 397 | if (WiFiSettingsLanguage::multiple()) { 398 | if (! spurt("/WiFiSettings-language", http.arg("language"))) ok = false; 399 | // Don't update immediately, because there is currently 400 | // no mechanism for reloading param strings. 401 | //language = http.arg("language"); 402 | //WiFiSettingsLanguage::select(T, language); 403 | } 404 | 405 | String pw = http.arg("password"); 406 | if (pw != "##**##**##**") { 407 | if (! spurt("/wifi-password", pw)) ok = false; 408 | } 409 | 410 | for (auto& p : params) { 411 | p->set(http.arg(p->name)); 412 | if (! p->store()) ok = false; 413 | } 414 | 415 | if (ok) { 416 | http.sendHeader("Location", "/"); 417 | http.send(302, "text/plain", "ok"); 418 | if (onConfigSaved) onConfigSaved(); 419 | } else { 420 | // Could be missing SPIFFS.begin(), unformatted filesystem, or broken flash. 421 | http.send(500, "text/plain", _WSL_T.error_fs); 422 | } 423 | }); 424 | 425 | http.on("/restart", HTTP_POST, [this, &http]() { 426 | http.send(200, "text/plain", _WSL_T.bye); 427 | if (onRestart) onRestart(); 428 | ESP.restart(); 429 | }); 430 | 431 | http.on("/rescan", HTTP_GET, [this, &http, &num_networks]() { 432 | http.sendHeader("Location", "/"); 433 | http.send(302, "text/plain", _WSL_T.wait); 434 | num_networks = WiFi.scanNetworks(); 435 | }); 436 | 437 | http.onNotFound([this, &http, &redirect]() { 438 | if (redirect()) return; 439 | http.send(404, "text/plain", "404"); 440 | }); 441 | 442 | http.begin(); 443 | 444 | for (;;) { 445 | http.handleClient(); 446 | dns.processNextRequest(); 447 | if (onPortalWaitLoop) onPortalWaitLoop(); 448 | esp_task_wdt_reset(); 449 | delay(1); 450 | } 451 | } 452 | 453 | bool WiFiSettingsClass::connect(bool portal, int wait_seconds) { 454 | begin(); 455 | 456 | ssid = slurp("/wifi-ssid"); 457 | String pw = slurp("/wifi-password"); 458 | if (ssid.length() == 0) { 459 | Serial.println(F("First contact!\n")); 460 | this->portal(); 461 | } 462 | 463 | Serial.print(F("Connecting to WiFi SSID ")); 464 | Serial.print(ssid); 465 | if (onConnect) onConnect(); 466 | 467 | WiFi.setHostname(hostname.c_str()); 468 | WiFi.begin(ssid.c_str(), pw.c_str()); 469 | WiFi.setHostname(hostname.c_str()); 470 | WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); // arduino-esp32 #2537 471 | WiFi.setHostname(hostname.c_str()); 472 | WiFi.mode(WIFI_STA); // arduino-esp32 #6278 473 | WiFi.setHostname(hostname.c_str()); 474 | 475 | unsigned long starttime = millis(); 476 | while (WiFi.status() != WL_CONNECTED && (wait_seconds < 0 || (millis() - starttime) < (unsigned)wait_seconds * 1000)) { 477 | Serial.print("."); 478 | delay(onWaitLoop ? onWaitLoop() : 100); 479 | } 480 | 481 | if (WiFi.status() != WL_CONNECTED) { 482 | Serial.println(F(" failed.")); 483 | if (onFailure) onFailure(); 484 | if (portal) this->portal(); 485 | return false; 486 | } 487 | 488 | Serial.println(WiFi.localIP().toString()); 489 | if (onSuccess) onSuccess(); 490 | return true; 491 | } 492 | 493 | void WiFiSettingsClass::begin() { 494 | if (begun) return; 495 | begun = true; 496 | 497 | // These things can't go in the constructor because the constructor runs 498 | // before ESPFS.begin() 499 | 500 | String user_language = slurp("/WiFiSettings-language"); 501 | user_language.trim(); 502 | if (user_language.length() && WiFiSettingsLanguage::available(user_language)) { 503 | language = user_language; 504 | } 505 | WiFiSettingsLanguage::select(_WSL_T, language); // can update language 506 | 507 | if (!secure) { 508 | secure = checkbox( 509 | F("WiFiSettings-secure"), 510 | false, 511 | _WSL_T.portal_wpa 512 | ); 513 | } 514 | 515 | if (!password.length()) { 516 | password = string( 517 | F("WiFiSettings-password"), 518 | 8, 63, 519 | "", 520 | _WSL_T.portal_password 521 | ); 522 | if (password == "") { 523 | // With regular 'init' semantics, the password would be changed 524 | // all the time. 525 | password = pwgen(); 526 | params.back()->set(password); 527 | params.back()->store(); 528 | } 529 | } 530 | 531 | if (hostname.endsWith("-")) hostname += ESPMAC; 532 | } 533 | 534 | WiFiSettingsClass::WiFiSettingsClass() { 535 | #ifdef ESP32 536 | hostname = F("esp32-"); 537 | #else 538 | hostname = F("esp8266-"); 539 | #endif 540 | 541 | language = "en"; 542 | } 543 | 544 | WiFiSettingsClass WiFiSettings; 545 | -------------------------------------------------------------------------------- /WiFiSettings.h: -------------------------------------------------------------------------------- 1 | #ifndef WiFiSettings_h 2 | #define WiFiSettings_h 3 | 4 | #include 5 | #include 6 | 7 | class WiFiSettingsClass { 8 | public: 9 | typedef std::function TCallback; 10 | typedef std::function TCallbackReturnsInt; 11 | typedef std::function TCallbackString; 12 | 13 | WiFiSettingsClass(); 14 | void begin(); 15 | bool connect(bool portal = true, int wait_seconds = 30); 16 | void portal(); 17 | String string(const String& name, const String& init = "", const String& label = ""); 18 | String string(const String& name, unsigned int max_length, const String& init = "", const String& label = ""); 19 | String string(const String& name, unsigned int min_length, unsigned int max_length, const String& init = "", const String& label = ""); 20 | long integer(const String& name, long init = 0, const String& label = ""); 21 | long integer(const String& name, long min, long max, long init = 0, const String& label = ""); 22 | bool checkbox(const String& name, bool init = false, const String& label = ""); 23 | void html(const String& tag, const String& contents, bool escape = true); 24 | void heading(const String& contents, bool escape = true); 25 | void warning(const String& contents, bool escape = true); 26 | void info(const String& contents, bool escape = true); 27 | 28 | String ssid; 29 | String hostname; 30 | String password; 31 | bool secure; 32 | String language; 33 | 34 | TCallback onConnect; 35 | TCallbackReturnsInt onWaitLoop; 36 | TCallback onSuccess; 37 | TCallback onFailure; 38 | TCallback onPortal; 39 | TCallback onPortalView; 40 | TCallbackString onUserAgent; 41 | TCallback onConfigSaved; 42 | TCallback onRestart; 43 | TCallback onPortalWaitLoop; 44 | private: 45 | bool begun; 46 | }; 47 | 48 | extern WiFiSettingsClass WiFiSettings; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /WiFiSettings_strings.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace WiFiSettingsLanguage { 4 | 5 | struct Texts { 6 | const __FlashStringHelper 7 | *title, 8 | *portal_wpa, 9 | *portal_password, 10 | *wait, 11 | *bye, 12 | *error_fs, 13 | *button_save, 14 | *button_restart, 15 | *scanning_short, 16 | *scanning_long, 17 | *rescan, 18 | *dot1x, 19 | *ssid, 20 | *wifi_password, 21 | *language 22 | ; 23 | const char 24 | *init 25 | ; 26 | }; 27 | 28 | #if \ 29 | !defined LANGUAGE_EN \ 30 | && !defined LANGUAGE_NL \ 31 | && !defined LANGUAGE_DE 32 | #define LANGUAGE_ALL 33 | #endif 34 | 35 | std::map languages { 36 | // Ordered alphabetically 37 | #if defined LANGUAGE_DE || defined LANGUAGE_ALL 38 | { "de", "Deutsch" }, 39 | #endif 40 | #if defined LANGUAGE_EN || defined LANGUAGE_ALL 41 | { "en", "English" }, 42 | #endif 43 | #if defined LANGUAGE_NL || defined LANGUAGE_ALL 44 | { "nl", "Nederlands" }, 45 | #endif 46 | }; 47 | 48 | bool available(const String& language) { 49 | return languages.count(language) == 1; 50 | } 51 | 52 | bool multiple() { 53 | return languages.size() > 1; 54 | } 55 | 56 | bool select(Texts& T, String& language) { 57 | if (! available(language)) { 58 | if (available("en")) language = "en"; 59 | else language = languages.begin()->first; 60 | } 61 | 62 | #if defined LANGUAGE_EN || defined LANGUAGE_ALL 63 | if (language == "en") { 64 | T.title = F("Configuration"); 65 | T.portal_wpa = F("Protect the configuration portal with a WiFi password"); 66 | T.portal_password = F("WiFi password for the configuration portal"); 67 | T.init = "default"; 68 | T.wait = F("Wait for it..."); 69 | T.bye = F("Bye!"); 70 | T.error_fs = F("Error while writing to flash filesystem."); 71 | T.button_save = F("Save"); 72 | T.button_restart = F("Restart device"); 73 | T.scanning_short = F("Scanning..."); 74 | T.scanning_long = F("Scanning for WiFi networks..."); 75 | T.rescan = F("rescan"); 76 | T.dot1x = F("(won't work: 802.1x is not supported)"); 77 | T.ssid = F("WiFi network name (SSID)"); 78 | T.wifi_password = F("WiFi password"); 79 | T.language = F("Language"); 80 | return true; 81 | } 82 | #endif 83 | 84 | #if defined LANGUAGE_NL || defined LANGUAGE_ALL 85 | if (language == "nl") { 86 | T.title = F("Configuratie"); 87 | T.portal_wpa = F("Beveilig de configuratieportal met een WiFi-wachtwoord"); 88 | T.portal_password = F("WiFi-wachtwoord voor de configuratieportal"); 89 | T.init = "standaard"; 90 | T.wait = F("Even wachten..."); 91 | T.bye = F("Doei!"); 92 | T.error_fs = F("Fout bij het schrijven naar het flash-bestandssysteem."); 93 | T.button_save = F("Opslaan"); 94 | T.button_restart = F("Herstarten"); 95 | T.scanning_short = F("Scant..."); 96 | T.scanning_long = F("Zoeken naar WiFi-netwerken..."); 97 | T.rescan = F("opnieuw scannen"); 98 | T.dot1x = F("(werkt niet: 802.1x wordt niet ondersteund)"); 99 | T.ssid = F("WiFi-netwerknaam (SSID)"); 100 | T.wifi_password = F("WiFi-wachtwoord"); 101 | T.language = F("Taal"); 102 | return true; 103 | } 104 | #endif 105 | 106 | #if defined LANGUAGE_DE || defined LANGUAGE_ALL 107 | if (language == "de") { 108 | T.title = F("Konfiguration"); 109 | T.portal_wpa = F("Das Konfigurationsportal mit einem Passwort schützen"); 110 | T.portal_password = F("Passwort für das Konfigurationsportal"); 111 | T.init = "Standard"; 112 | T.wait = F("Warten..."); 113 | T.bye = F("Tschüss!"); 114 | T.error_fs = F("Fehler beim Schreiben auf das Flash-Dateisystem"); 115 | T.button_save = F("Speichern"); 116 | T.button_restart = F("Gerät neustarten"); 117 | T.scanning_short = F("Suchen..."); 118 | T.scanning_long = F("Suche nach WiFi-Netzwerken..."); 119 | T.rescan = F("Erneut suchen"); 120 | T.dot1x = F("(nicht möglich: 802.1x nicht unterstützt)"); 121 | T.ssid = F("WiFi Netzwerkname (SSID)"); 122 | T.wifi_password = F("WiFi Passwort"); 123 | T.language = F("Sprache"); 124 | return true; 125 | } 126 | #endif 127 | 128 | return false; 129 | } 130 | 131 | } // namespace 132 | -------------------------------------------------------------------------------- /examples/Advanced/Advanced.ino: -------------------------------------------------------------------------------- 1 | /* 2 | WifiSettings advanced example 3 | 4 | Demonstrates callback functions and custom variables 5 | to be saved through WifiSettings. 6 | 7 | Source and further documentation available at 8 | https://github.com/Juerd/ESP-WiFiSettings 9 | 10 | Note: this example is written for ESP32. 11 | For ESP8266, use LittleFS.begin() instead of SPIFFS.begin(true). 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | // Status LED 18 | const uint32_t LED_PIN = 2; 19 | #define LED_ON LOW 20 | #define LED_OFF HIGH 21 | 22 | void setup() { 23 | Serial.begin(115200); 24 | SPIFFS.begin(true); // Will format on the first run after failing to mount 25 | 26 | pinMode(LED_PIN, OUTPUT); 27 | 28 | // Set custom callback functions 29 | WiFiSettings.onSuccess = []() { 30 | digitalWrite(LED_PIN, LED_ON); // Turn LED on 31 | }; 32 | WiFiSettings.onFailure = []() { 33 | digitalWrite(LED_PIN, LED_OFF); // Turn LED off 34 | }; 35 | WiFiSettings.onWaitLoop = []() { 36 | digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Toggle LED 37 | return 500; // Delay next function call by 500ms 38 | }; 39 | 40 | // Callback functions do not have to be lambda's, e.g. 41 | // WiFiSettings.onPortalWaitLoop = blink; 42 | 43 | // Define custom settings saved by WifiSettings 44 | // These will return the default if nothing was set before 45 | String host = WiFiSettings.string( "server_host", "default.example.org"); 46 | int port = WiFiSettings.integer("server_port", 443); 47 | 48 | // Connect to WiFi with a timeout of 30 seconds 49 | // Launches the portal if the connection failed 50 | WiFiSettings.connect(true, 30); 51 | } 52 | 53 | void loop() { 54 | // Your loop code here 55 | } 56 | -------------------------------------------------------------------------------- /examples/ArduinoOTA/ArduinoOTA.ino: -------------------------------------------------------------------------------- 1 | /* 2 | WifiSettings Arduino OTA example 3 | 4 | Demonstrates how to run Arduino OTA in tandem with WifiSettings, 5 | using the WifiSettings credentials. 6 | 7 | Source and further documentation available at 8 | https://github.com/Juerd/ESP-WiFiSettings 9 | 10 | Note: this example is written for ESP32. 11 | For ESP8266, use LittleFS.begin() instead of SPIFFS.begin(true). 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | // Start ArduinoOTA via WiFiSettings with the same hostname and password 19 | void setup_ota() { 20 | ArduinoOTA.setHostname(WiFiSettings.hostname.c_str()); 21 | ArduinoOTA.setPassword(WiFiSettings.password.c_str()); 22 | ArduinoOTA.begin(); 23 | } 24 | 25 | void setup() { 26 | Serial.begin(115200); 27 | SPIFFS.begin(true); // Will format on the first run after failing to mount 28 | 29 | // Force WPA secured WiFi for the software access point. 30 | // Because OTA is remote code execution (RCE) by definition, the password 31 | // should be kept secret. By default, WiFiSettings will become an insecure 32 | // WiFi access point and happily tell anyone the password. The password 33 | // will instead be provided on the Serial connection, which is a bit safer. 34 | WiFiSettings.secure = true; 35 | 36 | // Set callbacks to start OTA when the portal is active 37 | WiFiSettings.onPortal = []() { 38 | setup_ota(); 39 | }; 40 | WiFiSettings.onPortalWaitLoop = []() { 41 | ArduinoOTA.handle(); 42 | }; 43 | 44 | // Use stored credentials to connect to your WiFi access point. 45 | // If no credentials are stored or if the access point is out of reach, 46 | // an access point will be started with a captive portal to configure WiFi. 47 | WiFiSettings.connect(); 48 | 49 | Serial.print("Password: "); 50 | Serial.println(WiFiSettings.password); 51 | 52 | setup_ota(); // If you also want the OTA during regular execution 53 | } 54 | 55 | void loop() { 56 | ArduinoOTA.handle(); // If you also want the OTA during regular execution 57 | 58 | // Your loop code here 59 | } 60 | -------------------------------------------------------------------------------- /examples/Basic/Basic.ino: -------------------------------------------------------------------------------- 1 | /* 2 | WifiSettings basic example 3 | 4 | Source and further documentation available at 5 | https://github.com/Juerd/ESP-WiFiSettings 6 | 7 | Note: this example is written for ESP32. 8 | For ESP8266, use LittleFS.begin() instead of SPIFFS.begin(true). 9 | */ 10 | 11 | #include 12 | #include 13 | 14 | void setup() { 15 | Serial.begin(115200); 16 | SPIFFS.begin(true); // Will format on the first run after failing to mount 17 | 18 | // Use stored credentials to connect to your WiFi access point. 19 | // If no credentials are stored or if the access point is out of reach, 20 | // an access point will be started with a captive portal to configure WiFi. 21 | WiFiSettings.connect(); 22 | } 23 | 24 | void loop() { 25 | // Your loop code here 26 | } 27 | -------------------------------------------------------------------------------- /keywords.txt: -------------------------------------------------------------------------------- 1 | WiFiSettings KEYWORD1 2 | connect KEYWORD2 3 | portal KEYWORD2 4 | hostname KEYWORD2 5 | password KEYWORD2 6 | string KEYWORD2 7 | integer KEYWORD2 8 | checkbox KEYWORD2 9 | onConnect KEYWORD2 10 | onWaitLoop KEYWORD2 11 | onSuccess KEYWORD2 12 | onFailure KEYWORD2 13 | onPortal KEYWORD2 14 | onPortalWaitLoop KEYWORD2 15 | onConfigSaved KEYWORD2 16 | onRestart KEYWORD2 17 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ESP-WiFiSettings", 3 | "version": "3.9.2", 4 | "keywords": "esp32, esp8266, wifi", 5 | "description": "WiFi Manager for the ESP32 and ESP8266 Arduino environments", 6 | "authors": [ 7 | { "name": "Juerd Waalboer" }, 8 | { 9 | "name": "Reinier van der Leer", 10 | "url": "https://pwuts.nl/" 11 | } 12 | ], 13 | "repository": { "type": "git", "url": "https://github.com/Juerd/ESP-WiFiSettings" }, 14 | "frameworks": "arduino", 15 | "platforms": [ "espressif32", "espressif8266" ] 16 | } 17 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ESP-WiFiSettings 2 | version=3.9.2 3 | author=Juerd Waalboer,Pwuts 4 | maintainer=Juerd Waalboer <#####@juerd.nl> 5 | sentence=WiFi configuration manager for the ESP32 and ESP8266 platforms. 6 | paragraph=Starts an access point with captive portal to allow configuration of the WiFi network name (SSID) and password. 7 | category=Communication 8 | url=https://github.com/Juerd/ESP-WiFiSettings 9 | architectures=esp32,esp8266 10 | -------------------------------------------------------------------------------- /screenshots/advanced-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juerd/ESP-WiFiSettings/07abd2252607dd26976e6bb002e1584d7588f125/screenshots/advanced-example.png -------------------------------------------------------------------------------- /screenshots/basic-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juerd/ESP-WiFiSettings/07abd2252607dd26976e6bb002e1584d7588f125/screenshots/basic-example.png -------------------------------------------------------------------------------- /screenshots/full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juerd/ESP-WiFiSettings/07abd2252607dd26976e6bb002e1584d7588f125/screenshots/full.png -------------------------------------------------------------------------------- /screenshots/pwuts-climatenode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juerd/ESP-WiFiSettings/07abd2252607dd26976e6bb002e1584d7588f125/screenshots/pwuts-climatenode.png -------------------------------------------------------------------------------- /screenshots/snuffelding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Juerd/ESP-WiFiSettings/07abd2252607dd26976e6bb002e1584d7588f125/screenshots/snuffelding.png --------------------------------------------------------------------------------