From 6efde550979e9fdf5482771aa4256ce4fa452c3b Mon Sep 17 00:00:00 2001
From: DTTerastar
Date: Thu, 23 Jul 2026 20:30:11 -0400
Subject: [PATCH] Refactor core library: constants, endpoint helpers, README,
drop ArduinoOTA example
Non-functional cleanup extracted from the SerialImprov work so it can be
reviewed on its own:
- Extract string literals into named constants (ERROR_FLASH, ENDPOINT_NOT_FOUND,
ERROR_AP_START, JSON templates, content types).
- Add ensureMainEndpoint() and reindent the source (2-space) with related
endpoint/JSON handling cleanup.
- Rewrite README with expanded usage, HTTP API reference, and callback docs.
- Remove the ArduinoOTA example.
No behavior change to the WiFi portal or HTTP endpoints.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
README.md | 424 +++++++++++++++++++++++++++--
examples/ArduinoOTA/ArduinoOTA.ino | 59 ----
src/HeadlessWiFiSettings.cpp | 224 ++++++---------
src/HeadlessWiFiSettings.h | 105 +++----
4 files changed, 532 insertions(+), 280 deletions(-)
delete mode 100644 examples/ArduinoOTA/ArduinoOTA.ino
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
};