diff --git a/README.md b/README.md index 6d9fe20..f060b33 100644 --- a/README.md +++ b/README.md @@ -66,40 +66,255 @@ void loop() { } ``` -## JSON Endpoints +## WiFi Configuration -The library provides two main endpoints for configuration: +### Initial Setup -### /wifi/main +When the device first starts without any stored WiFi credentials, it will automatically enter portal mode. In portal mode: -This endpoint handles the primary configuration parameters. +1. The ESP32 creates an access point with the SSID specified in `HeadlessWiFiSettings.hostname` (default: `esp32-XXXXXX` where XXXXXX is a unique device ID) +2. A captive portal is started on port 80 +3. DNS server redirects all requests to the portal's IP address -- GET: Returns a JSON object containing all primary parameters -- POST: Updates primary parameters. Send parameters as form data. +### Configuring WiFi via HTTP Endpoints -Example response: +Connect to the ESP32's access point and use the endpoints below to configure WiFi: + +```bash +# Scan for available networks +curl http://192.168.4.1/wifi/scan + +# Get current WiFi settings +curl http://192.168.4.1/wifi/main + +# Configure WiFi credentials +curl -X POST http://192.168.4.1/wifi \ + -d "wifi-ssid=YourNetworkName" \ + -d "wifi-password=YourPassword" +``` + +### Multiple Endpoint Configuration + +You can organize parameters into different endpoints using `markEndpoint()`: + +```C++ +// Default "main" endpoint +String host = HeadlessWiFiSettings.string("server_host", "example.org"); +int port = HeadlessWiFiSettings.integer("server_port", 443); + +// Switch to "mqtt" endpoint +HeadlessWiFiSettings.markEndpoint("mqtt"); +String mqttHost = HeadlessWiFiSettings.string("mqtt_host", "mqtt.example.org"); +int mqttPort = HeadlessWiFiSettings.integer("mqtt_port", 1883); + +// Legacy "extras" endpoint (for backward compatibility) +HeadlessWiFiSettings.markExtra(); +String otherParam = HeadlessWiFiSettings.string("other_param", "value"); +``` + +## HTTP API Reference + +All endpoints are served over HTTP on port 80. The API returns JSON responses and accepts form-encoded POST data. + +### GET /wifi/scan + +Scans for available WiFi networks and returns their SSIDs and signal strengths. + +**Response:** ```json { - "server_host": "example.org", - "server_port": "443" + "networks": { + "NetworkName1": -45, + "NetworkName2": -67, + "NetworkName3": -82 + } } ``` -### /wifi/extras +Signal strength values are in dBm (negative numbers, closer to 0 is stronger). Duplicate SSIDs are merged with the strongest signal retained. -This endpoint handles additional parameters marked with `markExtra()`. +### GET /wifi or GET /wifi/main -- GET: Returns a JSON object containing all extra parameters -- POST: Updates extra parameters. Send parameters as form data. +Returns the current values and defaults for the main parameter endpoint. -Example response: +**Response:** ```json { - "custom_param": "value", - "another_param": "123" + "values": { + "wifi-ssid": "CurrentNetwork", + "wifi-password": "***###***", + "server_host": "example.org", + "server_port": 443 + }, + "defaults": { + "server_host": "default.example.org", + "server_port": 443 + } +} +``` + +Note: Password fields always return the masked value `***###***` for security. + +### GET /wifi/{endpoint} + +Returns values and defaults for a specific endpoint (e.g., `/wifi/mqtt`, `/wifi/extras`). + +**Response format:** Same as `/wifi/main` + +### POST /wifi or POST /wifi/main + +Updates configuration parameters on the main endpoint. + +**Request:** Send parameters as form-encoded data: +``` +POST /wifi +Content-Type: application/x-www-form-urlencoded + +wifi-ssid=MyNetwork&wifi-password=MyPassword&server_host=api.example.com&server_port=8080 +``` + +**Response:** +- `200 OK` - Configuration saved successfully +- `500 Internal Server Error` - Error writing to flash filesystem +- `404 Not Found` - Endpoint doesn't exist + +After a successful save, the `onConfigSaved` callback is triggered. + +### POST /wifi/{endpoint} + +Updates configuration parameters for a specific endpoint. + +**Request format:** Same as POST /wifi/main + +### GET /wifi/options/{parameter_name} + +Returns available options for dropdown parameters. + +**Example:** `GET /wifi/options/log_level` + +**Response:** +```json +["debug", "info", "warning", "error"] +``` + +**Error Response:** +- `404 Not Found` - Parameter not found or not a dropdown type + +## Complete Usage Example + +Here's a comprehensive example demonstrating most features: + +```C++ +#include +#include +#include + +void setup() { + Serial.begin(115200); + SPIFFS.begin(true); + + // Configure hostname + HeadlessWiFiSettings.hostname = "my-device-"; // Will append device ID + + // Set up callbacks + HeadlessWiFiSettings.onConnect = []() { + Serial.println("Connecting to WiFi..."); + }; + + HeadlessWiFiSettings.onSuccess = []() { + Serial.println("WiFi connected!"); + Serial.println(WiFi.localIP()); + }; + + HeadlessWiFiSettings.onFailure = []() { + Serial.println("WiFi connection failed"); + }; + + HeadlessWiFiSettings.onConfigSaved = []() { + Serial.println("Configuration saved to flash"); + }; + + // Define main configuration parameters + String serverHost = HeadlessWiFiSettings.string("server_host", "api.example.com", "Server Host"); + int serverPort = HeadlessWiFiSettings.integer("server_port", 1, 65535, 443, "Server Port"); + bool useTLS = HeadlessWiFiSettings.checkbox("use_tls", true, "Use TLS"); + + // Define MQTT parameters in separate endpoint + HeadlessWiFiSettings.markEndpoint("mqtt"); + String mqttBroker = HeadlessWiFiSettings.string("mqtt_broker", "mqtt.local", "MQTT Broker"); + int mqttPort = HeadlessWiFiSettings.integer("mqtt_port", 1883, "MQTT Port"); + String mqttUser = HeadlessWiFiSettings.string("mqtt_user", "", "MQTT Username"); + String mqttPass = HeadlessWiFiSettings.pstring("mqtt_pass", "", "MQTT Password"); + + // Define advanced parameters + HeadlessWiFiSettings.markExtra(); + std::vector logLevels = {"debug", "info", "warning", "error"}; + int logLevel = HeadlessWiFiSettings.dropdown("log_level", logLevels, 1, "Log Level"); + float updateInterval = HeadlessWiFiSettings.floating("update_interval", 0.1, 60.0, 5.0, "Update Interval (s)"); + + // Add custom HTTP endpoints + HeadlessWiFiSettings.onHttpSetup = [](AsyncWebServer* server) { + server->on("/status", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "application/json", "{\"status\":\"ok\",\"uptime\":" + String(millis()) + "}"); + }); + + server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/html", + "

