mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Portal SPA: single shell, JSON /api surface, remove legacy portal routes
Replace multi-page portal with hash-routed SPA, DFTE shell placeholders, and data-first JSON APIs. Remove legacy routes, root-template override path, and HTML-derived info pipeline. Add bootstrap feature flags, in-DOM toast/dialog UX, OTA via POST /u, and tests for shell, bootstrap, API JSON, and captive host rule. Rewrite README as fork disclaimer and modernization overview.
This commit is contained in:
@@ -1,18 +1,49 @@
|
||||
# WiFiManager
|
||||
|
||||
Espressif ESPx WiFi connection manager with a fallback web configuration portal.
|
||||
This repository is a **breaking fork** of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager).
|
||||
It is not a drop-in replacement for upstream behavior, APIs, templates, or portal customization patterns.
|
||||
|
||||
## Fork notice
|
||||
|
||||
**This is a fork with breaking changes.** The web portal is built with **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)): composable templates and chunked HTML responses instead of upstream’s monolithic string assembly.
|
||||
If you are evaluating this fork, assume that core web-portal architecture has changed and review the code before adopting it in an existing upstream-based project.
|
||||
|
||||
**This repository:** [alexhopeoconnor/WiFiManager](https://github.com/alexhopeoconnor/WiFiManager)
|
||||
**Upstream:** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
|
||||
**Upstream (original):** [tzapu/WiFiManager](https://github.com/tzapu/WiFiManager)
|
||||
## Breaking Changes
|
||||
|
||||
This fork intentionally modernizes and restructures the configuration portal.
|
||||
Notable differences from upstream include:
|
||||
|
||||
- The portal now serves a **single HTML shell** from `GET /`.
|
||||
- Client navigation is handled as a **SPA with hash routing**.
|
||||
- Interactive behavior is exposed through **JSON APIs under `/api/...`** plus firmware upload at **`POST /u`**.
|
||||
- Legacy multi-page portal routes and legacy root-template override paths have been removed.
|
||||
- The portal rendering pipeline is built around **DFTE** instead of upstream's monolithic HTML string assembly.
|
||||
- JSON endpoints are expected to be **data-first**, not derived from generated HTML fragments.
|
||||
|
||||
## Current Improvements And Modernization
|
||||
|
||||
This fork currently includes the following architectural improvements:
|
||||
|
||||
- A **single-shell portal architecture** with embedded bootstrap JSON and embedded application JS.
|
||||
- A **SPA-based configuration UI** for WiFi setup, parameters, info views, device actions, and OTA flow.
|
||||
- A **clean API surface** for WiFi scanning, WiFi save, parameters, info, status, restart, erase, portal exit, and captive-portal close behavior.
|
||||
- **Captive portal redirect handling** retained while removing duplicate legacy UI architecture.
|
||||
- **Data-first JSON generation** for portal APIs, including info/device/about data, instead of HTML-to-JSON parsing.
|
||||
- **Capability-driven UI flags** in bootstrap/API payloads so features like info, update, erase, and action visibility can be controlled by backend state.
|
||||
- **SPA-native feedback UX** using in-DOM dialog/toast behavior rather than page-based action flows.
|
||||
- A **shell-scoped placeholder model** centered on `%PAGE_TITLE%`, `%STYLES%`, `%SCRIPTS%`, `%BOOTSTRAP_JSON%`, and `%PORTAL_APP_JS%`.
|
||||
- A clearer separation between:
|
||||
- shell rendering
|
||||
- API responses
|
||||
- captive portal behavior
|
||||
- OTA handling
|
||||
- Updated tests focused on the **shell contract**, **bootstrap payloads**, and **API JSON shapes** rather than removed legacy portal pages.
|
||||
|
||||
## Dependencies
|
||||
|
||||
**DFTE** is required and is declared in `library.json` (PlatformIO / `lib_deps` pick it up automatically). For **this repo’s** `platformio.ini` tests, a **sibling checkout** is expected:
|
||||
This fork depends on **DFTE** ([Device Framework Template Engine](https://github.com/alexhopeoconnor/DFTE)) and **ESP32Async/ESPAsyncWebServer**.
|
||||
|
||||
For this repo's `platformio.ini` test setup, DFTE is expected as a sibling checkout:
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
@@ -20,11 +51,7 @@ lib_deps =
|
||||
symlink://../DFTE
|
||||
```
|
||||
|
||||
Clone `DFTE` next to `WiFiManager` (same parent directory), or point `lib_deps` at the Git URL used in `library.json`.
|
||||
|
||||
Other pinned dependency: **ESP32Async/ESPAsyncWebServer** (see `library.json`).
|
||||
|
||||
## Installation (PlatformIO)
|
||||
## Installation
|
||||
|
||||
```ini
|
||||
[env:your_environment]
|
||||
@@ -42,46 +69,9 @@ lib_deps =
|
||||
file:///path/to/WiFiManager
|
||||
```
|
||||
|
||||
## HTML customization (DFTE)
|
||||
## AI Assistance Notice
|
||||
|
||||
- **Page shell and routes** stream through DFTE (`TemplateRenderer` + chunked responses). Per-request bundles use their own `PlaceholderRegistry` for dynamic sections; the server also keeps a **shared** registry for defaults (`%STYLES%`, `%SCRIPTS%`, `%PAGE_TITLE%`, `%SUBTITLE%`, `%MENU%`, `%STATUS%`, …).
|
||||
- **`WM_TEMPLATE_REGISTRY_CAPACITY`** (default in `WiFiManagerServer.h`) sizes the shared registry; raise it if you register many custom placeholders.
|
||||
- **Override the root document template:** define **`WM_CUSTOM_ROOT_TEMPLATE_HEADER`** to a **quoted** include path (see `lib/WiFiManager/include/templates/RootSelector.h`). By default it includes `Root.h` (`WM_ROOT_TEMPLATE`).
|
||||
- **Advanced / tests:** `WiFiManagerServer::instance()` exposes the same hooks after the server exists; prefer the `WiFiManager` methods below for typical apps.
|
||||
|
||||
### `WiFiManager` API
|
||||
|
||||
```cpp
|
||||
wm.registerTemplateSetupCallback([](PlaceholderRegistry& reg) {
|
||||
// reg.registerProgmemData(...), registerRamData, registerDynamicTemplate, etc.
|
||||
});
|
||||
wm.rebuildPlaceholderRegistry(); // optional: after changing callback while portal runs
|
||||
```
|
||||
|
||||
`registerTemplateSetupCallback` may be called **before** `startConfigPortal` / `startWebPortal` (stored until the HTTP server is created). If the portal is **already** running, the callback is applied and the shared registry is **rebuilt** so changes take effect.
|
||||
|
||||
`rebuildPlaceholderRegistry(customizer)` forwards to the server when present: rebuilds defaults, then runs your optional one-shot customizer, or the registered setup callback if `customizer` is null.
|
||||
|
||||
### Content built as `String` / PROGMEM fragments
|
||||
|
||||
Some small HTML snippets (status messages, OTA blurb, help table, etc.) are still assembled in RAM or pulled from `HTML.h` and then injected into templates. That is **intentional**: not every line of markup needs its own placeholder or registry churn. Customize those via DFTE where it pays off (shell, branding, layout); keep trivial blobs as strings when appropriate.
|
||||
|
||||
## Debug: WiFiManager logging and DFTE
|
||||
|
||||
### How DFTE logging is designed
|
||||
|
||||
DFTE keeps a **single global pointer** `deviceFrameworkTemplateEngineLogger` (see DFTE’s `DeviceFrameworkTemplateEngineDebug.h`). By default it is **`nullptr`**: every `DFTE_LOG_*` macro expands to a cheap null check and does nothing—no virtual calls, no strings, no flash cost. A host (sketch or library) implements `DeviceFrameworkTemplateEngineLogger` (virtual `error` / `warn` / `info` / `debug`) and calls `deviceFrameworkTemplateEngineEnableLogging(&myLogger)` once. Until then, DFTE is silent on the happy path; internal code mostly uses `DFTE_LOG_ERROR` and `DFTE_LOG_WARN` when something is wrong.
|
||||
|
||||
### WiFiManager today
|
||||
|
||||
- **`WM_LOG_LEVEL`**: compile-time default is **`0`** (silent). Set **`1`–`5`** for `Error` … `Trace` (see `WiFiManagerLogLevel` in `WiFiManagerLogLevel.h`). Example: `-DWM_LOG_LEVEL=5` in `platformio.ini` for full verbosity. Strip all log **call sites** from the binary with **`WM_NO_LOG`** (or legacy **`WM_NODEBUG`**). At runtime, **`setLogEnabled`**, **`setLogOutput(enabled, maxLevel)`**, **`_logPrefix`**, and **`_logPort`** gate output; use **`WM_DEBUG_PORT`** to redirect the default `Print` target. **`WiFiManagerLogSink`** receives structured **`WiFiManagerLogMessage`** (level, subsystem, full line).
|
||||
- **`WM_DFTE_LOGGING`** (build flag, **off by default**): when defined, `WiFiManagerServer::setupTemplateEngine()` registers **`WiFiManagerDfteLogger`**, which forwards DFTE into **`WiFiManager::log(..., "WM/DFTE", ...)`**. It uses **`deviceFrameworkTemplateEngineEnableLogging(..., this)`** so **`disableLoggingForOwner(this)`** on shutdown does not clobber another owner. If DFTE logging was already enabled, WiFiManager does not replace it.
|
||||
|
||||
Use **`pio test -e esp8266_dfte_log --without-uploading --without-testing`** in this repo to compile the test firmware with **`WM_DFTE_LOGGING`** enabled.
|
||||
|
||||
### Design note
|
||||
|
||||
DFTE uses a small optional logger pointer; WiFiManager uses **`#ifndef WM_NO_LOG`** around log call sites so release builds can omit strings entirely. **`WiFiManagerLogSink`** lets hosts (e.g. **DeviceFramework**) map levels without parsing ad hoc prefixes.
|
||||
Parts of this codebase have been developed and refactored with the aid of AI coding agents under human direction and review.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*
|
||||
* @author Creator tzapu
|
||||
* @author tablatronix
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -139,8 +139,6 @@
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <TemplateEngine.h>
|
||||
|
||||
// Include utility functions
|
||||
#include "WiFiManagerUtils.h"
|
||||
|
||||
@@ -437,6 +435,9 @@ class WiFiManager
|
||||
// show OTA upload button on info page
|
||||
void setShowInfoUpdate(boolean enabled);
|
||||
|
||||
// show Info nav / info view (portal SPA); default true
|
||||
void setShowInfo(boolean enabled);
|
||||
|
||||
// set ap channel
|
||||
void setWiFiAPChannel(int32_t channel);
|
||||
|
||||
@@ -449,13 +450,6 @@ class WiFiManager
|
||||
// set the webapp title, default WiFiManager
|
||||
void setTitle(String title);
|
||||
|
||||
// DFTE (portal HTML): register extra placeholders or override defaults (%STYLES%, %PAGE_TITLE%, …).
|
||||
// Call before startConfigPortal / startWebPortal to have it applied when the server starts; if the
|
||||
// portal is already running, the shared registry is rebuilt immediately.
|
||||
void registerTemplateSetupCallback(std::function<void(PlaceholderRegistry&)> cb);
|
||||
// Rebuild the server’s shared PlaceholderRegistry (defaults + registerTemplateSetupCallback, or customizer if passed).
|
||||
void rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer = nullptr);
|
||||
|
||||
// add params to its own menu page and remove from wifi, NOT TO BE COMBINED WITH setMenu!
|
||||
void setParamsPage(bool enable);
|
||||
|
||||
@@ -589,7 +583,6 @@ class WiFiManager
|
||||
protected:
|
||||
// vars
|
||||
std::unique_ptr<WiFiManagerServer> _serverManager;
|
||||
std::function<void(PlaceholderRegistry&)> _templateSetupCallback;
|
||||
|
||||
// ip configs @todo struct ?
|
||||
IPAddress _ap_static_ip;
|
||||
@@ -684,6 +677,7 @@ class WiFiManager
|
||||
boolean _webClientCheck = true; // keep cp alive if web have client
|
||||
boolean _scanDispOptions = false; // show percentage in scans not icons
|
||||
boolean _paramsInWifi = true; // show custom parameters on wifi page
|
||||
boolean _showInfo = true; // show Info in portal SPA
|
||||
boolean _showInfoErase = true; // info page erase button
|
||||
boolean _showInfoUpdate = true; // info page update button
|
||||
boolean _showBack = false; // show back button
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* WiFiManagerDfteLogger.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Optional bridge from DFTE's logging hooks to WiFiManager::log.
|
||||
* Enabled only when the sketch / library build defines WM_DFTE_LOGGING.
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* WiFiManagerHandlers.h
|
||||
*
|
||||
* HTTP request handlers and rendering logic for WiFiManager
|
||||
* Handles all web portal page generation and request processing
|
||||
* HTTP request handlers for WiFiManager: single HTML shell + JSON APIs.
|
||||
*
|
||||
* @author tablatronix
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
@@ -16,20 +14,7 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <memory>
|
||||
#include "WiFiManager.h" // Need full definition for WiFiManagerRequestArgs
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// CSS CLASSES (for body class attribute)
|
||||
|
||||
const char C_root[] PROGMEM = "home";
|
||||
const char C_wifi[] PROGMEM = "wifi";
|
||||
const char C_info[] PROGMEM = "info";
|
||||
const char C_param[] PROGMEM = "param";
|
||||
const char C_close[] PROGMEM = "close";
|
||||
const char C_restart[] PROGMEM = "restart";
|
||||
const char C_exit[] PROGMEM = "exit";
|
||||
const char C_erase[] PROGMEM = "erase";
|
||||
const char C_update[] PROGMEM = "update";
|
||||
#include "WiFiManager.h"
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// FORM FIELD NAMES (for IP configuration forms)
|
||||
@@ -51,26 +36,12 @@ class WiFiManagerHandlers {
|
||||
public:
|
||||
WiFiManagerHandlers(WiFiManager* wm);
|
||||
|
||||
// HTTP Request Handlers
|
||||
void handleRoot(AsyncWebServerRequest *request);
|
||||
void handleWifi(AsyncWebServerRequest *request, boolean scan);
|
||||
void handleWifiSave(AsyncWebServerRequest *request);
|
||||
void handleParam(AsyncWebServerRequest *request);
|
||||
void handleParamSave(AsyncWebServerRequest *request);
|
||||
void handleInfo(AsyncWebServerRequest *request);
|
||||
void handleReset(AsyncWebServerRequest *request);
|
||||
void handleExit(AsyncWebServerRequest *request);
|
||||
void handleClose(AsyncWebServerRequest *request);
|
||||
void handleErase(AsyncWebServerRequest *request, boolean opt);
|
||||
void handleNotFound(AsyncWebServerRequest *request);
|
||||
void handleRequest(AsyncWebServerRequest *request);
|
||||
void handleWiFiScanRequest(AsyncWebServerRequest *request);
|
||||
void handleWiFiScanStatus(AsyncWebServerRequest *request);
|
||||
void handleUpdate(AsyncWebServerRequest *request);
|
||||
void handleUpdating(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
|
||||
void handleUpdateDone(AsyncWebServerRequest *request);
|
||||
|
||||
// JSON / SPA API (portal is a single-page app; only GET / serves HTML)
|
||||
void handleApiBootstrap(AsyncWebServerRequest *request);
|
||||
void handleApiWifiScanStatus(AsyncWebServerRequest *request);
|
||||
void handleApiWifiScan(AsyncWebServerRequest *request);
|
||||
@@ -81,39 +52,58 @@ class WiFiManagerHandlers {
|
||||
void handleApiInfo(AsyncWebServerRequest *request);
|
||||
void handleApiStatus(AsyncWebServerRequest *request);
|
||||
void handleApiDeviceRestart(AsyncWebServerRequest *request);
|
||||
void handleApiDeviceErase(AsyncWebServerRequest *request);
|
||||
void handleApiDeviceErase(AsyncWebServerRequest *request, boolean optionalErase = false);
|
||||
void handleApiPortalClose(AsyncWebServerRequest *request);
|
||||
void handleApiPortalExit(AsyncWebServerRequest *request);
|
||||
|
||||
// Captive Portal
|
||||
boolean captivePortal(AsyncWebServerRequest *request);
|
||||
void stopCaptivePortal();
|
||||
|
||||
// Rendering Methods
|
||||
String getMenuOut();
|
||||
String getMenuOut(String* outOpt);
|
||||
String getScanItemOut();
|
||||
String getParamOut();
|
||||
String getStaticOut();
|
||||
String getIpForm(String id, String title, String value);
|
||||
String getInfoData(String id);
|
||||
void reportStatus(String &page);
|
||||
/** True when the HTTP Host header does not match the portal’s canonical host:port (captive redirect needed). */
|
||||
static bool shouldRedirectCaptiveForHost(const String& requestHost, const String& serverLocWithPort);
|
||||
|
||||
// Internal helper (needed by handlers)
|
||||
void doParamSave(WiFiManager::WiFiManagerRequestArgs requestArgs);
|
||||
|
||||
/** Portal bootstrap JSON (same payload as GET /api/bootstrap). Public for tests and host integration. */
|
||||
String buildPortalBootstrapJson();
|
||||
/** Same JSON body as GET /api/wifi/meta (tests + embedding). */
|
||||
String buildApiWifiMetaJson();
|
||||
/** Same JSON body as GET /api/info (tests + embedding). */
|
||||
String buildApiInfoJson();
|
||||
/** Same JSON body as GET /api/params (tests + embedding). */
|
||||
String buildApiParamsGetJson();
|
||||
/** Same JSON body as GET /api/status (tests + embedding). */
|
||||
String buildApiStatusJson();
|
||||
|
||||
/** Fixed JSON bodies for POST action endpoints (single source for handlers + tests). */
|
||||
static String jsonApiDeviceRestartScheduled();
|
||||
static String jsonApiParamsSaveOk();
|
||||
static String jsonApiPortalCloseOk();
|
||||
static String jsonApiPortalExitOk();
|
||||
static String jsonApiPortalExitForbidden();
|
||||
static String jsonApiOtaUpdateSuccess();
|
||||
static String jsonApiEraseResponse(boolean success);
|
||||
|
||||
private:
|
||||
WiFiManager* _wm;
|
||||
void collectVisibleScanResults(std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
void appendVisibleScanResultsJson(String& json, const std::vector<const WiFiManager::WiFiScanNetwork*>& networks);
|
||||
|
||||
String buildPortalBootstrapJson();
|
||||
void applyWifiAndParamsFromRequest(AsyncWebServerRequest *request);
|
||||
void buildPlainStatusSummary(String& out);
|
||||
void appendPortalJsonStaticFields(String& json, bool& first);
|
||||
void appendPortalJsonCustomParams(String& json, bool& first);
|
||||
|
||||
void appendJsonKvItem(String& json, bool& first, const char* key, const String& label, const String& value);
|
||||
void appendOneInfoItemForId(String& json, bool& first, const char* id);
|
||||
void appendInfoSectionFromIds(String& json, const char* const* ids, size_t count, bool& first);
|
||||
|
||||
void appendPortalUiFeatureFlagsJson(String& json);
|
||||
void appendApiInfoActionsJson(String& json);
|
||||
|
||||
static void sendApiJson(AsyncWebServerRequest *request, int code, const String& json);
|
||||
};
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#endif // WiFiManagerHandlers_h
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* WiFiManagerLogLevel.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Compile-time and runtime log levels for WiFiManager. Higher numeric values
|
||||
* mean more verbose output. Matches the old 0..5 tier model for build flags.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* WiFiManagerLogSink.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Optional host-provided sink for WiFiManager log output. When set, logs are
|
||||
* delivered here instead of the configured Print stream (usually Serial).
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* WiFiManagerLogTemplates.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Template implementations for WiFiManager::log(...). Included at the end of WiFiManager.h.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* WiFiManagerParameter.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerParameter_h
|
||||
#define WiFiManagerParameter_h
|
||||
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
/**
|
||||
* WiFiManagerServer.h
|
||||
*
|
||||
* Server lifecycle management for WiFiManager
|
||||
* Handles AsyncWebServer creation, route registration, and shutdown
|
||||
*
|
||||
* @author tablatronix
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* HTTP server for the config portal: single HTML shell (GET /), JSON under /api/..., OTA POST /u,
|
||||
* captive-portal redirects, and 404 handling. No legacy page routes.
|
||||
*
|
||||
* Supported surface (authoritative; registerRoutes mirrors this):
|
||||
* GET /
|
||||
* GET /api/bootstrap
|
||||
* GET /api/wifi/scan-status
|
||||
* POST /api/wifi/scan
|
||||
* GET /api/wifi/meta
|
||||
* POST /api/wifi/save
|
||||
* GET /api/params
|
||||
* POST /api/params/save
|
||||
* GET /api/info
|
||||
* GET /api/status
|
||||
* POST /api/device/restart
|
||||
* POST /api/device/erase
|
||||
* POST /api/portal/close
|
||||
* POST /api/portal/exit
|
||||
* POST /u (multipart firmware upload; JSON completion response)
|
||||
* notFound -> captive redirect or 404
|
||||
*/
|
||||
|
||||
#ifndef WiFiManagerServer_h
|
||||
@@ -19,17 +36,13 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
// DFTE (template engine)
|
||||
#include <TemplateEngine.h>
|
||||
|
||||
#ifndef WM_TEMPLATE_REGISTRY_CAPACITY
|
||||
#define WM_TEMPLATE_REGISTRY_CAPACITY 24
|
||||
#define WM_TEMPLATE_REGISTRY_CAPACITY 16
|
||||
#endif
|
||||
|
||||
// Forward declarations
|
||||
class WiFiManager;
|
||||
|
||||
// Forward declaration - full definition needed in .cpp file
|
||||
class WiFiManagerHandlers;
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
@@ -37,23 +50,9 @@ class WiFiManagerDfteLogger;
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// HTTP ROUTES
|
||||
// HTTP routes (portal API only; no legacy /wifi, /info, … page URLs)
|
||||
|
||||
const char R_root[] PROGMEM = "/";
|
||||
const char R_wifi[] PROGMEM = "/wifi";
|
||||
const char R_wifinoscan[] PROGMEM = "/0wifi";
|
||||
const char R_wifisave[] PROGMEM = "/wifisave";
|
||||
const char R_info[] PROGMEM = "/info";
|
||||
const char R_param[] PROGMEM = "/param";
|
||||
const char R_paramsave[] PROGMEM = "/paramsave";
|
||||
const char R_restart[] PROGMEM = "/restart";
|
||||
const char R_exit[] PROGMEM = "/exit";
|
||||
const char R_close[] PROGMEM = "/close";
|
||||
const char R_erase[] PROGMEM = "/erase";
|
||||
const char R_scanstatus[] PROGMEM = "/wifistatus";
|
||||
const char R_scan[] PROGMEM = "/wifi/scan";
|
||||
const char R_update[] PROGMEM = "/update";
|
||||
const char R_updatedone[] PROGMEM = "/u";
|
||||
|
||||
const char R_api_bootstrap[] PROGMEM = "/api/bootstrap";
|
||||
const char R_api_wifi_scan_status[] PROGMEM = "/api/wifi/scan-status";
|
||||
@@ -69,60 +68,32 @@ const char R_api_device_erase[] PROGMEM = "/api/device/erase";
|
||||
const char R_api_portal_close[] PROGMEM = "/api/portal/close";
|
||||
const char R_api_portal_exit[] PROGMEM = "/api/portal/exit";
|
||||
|
||||
const char R_updatedone[] PROGMEM = "/u";
|
||||
|
||||
class WiFiManagerServer {
|
||||
public:
|
||||
// Singleton access
|
||||
static WiFiManagerServer* instance();
|
||||
|
||||
WiFiManagerServer(WiFiManager* wm);
|
||||
~WiFiManagerServer();
|
||||
|
||||
// Template engine setup and access
|
||||
void setupTemplateEngine();
|
||||
PlaceholderRegistry* getPlaceholderRegistry() { return _tplRegistry.get(); }
|
||||
|
||||
// Static zero-arg getters for DFTE RAM placeholders
|
||||
static const char* tplGetPageTitle();
|
||||
static const char* tplGetSubtitle();
|
||||
static const char* tplGetMenu();
|
||||
static const char* tplGetStatus();
|
||||
|
||||
// Template customization API
|
||||
// Consumers can register a callback to customize placeholders
|
||||
void registerTemplateSetupCallback(std::function<void(PlaceholderRegistry&)> cb) { _tplSetupCallback = cb; }
|
||||
|
||||
// Convenience helpers to register our defaults (granular + all)
|
||||
void registerDefaultStyles(PlaceholderRegistry& reg);
|
||||
void registerDefaultScripts(PlaceholderRegistry& reg);
|
||||
void registerDefaultPageTitle(PlaceholderRegistry& reg);
|
||||
void registerDefaultSubtitle(PlaceholderRegistry& reg);
|
||||
void registerDefaultMenu(PlaceholderRegistry& reg);
|
||||
void registerDefaultStatus(PlaceholderRegistry& reg);
|
||||
/** Shell placeholders only: %STYLES%, %SCRIPTS%, %PAGE_TITLE%. */
|
||||
void registerDefaultPlaceholders(PlaceholderRegistry& reg);
|
||||
void applyTemplateSetupCallback(PlaceholderRegistry& reg);
|
||||
|
||||
// Rebuild registry with defaults and optional customizer
|
||||
void rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer = nullptr);
|
||||
|
||||
// Create server instance
|
||||
void createServer(uint16_t port);
|
||||
|
||||
// Register all routes (takes handler callbacks)
|
||||
void registerRoutes();
|
||||
|
||||
// Setup DNS server for captive portal
|
||||
void setupDNSD();
|
||||
|
||||
// Process DNS requests (call periodically)
|
||||
void processDNS();
|
||||
|
||||
// Shutdown and cleanup server and DNS
|
||||
void shutdownServer();
|
||||
|
||||
// Get server instance
|
||||
AsyncWebServer* getServer() { return server.get(); }
|
||||
|
||||
// Get DNS server instance (for testing)
|
||||
DNSServer* getDNSServer() { return dnsServer.get(); }
|
||||
|
||||
private:
|
||||
@@ -131,24 +102,16 @@ class WiFiManagerServer {
|
||||
std::unique_ptr<AsyncWebServer> server;
|
||||
std::unique_ptr<DNSServer> dnsServer;
|
||||
|
||||
// Shared DFTE placeholder registry
|
||||
std::unique_ptr<PlaceholderRegistry> _tplRegistry;
|
||||
std::function<void(PlaceholderRegistry&)> _tplSetupCallback;
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
// DFTE -> WiFiManager::log bridge (only installed when WM_DFTE_LOGGING and no other logger registered)
|
||||
std::unique_ptr<WiFiManagerDfteLogger> _dfteLogger;
|
||||
bool _wmOwnsDfteLogSink = false;
|
||||
#endif
|
||||
|
||||
// Singleton instance
|
||||
static WiFiManagerServer* s_instance;
|
||||
};
|
||||
|
||||
// Include WiFiManagerHandlers.h after class definition to provide full definition
|
||||
// needed for std::unique_ptr destructor
|
||||
#include "WiFiManagerHandlers.h"
|
||||
|
||||
#endif // defined(ESP8266) || defined(ESP32)
|
||||
|
||||
#endif // WiFiManagerServer_h
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* String conversions, HTML encoding, and helper calculations
|
||||
* These functions have no dependency on WiFiManager instance state
|
||||
*
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* CSS.h
|
||||
* CSS stylesheet for WiFiManager web interface
|
||||
* Embedded styles to avoid external file dependencies
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* CSS stylesheet for WiFiManager web interface.
|
||||
* Embedded styles to avoid external file dependencies.
|
||||
*/
|
||||
|
||||
#ifndef _CSS_TEMPLATES_H_
|
||||
@@ -39,6 +43,21 @@ const char CSS_STYLE[] PROGMEM = "<style>"
|
||||
"body.invert .msg{color:#fff;background-color:#282828;border-top:1px solid #555;border-right:1px solid #555;border-bottom:1px solid #555;}"
|
||||
"body.invert .q[role=img]{-webkit-filter:invert(1);filter:invert(1);}"
|
||||
":disabled {opacity: 0.5;}"
|
||||
// toast + modal (SPA-native feedback; no window.alert)
|
||||
".wm-toast{position:fixed;left:50%;top:12px;transform:translateX(-50%);z-index:10001;max-width:92%;padding:10px 14px;border-radius:.35rem;"
|
||||
"box-shadow:0 2px 8px rgba(0,0,0,.2);font-size:.95rem;text-align:center;font-weight:600}"
|
||||
".wm-toast.ok{background:#e8f7ef;border:1px solid #5cb85c;color:#1a3d24}"
|
||||
".wm-toast.err{background:#fdeaea;border:1px solid #dc3630;color:#5c1616}"
|
||||
".wm-dialog{position:fixed;inset:0;z-index:10000;display:none;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}"
|
||||
".wm-dialog-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.45)}"
|
||||
".wm-dialog-panel{position:relative;z-index:1;background:#fff;border-radius:.4rem;padding:16px 18px;max-width:420px;width:100%;box-shadow:0 4px 24px rgba(0,0,0,.25);text-align:left}"
|
||||
".wm-dialog-msg{margin:0 0 12px 0;line-height:1.35}"
|
||||
".wm-dialog-actions{display:flex;gap:10px;justify-content:flex-end}"
|
||||
".wm-dialog-actions button{width:auto;min-width:100px}"
|
||||
".wm-dialog-ok{background-color:#1fa3ec !important}"
|
||||
"body.invert .wm-dialog-panel{background:#222;color:#fff;border:1px solid #444}"
|
||||
"body.invert .wm-toast.ok{background:#1a3322;border-color:#5cb85c;color:#d8f5e4}"
|
||||
"body.invert .wm-toast.err{background:#3a1818;border-color:#dc3630;color:#ffd4d4}"
|
||||
"</style>";
|
||||
|
||||
#endif // _CSS_TEMPLATES_H_
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Fragments.h
|
||||
* Reusable DFTE fragment templates for WiFiManager page composition.
|
||||
*/
|
||||
|
||||
#ifndef _WM_FRAGMENTS_TEMPLATE_H_
|
||||
#define _WM_FRAGMENTS_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_ACTION_FORM_TEMPLATE[] PROGMEM =
|
||||
"%ACTION_PREFIX%<form action='%ACTION%' method='%METHOD%'><button%BUTTON_CLASS_ATTR%>%BUTTON_LABEL%</button></form>%ACTION_SUFFIX%";
|
||||
|
||||
const char WM_SUBMIT_BUTTON_TEMPLATE[] PROGMEM =
|
||||
"<br/><br/><button type='submit'>%BUTTON_LABEL%</button>";
|
||||
|
||||
const char WM_CENTERED_BUTTON_TEMPLATE[] PROGMEM =
|
||||
"<br/><div class='c'><button id='%BUTTON_ID%' type='%BUTTON_TYPE%' onclick='%BUTTON_ONCLICK%'%BUTTON_EXTRA_ATTRS%>%BUTTON_LABEL%</button></div>";
|
||||
|
||||
const char WM_SECTION_BREAK_TEMPLATE[] PROGMEM =
|
||||
"<hr><br/>%SECTION_CONTENT%";
|
||||
|
||||
const char WM_SCAN_MESSAGE_TEMPLATE[] PROGMEM =
|
||||
"%SCAN_MESSAGE%<br/><br/>";
|
||||
|
||||
const char WM_SCAN_RESULT_ROW_TEMPLATE[] PROGMEM =
|
||||
"<div><a href='#p' onclick='c(this)' data-ssid='%SSID_ATTR%'>%SSID_TEXT%</a>"
|
||||
"<div role='img' aria-label='%QUALITY_LABEL%' title='%QUALITY_LABEL%' class='q q-%QUALITY_ICON% %LOCK_CLASS% %ICON_VISIBILITY_CLASS%'></div>"
|
||||
"<div class='q %VALUE_VISIBILITY_CLASS%'>%QUALITY_VALUE%</div></div>";
|
||||
|
||||
const char WM_FIELD_LABEL_BEFORE_TEMPLATE[] PROGMEM =
|
||||
"<label for='%FIELD_ID%'>%FIELD_LABEL%</label><br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>";
|
||||
|
||||
const char WM_FIELD_LABEL_AFTER_TEMPLATE[] PROGMEM =
|
||||
"<br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>"
|
||||
"<label for='%FIELD_ID%'>%FIELD_LABEL%</label>";
|
||||
|
||||
const char WM_FIELD_INPUT_ONLY_TEMPLATE[] PROGMEM =
|
||||
"<br/><input id='%FIELD_ID%' name='%FIELD_NAME%' maxlength='%FIELD_MAXLENGTH%' value='%FIELD_VALUE%'%FIELD_EXTRA_ATTRS%>";
|
||||
|
||||
const char WM_INFO_ROW_TEMPLATE[] PROGMEM =
|
||||
"<dt>%INFO_LABEL%</dt><dd>%INFO_VALUE%</dd>";
|
||||
|
||||
const char WM_INFO_SECTION_TEMPLATE[] PROGMEM =
|
||||
"%SECTION_PREFIX%<h3>%SECTION_TITLE%</h3><hr><dl>%SECTION_ROWS%</dl>%SECTION_SUFFIX%";
|
||||
|
||||
const char WM_STATUS_MESSAGE_TEMPLATE[] PROGMEM =
|
||||
"<div class='msg%STATUS_CLASS_SUFFIX%'><strong>%STATUS_TITLE%</strong>%STATUS_BODY%</div>";
|
||||
|
||||
const char WM_PAGE_HEADING_TEMPLATE[] PROGMEM =
|
||||
"<h1>%HEADER_TITLE%</h1><h3>%HEADER_SUBTITLE%</h3>";
|
||||
|
||||
#endif // _WM_FRAGMENTS_TEMPLATE_H_
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* HTML.h
|
||||
* HTML template fragments for WiFiManager web interface
|
||||
* Static PROGMEM strings to reduce string concatenations
|
||||
*/
|
||||
|
||||
#ifndef _HTML_TEMPLATES_H_
|
||||
#define _HTML_TEMPLATES_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Status messages
|
||||
const char HTML_STATUS_OFFPW[] PROGMEM = "<br/>Authentication failure";
|
||||
|
||||
const char HTML_STATUS_OFFNOAP[] PROGMEM = "<br/>AP not found";
|
||||
|
||||
const char HTML_STATUS_OFFFAIL[] PROGMEM = "<br/>Could not connect";
|
||||
|
||||
const char HTML_STATUS_NONE[] PROGMEM = "<div class='msg'>No AP set</div>";
|
||||
|
||||
const char HTML_SAVED[] PROGMEM = "<div class='msg'>Saving Credentials<br/>Trying to connect ESP to network.<br />If it fails reconnect to AP to try again</div>";
|
||||
|
||||
const char HTML_PARAMSAVED[] PROGMEM = "<div class='msg S'>Saved<br/></div>";
|
||||
|
||||
// Update page
|
||||
const char HTML_UPDATE[] PROGMEM = "Upload new firmware<br/><form method='POST' action='u' enctype='multipart/form-data' onchange=\"(function(el){document.getElementById('uploadbin').style.display = el.value=='' ? 'none' : 'initial';})(this)\"><input type='file' name='update' accept='.bin,application/octet-stream'><button id='uploadbin' type='submit' class='h D'>Update</button></form><small><a href='http://192.168.4.1/update' target='_blank'>* May not function inside captive portal, open in browser http://192.168.4.1</a></small>";
|
||||
|
||||
const char HTML_UPDATE_FAIL[] PROGMEM = "<div class='msg D'><strong>Update failed!</strong><Br/>Reboot device and try again</div>";
|
||||
|
||||
const char HTML_UPDATE_SUCCESS[] PROGMEM = "<div class='msg S'><strong>Update successful. </strong> <br/> Device rebooting now...</div>";
|
||||
|
||||
// Help page
|
||||
#ifndef WM_NOHELP
|
||||
const char HTML_HELP[] PROGMEM =
|
||||
"<br/><h3>Available pages</h3><hr>"
|
||||
"<table class='table'>"
|
||||
"<thead><tr><th>Page</th><th>Function</th></tr></thead><tbody>"
|
||||
"<tr><td><a href='/'>/</a></td>"
|
||||
"<td>Menu page.</td></tr>"
|
||||
"<tr><td><a href='/wifi'>/wifi</a></td>"
|
||||
"<td>Show WiFi scan results and enter WiFi configuration.</td></tr>"
|
||||
"<tr><td><a href='/0wifi'>/0wifi</a></td>"
|
||||
"<td>Show WiFi configuration without loading scan results until refresh is requested.</td></tr>"
|
||||
"<tr><td><a href='/wifisave'>/wifisave</a></td>"
|
||||
"<td>Save WiFi configuration information and configure device. Needs variables supplied.</td></tr>"
|
||||
"<tr><td><a href='/param'>/param</a></td>"
|
||||
"<td>Parameter page</td></tr>"
|
||||
"<tr><td><a href='/info'>/info</a></td>"
|
||||
"<td>Information page</td></tr>"
|
||||
"<tr><td><a href='/update'>/update</a></td>"
|
||||
"<td>OTA update page. Firmware upload posts to /u.</td></tr>"
|
||||
"<tr><td><a href='/close'>/close</a></td>"
|
||||
"<td>Close the captiveportal popup, config portal will remain active</td></tr>"
|
||||
"<tr><td>/exit</td>"
|
||||
"<td>Exit Config portal, config portal will close</td></tr>"
|
||||
"<tr><td>/restart</td>"
|
||||
"<td>Reboot the device</td></tr>"
|
||||
"<tr><td>/erase</td>"
|
||||
"<td>Erase WiFi configuration and reboot device. Device will not reconnect to a network until new WiFi configuration data is entered.</td></tr>"
|
||||
"</table>"
|
||||
"<p/>Fork <a href='https://github.com/alexhopeoconnor/WiFiManager'>https://github.com/alexhopeoconnor/WiFiManager</a>.";
|
||||
#else
|
||||
const char HTML_HELP[] PROGMEM = "";
|
||||
#endif
|
||||
|
||||
#endif // _HTML_TEMPLATES_H_
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Info.h
|
||||
* Streamed info page content template for WiFiManager.
|
||||
*/
|
||||
|
||||
#ifndef _WM_INFO_TEMPLATE_H_
|
||||
#define _WM_INFO_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_INFO_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"%INFO_STATUS%"
|
||||
"<section class='info-device'>%INFO_DEVICE_SECTION%</section>"
|
||||
"<section class='info-wifi'>%INFO_WIFI_SECTION%</section>"
|
||||
"<section class='info-about'>%INFO_ABOUT_SECTION%</section>"
|
||||
"%INFO_FOOTER%";
|
||||
|
||||
#endif // _WM_INFO_TEMPLATE_H_
|
||||
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* JS.h
|
||||
* JavaScript code for WiFiManager web interface
|
||||
* Client-side functionality for WiFi configuration
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* JavaScript code for WiFiManager web interface.
|
||||
* Client-side functionality for WiFi configuration.
|
||||
*/
|
||||
|
||||
#ifndef _JS_TEMPLATES_H_
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/**
|
||||
* Message.h
|
||||
* Streamed message page content template for WiFiManager.
|
||||
*/
|
||||
|
||||
#ifndef _WM_MESSAGE_TEMPLATE_H_
|
||||
#define _WM_MESSAGE_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_MESSAGE_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"%MESSAGE_BODY%"
|
||||
"%MESSAGE_ACTIONS%";
|
||||
|
||||
#endif // _WM_MESSAGE_TEMPLATE_H_
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* PageShell.h
|
||||
* Generic whole-document shell template for streamed WiFiManager pages.
|
||||
*/
|
||||
|
||||
#ifndef _WM_PAGE_SHELL_TEMPLATE_H_
|
||||
#define _WM_PAGE_SHELL_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_PAGE_SHELL_TEMPLATE[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'>"
|
||||
"<head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%DOC_TITLE%</title>"
|
||||
"%SCRIPTS%"
|
||||
"%PAGE_SCRIPTS%"
|
||||
"%STYLES%"
|
||||
"%PAGE_STYLES%"
|
||||
"</head>"
|
||||
"<body class='%BODY_CLASS%'>"
|
||||
"<div class='wrap'>"
|
||||
"%PAGE_CONTENT%"
|
||||
"</div>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
#endif // _WM_PAGE_SHELL_TEMPLATE_H_
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Param.h
|
||||
* Streamed setup page content template for WiFiManager.
|
||||
*/
|
||||
|
||||
#ifndef _WM_PARAM_TEMPLATE_H_
|
||||
#define _WM_PARAM_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_PARAM_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"<form method='POST' action='paramsave'>"
|
||||
"%PARAM_FIELDS%"
|
||||
"%PARAM_FORM_ACTIONS%</form>"
|
||||
"%PARAM_PAGE_ACTIONS%"
|
||||
"%PARAM_STATUS%";
|
||||
|
||||
#endif // _WM_PARAM_TEMPLATE_H_
|
||||
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* PortalAppJS.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Embedded SPA for WiFiManager config portal (hash routing + JSON APIs).
|
||||
*/
|
||||
|
||||
@@ -17,30 +21,96 @@ try{var el=document.getElementById('wm-bootstrap');if(el&&el.textContent)boot=JS
|
||||
function $(id){return document.getElementById(id);}
|
||||
function esc(t){if(!t)return'';return String(t).replace(/[&<>"']/g,function(c){return({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]);});}
|
||||
|
||||
var _wmToastTimer=null;
|
||||
function showToast(msg,isErr){
|
||||
var el=$('wm-toast');
|
||||
if(!el)return;
|
||||
el.textContent=msg||'';
|
||||
el.className='wm-toast '+(isErr?'err':'ok');
|
||||
el.style.display='block';
|
||||
if(_wmToastTimer)clearTimeout(_wmToastTimer);
|
||||
_wmToastTimer=setTimeout(function(){el.style.display='none';el.textContent='';},4500);
|
||||
}
|
||||
function showConfirm(msg,onOk){
|
||||
var dlg=$('wm-dialog'),dmsg=$('wm-dialog-msg'),bk=$('wm-dialog-backdrop'),ok=$('wm-dialog-ok'),cancel=$('wm-dialog-cancel');
|
||||
if(!dlg||!dmsg||!ok||!cancel){if(onOk)onOk();return;}
|
||||
dmsg.textContent=msg||'';
|
||||
dlg.style.display='flex';
|
||||
dlg.setAttribute('aria-hidden','false');
|
||||
function hide(){
|
||||
dlg.style.display='none';
|
||||
dlg.setAttribute('aria-hidden','true');
|
||||
ok.onclick=null;
|
||||
cancel.onclick=null;
|
||||
if(bk)bk.onclick=null;
|
||||
}
|
||||
function onCancel(){hide();}
|
||||
cancel.onclick=onCancel;
|
||||
if(bk)bk.onclick=onCancel;
|
||||
ok.onclick=function(){hide();if(onOk)onOk();};
|
||||
}
|
||||
|
||||
function setView(html){
|
||||
var a=$('app');
|
||||
if(!a)return;
|
||||
a.innerHTML="<div class='wrap'>"+html+"</div>";
|
||||
}
|
||||
|
||||
function renderMetaField(f){
|
||||
if(!f)return'';
|
||||
if(f.html)return f.html;
|
||||
var id=f.id||f.name||'';
|
||||
var nm=f.name||f.id||'';
|
||||
var domId=(id==='s')?'wm-s':(id==='p')?'wm-p':('wm-f-'+id);
|
||||
var t=f.type||'text';
|
||||
var h='<label for="'+domId+'">'+esc(f.label||'')+'</label>';
|
||||
h+='<input id="'+domId+'" name="'+esc(nm)+'" type="'+esc(t)+'"';
|
||||
if(f.maxlength)h+=' maxlength="'+f.maxlength+'"';
|
||||
if(f.value!==undefined&&f.value!==null)h+=' value="'+esc(f.value)+'"';
|
||||
if(f.placeholder)h+=' placeholder="'+esc(f.placeholder)+'"';
|
||||
if(f.customAttrs)h+=' '+f.customAttrs;
|
||||
h+='/>';
|
||||
return h;
|
||||
}
|
||||
|
||||
function renderFieldList(arr){
|
||||
if(!arr||!arr.length)return'';
|
||||
var h='',i;
|
||||
for(i=0;i<arr.length;i++)h+=renderMetaField(arr[i]);
|
||||
return h;
|
||||
}
|
||||
|
||||
function navBar(){
|
||||
var f=boot.features||{};
|
||||
var h="<p style='text-align:center'>";
|
||||
h+="<a href='#/'>Home</a> · ";
|
||||
h+="<a href='#/wifi'>WiFi</a>";
|
||||
if(!f.paramsInWifi){h+=" · <a href='#/setup'>Setup</a>";}
|
||||
h+=" · <a href='#/info'>Info</a>";
|
||||
h+=" · <a href='#/update'>Update</a>";
|
||||
if(f.showInfo!==false){h+=" · <a href='#/info'>Info</a>";}
|
||||
if(f.showUpdate){h+=" · <a href='#/update'>Update</a>";}
|
||||
if(boot.showBack){h+=" · <a href='#' onclick='history.back();return false;'>Back</a>";}
|
||||
h+="</p><hr/>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function deviceActionsHtml(){
|
||||
var f=boot.features||{};
|
||||
var h='', ok=false;
|
||||
if(f.showRestart){h+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";ok=true;}
|
||||
if(f.showExitPortal){h+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";ok=true;}
|
||||
if(f.showCloseCaptive){h+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";ok=true;}
|
||||
if(!ok)return'';
|
||||
return "<div class='device-actions'><h3>Device actions</h3>"+h+"</div>";
|
||||
}
|
||||
|
||||
function viewHome(){
|
||||
var sub=esc(boot.subtitle||'');
|
||||
var t=esc(boot.title||'WiFiManager');
|
||||
var html=navBar();
|
||||
html+="<h1>"+t+"</h1><h3>"+sub+"</h3>";
|
||||
if(boot.initialStatus)html+="<p class='status'>"+esc(boot.initialStatus)+"</p>";
|
||||
html+="<p>Use the links above to configure WiFi, view device info, or update firmware.</p>";
|
||||
html+=deviceActionsHtml();
|
||||
setView(html);
|
||||
}
|
||||
|
||||
@@ -99,11 +169,16 @@ function viewWifi(){
|
||||
var html=navBar()+"<h2>WiFi</h2>";
|
||||
html+="<div id='wm-scan-results'></div>";
|
||||
html+="<form id='wm-wifi-form' onsubmit='return portalWifiSave(event)'>";
|
||||
html+="<label>SSID</label><input id='wm-s' name='s' maxlength='32' value='"+esc(m.ssidPlaceholder||'')+"'/>";
|
||||
html+="<label>Password</label><input id='wm-p' name='p' type='password' maxlength='64' placeholder='"+esc(m.passwordPlaceholder||'')+"'/>";
|
||||
html+="<input type='checkbox' id='wm-showpass' onclick='var p=document.getElementById(\"wm-p\");p.type=this.checked?\"text\":\"password\"'/> <label for='wm-showpass'>Show password</label>";
|
||||
if(m.staticFieldsHtml){html+=m.staticFieldsHtml;}
|
||||
if(m.paramsHtml){html+=m.paramsHtml;}
|
||||
html+=renderFieldList(m.wifiFields||[]);
|
||||
var wf=m.wifiFields||[];
|
||||
var hasPass=false;
|
||||
var i;
|
||||
for(i=0;i<wf.length;i++){if(wf[i]&&wf[i].id==='p'){hasPass=true;break;}}
|
||||
if(hasPass){
|
||||
html+="<input type='checkbox' id='wm-showpass' onclick='var p=document.getElementById(\"wm-p\");if(p)p.type=this.checked?\"text\":\"password\"'/> <label for='wm-showpass'>Show password</label>";
|
||||
}
|
||||
html+=renderFieldList(m.staticFields||[]);
|
||||
html+=renderFieldList(m.params||[]);
|
||||
html+="<button type='submit'>Save</button></form>";
|
||||
html+="<br/><button type='button' id='wm-refresh-scan' onclick='wifiRefresh()'>Refresh scan</button>";
|
||||
html+="<div id='wm-wifi-msg'></div>";
|
||||
@@ -143,22 +218,52 @@ function viewInfo(){
|
||||
h+="</dl>";
|
||||
return h;
|
||||
}
|
||||
html+=section('Status',d.status||[]);
|
||||
var st=d.status;
|
||||
if(st && typeof st==='object' && !Array.isArray(st)){
|
||||
html+="<h3>Status</h3><hr/><dl>";
|
||||
html+="<dt>Connected</dt><dd>"+esc(String(st.connected))+"</dd>";
|
||||
if(st.ssid!==undefined)html+="<dt>SSID</dt><dd>"+esc(st.ssid)+"</dd>";
|
||||
if(st.stationIp)html+="<dt>Station IP</dt><dd>"+esc(st.stationIp)+"</dd>";
|
||||
if(st.apIp)html+="<dt>AP IP</dt><dd>"+esc(st.apIp)+"</dd>";
|
||||
if(st.summary)html+="<dt>Summary</dt><dd>"+esc(st.summary)+"</dd>";
|
||||
html+="</dl>";
|
||||
} else {
|
||||
html+=section('Status',d.status||[]);
|
||||
}
|
||||
html+=section('Device',d.device||[]);
|
||||
html+=section('WiFi',d.wifi||[]);
|
||||
html+=section('About',d.about||[]);
|
||||
var act=d.actions||{};
|
||||
html+="<h3>Actions</h3><hr/>";
|
||||
if(act.showRestart)html+="<p><button type='button' onclick='portalRestart()'>Restart</button></p>";
|
||||
if(act.showExitPortal)html+="<p><button type='button' onclick='portalExit()'>Exit portal</button></p>";
|
||||
if(act.showCloseCaptive)html+="<p><button type='button' onclick='portalCloseCaptive()'>Stop captive portal detection</button></p>";
|
||||
if(act.showUpdate)html+="<p><a href='#/update'>Firmware update</a></p>";
|
||||
if(act.showErase)html+="<p><button type='button' class='D' onclick='portalErase()'>Erase WiFi config</button></p>";
|
||||
setView(html);
|
||||
});
|
||||
}
|
||||
|
||||
function portalConfirmPost(url, promptMsg){
|
||||
showConfirm(promptMsg||'Continue?',function(){
|
||||
api(url,{method:'POST'}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
var m=(j&&j.message)?j.message:(res.ok?'OK':'Error');
|
||||
showToast(m,!res.ok);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.portalRestart=function(){portalConfirmPost('/api/device/restart','Restart the device?');};
|
||||
window.portalExit=function(){portalConfirmPost('/api/portal/exit','Exit the configuration portal?');};
|
||||
window.portalCloseCaptive=function(){portalConfirmPost('/api/portal/close','Stop redirecting clients to this portal (captive detection off)?');};
|
||||
|
||||
window.portalErase=function(){
|
||||
if(!confirm('Erase WiFi configuration?'))return;
|
||||
api('/api/device/erase',{method:'POST'}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
alert((j&&j.message)?j.message:'Done');
|
||||
showConfirm('Erase WiFi configuration?',function(){
|
||||
api('/api/device/erase',{method:'POST'}).then(function(res){
|
||||
try{var j=JSON.parse(res.body);}catch(e){j={};}
|
||||
showToast((j&&j.message)?j.message:(res.ok?'Done':'Error'),!res.ok);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,7 +272,9 @@ function viewSetup(){
|
||||
api('/api/params').then(function(res){
|
||||
try{var d=JSON.parse(res.body);}catch(e){d={};}
|
||||
var html=navBar()+"<h2>Parameters</h2>";
|
||||
html+="<form id='wm-param-form' onsubmit='return portalParamSave(event)'>"+(d.paramsHtml||'')+"<button type='submit'>Save</button></form>";
|
||||
html+="<form id='wm-param-form' onsubmit='return portalParamSave(event)'>";
|
||||
html+=renderFieldList(d.params||[]);
|
||||
html+="<button type='submit'>Save</button></form>";
|
||||
html+="<div id='wm-param-msg'></div>";
|
||||
setView(html);
|
||||
});
|
||||
@@ -186,17 +293,59 @@ window.portalParamSave=function(ev){
|
||||
};
|
||||
|
||||
function viewUpdate(){
|
||||
var f=boot.features||{};
|
||||
var html=navBar()+"<h2>Firmware update</h2>";
|
||||
html+="<form method='POST' action='/u' enctype='multipart/form-data'>";
|
||||
html+="<input type='file' name='update'/>";
|
||||
html+="<button type='submit'>Update</button></form>";
|
||||
html+="<p><small>Upload a .bin firmware. Device may restart after update.</small></p>";
|
||||
if(!f.showUpdate){html+="<p>Firmware update is disabled.</p>";setView(html);return;}
|
||||
html+="<form id='wm-ota-form' onsubmit='return portalOtaSubmit(event)'>";
|
||||
html+="<input type='file' id='wm-ota-file' name='update'/>";
|
||||
html+="<button type='submit'>Upload</button></form>";
|
||||
html+="<p><small>Upload a .bin firmware. The device restarts after a successful update.</small></p>";
|
||||
html+="<pre id='wm-ota-log'></pre>";
|
||||
setView(html);
|
||||
}
|
||||
|
||||
window.portalOtaSubmit=function(ev){
|
||||
ev.preventDefault();
|
||||
var fi=$('wm-ota-file');
|
||||
var logEl=$('wm-ota-log');
|
||||
if(!fi||!fi.files||!fi.files[0]){
|
||||
if(logEl)logEl.textContent='Choose a firmware file first.';
|
||||
return false;
|
||||
}
|
||||
if(logEl)logEl.textContent='Uploading…';
|
||||
var xhr=new XMLHttpRequest();
|
||||
xhr.upload.onprogress=function(e){
|
||||
if(!logEl||!e.lengthComputable)return;
|
||||
logEl.textContent='Uploading… '+Math.round(100*e.loaded/e.total)+'%';
|
||||
};
|
||||
xhr.onreadystatechange=function(){
|
||||
if(xhr.readyState!==4)return;
|
||||
var t=xhr.responseText||'';
|
||||
if(logEl)logEl.textContent=t;
|
||||
var httpOk=xhr.status>=200&&xhr.status<300;
|
||||
try{
|
||||
var j=JSON.parse(t);
|
||||
if(j&&j.message){
|
||||
if(logEl)logEl.textContent=j.message;
|
||||
showToast(j.message,j.ok===false);
|
||||
return;
|
||||
}
|
||||
}catch(e){}
|
||||
showToast(httpOk?'Upload finished.':('HTTP '+xhr.status),!httpOk);
|
||||
};
|
||||
xhr.open('POST','/u');
|
||||
var fd=new FormData();
|
||||
fd.append('update',fi.files[0]);
|
||||
xhr.send(fd);
|
||||
return false;
|
||||
};
|
||||
|
||||
function route(){
|
||||
var h=location.hash||'#/';
|
||||
if(h.indexOf('#/')!==0)h='#/';
|
||||
var f=boot.features||{};
|
||||
if(h==='#/info'&&f.showInfo===false){viewHome();return;}
|
||||
if(h==='#/update'&&f.showUpdate===false){viewHome();return;}
|
||||
if(h==='#/'||h==='#'){viewHome();return;}
|
||||
if(h==='#/wifi'){viewWifi();return;}
|
||||
if(h==='#/info'){viewInfo();return;}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Root.h
|
||||
* Whole-document root template for WiFiManager using DFTE placeholders
|
||||
*/
|
||||
|
||||
#ifndef _WM_ROOT_TEMPLATE_H_
|
||||
#define _WM_ROOT_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Whole document template:
|
||||
// - Injects %SCRIPTS% and %STYLES% (these already include <script>/<style> tags)
|
||||
// - Mirrors existing structure: body class "home" and wrapper div.wrap
|
||||
// - Renders title/subtitle, then menu and status
|
||||
const char WM_ROOT_TEMPLATE[] PROGMEM =
|
||||
"<!DOCTYPE html>"
|
||||
"<html lang='en'>"
|
||||
"<head>"
|
||||
"<meta name='format-detection' content='telephone=no'>"
|
||||
"<meta charset='UTF-8'>"
|
||||
"<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'/>"
|
||||
"<title>%PAGE_TITLE%</title>"
|
||||
"%SCRIPTS%"
|
||||
"%STYLES%"
|
||||
"</head>"
|
||||
"<body class='home'>"
|
||||
"<div class='wrap'>"
|
||||
"<h1>%PAGE_TITLE%</h1>"
|
||||
"<h3>%SUBTITLE%</h3>"
|
||||
"%MENU%"
|
||||
"%STATUS%"
|
||||
"</div>"
|
||||
"</body>"
|
||||
"</html>";
|
||||
|
||||
#endif // _WM_ROOT_TEMPLATE_H_
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* RootSelector.h
|
||||
* Allows overriding the root template header via build flag
|
||||
* Define WM_CUSTOM_ROOT_TEMPLATE_HEADER to a quoted header path.
|
||||
*/
|
||||
#ifndef _WM_ROOT_SELECTOR_H_
|
||||
#define _WM_ROOT_SELECTOR_H_
|
||||
|
||||
#ifdef WM_CUSTOM_ROOT_TEMPLATE_HEADER
|
||||
#include WM_CUSTOM_ROOT_TEMPLATE_HEADER
|
||||
#else
|
||||
#include "RootShell.h"
|
||||
#endif
|
||||
|
||||
#endif // _WM_ROOT_SELECTOR_H_
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/**
|
||||
* RootShell.h
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Single-page portal shell: only HTML document served for GET /.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/*.
|
||||
* Dynamic UI is driven by embedded JS + JSON APIs under /api/...
|
||||
*/
|
||||
|
||||
#ifndef _WM_ROOT_SHELL_TEMPLATE_H_
|
||||
@@ -21,6 +25,15 @@ const char WM_ROOT_SHELL_TEMPLATE[] PROGMEM =
|
||||
"%STYLES%"
|
||||
"</head>"
|
||||
"<body class='portal'>"
|
||||
"<div id='wm-toast' class='wm-toast' aria-live='polite' role='status' style='display:none'></div>"
|
||||
"<div id='wm-dialog' class='wm-dialog' style='display:none' aria-hidden='true'>"
|
||||
"<div class='wm-dialog-backdrop' id='wm-dialog-backdrop'></div>"
|
||||
"<div class='wm-dialog-panel'>"
|
||||
"<p class='wm-dialog-msg' id='wm-dialog-msg'></p>"
|
||||
"<div class='wm-dialog-actions'>"
|
||||
"<button type='button' id='wm-dialog-cancel'>Cancel</button>"
|
||||
"<button type='button' id='wm-dialog-ok' class='wm-dialog-ok'>OK</button>"
|
||||
"</div></div></div>"
|
||||
"<div id='app'></div>"
|
||||
"<script id='wm-bootstrap' type='application/json'>%BOOTSTRAP_JSON%</script>"
|
||||
"<script>"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* WiFi.h
|
||||
* Streamed WiFi configuration page content template for WiFiManager.
|
||||
*/
|
||||
|
||||
#ifndef _WM_WIFI_TEMPLATE_H_
|
||||
#define _WM_WIFI_TEMPLATE_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char WM_WIFI_CONTENT_TEMPLATE[] PROGMEM =
|
||||
"<div id='scan-results'>%WIFI_SCAN_CONTENT%</div>"
|
||||
"<form method='POST' action='wifisave'>"
|
||||
"<label for='s'>SSID</label>"
|
||||
"<input id='s' name='s' maxlength='32' autocorrect='off' autocapitalize='none' placeholder='%WIFI_SSID_PLACEHOLDER%'>"
|
||||
"<br/>"
|
||||
"<label for='p'>Password</label>"
|
||||
"<input id='p' name='p' maxlength='64' type='password' placeholder='%WIFI_PASSWORD_PLACEHOLDER%'>"
|
||||
"<input type='checkbox' id='showpass' onclick='f()'> <label for='showpass'>Show Password</label><br/>"
|
||||
"%WIFI_STATIC_FIELDS%"
|
||||
"%WIFI_PARAM_SECTION%"
|
||||
"%WIFI_FORM_ACTIONS%</form>"
|
||||
"%WIFI_PAGE_ACTIONS%"
|
||||
"%WIFI_STATUS%";
|
||||
|
||||
#endif // _WM_WIFI_TEMPLATE_H_
|
||||
@@ -1,168 +0,0 @@
|
||||
/**
|
||||
* WiFiPollingJS.h
|
||||
* JavaScript code for WiFi network scanning with AJAX polling
|
||||
* This is embedded in the WiFi configuration page for async WiFi scanning
|
||||
*/
|
||||
|
||||
#ifndef _WIFI_POLLING_JS_H_
|
||||
#define _WIFI_POLLING_JS_H_
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
const char PROGMEM WIFI_POLLING_JS[] = R"rawliteral(
|
||||
<script>
|
||||
let scanPollInterval = null;
|
||||
let scanBusy = false;
|
||||
|
||||
function setRefreshButtonState(disabled, label) {
|
||||
const btn = document.getElementById('refresh-btn');
|
||||
if (!btn) return;
|
||||
btn.disabled = disabled;
|
||||
btn.textContent = label;
|
||||
}
|
||||
|
||||
function showScanMessage(message) {
|
||||
const scanResults = document.getElementById('scan-results');
|
||||
if (!scanResults) return;
|
||||
scanResults.innerHTML = message + '<br/><br/>';
|
||||
}
|
||||
|
||||
async function refreshScan() {
|
||||
if (scanBusy) return false;
|
||||
scanBusy = true;
|
||||
setRefreshButtonState(true, 'Scanning...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/wifi/scan', { method: 'POST' });
|
||||
if (!response.ok) throw new Error('scan request failed');
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
scanBusy = false;
|
||||
setRefreshButtonState(false, 'Refresh');
|
||||
showScanMessage('Could not start scan. Try again.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (scanPollInterval) return;
|
||||
updateScanStatus();
|
||||
scanPollInterval = setInterval(updateScanStatus, 1000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (scanPollInterval) {
|
||||
clearInterval(scanPollInterval);
|
||||
scanPollInterval = null;
|
||||
}
|
||||
scanBusy = false;
|
||||
setRefreshButtonState(false, 'Refresh');
|
||||
}
|
||||
|
||||
function updateScanStatus() {
|
||||
fetch('/wifistatus')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.scanning) {
|
||||
showScanMessage('Scanning for networks...');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'timeout') {
|
||||
showScanMessage('Scan timed out. Refresh to try again.');
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'failed') {
|
||||
showScanMessage('Scan failed. Refresh to try again.');
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
updateNetworkList(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error polling scan status:', error);
|
||||
showScanMessage('Scan status unavailable.');
|
||||
stopPolling();
|
||||
});
|
||||
}
|
||||
|
||||
function updateNetworkList(data) {
|
||||
let html = '';
|
||||
if(data.count === 0) {
|
||||
html = 'No networks found. Refresh to scan again.<br/><br/>';
|
||||
} else if(data.networks && data.networks.length > 0) {
|
||||
data.networks.forEach(function(network) {
|
||||
let qualityPercent = network.quality + '%';
|
||||
let qualityIcon = Math.round((network.quality / 100) * 3) + 1;
|
||||
if(qualityIcon < 1) qualityIcon = 1;
|
||||
if(qualityIcon > 4) qualityIcon = 4;
|
||||
let encClass = network.encrypted ? 'l' : '';
|
||||
let ssidEscaped = escapeHtml(network.ssid);
|
||||
html += '<div><a href="#p" onclick="c(this)" data-ssid="' + ssidEscaped + '">' + ssidEscaped + '</a>';
|
||||
html += '<div role="img" aria-label="' + qualityPercent + '" title="' + qualityPercent + '" class="q q-' + qualityIcon + ' ' + encClass + '"></div>';
|
||||
html += '<div class="q">' + qualityPercent + '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
}
|
||||
document.getElementById('scan-results').innerHTML = html;
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var map = {};
|
||||
map['&'] = '&';
|
||||
map['<'] = '<';
|
||||
map['>'] = '>';
|
||||
map['"'] = '"';
|
||||
map["'"] = ''';
|
||||
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
const refreshButton = document.getElementById('refresh-btn');
|
||||
const skipInitialScan = refreshButton && refreshButton.dataset.skipInitialScan === 'true';
|
||||
const pField = document.getElementById('p');
|
||||
if (pField) {
|
||||
pField.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (skipInitialScan) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/wifistatus')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.scanning) {
|
||||
scanBusy = true;
|
||||
setRefreshButtonState(true, 'Scanning...');
|
||||
startPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'timeout') {
|
||||
showScanMessage('Scan timed out. Refresh to try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.error === 'failed') {
|
||||
showScanMessage('Scan failed. Refresh to try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
updateNetworkList(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading initial scan status:', error);
|
||||
showScanMessage('Scan status unavailable.');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
)rawliteral";
|
||||
|
||||
#endif // _WIFI_POLLING_JS_H_
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
*
|
||||
* @author Creator tzapu
|
||||
* @author tablatronix
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#include "WiFiManager.h"
|
||||
#include "WiFiManagerLogSink.h"
|
||||
#include "WiFiManagerServer.h" // Need menu tokens
|
||||
#include "WiFiManagerServer.h"
|
||||
|
||||
#if defined(ESP8266) || defined(ESP32)
|
||||
|
||||
@@ -508,7 +508,6 @@ void WiFiManager::setupConfigPortal() {
|
||||
// Lazy initialization: only create server manager when config portal is actually needed
|
||||
if (!_serverManager) {
|
||||
_serverManager = std::make_unique<WiFiManagerServer>(this);
|
||||
_serverManager->registerTemplateSetupCallback(_templateSetupCallback);
|
||||
}
|
||||
_serverManager->createServer(_httpPort);
|
||||
_serverManager->registerRoutes();
|
||||
@@ -2013,6 +2012,10 @@ void WiFiManager::setShowInfoUpdate(boolean enabled){
|
||||
_showInfoUpdate = enabled;
|
||||
}
|
||||
|
||||
void WiFiManager::setShowInfo(boolean enabled){
|
||||
_showInfo = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the config portal is running
|
||||
* @return bool true if active
|
||||
@@ -2054,20 +2057,6 @@ void WiFiManager::setTitle(String title){
|
||||
_title = title;
|
||||
}
|
||||
|
||||
void WiFiManager::registerTemplateSetupCallback(std::function<void(PlaceholderRegistry&)> cb) {
|
||||
_templateSetupCallback = std::move(cb);
|
||||
if (_serverManager) {
|
||||
_serverManager->registerTemplateSetupCallback(_templateSetupCallback);
|
||||
_serverManager->rebuildPlaceholderRegistry();
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManager::rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer) {
|
||||
if (_serverManager) {
|
||||
_serverManager->rebuildPlaceholderRegistry(std::move(customizer));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set params as separate page not in wifi
|
||||
* NOT COMPATIBLE WITH setMenu!
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* WiFiManagerParameter.cpp
|
||||
*
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
#include "WiFiManagerParameter.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* WiFiManagerServer.cpp
|
||||
*
|
||||
* Server lifecycle management implementation
|
||||
*
|
||||
* @author tablatronix
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*
|
||||
* Server lifecycle: single shell + JSON APIs + OTA + captive portal.
|
||||
* Route inventory matches WiFiManagerServer.h (GET /, /api/..., POST /u, onNotFound).
|
||||
*/
|
||||
|
||||
#include "WiFiManagerServer.h"
|
||||
#include "WiFiManager.h"
|
||||
#include "WiFiManagerHandlers.h" // Full definition needed for unique_ptr
|
||||
#include "WiFiManagerHandlers.h"
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
#include "WiFiManagerDfteLogger.h"
|
||||
#include <DeviceFrameworkTemplateEngineDebug.h>
|
||||
@@ -27,50 +27,18 @@ WiFiManagerServer* WiFiManagerServer::instance() {
|
||||
}
|
||||
|
||||
WiFiManagerServer::WiFiManagerServer(WiFiManager* wm)
|
||||
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {
|
||||
: _wm(wm), _handlers(std::make_unique<WiFiManagerHandlers>(wm)) {
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
WiFiManagerServer::~WiFiManagerServer() = default;
|
||||
|
||||
// Static getters for RAM placeholders (registry requires zero-arg functions) using singleton
|
||||
const char* WiFiManagerServer::tplGetPageTitle() {
|
||||
auto srv = WiFiManagerServer::instance();
|
||||
if (!srv || !srv->_wm) return "";
|
||||
return srv->_wm->_title.c_str();
|
||||
}
|
||||
|
||||
const char* WiFiManagerServer::tplGetSubtitle() {
|
||||
static String buf;
|
||||
auto srv = WiFiManagerServer::instance();
|
||||
if (!srv || !srv->_wm) { buf = ""; return buf.c_str(); }
|
||||
WiFiManager* wm = srv->_wm;
|
||||
if (wm->configPortalActive) {
|
||||
buf = wm->_apName;
|
||||
} else {
|
||||
buf = wm->getWiFiHostname() + " - " + WiFi.localIP().toString();
|
||||
}
|
||||
return buf.c_str();
|
||||
}
|
||||
|
||||
const char* WiFiManagerServer::tplGetMenu() {
|
||||
static String buf;
|
||||
auto srv = WiFiManagerServer::instance();
|
||||
if (!srv || !srv->_handlers) { buf = ""; return buf.c_str(); }
|
||||
buf = "";
|
||||
srv->_handlers->getMenuOut(&buf);
|
||||
return buf.c_str();
|
||||
}
|
||||
|
||||
const char* WiFiManagerServer::tplGetStatus() {
|
||||
static String buf;
|
||||
auto srv = WiFiManagerServer::instance();
|
||||
if (!srv || !srv->_handlers) { buf = ""; return buf.c_str(); }
|
||||
buf = "";
|
||||
srv->_handlers->reportStatus(buf);
|
||||
return buf.c_str();
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultStyles(PlaceholderRegistry& reg) {
|
||||
reg.registerProgmemData("%STYLES%", CSS_STYLE);
|
||||
}
|
||||
@@ -83,59 +51,21 @@ void WiFiManagerServer::registerDefaultPageTitle(PlaceholderRegistry& reg) {
|
||||
reg.registerRamData("%PAGE_TITLE%", &WiFiManagerServer::tplGetPageTitle);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultSubtitle(PlaceholderRegistry& reg) {
|
||||
reg.registerRamData("%SUBTITLE%", &WiFiManagerServer::tplGetSubtitle);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultMenu(PlaceholderRegistry& reg) {
|
||||
reg.registerRamData("%MENU%", &WiFiManagerServer::tplGetMenu);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultStatus(PlaceholderRegistry& reg) {
|
||||
reg.registerRamData("%STATUS%", &WiFiManagerServer::tplGetStatus);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerDefaultPlaceholders(PlaceholderRegistry& reg) {
|
||||
registerDefaultStyles(reg);
|
||||
registerDefaultScripts(reg);
|
||||
registerDefaultPageTitle(reg);
|
||||
registerDefaultSubtitle(reg);
|
||||
registerDefaultMenu(reg);
|
||||
registerDefaultStatus(reg);
|
||||
}
|
||||
|
||||
void WiFiManagerServer::applyTemplateSetupCallback(PlaceholderRegistry& reg) {
|
||||
if (_tplSetupCallback) {
|
||||
_tplSetupCallback(reg);
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManagerServer::rebuildPlaceholderRegistry(std::function<void(PlaceholderRegistry&)> customizer) {
|
||||
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
|
||||
registerDefaultPlaceholders(*_tplRegistry);
|
||||
if (customizer) {
|
||||
customizer(*_tplRegistry);
|
||||
} else {
|
||||
applyTemplateSetupCallback(*_tplRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
void WiFiManagerServer::setupTemplateEngine() {
|
||||
// Initialize shared placeholder registry once
|
||||
if (!_tplRegistry) {
|
||||
_tplRegistry = std::unique_ptr<PlaceholderRegistry>(new PlaceholderRegistry(WM_TEMPLATE_REGISTRY_CAPACITY));
|
||||
} else {
|
||||
_tplRegistry->clear();
|
||||
}
|
||||
|
||||
// Register all default placeholders
|
||||
registerDefaultPlaceholders(*_tplRegistry);
|
||||
|
||||
// Allow consumers to customize placeholders
|
||||
applyTemplateSetupCallback(*_tplRegistry);
|
||||
|
||||
#ifdef WM_DFTE_LOGGING
|
||||
// Install DFTE log sink only if nothing else registered yet (e.g. DeviceFramework's bridge).
|
||||
if (!deviceFrameworkTemplateEngineIsLoggingEnabled()) {
|
||||
if (!_dfteLogger) {
|
||||
_dfteLogger = std::make_unique<WiFiManagerDfteLogger>(_wm);
|
||||
@@ -147,53 +77,48 @@ void WiFiManagerServer::setupTemplateEngine() {
|
||||
}
|
||||
|
||||
void WiFiManagerServer::createServer(uint16_t port) {
|
||||
// If server already exists, shutdown first to prevent leaks and connection drops
|
||||
if (server) {
|
||||
#ifndef WM_NO_LOG
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("Server already exists, shutting down first"));
|
||||
#endif
|
||||
#endif
|
||||
shutdownServer();
|
||||
}
|
||||
|
||||
#ifndef WM_NO_LOG
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Info, kWiFiMgrLogSubsystem, F("Starting Web Portal"));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if(port != 80) {
|
||||
#ifndef WM_NO_LOG
|
||||
if (port != 80) {
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("http server started with custom port: "), port);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
server.reset(new AsyncWebServer(port));
|
||||
|
||||
// Ensure template engine is ready whenever a server is (re)created
|
||||
setupTemplateEngine();
|
||||
}
|
||||
|
||||
void WiFiManagerServer::registerRoutes() {
|
||||
if (!server) {
|
||||
#ifndef WM_NO_LOG
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Error, kWiFiMgrLogSubsystem, F("[ERROR] Server not created, call createServer() first"));
|
||||
#endif
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure template engine is initialized before routes use it
|
||||
if (!_tplRegistry) {
|
||||
setupTemplateEngine();
|
||||
}
|
||||
|
||||
if (_wm->_webservercallback != NULL) {
|
||||
#ifndef WM_NO_LOG
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("[CB] _webservercallback calling"));
|
||||
#endif
|
||||
_wm->_webservercallback(); // @CALLBACK
|
||||
#endif
|
||||
_wm->_webservercallback();
|
||||
}
|
||||
|
||||
/* Setup httpd callbacks, web pages: root, wifi config pages, SO captive portal detectors and not found. */
|
||||
// Shell: only HTML document. JSON + actions + OTA below (no legacy /wifi, /info, … routes).
|
||||
|
||||
// Register routes with lambdas
|
||||
server->on(WM_G(R_root), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleRoot(request);
|
||||
});
|
||||
@@ -238,87 +163,31 @@ void WiFiManagerServer::registerRoutes() {
|
||||
this->_handlers->handleApiPortalExit(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_wifi), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleWifi(request, true);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_wifinoscan), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleWifi(request, false);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_wifisave), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleWifiSave(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_info), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleInfo(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_param), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleParam(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_paramsave), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleParamSave(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_restart), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleReset(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_exit), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleExit(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_close), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleClose(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_erase), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleErase(request, false);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_scanstatus), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleWiFiScanStatus(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_scan), HTTP_POST, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleWiFiScanRequest(request);
|
||||
});
|
||||
|
||||
// OTA Update routes
|
||||
server->on(WM_G(R_update), HTTP_GET, [this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleUpdate(request);
|
||||
});
|
||||
|
||||
server->on(WM_G(R_updatedone), HTTP_POST,
|
||||
[this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleUpdateDone(request);
|
||||
},
|
||||
[this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
this->_handlers->handleUpdating(request, filename, index, data, len, final);
|
||||
}
|
||||
);
|
||||
[this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleUpdateDone(request);
|
||||
},
|
||||
[this](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len,
|
||||
bool final) {
|
||||
this->_handlers->handleUpdating(request, filename, index, data, len, final);
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
server->onNotFound([this](AsyncWebServerRequest *request) {
|
||||
this->_handlers->handleNotFound(request);
|
||||
});
|
||||
|
||||
server->begin(); // Web server start
|
||||
#ifndef WM_NO_LOG
|
||||
server->begin();
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Debug, kWiFiMgrLogSubsystem, F("HTTP server started"));
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void WiFiManagerServer::setupDNSD() {
|
||||
dnsServer.reset(new DNSServer());
|
||||
|
||||
/* Setup the DNS server redirecting all the domains to the apIP */
|
||||
dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
|
||||
#ifndef WM_NO_LOG
|
||||
#ifndef WM_NO_LOG
|
||||
_wm->log(WiFiManagerLogLevel::Trace, kWiFiMgrLogSubsystem, F("dns server started with ip: "), WiFi.softAPIP());
|
||||
#endif
|
||||
#endif
|
||||
const uint8_t DNS_PORT = 53;
|
||||
dnsServer->start(DNS_PORT, F("*"), WiFi.softAPIP());
|
||||
}
|
||||
@@ -338,20 +207,16 @@ void WiFiManagerServer::shutdownServer() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// AsyncWebServer doesn't have stop(), just reset the unique_ptr to free resources
|
||||
if (server) {
|
||||
server.reset();
|
||||
}
|
||||
|
||||
// Shutdown DNS server
|
||||
if (dnsServer) {
|
||||
dnsServer->stop(); // free heap ?
|
||||
dnsServer->stop();
|
||||
dnsServer.reset();
|
||||
}
|
||||
|
||||
// Clear singleton on shutdown
|
||||
if (s_instance == this) s_instance = nullptr;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* WiFiManagerUtils.cpp
|
||||
* Implementation of pure utility functions for WiFiManager
|
||||
*
|
||||
* @author Alex Hope-O'Connor
|
||||
* @author alexhopeoconnor
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_config_portal_multiple_start_stop),
|
||||
TEST_ENTRY(test_config_portal_already_active),
|
||||
TEST_ENTRY(test_get_config_portal_ssid),
|
||||
TEST_ENTRY(test_root_render_menu_state_transitions),
|
||||
TEST_ENTRY(test_root_render_snapshot_consistency),
|
||||
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
|
||||
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
|
||||
TEST_ENTRY(test_root_render_interleaved_context_isolation),
|
||||
|
||||
// Non-blocking tests
|
||||
@@ -113,15 +113,15 @@ TestCase tests[] = {
|
||||
TEST_ENTRY(test_scan_generation_invalidated_on_reset),
|
||||
|
||||
// Template rendering tests
|
||||
TEST_ENTRY(test_page_shell_supports_page_assets),
|
||||
TEST_ENTRY(test_root_template_renders_core_placeholders),
|
||||
TEST_ENTRY(test_wifi_template_renders_scan_container_and_form_fields),
|
||||
TEST_ENTRY(test_message_template_renders_body_and_actions),
|
||||
TEST_ENTRY(test_param_template_renders_shared_actions),
|
||||
TEST_ENTRY(test_info_template_renders_sections_and_footer),
|
||||
TEST_ENTRY(test_fragment_action_form_renders_shared_button_markup),
|
||||
TEST_ENTRY(test_fragment_info_row_and_section_render),
|
||||
TEST_ENTRY(test_fragment_centered_button_supports_extra_attrs),
|
||||
TEST_ENTRY(test_shell_template_renders_core_placeholders),
|
||||
|
||||
TEST_ENTRY(test_captive_redirect_host_rule),
|
||||
TEST_ENTRY(test_api_wifi_meta_json_shape),
|
||||
TEST_ENTRY(test_api_info_json_shape),
|
||||
TEST_ENTRY(test_api_params_json_shape),
|
||||
TEST_ENTRY(test_api_status_json_shape),
|
||||
TEST_ENTRY(test_api_action_response_json_literals),
|
||||
TEST_ENTRY(test_api_ota_done_success_json_literal),
|
||||
|
||||
// State transition tests
|
||||
TEST_ENTRY(test_portal_to_connected_transition),
|
||||
|
||||
@@ -40,8 +40,8 @@ void test_stop_web_portal();
|
||||
void test_config_portal_multiple_start_stop();
|
||||
void test_config_portal_already_active();
|
||||
void test_get_config_portal_ssid();
|
||||
void test_root_render_menu_state_transitions();
|
||||
void test_root_render_snapshot_consistency();
|
||||
void test_bootstrap_json_portal_feature_flags();
|
||||
void test_bootstrap_json_snapshot_consistency();
|
||||
void test_root_render_interleaved_context_isolation();
|
||||
|
||||
// Non-blocking tests
|
||||
@@ -123,15 +123,16 @@ void test_scan_cancels_when_lifecycle_blocked();
|
||||
void test_scan_generation_invalidated_on_reset();
|
||||
|
||||
// Template rendering tests
|
||||
void test_page_shell_supports_page_assets();
|
||||
void test_root_template_renders_core_placeholders();
|
||||
void test_wifi_template_renders_scan_container_and_form_fields();
|
||||
void test_message_template_renders_body_and_actions();
|
||||
void test_param_template_renders_shared_actions();
|
||||
void test_info_template_renders_sections_and_footer();
|
||||
void test_fragment_action_form_renders_shared_button_markup();
|
||||
void test_fragment_info_row_and_section_render();
|
||||
void test_fragment_centered_button_supports_extra_attrs();
|
||||
void test_shell_template_renders_core_placeholders();
|
||||
|
||||
// API JSON + captive helper tests
|
||||
void test_captive_redirect_host_rule();
|
||||
void test_api_wifi_meta_json_shape();
|
||||
void test_api_info_json_shape();
|
||||
void test_api_params_json_shape();
|
||||
void test_api_status_json_shape();
|
||||
void test_api_action_response_json_literals();
|
||||
void test_api_ota_done_success_json_literal();
|
||||
|
||||
// State transition tests
|
||||
void test_portal_to_connected_transition();
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerHandlers.h>
|
||||
#include "../test_main.h"
|
||||
|
||||
void test_captive_redirect_host_rule() {
|
||||
Serial.println("[TEST] Testing captive redirect host comparison...");
|
||||
|
||||
TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("captive.apple.com", "192.168.4.1"));
|
||||
TEST_ASSERT_TRUE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.2", "192.168.4.1"));
|
||||
TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("192.168.4.1", "192.168.4.1"));
|
||||
TEST_ASSERT_FALSE(WiFiManagerHandlers::shouldRedirectCaptiveForHost("example.com", ""));
|
||||
|
||||
Serial.println("[TEST] Captive redirect host rule test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_wifi_meta_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/wifi/meta JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiWifiMetaJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifiFields\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"staticFields\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"canRefreshScan\":true")));
|
||||
|
||||
Serial.println("[TEST] WiFi meta JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_info_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/info JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiInfoJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"status\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"connected\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"device\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"wifi\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"about\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"showUpdate\"")));
|
||||
|
||||
Serial.println("[TEST] API info JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_params_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/params JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiParamsGetJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"actions\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"showBack\"")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"params\":[")));
|
||||
|
||||
Serial.println("[TEST] API params JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_status_json_shape() {
|
||||
Serial.println("[TEST] Testing /api/status JSON shape...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
String j = handlers.buildApiStatusJson();
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"text\"")));
|
||||
TEST_ASSERT_EQUAL('{', j.charAt(0));
|
||||
TEST_ASSERT_EQUAL('}', j.charAt(j.length() - 1));
|
||||
|
||||
Serial.println("[TEST] API status JSON shape test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_action_response_json_literals() {
|
||||
Serial.println("[TEST] Testing POST /api action JSON payloads...");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Restart scheduled\"}",
|
||||
WiFiManagerHandlers::jsonApiDeviceRestartScheduled().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Setup saved\"}",
|
||||
WiFiManagerHandlers::jsonApiParamsSaveOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Captive portal detection disabled\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalCloseOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Exiting portal\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalExitOk().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":false,\"message\":\"Exit not allowed\"}",
|
||||
WiFiManagerHandlers::jsonApiPortalExitForbidden().c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"WiFi configuration erased. Device will restart shortly.\"}",
|
||||
WiFiManagerHandlers::jsonApiEraseResponse(true).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":false,\"message\":\"Erase failed\"}",
|
||||
WiFiManagerHandlers::jsonApiEraseResponse(false).c_str());
|
||||
|
||||
Serial.println("[TEST] Action response JSON literals test completed successfully");
|
||||
}
|
||||
|
||||
void test_api_ota_done_success_json_literal() {
|
||||
Serial.println("[TEST] Testing POST /u completion success JSON...");
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING(
|
||||
"{\"ok\":true,\"message\":\"Firmware updated. Restarting...\"}",
|
||||
WiFiManagerHandlers::jsonApiOtaUpdateSuccess().c_str());
|
||||
|
||||
Serial.println("[TEST] OTA success JSON literal test completed successfully");
|
||||
}
|
||||
@@ -3,13 +3,12 @@
|
||||
#include <WiFiManager.h>
|
||||
#include <WiFiManagerHandlers.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include "templates/RootSelector.h"
|
||||
#include "templates/RootShell.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char kEmptyTemplateChunk[] PROGMEM = "";
|
||||
const char kTestTitle[] PROGMEM = "WiFiManager";
|
||||
const char kTestSubtitle[] PROGMEM = "Root Test";
|
||||
|
||||
const char* dynamicStringGetter(void* userData) {
|
||||
const auto* value = static_cast<const String*>(userData);
|
||||
@@ -56,37 +55,29 @@ void renderContextsInterleaved(TemplateContext& first, String& firstOutput,
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_root_render_menu_state_transitions() {
|
||||
Serial.println("[TEST] Testing root menu state transitions...");
|
||||
void test_bootstrap_json_portal_feature_flags() {
|
||||
Serial.println("[TEST] Testing bootstrap JSON portal feature flags...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
|
||||
String menuBefore;
|
||||
handlers.getMenuOut(&menuBefore);
|
||||
TEST_ASSERT_EQUAL(-1, menuBefore.indexOf("/close"));
|
||||
String idleBootstrap = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_EQUAL(-1, idleBootstrap.indexOf(F("\"showCloseCaptive\":true")));
|
||||
|
||||
wm.setConfigPortalTimeout(5);
|
||||
wm.startConfigPortal("RootStateAP");
|
||||
wm.process();
|
||||
delay(100);
|
||||
wm.wmTestSetPortalActive(true);
|
||||
wm.setCaptivePortalEnable(true);
|
||||
|
||||
String menuActive;
|
||||
handlers.getMenuOut(&menuActive);
|
||||
TEST_ASSERT_NOT_EQUAL(-1, menuActive.indexOf("/close"));
|
||||
String apBootstrap = handlers.buildPortalBootstrapJson();
|
||||
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showCloseCaptive\":true")));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, apBootstrap.indexOf(F("\"showExitPortal\":true")));
|
||||
|
||||
wm.stopConfigPortal();
|
||||
delay(100);
|
||||
wm.wmTestSetPortalActive(false);
|
||||
|
||||
String menuAfter;
|
||||
handlers.getMenuOut(&menuAfter);
|
||||
TEST_ASSERT_EQUAL(-1, menuAfter.indexOf("/close"));
|
||||
|
||||
Serial.println("[TEST] Root menu state transitions test completed successfully");
|
||||
Serial.println("[TEST] Bootstrap portal feature flags test completed successfully");
|
||||
}
|
||||
|
||||
void test_root_render_snapshot_consistency() {
|
||||
Serial.println("[TEST] Testing root render snapshot consistency...");
|
||||
void test_bootstrap_json_snapshot_consistency() {
|
||||
Serial.println("[TEST] Testing bootstrap JSON snapshot consistency...");
|
||||
|
||||
WiFiManager wm;
|
||||
WiFiManagerHandlers handlers(&wm);
|
||||
@@ -96,24 +87,15 @@ void test_root_render_snapshot_consistency() {
|
||||
wm.process();
|
||||
delay(100);
|
||||
|
||||
String menuSnapshot1;
|
||||
String statusSnapshot1;
|
||||
handlers.getMenuOut(&menuSnapshot1);
|
||||
handlers.reportStatus(statusSnapshot1);
|
||||
String snap1 = handlers.buildPortalBootstrapJson();
|
||||
String snap2 = handlers.buildPortalBootstrapJson();
|
||||
|
||||
String menuSnapshot2;
|
||||
String statusSnapshot2;
|
||||
handlers.getMenuOut(&menuSnapshot2);
|
||||
handlers.reportStatus(statusSnapshot2);
|
||||
|
||||
TEST_ASSERT_GREATER_THAN(0, menuSnapshot1.length());
|
||||
TEST_ASSERT_GREATER_THAN(0, statusSnapshot1.length());
|
||||
TEST_ASSERT_EQUAL_STRING(menuSnapshot1.c_str(), menuSnapshot2.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING(statusSnapshot1.c_str(), statusSnapshot2.c_str());
|
||||
TEST_ASSERT_GREATER_THAN(32, snap1.length());
|
||||
TEST_ASSERT_EQUAL_STRING(snap1.c_str(), snap2.c_str());
|
||||
|
||||
wm.stopConfigPortal();
|
||||
|
||||
Serial.println("[TEST] Root render snapshot consistency test completed successfully");
|
||||
Serial.println("[TEST] Bootstrap JSON snapshot consistency test completed successfully");
|
||||
}
|
||||
|
||||
void test_root_render_interleaved_context_isolation() {
|
||||
@@ -125,58 +107,56 @@ void test_root_render_interleaved_context_isolation() {
|
||||
registryA.registerProgmemData("%SCRIPTS%", kEmptyTemplateChunk);
|
||||
registryA.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||
registryA.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||
registryA.registerProgmemData("%SUBTITLE%", kTestSubtitle);
|
||||
|
||||
registryB.registerProgmemData("%SCRIPTS%", kEmptyTemplateChunk);
|
||||
registryB.registerProgmemData("%STYLES%", kEmptyTemplateChunk);
|
||||
registryB.registerProgmemData("%PAGE_TITLE%", kTestTitle);
|
||||
registryB.registerProgmemData("%SUBTITLE%", kTestSubtitle);
|
||||
|
||||
String menuA = F("<div>Menu A</div>");
|
||||
String statusA = F("<div>Status A</div>");
|
||||
String menuB = F("<div>Menu B</div>");
|
||||
String statusB = F("<div>Status B</div>");
|
||||
String bootstrapA = F("{\"ctx\":\"A\"}");
|
||||
String appJsA = F("// shell A");
|
||||
String bootstrapB = F("{\"ctx\":\"B\"}");
|
||||
String appJsB = F("// shell B");
|
||||
|
||||
DynamicTemplateDescriptor menuDescriptorA{};
|
||||
DynamicTemplateDescriptor statusDescriptorA{};
|
||||
DynamicTemplateDescriptor menuDescriptorB{};
|
||||
DynamicTemplateDescriptor statusDescriptorB{};
|
||||
DynamicTemplateDescriptor bootstrapDescriptorA{};
|
||||
DynamicTemplateDescriptor appJsDescriptorA{};
|
||||
DynamicTemplateDescriptor bootstrapDescriptorB{};
|
||||
DynamicTemplateDescriptor appJsDescriptorB{};
|
||||
|
||||
configureDescriptor(menuDescriptorA, menuA);
|
||||
configureDescriptor(statusDescriptorA, statusA);
|
||||
configureDescriptor(menuDescriptorB, menuB);
|
||||
configureDescriptor(statusDescriptorB, statusB);
|
||||
configureDescriptor(bootstrapDescriptorA, bootstrapA);
|
||||
configureDescriptor(appJsDescriptorA, appJsA);
|
||||
configureDescriptor(bootstrapDescriptorB, bootstrapB);
|
||||
configureDescriptor(appJsDescriptorB, appJsB);
|
||||
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%MENU%", &menuDescriptorA),
|
||||
"Registry A menu placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%STATUS%", &statusDescriptorA),
|
||||
"Registry A status placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%MENU%", &menuDescriptorB),
|
||||
"Registry B menu placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%STATUS%", &statusDescriptorB),
|
||||
"Registry B status placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorA),
|
||||
"Registry A bootstrap placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryA.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorA),
|
||||
"Registry A app JS placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%BOOTSTRAP_JSON%", &bootstrapDescriptorB),
|
||||
"Registry B bootstrap placeholder should register");
|
||||
TEST_ASSERT_TRUE_MESSAGE(registryB.registerDynamicTemplate("%PORTAL_APP_JS%", &appJsDescriptorB),
|
||||
"Registry B app JS placeholder should register");
|
||||
|
||||
TemplateContext contextA;
|
||||
TemplateContext contextB;
|
||||
contextA.setRegistry(®istryA);
|
||||
contextB.setRegistry(®istryB);
|
||||
|
||||
TemplateRenderer::initializeContext(contextA, WM_ROOT_TEMPLATE);
|
||||
TemplateRenderer::initializeContext(contextB, WM_ROOT_TEMPLATE);
|
||||
TemplateRenderer::initializeContext(contextA, WM_ROOT_SHELL_TEMPLATE);
|
||||
TemplateRenderer::initializeContext(contextB, WM_ROOT_SHELL_TEMPLATE);
|
||||
|
||||
String outputA;
|
||||
String outputB;
|
||||
renderContextsInterleaved(contextA, outputA, contextB, outputB);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("Menu A"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("Status A"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("Menu B"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("Status B"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("{\"ctx\":\"A\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputA.indexOf("// shell A"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("{\"ctx\":\"B\"}"));
|
||||
TEST_ASSERT_EQUAL(-1, outputA.indexOf("// shell B"));
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("Menu B"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("Status B"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("Menu A"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("Status A"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"B\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, outputB.indexOf("// shell B"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("{\"ctx\":\"A\"}"));
|
||||
TEST_ASSERT_EQUAL(-1, outputB.indexOf("// shell A"));
|
||||
|
||||
Serial.println("[TEST] Root render interleaved context isolation test completed successfully");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
#include <unity.h>
|
||||
#include <Arduino.h>
|
||||
#include <TemplateEngine.h>
|
||||
#include "templates/Fragments.h"
|
||||
#include "templates/Info.h"
|
||||
#include "templates/Param.h"
|
||||
#include "templates/PageShell.h"
|
||||
#include "templates/Root.h"
|
||||
#include "templates/WiFi.h"
|
||||
#include "templates/Message.h"
|
||||
#include "templates/RootShell.h"
|
||||
#include "../test_main.h"
|
||||
|
||||
namespace {
|
||||
@@ -31,221 +25,30 @@ String renderTemplate(const char* templateData, PlaceholderRegistry& registry) {
|
||||
|
||||
const char kEmpty[] PROGMEM = "";
|
||||
const char kDocTitle[] PROGMEM = "Config ESP";
|
||||
const char kBodyClass[] PROGMEM = "wifi";
|
||||
const char kPageScripts[] PROGMEM = "<script>console.log('wifi page');</script>";
|
||||
const char kPageStyles[] PROGMEM = "<style>.wifi{color:#000;}</style>";
|
||||
const char kPageContent[] PROGMEM = "<div>Body</div>";
|
||||
const char kScanContent[] PROGMEM = "Scanning for networks...<br/><br/>";
|
||||
const char kSsidPlaceholder[] PROGMEM = "Office";
|
||||
const char kPasswordPlaceholder[] PROGMEM = "********";
|
||||
const char kStaticFields[] PROGMEM = "<hr><br/><label for='ip'>Static IP</label>";
|
||||
const char kParamSection[] PROGMEM = "<hr><br/><label for='param_0'>Token</label>";
|
||||
const char kFormActions[] PROGMEM = "<br/><br/><button type='submit'>Save</button>";
|
||||
const char kPageActions[] PROGMEM = "<br/><div class='c'><button id='refresh-btn' type='button' onclick='return refreshScan()'>Refresh</button></div><hr><br/><form action='/' method='get'><button>Back</button></form>";
|
||||
const char kMessageActions[] PROGMEM = "<hr><br/><form action='/' method='get'><button>Back</button></form>";
|
||||
const char kStatus[] PROGMEM = "<div class='msg'>Status</div>";
|
||||
const char kMessageBody[] PROGMEM = "<div class='msg S'>Saved</div>";
|
||||
const char kInfoSection[] PROGMEM = "<h3>About</h3><hr><dl><dt>Build date</dt><dd>today</dd></dl>";
|
||||
const char kInfoFooter[] PROGMEM = "<hr><br/><form action='/update' method='get'><button>Update</button></form>";
|
||||
const char kPageTitle[] PROGMEM = "WiFiManager";
|
||||
const char kSubtitle[] PROGMEM = "Setup";
|
||||
const char kMenu[] PROGMEM = "<form><button>Configure WiFi</button></form>";
|
||||
const char kActionPrefix[] PROGMEM = "<hr><br/>";
|
||||
const char kActionPath[] PROGMEM = "/update";
|
||||
const char kActionMethod[] PROGMEM = "get";
|
||||
const char kActionLabel[] PROGMEM = "Update";
|
||||
const char kActionClassAttr[] PROGMEM = " class='D'";
|
||||
const char kActionSuffix[] PROGMEM = "<br/>\n";
|
||||
const char kButtonId[] PROGMEM = "refresh-btn";
|
||||
const char kButtonType[] PROGMEM = "button";
|
||||
const char kButtonOnClick[] PROGMEM = "return refreshScan()";
|
||||
const char kButtonExtraAttrs[] PROGMEM = " data-skip-initial-scan='true'";
|
||||
const char kInfoLabel[] PROGMEM = "SDK version";
|
||||
const char kInfoValue[] PROGMEM = "3.1.2";
|
||||
const char kSectionTitle[] PROGMEM = "Device";
|
||||
const char kSectionRows[] PROGMEM = "<dt>Chip ID</dt><dd>abc</dd>";
|
||||
const char kBootstrapJson[] PROGMEM = "{\"title\":\"Test\"}";
|
||||
const char kPortalAppJs[] PROGMEM = "console.log('portal');";
|
||||
|
||||
} // namespace
|
||||
|
||||
void test_page_shell_supports_page_assets() {
|
||||
Serial.println("[TEST] Testing page shell page-scoped assets...");
|
||||
|
||||
PlaceholderRegistry registry(12);
|
||||
registry.registerProgmemData("%SCRIPTS%", kEmpty);
|
||||
registry.registerProgmemData("%PAGE_SCRIPTS%", kPageScripts);
|
||||
registry.registerProgmemData("%STYLES%", kEmpty);
|
||||
registry.registerProgmemData("%PAGE_STYLES%", kPageStyles);
|
||||
registry.registerProgmemData("%DOC_TITLE%", kDocTitle);
|
||||
registry.registerProgmemData("%BODY_CLASS%", kBodyClass);
|
||||
registry.registerProgmemData("%PAGE_CONTENT%", kPageContent);
|
||||
|
||||
String output = renderTemplate(WM_PAGE_SHELL_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("wifi page"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(".wifi{color:#000;}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<div>Body</div>"));
|
||||
|
||||
Serial.println("[TEST] Page shell page-scoped assets test completed successfully");
|
||||
}
|
||||
|
||||
void test_wifi_template_renders_scan_container_and_form_fields() {
|
||||
Serial.println("[TEST] Testing WiFi page template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(16);
|
||||
registry.registerProgmemData("%WIFI_SCAN_CONTENT%", kScanContent);
|
||||
registry.registerProgmemData("%WIFI_SSID_PLACEHOLDER%", kSsidPlaceholder);
|
||||
registry.registerProgmemData("%WIFI_PASSWORD_PLACEHOLDER%", kPasswordPlaceholder);
|
||||
registry.registerProgmemData("%WIFI_STATIC_FIELDS%", kStaticFields);
|
||||
registry.registerProgmemData("%WIFI_PARAM_SECTION%", kParamSection);
|
||||
registry.registerProgmemData("%WIFI_FORM_ACTIONS%", kFormActions);
|
||||
registry.registerProgmemData("%WIFI_PAGE_ACTIONS%", kPageActions);
|
||||
registry.registerProgmemData("%WIFI_STATUS%", kStatus);
|
||||
|
||||
String output = renderTemplate(WM_WIFI_CONTENT_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='scan-results'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Scanning for networks..."));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("placeholder='Office'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
|
||||
|
||||
Serial.println("[TEST] WiFi page template rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_message_template_renders_body_and_actions() {
|
||||
Serial.println("[TEST] Testing message template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%MESSAGE_BODY%", kMessageBody);
|
||||
registry.registerProgmemData("%MESSAGE_ACTIONS%", kMessageActions);
|
||||
|
||||
String output = renderTemplate(WM_MESSAGE_CONTENT_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Saved"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
|
||||
|
||||
Serial.println("[TEST] Message template rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_param_template_renders_shared_actions() {
|
||||
Serial.println("[TEST] Testing param page template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%PARAM_FIELDS%", kParamSection);
|
||||
registry.registerProgmemData("%PARAM_FORM_ACTIONS%", kFormActions);
|
||||
registry.registerProgmemData("%PARAM_PAGE_ACTIONS%", kMessageActions);
|
||||
registry.registerProgmemData("%PARAM_STATUS%", kStatus);
|
||||
|
||||
String output = renderTemplate(WM_PARAM_CONTENT_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Token"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Save"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Back"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
|
||||
|
||||
Serial.println("[TEST] Param page template rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_info_template_renders_sections_and_footer() {
|
||||
Serial.println("[TEST] Testing info page template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%INFO_STATUS%", kStatus);
|
||||
registry.registerProgmemData("%INFO_DEVICE_SECTION%", kInfoSection);
|
||||
registry.registerProgmemData("%INFO_WIFI_SECTION%", kInfoSection);
|
||||
registry.registerProgmemData("%INFO_ABOUT_SECTION%", kInfoSection);
|
||||
registry.registerProgmemData("%INFO_FOOTER%", kInfoFooter);
|
||||
|
||||
String output = renderTemplate(WM_INFO_CONTENT_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Build date"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Update"));
|
||||
|
||||
Serial.println("[TEST] Info page template rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_fragment_action_form_renders_shared_button_markup() {
|
||||
Serial.println("[TEST] Testing shared action fragment rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%ACTION_PREFIX%", kActionPrefix);
|
||||
registry.registerProgmemData("%ACTION%", kActionPath);
|
||||
registry.registerProgmemData("%METHOD%", kActionMethod);
|
||||
registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel);
|
||||
registry.registerProgmemData("%BUTTON_CLASS_ATTR%", kActionClassAttr);
|
||||
registry.registerProgmemData("%ACTION_SUFFIX%", kActionSuffix);
|
||||
|
||||
String output = renderTemplate(WM_ACTION_FORM_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("action='/update'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("class='D'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update</button>"));
|
||||
|
||||
Serial.println("[TEST] Shared action fragment rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_fragment_info_row_and_section_render() {
|
||||
Serial.println("[TEST] Testing shared info fragment rendering...");
|
||||
|
||||
PlaceholderRegistry rowRegistry(4);
|
||||
rowRegistry.registerProgmemData("%INFO_LABEL%", kInfoLabel);
|
||||
rowRegistry.registerProgmemData("%INFO_VALUE%", kInfoValue);
|
||||
|
||||
String rowOutput = renderTemplate(WM_INFO_ROW_TEMPLATE, rowRegistry);
|
||||
TEST_ASSERT_NOT_EQUAL(-1, rowOutput.indexOf("<dt>SDK version</dt><dd>3.1.2</dd>"));
|
||||
|
||||
PlaceholderRegistry sectionRegistry(6);
|
||||
sectionRegistry.registerProgmemData("%SECTION_PREFIX%", kEmpty);
|
||||
sectionRegistry.registerProgmemData("%SECTION_TITLE%", kSectionTitle);
|
||||
sectionRegistry.registerProgmemData("%SECTION_ROWS%", kSectionRows);
|
||||
sectionRegistry.registerProgmemData("%SECTION_SUFFIX%", kActionSuffix);
|
||||
|
||||
String sectionOutput = renderTemplate(WM_INFO_SECTION_TEMPLATE, sectionRegistry);
|
||||
TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("<h3>Device</h3>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, sectionOutput.indexOf("<dt>Chip ID</dt><dd>abc</dd>"));
|
||||
|
||||
Serial.println("[TEST] Shared info fragment rendering test completed successfully");
|
||||
}
|
||||
|
||||
void test_fragment_centered_button_supports_extra_attrs() {
|
||||
Serial.println("[TEST] Testing centered button fragment extra attrs...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%BUTTON_ID%", kButtonId);
|
||||
registry.registerProgmemData("%BUTTON_TYPE%", kButtonType);
|
||||
registry.registerProgmemData("%BUTTON_ONCLICK%", kButtonOnClick);
|
||||
registry.registerProgmemData("%BUTTON_LABEL%", kActionLabel);
|
||||
registry.registerProgmemData("%BUTTON_EXTRA_ATTRS%", kButtonExtraAttrs);
|
||||
|
||||
String output = renderTemplate(WM_CENTERED_BUTTON_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='refresh-btn'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("data-skip-initial-scan='true'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf(">Update</button>"));
|
||||
|
||||
Serial.println("[TEST] Centered button fragment extra attrs test completed successfully");
|
||||
}
|
||||
|
||||
void test_root_template_renders_core_placeholders() {
|
||||
Serial.println("[TEST] Testing root template rendering...");
|
||||
void test_shell_template_renders_core_placeholders() {
|
||||
Serial.println("[TEST] Testing portal shell template rendering...");
|
||||
|
||||
PlaceholderRegistry registry(8);
|
||||
registry.registerProgmemData("%SCRIPTS%", kEmpty);
|
||||
registry.registerProgmemData("%STYLES%", kEmpty);
|
||||
registry.registerProgmemData("%PAGE_TITLE%", kPageTitle);
|
||||
registry.registerProgmemData("%SUBTITLE%", kSubtitle);
|
||||
registry.registerProgmemData("%MENU%", kMenu);
|
||||
registry.registerProgmemData("%STATUS%", kStatus);
|
||||
registry.registerProgmemData("%PAGE_TITLE%", kDocTitle);
|
||||
registry.registerProgmemData("%BOOTSTRAP_JSON%", kBootstrapJson);
|
||||
registry.registerProgmemData("%PORTAL_APP_JS%", kPortalAppJs);
|
||||
|
||||
String output = renderTemplate(WM_ROOT_TEMPLATE, registry);
|
||||
String output = renderTemplate(WM_ROOT_SHELL_TEMPLATE, registry);
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("WiFiManager"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Setup"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Configure WiFi"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("Status"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<title>Config ESP</title>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("<div id='app'></div>"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-toast'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-dialog'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("id='wm-bootstrap'"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("{\"title\":\"Test\"}"));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, output.indexOf("console.log('portal')"));
|
||||
|
||||
Serial.println("[TEST] Root template rendering test completed successfully");
|
||||
Serial.println("[TEST] Portal shell template rendering test completed successfully");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user