Compare commits

..
6 Commits
45 changed files with 1011 additions and 275 deletions
+2
View File
@@ -20,6 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: bash -n scripts/*.sh
- run: ./scripts/check-docs.sh
compile-tests:
@@ -35,3 +36,4 @@ jobs:
python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19
- run: ./scripts/test.sh compile --platform ${{ matrix.environment }}
- run: ./scripts/test.sh examples --platform ${{ matrix.environment }}
+2
View File
@@ -18,7 +18,9 @@ jobs:
python-version: '3.11'
- run: python -m pip install --upgrade platformio==6.1.19
- run: ./scripts/test.sh compile --platform esp8266
- run: ./scripts/test.sh examples --platform esp8266
- run: ./scripts/test.sh compile --platform esp32
- run: ./scripts/test.sh examples --platform esp32
- run: ./scripts/check-docs.sh
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
- run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## 3.2.1
- Ensure externally supplied station profiles remain authoritative on ESP32 by clearing stale SDK-owned credentials before opening the portal for an empty or invalid external profile set.
- Pin DFTE 1.2.0, the tested template-engine release used by the maintained portal.
## 3.2.0
- Refine the device-hosted portal with a startup Wi-Fi scan, stable loading overlays, clearer connection progress, and a resettable configuration timeout.
- Keep a successful portal-to-station hand-off reachable until the browser acknowledges its redirect, with a bounded fallback for captive or headless clients.
- Rename the presentation field `homeIntro` to `tagline` so portal identity and wording are clearer; update portal bootstrap contract to v3.
- Add focused Basic Portal, Branded Portal, Custom Portal Content, and Station Profiles examples for ESP8266 and ESP32, with real-hardware README captures.
- Strengthen release CI with documentation and clean-consumer/example compilation checks for both supported targets.
## 3.1.0
- Add an opt-in primary/fallback station-profile controller with bounded failover, reconnection, a durable consumer-supplied store, and profile-aware portal APIs. DeviceFramework uses this to persist verified WiFi profiles transactionally.
+45 -44
View File
@@ -1,15 +1,15 @@
# WiFiManager
WiFiManager is the maintained ESP8266/ESP32 configuration-portal fork used by DeviceFramework. It is a deliberate breaking fork of upstream [`tzapu/WiFiManager`](https://github.com/tzapu/WiFiManager), with a streamed single-page portal and typed JSON APIs rather than upstream’s legacy page/template model.
WiFiManager gives ESP8266 and ESP32 firmware a polished, self-hosted Wi-Fi setup experience. It reconnects to saved networks and opens a temporary captive portal when it cannot, so users can configure the device from any phone or browser without a cloud service or companion app.
## Why use it
## See it on real hardware
- **Single-shell portal:** one responsive SPA for WiFi, parameters, information, actions, and firmware update flow.
- **Data-first APIs:** portal state and actions are exposed under `/api/...`, not scraped from HTML.
- **Controlled portal UI:** semantic identity/theme values and structured portal APIs without exposing portal internals.
- **ESP8266 and ESP32:** clean PlatformIO consumers resolve the right asynchronous TCP transport automatically.
| Brand the built-in portal | Combine Wi-Fi and application setup |
| --- | --- |
| ![Branded WiFiManager portal overview on an ESP32.](docs/assets/portal-esp32-branded-overview.png) | ![WiFiManager network picker and custom MQTT broker field on an ESP8266.](docs/assets/portal-esp8266-custom-wifi.png) |
| Give each product its own title, identity, icon, and color theme without copying portal HTML. | Show live nearby networks and collect application values, such as an MQTT broker, in the same setup flow. |
## Try it
## Start with a working portal
```cpp
#include <Arduino.h>
@@ -20,71 +20,72 @@ WiFiManager wifi;
void setup() {
Serial.begin(115200);
wifi.setConfigPortalTimeout(180);
if (!wifi.autoConnect("Device Setup", "change-me")) {
ESP.restart();
}
wifi.autoConnect("Device Setup", "change-me");
}
void loop() {}
void loop() {
wifi.process();
}
```
For a DeviceFramework device, configure the framework’s shared device password instead. DeviceFramework applies it consistently to the provisioning AP, OTA, HTTP Basic authentication, and WebSerial.
When saved Wi-Fi is unavailable, `autoConnect()` starts the portal asynchronously. Call `process()` from every `loop()` iteration while it may be open. Flash [Basic Portal](examples/BasicPortal/) to try this exact flow; its README gives the network name, password, portal address, and expected result after saving Wi-Fi.
### Brand it
## Make it yours
```cpp
const char kTitle[] PROGMEM = "Set up Temperature Monitor";
const char kBrand[] PROGMEM = "Tree";
const char kBrand[] PROGMEM = "Example Devices";
const char kAccent[] PROGMEM = "#347a45";
const WiFiManagerPortalConfig kPortalUI = {
WiFiManagerPortalText::progmem(kTitle),
WiFiManagerPortalText::progmem(kBrand),
{}, {}, {},
{
{}, {}, {}, {}, {},
WiFiManagerPortalText::progmem(kAccent),
{}, {}, {}, {}, {},
10, 0,
},
};
void setup() {
wifi.setPortalConfig(kPortalUI); // before the portal starts
WiFiManagerPortalConfig portalUI;
// Leave fields unset to keep WiFiManager's built-in portal values.
portalUI.title = WiFiManagerPortalText::progmem(kTitle);
portalUI.identityText = WiFiManagerPortalText::progmem(kBrand);
// Theme values customise the built-in styles; they do not replace the portal.
portalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
portalUI.theme.cornerRadiusPx = 10;
wifi.setPortalConfig(portalUI); // Apply before the portal starts.
wifi.autoConnect("Device Setup");
}
void loop() {
wifi.process();
}
```
The full standalone example, including a static SVG, is [BrandedPortal](examples/BrandedPortal/BrandedPortal.ino).
The [Branded Portal](examples/BrandedPortal/) example includes a static SVG, accessible identity text, and a small semantic theme. [Custom Portal Content](examples/CustomPortalContent/) shows the supported parameters, information sections, and home cards without replacing the portal shell.
## Explore the portal
## What it provides
- **Captive Wi-Fi setup:** starts an access point only when saved network credentials cannot connect.
- **Responsive portal:** one small portal for Wi-Fi, application fields, information, actions, and firmware update flow.
- **Structured APIs:** JSON endpoints under `/api/...` for applications that need portal state or connection results.
- **Product presentation:** title, logo, tagline, and semantic colour tokens without copying the portal HTML.
- **Primary and fallback networks:** an opt-in two-network controller backed by an application-provided store.
- **ESP8266 and ESP32 support:** the package resolves its asynchronous web dependencies for the selected target.
## Choose a guide
| Goal | Guide |
| --- | --- |
| Brand the portal or add structured built-in content | [Portal UI](docs/PORTAL_UI.md) |
| Configure primary/fallback station profiles | [Station profiles](docs/STATION_PROFILES.md) |
| Understand the JSON APIs and station-connect handoff | [Portal API](docs/PORTAL_API.md) |
| Build a clean consumer or work on this fork | [Testing](docs/TESTING.md) · [Development](docs/DEVELOPMENT.md) |
| Build or flash a complete example | [Examples](examples/README.md) |
| Contribute to this fork or prepare a release | [Development](docs/DEVELOPMENT.md) |
## Install
```ini
[common]
lib_deps =
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.1.0
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.1
```
The suffix after `#` is a Git ref. PlatformIO clones the repository and checks out that release tag; GitHub Release assets are unrelated. This repository is supported through PlatformIO. Use a local `symlink://` or `file://` dependency only while actively changing this fork.
The suffix after `#` is a Git ref. PlatformIO clones the repository and checks out that release tag; GitHub Release assets are unrelated. Arduino IDE users can install this repository as a library checkout.
## Development and releases
```bash
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
./scripts/check-docs.sh
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
```
Tagging repeats the board-free compile checks, validates the package, and creates a GitHub Release from the corresponding changelog section. It does not publish to the PlatformIO Registry or deploy firmware.
See the [documentation index](docs/README.md), [release history](CHANGELOG.md), and [licence](LICENSE).
See the [documentation index](docs/README.md), [examples](examples/README.md), [release history](CHANGELOG.md), and [licence](LICENSE).
+3 -1
View File
@@ -14,9 +14,11 @@ known six-second asynchronous scan failure in the older 2.0.17 framework. Core
ignore settings shown in this repository `platformio.ini`; keep those settings
when adding an ESP32 environment.
Before a release, update `library.json`, `CHANGELOG.md`, and the relevant public documentation, then run:
Start a release with `bump-version.sh`. It updates package metadata and canonical installation snippets, then creates the changelog section. Replace its generated TODO with the release summary and update any behavioural documentation before running:
```bash
./scripts/bump-version.sh vMAJOR.MINOR.PATCH
# Replace the generated CHANGELOG TODO with the release summary.
./scripts/check-docs.sh
./scripts/test.sh compile --platform esp8266
./scripts/test.sh compile --platform esp32
+6 -11
View File
@@ -11,31 +11,26 @@ WiFiManager wifi;
void setup() {
Serial.begin(115200);
wifi.setConfigPortalTimeout(180);
if (!wifi.autoConnect("Example Setup", "change-me")) {
ESP.restart();
}
wifi.autoConnect("Example Setup", "change-me");
}
void loop() {
wifi.process(); // Keeps the portal responsive when setup is needed.
// Normal work begins after autoConnect succeeds.
}
```
`autoConnect()` first tries stored station credentials. If that cannot connect, it starts the AP and portal with the supplied name and password. Use a Wi-Fi-valid AP password.
`autoConnect()` first tries stored station credentials. If that cannot connect, it starts the AP and portal with the supplied name and password, then returns `false`; call `process()` from `loop()` so that portal can run. Use a Wi-Fi-valid AP password.
## PlatformIO dependency
```ini
lib_deps =
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.1.0
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.1
```
The package manifest resolves DFTE, ESPAsyncWebServer, and the correct TCP transport for ESP8266 or ESP32. A consuming project should not duplicate those dependencies unless it is deliberately testing an unreleased stack change.
The package includes the asynchronous web and TCP dependencies required by the selected ESP8266 or ESP32 target. Add WiFiManager as the application’s direct dependency; do not copy its internal dependency list into your project.
## DeviceFramework applications
DeviceFramework creates and configures WiFiManager for its normal lifecycle. Use DeviceFramework’s persistent shared device-password API instead of separately configuring an AP, OTA, HTTP, and WebSerial password.
Next: [portal UI](PORTAL_UI.md) or [portal API](PORTAL_API.md).
Next: build [Basic Portal](../examples/BasicPortal/), then explore [portal UI](PORTAL_UI.md) or [portal API](PORTAL_API.md).
Back to [documentation](README.md) · [project overview](../README.md).
+8 -2
View File
@@ -2,6 +2,8 @@
The portal uses JSON endpoints under `/api/...` for WiFi scans and saves, parameters, information, status, restart/erase/exit actions, captive-portal closure, and station-connect status. Consumers should treat the documented response shapes as the contract; portal HTML is not an API.
The current bootstrap contract is version 3. It includes `brand.tagline`, the short product tagline rendered in the portal header.
## Profile-mode WiFi metadata
When an application supplies a `WiFiManagerStationProfileStore`, `GET /api/wifi/meta` reports a primary profile and optional fallback without returning passwords. `POST /api/wifi/save` verifies a submitted candidate before committing it; see [station profiles](STATION_PROFILES.md) for fields and lifecycle.
@@ -20,9 +22,13 @@ When `portalSetBehaviorConnectOnSave(true)` is enabled, saving credentials queue
}
```
`stationIp` and `redirectUrl` are present only after a successful join. If the portal server is not on port 80, `redirectUrl` includes that port.
`stationIp` and `redirectUrl` are present only after a successful join. If the portal server is not on port 80, `redirectUrl` includes that port. When connect-on-save is disabled, a saved configuration has no station address and the built-in portal remains open.
On success, WiFiManager keeps the portal alive briefly so the client can read the final status and navigate before the AP is shut down. Captive-portal helpers, DHCP timing, browser behaviour, and network isolation can still prevent an automatic redirect, so clients must handle a visible address as well.
On success, the SPA first reads the station address, then POSTs `/api/wifi/connect-complete`. WiFiManager keeps the portal available for a fallback period while it waits for that acknowledgement, then closes it after a short grace delay so the normal device web server can start. This removes the old client-side race where the portal could disappear before the final status arrived. Captive-portal helpers, DHCP timing, browser behaviour, and network isolation can still prevent an automatic redirect, so clients must retain the visible address as a fallback.
## Portal timeout
When a configuration-portal timeout is enabled, `POST /api/portal/timeout-reset` restarts the countdown and returns `{ "ok": true, "timeoutSecondsRemaining": N }`. The built-in overview exposes this as an explicit reset control; custom clients may use the documented endpoint too.
## API design rules
+24 -26
View File
@@ -14,40 +14,39 @@ WiFiManager wifi;
namespace {
const char kTitle[] PROGMEM = "Set up Temperature Monitor";
const char kIdentity[] PROGMEM = "Tree";
const char kIntro[] PROGMEM = "Connect this device to Wi-Fi.";
const char kLogoAlt[] PROGMEM = "Tree";
const char kIdentity[] PROGMEM = "Example Devices";
const char kTagline[] PROGMEM = "Reliable setup for connected devices.";
const char kLogoAlt[] PROGMEM = "Example Devices";
const char kLogo[] PROGMEM = R"svg(<svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="28"/></svg>)svg";
const char kPage[] PROGMEM = "#f4f7f3";
const char kSurface[] PROGMEM = "#ffffff";
const char kAccent[] PROGMEM = "#347a45";
const char kAccentText[] PROGMEM = "#ffffff";
const WiFiManagerPortalConfig kPortalUI = {
WiFiManagerPortalText::progmem(kTitle),
WiFiManagerPortalText::progmem(kIdentity),
WiFiManagerPortalText::progmem(kIntro),
WiFiManagerPortalAsset::svgFromProgmem(kLogo),
WiFiManagerPortalText::progmem(kLogoAlt),
{
WiFiManagerPortalText::progmem(kPage),
WiFiManagerPortalText::progmem(kSurface),
{}, {}, {},
WiFiManagerPortalText::progmem(kAccent),
{},
WiFiManagerPortalText::progmem(kAccentText),
{}, {}, {},
10, 6,
},
};
WiFiManagerPortalConfig kPortalUI;
}
void setup() {
wifi.setPortalConfig(kPortalUI);
// Set only the identity values this product needs.
kPortalUI.title = WiFiManagerPortalText::progmem(kTitle);
kPortalUI.identityText = WiFiManagerPortalText::progmem(kIdentity);
kPortalUI.tagline = WiFiManagerPortalText::progmem(kTagline);
kPortalUI.logo = WiFiManagerPortalAsset::svgFromProgmem(kLogo);
kPortalUI.logoAltText = WiFiManagerPortalText::progmem(kLogoAlt);
// Unset theme values retain the built-in style.
kPortalUI.theme.pageBackground = WiFiManagerPortalText::progmem(kPage);
kPortalUI.theme.surface = WiFiManagerPortalText::progmem(kSurface);
kPortalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
kPortalUI.theme.accentText = WiFiManagerPortalText::progmem(kAccentText);
kPortalUI.theme.cornerRadiusPx = 10;
kPortalUI.theme.smallCornerRadiusPx = 6;
wifi.setPortalConfig(kPortalUI); // Configure before the portal starts.
wifi.autoConnect("Temperature Monitor");
}
void loop() {}
void loop() { wifi.process(); }
```
The complete buildable example is [BrandedPortal](../examples/BrandedPortal/BrandedPortal.ino). The compile fixture exercises this API on ESP8266 and ESP32.
@@ -58,9 +57,9 @@ Leave a text or colour value empty, or a radius at `0`, to retain the built-in s
| Field | Used by |
| --- | --- |
| `title` | Document title and portal heading |
| `identityText` | Product/device line below the heading |
| `homeIntro` | Introductory text on the home view |
| `title` | Document title and concise setup-page heading |
| `identityText` | Company or product name in the header above navigation |
| `tagline` | Short tagline in the header above navigation |
| `logo.svg`, `logoAltText` | Optional trusted inline SVG and its accessible label |
| `pageBackground`, `surface`, `text`, `mutedText`, `border` | Portal surfaces and text |
| `accent`, `accentHover`, `accentText` | Primary links and actions |
@@ -75,6 +74,5 @@ Presentation uses one configuration route: `setPortalConfig()`. Existing structu
There is no arbitrary HTML shell, route replacement, navigation injection, raw stylesheet, or script hook. If a product needs a new portal capability, add a narrow WiFiManager contract and test it on both supported targets.
For a DeviceFramework device, use `DeviceFrameworkUIConfig`; DeviceFramework maps its product-level configuration into this portal API. See [DeviceFramework web UI](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/WEB_UI.md).
Back to the [documentation index](README.md) or [project overview](../README.md).
+1 -9
View File
@@ -1,20 +1,12 @@
# WiFiManager documentation
This maintained fork intentionally has a narrower, explicit customisation boundary than upstream WiFiManager.
| I want to… | Read |
| --- | --- |
| Start a basic portal or install a released dependency | [Getting started](GETTING_STARTED.md) |
| Brand the portal or add structured built-in content | [Portal UI](PORTAL_UI.md) |
| Configure primary/fallback station profiles or their portal workflow | [Station profiles](STATION_PROFILES.md) |
| Consume JSON endpoints or station-connect status | [Portal API](PORTAL_API.md) |
| Build a clean consumer | [Testing](TESTING.md) |
| Build or flash a complete example | [Examples](../examples/README.md) |
| Work on this fork or make a release | [Development and releases](DEVELOPMENT.md) |
## Documentation rules
- Portal UI owns branding, semantic theming, and the supported `portal*` surface.
- Portal API owns HTTP response contracts.
- The root README is a short onboarding page, not an API reference.
Back to the [project overview](../README.md).
+2 -2
View File
@@ -2,7 +2,7 @@
WiFiManager 3.1.0 adds an opt-in station-profile controller for applications that need a primary Wi-Fi network and one fallback. It is independent of the legacy `autoConnect()` flow: existing WiFiManager consumers do not need to change.
DeviceFramework enables this controller and supplies its CRC-protected storage. A direct WiFiManager consumer supplies its own durable store, or can use the controller only for the current process.
A WiFiManager application supplies its own durable store. The controller can also be used with an in-memory store for a temporary session, but credentials will not survive a restart.
## Lifecycle
@@ -57,6 +57,6 @@ In profile mode, the existing Wi-Fi page becomes a two-profile form. It remains
The portal requires a non-empty primary SSID. Its API never returns a password; it only reports whether one is set.
See [Portal API](PORTAL_API.md) for the shared connection-status response and [DeviceFramework configuration](https://github.com/alexhopeoconnor/DeviceFramework/blob/main/docs/CONFIGURATION.md) for the local-profile JSON that supplies these slots.
See [Portal API](PORTAL_API.md) for the shared connection-status response. The buildable [Station Profiles](../examples/StationProfiles/) example contains a compact EEPROM-backed store for both supported ESP targets.
Back to [documentation](README.md) · [project overview](../README.md).
+1 -1
View File
@@ -9,6 +9,6 @@ check, so dependency resolution uses the current manifest rather than a stale
./scripts/test.sh compile --platform esp32
```
CI runs both checks for pushes to the maintained branch and pull requests. They compile only; hardware portals remain a local integration concern for an application or DeviceFramework’s connected-device suite.
CI runs both checks for pushes to the maintained branch and pull requests. They compile only; hardware portals remain a local integration concern for the consuming application.
Back to [documentation](README.md) · [project overview](../README.md).
Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+17
View File
@@ -0,0 +1,17 @@
#include <Arduino.h>
#include <WiFiManager.h>
WiFiManager portal;
void setup() {
Serial.begin(115200);
portal.setConfigPortalTimeout(180);
if (portal.autoConnect("WiFiManager Basic", "example-pass")) {
Serial.println("Connected. Run your normal application here.");
} else {
Serial.println("Setup portal started at http://192.168.4.1/");
}
}
void loop() { portal.process(); }
+12
View File
@@ -0,0 +1,12 @@
# Basic Portal
This is the smallest useful WiFiManager application. It first tries the credentials the ESP platform already knows. If it cannot connect, it opens an access point named **WiFiManager Basic** with password **example-pass**.
1. Build and flash the selected `esp8266` or `esp32` environment.
2. Connect a phone or computer to **WiFiManager Basic**.
3. Open `http://192.168.4.1/` if your captive-portal helper does not open it automatically.
4. Select a network and save it. The board joins that network and the next reboot reconnects without opening the portal.
The access-point password is only an example. Choose a unique, Wi-Fi-valid password for a real product.
See the shared [example guide](../README.md) and [getting started](../../docs/GETTING_STARTED.md).
+26
View File
@@ -0,0 +1,26 @@
[platformio]
default_envs = esp8266
src_dir = .
[common]
framework = arduino
lib_ldf_mode = deep+
lib_deps =
WiFiManager=symlink://../..
[env:esp8266]
extends = common
platform = espressif8266
board = d1_mini
platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:esp32]
extends = common
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = esp32dev
build_unflags = -std=gnu++11
build_flags =
-std=gnu++14
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
+23 -26
View File
@@ -5,46 +5,43 @@ WiFiManager wifi;
namespace {
const char kPortalTitle[] PROGMEM = "Set up Temperature Monitor";
const char kPortalIdentity[] PROGMEM = "Tree";
const char kPortalIntro[] PROGMEM = "Connect this device to Wi-Fi.";
const char kPortalLogoAlt[] PROGMEM = "Tree";
const char kTreeLogo[] PROGMEM = "<svg viewBox='0 0 64 64' aria-hidden='true'><circle cx='32' cy='32' r='28' fill='#347a45'/></svg>";
const char kPortalIdentity[] PROGMEM = "Example Devices";
const char kPortalTagline[] PROGMEM = "Reliable setup for connected devices.";
const char kPortalLogoAlt[] PROGMEM = "Example Devices";
const char kExampleLogo[] PROGMEM = "<svg viewBox='0 0 64 64' aria-hidden='true'><circle cx='32' cy='32' r='28' fill='#347a45'/></svg>";
const char kPage[] PROGMEM = "#f4f7f3";
const char kSurface[] PROGMEM = "#ffffff";
const char kText[] PROGMEM = "#1c251e";
const char kAccent[] PROGMEM = "#347a45";
const char kAccentText[] PROGMEM = "#ffffff";
const WiFiManagerPortalConfig kPortalUI = {
WiFiManagerPortalText::progmem(kPortalTitle),
WiFiManagerPortalText::progmem(kPortalIdentity),
WiFiManagerPortalText::progmem(kPortalIntro),
WiFiManagerPortalAsset::svgFromProgmem(kTreeLogo),
WiFiManagerPortalText::progmem(kPortalLogoAlt),
{
WiFiManagerPortalText::progmem(kPage),
WiFiManagerPortalText::progmem(kSurface),
WiFiManagerPortalText::progmem(kText),
{}, {},
WiFiManagerPortalText::progmem(kAccent),
{},
WiFiManagerPortalText::progmem(kAccentText),
{}, {}, {},
10, 6,
},
};
WiFiManagerPortalConfig kPortalUI;
}
void setup() {
Serial.begin(115200);
// Brand the built-in portal; unassigned fields retain their default values.
kPortalUI.title = WiFiManagerPortalText::progmem(kPortalTitle);
kPortalUI.identityText = WiFiManagerPortalText::progmem(kPortalIdentity);
kPortalUI.tagline = WiFiManagerPortalText::progmem(kPortalTagline);
kPortalUI.logo = WiFiManagerPortalAsset::svgFromProgmem(kExampleLogo);
kPortalUI.logoAltText = WiFiManagerPortalText::progmem(kPortalLogoAlt);
// Override only the theme tokens that define this product's appearance.
kPortalUI.theme.pageBackground = WiFiManagerPortalText::progmem(kPage);
kPortalUI.theme.surface = WiFiManagerPortalText::progmem(kSurface);
kPortalUI.theme.text = WiFiManagerPortalText::progmem(kText);
kPortalUI.theme.accent = WiFiManagerPortalText::progmem(kAccent);
kPortalUI.theme.accentText = WiFiManagerPortalText::progmem(kAccentText);
kPortalUI.theme.cornerRadiusPx = 10;
kPortalUI.theme.smallCornerRadiusPx = 6;
if (!wifi.setPortalConfig(kPortalUI)) {
Serial.println("Portal UI configuration was rejected");
}
wifi.setConfigPortalTimeout(180);
if (!wifi.autoConnect("Temperature Monitor")) {
ESP.restart();
}
wifi.autoConnect("Temperature Monitor");
}
void loop() {}
void loop() { wifi.process(); }
+9
View File
@@ -0,0 +1,9 @@
# Branded Portal
This example uses the supported `WiFiManagerPortalConfig` presentation API to give the built-in portal a product name, company identity, tagline, inline SVG mark, and semantic colour tokens.
Flash the `esp8266` or `esp32` environment, join **Temperature Monitor**, and open `http://192.168.4.1/`. The visual changes come from static firmware data; WiFiManager still owns the portal routes, forms, validation, and captive-network behaviour.
Use only trusted compiled SVG data. Keep the backing strings static for the lifetime of the firmware, then call `setPortalConfig()` before opening a portal.
See [Portal UI](../../docs/PORTAL_UI.md) and the shared [example guide](../README.md).
+26
View File
@@ -0,0 +1,26 @@
[platformio]
default_envs = esp8266
src_dir = .
[common]
framework = arduino
lib_ldf_mode = deep+
lib_deps =
WiFiManager=symlink://../..
[env:esp8266]
extends = common
platform = espressif8266
board = d1_mini
platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:esp32]
extends = common
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = esp32dev
build_unflags = -std=gnu++11
build_flags =
-std=gnu++14
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
@@ -0,0 +1,29 @@
#include <Arduino.h>
#include <WiFiManager.h>
WiFiManager portal;
WiFiManagerParameter brokerHost("broker_host", "MQTT broker", "mqtt.local", 40);
void setup() {
Serial.begin(115200);
portal.portalAddParameter(&brokerHost);
portal.portalAddInfoSection({
"device", "Example device",
{{"firmware", "Firmware", "1.0.0"}, {"sensor", "Sensor", "Ready"}},
});
portal.portalAddHomeCard({
"hint", "What this example adds", PortalHomeCardKind::Callout,
"A normal text setting, a status section, and a home-page callout.", {},
});
portal.setSaveParamsCallback([](WiFiManager::WiFiManagerRequestArgs) {
Serial.print("MQTT broker selected: ");
Serial.println(brokerHost.getValue());
});
portal.setConfigPortalTimeout(180);
portal.autoConnect("WiFiManager Content", "example-pass");
}
void loop() { portal.process(); }
+13
View File
@@ -0,0 +1,13 @@
# Custom Portal Content
This example keeps WiFiManager’s portal navigation, validation, and captive behaviour, while adding three application-owned pieces of content:
- an editable MQTT broker host field;
- a compact device-information section;
- a callout on the portal overview.
It opens **WiFiManager Content** with password **example-pass** until it has working station credentials. Save the form, then inspect serial output to see the selected broker value.
The parameter object is global because WiFiManager reads it for the lifetime of the portal. In a real application, copy the value into that application’s own validated persistent configuration inside the save callback.
See [Portal UI](../../docs/PORTAL_UI.md) and the shared [example guide](../README.md).
@@ -0,0 +1,26 @@
[platformio]
default_envs = esp8266
src_dir = .
[common]
framework = arduino
lib_ldf_mode = deep+
lib_deps =
WiFiManager=symlink://../..
[env:esp8266]
extends = common
platform = espressif8266
board = d1_mini
platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:esp32]
extends = common
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = esp32dev
build_unflags = -std=gnu++11
build_flags =
-std=gnu++14
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
+22
View File
@@ -0,0 +1,22 @@
# WiFiManager examples
Every directory below is a standalone PlatformIO project. Build it from the repository root or the example directory:
```bash
pio run -d examples/BasicPortal -e esp8266
pio run -d examples/BasicPortal -e esp8266 -t upload
pio device monitor -d examples/BasicPortal -e esp8266
```
Choose `esp32` for an ESP32 development board. The examples use the checked-out WiFiManager source, so they are also useful while developing this fork.
| Example | Start here when you want to… |
| --- | --- |
| [Basic Portal](BasicPortal/) | provision one board through the normal saved-network-or-portal flow |
| [Custom Portal Content](CustomPortalContent/) | add application settings and useful status cards to the built-in portal |
| [Branded Portal](BrandedPortal/) | change the portal’s identity, logo, and semantic visual theme |
| [Station Profiles](StationProfiles/) | remember a primary Wi-Fi network and one fallback in application-owned EEPROM storage |
The portal examples intentionally begin with no station credentials. On first boot, connect to the access point printed on serial and open `http://192.168.4.1/`.
Back to the [project overview](../README.md).
+9
View File
@@ -0,0 +1,9 @@
# Station Profiles
This example gives WiFiManager a small application-owned EEPROM store. The portal accepts a required primary Wi-Fi network and an optional fallback, verifies a submitted network before saving it, then remembers the last successful choice across restarts.
On a blank board, connect to **WiFiManager Profiles** with password **example-pass** and open `http://192.168.4.1/`. Enter a primary network and, if useful, one fallback. After a successful connection, restart the board to confirm that it tries the saved profiles before reopening the portal.
`StoredProfiles` is intentionally simple so the ownership boundary is visible. A production application should add its own record versioning and integrity protection around the application’s complete configuration; WiFiManager only owns network-selection policy.
See [Station profiles](../../docs/STATION_PROFILES.md) and the shared [example guide](../README.md).
@@ -0,0 +1,63 @@
#include <Arduino.h>
#include <EEPROM.h>
#include <WiFiManager.h>
namespace {
constexpr uint32_t kStoreMagic = 0x574D5031; // "WMP1"
struct StoredProfiles {
uint32_t magic;
WiFiManagerStationProfiles profiles;
};
class EepromProfileStore final : public WiFiManagerStationProfileStore {
public:
bool begin() {
#if defined(ESP32)
return EEPROM.begin(sizeof(StoredProfiles));
#else
EEPROM.begin(sizeof(StoredProfiles));
return true;
#endif
}
bool load(WiFiManagerStationProfiles& profiles) override {
StoredProfiles stored{};
EEPROM.get(0, stored);
if (stored.magic != kStoreMagic) {
return false;
}
profiles = stored.profiles;
return true;
}
bool save(const WiFiManagerStationProfiles& profiles) override {
EEPROM.put(0, StoredProfiles{kStoreMagic, profiles});
return EEPROM.commit();
}
bool clear() override {
EEPROM.put(0, StoredProfiles{});
return EEPROM.commit();
}
};
EepromProfileStore profileStore;
WiFiManager portal;
} // namespace
void setup() {
Serial.begin(115200);
if (!profileStore.begin()) {
Serial.println("Could not initialise EEPROM profile storage");
return;
}
portal.setStationProfileStore(&profileStore);
portal.setStationRecoveryInterval(30000);
portal.startStationConnection("WiFiManager Profiles", "example-pass");
}
void loop() {
portal.process();
}
+26
View File
@@ -0,0 +1,26 @@
[platformio]
default_envs = esp8266
src_dir = .
[common]
framework = arduino
lib_ldf_mode = deep+
lib_deps =
WiFiManager=symlink://../..
[env:esp8266]
extends = common
platform = espressif8266
board = d1_mini
platform_packages =
platformio/framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git#521ae60a89e64bb0d1eb7a0b7addf620ced5cad3
[env:esp32]
extends = common
platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip
board = esp32dev
build_unflags = -std=gnu++11
build_flags =
-std=gnu++14
-DSOC_WIFI_SUPPORTED=1
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
+4 -2
View File
@@ -268,7 +268,7 @@ struct PortalHomeCard {
struct PortalBrandState {
WiFiManagerPortalText title;
WiFiManagerPortalText identityTextOverride;
WiFiManagerPortalText homeIntro;
WiFiManagerPortalText tagline;
WiFiManagerPortalAsset logo;
WiFiManagerPortalText logoAltText;
};
@@ -915,7 +915,7 @@ class WiFiManager
// preload scanning causes AP to delay showing for users, but also caches and lets the cp load faster once its open
// my scan takes 7-10 seconds
public:
boolean _preloadwifiscan = false; // preload wifiscan if true
boolean _preloadwifiscan = true; // begin one asynchronous scan as the portal starts
unsigned int _scancachetime = 30000; // ms cache time for preload scans
protected:
@@ -963,6 +963,7 @@ protected:
void handleStationAttemptFailure(uint8_t status, const String& message);
void handleStationConnectionSuccess();
void completePortalStationAttempt(bool success, uint8_t status, const String& message);
void acknowledgePortalConnectHandoff();
void enterStationPortal();
bool hasUsableStationConnection() const;
bool isStationProfileEnabled(const WiFiManagerStationProfiles& profiles, uint8_t slot) const;
@@ -1083,6 +1084,7 @@ protected:
_cpConnectStationIp = stationIp;
_cpConnectStatus = status;
}
void wmTestCompleteProfilePortalConnectionSuccess();
void wmTestSetPortalConnectFailure(const String& message, uint8_t status = WL_CONNECT_FAILED) {
_cpConnectState = wm_cp_connect_state_t::failed;
_cpConnectMessage = message;
@@ -55,6 +55,8 @@ class WiFiManagerHandlers {
void handleApiWifiMeta(AsyncWebServerRequest *request);
void handleApiWifiSave(AsyncWebServerRequest *request);
void handleApiWifiConnectStatus(AsyncWebServerRequest *request);
void handleApiWifiConnectComplete(AsyncWebServerRequest *request);
void handleApiPortalTimeoutReset(AsyncWebServerRequest *request);
void handleApiParamsGet(AsyncWebServerRequest *request);
void handleApiParamsSave(AsyncWebServerRequest *request);
void handleApiInfo(AsyncWebServerRequest *request);
@@ -84,7 +84,7 @@ struct WiFiManagerPortalTheme {
struct WiFiManagerPortalConfig {
WiFiManagerPortalText title;
WiFiManagerPortalText identityText;
WiFiManagerPortalText homeIntro;
WiFiManagerPortalText tagline;
WiFiManagerPortalAsset logo;
WiFiManagerPortalText logoAltText;
WiFiManagerPortalTheme theme;
@@ -15,6 +15,8 @@
* GET /api/wifi/meta
* POST /api/wifi/save
* GET /api/wifi/connect-status
* POST /api/wifi/connect-complete
* POST /api/portal/timeout-reset
* GET /api/params
* POST /api/params/save
* GET /api/info
@@ -55,6 +57,8 @@ const char R_api_wifi_scan[] PROGMEM = "/api/wifi/scan";
const char R_api_wifi_meta[] PROGMEM = "/api/wifi/meta";
const char R_api_wifi_save[] PROGMEM = "/api/wifi/save";
const char R_api_wifi_connect_status[] PROGMEM = "/api/wifi/connect-status";
const char R_api_wifi_connect_complete[] PROGMEM = "/api/wifi/connect-complete";
const char R_api_portal_timeout_reset[] PROGMEM = "/api/portal/timeout-reset";
const char R_api_params[] PROGMEM = "/api/params";
const char R_api_params_save[] PROGMEM = "/api/params/save";
const char R_api_info[] PROGMEM = "/api/info";
+34 -9
View File
@@ -74,19 +74,21 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-icon-label{display:inline-flex;align-items:center;gap:8px;min-width:0}"
".wm-icon{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;flex:0 0 1.1rem;color:currentColor}"
".wm-icon svg{display:block;width:100%;height:100%}"
/* Hero / page chrome */
".wm-hero{margin-bottom:20px}"
".wm-hero-title{font-size:1.65rem;line-height:1.2;margin-bottom:.25em}"
".wm-hero-sub{font-size:1rem;color:var(--wm-muted);font-weight:500;margin:0}"
".wm-brand-logo{margin-bottom:12px;line-height:0}"
".wm-brand-logo svg{display:block;max-width:160px;height:auto}"
".wm-hero-intro{font-size:.95rem;color:var(--wm-muted);margin:.5em 0 0;line-height:1.45;max-width:42ch}"
/* Header / page chrome */
".wm-site-header{display:flex;align-items:center;gap:10px;min-height:40px;margin:0 0 12px}"
".wm-site-logo{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex:0 0 40px;line-height:0}"
".wm-site-logo svg{display:block;width:100%;height:100%}"
".wm-site-brand-copy{min-width:0}"
".wm-site-brand-name{margin:0;font-size:.78rem;font-weight:800;letter-spacing:.09em;text-transform:uppercase;color:var(--wm-text)}"
".wm-site-tagline{margin:2px 0 0;font-size:.82rem;line-height:1.25;color:var(--wm-muted)}"
".wm-page-head{margin-bottom:16px}"
".wm-page-head h1{font-size:1.45rem}"
".wm-page-desc{font-size:.95rem;color:var(--wm-muted);margin:.35em 0 0}"
/* Cards */
".wm-card{background:var(--wm-surface);border:1px solid var(--wm-border);border-radius:var(--wm-radius);"
"box-shadow:var(--wm-shadow);padding:18px 16px;margin-bottom:16px;text-align:left}"
".wm-scan-card,.wm-ota-card{position:relative;overflow:hidden}"
".wm-scan-results{min-height:126px}"
".wm-card-title{font-size:1rem;margin:0 0 12px;padding-bottom:10px;border-bottom:1px solid var(--wm-border);"
"color:var(--wm-text)}"
".wm-card-subtitle{margin:-4px 0 12px;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
@@ -120,6 +122,31 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-checkbox-row{display:flex;align-items:center;gap:8px;margin:12px 0;font-size:.92rem}"
".wm-checkbox-row label{display:inline;margin:0;font-weight:500;color:var(--wm-text)}"
".wm-form-actions{margin-top:16px}"
".wm-wifi-details-card{position:relative}"
".wm-wifi-save-overlay{position:fixed;inset:0;z-index:10002;display:none;align-items:center;justify-content:center;padding:18px;"
"box-sizing:border-box;background:rgba(15,23,42,.58)}"
".wm-wifi-save-panel{display:flex;align-items:center;gap:14px;width:100%;max-width:340px;padding:18px;border:1px solid var(--wm-border);"
"border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
".wm-wifi-save-panel strong{display:block;font-size:1rem;color:var(--wm-text)}"
".wm-wifi-save-panel p{margin:4px 0 0;font-size:.9rem;line-height:1.45;color:var(--wm-muted)}"
".wm-operation-overlay{position:absolute;inset:0;z-index:5;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(15,23,42,.56)}"
".wm-operation-panel{display:flex;align-items:center;gap:12px;width:100%;max-width:310px;padding:16px;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-surface);box-shadow:var(--wm-shadow);text-align:left}"
".wm-operation-panel strong{display:block;font-size:.98rem;color:var(--wm-text)}"
".wm-operation-panel p{margin:3px 0 0;font-size:.88rem;line-height:1.4;color:var(--wm-muted)}"
".wm-operation-progress{margin-top:10px}"
".wm-operation-progress-track{height:7px;overflow:hidden;border-radius:999px;background:var(--wm-border)}"
".wm-operation-progress-track span{display:block;width:0;height:100%;border-radius:inherit;background:var(--wm-brand);transition:width .15s ease}"
".wm-operation-progress-label{display:block;margin-top:4px;font-size:.8rem;font-weight:700;color:var(--wm-muted);text-align:right}"
".wm-portal-timeout-row{display:flex;align-items:stretch;gap:8px;margin:0 0 12px}"
".wm-portal-timeout-row .wm-status{flex:1;margin:0}"
".wm-icon-button{display:inline-flex;align-items:center;justify-content:center;width:42px;min-height:42px;padding:0;border:1px solid var(--wm-border);border-radius:var(--wm-radius-sm);background:var(--wm-bg);color:var(--wm-text);cursor:pointer}"
".wm-icon-button:hover{border-color:var(--wm-brand);color:var(--wm-brand)}"
".wm-icon-button:disabled{opacity:.5;cursor:not-allowed}"
".wm-icon-button .wm-icon{width:1.05rem;height:1.05rem}"
".wm-inline-link{display:inline-block;margin-top:12px;font-size:.92rem}"
".wm-spinner{width:24px;height:24px;flex:0 0 24px;border:3px solid var(--wm-border);border-top-color:var(--wm-brand);border-radius:50%;animation:wm-spin .8s linear infinite}"
"@keyframes wm-spin{to{transform:rotate(360deg)}}"
"@media (prefers-reduced-motion:reduce){.wm-spinner{animation:none;border-right-color:var(--wm-brand)}}"
/* Buttons */
".wm-btn{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:0 18px;"
"font-size:1rem;font-weight:600;font-family:inherit;border-radius:var(--wm-radius-sm);border:0;cursor:pointer;"
@@ -180,8 +207,6 @@ const char CSS_STYLE[] PROGMEM = "<style>"
".wm-kv dd:last-of-type{border-bottom:0}"
/* Feedback areas */
"#wm-wifi-msg,#wm-param-msg{font-size:.9rem;margin-top:12px;min-height:1.2em;color:var(--wm-muted)}"
"#wm-ota-log{font-size:.8rem;background:var(--wm-bg);padding:12px;border-radius:var(--wm-radius-sm);"
"border:1px solid var(--wm-border);overflow:auto;max-height:160px}"
// invert legacy body
"body.invert{background-color:var(--wm-bg)}"
"body.invert .msg{border-color:var(--wm-border)}"
+354 -115
View File
@@ -120,7 +120,7 @@ function startPortalTimeoutCountdown(initialSeconds){
function render(){
if(!el)return;
el.className='wm-status'+(levelClass(remaining)?' '+levelClass(remaining):'');
el.textContent='Captive portal timeout: '+formatDurationSeconds(remaining);
el.textContent='Portal closes in '+formatDurationSeconds(remaining);
}
render();
_wmPortalTimeoutTimer=setInterval(function(){
@@ -260,6 +260,28 @@ function rowsWithoutKeys(rows,blocked){
return out;
}
function portalHeader(){
var brand=boot.brand||{};
var ctx=boot.context||{};
var identity=esc(ctx.identityText||brand.title||'WiFiManager');
var logoAlt=esc(brand.logoAltText||'');
var h="<header class='wm-site-header'>";
if(brand.logoSvg){h+="<div class='wm-site-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
h+="<div class='wm-site-brand-copy'><p class='wm-site-brand-name'>"+identity+"</p>";
if(brand.tagline){h+="<p class='wm-site-tagline'>"+esc(brand.tagline)+"</p>";}
h+="</div></header>";
return h;
}
function applyPortalFavicon(){
var brand=boot.brand||{};
if(!brand.logoSvg)return;
var icon=document.querySelector("link[rel~='icon']");
if(!icon){icon=document.createElement('link');icon.rel='icon';document.head.appendChild(icon);}
icon.type='image/svg+xml';
icon.href='data:image/svg+xml,'+encodeURIComponent(brand.logoSvg);
}
function navBar(active){
active=active||'home';
var pages=boot.pages||{};
@@ -269,11 +291,11 @@ function navBar(active){
var ps=pages.setup||{};
var pi=pages.info||{};
var pu=pages.update||{};
var h="<nav class='wm-nav' aria-label='Configuration'>";
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Home')+"</a>";
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','WiFi')+"</a>";
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Setup')+"</a>";}
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Info')+"</a>";}
var h=portalHeader()+"<nav class='wm-nav' aria-label='Configuration'>";
h+="<a class='wm-nav-link"+(active==='home'?' wm-nav-link--active':'')+"' href='#/'>"+labelWithIcon('home','Overview')+"</a>";
h+="<a class='wm-nav-link"+(active==='wifi'?' wm-nav-link--active':'')+"' href='#/wifi'>"+labelWithIcon('wifi','Wi-Fi')+"</a>";
if(!pinWifi && ps.visible!==false){h+="<a class='wm-nav-link"+(active==='setup'?' wm-nav-link--active':'')+"' href='#/setup'>"+labelWithIcon('setup','Settings')+"</a>";}
if(pi.visible!==false){h+="<a class='wm-nav-link"+(active==='info'?' wm-nav-link--active':'')+"' href='#/info'>"+labelWithIcon('info','Device')+"</a>";}
if(pu.visible){h+="<a class='wm-nav-link"+(active==='update'?' wm-nav-link--active':'')+"' href='#/update'>"+labelWithIcon('update','Update')+"</a>";}
if(actions.back&&actions.back.visible){h+="<a href='#' class='wm-nav-link' id='wm-nav-back'>"+labelWithIcon('back','Back')+"</a>";}
h+="</nav>";
@@ -311,30 +333,91 @@ function renderExtraHomeCards(cards){
return out;
}
function renderHomeStatusDetails(info){
var st=info.status||{};
var rows=[];
if(st.connected!==undefined)rows.push({label:'Wi-Fi',value:st.connected?'Connected':'Not connected'});
if(st.ssid)rows.push({label:'Network',value:st.ssid});
if(st.connected&&st.stationIp&&st.stationIp!=='0.0.0.0'&&st.stationIp!=='(IP unset)')rows.push({label:'Address',value:st.stationIp});
else if(st.apIp)rows.push({label:'Portal',value:st.apIp});
var device=info.device||[];
var wanted={uptime:true,freeheap:true};
for(var i=0;i<device.length;i++){
if(device[i]&&wanted[device[i].key]){
rows.push({label:device[i].label||device[i].key,value:device[i].value||''});
}
}
if(!rows.length)return"<p class='wm-lead'>Device details are unavailable.</p>";
var html="<dl class='wm-kv wm-home-status-details'>";
for(var j=0;j<rows.length;j++){
html+="<dt>"+esc(rows[j].label)+"</dt>";
html+="<dd>"+esc(rows[j].value)+"</dd>";
}
return html+="</dl>";
}
function loadHomeStatus(){
var box=$('wm-home-status-details');
if(!box)return;
api('/api/info').then(function(res){
var info={};
try{info=JSON.parse(res.body);}catch(e){}
box.innerHTML=renderHomeStatusDetails(info);
}).catch(function(){
box.innerHTML="<p class='wm-lead'>Unable to load device details.</p>";
});
}
function resetPortalTimeout(){
var button=$('wm-reset-portal-timeout');
if(button)button.disabled=true;
api('/api/portal/timeout-reset',{method:'POST'}).then(function(res){
var data={};
try{data=JSON.parse(res.body);}catch(e){}
if(!res.ok){
showToast(data.message||'Unable to reset portal timeout.',true);
return;
}
startPortalTimeoutCountdown(data.timeoutSecondsRemaining||0);
showToast('Portal timeout reset.',false);
}).catch(function(){
showToast('Unable to reset portal timeout.',true);
}).then(function(){
if(button)button.disabled=false;
});
}
function viewHome(){
var brand=boot.brand||{};
var ctx=boot.context||{};
var t=esc(brand.title||'WiFiManager');
var ident=ctx.identityText?esc(ctx.identityText):'';
var title=esc(brand.title||'WiFiManager');
var summary=ctx.statusSummary||'';
var timeoutSeconds=ctx.portalTimeoutSecondsRemaining||0;
var html=navBar('home');
html+="<header class='wm-hero'>";
var logoAlt=esc(brand.logoAltText||'');
if(brand.logoSvg){html+="<div class='wm-brand-logo'"+(logoAlt?" role='img' aria-label='"+logoAlt+"'":"")+">"+brand.logoSvg+"</div>";}
html+="<h1 class='wm-hero-title'>"+t+"</h1>";
if(ident){html+="<p class='wm-hero-sub'>"+ident+"</p>";}
if(brand.homeIntro){html+="<p class='wm-hero-intro'>"+esc(brand.homeIntro)+"</p>";}
html+="</header>";
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
if(summary){html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";}
if(timeoutSeconds>0){html+="<p class='wm-status' id='wm-portal-timeout'></p>";}
html+="<p class='wm-lead'>Use <strong>WiFi</strong> to scan networks and connect. Open <strong>Info</strong> for addresses and firmware actions.</p>";
html+="</div>";
html+="<div class='wm-page-head'><h1>"+title+"</h1></div>";
html+="<div class='wm-card'>"+cardTitleHtml('status','Device status');
if(summary){
html+="<p class='wm-home-summary'>"+esc(summary)+"</p>";
}
if(timeoutSeconds>0){
html+="<div class='wm-portal-timeout-row'>";
html+="<p class='wm-status' id='wm-portal-timeout'></p>";
html+="<button type='button' class='wm-icon-button'";
html+=" id='wm-reset-portal-timeout' aria-label='Reset portal timeout'";
html+=" title='Reset portal timeout'>"+iconSvg('restart')+"</button></div>";
}
html+="<div id='wm-home-status-details'>";
html+="<p class='wm-lead'>Loading device details…</p></div>";
html+="<a class='wm-inline-link' href='#/info'>View full device details</a></div>";
html+=renderExtraHomeCards(boot.extraHomeCards||[]);
html+=deviceActionsHtml();
setView(html);
if(timeoutSeconds>0)startPortalTimeoutCountdown(timeoutSeconds);
if(timeoutSeconds>0){
startPortalTimeoutCountdown(timeoutSeconds);
var reset=$('wm-reset-portal-timeout');
if(reset)reset.addEventListener('click',resetPortalTimeout);
}
loadHomeStatus();
}
// --- HTTP ---
@@ -366,27 +449,55 @@ function stopWifiScanPolling(){
setScanRefreshEnabled(true);
}
function setWifiScanProgress(visible,title,detail){
var overlay=$('wm-wifi-scan-overlay');
var titleEl=$('wm-wifi-scan-title');
var detailEl=$('wm-wifi-scan-detail');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Scanning Wi-Fi';
if(detailEl)detailEl.textContent=detail||'';
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function startWifiScanPolling(box){
stopWifiScanPolling();
setScanRefreshEnabled(false);
setWifiScanProgress(true,'Scanning Wi-Fi','Looking for nearby networks…');
var token=++_wmWifiScanPollToken;
var unavailableDeadline=Date.now()+20000;
function retryAfterTemporaryRadioLoss(){
if(Date.now()<unavailableDeadline){
setWifiScanProgress(true,'Scanning Wi-Fi','Waiting for the device radio to return…');
_wmWifiScanPollTimer=setTimeout(poll,1000);
return true;
}
return false;
}
function poll(){
api('/api/wifi/scan-status').then(function(res){
if(token!==_wmWifiScanPollToken)return;
var d={};
try{d=JSON.parse(res.body);}catch(e){return;}
if(!res.ok)throw new Error('scan status unavailable');
var data={};
try{data=JSON.parse(res.body);}catch(e){
if(retryAfterTemporaryRadioLoss())return;
throw e;
}
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanList(d);
if(!d.scanning){
if(!data.scanning){
if(box)box.innerHTML=renderScanList(data);
setWifiScanProgress(false);
stopWifiScanPolling();
return;
}
_wmWifiScanPollTimer=setTimeout(poll,800);
}).catch(function(){
if(token!==_wmWifiScanPollToken)return;
if(retryAfterTemporaryRadioLoss())return;
stopWifiScanPolling();
setWifiScanProgress(false);
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Portal became unreachable while scanning.');
if(box)box.innerHTML=renderScanError('The portal did not return after scanning.');
});
}
poll();
@@ -440,21 +551,23 @@ function wifiRefresh(){
var box=getScanResultsBox();
if(!box)return;
setScanRefreshEnabled(false);
box.innerHTML="<div class='wm-scan-list'><p class='wm-lead' style='margin:0;padding:14px 12px'>Starting scan…</p></div>";
setWifiScanProgress(true,'Preparing Wi-Fi scan','Starting a scan…');
api('/api/wifi/scan',{method:'POST'}).then(function(res){
box=getScanResultsBox()||box;
if(!res.ok){
setScanRefreshEnabled(true);
if(box)box.innerHTML=renderScanError('Failed to start WiFi scan.');
showToast('Failed to start WiFi scan.',true);
setWifiScanProgress(false);
if(box)box.innerHTML=renderScanError('Failed to start Wi-Fi scan.');
showToast('Failed to start Wi-Fi scan.',true);
return;
}
startWifiScanPolling(box);
}).catch(function(){
stopWifiScanPolling();
setWifiScanProgress(false);
box=getScanResultsBox()||box;
if(box)box.innerHTML=renderScanError('Failed to reach portal while starting scan.');
showToast('Failed to reach portal while starting scan.',true);
if(box)box.innerHTML=renderScanError('Failed to reach the portal while starting a scan.');
showToast('Failed to reach the portal while starting a scan.',true);
});
}
@@ -530,57 +643,76 @@ function renderStationProfileFields(profiles){
}
function viewWifi(){
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading WiFi options…</p></div>");
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Loading Wi-Fi options…</p></div>");
api('/api/wifi/meta').then(function(res){
try{var m=JSON.parse(res.body);}catch(e){m={};}
var meta={};
try{meta=JSON.parse(res.body);}catch(e){}
var html=navBar('wifi');
html+="<div class='wm-page-head'><h1>WiFi</h1><p class='wm-page-desc'>Pick a network below, confirm SSID and password, then connect.</p></div>";
html+="<div class='wm-card'>"+cardTitleHtml('scan','Networks nearby');
html+="<div id='wm-scan-results'></div>";
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button></div>";
html+="<div class='wm-card'>"+cardTitleHtml('wifi','Network details');
if(Array.isArray(m.profiles) && m.profiles.length){
html+=renderStationProfileFields(m.profiles);
html+="<div class='wm-page-head'><h1>Connect to Wi-Fi</h1>";
html+="<p class='wm-page-desc'>Choose a network, then save and connect.</p></div>";
html+="<div class='wm-card wm-scan-card'>"+cardTitleHtml('scan','Networks nearby');
html+="<div id='wm-scan-results' class='wm-scan-results'>";
html+="<p class='wm-lead'>Preparing nearby networks…</p></div>";
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
html+=" id='wm-refresh-scan'>"+labelWithIcon('scan','Scan again')+"</button>";
html+="<div id='wm-wifi-scan-overlay' class='wm-operation-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span>";
html+="<div><strong id='wm-wifi-scan-title'>Scanning Wi-Fi</strong>";
html+="<p id='wm-wifi-scan-detail'>Looking for nearby networks…</p></div></div></div></div>";
html+="<div class='wm-card wm-wifi-details-card'>"+cardTitleHtml('wifi','Network details');
if(Array.isArray(meta.profiles)&&meta.profiles.length){
html+=renderStationProfileFields(meta.profiles);
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
}else{
html+="<form class='wm-wifi-data-form' id='wm-wifi-form'>";
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+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/> <label for='wm-showpass'>Show password</label></div>";
html+=renderFieldList(meta.wifiFields||[]);
var wifiFields=meta.wifiFields||[];
var hasPassword=false;
for(var i=0;i<wifiFields.length;i++){
if(wifiFields[i]&&wifiFields[i].id==='p'){hasPassword=true;break;}
}
if(hasPassword){
html+="<div class='wm-checkbox-row'><input type='checkbox' id='wm-showpass'/>";
html+=" <label for='wm-showpass'>Show password</label></div>";
}
}
html+=renderFieldList(m.staticFields||[]);
html+=renderFieldList(m.params||[]);
html+=renderFieldList(meta.staticFields||[]);
html+=renderFieldList(meta.params||[]);
html+="</form>";
html+="<div class='wm-form-actions'><button type='button' class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>"+labelWithIcon('connect','Save and connect')+"</button>";
if(Array.isArray(m.profiles) && m.profiles.length){
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block' data-wm-wifi-action='save'>Save for later</button>";
html+="<div class='wm-form-actions'><button type='button'";
html+=" class='wm-btn wm-btn--primary wm-btn--block' data-wm-wifi-action='connect'>";
html+=labelWithIcon('connect','Save and connect')+"</button>";
if(Array.isArray(meta.profiles)&&meta.profiles.length){
html+="<button type='button' class='wm-btn wm-btn--secondary wm-btn--block'";
html+=" data-wm-wifi-action='save'>Save for later</button>";
}
html+="</div>";
html+="<div id='wm-wifi-msg'></div></div>";
html+="</div><div id='wm-wifi-msg'></div></div>";
html+="<div id='wm-wifi-save-overlay' class='wm-wifi-save-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-wifi-save-panel'><span class='wm-spinner' aria-hidden='true'></span>";
html+="<div><strong id='wm-wifi-save-title'>Saving settings</strong>";
html+="<p id='wm-wifi-save-detail'>Please wait…</p></div></div></div>";
setView(html);
bindWifiViewEvents();
setScanRefreshEnabled(true);
api('/api/wifi/scan-status').then(function(r2){
try{var d=JSON.parse(r2.body);}catch(e){d={};}
api('/api/wifi/scan-status').then(function(scanRes){
var scan={};
try{scan=JSON.parse(scanRes.body);}catch(e){}
var box=getScanResultsBox();
if(shouldAutoStartWifiScan(d)){
if(shouldAutoStartWifiScan(scan)){
wifiRefresh();
return;
}
if(box)box.innerHTML=renderScanList(d);
if(d&&d.scanning){startWifiScanPolling(box);}
if(box)box.innerHTML=renderScanList(scan);
if(scan&&scan.scanning)startWifiScanPolling(box);
}).catch(function(){
var box=getScanResultsBox();
if(box)box.innerHTML=renderScanError('Failed to reach portal while loading scan status.');
if(box)box.innerHTML=renderScanError('Failed to load nearby networks.');
setScanRefreshEnabled(true);
});
}).catch(function(){
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load WiFi options. Reconnect to the portal and try again.</p></div>");
setView(navBar('wifi')+"<div class='wm-card'><p class='wm-lead'>Failed to load Wi-Fi options. Reconnect to the portal and try again.</p></div>");
});
}
@@ -588,6 +720,33 @@ function viewWifi(){
var _wmWifiConnectPollTimer=null;
var _wmWifiConnectPollToken=0;
var _wmWifiConnectPending=false;
var _wmWifiSaveInFlight=false;
function setWifiSaveControlsDisabled(disabled){
var controls=document.querySelectorAll('.wm-wifi-data-form input,.wm-wifi-data-form select,.wm-wifi-data-form textarea,[data-wm-wifi-action],#wm-refresh-scan');
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
}
function setWifiSaveProgress(visible,title,detail){
var overlay=$('wm-wifi-save-overlay'),titleEl=$('wm-wifi-save-title'),detailEl=$('wm-wifi-save-detail');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Saving settings';
if(detailEl)detailEl.textContent=detail||'';
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function beginWifiSave(title,detail){
_wmWifiSaveInFlight=true;
setWifiSaveControlsDisabled(true);
setWifiSaveProgress(true,title,detail);
}
function finishWifiSave(){
_wmWifiSaveInFlight=false;
setWifiSaveControlsDisabled(false);
setWifiSaveProgress(false);
}
function stopWifiConnectPolling(){
_wmWifiConnectPollToken++;
@@ -597,47 +756,72 @@ function stopWifiConnectPolling(){
}
}
function completeWifiHandoff(data,attempt){
attempt=attempt||0;
api('/api/wifi/connect-complete',{method:'POST'}).then(function(res){
if(!res.ok){
if(attempt<2){setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);return;}
throw new Error('handoff acknowledgement rejected');
}
if(data.redirectUrl){
setTimeout(function(){window.location.href=data.redirectUrl;},1200);
}else{
location.hash='#/';
}
}).catch(function(){
if(attempt<2){
setTimeout(function(){completeWifiHandoff(data,attempt+1);},500);
return;
}
setWifiSaveProgress(true,'Wi-Fi connected','Open the device address shown below.');
showToast('Wi-Fi connected, but automatic redirect was unavailable.',true);
});
}
function pollWifiConnectStatus(){
stopWifiConnectPolling();
var token=++_wmWifiConnectPollToken;
function poll(){
api('/api/wifi/connect-status').then(function(res){
if(token!==_wmWifiConnectPollToken)return;
var j={};
var data={};
var msg=$('wm-wifi-msg');
try{j=JSON.parse(res.body);}catch(e){}
if(msg&&j.message){
msg.innerHTML=esc(j.message);
}
if(j.state==='success'){
try{data=JSON.parse(res.body);}catch(e){}
if(msg&&data.message)msg.innerHTML=esc(data.message);
if(data.state==='success'){
stopWifiConnectPolling();
_wmWifiConnectPending=false;
if(msg && j.stationIp){
msg.innerHTML="Connected to WiFi. Redirecting to <a href='"+esc(j.redirectUrl||("http://"+j.stationIp+"/"))+"'>"+esc(j.stationIp)+"</a>…";
if(!data.stationIp){
finishWifiSave();
if(msg)msg.innerHTML=esc(data.message||'Settings saved.');
showToast(data.message||'Settings saved.',false);
return;
}
showToast(j.message||'WiFi connected',false);
if(j.redirectUrl){
setTimeout(function(){ window.location.href=j.redirectUrl; }, 1400);
}else{
location.hash='#/';
var destination=data.redirectUrl||'';
setWifiSaveProgress(true,'Connected to Wi-Fi','Opening '+data.stationIp+'…');
if(msg){
msg.innerHTML="Connected. Opening <a href='"+esc(destination||('http://'+data.stationIp+'/'))+"'>";
msg+=esc(data.stationIp)+"</a>…";
}
}else if(j.state==='failed'){
showToast(data.message||'Wi-Fi connected',false);
completeWifiHandoff(data);
}else if(data.state==='failed'){
stopWifiConnectPolling();
_wmWifiConnectPending=false;
showToast(j.message||'WiFi connect failed',true);
finishWifiSave();
showToast(data.message||'Wi-Fi connection failed',true);
}else{
setWifiSaveProgress(true,'Connecting to Wi-Fi',data.message||'Waiting for the device to join the selected network…');
_wmWifiConnectPollTimer=setTimeout(poll,700);
}
}).catch(function(){
if(token!==_wmWifiConnectPollToken)return;
stopWifiConnectPolling();
finishWifiSave();
var msg=$('wm-wifi-msg');
if(msg){
msg.innerHTML=_wmWifiConnectPending
? 'Portal became unreachable after saving. If the device joined your WiFi network, reconnect to it there.'
: 'Portal became unreachable while checking connection status.';
}
if(msg)msg.innerHTML='Unable to check Wi-Fi connection status. Try again.';
_wmWifiConnectPending=false;
showToast('Unable to check Wi-Fi connection status.',true);
});
}
poll();
@@ -652,9 +836,11 @@ function collectWifiFormData(){
}
function submitWifi(action){
if(_wmWifiSaveInFlight)return false;
var fd=collectWifiFormData();
if(action)fd.append('stationAction',action);
var msg=$('wm-wifi-msg');
beginWifiSave(action==='save'?'Saving settings':'Saving and connecting',action==='save'?'Saving these settings for a later connection...':'Saving settings and preparing the WiFi connection...');
if(msg)msg.innerHTML='Submitting...';
api('/api/wifi/save',{method:'POST',body:fd}).then(function(res){
var j={};
@@ -664,8 +850,19 @@ function submitWifi(action){
}
if(res.status===202){
_wmWifiConnectPending=true;
setWifiSaveProgress(true,'Connecting to WiFi',(j&&j.message)?j.message:'Waiting for the device to join the selected network...');
pollWifiConnectStatus();
}else if(res.ok){
finishWifiSave();
showToast((j&&j.message)?j.message:'Settings saved.',false);
}else{
finishWifiSave();
showToast((j&&j.message)?j.message:'Unable to save settings.',true);
}
}).catch(function(){
finishWifiSave();
if(msg)msg.innerHTML='Unable to reach the portal while saving. Please try again.';
showToast('Unable to reach the portal while saving.',true);
});
return false;
}
@@ -686,7 +883,7 @@ function viewInfo(){
api('/api/info').then(function(res){
try{var d=JSON.parse(res.body);}catch(e){d={};}
var html=navBar('info');
html+="<div class='wm-page-head'><h1>Device info</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
html+="<div class='wm-page-head'><h1>Device</h1><p class='wm-page-desc'>Connection status, hardware, and actions.</p></div>";
var st=d.status;
if(st && typeof st==='object' && !Array.isArray(st)){
html+="<div class='wm-card'>"+cardTitleHtml('status','Status');
@@ -755,7 +952,7 @@ function viewSetup(){
api('/api/params').then(function(res){
try{var d=JSON.parse(res.body);}catch(e){d={};}
var html=navBar('setup');
html+="<div class='wm-page-head'><h1>Setup</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
html+="<div class='wm-page-head'><h1>Settings</h1><p class='wm-page-desc'>Custom parameters stored on this device.</p></div>";
html+="<div class='wm-card'><form id='wm-param-form'>";
html+=renderFieldList(d.params||[]);
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('save','Save parameters')+"</button></div>";
@@ -776,59 +973,100 @@ window.portalParamSave=function(ev){
return false;
};
function setOtaControlsDisabled(disabled){
var controls=document.querySelectorAll('#wm-ota-form input,#wm-ota-form button');
for(var i=0;i<controls.length;i++)controls[i].disabled=disabled;
}
function setOtaProgress(visible,title,detail,percent){
var overlay=$('wm-ota-overlay');
var titleEl=$('wm-ota-title');
var detailEl=$('wm-ota-detail');
var progress=$('wm-ota-progress');
var bar=$('wm-ota-progress-bar');
var label=$('wm-ota-progress-label');
if(!overlay)return;
if(titleEl)titleEl.textContent=title||'Uploading firmware';
if(detailEl)detailEl.textContent=detail||'';
var hasPercent=typeof percent==='number';
if(progress)progress.style.display=hasPercent?'block':'none';
if(hasPercent){
var value=Math.max(0,Math.min(100,Math.round(percent)));
if(bar)bar.style.width=value+'%';
if(label)label.textContent=value+'%';
}
overlay.style.display=visible?'flex':'none';
overlay.setAttribute('aria-hidden',visible?'false':'true');
}
function viewUpdate(){
var pages=boot.pages||{};
var pupd=pages.update||{};
var update=pages.update||{};
var html=navBar('update');
html+="<div class='wm-page-head'><h1>Firmware update</h1><p class='wm-page-desc'>Flash a new firmware build (.bin). The device reboots when done.</p></div>";
if(!pupd.visible){
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this configuration.</p></div>";
html+="<div class='wm-page-head'><h1>Update firmware</h1>";
html+="<p class='wm-page-desc'>Upload a firmware build for this device.</p></div>";
if(!update.visible){
html+="<div class='wm-card'><p class='wm-lead'>Firmware update is disabled for this device.</p></div>";
setView(html);
return;
}
html+="<div class='wm-card'><form id='wm-ota-form'>";
html+="<div class='wm-card wm-ota-card'><form id='wm-ota-form'>";
html+="<div class='wm-field'><label for='wm-ota-file'>Firmware file</label>";
html+="<input type='file' id='wm-ota-file' name='update' accept='.bin,.bin.gz'/></div>";
html+="<div class='wm-form-actions'><button type='submit' class='wm-btn wm-btn--primary wm-btn--block'>"+labelWithIcon('upload','Upload firmware')+"</button></div>";
html+="</form>";
html+="<p class='wm-lead' style='margin-top:14px;margin-bottom:8px'>Choose the correct binary for this chip. Upload progress appears below.</p>";
html+="<pre id='wm-ota-log'></pre></div>";
html+="<div class='wm-form-actions'><button type='submit'";
html+=" class='wm-btn wm-btn--primary wm-btn--block'>";
html+=labelWithIcon('upload','Upload firmware')+"</button></div></form>";
html+="<p class='wm-lead'>Use the binary built for this device. It will restart after a successful update.</p>";
html+="<div id='wm-ota-overlay' class='wm-operation-overlay'";
html+=" role='status' aria-live='polite' aria-atomic='true' aria-hidden='true'>";
html+="<div class='wm-operation-panel'><span class='wm-spinner' aria-hidden='true'></span><div>";
html+="<strong id='wm-ota-title'>Uploading firmware</strong>";
html+="<p id='wm-ota-detail'>Preparing upload…</p>";
html+="<div id='wm-ota-progress' class='wm-operation-progress' style='display:none'>";
html+="<div class='wm-operation-progress-track'><span id='wm-ota-progress-bar'></span></div>";
html+="<span id='wm-ota-progress-label'>0%</span></div></div></div></div></div>";
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.';
var fileInput=$('wm-ota-file');
if(!fileInput||!fileInput.files||!fileInput.files[0]){
showToast('Choose a firmware file first.',true);
return false;
}
if(logEl)logEl.textContent='Uploading…';
setOtaControlsDisabled(true);
setOtaProgress(true,'Uploading firmware','Preparing upload…');
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.upload.onprogress=function(event){
if(!event.lengthComputable)return;
var percent=100*event.loaded/event.total;
setOtaProgress(true,'Uploading firmware','Transferring firmware…',percent);
};
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);
var text=xhr.responseText||'';
var data={};
try{data=JSON.parse(text);}catch(e){}
var ok=xhr.status>=200&&xhr.status<300&&data.ok!==false;
if(ok){
setOtaProgress(true,'Firmware updated','Restarting device…',100);
showToast(data.message||'Firmware updated. Restarting device…',false);
return;
}
setOtaProgress(false);
setOtaControlsDisabled(false);
showToast(data.message||('Upload failed (HTTP '+xhr.status+').'),true);
};
xhr.onerror=function(){
setOtaProgress(false);
setOtaControlsDisabled(false);
showToast('Upload failed before the device responded.',true);
};
xhr.open('POST','/u');
var fd=new FormData();
fd.append('update',fi.files[0]);
xhr.send(fd);
var formData=new FormData();
formData.append('update',fileInput.files[0]);
xhr.send(formData);
return false;
};
@@ -853,6 +1091,7 @@ function route(){
}
function bootPortal(){
applyPortalFavicon();
route();
try{document.dispatchEvent(new CustomEvent('wm:ready',{detail:{boot:boot}}));}catch(e){}
}
+37 -3
View File
@@ -28,6 +28,10 @@ namespace {
constexpr size_t kMaxHostnameLength = 32;
constexpr uint8_t kSoftApStartMaxAttempts = 3;
constexpr unsigned long kStationProfileSwitchDelayMs = 2000UL; // ESP WiFi needs time to leave a failed association.
// A portal client acknowledges the successful station hand-off before the AP
// is closed. The fallback protects headless/captive clients that disappear.
constexpr unsigned long kPortalSuccessHandoffFallbackMs = 12000UL;
constexpr unsigned long kPortalSuccessHandoffAcknowledgedDelayMs = 700UL;
bool isValidHostnameChar(char c) {
return isAlphaNumeric(c) || c == '-';
@@ -345,6 +349,13 @@ bool WiFiManager::startStationConnection(char const *apName, char const *apPassw
_stationStatus.configuredProfiles = configuredStationProfileCount(_stationProfiles);
if (!validateStationProfiles(_stationProfiles)) {
// In station-profile mode the caller store is the single source of
// truth. ESP32 can reconnect from credentials retained in its own NVS
// before the portal is started, which would otherwise bypass an empty or
// invalid external profile set. Clear only that SDK-owned state here;
// the external store remains untouched and is what the portal will save.
WiFi_Disconnect();
WiFi_eraseConfig();
_stationStatus.message = _stationStatus.configuredProfiles == 0
? F("No WiFi profiles configured")
: F("Stored WiFi profiles are invalid");
@@ -505,6 +516,14 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
_cpConnectState = success ? wm_cp_connect_state_t::success : wm_cp_connect_state_t::failed;
if (success) {
// Profile-backed provisioning reaches this path rather than
// processPortalConnect(). Keep the AP available until the SPA has read
// the station address and acknowledged its redirect hand-off.
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
_cpConnectCloseAt = millis() + kPortalSuccessHandoffFallbackMs;
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
return;
}
if (_savewificallback != NULL) {
_savewificallback();
}
@@ -518,6 +537,22 @@ void WiFiManager::completePortalStationAttempt(bool success, uint8_t status, con
}
}
void WiFiManager::acknowledgePortalConnectHandoff() {
if (_cpConnectState != wm_cp_connect_state_t::success_waiting_close) {
return;
}
_cpConnectCloseAt = millis() + kPortalSuccessHandoffAcknowledgedDelayMs;
}
#ifdef UNIT_TEST
void WiFiManager::wmTestCompleteProfilePortalConnectionSuccess() {
_stationCandidateFromPortal = true;
configPortalActive = true;
_disableConfigPortal = true;
completePortalStationAttempt(true, WL_CONNECTED, F("WiFi connected"));
}
#endif
void WiFiManager::handleStationConnectionSuccess() {
const uint8_t slot = _stationStatus.attemptedSlot;
const bool wasCandidate = _stationCandidateActive;
@@ -1177,7 +1212,6 @@ void WiFiManager::queuePortalConnect(const String& ssid, const String& pass) {
void WiFiManager::processPortalConnect() {
const unsigned long now = millis();
const unsigned long successHandoffDelayMs = 2500UL;
auto finalizePortalConnectSuccess = [&]() {
_cpConnectState = wm_cp_connect_state_t::success;
if (_savewificallback != NULL) {
@@ -1279,7 +1313,7 @@ void WiFiManager::processPortalConnect() {
? String(F("WiFi connected. Redirecting to ")) + _cpConnectStationIp
: String(F("WiFi connected"));
if (_disableConfigPortal && (configPortalActive || webPortalActive)) {
_cpConnectCloseAt = now + successHandoffDelayMs;
_cpConnectCloseAt = now + kPortalSuccessHandoffFallbackMs;
_cpConnectState = wm_cp_connect_state_t::success_waiting_close;
} else {
finalizePortalConnectSuccess();
@@ -2635,7 +2669,7 @@ bool WiFiManager::setPortalConfig(const WiFiManagerPortalConfig& config) {
? WiFiManagerPortalText::progmem(kDefaultPortalTitle)
: config.title;
_portalBrand.identityTextOverride = config.identityText;
_portalBrand.homeIntro = config.homeIntro;
_portalBrand.tagline = config.tagline;
_portalBrand.logo = config.logo;
_portalBrand.logoAltText = config.logoAltText;
_portalTheme = config.theme;
+30 -3
View File
@@ -1064,12 +1064,12 @@ String WiFiManagerHandlers::buildPortalBootstrapJson() {
const bool portalRunning = _wm->configPortalActive || _wm->webPortalActive;
String json;
reservePage(json, 1200);
json += F("{\"contractVersion\":2");
json += F("{\"contractVersion\":3");
json += F(",\"brand\":{");
json += F("\"title\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.title);
json += F("\",\"homeIntro\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.homeIntro);
json += F("\",\"tagline\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.tagline);
json += F("\",\"logoSvg\":\"");
jsonAppendEscaped(json, _wm->_portalBrand.logo.svg);
json += F("\",\"logoAltText\":\"");
@@ -1404,6 +1404,33 @@ void WiFiManagerHandlers::handleApiWifiConnectStatus(AsyncWebServerRequest *requ
sendApiJson(request, 200, buildApiWifiConnectStatusJson());
}
void WiFiManagerHandlers::handleApiWifiConnectComplete(AsyncWebServerRequest *request) {
handleRequest(request);
if (!_wm->didConfigPortalConnectSucceed() || _wm->_cpConnectStationIp.length() == 0) {
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"WiFi hand-off is not ready\"}"));
return;
}
// The SPA has received the station address and can navigate to it. Keep the
// portal alive briefly so the normal device web server can start cleanly.
sendApiJson(request, 200, F("{\"ok\":true}"));
_wm->acknowledgePortalConnectHandoff();
}
void WiFiManagerHandlers::handleApiPortalTimeoutReset(AsyncWebServerRequest *request) {
handleRequest(request);
if (!_wm->configPortalActive || _wm->_configPortalTimeout == 0) {
sendApiJson(request, 409, F("{\"ok\":false,\"message\":\"Portal timeout is not active\"}"));
return;
}
_wm->_configPortalStart = millis();
String json = F("{\"ok\":true,\"timeoutSecondsRemaining\":");
json += String((_wm->_configPortalTimeout + 999UL) / 1000UL);
json += F("}");
sendApiJson(request, 200, json);
}
String WiFiManagerHandlers::buildApiParamsGetJson() {
String json = F("{\"params\":[");
bool first = true;
@@ -96,6 +96,12 @@ void WiFiManagerServer::registerRoutes() {
server->on(WM_G(R_api_wifi_connect_status), HTTP_GET, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiWifiConnectStatus(request);
});
server->on(WM_G(R_api_wifi_connect_complete), HTTP_POST, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiWifiConnectComplete(request);
});
server->on(WM_G(R_api_portal_timeout_reset), HTTP_POST, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiPortalTimeoutReset(request);
});
server->on(WM_G(R_api_params), HTTP_GET, [this](AsyncWebServerRequest *request) {
this->_handlers->handleApiParamsGet(request);
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "WiFiManager",
"version": "3.1.0",
"version": "3.2.1",
"keywords": [
"wifi",
"wi-fi",
@@ -58,7 +58,7 @@
},
{
"name": "DeviceFrameworkTemplateEngine",
"version": "https://github.com/alexhopeoconnor/DFTE.git#v1.1.0"
"version": "https://github.com/alexhopeoconnor/DFTE.git#v1.2.0"
}
],
"build": {
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $0 vMAJOR.MINOR.PATCH" >&2
exit 2
}
tag="${1:-}"
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
version="${tag#v}"
repo_url="https://github.com/alexhopeoconnor/WiFiManager.git"
reference_files=(README.md docs/GETTING_STARTED.md)
current_version="$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$root/library.json" | head -n 1)"
[[ "$current_version" != "$version" ]] || {
echo "library.json already declares $version; choose a new version." >&2
exit 1
}
grep -q "^## $version$" "$root/CHANGELOG.md" && {
echo "CHANGELOG.md already has a $version section; choose a new version." >&2
exit 1
}
sed -i -E '0,/"version": "[0-9]+\.[0-9]+\.[0-9]+"/s//"version": "'"$version"'"/' "$root/library.json"
for file in "${reference_files[@]}"; do
sed -i -E "s|${repo_url}#v[0-9]+\.[0-9]+\.[0-9]+|${repo_url}#v${version}|g" "$root/$file"
done
temp_file="$(mktemp)"
trap 'rm -f "$temp_file"' EXIT
{
IFS= read -r changelog_heading < "$root/CHANGELOG.md"
[[ "$changelog_heading" == "# Changelog" ]] || {
echo "CHANGELOG.md must begin with # Changelog" >&2
exit 1
}
printf '%s\n\n## %s\n\n- TODO: Describe this release.\n' "$changelog_heading" "$version"
tail -n +2 "$root/CHANGELOG.md"
} > "$temp_file"
mv "$temp_file" "$root/CHANGELOG.md"
echo "Updated WiFiManager declarations and canonical install references to $tag."
echo "Replace the generated changelog TODO with the release summary, then run scripts/check-docs.sh and scripts/prepare-release.sh $tag."
+13
View File
@@ -42,6 +42,19 @@ for required in README.md CHANGELOG.md docs/README.md docs/GETTING_STARTED.md do
fi
done
while IFS= read -r example; do
for required in README.md platformio.ini; do
if [[ ! -f "$example/$required" ]]; then
printf 'Incomplete example: %s is missing %s\n' "${example#$root/}" "$required" >&2
failed=1
fi
done
if ! find "$example" -maxdepth 2 -type f \( -name '*.ino' -o -name '*.cpp' \) -print -quit | grep -q .; then
printf 'Incomplete example: %s has no sketch source\n' "${example#$root/}" >&2
failed=1
fi
done < <(find "$root/examples" -mindepth 1 -maxdepth 1 -type d -print | sort)
if [[ "$failed" -ne 0 ]]; then
exit 1
fi
+20 -1
View File
@@ -32,11 +32,30 @@ if [[ -f "$root/library.properties" ]]; then
fi
fi
if awk -v heading="## $version" '
$0 == heading { found = 1; next }
found && /^## / { exit }
found { print }
' "$root/CHANGELOG.md" | grep -Fq 'TODO: Describe this release.'; then
echo "CHANGELOG.md still has the generated TODO for $version" >&2
exit 1
fi
repo_url="https://github.com/alexhopeoconnor/WiFiManager.git"
validate_reference() {
local file="$1"
local reference_count
reference_count="$(grep -F "$repo_url#v" "$root/$file" | wc -l)"
[[ "$reference_count" -eq 1 ]] || { echo "$file must contain exactly one canonical release reference" >&2; exit 1; }
grep -Fq "$repo_url#$tag" "$root/$file" || { echo "$file does not reference $tag" >&2; exit 1; }
}
validate_reference README.md
validate_reference docs/GETTING_STARTED.md
git -C "$root" diff --check
package_dir="$(mktemp -d)"
trap 'rm -rf "$package_dir"' EXIT
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
echo "Validated PlatformIO package for $tag"
echo "Validated release metadata and PlatformIO package for $tag"
if [[ "${2:-}" == "--tag" ]]; then
git -C "$root" diff --quiet
+15 -2
View File
@@ -2,15 +2,28 @@
set -euo pipefail
usage() {
echo "Usage: $0 compile --platform esp8266|esp32" >&2
echo "Usage: $0 compile|examples --platform esp8266|esp32" >&2
exit 2
}
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
[[ $# -eq 3 && ( "${1:-}" == "compile" || "${1:-}" == "examples" ) && "${2:-}" == "--platform" ]] || usage
platform="${3:-}"
[[ "$platform" == "esp8266" || "$platform" == "esp32" ]] || usage
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ "$1" == "examples" ]]; then
mapfile -t examples < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
if (( ${#examples[@]} == 0 )); then
echo "No example projects found" >&2
exit 1
fi
for example in "${examples[@]}"; do
pio run -d "$example" -e "$platform" </dev/null
done
echo "WiFiManager examples compile check passed for $platform"
exit 0
fi
cached_library="$root/test/compile-project/.pio/libdeps/${platform}/WiFiManager"
# The fixture intentionally declares only this local package. Remove a prior
# link so each check resolves the current manifest as a fresh consumer would.
+2 -2
View File
@@ -4,7 +4,7 @@
namespace {
const char kTitle[] PROGMEM = "Set up Compile Fixture";
const char kIdentity[] PROGMEM = "WiFiManager";
const char kIntro[] PROGMEM = "A branded portal compile check.";
const char kTagline[] PROGMEM = "A branded portal compile check.";
const char kLogoAlt[] PROGMEM = "WiFiManager";
const char kLogo[] PROGMEM = "<svg viewBox='0 0 24 24'></svg>";
const char kPage[] PROGMEM = "#f4f7f3";
@@ -19,7 +19,7 @@ const char kAccentText[] PROGMEM = "#ffffff";
const WiFiManagerPortalConfig kPortalConfig = {
WiFiManagerPortalText::progmem(kTitle),
WiFiManagerPortalText::progmem(kIdentity),
WiFiManagerPortalText::progmem(kIntro),
WiFiManagerPortalText::progmem(kTagline),
WiFiManagerPortalAsset::svgFromProgmem(kLogo),
WiFiManagerPortalText::progmem(kLogoAlt),
{
+2 -1
View File
@@ -13,6 +13,7 @@ TestCase tests[] = {
TEST_ENTRY(test_api_wifi_meta_json_shape),
TEST_ENTRY(test_api_wifi_meta_password_field_type),
TEST_ENTRY(test_api_wifi_connect_status_success_redirect),
TEST_ENTRY(test_profile_portal_success_keeps_handoff_alive),
TEST_ENTRY(test_api_info_json_shape),
TEST_ENTRY(test_api_params_json_shape),
TEST_ENTRY(test_api_status_json_shape),
@@ -37,7 +38,7 @@ TestCase tests[] = {
TEST_ENTRY(test_get_config_portal_ssid),
TEST_ENTRY(test_bootstrap_json_portal_feature_flags),
TEST_ENTRY(test_portal_default_presentation),
TEST_ENTRY(test_bootstrap_json_contract_v2),
TEST_ENTRY(test_bootstrap_json_contract_v3),
TEST_ENTRY(test_bootstrap_json_snapshot_consistency),
TEST_ENTRY(test_root_render_interleaved_context_isolation),
TEST_ENTRY(test_portal_presentation_configuration),
+2 -1
View File
@@ -39,7 +39,7 @@ void test_config_portal_already_active();
void test_get_config_portal_ssid();
void test_bootstrap_json_portal_feature_flags();
void test_portal_default_presentation();
void test_bootstrap_json_contract_v2();
void test_bootstrap_json_contract_v3();
void test_bootstrap_json_snapshot_consistency();
void test_root_render_interleaved_context_isolation();
void test_portal_presentation_configuration();
@@ -117,6 +117,7 @@ void test_captive_redirect_host_rule();
void test_api_wifi_meta_json_shape();
void test_api_wifi_meta_password_field_type();
void test_api_wifi_connect_status_success_redirect();
void test_profile_portal_success_keeps_handoff_alive();
void test_api_info_json_shape();
void test_api_params_json_shape();
void test_api_status_json_shape();
@@ -78,6 +78,24 @@ void test_api_wifi_connect_status_success_redirect() {
Serial.println("[TEST] WiFi connect-status success redirect payload test completed successfully");
}
void test_profile_portal_success_keeps_handoff_alive() {
Serial.println("[TEST] Testing profile portal success hand-off delay...");
WiFiManager wm;
#ifdef UNIT_TEST
wm.wmTestCompleteProfilePortalConnectionSuccess();
TEST_ASSERT_EQUAL(WiFiManager::WM_CP_CONNECT_SUCCESS, wm.getConfigPortalConnectState());
TEST_ASSERT_TRUE_MESSAGE(
wm.isConfigPortalConnectPending(),
"Profile-backed portal success must remain pending while the client reads the redirect response"
);
#else
TEST_IGNORE_MESSAGE("UNIT_TEST helpers unavailable");
#endif
Serial.println("[TEST] Profile portal success hand-off delay test completed successfully");
}
void test_api_info_json_shape() {
Serial.println("[TEST] Testing /api/info JSON shape...");
@@ -82,13 +82,13 @@ void test_portal_default_presentation() {
const String bootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"title\":\"WiFiManager\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"homeIntro\":\"\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"tagline\":\"\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoSvg\":\"\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoAltText\":\"\"")));
}
void test_bootstrap_json_contract_v2() {
Serial.println("[TEST] Testing bootstrap JSON v2 contract...");
void test_bootstrap_json_contract_v3() {
Serial.println("[TEST] Testing bootstrap JSON v3 contract...");
WiFiManager wm;
WiFiManagerHandlers handlers(&wm);
@@ -96,7 +96,7 @@ void test_bootstrap_json_contract_v2() {
WiFiManagerPortalConfig portal;
portal.title = WiFiManagerPortalText::ram("Solar Battery Monitor Setup");
portal.identityText = WiFiManagerPortalText::ram("Solar Battery Monitor");
portal.homeIntro = WiFiManagerPortalText::ram("Connect this device to WiFi and finish setup.");
portal.tagline = WiFiManagerPortalText::ram("Connect this device to WiFi and finish setup.");
portal.logo = WiFiManagerPortalAsset::svgFromRam("<svg viewBox='0 0 24 24'></svg>");
TEST_ASSERT_TRUE(wm.setPortalConfig(portal));
wm.portalSetPageInfoVisible(true);
@@ -115,10 +115,10 @@ void test_bootstrap_json_contract_v2() {
wm.portalAddHomeCard(card);
String j = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"contractVersion\":2")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"contractVersion\":3")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"brand\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"title\":\"Solar Battery Monitor Setup\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"homeIntro\":\"Connect this device to WiFi and finish setup.\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"tagline\":\"Connect this device to WiFi and finish setup.\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"logoSvg\":\"<svg viewBox='0 0 24 24'></svg>\"")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"context\":{")));
TEST_ASSERT_NOT_EQUAL(-1, j.indexOf(F("\"identityText\":\"Solar Battery Monitor\"")));
@@ -238,10 +238,10 @@ void test_portal_presentation_configuration() {
WiFiManagerHandlers handlers(&wm);
WiFiManagerPortalConfig config;
config.title = WiFiManagerPortalText::ram("Set up Temperature Monitor");
config.identityText = WiFiManagerPortalText::ram("Tree");
config.homeIntro = WiFiManagerPortalText::ram("Connect this device to WiFi.");
config.identityText = WiFiManagerPortalText::ram("Example Devices");
config.tagline = WiFiManagerPortalText::ram("Connect this device to WiFi.");
config.logo = WiFiManagerPortalAsset::svgFromRam("<svg viewBox='0 0 24 24'></svg>");
config.logoAltText = WiFiManagerPortalText::ram("Tree logo");
config.logoAltText = WiFiManagerPortalText::ram("Example Devices logo");
config.theme.pageBackground = WiFiManagerPortalText::ram("#f4f7f3");
config.theme.surface = WiFiManagerPortalText::ram("#ffffff");
config.theme.text = WiFiManagerPortalText::ram("#1c251e");
@@ -261,8 +261,8 @@ void test_portal_presentation_configuration() {
String bootstrap = handlers.buildPortalBootstrapJson();
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"title\":\"Set up Temperature Monitor\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"identityText\":\"Tree\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoAltText\":\"Tree logo\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"identityText\":\"Example Devices\"")));
TEST_ASSERT_NOT_EQUAL(-1, bootstrap.indexOf(F("\"logoAltText\":\"Example Devices logo\"")));
WiFiManagerPortalConfig unsafeConfig = config;
unsafeConfig.theme.accent = WiFiManagerPortalText::ram("#347a45;body{display:none}");