My Device

" + "

Configure via /wifi/main

" + "

MQTT settings at /wifi/mqtt

" + ); + }); + }; + + // Connect to WiFi (30 second timeout, show portal on failure) + HeadlessWiFiSettings.connect(true, 30); + + // Now use the configured values + Serial.printf("Server: %s:%d (TLS: %s)\n", + serverHost.c_str(), serverPort, useTLS ? "yes" : "no"); + Serial.printf("MQTT: %s:%d\n", mqttBroker.c_str(), mqttPort); + Serial.printf("Log level: %s\n", logLevels[logLevel].c_str()); +} + +void loop() { + // Your application code here + delay(100); } ``` +### Testing the API + +Once the device is running, you can interact with it using curl or any HTTP client: + +```bash +# Check status +curl http://my-device-123456.local/status + +# Get main configuration +curl http://my-device-123456.local/wifi/main + +# Update server settings +curl -X POST http://my-device-123456.local/wifi \ + -d "server_host=newserver.com" \ + -d "server_port=8443" \ + -d "use_tls=1" + +# Get MQTT configuration +curl http://my-device-123456.local/wifi/mqtt + +# Update MQTT credentials +curl -X POST http://my-device-123456.local/wifi/mqtt \ + -d "mqtt_user=myuser" \ + -d "mqtt_pass=mypassword" + +# Get dropdown options +curl http://my-device-123456.local/wifi/options/log_level + +# Scan for WiFi networks +curl http://my-device-123456.local/wifi/scan +``` + ## Installing Automated installation: @@ -145,24 +360,86 @@ Disconnects any active WiFi and turns the ESP into an access point that serves t This function never ends. A restart is required to resume normal operation. -#### HeadlessWiFiSettings.integer(...) #### HeadlessWiFiSettings.string(...) + +```C++ +String string(String name, String init = "", String label = name); +String string(String name, unsigned int max_length, String init = "", String label = name); +String string(String name, unsigned int min_length, unsigned int max_length, String init = "", String label = name); +``` + +Configures a custom string parameter and returns the current value. When no value is configured, the `init` value is returned. + +#### HeadlessWiFiSettings.pstring(...) + +```C++ +String pstring(String name, String init = "", String label = name); +``` + +Like `string()`, but for password fields. Values are masked in JSON responses as `***###***`. + +#### HeadlessWiFiSettings.integer(...) + +```C++ +long integer(String name, long init = 0, String label = name); +long integer(String name, long min, long max, long init = 0, String label = name); +``` + +Configures a custom integer parameter with optional min/max constraints. + +#### HeadlessWiFiSettings.floating(...) + +```C++ +float floating(String name, float init = 0, String label = name); +float floating(String name, long min, long max, float init = 0, String label = name); +``` + +Configures a custom floating-point parameter with optional min/max constraints. + #### HeadlessWiFiSettings.checkbox(...) ```C++ -int integer(String name, [long min, long max,] int init = 0, String label = name); -String string(String name, [[unsigned int min_length,] unsigned int max_length,] String init = "", String label = name); bool checkbox(String name, bool init = false, String label = name); ``` -Configures a custom configurable option and returns the current value. When no -value (or an empty string) is configured, the value given as `init` is returned. +Configures a boolean checkbox parameter. + +#### HeadlessWiFiSettings.dropdown(...) + +```C++ +long dropdown(String name, std::vector options, long init = 0, String label = name); +``` + +Configures a dropdown parameter with predefined options. Returns the index of the selected option. The available options can be retrieved via `/wifi/options/{name}`. -These functions should be called *before* calling `.connect()` or `.portal()`. +**Note:** All parameter configuration functions should be called *before* calling `.connect()` or `.portal()`. -The `name` is used as the filename in the SPIFFS, and as a parameter name in the JSON endpoints. +The `name` is used as the filename in SPIFFS and as the parameter name in JSON endpoints. -Some restrictions for the values can be given. For integers, a range can be specified by supplying both `min` and `max`. For strings, a maximum length can be specified as `max_length`. A minimum string length can be set with `min_length`, effectively making the field mandatory: it can no longer be left empty to get the `init` value. +#### HeadlessWiFiSettings.markEndpoint(...) + +```C++ +void markEndpoint(String name); +``` + +Switches to a named endpoint. All subsequent parameter definitions will be grouped under this endpoint and accessible via `/wifi/{name}`. + +```C++ +// Parameters added to "main" endpoint +String host = HeadlessWiFiSettings.string("host", "example.org"); + +// Switch to custom endpoint +HeadlessWiFiSettings.markEndpoint("mqtt"); +String mqttHost = HeadlessWiFiSettings.string("mqtt_host", "broker.local"); +``` + +#### HeadlessWiFiSettings.markExtra() + +```C++ +void markExtra(); +``` + +Convenience function that switches to the "extras" endpoint. Equivalent to `markEndpoint("extras")`. ### Variables @@ -199,6 +476,105 @@ bool By setting this to `true`, before any custom configuration parameter is defined, secure mode will be forced, instead of the default behavior. +### Callbacks + +Callbacks can be assigned to customize behavior at various stages. All callbacks are optional. + +#### HeadlessWiFiSettings.onHttpSetup + +```C++ +std::function onHttpSetup; +``` + +Called during HTTP server setup, before the server starts. Allows you to add custom routes or modify server configuration. + +```C++ +HeadlessWiFiSettings.onHttpSetup = [](AsyncWebServer* server) { + server->on("/custom", HTTP_GET, [](AsyncWebServerRequest* request) { + request->send(200, "text/plain", "Custom endpoint"); + }); +}; +``` + +#### HeadlessWiFiSettings.onConnect + +```C++ +std::function onConnect; +``` + +Called when attempting to connect to WiFi, before the connection starts. + +#### HeadlessWiFiSettings.onSuccess + +```C++ +std::function onSuccess; +``` + +Called when WiFi connection is successful. + +#### HeadlessWiFiSettings.onFailure + +```C++ +std::function onFailure; +``` + +Called when WiFi connection fails after the timeout period. + +#### HeadlessWiFiSettings.onWaitLoop + +```C++ +std::function onWaitLoop; +``` + +Called repeatedly while waiting for WiFi connection. Return the delay in milliseconds until the next call (default: 100ms). + +```C++ +HeadlessWiFiSettings.onWaitLoop = []() { + // Blink LED or update display + return 50; // Call again in 50ms +}; +``` + +#### HeadlessWiFiSettings.onPortal + +```C++ +std::function onPortal; +``` + +Called when the configuration portal starts. + +#### HeadlessWiFiSettings.onPortalView + +```C++ +std::function onPortalView; +``` + +Called when someone views the portal page (currently unused in headless mode). + +#### HeadlessWiFiSettings.onPortalWaitLoop + +```C++ +std::function onPortalWaitLoop; +``` + +Called repeatedly while the portal is running. Return the delay in milliseconds until the next call. + +#### HeadlessWiFiSettings.onConfigSaved + +```C++ +std::function onConfigSaved; +``` + +Called after configuration parameters are successfully saved to flash storage. + +#### HeadlessWiFiSettings.onUserAgent + +```C++ +std::function onUserAgent; +``` + +Called with the User-Agent string from HTTP requests (currently unused in headless mode). + ## History This was forked from https://github.com/Juerd/ESP-WiFiSettings when it was converted to use AsyncWebServer instead of WebServer. This version removes the web UI in favor of JSON endpoints. diff --git a/examples/ArduinoOTA/ArduinoOTA.ino b/examples/ArduinoOTA/ArduinoOTA.ino deleted file mode 100644 index 8f0eee6..0000000 --- a/examples/ArduinoOTA/ArduinoOTA.ino +++ /dev/null @@ -1,59 +0,0 @@ -/* - AsyncWiFiSettings Arduino OTA example - - Demonstrates how to run Arduino OTA in tandem with AsyncWiFiSettings, - using the AsyncWiFiSettings credentials. - - Source and further documentation available at - https://github.com/ESPresense/AsyncWiFiSettings - - Note: this example is written for ESP32. - For ESP8266, use LittleFS.begin() instead of SPIFFS.begin(true). -*/ - -#include -#include -#include - -// Start ArduinoOTA via AsyncWiFiSettings with the same hostname and password -void setup_ota() { - ArduinoOTA.setHostname(AsyncWiFiSettings.hostname.c_str()); - ArduinoOTA.setPassword(AsyncWiFiSettings.password.c_str()); - ArduinoOTA.begin(); -} - -void setup() { - Serial.begin(115200); - SPIFFS.begin(true); // Will format on the first run after failing to mount - - // Force WPA secured WiFi for the software access point. - // Because OTA is remote code execution (RCE) by definition, the password - // should be kept secret. By default, AsyncWiFiSettings will become an insecure - // WiFi access point and happily tell anyone the password. The password - // will instead be provided on the Serial connection, which is a bit safer. - AsyncWiFiSettings.secure = true; - - // Set callbacks to start OTA when the portal is active - AsyncWiFiSettings.onPortal = []() { - setup_ota(); - }; - AsyncWiFiSettings.onPortalWaitLoop = []() { - ArduinoOTA.handle(); - }; - - // Use stored credentials to connect to your WiFi access point. - // If no credentials are stored or if the access point is out of reach, - // an access point will be started with a captive portal to configure WiFi. - AsyncWiFiSettings.connect(); - - Serial.print("Password: "); - Serial.println(AsyncWiFiSettings.password); - - setup_ota(); // If you also want the OTA during regular execution -} - -void loop() { - ArduinoOTA.handle(); // If you also want the OTA during regular execution - - // Your loop code here -} diff --git a/src/HeadlessWiFiSettings.cpp b/src/HeadlessWiFiSettings.cpp index fb0d592..85d85ef 100644 --- a/src/HeadlessWiFiSettings.cpp +++ b/src/HeadlessWiFiSettings.cpp @@ -38,6 +38,17 @@ namespace { #define Sprintf(f, ...) ({ char* s; asprintf(&s, f, __VA_ARGS__); String r = s; free(s); r; }) namespace { // Helpers +static constexpr char ERROR_FLASH[] = "Error writing to flash filesystem"; +static constexpr char ENDPOINT_NOT_FOUND[] = "Endpoint not found"; +static constexpr char ERROR_AP_START[] = "Failed to start access point!"; +static constexpr char WIFI_PATH[] = "/wifi/"; +static constexpr char JSON_NAME_VALUE[] = "\"{name}\":\"{value}\""; +static constexpr char JSON_NAME_NUM[] = "\"{name}\":{value}"; +static constexpr char CONTENT_JSON[] = "application/json; charset=utf-8"; +static constexpr char CONTENT_TEXT[] = "text/plain"; + +void ensureMainEndpoint(); + String slurp(const String &fn) { File f = ESPFS.open(fn, "r"); String r = f.readString(); @@ -55,6 +66,24 @@ namespace { // Helpers return w == content.length(); } + // Helper to format JSON string values + String jsonString(const String &name, const String &value) { + if (value == "") return ""; + String j = F(JSON_NAME_VALUE); + j.replace("{name}", json_encode(name)); + j.replace("{value}", json_encode(value)); + return j; + } + + // Helper to format JSON numeric values + String jsonNumeric(const String &name, const String &value) { + if (value == "") return ""; + String j = F(JSON_NAME_NUM); + j.replace("{name}", json_encode(name)); + j.replace("{value}", value); + return j; + } + enum class ParamType { Dropdown, String, @@ -97,42 +126,16 @@ namespace { // Helpers std::vector options; - String jsonValue() { - if (value == "") return ""; - String j = F("\"{name}\":\"{value}\""); - j.replace("{name}", json_encode(name)); - j.replace("{value}", json_encode(value)); - return j; - } - - String jsonDefault() { - if (init == "") return ""; - String j = F("\"{name}\":\"{value}\""); - j.replace("{name}", json_encode(name)); - j.replace("{value}", json_encode(init)); - return j; - } + String jsonValue() { return jsonString(name, value); } + String jsonDefault() { return jsonString(name, init); } }; struct HeadlessWiFiSettingsString : HeadlessWiFiSettingsParameter { HeadlessWiFiSettingsString() { type = ParamType::String; } virtual void set(const String &v) { value = v; } - String jsonValue() { - if (value == "") return ""; - String j = F("\"{name}\":\"{value}\""); - j.replace("{name}", json_encode(name)); - j.replace("{value}", json_encode(value)); - return j; - } - - String jsonDefault() { - if (init == "") return ""; - String j = F("\"{name}\":\"{value}\""); - j.replace("{name}", json_encode(name)); - j.replace("{value}", json_encode(init)); - return j; - } + String jsonValue() { return jsonString(name, value); } + String jsonDefault() { return jsonString(name, init); } }; static const char* const MASKED_PASSWORD = "***###***"; @@ -144,80 +147,32 @@ namespace { // Helpers value = v; } - String jsonValue() { - if (!value.length()) return ""; - String j = F("\"{name}\":\"{value}\""); - j.replace("{name}", json_encode(name)); - j.replace("{value}", json_encode(MASKED_PASSWORD)); - return j; - } - - String jsonDefault() { - return ""; - } - }; // HeadlessWiFiSettingsPassword + String jsonValue() { return value.length() ? jsonString(name, MASKED_PASSWORD) : ""; } + String jsonDefault() { return ""; } + }; struct HeadlessWiFiSettingsInt : HeadlessWiFiSettingsParameter { HeadlessWiFiSettingsInt() { type = ParamType::Int; } virtual void set(const String &v) { value = v; } - String jsonValue() { - if (value == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", String(value.toInt())); - return j; - } - - String jsonDefault() { - if (init == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", String(init.toInt())); - return j; - } + String jsonValue() { return jsonNumeric(name, value.length() ? String(value.toInt()) : ""); } + String jsonDefault() { return jsonNumeric(name, init.length() ? String(init.toInt()) : ""); } }; struct HeadlessWiFiSettingsFloat : HeadlessWiFiSettingsParameter { HeadlessWiFiSettingsFloat() { type = ParamType::Float; } virtual void set(const String &v) { value = v; } - String jsonValue() { - if (value == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", String(value.toFloat())); - return j; - } - - String jsonDefault() { - if (init == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", String(init.toFloat())); - return j; - } + String jsonValue() { return jsonNumeric(name, value.length() ? String(value.toFloat()) : ""); } + String jsonDefault() { return jsonNumeric(name, init.length() ? String(init.toFloat()) : ""); } }; struct HeadlessWiFiSettingsBool : HeadlessWiFiSettingsParameter { HeadlessWiFiSettingsBool() { type = ParamType::Bool; } virtual void set(const String &v) { value = v.length() ? "1" : "0"; } - String jsonValue() { - if (value == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", value.toInt() ? "true" : "false"); - return j; - } - - String jsonDefault() { - if (init == "") return ""; - String j = F("\"{name}\":{value}"); - j.replace("{name}", json_encode(name)); - j.replace("{value}", init.toInt() ? "true" : "false"); - return j; - } + String jsonValue() { return jsonNumeric(name, value.length() ? (value.toInt() ? "true" : "false") : ""); } + String jsonDefault() { return jsonNumeric(name, init.length() ? (init.toInt() ? "true" : "false") : ""); } }; // Parallel vectors for endpoint names and parameters @@ -225,17 +180,21 @@ namespace { // Helpers std::vector> endpointParams; uint8_t currentEndpointIndex = 0; - std::vector *params() { - // Ensure we have at least the main endpoint + void ensureMainEndpoint() { if (endpointNames.empty()) { endpointNames.push_back("main"); endpointParams.push_back({}); } + } + + std::vector *params() { + ensureMainEndpoint(); return &endpointParams[currentEndpointIndex]; } // Find or create endpoint uint8_t findOrCreateEndpoint(const String& name) { + ensureMainEndpoint(); // Look for existing endpoint for (size_t i = 0; i < endpointNames.size(); i++) { if (endpointNames[i] == name) { @@ -247,6 +206,17 @@ namespace { // Helpers endpointParams.push_back({}); return endpointNames.size() - 1; } + + // Find existing endpoint (returns -1 if not found) + int findEndpoint(const String& name) { + ensureMainEndpoint(); + for (size_t i = 0; i < endpointNames.size(); i++) { + if (endpointNames[i] == name) { + return i; + } + } + return -1; + } } // namespace String HeadlessWiFiSettingsClass::pstring(const String &name, const String &init, const String &label) { @@ -407,7 +377,7 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { Serial.print("GET "); Serial.println(path); - String paramName = path.substring(13); // Remove "/wifi/options/" + String paramName = path.substring(14); // Remove "/wifi/options/" // Search all endpoints for the parameter HeadlessWiFiSettingsDropdown* dropdown = nullptr; @@ -424,11 +394,11 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { } if (!dropdown) { - request->send(404, "text/plain", "Dropdown not found"); + request->send(404, CONTENT_TEXT, "Dropdown not found"); return; } - AsyncResponseStream *response = request->beginResponseStream("application/json; charset=utf-8"); + AsyncResponseStream *response = request->beginResponseStream(CONTENT_JSON); response->print("["); bool needsComma = false; for (const auto& option : dropdown->options) { @@ -446,7 +416,7 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { Serial.println(path); int numNetworks = WiFi.scanNetworks(); - AsyncResponseStream *response = request->beginResponseStream("application/json; charset=utf-8"); + AsyncResponseStream *response = request->beginResponseStream(CONTENT_JSON); response->print("{\"networks\":{"); bool needsComma = false; @@ -498,34 +468,16 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { String path = request->url(); Serial.print("GET "); Serial.println(path); - String endpointName; - size_t endpointIndex; - if (path == "/wifi") { - endpointName = "main"; - } else if (path.startsWith("/wifi/")) { - endpointName = path.substring(6); // Remove "/wifi/" - } else { - request->send(404); - return; - } + String endpointName = (path.length() <= 6) ? "main" : path.substring(6); + int endpointIndex = findEndpoint(endpointName); - // Find the endpoint - bool found = false; - for (size_t i = 0; i < endpointNames.size(); i++) { - if (endpointNames[i] == endpointName) { - endpointIndex = i; - found = true; - break; - } - } - - if (!found) { - request->send(404, "text/plain", "Endpoint not found"); + if (endpointIndex < 0) { + request->send(404, CONTENT_TEXT, ENDPOINT_NOT_FOUND); return; } - AsyncResponseStream *response = request->beginResponseStream("application/json; charset=utf-8"); + AsyncResponseStream *response = request->beginResponseStream(CONTENT_JSON); response->print("{"); // Output current values @@ -560,30 +512,11 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { Serial.print("POST "); Serial.println(path); - String endpointName; - size_t endpointIndex; - - if (path == "/wifi") { - endpointName = "main"; - } else if (path.startsWith("/wifi/")) { - endpointName = path.substring(6); // Remove "/wifi/" - } else { - request->send(404); - return; - } - - // Find the endpoint - bool found = false; - for (size_t i = 0; i < endpointNames.size(); i++) { - if (endpointNames[i] == endpointName) { - endpointIndex = i; - found = true; - break; - } - } + String endpointName = (path.length() <= 6) ? "main" : path.substring(6); + int endpointIndex = findEndpoint(endpointName); - if (!found) { - request->send(404, "text/plain", "Endpoint not found"); + if (endpointIndex < 0) { + request->send(404, CONTENT_TEXT, ENDPOINT_NOT_FOUND); return; } @@ -597,7 +530,8 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { request->send(200); if (onConfigSaved) onConfigSaved(); } else { - request->send(500, "text/plain", "Error writing to flash filesystem"); + Serial.println(ERROR_FLASH); + request->send(500, CONTENT_TEXT, ERROR_FLASH); } }); @@ -606,7 +540,7 @@ void HeadlessWiFiSettingsClass::httpSetup(bool wifi) { Serial.print("GET "); Serial.println(path); if (redirect(request)) return; - request->send(404, "text/plain", "404"); + request->send(404, CONTENT_TEXT, "404"); }); http.begin(); @@ -686,11 +620,11 @@ void HeadlessWiFiSettingsClass::portal() { if (secure && password.length()) { Serial.printf("SSID: '%s', Password: '%s'\n", hostname.c_str(), password.c_str()); if (!WiFi.softAP(hostname.c_str(), password.c_str())) - Serial.println("Failed to start access point!"); + Serial.println(ERROR_AP_START); } else { Serial.printf("SSID: '%s'\n", hostname.c_str()); if (!WiFi.softAP(hostname.c_str())) - Serial.println("Failed to start access point!"); + Serial.println(ERROR_AP_START); } delay(500); DNSServer dns; @@ -731,8 +665,8 @@ bool HeadlessWiFiSettingsClass::connect(bool portal, int wait_seconds) { WiFi.persistent(false); WiFi.setAutoReconnect(false); - String ssid = slurp("/wifi-ssid"); - String pw = slurp("/wifi-password"); + String const ssid = slurp("/wifi-ssid"); + String const pw = slurp("/wifi-password"); if (ssid.length() == 0) { Serial.println(F("First contact!\n")); if (portal) { diff --git a/src/HeadlessWiFiSettings.h b/src/HeadlessWiFiSettings.h index b7d5146..6b8f906 100644 --- a/src/HeadlessWiFiSettings.h +++ b/src/HeadlessWiFiSettings.h @@ -18,63 +18,64 @@ #endif class HeadlessWiFiSettingsClass { - public: - typedef std::function TCallback; - typedef std::function TCallbackReturnsInt; - typedef std::function TCallbackString; + public: + typedef std::function TCallback; + typedef std::function TCallbackReturnsInt; + typedef std::function TCallbackString; - HeadlessWiFiSettingsClass(); - void markExtra(); - void markEndpoint(const String& name); - void begin(); - bool connect(bool portal = true, int wait_seconds = 60); - void portal(); - void httpSetup(bool softAP = false); - void beginSerialImprov(const String& firmwareName, - const String& firmwareVersion, - const String& deviceName = "", - Stream* serial = nullptr, - const String& deviceUrl = ""); - void serialImprovLoop(); - String string(const String &name, const String &init = "", const String &label = ""); - String string(const String& name, unsigned int max_length, const String& init = "", const String& label = ""); - String string(const String& name, unsigned int min_length, unsigned int max_length, const String& init = "", const String& label = ""); - String pstring(const String& name, const String& init = "", const String& label = ""); - long dropdown(const String& name, std::vector options, long init = 0, const String& label = ""); - long integer(const String& name, long init = 0, const String& label = ""); - long integer(const String& name, long min, long max, long init = 0, const String& label = ""); - float floating(const String &name, float init = 0, const String &label = ""); - float floating(const String &name, long min, long max, float init = 0, const String &label = ""); - bool checkbox(const String& name, bool init = false, const String& label = ""); + HeadlessWiFiSettingsClass(); + void markExtra(); + void markEndpoint(const String &name); + void begin(); + bool connect(bool portal = true, int wait_seconds = 60); + void portal(); + void httpSetup(bool softAP = false); + void beginSerialImprov(const String &firmwareName, + const String &firmwareVersion, + const String &deviceName = "", + Stream *serial = nullptr, + const String &deviceUrl = ""); + void serialImprovLoop(); + String string(const String &name, const String &init = "", const String &label = ""); + String string(const String &name, unsigned int max_length, const String &init = "", const String &label = ""); + String string(const String &name, unsigned int min_length, unsigned int max_length, const String &init = "", const String &label = ""); + String pstring(const String &name, const String &init = "", const String &label = ""); + long dropdown(const String &name, std::vector options, long init = 0, const String &label = ""); + long integer(const String &name, long init = 0, const String &label = ""); + long integer(const String &name, long min, long max, long init = 0, const String &label = ""); + float floating(const String &name, float init = 0, const String &label = ""); + float floating(const String &name, long min, long max, float init = 0, const String &label = ""); + bool checkbox(const String &name, bool init = false, const String &label = ""); - String hostname; - String password; - bool secure; + String hostname; + String password; + bool secure; - std::function onHttpSetup; - TCallback onConnect; - TCallbackReturnsInt onWaitLoop; - TCallback onSuccess; - TCallback onFailure; - TCallback onPortal; - TCallback onPortalView; - TCallbackString onUserAgent; - TCallback onConfigSaved; - TCallback onRestart; - TCallbackReturnsInt onPortalWaitLoop; - TCallback onImprovIdentify; - private: - AsyncWebServer http; - bool begun = false; - bool httpBegun = false; + std::function onHttpSetup; + TCallback onConnect; + TCallbackReturnsInt onWaitLoop; + TCallback onSuccess; + TCallback onFailure; + TCallback onPortal; + TCallback onPortalView; + TCallbackString onUserAgent; + TCallback onConfigSaved; + TCallback onRestart; + TCallbackReturnsInt onPortalWaitLoop; + TCallback onImprovIdentify; + + private: + AsyncWebServer http; + bool begun = false; + bool httpBegun = false; #if HEADLESS_WIFI_SETTINGS_HAS_IMPROV - ImprovWiFi* improv = nullptr; - Stream* improvSerial = nullptr; - bool handleImprovCredentials(const char* ssid, const char* password); - void handleImprovIdentify(); - static bool improvConnectTrampoline(const char* ssid, const char* password); + ImprovWiFi *improv = nullptr; + Stream *improvSerial = nullptr; + bool handleImprovCredentials(const char *ssid, const char *password); + void handleImprovIdentify(); + static bool improvConnectTrampoline(const char *ssid, const char *password); #if defined(IMPROV_WIFI_LIBRARY_HAS_IDENTIFY_CALLBACK) - static void improvIdentifyTrampoline(); + static void improvIdentifyTrampoline(); #endif #endif };