mirror of
https://github.com/alexhopeoconnor/WiFiManager.git
synced 2026-10-04 02:48:13 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fec33b952 | ||
|
|
f8b87f7865 | ||
|
|
e192c5a94c |
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# 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.
|
||||
| Branded ESP32 portal | ESP8266 portal with an application field |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Try it
|
||||
These are unmodified browser captures of [Branded Portal](examples/BrandedPortal/) and [Custom Portal Content](examples/CustomPortalContent/) running on the supported boards. The portal is served by the device itself; no cloud UI or companion app is involved.
|
||||
|
||||
## Start with a working portal
|
||||
|
||||
```cpp
|
||||
#include <Arduino.h>
|
||||
@@ -20,21 +21,21 @@ 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 = {
|
||||
@@ -53,38 +54,41 @@ void setup() {
|
||||
wifi.setPortalConfig(kPortalUI); // 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.0
|
||||
```
|
||||
|
||||
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
@@ -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
|
||||
|
||||
@@ -27,15 +27,11 @@ void loop() {
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.1.0
|
||||
WiFiManager=https://github.com/alexhopeoconnor/WiFiManager.git#v3.2.0
|
||||
```
|
||||
|
||||
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
@@ -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
|
||||
|
||||
|
||||
+7
-8
@@ -14,9 +14,9 @@ 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";
|
||||
@@ -26,7 +26,7 @@ const char kAccentText[] PROGMEM = "#ffffff";
|
||||
const WiFiManagerPortalConfig kPortalUI = {
|
||||
WiFiManagerPortalText::progmem(kTitle),
|
||||
WiFiManagerPortalText::progmem(kIdentity),
|
||||
WiFiManagerPortalText::progmem(kIntro),
|
||||
WiFiManagerPortalText::progmem(kTagline),
|
||||
WiFiManagerPortalAsset::svgFromProgmem(kLogo),
|
||||
WiFiManagerPortalText::progmem(kLogoAlt),
|
||||
{
|
||||
@@ -58,9 +58,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 +75,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
@@ -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,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
@@ -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 |
@@ -0,0 +1,15 @@
|
||||
#include <Arduino.h>
|
||||
#include <WiFiManager.h>
|
||||
|
||||
WiFiManager portal;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
portal.setConfigPortalTimeout(180);
|
||||
|
||||
portal.autoConnect("WiFiManager Basic", "example-pass");
|
||||
|
||||
Serial.println("Connected. Run your normal application here.");
|
||||
}
|
||||
|
||||
void loop() { portal.process(); }
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -5,10 +5,10 @@ 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";
|
||||
@@ -18,8 +18,8 @@ const char kAccentText[] PROGMEM = "#ffffff";
|
||||
const WiFiManagerPortalConfig kPortalUI = {
|
||||
WiFiManagerPortalText::progmem(kPortalTitle),
|
||||
WiFiManagerPortalText::progmem(kPortalIdentity),
|
||||
WiFiManagerPortalText::progmem(kPortalIntro),
|
||||
WiFiManagerPortalAsset::svgFromProgmem(kTreeLogo),
|
||||
WiFiManagerPortalText::progmem(kPortalTagline),
|
||||
WiFiManagerPortalAsset::svgFromProgmem(kExampleLogo),
|
||||
WiFiManagerPortalText::progmem(kPortalLogoAlt),
|
||||
{
|
||||
WiFiManagerPortalText::progmem(kPage),
|
||||
@@ -42,9 +42,7 @@ void setup() {
|
||||
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(); }
|
||||
|
||||
@@ -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).
|
||||
@@ -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(); }
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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";
|
||||
|
||||
@@ -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)}"
|
||||
|
||||
@@ -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){}
|
||||
}
|
||||
|
||||
@@ -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 == '-';
|
||||
@@ -505,6 +509,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 +530,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 +1205,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 +1306,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 +2662,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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "WiFiManager",
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.0",
|
||||
"keywords": [
|
||||
"wifi",
|
||||
"wi-fi",
|
||||
|
||||
Executable
+46
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}");
|
||||
|
||||
Reference in New Issue
Block a user